Appearance
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.
| Identifier | Use it for |
|---|---|
continuationToken | Continue a conversation through its channel |
sessionId | Stream 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?
| Mode | Created by | What happens after a turn |
|---|---|---|
chat | HTTP sessions, channel send, Slack, or MCP ask | Waits in session.waiting and accepts follow-ups |
task | Markdown schedules and fire-and-forget dispatch | Ends 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:
| Path | Busy-session policy |
|---|---|
HTTP playground / POST /v1/session/:id / MCP ask | Preempt (default): interrupt the in-flight turn, wait for it to settle, then run the new message |
| Slack mentions / DMs / alert-watch | Coalesce: 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.
| Phase | Events | What they tell you |
|---|---|---|
| Session | session.started, ab.assigned, session.waiting, session.completed, session.failed | Session creation, A/B enrollment, readiness, and task completion |
| Agent | agent.bound | Cursor SDK agent ID and cloud conversation URL |
| Input | message.received | A user message was accepted |
| Turn | turn.queued, turn.started, turn.completed, turn.failed | Queue position under a maxRunningTurns cap, then turn status, final result, and token usage |
| Steps | step.started, step.completed | Model step boundaries and duration |
| Reasoning | reasoning.appended, reasoning.completed | Streamed reasoning blocks |
| Reply | message.appended, message.completed | Text deltas and finalized assistant messages |
| Tools | actions.requested, action.result | Tool names, validated arguments, outputs, and errors |
| Approvals | action.approval_requested, action.approval_resolved | A parked tool call and the human decision |
| Subagents | subagent.called, subagent.completed | Delegated work |
| Artifacts | artifact.tagged | A 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 path | Lands 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 workspaceFiles | written 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 workspacesDeleting 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.ndjsonThe 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.