Manage conversations
Seed, read, interrupt, and list sessions from your application server.
For a complete application, follow the chat tutorial.
These focused recipes use an authenticated server-side client created with
SDK client setup; agentId and id come from earlier API
responses. Use session lifecycle for shared behavior
and HTTP session reference for other languages.
Who a message is from
A message carries a role: user, system or assistant. It defaults to user,
unless you specify another role.
const session = await client.sessions.create({
agentId,
messages: [
{ role: "system", text: "The customer is on the pro plan, in the EU." },
{ role: "assistant", text: "Earlier I identified two launch risks in the source documents." },
{ text: "And the shipping on that one?" }
]
})That is one turn, not three. Only a user message asks for one, which is the
point of the other two: you seed the situation and the agent answers the question,
once.
systemis an instruction, not a turn in the conversation. It becomes an additional system block after the agent's own prompt, and several of them concatenate in the order you sent them. Use it for the paragraph your server computes for the session. It follows the stable prompt prefix and survives compaction.assistantis a reply you are asserting on the agent's behalf: a seeded answer, or a decision made elsewhere that the conversation continues from.useris a question, and it is what wakes the session.
Two dials on top of that. start: true asks for a turn from a system or assistant
message, for when the seeding is the ask; start: false on a user message records
it without answering. And mode: "steer" is refused for a non-user role: there is
no mid-turn seam to fold a system block into, and none is needed, since the prompt is
rebuilt from the log at every step.
Over the stream, a system message is never disclosed. It is content your server
wrote for the model, in the same category as the agent's system prompt. A seeded
assistant message is disclosed, and arrives as the assistant turn it is.
Reading it
// A page of the log, oldest first.
const events = await client.sessions.events({ id, after: 0 })
// A tail, server-side. Reconnects are the caller's business; `after` makes them safe.
for await (const event of client.sessions.stream(id, { after: lastSeq })) { … }For a browser, do not proxy either of those: mint a stream token and let the page read the SSE route directly.
Interrupting
await client.sessions.interrupt({ id })This cuts the running turn immediately, without going through the mailbox. The session
writes session.status(idle, interrupted); a tool call that was in flight stays open
in the log and is rendered to the model as interrupted on the next turn.
To redirect rather than stop, send with mode: "steer". See
delivery modes.
Listing
sessions.list filters by containment on metadata, and by agent, status and identity.
Everything narrowing lives under filter.
await client.sessions.list({
filter: {
agentId,
status: "idle",
roots: true, // only sessions with no parent
metadata: { customerId: "cus_42" } // containment; see Metadata
}
})
await client.sessions.children({ id }) // sub-agent sessions of this oneA sub-agent execution is a real session, so it is listed
like any other. roots: true leaves the children out, which is what a list of the
work somebody actually started looks like; omitted or false, everything is listed.
Starting one exactly once
sessions.create takes an optional idempotencyKey, unique per environment.
sessions.send takes one too, unique per session; that half is on
enqueue and steer.
await client.sessions.create({
agentId,
initialMessage: "run the nightly report for tenant 42",
idempotencyKey: `nightly:${tenantId}:${runDate}`
})A duplicate returns the original session and does not send its initial message again. Concurrent creates with the same key resolve to the same session. Reusing a key with a different agent or message still returns the original; use a new key for new work.