Skip to content

🐛 Bug: Reasoning / Thinking events in SSE stream cause agent to stall #3323

Closed

Description

@linussuperfly

♻️ Reproduction Steps

Summary

When Anthropic models produce reasoning/thinking tokens and CopilotKit emits REASONING_* events in the AG-UI SSE stream, the agent stalls permanently after the thinking phase completes. No further tool calls, text generation, or RUN_END events are emitted. The agent simply stops.

Stripping all REASONING_* events from the SSE response stream before they reach the frontend resolves the issue — the agent proceeds normally with tool calls and text output.

Detailed description

Setup

We use BuiltInAgent from @copilotkitnext/agent with Anthropic models. Thinking/reasoning is configured in two complementary ways:

  1. Vercel AI SDK providerOptions — passed to createLanguageModelForAgent:

    providerOptions: {
      anthropic: {
        thinking: { type: 'adaptive' },
        effort: 'medium',
      }
    }
  2. HTTP-level injection — a custom fetch wrapper that intercepts the outbound Anthropic API request and injects thinking, max_tokens, and model-level effort params into the JSON body (as a fallback / override for values the SDK may not pass through).

Both approaches work correctly at the Anthropic API level — thinking tokens are generated and streamed back.

What we tried (chronologically)

  1. Verified @ai-sdk/anthropic version: Initially had 2.0.56 which lacked type: 'adaptive' support in its Zod schema. Upgraded to 2.0.67 which natively supports adaptive thinking. Did not fix the stall.

  2. Verified CopilotKit version consistency: Ensured all @copilotkit/* and @copilotkitnext/* packages are at 1.52.1 (the version that added reasoning support per PR #3165). Did not fix the stall.

  3. Increased maxTokens: Set maxTokens to the model maximum (64000 for Sonnet 4.6) to rule out token budget exhaustion. Did not fix the stall.

  4. Disabled SDK-level thinking settings (kept HTTP-level injection): Set providerOptions.anthropic.thinking to undefined so the SDK is unaware of thinking, but the HTTP-level fetch wrapper still injects thinking: { type: 'adaptive' } into the raw API request body. Anthropic still returns thinking tokens, and the agent still stalled. This proved the issue is not in how we configure the SDK, but in how CopilotKit handles the streamed reasoning events.

  5. Stripped all REASONING_* events from the SSE stream: Added a TransformStream in our response pipeline that filters out any SSE data: line where the parsed JSON has type starting with "REASONING". This fixed the stall. Agents proceed normally to tool calls and text generation.

Workaround (current)

We strip REASONING_* events from the AG-UI SSE stream before the response reaches the frontend:

// Inside our SSE TransformStream rewrite function
const parsed = JSON.parse(jsonPart)
if (parsed?.type?.startsWith?.('REASONING')) return '' // drop the line

This is behind a feature flag so we can re-enable reasoning events when a fix is available.

Key observation

The Anthropic API call itself succeeds. Thinking tokens are generated and the model produces a complete response (tool calls and/or text). The issue is entirely in how CopilotKit's runtime translates the Vercel AI SDK's reasoning stream into AG-UI SSE events. It appears that REASONING_END is never emitted, which may leave the runtime's internal state machine stuck in a "reasoning" state, preventing it from emitting subsequent text or tool call events. When REASONING_* events are stripped from the stream, the agent works perfectly — even though Anthropic is still performing reasoning internally (the thinking just isn't surfaced to the frontend).

Expected behavior

After reasoning content finishes streaming, a REASONING_END event should be emitted, followed by the agent continuing its run with TEXT_MESSAGE_START, TOOL_CALL_START, or RUN_END as appropriate. The presence of REASONING_* events in the stream should not cause the agent run to stall.

Steps to reproduce

  1. Configure a BuiltInAgent with an Anthropic model that supports thinking (e.g., Claude Sonnet 4.6).
  2. Set providerOptions.anthropic.thinking to { type: 'adaptive' } (or { type: 'enabled', budgetTokens: N }).
  3. Send a message that triggers agent tool use (not just a simple text reply).
  4. Observe that REASONING_START and REASONING_MESSAGE_CONTENT events are emitted, but no REASONING_END ever arrives. The stream stalls — no further events of any kind.
  5. Strip all REASONING_* events from the SSE stream and repeat — agent proceeds normally.

Possibly related

  • CopilotKit PR #3165 — Added reasoning message support
  • The issue may be in how the runtime converts Vercel AI SDK reasoning chunks into AG-UI events — specifically, REASONING_END never being emitted, which could block the state machine from transitioning to text/tool-call phases

✅ Expected Behavior

SSE returns REASONING_END which then resumes flow.

❌ Actual Behavior

What happens

  1. Agent receives a user message and starts a run.
  2. Anthropic returns thinking/reasoning content. CopilotKit's runtime translates these into AG-UI SSE events:
    • REASONING_START
    • REASONING_MESSAGE_CONTENT (multiple, streaming the thinking text)
  3. After the reasoning content finishes streaming, the stream stalls. No REASONING_END is ever emitted. No TEXT_MESSAGE_START, no TOOL_CALL_START, no RUN_END — nothing. The stream hangs indefinitely.
  4. The frontend shows the thinking content but the agent never proceeds to action.

Screenshots

No response

𝌚 CopilotKit Version

## Environment

| Dependency | Version |
|---|---|
| `@copilotkit/runtime` | `1.52.1` |
| `@copilotkitnext/agent` | `1.52.1` |
| `@copilotkit/react-core` | `1.52.1` |
| `@ai-sdk/anthropic` | `2.0.67` |
| `ai` (Vercel AI SDK) | `5.0.116` |
| Node.js | v24 |

**LLM models used:** Anthropic Claude Sonnet 4.6, Claude Opus 4.6, Claude Opus 4.5 — all with `thinking` configured via the Vercel AI SDK's `providerOptions`.

📄 Logs (Optional)

Activity

  1. linussuperfly commented on Mar 16, 2026

    @linussuperfly
    Author

    Here is actual root cause and solution for this bug @giulio-leone

    CopilotKit Bug Report: Agent stalls when Anthropic reasoning/thinking is enabled

    Environment

    Dependency Version
    @copilotkit/runtime 1.52.1
    @copilotkitnext/agent 1.52.1
    @copilotkit/react-core 1.52.1
    @ag-ui/core 0.0.46
    @ag-ui/client 0.0.46
    @ai-sdk/anthropic 2.0.67 / 0.78.0
    ai (Vercel AI SDK) 5.0.116

    Models tested: Anthropic Claude Sonnet 4.6, Claude Opus 4.6, Claude Opus 4.5 -- all with thinking configured via providerOptions.anthropic.thinking.

    CopilotKit mode: useSingleEndpoint={true}, V2 interface (@copilotkit/react-core/v2).

    Symptom

    When Anthropic models produce reasoning/thinking tokens, the agent stalls permanently after the reasoning phase completes. The UI shows reasoning content streaming in, but once the last REASONING_MESSAGE_CONTENT event is processed, no further events are delivered to the application -- no REASONING_MESSAGE_END, no TEXT_MESSAGE_START, no tool calls, no RUN_FINISHED. The agent appears frozen.

    Disabling reasoning (so the model does not produce thinking tokens) makes the agent work perfectly. Re-enabling reasoning reproduces the stall 100% of the time.

    Root cause

    There are two issues in @copilotkitnext/agent's BuiltInAgent reasoning event handling:

    Issue 1: Empty reasoning deltas kill the event pipeline (critical)

    The reasoning-delta case in the fullStream loop creates REASONING_MESSAGE_CONTENT events with:

    delta: ("text" in part ? part.text : part.delta) ?? ""

    When the Vercel AI SDK produces a reasoning-delta where both part.text and part.delta are undefined or null, the ?? operator falls back to "" (empty string).

    The @ag-ui/core Zod schema for ReasoningMessageContentEventSchema has a refinement that rejects empty deltas:

    delta: z.string().refine((s) => s.length > 0, "Delta must not be an empty string")

    This causes EventSchemas.parse() to throw inside @ag-ui/client's transformHttpEventStream function (L). The thrown error is caught by the generic handler:

    F(n).subscribe({
      next: e => {
        try {
          let n = EventSchemas.parse(e);
          t.next(n);
        } catch(e) {
          t.error(e);  // Kills the entire Observable pipeline
        }
      },
      ...
    });

    The t.error(e) call errors the output Subject, which propagates through the RxJS Observable pipeline. In useSingleEndpoint mode, both the runAgent and connectAgent pipelines process events from the same backend SSE stream -- both die simultaneously when the validation error propagates.

    After the pipelines die, the backend continues streaming remaining events (REASONING_MESSAGE_END, REASONING_END, TEXT_MESSAGE_START, tool calls, RUN_FINISHED), but they are never processed by the dead frontend pipelines.

    Critically, no onRunFailed callback fires because the catchError operator in the pipeline returns a completing Observable (via onError -> stopPropagation), so the error is silently swallowed from the application's perspective. The only visible symptom is the agent appearing frozen.

    Issue 2: No defensive auto-close for reasoning state (secondary)

    If the AI SDK does not produce a reasoning-end event (e.g., due to an abort, error, or the model transitioning directly to text/tool output), the BuiltInAgent never emits REASONING_MESSAGE_END or REASONING_END. This leaves the AG-UI event state machine in an inconsistent state.

    This is the issue addressed by PR #3399, but that PR alone does not fix the stall because the empty-delta issue (Issue 1) kills the pipeline before any auto-close logic can run.

    Fix

    Two changes in @copilotkitnext/agent's BuiltInAgent fullStream loop:

    1. Skip empty reasoning deltas

    case "reasoning-delta": {
        const delta = ("text" in part ? part.text : part.delta) ?? "";
        if (delta.length === 0) break;  // Skip empty deltas
        const reasoningDeltaEvent = {
            type: EventType.REASONING_MESSAGE_CONTENT,
            messageId: reasoningMessageId,
            delta: delta
        };
        subscriber.next(reasoningDeltaEvent);
        break;
    }

    2. Defensive auto-close for reasoning state

    Track whether the agent is currently in a reasoning phase and auto-close it when transitioning to a different state:

    let isInReasoning = false;
    const closeReasoningIfOpen = () => {
        if (!isInReasoning) return;
        isInReasoning = false;
        subscriber.next({ type: EventType.REASONING_MESSAGE_END, messageId: reasoningMessageId });
        subscriber.next({ type: EventType.REASONING_END, messageId: reasoningMessageId });
    };

    Set isInReasoning = true in the reasoning-start case, isInReasoning = false in the reasoning-end case, and call closeReasoningIfOpen() at the start of:

    • abort
    • tool-input-start
    • text-start
    • tool-call
    • finish
    • error
    • The catch block of the for await loop

    How we diagnosed this

    1. Multi-layer SSE logging confirmed that the backend sends all events correctly (including REASONING_MESSAGE_END, REASONING_END, TEXT_MESSAGE_START, RUN_FINISHED). A raw window.fetch interceptor on the frontend confirmed the browser receives all these events over the SSE stream.

    2. Agent event subscription (agent.subscribe({ onEvent, onRunFailed, onRunFinalized })) showed that the AG-UI apply() pipeline processes REASONING_MESSAGE_CONTENT events correctly, then onRunFinalized fires prematurely. onRunFailed never fires.

    3. Pipeline-level diagnostics in @ag-ui/client (labeling runAgent and connectAgent finalize callbacks with console.trace()) revealed that the runAgent pipeline finalizes via Subject2.error originating from EventSchemas.parse() in transformHttpEventStream. The stack trace showed the error propagating through catchError which swallows it and completes the pipeline.

    4. Inspecting @ag-ui/core schemas revealed the ReasoningMessageContentEventSchema has delta: z.string().refine(s => s.length > 0), which rejects empty strings.

    5. Inspecting @copilotkitnext/agent source confirmed the reasoning-delta handler uses ?? "" as a fallback, producing empty deltas that fail validation.

    Related

  2. added 3 commits that reference this issue on Mar 25, 2026
    fedecd8
    6518827
    8aafcbe
  3. added a commit that references this issue on Apr 2, 2026
    8fb0ed5
  4. added a commit that references this issue on Sep 11, 2026
    2fd9f06
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