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.
| Identifier | Use it for |
|---|---|
continuationToken | Continue a conversation through its channel |
sessionId | Stream 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
| 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.
Follow-ups
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 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.
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.
| Phase | Events | What they tell you |
|---|---|---|
| Session | session.started, session.waiting, session.completed, session.failed | Session creation, readiness, and task completion |
| 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, actions.completed, action.result, action.tool_retry | Tool names, validated arguments, outputs, and errors. action.tool_retry means a read is running again after a transient failure |
| 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 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:
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
BDK creates a workspace before the first local turn:
| Source path | Lands 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 workspaceFiles | written 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 trajectory with a saved trace:
bdk trajectory --events <state-root>/traces/<sessionId>.ndjsonThe 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.