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