From 6d88dc1fe5339c378cf85172aa2b904a01973c63 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:06:33 +0530 Subject: [PATCH] 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 --- run_agent.py | 2 +- tests/agent/test_run_agent.py | 5 ++--- tools/todo_tool.py | 9 +++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/run_agent.py b/run_agent.py index 78f8fdb20d..bf863c5871 100644 --- a/run_agent.py +++ b/run_agent.py @@ -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: diff --git a/tests/agent/test_run_agent.py b/tests/agent/test_run_agent.py index e5898ececd..a40eadfabe 100644 --- a/tests/agent/test_run_agent.py +++ b/tests/agent/test_run_agent.py @@ -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 = [ diff --git a/tools/todo_tool.py b/tools/todo_tool.py index ecf1319e94..5deb184570 100644 --- a/tools/todo_tool.py +++ b/tools/todo_tool.py @@ -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