# Implementing Streaming Responses with Cursor SDK Agents: A Complete Guide

> Learn to implement streaming responses with Cursor SDK agents. Use run.stream() to get real-time SDKMessage objects for assistant messages, tool calls, and status updates.

- Repository: [Cursor/plugins](https://github.com/cursor/plugins)
- Tags: how-to-guide
- Published: 2026-05-25

---

**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`](https://github.com/cursor/plugins/blob/main/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:

```ts
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`](https://github.com/cursor/plugins/blob/main/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.

```bash

# 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`](https://github.com/cursor/plugins/blob/main/cli/task.ts).

## Accumulating Output and Run Completion

### The waitAndHandoff Pattern

The `waitAndHandoff` routine in [`agent-manager.ts`](https://github.com/cursor/plugins/blob/main/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):

```ts
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`](https://github.com/cursor/plugins/blob/main/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.

```ts
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:

```ts
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:

```ts
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`](https://github.com/cursor/plugins/blob/main/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`](https://github.com/cursor/plugins/blob/main/orchestrate/skills/orchestrate/scripts/cli/task.ts) | CLI façade: `tail` command implementation (lines 34-71) |
| [`orchestrate/skills/orchestrate/scripts/core/prompts.ts`](https://github.com/cursor/plugins/blob/main/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`](https://github.com/cursor/plugins/blob/main/agent-manager.ts) (lines 845-857) for accumulation and [`cli/task.ts`](https://github.com/cursor/plugins/blob/main/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`](https://github.com/cursor/plugins/blob/main/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`](https://github.com/cursor/plugins/blob/main/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.