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. Continue it with a continuation token, inspect it with a session ID, and follow progress on the NDJSON stream. Chat sessions wait for follow-ups; task sessions run once and stop.

Session contents ​

Each session combines:

  • A channel and authenticated caller
  • A conversation the caller can continue
  • A workspace for local turns
  • An NDJSON event stream
  • State that lets the conversation resume after a restart

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

Session identifiers ​

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, 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.

Session modes ​

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.

Follow-ups ​

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 and queue the follow-up. The running turn may receive it at a tool boundary; any asks that remain become one follow-up turn when it finishes

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

Track which follow-up reached the agent ​

Every send() returns a deliveryId. Pass your own as the deliveryId option, such as the webhook delivery ID you already hold, or omit it to get a fresh dlv_… ID. A coalesced follow-up reaches the agent in one of two ways, and the ID is on the event for that path:

PathEventField
Steered into the running turnnudge.delivereddata.deliveryId
Fell back to the next turnThat turn's message.receiveddata.deliveryIds

message.received lists every send its prompt carries, in arrival order. Follow-ups queued behind a busy turn drain into one turn, so the list can hold several IDs. A direct send that starts a turn gets a one-item list.

ts
// In a route or channel hook: name the steer after the webhook delivery.
await send(`Incident ${incident.id} was acknowledged.`, {
  continuationToken: incident.id,
  admission: "coalesce",
  deliveryId: webhookId,
});

// In the channel's `events` map:
const steered = new Set<string>();
const prompted = new Set<string>();
const events: ChannelEventHandlers = {
  "nudge.delivered": async (event) => {
    if (event.data.deliveryId !== undefined) {
      steered.add(event.data.deliveryId);
    }
  },
  "message.received": async (event) => {
    for (const id of event.data.deliveryIds ?? []) {
      prompted.add(id);
    }
  },
};

A local Slack session can also pick up follow-ups at a tool boundary from its own thread store. That nudge.delivered has no deliveryId, which tells it apart from a steered send.

POST /v1/session/:id/stop interrupts a turn without sending a new message. The stream records turn.failed with status: "cancelled"; the session stays a chat session and can take another follow-up. A whole-message Slack stop / @agent stop does the same for that thread and clears pending coalesced follow-ups.

See Tools for direct-call behavior while a session is busy.

Stream events ​

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, session.waiting, session.completed, session.failedSession creation, readiness, and task completion
Inputmessage.received, nudge.deliveredA user message was accepted as a turn's prompt, or reached a running turn mid-turn. Both name the sends they carry
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, actions.completed, action.result, action.tool_retryTool names, validated arguments, outputs, and errors. action.tool_retry means a read is running again after a transient failure
Subagentssubagent.called, subagent.completedDelegated work
Artifactsartifact.taggedA durable artifact was tagged for this session, by host code or tag_artifact

Pair actions.requested with actions.completed for each model-step batch. action.result is the live per-call envelope. Read turn.completed.data.usage for input, output, and cache token counts.

Stream or replay 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, including after a server restart. 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.

Local session workspace ​

Agent SDK creates a workspace before the first local turn:

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

Instructions reach the model as its system prompt; they aren't written into the workspace. See Instructions.

Local turns use this workspace as their 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.

Session storage ​

Local session state lives under --state-root. A slugged mount stores it in a subdirectory named for the slug.

Deleting a session directory removes the session from the server: it disappears from listings and can no longer be streamed or continued.

Inspect a saved event stream ​

Use session get --file with a saved trace:

text
agent-sdk session get --file <state-root>/traces/<sessionId>.ndjson

The command prints tool calls, the reply, and token usage in the same JSON shape as run. Open the session's Trace view in the playground for a visual timeline.