Skip to content

Sessions, events, and streaming

A session keeps one conversation, its workspace, and an append-only record of every message and tool call.

What does a session contain?

Each session combines:

  • A channel and authenticated caller
  • A conversation the caller can continue
  • A workspace for local turns
  • An NDJSON event stream
  • Runtime state needed to resume after a server restart

Sessions belong to the principal that created them. Follow-up, stream, and list routes return 403 when another caller tries to access one.

Which session identifier should I use?

Sessions have two identifiers because conversation routing and inspection are different jobs.

IdentifierUse it for
continuationTokenContinue a conversation through its channel
sessionIdStream events, inspect state, resolve approvals, or run a session-bound tool call

Channels decide what a continuation token looks like. Slack uses its thread identity. A PR channel can use a key such as pr:owner/repo#1. The built-in HTTP API returns an opaque token and rotates it after each accepted follow-up. Reusing a stale HTTP token returns 409.

Use the continuation token to keep talking. Use the session ID to observe or manage the stored session.

Which session modes are available?

ModeCreated byWhat happens after a turn
chatHTTP sessions, channel send, Slack, or MCP askWaits in session.waiting and accepts follow-ups
taskMarkdown schedules and fire-and-forget dispatchEnds in session.completed or session.failed

Task sessions don't accept follow-ups. Trying one returns 409.

What happens when I send a follow-up?

A follow-up to an idle chat session starts another turn. Admission when the session is already busy depends on the channel:

PathBusy-session policy
HTTP playground / POST /v1/session/:id / MCP askPreempt (default): interrupt the in-flight turn, wait for it to settle, then run the new message
Slack mentions / DMs / alert-watchCoalesce: leave the active turn running, enqueue the follow-up, and drain queued asks into one follow-up turn when the active turn finishes (no mid-turn tool/hook inject)

Pass admission: "coalesce" on send() to opt into the Slack policy from other callers. Omit it (or pass "preempt") to keep interrupt semantics.

POST /v1/session/:id/stop interrupts a turn without sending a new message. Interrupted turns record turn.failed with "turn interrupted". This means the turn was preempted. A whole-message Slack stop / @agent stop does the same for that thread and clears pending coalesced nudges.

Session-bound deterministic tool calls share the same execution lock. They return 409 session_busy while a model turn is running.

Which events can I stream?

Each NDJSON line uses this envelope: { type, index, sessionId, turnId?, at, data }. The index increases within one session. The at field is an ISO-8601 timestamp.

PhaseEventsWhat they tell you
Sessionsession.started, ab.assigned, session.waiting, session.completed, session.failedSession creation, A/B enrollment, readiness, and task completion
Agentagent.boundCursor SDK agent ID and cloud conversation URL
Inputmessage.receivedA user message was accepted
Turnturn.queued, turn.started, turn.completed, turn.failedQueue position under a maxRunningTurns cap, then turn status, final result, and token usage
Stepsstep.started, step.completedModel step boundaries and duration
Reasoningreasoning.appended, reasoning.completedStreamed reasoning blocks
Replymessage.appended, message.completedText deltas and finalized assistant messages
Toolsactions.requested, action.resultTool names, validated arguments, outputs, and errors
Approvalsaction.approval_requested, action.approval_resolvedA parked tool call and the human decision
Subagentssubagent.called, subagent.completedDelegated work
Artifactsartifact.taggedA durable artifact was tagged for this session, by host code or tag_artifact

Pair actions.requested with action.result to reconstruct the tool trajectory. Read turn.completed.data.usage for input, output, and cache token counts.

How do I stream or replay session events?

One endpoint handles both live streaming and replay:

bash
curl -N 'http://127.0.0.1:3000/<slug>/v1/session/ses_…/stream?startIndex=0'

Pass startIndex to continue after the last event you received. Omit it or pass 0 to replay the full session before following new events. GET /v1/session/:id/events returns a one-time dump without staying connected.

Event streams replay from disk after a server restart. Conversation state resumes from the Cursor SDK store.

What goes into a local session workspace?

The Agent SDK creates a workspace before the first local turn:

Source pathLands as
instructions.*AGENTS.md
skills/*.cursor/skills/<name>/SKILL.md
agent tools (execution: "agent")scripts under .agent-serve/tools/, with a catalog in AGENTS.md
sandbox/workspace/**copied in as seed files
per-send workspaceFileswritten before the turn

The local harness uses this workspace as its working directory. Parent directories can contribute AGENTS.md and .cursor settings. Set local.cwd when you need a clean parent directory. A channel can also provide a different working directory for one session, such as a PR worktree.

See Agent config: local cwd for the inheritance rules.

Where does the Agent SDK store session data?

Local state uses one directory tree:

text
<project>/.agent-serve/           # or <stateRoot>/<slug>/ under serve
  sessions/<id>/session.json       # metadata: channel, mode, principal, tokens
  sessions/<id>/events.ndjson      # the durable stream
  sessions/<id>/workspace/         # the harness cwd
  traces/<sessionId>.ndjson        # written by `run`
  runner/                          # Cursor SDK conversation store
  tool-calls/<callId>/             # ephemeral deterministic-call workspaces

Deleting a session directory removes the session from the server: it disappears from listings and can no longer be streamed or continued. The runner/ store keeps its own conversation copy until you remove it. Cloud conversations remain on the Cursor backend.

Change the root with --state-root or stateRoot. Keep it outside repositories whose parent rules shouldn't reach the agent. See local session workspaces.

How do I inspect a saved event stream?

Use trajectory with a trace or session event file:

bash
agent-sdk trajectory --events .agent-serve/traces/<sessionId>.ndjson
agent-sdk trajectory --events <stateRoot>/<slug>/sessions/<id>/events.ndjson

The command prints tool calls, the reply, and token usage in the same JSON shape as run. Use Open trace in the playground for a visual view.