#afterRequestMiddleware Returning Empty Messages in v2

1 messages · Page 1 of 1 (latest)

oblique sand
#

I’m using CopilotRuntime with a BuiltInAgent, and I’ve added an afterRequestMiddleware to inspect the messages:

return new CopilotRuntime<Parameter[]>({
  agents: {
    default: new BuiltInAgent({
      model,
      prompt: AGENT_META_PROMPT,
      maxOutputTokens: modelConfig.maxOutputTokens,
      maxSteps: 10,
      tools,
    }),
  },
  afterRequestMiddleware: async ({ messages }) => {
    console.log("AFTER REQUEST MIDDLEWARE", { messages });
  },
});

However, I’m consistently receiving an empty messages array in the middleware.

Is this expected behavior in v2, or has there been any change in how/when messages are populated in afterRequestMiddleware?

deep cypressBOT
#

This solution has been AI generated

You're seeing this because of how v2 streams results. In CopilotRuntime v2, afterRequestMiddleware runs right after the response is set up, but messages are produced during the SSE stream. If the SSE parser can't reconstruct any events, the middleware receives an empty array.

Why this happens in v2

  • CopilotRuntime uses Server‑Sent Events (SSE) to stream the agent’s output.
  • afterRequestMiddleware clones and parses the SSE response to rebuild the message list.
  • If the response isn’t text/event-stream or expected event types aren’t present, the parser returns [].
  • BuiltInAgent emits text/tool-call chunks; the parser looks for events like MESSAGES_SNAPSHOT, TEXT_MESSAGE_START, TEXT_MESSAGE_CHUNK, and a terminal run event to assemble messages.

What to check

  • Verify the response is truly SSE: Content-Type: text/event-stream.
  • Ensure the agent emits the expected events; simple BuiltInAgent runs may not emit MESSAGES_SNAPSHOT, so the parser relies on accumulating chunk events.
  • Confirm the stream reaches a terminal event (e.g., RUN_FINISHED); interrupted streams can yield empty/partial results.
  • Note: this middleware shows messages generated by this run, not your original input history. If you need full history, configure your agent to forward/snapshot it during the run.

Helpful debugging

afterRequestMiddleware: async (args) => {
  // args includes { messages, threadId, runId } reconstructed from SSE
  console.log("AFTER REQUEST MIDDLEWARE", args);
}

If you consistently get [], the SSE parser likely didn’t detect any valid event payloads in the stream body.

Reference

  • The SSE parsing logic runs inside the runtime’s middleware SSE parser and returns messages, plus threadId and runId extracted from RUN_STARTED.
#

Was this helpful?

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

deep cypressBOT
#

Thank you for your feedback! Our team will provide additional assistance. 🔄

rare pecan
#

@acoustic wasp

shell ginkgo
#

hey @oblique sand did you manage to find a fix for this ? I'm having the same problem;

 GET /api/auth/session 200 in 308ms (next.js: 277ms, application-code: 30ms)
Failed to read SSE response body in afterRequestMiddleware
Thread undefined - Prompt Tokens: 0, Total Tokens: 0
#

Hey @ivory oracle, @strange flume could you take a quick look at this when you have a moment?

shell ginkgo
#

for anyone having similar issues, here are two patterns to achieve similar behavior:

  1. create a middleware (server side)
import {
  Middleware,
  RunAgentInput,
  AbstractAgent,
  BaseEvent,
} from "@ag-ui/client";
import { Observable } from "rxjs";

export class LoggingMiddleware extends Middleware {
  run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
    return new Observable<BaseEvent>((subscriber) => {
      const sub = this.runNextWithState(input, next).subscribe({
        next: ({ event }) => {
          console.log("[agent event]", event.type);
          subscriber.next(event);
        },
        error: (err) => subscriber.error(err),
        complete: () => subscriber.complete(),
      });
      return () => sub.unsubscribe();
    });
  }
}
  1. subscribe to agent's events (client side):
import { useEffect } from "react";
import { useAgent } from "@copilotkit/react-core/v2";

function EventListener() {
  const { agent } = useAgent();

  useEffect(() => {
    const { unsubscribe } = agent.subscribe({
      onRunStartedEvent: () => console.log("Started"),
      onRunFinalized: () => console.log("Finished"),
    });

    return unsubscribe;
  }, []);

  return null;
}

Docs:

kindred chasm
#

Hey @shell ginkgo, 100% . Thanks for adding the docs links here.
This is the recommended path.