Skip to content

AUDIT-05 — High — Gemini parallel function calls are broken #62

Closed

Description

@getdownandcode

Summary

Severity: High
Area: Gemini adapter
Affected files: agentkit/llm/gemini.py:104-118, :172-184
Suggested branch: fix/gemini-parallel-function-calls
Suggested PR title: fix(gemini): group parallel function responses into one turn

This is finding AUDIT-05 of 18 from a consolidated audit of agentkit/. It is deliberately self-contained so it can be fixed on its own branch with its own PR.

Related findings: Related: AUDIT-04 (signature persistence), AUDIT-06 (thought parts).


AUDIT-05 — High — Gemini parallel function calls are broken

File — agentkit/llm/gemini.py:104-118 and :172-184

Current code — agentkit/llm/gemini.py:104-118

            elif msg.role == Role.TOOL:
                tool_name = (
                    tool_id_to_name.get(msg.tool_call_id or "") or msg.tool_call_id or "tool"
                )
                contents.append(
                    types.Content(
                        role="user",
                        parts=[
                            types.Part.from_function_response(
                                name=tool_name,
                                response={"result": msg.content},
                            )
                        ],
                    )
                )

Current code — agentkit/llm/gemini.py:172-184

                        if getattr(part, "function_call", None):
                            fc = part.function_call
                            raw_sig = getattr(part, "thought_signature", None)
                            sig = raw_sig if isinstance(raw_sig, bytes) else None
                            tool_calls.append(
                                ToolCall(
                                    # Full UUID: tool_call_id is the join key between an
                                    # assistant tool call and its tool result, so a truncated
                                    # 32-bit id risks collisions within a long session.
                                    id=str(uuid.uuid4()),
                                    name=fc.name,
                                    arguments=dict(fc.args or {}),
                                    thought_signature=sig,
                                )
                            )

Why this is a bug

  1. Every tool result becomes its own Content(role="user") with a single function_response part. Gemini validates the call turn: "Please ensure that the number of function response parts is equal to the number of function call parts of the function call turn" → parallel calls produce 400 INVALID_ARGUMENT.
  2. FunctionResponse.id is never set, and the provider's own FunctionCall.id (present in google-genai 2.25: FunctionCall.id, FunctionResponse.id) is discarded in favour of a fresh UUID. Two parallel calls to the same tool therefore cannot be correlated to their results.
  3. The provider id is thrown away, so any future provider-side correlation is impossible.

Reproduced

provider parts: [function_call(name="calculator", id="fc1"), function_call(name="calculator", id="fc2")]
adapter out   : ToolCall(id=<uuid4>), ToolCall(id=<uuid4>)          # 'fc1'/'fc2' discarded
outbound shape: [Content(role=user, 1 part), Content(role=user, 1 part)]  # 2 separate user contents
function_response.id = None

Pass cases

  • All tool results for one assistant turn are sent in one Content containing one function_response part per call.
  • FunctionResponse.id is set to the id of the FunctionCall it answers, and FunctionCall.id (the provider's) is preserved end-to-end (not replaced by a random UUID).
  • A test with two parallel calls to the same tool asserts the responses are emitted in call order and carry the matching ids.
  • A test asserts the outbound request contains exactly as many function_response parts as the assistant turn had function_call parts.
  • Single-tool-call turns keep working unchanged (no regression), and the existing thought-signature forwarding test still passes.


Definition of done (per AGENTS.md)

  • New/updated unit and integration tests covering the pass cases above, all passing (pytest).
  • ruff check ., ruff format --check . and mypy agentkit clean.
  • Docstrings updated where behaviour changed; docs/PROGRESS.md updated (status, PR link, commit count, notes).
  • A new ADR in docs/DECISIONS.md if an architectural decision was made.
  • Branch pushed and PR opened (never commit directly to main).

Baseline at the time of writing: pytest is green on main (all tests pass; tests/real/* skipped unless AGENTKIT_REAL_INFRA=1).

Activity

  1. getdownandcode commented on Oct 20, 2026

    @getdownandcode
    OwnerAuthor

    Audit index (18 separate issues, fix one by one)

    ID Issue Severity
    AUDIT-01 #58 Critical
    AUDIT-02 #59 Critical
    AUDIT-03 #60 High
    AUDIT-04 #61 High
    AUDIT-05 #62 High
    AUDIT-06 #63 High/Medium
    AUDIT-07 #64 Medium-High
    AUDIT-08 #65 Medium-High
    AUDIT-09 #66 Medium-High
    AUDIT-10 #67 Medium-High
    AUDIT-11 #68 Medium
    AUDIT-12 #69 Medium
    AUDIT-13 #70 Medium
    AUDIT-14 #71 Medium
    AUDIT-15 #72 Medium
    AUDIT-16 #73 Medium
    AUDIT-17 #74 Low
    AUDIT-18 #75 Low/Deployment

    Each issue is self-contained: current code, reproduction, pass cases, suggested branch and PR title. Reference the relevant one with Fixes #NN in your PR.

  2. HamzaKhanBUIC commented on Oct 20, 2026

    @HamzaKhanBUIC

    Opened PR #79 with the complete fix and regression test suite:

    • In �gentkit/llm/gemini.py, grouped consecutive Role.TOOL function responses following an assistant turn into a single ypes.Content(role=user) container as required by Gemini's parallel function call specification.
    • Preserved provider FunctionCall.id when present with UUID fallback, and explicitly set outbound ool_part.function_response.id = msg.tool_call_id.
    • Added regression test est_gemini_parallel_function_calls_grouped_and_preserve_ids in ests/unit/test_gemini_adapter.py.
    • Verified clean across pytest,
      uff check,
      uff format --check, and mypy agentkit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions