Skip to content

AG-UI: tool result messages missing from conversation history on subsequent turns (causes LLM 400) #3884

Closed

Description

@nebrass

♻️ Reproduction Steps

  1. Set up a CopilotKit v2 <CopilotChat> with an AG-UI HttpAgent backend that has server-side tools (e.g., MCP tools)
  2. Send a message that triggers a tool call (e.g., "What's the weather in Seattle?")
  3. Agent streams: TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END → TOOL_CALL_RESULT → TEXT_MESSAGE_* → RUN_FINISHED
  4. First turn completes successfully — user sees the tool result and text response
  5. Send a second message (e.g., "What is 5 * 33?")
  6. Backend receives the conversation history and returns 400 Bad Request from the LLM provider

Root Cause

When CopilotKit replays the conversation history on the second turn, it sends the messages array to the AG-UI backend without the tool result message. The history looks like:

user: "What's the weather in Seattle?"
assistant: [tool_calls: [{id: "call_abc", function: {name: "get_weather", ...}}]]
assistant: "The weather in Seattle is 15°C and partly cloudy."
user: "What is 5 * 33?"

The correct history should be:

user: "What's the weather in Seattle?"
assistant: [tool_calls: [{id: "call_abc", function: {name: "get_weather", ...}}]]
tool: {tool_call_id: "call_abc", content: "Seattle: 15°C, partly cloudy"}
assistant: "The weather in Seattle is 15°C and partly cloudy."
user: "What is 5 * 33?"

The tool result message for call_abc is completely absent. This causes both OpenAI and Azure OpenAI to reject the request with a 400 error because tool_calls in the assistant message have no corresponding tool result.

✅ Expected Behavior

CopilotKit should persist TOOL_CALL_RESULT events in its internal conversation state and include the corresponding tool role messages when replaying history to the backend on subsequent turns.

❌ Actual Behavior

Tool result messages are dropped from the conversation state. On the second turn, the backend receives assistant(tool_calls) followed directly by assistant(text) with no tool(result) in between. The LLM provider (OpenAI/Azure OpenAI) rejects this with a 400 error.

Error from Azure OpenAI Responses API:

Code: agent_run_error_event
Message: An internal error has occurred while streaming events.

The underlying cause is a 400 from the LLM because the message history violates the constraint that every tool_calls entry must have a matching tool result message.

Workaround

We subclass AgentFrameworkAgent on the backend to inject synthetic tool result dicts into the raw messages before they enter the library pipeline:

class ToolGapFixAgent(AgentFrameworkAgent):
    async def run(self, input_data):
        messages = input_data.get("messages")
        if messages:
            input_data = {**input_data, "messages": fix_tool_gaps(messages)}
        async for event in super().run(input_data):
            yield event

Where fix_tool_gaps scans for assistant messages with tool_calls not followed by a tool result, and injects synthetic ones.

Environment

@copilotkit/react-core: 1.55.3
@ag-ui/client: 0.0.52
agent-framework-ag-ui: 1.0.0b260311 (Python backend)
LLM provider: Azure OpenAI (Responses API)
Transport: AG-UI HttpAgent via agents__unsafe_dev_only prop

Frontend Setup

const agent = new HttpAgent({ url: "/ag-ui" });

<CopilotKit
  runtimeUrl="/ag-ui"
  agent="my-agent"
  agents__unsafe_dev_only={{ "my-agent": agent }}
  useSingleEndpoint={false}
>
  <CopilotChat />
</CopilotKit>

Related Issues

