Affected package: ag-ui-agent-spec (the ag_ui_agentspec adapter, ag-ui-protocol/ag-ui → integrations/agent-spec/python), langgraph runtime.
Stack: pyagentspec 26.2.0.dev6, langgraph runtime, OpenAI via langchain-openai; consumed by a CopilotKit V2 frontend over AG-UI. Python 3.12.
Severity: High — any conversation that uses a server-side tool fails on the next user turn. Blocks multi-turn agents and the human-in-the-loop (confirm-then-act) pattern.
When an Agent Spec agent with ServerTools runs on the LangGraph runtime behind add_agentspec_fastapi_endpoint, the first turn works. The tool calls emit a warning:
AG-UI tool-call correlation miss: no ToolExecutionRequest recorded for request_id='call_…';
using the raw request_id as a surrogate tool_call_id. The emitted tool result may be
orphaned because the frontend never saw this id.
On the second turn (any follow-up after a turn that called a tool), the LangGraph model node fails:
openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid parameter:
messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.",
'type': 'invalid_request_error', 'param': 'messages.[N].role'}}
Server-side tool calls are not recorded as ToolExecutionRequests, so the adapter emits the tool result with a surrogate tool_call_id (the raw request_id) that the frontend never associated with an assistant tool_calls entry. The conversation history that the frontend then replays on the next turn therefore contains a role: "tool" message with no preceding assistant message carrying the matching tool_calls. OpenAI rejects that message sequence (400). The first-turn correlation miss warning and the second-turn 400 are the same defect observed at two points.
Agent with a ServerTool (e.g. recall_memory(query)), serialize, and serve it:
add_agentspec_fastapi_endpoint(app, AgentSpecAgent(agent_json, runtime="langgraph", tool_registry={...}), path="/run").correlation miss warning.Observed in the Oracle × CopilotKit cookbook: turn 1 (recall + search via search_trips) works and is correctly personalized; replying “confirm” (turn 2) fails with the 400, so the book_trip (requires_confirmation) HITL flow can never be reached.
requires_confirmation flow (propose → user confirms → execute) is unreachable, because confirmation is inherently a second turn.Record a ToolExecutionRequest for every server-tool invocation so the emitted tool result carries the same tool_call_id the assistant tool_calls entry used (and is visible to the frontend), so the replayed history is a valid assistant(tool_calls) → tool(result) sequence. Alternatively, reconcile tool_call_ids when reconstructing LangGraph message history from the incoming AG-UI messages so orphaned tool messages are repaired or dropped before the model call.
The cookbook now applies a server-side workaround in agent/concierge/server.py. The
LangGraph runner is checkpointed per thread_id and, each turn, tries to append only
the client messages whose ids aren’t already in the checkpoint
(filter_only_new_messages). But CopilotKit re-sends the full history with ids
that never match the checkpoint’s, so a second copy of the
assistant(tool_calls)/tool block is appended and the merged history is invalid.
Since the client already sends the full, valid history every turn, we replace the
adapter’s incremental merge with a full-history replace: monkey-patch
filter_only_new_messages to prepend a RemoveMessage(REMOVE_ALL_MESSAGES) and return
the client’s history verbatim, so add_messages clears the checkpoint’s copy and uses
the client’s valid history. This restores multi-turn conversations and makes the
book_flight (requires_confirmation) HITL flow reachable (search → pick → confirm →
boarding pass all work). The adapter drives every turn — including HITL resume — through
astream({"messages": ...}), so the replace covers that path too.
The full-history replace handles the duplicate/orphan direction above, but a second
failure mode is its inverse. book_flight is a client-side HITL tool: calling it
interrupts the run and emits an assistant message with a tool_call, then waits for the
UI to return a result when the traveler clicks Confirm & book / Cancel. If the
traveler instead sends another chat message, that tool_call is never answered, so the
replayed history carries an assistant(tool_calls) with no following tool result →
OpenAI 400:
An assistant message with 'tool_calls' must be followed by tool messages responding to
each 'tool_call_id'. The following tool_call_ids did not have response messages: call_…
(param: messages.[N].role)
_repair_dangling_tool_calls (server.py) fixes this: before handing the client’s history
to the graph, for any assistant tool_call with no following tool result it inserts a
synthetic “not completed” tool result right after the assistant message, so the sequence
is valid and the model answers the new question gracefully. Reproduced + verified in-browser
(book conversationally → Confirm card → ask something else → previously 400, now answers).
Only the conversational booking path triggers it; the flight-card “Select this flight” path
books client-side with no agent HITL.
This is a workaround, not a fix: it lives in cookbook code and reaches into a private
adapter function. Remove it once the upstream adapter records ToolExecutionRequests so
the emitted tool-call ids correlate (the “Suggested direction” above). Pin to a fixed
adapter commit and re-test as the integration matures.
ag-ui-agent-spec installed from git+https://github.com/ag-ui-protocol/ag-ui.git#subdirectory=integrations/agent-spec/python ([langgraph] extra)pyagentspec 26.2.0.dev6, langgraph runtime, langchain-openai, Python 3.120.0.0-mme-ag-ui-0-0-46-…), @ag-ui/client ^0.0.46OpenAiCompatibleConfig (key from env)