Skip to content

Python: [Bug]: OllamaChatClient: tool calls reuse the function name as call_id, so repeated calls in one turn collide #6788

Description

@antsok

Description

OllamaChatClient: tool calls reuse the function name as call_id, so repeated calls in one turn collide (parallel tool calls break; HITL approval cards never resolve)

Summary

OllamaChatClient assigns every parsed tool call a call_id equal to the function name (Ollama's API returns no per-call id). This is fine when each tool is invoked once per turn, but it collides whenever a model issues the same tool more than once in a single assistant message — e.g. a parallel "search A and B" that emits two microsoft_docs_search calls. Both calls then share one call_id, so every downstream layer that correlates by call_id treats them as a single call:

  • AG-UI (agent-framework-ag-ui) folds the second call's arguments into the first tool_call_id (one TOOL_CALL_START, two TOOL_CALL_ARGS deltas), producing a concatenated, invalid-JSON arguments string.
  • Only one approval / confirm_changes is created; the second logical call is effectively lost.
  • A CopilotKit/AG-UI frontend marks a tool "complete" only when a tool-role result message shares its tool_call_id, so the surviving card stays stuck "Running" after approval.

Code Sample

Error Messages / Stack Traces

Package Versions

agent-framework-core: 1.9.0, agent-framework-ag-ui:1.0.0rc5, agent-framework-ollama:1.0.0b260521, ollama: 0.5.3

Python Version

Python 3.12

Additional Context

Reproduction

  1. Wrap a harness agent backed by an Ollama model in AgentFrameworkAgent and expose it via add_agent_framework_fastapi_endpoint (require_confirmation=True to see the HITL effect).
  2. Prompt the model to call the same tool twice in one turn, e.g. "Search Microsoft Learn for (1) Azure landing zone management groups and (2) the Well-Architected reliability pillar — issue both searches now."
  3. Observe the AG-UI event stream.

Observed events — two distinct queries arrive under one tool_call_id:

TOOL_CALL_START  tool_call_id=microsoft_docs_search  name=microsoft_docs_search
TOOL_CALL_ARGS   tool_call_id=microsoft_docs_search  {"query":"Azure landing zone management groups…"}
TOOL_CALL_ARGS   tool_call_id=microsoft_docs_search  {"query":"Azure Well-Architected reliability pillar…"}   # same id
TOOL_CALL_END    tool_call_id=microsoft_docs_search
# …a single confirm_changes for one call; the second is gone

Expected

Each tool call in a turn carries a unique call_id, producing one TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END (and one approval, and one result) per call — so N parallel calls render as N independently-resolving tool chips, matching providers (OpenAI, Foundry) that supply real per-call ids.

Actual

All same-named calls in a turn share one call_id. AG-UI merges them into one tool call with concatenated arguments; only the first is approved/executed; the frontend card never resolves (stuck "Running"); the remaining calls are silently dropped.

Root cause

In agent_framework_ollama._chat_client.OllamaChatClient._parse_tool_calls_from_ollama, the call_id is set to the function name:

def _parse_tool_calls_from_ollama(self, tool_calls):
    contents = []
    for tool in tool_calls:
        fcc = Content.from_function_call(
            call_id=tool.function.name,   # <-- not unique across repeated calls
            name=tool.function.name,
            arguments=tool.function.arguments if isinstance(tool.function.arguments, dict) else "",
            raw_representation=tool.function,
        )
        ...

The name is also reused as the correlation key on the return trip: _format_tool_message serializes a tool result with tool_name=item.call_id. So the name-as-id is load-bearing for Ollama's history round-trip, which is why a fix must keep the real function name reaching _format_tool_message even after the call_id is made unique.

Impact

  • Parallel/repeated tool calls are broken for every Ollama model: the second and later same-named calls are dropped, and their arguments corrupt the first call's payload (invalid JSON).
  • HITL approval (require_confirmation) never resolves the affected tool in any AG-UI consumer (CopilotKit, etc.) — a visible, permanent "Running" card.
  • Silent: the model "thinks" it issued multiple searches, but only one runs, degrading answer quality with no error.

Suggested fix

Assign each parsed tool call a unique, stable call_id (Ollama delivers a turn's tool calls complete in one chunk, so a content-derived id is stable across stream re-parses), while preserving the function name for the result round-trip. For example, derive the id from name + position + a short hash of the arguments, and map it back to the bare name in _format_tool_message:

# _parse_tool_calls_from_ollama
call_id = f"{name}:{index}:{blake2s(args_json).hexdigest()[:8]}"

# _format_tool_message — restore the real tool name Ollama needs to correlate
tool_name = call_id.rsplit(":", 2)[0]

Add a regression test asserting that two same-named calls in one response yield two distinct call_ids and that the tool name still round-trips intact.

Workaround

In agent-framework-ollama, subclass OllamaChatClient and override _parse_tool_calls_from_ollama to stamp a unique, stable call_id per call, plus _format_tool_message to strip the suffix back to the bare tool name. Verified: with unique ids, a parallel "search A and B" turn emits two independent TOOL_CALL_START lifecycles and two approval cards that each resolve to "Done". See ats._ollama.StructuredOllamaChatClient._parse_tool_calls_from_ollama and ._format_tool_message.

Metadata

Metadata

Assignees

Labels

pythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

No fields configured for Bug.

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions