Implementing Streaming Responses with Cursor SDK Agents: A Complete Guide

Cursor SDK agents expose a Server-Sent Events (SSE) stream via run.stream() that yields SDKMessage objects, enabling real-time consumption of assistant messages, tool calls, and status updates as the model runs.

The cursor/plugins repository provides the orchestrate plugin, which manages complex AI agent workflows through the Cursor SDK. When implementing streaming responses with Cursor SDK agents, developers interact with an asynchronous iterable API that emits events throughout the model's execution lifecycle, from initial prompt processing to final hand-off generation.

Understanding the SSE Stream Architecture

Core Event Types

The stream returns SDKMessage objects with distinct type fields. According to the implementation in orchestrate/skills/orchestrate/scripts/core/agent-manager.ts, the primary event types include:

  • assistant: Text blocks containing the model's response content
  • thinking: Model reasoning text emitted during inference
  • tool_call: Execution metadata including name, status, and ID
  • status: Run state transitions and lifecycle events
  • task: Human-visible task messages for operator communication

The AsyncIterable API

The fundamental interface exposed by the Cursor SDK follows this pattern:

const stream = run.stream(); // AsyncIterable<SDKMessage>
for await (const event of stream) {
  // event.type === "assistant" | "tool_call" | "status" | …
}

Each iteration yields the next SSE event as the model produces output, allowing your application to process tokens as they arrive rather than waiting for complete generation.

Interactive Tailing and Real-Time Monitoring

The CLI tail Command

The orchestrate plugin provides a command-line interface for streaming observation. Located in orchestrate/skills/orchestrate/scripts/cli/task.ts (lines 34-71), the tail command creates an AgentManager instance and iterates over mgr.tail(task) to display live output to stdout/stderr.


# Stream live output for task "build" in workspace ./my-workspace

bun cli.ts tail ./my-workspace build

Filtering Output Streams

The CLI implements an --only-text flag that suppresses non-assistant events. This is achieved by checking ev.type === "assistant" before printing content blocks, as implemented in the loop at lines 34-71 of cli/task.ts.

Accumulating Output and Run Completion

The waitAndHandoff Pattern

The waitAndHandoff routine in agent-manager.ts (lines 1226-1232) demonstrates production-grade stream buffering. While awaiting run.wait() for the terminal status, this pattern concatenates every assistant text block into accumulatedText (lines 845-857):

let accumulated = "";
for await (const ev of run.stream()) {
  if (ev.type === "assistant") {
    for (const blk of ev.message.content) {
      if (blk.type === "text") accumulated += blk.text;
    }
  }
}
// After the stream ends, `accumulated` holds the complete assistant response

This ensures a complete hand-off file can be generated even if the run never emits a final result message. The loop lives at lines 845-851, where each SSE event is await-ed until the stream exhausts.

Advanced Stream Handling Patterns

Tool-Call Tracking and Diagnostics

The implementation at lines 848-851 of agent-manager.ts records the most recent tool-call name and timestamp during stream iteration. This metadata enables failure diagnostics and side-car construction when runs encounter OOM-type errors or infinite loops.

let lastTool: { name: string; ts: number } | null = null;

for await (const ev of run.stream()) {
  if (ev.type === "tool_call") {
    lastTool = { name: ev.name ?? "unknown", ts: Date.now() };
    console.error(`[tool_call] ${ev.name} ${ev.status}`);
  }
}

Idle Detection and Error Resilience

The stream updates lastSseActivityAt on every event (lines 842-849), which a watchdog monitors to detect stuck runs. For error handling, lines 860-862 catch SSE stream errors and ignore them because run.wait() serves as the authoritative completion mechanism. This allows the orchestration loop to continue to the wait step even if the network stream drops unexpectedly.

Practical Implementation Examples

Programmatic Stream Processing

For custom implementations that mirror the built-in tail command but with full control:

import { loadOrBail } from "./orchestrate/skills/orchestrate/scripts/util.ts";

async function streamTask(workspace: string, taskName: string) {
  const mgr = await loadOrBail(workspace);
  for await (const ev of mgr.tail(taskName)) {
    if (ev.type === "assistant") {
      // Print only the assistant's textual content
      for (const block of ev.message.content) {
        if (block.type === "text") process.stdout.write(block.text);
      }
    }
  }
}

Detecting Tool-Call Activity

This pattern tracks tool execution during streaming for custom telemetry:

let lastTool: { name: string; ts: number } | null = null;

for await (const ev of run.stream()) {
  if (ev.type === "tool_call") {
    lastTool = { name: ev.name ?? "unknown", ts: Date.now() };
    console.error(`[tool_call] ${ev.name} ${ev.status}`);
  }
}

Key Source Files Reference

File Role
orchestrate/skills/orchestrate/scripts/core/agent-manager.ts Core orchestration: spawning agents, waitAndHandoff, streaming logic (lines 845-862, 1226-1232)
orchestrate/skills/orchestrate/scripts/cli/task.ts CLI façade: tail command implementation (lines 34-71)
orchestrate/skills/orchestrate/scripts/core/prompts.ts Prompt construction that drives the streaming response

These files implement the full flow: prompt → agent → run → SSE stream → accumulation → hand-off.

Summary

  • Cursor SDK agents provide run.stream() returning an AsyncIterable<SDKMessage> for Server-Sent Events consumption
  • The orchestrate plugin implements streaming in agent-manager.ts (lines 845-857) for accumulation and cli/task.ts (lines 34-71) for real-time display
  • Event types include assistant, tool_call, status, and thinking, each requiring distinct handling logic
  • The waitAndHandoff pattern buffers text while awaiting run.wait() completion to ensure durable hand-off generation
  • Tool-call tracking and idle detection rely on stream metadata updated at lines 848-851 and 842-849
  • SSE errors are gracefully ignored at lines 860-862 because run.wait() provides authoritative run status

Frequently Asked Questions

How do I access the stream from a Cursor SDK agent?

After calling run.send(prompt), access the SSE stream via run.stream(), which returns an AsyncIterable<SDKMessage>. Use a for await...of loop to receive events in real-time as the model generates output, processing each SDKMessage based on its type field.

What is the difference between run.stream() and run.wait()?

run.stream() provides live Server-Sent Events during execution, yielding partial assistant messages and tool calls, while run.wait() blocks until the run reaches a terminal state and returns the final result. Production code typically consumes the stream for real-time updates while simultaneously awaiting run.wait() for authoritative completion signaling, as implemented in the waitAndHandoff routine.

How does the orchestrate plugin handle streaming errors?

According to lines 860-862 in agent-manager.ts, errors from the SSE stream are caught and ignored because run.wait() serves as the authoritative completion mechanism. This ensures that transient stream failures don't terminate the orchestration workflow, allowing the system to proceed to hand-off generation once wait() resolves.

Can I filter specific event types from the stream?

Yes. The CLI implementation in cli/task.ts (lines 34-71) demonstrates filtering with an --only-text flag that processes only assistant type events. Similarly, you can implement conditional logic within your for await...of loop to handle tool_call, status, or thinking events differently based on your application's requirements.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →