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 contentthinking: Model reasoning text emitted during inferencetool_call: Execution metadata including name, status, and IDstatus: Run state transitions and lifecycle eventstask: 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 anAsyncIterable<SDKMessage>for Server-Sent Events consumption - The orchestrate plugin implements streaming in
agent-manager.ts(lines 845-857) for accumulation andcli/task.ts(lines 34-71) for real-time display - Event types include
assistant,tool_call,status, andthinking, each requiring distinct handling logic - The
waitAndHandoffpattern buffers text while awaitingrun.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →