Fix tool-mediation loop: agents never executed tools#83
Merged
Conversation
Tool-using agents ran one turn, executed zero tools, and returned an empty reply — autonomous tool use was fully broken with every provider. Two causes: 1. Declaration naming. The agents-api conversation loop validates client tool declarations against `^[a-z][a-z0-9_-]*/[a-z][a-z0-9_-]*$` and derives the declaration source from the segment before the slash, which MUST equal `client`. openclaWP handed the loop provider-sanitized names (`openclawp__get-recent-posts`, no slash, source `openclawp`), so every declaration was dropped, `mediation_enabled` flipped to false, and tools never executed. Add OpenclaWP_Tools_Resolver::loop_tools() — the loop-only variant that re-keys declarations + the executor maps under `client/<name>` (source `client`); the runner maps the model's returned tool-call names back through loop_name(). for_agent() stays unprefixed so the MCP tool surface and the provider-facing function names are unchanged. 2. Message round-trip. to_ai_client_messages() dropped tool_call messages and passed tool_result content as plain user text, so the model never saw its own call or the result in the protocol shape — it re-issued the same call every turn until max_turns, never synthesizing an answer. Convert tool_call -> model message with a FunctionCall part and tool_result -> user message with a FunctionResponse part (provider name, client/ prefix stripped, matched by tool_call_id). Also lowercase sanitize_name() output so the client/<name> form satisfies the loop's lowercase-only name pattern. Verified end-to-end on two local Studio sites (Anthropic claude-haiku): an orchestrator agent autonomously calls a cross-site A2A tool, the peer agent autonomously calls get-recent-posts, and real data flows back — single tool-call cycle, no re-calling. Tests: tests/unit/ToolsResolverTest.php guards the client/ naming contract; tests/smoke.php asserts the tool_call/tool_result -> FunctionCall/FunctionResponse round-trip in a real WP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Tool-using agents ran one turn, executed zero tools, and returned an empty reply — autonomous tool use was fully broken with every provider (Ollama and Anthropic). The model emitted a tool call (
tool_call_count:1) but the loop completed withtool_results:0.Root causes (two)
1. Declaration naming. The agents-api conversation loop validates client tool declarations against
^[a-z][a-z0-9_-]*/[a-z][a-z0-9_-]*$and derives the declaration source from the segment before the slash, which must equalclient(WP_Agent_Tool_Declaration::validate()). openclaWP handed the loop the provider-sanitized name (openclawp__get-recent-posts— no slash, sourceopenclawp), so every declaration was dropped →mediation_enabledbecame false → tools never executed.2. Message round-trip.
to_ai_client_messages()droppedtool_callmessages and passedtool_resultcontent as plain user text. The model never saw its own call/result in the protocol shape, so it re-issued the same call every turn untilmax_turns, never answering.The fix
OpenclaWP_Tools_Resolver::loop_tools()— the loop-only variant that re-keys declarations + executor maps underclient/<name>(sourceclient). The runner uses it;for_agent()stays unprefixed so the MCP tool surface and provider function names are unchanged (verified).loop_name().to_ai_client_messages()now convertstool_call→ model message with a FunctionCall part andtool_result→ user message with a FunctionResponse part (provider name,client/stripped, matched bytool_call_id).sanitize_name()lowercases soclient/<name>satisfies the loop's lowercase-only pattern.Verification
End-to-end on two local Studio sites (Anthropic
claude-haiku-4-5): an orchestrator agent autonomously calls a cross-site A2A tool → the peer agent autonomously callsget-recent-posts→ real data flows back, in a single tool-call cycle.tests/unit/ToolsResolverTest.php— guards theclient/naming contract against the exact agents-api regex (14 assertions).tests/smoke.php— asserts thetool_call/tool_result→FunctionCall/FunctionResponseround-trip in real WP.for_agent()/MCP translator names stayopenclawp__…(noclient/leak).🤖 Generated with Claude Code