|
1 | 1 | --- |
2 | 2 | title: "Sessions" |
3 | 3 | sidebarTitle: "Sessions" |
4 | | -description: "The durable, task-bound, bi-directional I/O primitive that backs chat.agent — sessions.list / open / start / close plus the SessionHandle (in/out) API." |
| 4 | +description: "A Session is a pair of durable streams — input carries your users' messages to the agent, output carries everything the agent produces back — plus orchestration of the runs that process them." |
5 | 5 | --- |
6 | 6 |
|
7 | 7 | import RcBanner from "/snippets/ai-chat-rc-banner.mdx"; |
8 | 8 |
|
9 | 9 | <RcBanner /> |
10 | 10 |
|
11 | | -A **Session** is a durable, task-bound, bi-directional I/O channel pair. It outlives any single run: a Session row is keyed on a stable `externalId` (e.g. `chatId`), holds the conversation's identity across run boundaries, and exposes two realtime streams — `.in` (clients → task) and `.out` (task → clients). |
| 11 | +**A Session is a pair of durable streams.** The input stream (`.in`) carries incoming user messages to your task. The output stream (`.out`) carries everything the agent produces back to your clients: AI generation parts (text, reasoning, tool calls) and any custom data parts you write. |
| 12 | + |
| 13 | +Sessions also **orchestrate the runs that process those streams**. A Session is keyed on your stable id (`externalId` — for chat, the `chatId`) and owns its current run: when a run suspends, idles out, or hands off to a new version, the Session starts or swaps to a fresh run and the streams carry on. Clients keep sending and reading against the same id; they never know a run changed underneath. |
| 14 | + |
| 15 | +```mermaid |
| 16 | +flowchart LR |
| 17 | + C[Browser / backend clients] -- "user messages" --> IN([Session .in]) |
| 18 | + IN --> R["current run<br/>(runs come and go)"] |
| 19 | + R -- "text, reasoning, tool calls,<br/>data parts" --> OUT([Session .out]) |
| 20 | + OUT --> C |
| 21 | +``` |
12 | 22 |
|
13 | 23 | `chat.agent` is built on Sessions. You can also use them directly for any pattern that needs durable bi-directional streaming across runs: long-lived agent inboxes, multi-step approval flows, server-to-server pipelines that survive worker restarts. |
14 | 24 |
|
| 25 | +## A minimal example |
| 26 | + |
| 27 | +A task that echoes whatever lands on its input stream, and a backend that starts the session, sends a message, and reads the reply: |
| 28 | + |
| 29 | +```ts trigger/inbox.ts |
| 30 | +import { task, sessions } from "@trigger.dev/sdk"; |
| 31 | + |
| 32 | +export const inboxAgent = task({ |
| 33 | + id: "inbox-agent", |
| 34 | + run: async (payload: { sessionId: string }) => { |
| 35 | + const session = sessions.open(payload.sessionId); |
| 36 | + |
| 37 | + while (true) { |
| 38 | + // Suspends the run (no compute billed) until a record arrives. |
| 39 | + const next = await session.in.wait<{ text: string }>({ timeout: "1h" }); |
| 40 | + if (!next.ok) return; |
| 41 | + await session.out.append({ type: "reply", text: `echo: ${next.output.text}` }); |
| 42 | + } |
| 43 | + }, |
| 44 | +}); |
| 45 | +``` |
| 46 | + |
| 47 | +```ts Your backend |
| 48 | +import { sessions } from "@trigger.dev/sdk"; |
| 49 | + |
| 50 | +// Atomically create the session AND trigger its first run. |
| 51 | +await sessions.start({ |
| 52 | + type: "inbox", |
| 53 | + externalId: userId, |
| 54 | + taskIdentifier: "inbox-agent", |
| 55 | + triggerConfig: { basePayload: { sessionId: userId } }, |
| 56 | +}); |
| 57 | + |
| 58 | +const session = sessions.open(userId); |
| 59 | +await session.in.send({ text: "hello" }); |
| 60 | + |
| 61 | +const stream = await session.out.read({ signal: AbortSignal.timeout(30_000) }); |
| 62 | +for await (const chunk of stream) { |
| 63 | + console.log(chunk); // { type: "reply", text: "echo: hello" } |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +The run can suspend, crash, or be replaced between the `send` and the `read` — the streams are durable, so nothing is lost and the client code doesn't change. |
| 68 | + |
| 69 | +## Sessions and runs |
| 70 | + |
| 71 | +One Session spans many runs over its lifetime. The Session row tracks `currentRunId`; the runs do the work: |
| 72 | + |
| 73 | +- **First run**: created atomically by `sessions.start` (no gap where the session exists but nothing is listening). |
| 74 | +- **Idle suspend**: a run blocked on `in.wait` suspends and frees compute. A new record on `.in` wakes it. |
| 75 | +- **Continuation**: when a run ends (idle timeout, `chat.endRun`, a crash, a version upgrade), the next incoming record triggers a fresh run against the same Session. The new run picks up the streams where the old one left off. |
| 76 | + |
| 77 | +This is what makes a Session the durable identity for a conversation: runs are an execution detail, the Session (and its `externalId`) is what your clients address. See [How it works](/ai-chat/how-it-works) for how `chat.agent` drives this loop. |
| 78 | + |
15 | 79 | ## When to reach for Sessions directly |
16 | 80 |
|
17 | 81 | `chat.agent` handles 90% of chat-shaped workloads — message accumulation, the turn loop, stop signals, lifecycle hooks. Use the raw `sessions` API when you need any of: |
|
0 commit comments