Activity

  1. copilotkit-support-bot commented on Apr 14, 2026

    @copilotkit-support-bot

    The issue is caused by TOOL_CALL_RESULT events not being persisted into the internal conversation state, leading to missing tool role messages on subsequent turns and a 400 from the LLM provider. The fix is to handle onToolCallResultEvent in the StateManager and add a tool message to the agent's history, or apply a frontend workaround that injects the missing messages.

    Details

    To address the missing TOOL_CALL_RESULT messages in the conversation history when using AG-UI with CopilotKit v2, it appears the StateManager in @copilotkit/core is missing a handler for the TOOL_CALL_RESULT event. While other message types trigger an onNewMessage notification (which persists them to the internal conversation state), TOOL_CALL_RESULT events are currently processed by the UI for rendering but are not being persisted back into the agent's message history.

    Root Cause Analysis

    In packages/core/src/core/state-manager.ts, the subscribeToAgent method sets up listeners for various agent events. It currently handles onNewMessage, onStateSnapshotEvent, onMessagesSnapshotEvent, etc., but does not include a handler for onToolCallResultEvent.

    Because TOOL_CALL_RESULT is a protocol-level event that represents a message with the tool role, it must be added to the agent's message list to ensure that subsequent turns include the necessary tool output for LLM validation (avoiding the 400 Bad Request error).

    Recommended Solution

    You can resolve this by adding an onToolCallResultEvent handler to the StateManager subscription. This handler should call agent.addMessage with a message of role tool.

    In packages/core/src/core/state-manager.ts:

    // Inside subscribeToAgent's agent.subscribe({...})
    onToolCallResultEvent: ({ event }) => {
      if (revoked) return;
      
      // Ensure the message isn't already there (deduplication)
      const exists = agent.messages.some(m => m.id === event.messageId);
      if (!exists) {
        agent.addMessage({
          id: event.messageId,
          role: "tool",
          content: event.content,
          toolCallId: event.toolCallId,
        });
      }
    },

    Alternative Workaround (Frontend)

    If you cannot modify the core library, you can apply a similar fix by subscribing to the agent directly in your React application and manually injecting the missing messages:

    useEffect(() => {
      const { unsubscribe } = agent.subscribe({
        onToolCallResultEvent: ({ event }) => {
          const exists = agent.messages.some(m => m.id === event.messageId);
          if (!exists) {
            agent.addMessage({
              id: event.messageId,
              role: "tool",
              content: event.content,
              toolCallId: event.toolCallId,
            });
          }
        }
      });
      return unsubscribe;
    }, [agent]);

    Sources & Documentation

    Confidence Assessment: High. The absence of the onToolCallResultEvent listener in StateManager directly explains why these specific messages aren't being tracked in the conversation history between turns.


    Was this helpful?

    If this solution worked for you, please click on the appropriate option below to help us improve:

    ✅ This was helpful | ❌ Need more help

  2. copilotkit-support-bot commented on Apr 14, 2026

    @copilotkit-support-bot

    👋 Thanks for your feedback!

    I've escalated this issue to our development team. Someone from our team will review your request and provide additional assistance shortly.

    If this is blocking production, please book a meeting with us at https://cal.com/nathan-tarbert-copilotkit/15min

  3. added a commit that references this issue on Apr 14, 2026
    b5202a0
  4. khchan commented on May 1, 2026

    @khchan

    Thank you @nebrass! I'm experiencing exactly the same issue with Microsoft Agent Framework and CopilotKit 🙏
    Can you share how fix_tool_gaps looked like for your workaround?

  5. djw-bsn commented on May 11, 2026

    @djw-bsn

    We also have the same issue with Agent Framework and Copilotkit. At the moment we are having to patch the messages in our backend

  6. added a commit that references this issue on Jun 18, 2026
    64da0ec
  7. linear commented on Jul 21, 2026

    @linear
  8. BenTaylorDev commented on Sep 30, 2026

    @BenTaylorDev
    Contributor

    Closing this: the root cause was in Microsoft Agent Framework's AG-UI adapter, not CopilotKit. Its final snapshot reused the text message's id for the tool-call message, so the history came back out of order and the adapter's tool-history sanitizer then dropped the tool result, which is what produced the 400. It's fixed in agent-framework-ag-ui >= 1.2.0 (microsoft/agent-framework#7223), and @nebrass confirmed on #3885 that the two-turn flow now works. If you still hit it on 1.2.0+, reopen with your versions.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions