Appearance
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
| Identifier | Use it for |
|---|---|
continuationToken | Continue a conversation through its channel |
sessionId | Stream 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.
| Path | Busy-session policy |
|---|---|
HTTP playground, POST /v1/session/:id, MCP ask | Preempt (the default). Interrupts the running turn, then runs the new message |
| Slack mentions, DMs, alert-watch | Coalesce. 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.
| Path | Event | Field |
|---|---|---|
| Steered into the running turn | nudge.delivered | data.deliveryId |
| Fell back to the next turn | That turn's message.received | data.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.
| Phase | Events | What they tell you |
|---|---|---|
| Session | session.started, session.waiting, session.completed, session.failed | Creation, readiness, and task completion |
| Input | message.received, nudge.delivered | A prompt was accepted or reached a running turn. Both carry delivery IDs |
| Turn | turn.queued, turn.started, turn.completed, turn.failed | Queueing under maxRunningTurns, then status, 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 finished assistant messages |
| Tools | actions.requested, actions.completed, action.result, action.tool_retry | Tool names, arguments, outputs, and errors. action.tool_retry marks a read retried after a transient failure |
| Subagents | subagent.called, subagent.completed | Delegated work |
| Artifacts | artifact.tagged | An 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 path | Lands as |
|---|---|
skills/* | .cursor/skills/<name>/SKILL.md |
agent tools (execution: "agent") | Scripts listed in the system prompt |
sandbox/workspace/** | Seed files |
per-send workspaceFiles | Files 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.