Repository navigation
AUDIT-05 — High — Gemini parallel function calls are broken #62
Description
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
- Every tool result becomes its own
Content(role="user")with a singlefunction_responsepart. 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 produce400 INVALID_ARGUMENT. FunctionResponse.idis never set, and the provider's ownFunctionCall.id(present ingoogle-genai2.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.- 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
Contentcontaining onefunction_responsepart per call. -
FunctionResponse.idis set to the id of theFunctionCallit answers, andFunctionCall.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_responseparts as the assistant turn hadfunction_callparts. - 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 .andmypy agentkitclean. - Docstrings updated where behaviour changed;
docs/PROGRESS.mdupdated (status, PR link, commit count, notes). - A new ADR in
docs/DECISIONS.mdif 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
getdownandcode commentedon Oct 20, 2026
Audit index (18 separate issues, fix one by one)
Each issue is self-contained: current code, reproduction, pass cases, suggested branch and PR title. Reference the relevant one with
Fixes #NNin your PR.HamzaKhanBUIC commentedon Oct 20, 2026
Opened PR #79 with the complete fix and regression test suite:
uff check,
uff format --check, and mypy agentkit.