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
- 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).
- 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."
- 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.
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
OllamaChatClientassigns every parsed tool call acall_idequal 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 twomicrosoft_docs_searchcalls. Both calls then share onecall_id, so every downstream layer that correlates bycall_idtreats them as a single call:agent-framework-ag-ui) folds the second call's arguments into the firsttool_call_id(oneTOOL_CALL_START, twoTOOL_CALL_ARGSdeltas), producing a concatenated, invalid-JSON arguments string.confirm_changesis created; the second logical call is effectively lost.tool-role result message shares itstool_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
AgentFrameworkAgentand expose it viaadd_agent_framework_fastapi_endpoint(require_confirmation=Trueto see the HITL effect).Observed events — two distinct queries arrive under one
tool_call_id:Expected
Each tool call in a turn carries a unique
call_id, producing oneTOOL_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, thecall_idis set to the function name:The name is also reused as the correlation key on the return trip:
_format_tool_messageserializes a tool result withtool_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_messageeven after thecall_idis made unique.Impact
require_confirmation) never resolves the affected tool in any AG-UI consumer (CopilotKit, etc.) — a visible, permanent "Running" card.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: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, subclassOllamaChatClientand override_parse_tool_calls_from_ollamato stamp a unique, stablecall_idper call, plus_format_tool_messageto strip the suffix back to the bare tool name. Verified: with unique ids, a parallel "search A and B" turn emits two independentTOOL_CALL_STARTlifecycles and two approval cards that each resolve to "Done". Seeats._ollama.StructuredOllamaChatClient._parse_tool_calls_from_ollamaand._format_tool_message.