fix(response): reject malformed response arguments

Raise RepairableException when response lacks a top-level text or message string. The existing recovery path now warns the model to correct the call instead of exposing a KeyError, while preserving legacy message arguments.

Alessandro committed Jul 23, 2026 at 09:05 UTC d3a81ac65bbe9e50bf938a71bafa2e64485a946a
3 files changed +55 -2
tests/test_response_tool_validation.py new
+43
@@ -0,0 +1,43 @@
1 +import sys
2 +from pathlib import Path
3 +
4 +import pytest
5 +
6 +
7 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
8 +if str(PROJECT_ROOT) not in sys.path:
9 + sys.path.insert(0, str(PROJECT_ROOT))
10 +
11 +from helpers.errors import RepairableException
12 +from tools.response import ResponseTool
13 +
14 +
15 +@pytest.mark.asyncio
16 +@pytest.mark.parametrize("args", [{"text": "ok"}, {"message": "ok"}])
17 +async def test_response_tool_accepts_text_or_message(args) -> None:
18 + tool = ResponseTool(None, "response", None, args, "", None)
19 +
20 + response = await tool.execute()
21 +
22 + assert response.message == "ok"
23 + assert response.break_loop is True
24 +
25 +
26 +@pytest.mark.asyncio
27 +async def test_response_tool_rejects_nested_response_args() -> None:
28 + tool = ResponseTool(
29 + None,
30 + "response",
31 + None,
32 + {
33 + "thoughts": [],
34 + "headline": "Providing exact payload",
35 + "tool_name": "response",
36 + "tool_args": {"text": "nested"},
37 + },
38 + "",
39 + None,
40 + )
41 +
42 + with pytest.raises(RepairableException, match="top-level text or message"):
43 + await tool.execute()
tools/response.py
+8 -1
@@ -1,10 +1,17 @@
1 +from helpers.errors import RepairableException
2 from helpers.tool import Tool, Response
3
4
5 class ResponseTool(Tool):
6
7 async def execute(self, **kwargs):
7 - return Response(message=self.args["text"] if "text" in self.args else self.args["message"], break_loop=True)
8 + for key in ("text", "message"):
9 + message = self.args.get(key)
10 + if isinstance(message, str):
11 + return Response(message=message, break_loop=True)
12 + raise RepairableException(
13 + "response tool requires a top-level text or message string argument"
14 + )
15
16 async def before_execution(self, **kwargs):
17 # self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding", content=self.args.get("text", ""))
tools/response.py.dox.md
+4 -1
@@ -22,7 +22,10 @@
22 - Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
23 - `ResponseTool` is a `Tool`.
24 - `ResponseTool` defines `execute(...)`.
25 -- Imported dependency areas include: `helpers.tool`.
25 +- `ResponseTool` requires a top-level string `text` or legacy `message` argument.
26 + Invalid arguments raise `RepairableException` so the agent can surface a correction
27 + warning and retry rather than crash.
28 +- Imported dependency areas include: `helpers.errors`, `helpers.tool`.
29
30 ## Key Concepts
31