From 4b0feac1f45702fc15f5b0d42838ce75edbfef4e Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:52:11 +0200 Subject: [PATCH] Fix malformed native Responses tool output Keep Agent Zero wrapper examples out of native function descriptions and expose the response text schema.\n\nRoute concatenated tool envelopes through repair before the plain response hook can render them as final text. --- .../end/_10_log_plain_responses.py | 5 ++++- helpers/extract_tools.py | 9 +++++++++ helpers/extract_tools.py.dox.md | 2 +- helpers/responses_tools.py | 2 +- helpers/responses_tools.py.dox.md | 1 + prompts/agent.system.tool.response.md | 4 ++-- tests/test_responses_tools.py | 13 +++++++++++++ tests/test_tool_request_normalization.py | 8 ++++++++ 8 files changed, 39 insertions(+), 5 deletions(-) diff --git a/extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py b/extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py index e11e660f6..28c80e77c 100644 --- a/extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py +++ b/extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py @@ -23,7 +23,10 @@ class LogPlainResponses(Extension): message = call_args[1] if not isinstance(message, str) or not message: return - if extract_tools.extract_tool_request(message) is not None: + if ( + extract_tools.extract_tool_request(message) is not None + or extract_tools.is_misformatted_tool_request(message) + ): return params = getattr(getattr(self.agent, "loop_data", None), "params_temporary", None) diff --git a/helpers/extract_tools.py b/helpers/extract_tools.py index 55a1a67e0..e1996da34 100644 --- a/helpers/extract_tools.py +++ b/helpers/extract_tools.py @@ -38,6 +38,15 @@ def is_misformatted_tool_request(content: str) -> bool: return False content = content.strip() + roots = extract_json_root_strings(content) + if ( + len(roots) > 1 + and content.startswith("{") + and content.endswith("}") + and any(extract_tool_request(root) is not None for root in roots) + ): + return True + for fenced_content in re.findall( r"```(?:json)?\s*(.*?)```", content, flags=re.IGNORECASE | re.DOTALL ): diff --git a/helpers/extract_tools.py.dox.md b/helpers/extract_tools.py.dox.md index adfc19881..d2892cb3b 100644 --- a/helpers/extract_tools.py.dox.md +++ b/helpers/extract_tools.py.dox.md @@ -29,7 +29,7 @@ - Dirty parsing scans complete JSON object roots in prose and prefers the first object that normalizes as a valid tool request for permissive repair and legacy callers. Normalization accepts canonical `tool_name`/`tool_args`, legacy `tool`/`args`, native `type="function"` `name`/`parameters`, and a single-item `actions` wrapper; malformed or multi-action wrappers are rejected. - `extract_tool_request` is the execution boundary: it accepts a request only when the complete trimmed content is one valid tool object. Plain text, ordinary JSON, and tool-shaped JSON embedded in prose remain final text. -- `is_misformatted_tool_request` identifies either a tool request wrapped in a JSON code fence or a complete Agent Zero envelope that starts with `thoughts` and whose dirty parser has absorbed `headline`, `tool_name`, and `tool_args` into that list. It routes that output to the existing repair prompt without executing it. +- `is_misformatted_tool_request` identifies a tool request wrapped in a JSON code fence, concatenated complete roots containing tool intent, or a complete Agent Zero envelope that starts with `thoughts` and whose dirty parser has absorbed `headline`, `tool_name`, and `tool_args` into that list. It routes that output to the existing repair prompt without executing it. - Streaming tool snapshots use `extract_tool_request`; the permissive root helpers remain available for repair and legacy callers, not tool execution. - Root extraction ignores objects nested inside an open parent object, so streamed wrapper tools such as `parallel` cannot stop early on the first nested `tool_calls` item. - Imported dependency areas include: `dirty_json`, `helpers.modules`, `re`, `regex`, `typing`. diff --git a/helpers/responses_tools.py b/helpers/responses_tools.py index 453e7d731..3b7ff725f 100644 --- a/helpers/responses_tools.py +++ b/helpers/responses_tools.py @@ -157,7 +157,7 @@ def _description_from_prompt(prompt: str, *, fallback: str) -> str: in_fence = False for raw_line in (prompt or "").splitlines(): line = raw_line.strip() - if line.startswith("```"): + if line.startswith(("```", "~~~")): in_fence = not in_fence continue if in_fence or not line: diff --git a/helpers/responses_tools.py.dox.md b/helpers/responses_tools.py.dox.md index e7ec0e760..6fd2727d4 100644 --- a/helpers/responses_tools.py.dox.md +++ b/helpers/responses_tools.py.dox.md @@ -15,6 +15,7 @@ - Build local function tools from enabled `agent.system.tool.*.md` prompt files. - Local prompt-derived function names prefer explicit `"tool_name"` examples, then the first prompt heading, and only fall back to the prompt filename when the prompt declares no callable name. - Function parameter schemas are object schemas with an explicit `properties` object so OpenAI-compatible servers that validate chat-style tool payloads accept permissive tools. +- Native tool descriptions omit both backtick- and tilde-fenced usage examples so Agent Zero text envelopes are not presented as function arguments. - Preserve original Agent Zero tool names through the native Responses name map. - Keep MCP tool schemas merged after local prompt-derived tools. - Connector remote tools are advertised only when `_a0_connector` runtime metadata says the matching connected CLI capability is currently available. diff --git a/prompts/agent.system.tool.response.md b/prompts/agent.system.tool.response.md index 195e78c5d..9292882ae 100644 --- a/prompts/agent.system.tool.response.md +++ b/prompts/agent.system.tool.response.md @@ -1,7 +1,7 @@ ### response: final answer to user ends task processing use only when done or no task active -put result in text arg +args: `text` default to balanced, concise answers: informative but tight, not terse and not verbose. usage: ~~~json @@ -17,4 +17,4 @@ usage: } ~~~ -{{ include "agent.system.response_tool_tips.md" }} \ No newline at end of file +{{ include "agent.system.response_tool_tips.md" }} diff --git a/tests/test_responses_tools.py b/tests/test_responses_tools.py index fc187d1f7..9bb3219f0 100644 --- a/tests/test_responses_tools.py +++ b/tests/test_responses_tools.py @@ -146,3 +146,16 @@ def test_responses_function_tools_add_empty_properties_to_mcp_schemas( }, } ] + + +def test_response_tool_native_contract_omits_wrapper_and_exposes_text(): + prompt = (PROJECT_ROOT / "prompts" / "agent.system.tool.response.md").read_text( + encoding="utf-8" + ) + + description = responses_tools._description_from_prompt(prompt, fallback="response") + schema = responses_tools._schema_from_prompt(prompt) + + assert '"tool_name"' not in description + assert "~~~" not in description + assert schema["properties"] == {"text": {"type": "string"}} diff --git a/tests/test_tool_request_normalization.py b/tests/test_tool_request_normalization.py index 41e628496..f982211c5 100644 --- a/tests/test_tool_request_normalization.py +++ b/tests/test_tool_request_normalization.py @@ -131,6 +131,12 @@ def test_extract_tool_request_requires_a_complete_tool_message() -> None: def test_is_misformatted_tool_request_requires_agent_tool_envelope() -> None: request = '{"tool_name":"response","tool_args":{"text":"ok"}}' + concatenated = ( + '{"thoughts":[],"headline":"Inspecting","tool_name":"code_execution_tool",' + '"tool_args":{"code":"pwd"}}' + '{"thoughts":[],"headline":"Answering","tool_name":"response",' + '"tool_args":{"text":"done"}}' + ) malformed = ( '{"thoughts":["Plan the work", "Run the tools", ' '"headline":"Save results", "tool_name":"parallel", ' @@ -140,6 +146,8 @@ def test_is_misformatted_tool_request_requires_agent_tool_envelope() -> None: assert extract_tool_request(malformed) is None assert is_misformatted_tool_request(malformed) is True + assert extract_tool_request(concatenated) is None + assert is_misformatted_tool_request(concatenated) is True assert is_misformatted_tool_request(f"Intro\n```json\n{request}\n```") is True assert is_misformatted_tool_request('{"status":"planning"}') is False assert is_misformatted_tool_request(f"Example: {request}") is False