Cereal docs
Sessions and UI

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.

  • system is 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.
  • assistant is a reply you are asserting on the agent's behalf: a seeded answer, or a decision made elsewhere that the conversation continues from.
  • user is 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 one

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

On this page