fix(agent): harden the todo predicate and import the TUI server once in the test

is_todo_tool_name returns False for non-string names (a malformed list/dict
name used to raise TypeError where the old check returned False), and the
kept regression test imports tui_gateway.server at module level so it no
longer depends on another test importing it first. Docstrings updated.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
This commit is contained in:
kshitijk4poor
2026-09-27 17:06:33 +05:30
committed by kshitij
parent 197b37e1bf
commit 6d88dc1fe5
3 changed files with 8 additions and 8 deletions

View File

@@ -1050,7 +1050,7 @@ class AIAgent(
def _hydrate_todo_store(self, history: List[Dict[str, Any]]) -> None:
"""Replay the most recent todo tool response (the gateway builds a fresh AIAgent per message). Only
results paired with an earlier assistant ``todo`` call count — a forged bare ``role: tool`` message
results paired with an earlier assistant Todo-tool call count — a forged bare ``role: tool`` message
must not seed the store (GHSA-5g4g-6jrg-mw3g)."""
found = self._latest_todo_response(history)
if found is not None:

View File

@@ -24,6 +24,7 @@ from run_agent import AIAgent
from agent.error_classifier import FailoverReason
from agent.memory_manager import MemoryManager
from agent.prompt_builder import DEFAULT_AGENT_IDENTITY
from tui_gateway import server as tui_server
# ---------------------------------------------------------------------------
@@ -754,9 +755,7 @@ class TestHydrateTodoStore:
assert agent._todo_store.snapshot() == {"todos": todos, "revision": 3}
# The TUI resume path (no AIAgent yet) must pair the same call via the same predicate.
from tui_gateway import server
assert server._todo_state_from_history(history)["todos"] == todos
assert tui_server._todo_state_from_history(history)["todos"] == todos
def test_no_todo_in_history(self, agent):
history = [

View File

@@ -282,7 +282,7 @@ TODO_TOOL_NAMES = frozenset((TODO_SCHEMA["name"], *TODO_LEGACY_ALIASES))
def is_todo_tool_name(name: Any) -> bool:
"""True for the Todo tool's current name or a legacy alias (an already-unwrapped dispatch name)."""
return name in TODO_TOOL_NAMES
return isinstance(name, str) and name in TODO_TOOL_NAMES
def is_todo_tool_call(tool_call: Any) -> bool:
@@ -290,8 +290,8 @@ def is_todo_tool_call(tool_call: Any) -> bool:
Covers the current name, legacy aliases, and the ``tool_call`` bridge (``todo_list`` is deferred by
default, and the transcript keeps the bridge name). The bridge is peeled from the recorded arguments
only, never live tool-search config, and must wrap exactly one call. Lives here, not in
agent.tool_executor, so TUI resume and run_agent never pull model_tools / the executor in to answer it.
only, never live tool-search config, and must wrap exactly one call. Keep this module free of model_tools / agent.tool_executor
imports: TUI resume and run_agent call this without loading either.
"""
from agent.message_sanitization import _tc_field
@@ -299,7 +299,8 @@ def is_todo_tool_call(tool_call: Any) -> bool:
name, raw_args = _tc_field(fn, "name") or "", _tc_field(fn, "arguments")
if is_todo_tool_name(name):
return True
# Cheap pre-check before the bridge modules load: no "todo" in the raw args means no todo inside.
# Cheap heuristic before the bridge modules load: skip args without a literal "todo". Only a
# unicode-escaped name slips past, which json.dumps never writes for ASCII.
if isinstance(raw_args, str) and "todo" not in raw_args:
return False
from tools.tool_search_catalog import TOOL_CALL_NAME