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.

Session contents ​

Each session also has a channel, an authenticated caller, an NDJSON event stream, and state that survives a restart. Sessions belong to the principal that created them. Another caller gets 403 from that session's routes.

Session identifiers ​

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

Each channel picks its token format, such as a Slack thread or pr:owner/repo#1. The built-in HTTP API returns an opaque token and rotates it after each accepted follow-up, so a stale HTTP token returns 409.

Session modes ​

HTTP sessions, channel send, Slack, and MCP ask create chat sessions, which wait in session.waiting for follow-ups. Markdown schedules and fire-and-forget dispatch create task sessions, which end in session.completed or session.failed. A follow-up to a task session returns 409.

Follow-ups ​

A follow-up starts a turn right away on an idle session. On a busy one, the channel decides what happens.

PathBusy-session policy
HTTP playground, POST /v1/session/:id, MCP askPreempt (the default). Interrupts the running turn, then runs the new message
Slack mentions, DMs, alert-watchCoalesce. The running turn may pick up the follow-up at a tool boundary, and the rest become one turn when it finishes

Pass admission: "coalesce" on send() to use the Slack policy from other callers.

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

Track which follow-up reached the agent ​

Every send() returns a deliveryId. Pass your own, such as a webhook delivery ID, or omit it to get a fresh dlv_… ID. A coalesced follow-up reaches the agent in one of two ways.

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, so one turn can carry several IDs.

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);
    }
  },
};

In a local Slack session, a nudge.delivered event with no deliveryId came from a new Slack message, not from a send() call.

Stream events ​

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

PhaseEventsWhat they tell you
Sessionsession.started, session.waiting, session.completed, session.failedCreation, readiness, and task completion
Inputmessage.received, nudge.deliveredA prompt was accepted or reached a running turn. Both carry delivery IDs
Turnturn.queued, turn.started, turn.completed, turn.failedQueueing under maxRunningTurns, then status, result, and token usage
Stepsstep.started, step.completedModel step boundaries and duration
Reasoningreasoning.appended, reasoning.completedStreamed reasoning blocks
Replymessage.appended, message.completedText deltas and finished assistant messages
Toolsactions.requested, actions.completed, action.result, action.tool_retryTool names, arguments, outputs, and errors. action.tool_retry marks a read retried after a transient failure
Subagentssubagent.called, subagent.completedDelegated work
Artifactsartifact.taggedAn artifact was tagged

actions.requested and actions.completed bracket each model step's tool batch, and action.result carries each call live. Read turn.completed.data.usage for token counts. HTTP API shows how to follow the stream.

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 listed in the system prompt
sandbox/workspace/**Seed files
per-send workspaceFilesFiles written before the turn

On a local turn, Agent SDK sends the instructions as the system prompt, and on a turn in the cloud it includes them in the first user message. Nothing is written into the session workspace as AGENTS.md. See Instructions.

A channel can give one session a different directory, such as a PR worktree. Agent config covers what parent directories add.

Session storage ​

Local session state lives under --state-root, in a subdirectory named for the slug on a slugged mount. Deleting a session directory removes the session from the server.

Inspect a saved event stream ​

agent-sdk session get --file <state-root>/traces/<sessionId>.ndjson prints a saved trace's tool calls, reply, and token usage in the same JSON shape as run. For a session on a running server, agent-sdk session get <sessionId> --view events prints the raw event list. The playground's Trace view shows a timeline.