Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(agents): support parallel tool calls and improve recovery prompt#945

Closed
theonlychant wants to merge 4 commits into
amd:mainfrom
theonlychant:fix/parallel-tool-calls
Closed

fix(agents): support parallel tool calls and improve recovery prompt#945
theonlychant wants to merge 4 commits into
amd:mainfrom
theonlychant:fix/parallel-tool-calls

Conversation

@theonlychant

Copy link
Copy Markdown
Contributor

Summary

Adds support for parallel tool_calls in Agent._parse_response using
a per-instance queue (Option A from #944), and makes the recovery prompt
context-aware for parallel-call failures vs malformed argument failures.

Why

Tool-calling models like Gemma-4-E4B-it-GGUF (GAIA's default since #865)
routinely emit multiple tool_calls in a single response for multi-intent
inputs. The previous code raised NotImplementedError on any response with
more than one tool call, causing all tools to fail silently after three
retries. This fix dispatches parallel calls sequentially without requiring
an extra LLM round-trip per call.

Linked issue

Closes #944

Changes

  • _parse_response now queues extra tool calls in _pending_tool_calls
    instead of raising NotImplementedError
  • process_query drains the queue before re-prompting the LLM
  • Recovery prompt is now context-aware - parallel-call failures get a
    different message than malformed argument failures
  • Added unit tests covering: two parallel calls, three parallel calls with
    one error, and a parallel-then-conversational turn

Test plan

  • pytest tests/unit/ - all passing locally
  • python util/lint.py --all - no failures
  • Manual: submit multi-intent utterance with Gemma-4-E4B and verify
    all tool calls fire

Checklist

  • I have linked a GitHub issue above (Closes #944).
  • I have described why this change is being made, not just what changed.
  • I have run linting and tests locally.
  • I have updated documentation if user-visible behavior changed.

@kovtcharov

Copy link
Copy Markdown
Collaborator

@claude review this PR.

@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

PR review

Thanks for tackling #944 — the underlying problem (silent failures on multi-tool_calls responses from Gemma-4-E4B and similar) is real. Here's what I'd want addressed before merge.

🟡 Code/description mismatch

The PR description says the fix uses "a per-instance queue (_pending_tool_calls)" and that "process_query drains the queue before re-prompting the LLM." The actual code does neither — there's no _pending_tool_calls attribute (grep returns no matches), and _parse_response returns a list that's iterated inline within a single iteration of the process_query loop. Either rewrite the description to match the implementation, or rework the implementation to match Option A from #944. As-is a reviewer reading the description and then the diff will be confused, and future readers will trust the description.

🟡 Large duplicated execution block

src/gaia/agents/base/agent.py:2602-2692 is ~85 lines of logic that closely mirrors the existing single-tool-call execution path (compare to src/gaia/agents/base/agent.py:1965-2020 and the path immediately below the new block). Tool-call history tracking, repeat detection, large-result handling, error formatting, console printing — all duplicated. If the single-call shape were normalized to "always a list of one" the new sequential loop could replace the existing single-call path, and the rest of process_query becomes simpler. At minimum, extract a _execute_parsed_tool_call(call, …) helper so both paths call the same code — otherwise the next bug fix has to be made in two places (and it'll get made in one).

🟡 Dead NotImplementedError recovery branch

src/gaia/agents/base/agent.py:2564-2581 adds a recovery prompt keyed on isinstance(parse_exc, NotImplementedError). But after this PR, _parse_response no longer raises NotImplementedError for parallel tool calls — it returns a list. So this branch is unreachable in production; it only fires in the unit test that monkeypatches _parse_llm_response to raise. Either drop the branch entirely, or keep one path: native parallel calls succeed (new behaviour), parsing failures of any other shape go through the existing malformed-args message. The current "context-aware recovery" framing in the PR description is misleading because the parallel-call path never reaches recovery anymore.

🟡 Conversation entry shape regression

src/gaia/agents/base/agent.py:2604 appends {"role": "assistant", "content": {"tool_calls": parsed}}content is a dict. Existing assistant entries in conversation are strings or parsed dicts with the planning shape (thought/goal/tool/tool_args). Anything that JSON-serialises conversation history or renders it for UI/SSE (src/gaia/ui/) likely assumes one of those two shapes. Worth checking the renderers and persistence layer — a quick grep for m["content"] / entry["content"] against role == "assistant" will tell you whether this is safe.

🟡 Unresolved decision left in code

src/gaia/agents/base/agent.py:2688:

# Continue processing remaining calls (or break?) — prefer to continue

A comment phrased as a question is a "this isn't bulletproof yet" signal. Per the CLAUDE.md "Commit Only When Bulletproof" rule, decide and document the why — e.g. "Continue on error so we still surface the partial results to the LLM; the model can decide whether to retry the failed call." Then drop the parenthetical.

🟡 Tests pollute the global tool registry

Both new test files inject directly into _TOOL_REGISTRY and never clean up:

  • tests/unit/test_agent_parallel_tool_calls.py:36-45
  • tests/unit/test_agent_parallel_tool_calls_extra.py:28-30, 66

_TOOL_REGISTRY is a module-level singleton (src/gaia/agents/base/tools.py:16). Names like q, ok1, errtool, tool_one, tool_two will leak into every test that runs after these in the same pytest session — and q in particular is generic enough to collide with real tools or other tests. Wrap the inserts in a pytest fixture that snapshots/restores the registry, or use monkeypatch.setitem(_TOOL_REGISTRY, ...) (which pytest cleans up automatically).

🟢 Minor

  • tests/unit/test_agent_parallel_tool_calls_extra.py is missing a trailing newline (\ No newline at end of file in the diff) — python util/lint.py --all should flag this.
  • import pytest in both test files is unused at the top level (the monkeypatch fixture works via parameter injection). Remove or use it.
  • Recovery prompt's example values 'brief', 'detailed', 'bullets' (agent.py:2589) are summarize-tool-specific. Generic text in a base-class recovery prompt biases the model toward the wrong vocabulary for, say, code_search or jira_jql. Consider dropping the examples or making them tool-agnostic ("use the documented enum values").

Test plan additions to consider

Summary

The core idea is right and the new path does what's needed for native multi-tool_calls envelopes. Blockers: align description with code, kill the dead NotImplementedError branch (or restore the queue-based design the description claims), de-duplicate the execution loop, and fix the _TOOL_REGISTRY leak in tests. After those, this looks good to me.

@kovtcharov-amd — flagging for your call on whether the conversation-entry shape change at agent.py:2604 is safe given the UI renderer and persistence layer; that's the one thing I can't fully assess from the diff alone.

@theonlychant

Copy link
Copy Markdown
Contributor Author

@claude review this PR

@theonlychant

Copy link
Copy Markdown
Contributor Author

I fixed the unit-tests and lint errors here you can tell me if you like the changes here or I need to close this PR @kovtcharov

@itomek-amd

Copy link
Copy Markdown
Collaborator

@theonlychant — thanks for jumping on #944 quickly; the underlying problem was real and you got a working fix into a PR within hours, which is genuinely useful.

To answer your question on #946: yes, closing this one is the right call. #946 is the more comprehensive landing and is already approved, so duplicate review attention here just adds noise. A few things #946 covers that this PR doesn't, for context (not asks):

Please go ahead and close this one. Thanks again for taking a swing — the bug report on #944 itself was thorough enough to make both PRs possible in the same window.

@itomek-amd itomek-amd closed this May 4, 2026
@theonlychant theonlychant deleted the fix/parallel-tool-calls branch May 5, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent loop rejects parallel tool_calls from native tool-calling models (Gemma-4-E4B default)

3 participants