@trigger.dev/sdk 4.5.0-rc.6 → 4.5.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +171 -5
- package/dist/commonjs/v3/ai.js +309 -22
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +8 -0
- package/dist/commonjs/v3/chat-server.js +32 -10
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat-server.test.js +51 -0
- package/dist/commonjs/v3/chat-server.test.js.map +1 -1
- package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
- package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +3 -2
- package/dist/commonjs/v3/sessions.js +3 -2
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +171 -5
- package/dist/esm/v3/ai.js +309 -22
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +8 -0
- package/dist/esm/v3/chat-server.js +32 -10
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat-server.test.js +51 -0
- package/dist/esm/v3/chat-server.test.js.map +1 -1
- package/dist/esm/v3/createStartSessionAction.test.js +30 -0
- package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +3 -2
- package/dist/esm/v3/sessions.js +3 -2
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/prompts.mdx +430 -0
- package/docs/ai-chat/actions.mdx +115 -0
- package/docs/ai-chat/anatomy.mdx +71 -0
- package/docs/ai-chat/backend.mdx +817 -0
- package/docs/ai-chat/background-injection.mdx +221 -0
- package/docs/ai-chat/changelog.mdx +850 -0
- package/docs/ai-chat/chat-local.mdx +174 -0
- package/docs/ai-chat/client-protocol.mdx +1081 -0
- package/docs/ai-chat/compaction.mdx +411 -0
- package/docs/ai-chat/custom-agents.mdx +364 -0
- package/docs/ai-chat/error-handling.mdx +415 -0
- package/docs/ai-chat/fast-starts.mdx +672 -0
- package/docs/ai-chat/frontend.mdx +580 -0
- package/docs/ai-chat/how-it-works.mdx +230 -0
- package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
- package/docs/ai-chat/mcp.mdx +101 -0
- package/docs/ai-chat/overview.mdx +90 -0
- package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
- package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
- package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
- package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
- package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
- package/docs/ai-chat/patterns/skills.mdx +221 -0
- package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
- package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
- package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
- package/docs/ai-chat/pending-messages.mdx +343 -0
- package/docs/ai-chat/prompt-caching.mdx +206 -0
- package/docs/ai-chat/quick-start.mdx +161 -0
- package/docs/ai-chat/reference.mdx +909 -0
- package/docs/ai-chat/server-chat.mdx +263 -0
- package/docs/ai-chat/sessions.mdx +333 -0
- package/docs/ai-chat/testing.mdx +682 -0
- package/docs/ai-chat/tools.mdx +191 -0
- package/docs/ai-chat/types.mdx +242 -0
- package/docs/ai-chat/upgrade-guide.mdx +515 -0
- package/docs/apikeys.mdx +54 -0
- package/docs/building-with-ai.mdx +261 -0
- package/docs/bulk-actions.mdx +49 -0
- package/docs/changelog.mdx +6 -0
- package/docs/cli-deploy-commands.mdx +9 -0
- package/docs/cli-dev-commands.mdx +9 -0
- package/docs/cli-dev.mdx +8 -0
- package/docs/cli-init-commands.mdx +58 -0
- package/docs/cli-introduction.mdx +25 -0
- package/docs/cli-list-profiles-commands.mdx +42 -0
- package/docs/cli-login-commands.mdx +33 -0
- package/docs/cli-logout-commands.mdx +33 -0
- package/docs/cli-preview-archive.mdx +59 -0
- package/docs/cli-promote-commands.mdx +9 -0
- package/docs/cli-switch.mdx +43 -0
- package/docs/cli-update-commands.mdx +42 -0
- package/docs/cli-whoami-commands.mdx +33 -0
- package/docs/community.mdx +6 -0
- package/docs/config/config-file.mdx +602 -0
- package/docs/config/extensions/additionalFiles.mdx +38 -0
- package/docs/config/extensions/additionalPackages.mdx +40 -0
- package/docs/config/extensions/aptGet.mdx +34 -0
- package/docs/config/extensions/audioWaveform.mdx +20 -0
- package/docs/config/extensions/custom.mdx +380 -0
- package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
- package/docs/config/extensions/esbuildPlugin.mdx +31 -0
- package/docs/config/extensions/ffmpeg.mdx +45 -0
- package/docs/config/extensions/lightpanda.mdx +56 -0
- package/docs/config/extensions/overview.mdx +67 -0
- package/docs/config/extensions/playwright.mdx +195 -0
- package/docs/config/extensions/prismaExtension.mdx +1014 -0
- package/docs/config/extensions/puppeteer.mdx +30 -0
- package/docs/config/extensions/pythonExtension.mdx +182 -0
- package/docs/config/extensions/syncEnvVars.mdx +291 -0
- package/docs/context.mdx +235 -0
- package/docs/database-connections.mdx +213 -0
- package/docs/deploy-environment-variables.mdx +435 -0
- package/docs/deployment/atomic-deployment.mdx +172 -0
- package/docs/deployment/overview.mdx +257 -0
- package/docs/deployment/preview-branches.mdx +224 -0
- package/docs/errors-retrying.mdx +379 -0
- package/docs/github-actions.mdx +222 -0
- package/docs/github-integration.mdx +136 -0
- package/docs/github-repo.mdx +8 -0
- package/docs/help-email.mdx +6 -0
- package/docs/help-slack.mdx +11 -0
- package/docs/hidden-tasks.mdx +56 -0
- package/docs/how-it-works.mdx +454 -0
- package/docs/how-to-reduce-your-spend.mdx +217 -0
- package/docs/idempotency.mdx +504 -0
- package/docs/introduction.mdx +223 -0
- package/docs/limits.mdx +241 -0
- package/docs/logging.mdx +195 -0
- package/docs/machines.mdx +952 -0
- package/docs/manual-setup.mdx +632 -0
- package/docs/mcp-agent-rules.mdx +41 -0
- package/docs/mcp-introduction.mdx +385 -0
- package/docs/mcp-tools.mdx +273 -0
- package/docs/migrating-from-v3.mdx +334 -0
- package/docs/observability/dashboards.mdx +102 -0
- package/docs/observability/query.mdx +585 -0
- package/docs/open-source-contributing.mdx +16 -0
- package/docs/open-source-self-hosting.mdx +541 -0
- package/docs/private-networking/aws-console-setup.mdx +304 -0
- package/docs/private-networking/overview.mdx +144 -0
- package/docs/private-networking/troubleshooting.mdx +78 -0
- package/docs/queue-concurrency.mdx +354 -0
- package/docs/quick-start.mdx +97 -0
- package/docs/realtime/auth.mdx +208 -0
- package/docs/realtime/backend/overview.mdx +45 -0
- package/docs/realtime/backend/streams.mdx +418 -0
- package/docs/realtime/backend/subscribe.mdx +225 -0
- package/docs/realtime/how-it-works.mdx +94 -0
- package/docs/realtime/overview.mdx +63 -0
- package/docs/realtime/react-hooks/overview.mdx +73 -0
- package/docs/realtime/react-hooks/streams.mdx +449 -0
- package/docs/realtime/react-hooks/subscribe.mdx +674 -0
- package/docs/realtime/react-hooks/swr.mdx +87 -0
- package/docs/realtime/react-hooks/triggering.mdx +194 -0
- package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
- package/docs/realtime/run-object.mdx +174 -0
- package/docs/replaying.mdx +72 -0
- package/docs/request-feature.mdx +6 -0
- package/docs/roadmap.mdx +6 -0
- package/docs/run-tests.mdx +20 -0
- package/docs/run-usage.mdx +113 -0
- package/docs/runs/heartbeats.mdx +38 -0
- package/docs/runs/max-duration.mdx +139 -0
- package/docs/runs/metadata.mdx +734 -0
- package/docs/runs/priority.mdx +31 -0
- package/docs/runs.mdx +396 -0
- package/docs/self-hosting/docker.mdx +458 -0
- package/docs/self-hosting/env/supervisor.mdx +74 -0
- package/docs/self-hosting/env/webapp.mdx +276 -0
- package/docs/self-hosting/kubernetes.mdx +601 -0
- package/docs/self-hosting/overview.mdx +108 -0
- package/docs/skills.mdx +85 -0
- package/docs/tags.mdx +120 -0
- package/docs/tasks/overview.mdx +697 -0
- package/docs/tasks/scheduled.mdx +382 -0
- package/docs/tasks/schemaTask.mdx +413 -0
- package/docs/tasks/streams.mdx +884 -0
- package/docs/triggering.mdx +1320 -0
- package/docs/troubleshooting-alerts.mdx +385 -0
- package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
- package/docs/troubleshooting-github-issues.mdx +6 -0
- package/docs/troubleshooting-uptime-status.mdx +6 -0
- package/docs/troubleshooting.mdx +398 -0
- package/docs/upgrading-packages.mdx +80 -0
- package/docs/vercel-integration.mdx +207 -0
- package/docs/versioning.mdx +56 -0
- package/docs/video-walkthrough.mdx +23 -0
- package/docs/wait-for-token.mdx +540 -0
- package/docs/wait-for.mdx +42 -0
- package/docs/wait-until.mdx +53 -0
- package/docs/wait.mdx +18 -0
- package/docs/writing-tasks-introduction.mdx +33 -0
- package/package.json +8 -5
- package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
- package/skills/trigger-authoring-tasks/SKILL.md +254 -0
- package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
- package/skills/trigger-cost-savings/SKILL.md +116 -0
- package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Lifecycle hooks"
|
|
3
|
+
sidebarTitle: "Lifecycle hooks"
|
|
4
|
+
description: "Hook into every stage of a chat agent's run: preload, turn start, turn complete, suspend, resume, and more."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
|
|
8
|
+
|
|
9
|
+
<RcBanner />
|
|
10
|
+
|
|
11
|
+
`chat.agent({ ... })` accepts a set of lifecycle hooks for persisting state, validating input, transforming messages, and reacting to suspension and resumption. They fire at well-defined points in the chat agent's lifetime.
|
|
12
|
+
|
|
13
|
+
**Once per worker process (every fresh run boot):** `onBoot` → `onPreload` (preloaded runs only).
|
|
14
|
+
|
|
15
|
+
**Once per chat (first message of the chat's lifetime):** `onChatStart`.
|
|
16
|
+
|
|
17
|
+
**Per-turn order:** `onValidateMessages` → `hydrateMessages` → `onChatStart` (chat's first message only) → `onTurnStart` → `run()` → `onBeforeTurnComplete` → `onTurnComplete`.
|
|
18
|
+
|
|
19
|
+
**Suspend / resume:** `onChatSuspend` fires when the run transitions from idle to suspended (waiting on the next message); `onChatResume` fires on wake.
|
|
20
|
+
|
|
21
|
+
**Four scopes to keep straight:**
|
|
22
|
+
|
|
23
|
+
| Scope | Fires when | Use for |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| **Process** ([`onBoot`](#onboot)) | Every fresh worker boots — initial, preloaded, and reactive continuation (post-cancel/crash/`endRun`/upgrade). | Initialize `chat.local`, open per-process resources, re-hydrate state from your DB on continuation. |
|
|
26
|
+
| **Recovery** ([`onRecoveryBoot`](#onrecoveryboot)) | Continuation boot where the dead run was mid-stream — a partial assistant survives on `session.out`. | Override the smart default — drop the partial, synthesize tool results, emit a recovery banner. |
|
|
27
|
+
| **Chat** ([`onChatStart`](#onchatstart)) | First message of a chat's lifetime. Does NOT fire on continuation runs or OOM retries. | One-time DB rows for the chat, resources tied to the chat's lifetime. |
|
|
28
|
+
| **Turn** ([`onTurnStart`](#onturnstart), [`onTurnComplete`](#onturncomplete), etc.) | Every turn. | Persist messages, post-process responses. |
|
|
29
|
+
|
|
30
|
+
## Task context (`ctx`)
|
|
31
|
+
|
|
32
|
+
Every chat lifecycle callback and the `run` payload include `ctx`: the same run context object as `task({ run: (payload, { ctx }) => ... })`. Import the type with `import type { TaskRunContext } from "@trigger.dev/sdk"` (the `Context` export is the same type). Use `ctx` for tags, metadata, or any API that needs the full run record. The string `runId` on chat events is always `ctx.run.id` (both are provided for convenience). See [Task context (`ctx`)](/ai-chat/reference#task-context-ctx) in the API reference.
|
|
33
|
+
|
|
34
|
+
Standard [task lifecycle hooks](/tasks/overview) such as `onWait`, `onResume`, `onComplete`, and `onFailure` are also available on `chat.agent()` with the same shapes as on a normal `task()` — but prefer the chat-specific [`onChatSuspend` / `onChatResume`](#onchatsuspend--onchatresume) for any chat-related work. The generic hooks fire on every wait/resume (including ones the runtime uses internally for non-chat reasons); the chat-specific ones fire only at the idle-to-suspended transition you actually care about and carry full chat context.
|
|
35
|
+
|
|
36
|
+
## onBoot
|
|
37
|
+
|
|
38
|
+
Fires **once per worker process picking up the chat** — for the initial run, for preloaded runs, AND for reactive continuation runs (post-cancel, crash, `endRun`, `requestUpgrade`, OOM retry). Does NOT fire when the same run resumes from snapshot via the idle-window suspend/resume path — use [`onChatResume`](#onchatsuspend--onchatresume) for that.
|
|
39
|
+
|
|
40
|
+
This is the right place to initialize anything that lives in the JS process for the lifetime of the run: [`chat.local`](/ai-chat/chat-local) state, [DB connections](/database-connections), sandboxes, in-memory caches. It runs before `onPreload`, `onChatStart`, the continuation-wait branch, and any turn — so anything you set up here is available everywhere downstream.
|
|
41
|
+
|
|
42
|
+
<Warning>
|
|
43
|
+
If you initialize `chat.local` only in `onChatStart`, your `run()` will crash on continuation runs with `chat.local can only be modified after initialization`. `onChatStart` is once-per-chat by contract; `chat.local` is per-process and needs `onBoot`.
|
|
44
|
+
</Warning>
|
|
45
|
+
|
|
46
|
+
Branch on `continuation` to decide whether to load existing state from your DB or start fresh:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export const myChat = chat.agent({
|
|
50
|
+
id: "my-chat",
|
|
51
|
+
clientDataSchema: z.object({ userId: z.string() }),
|
|
52
|
+
onBoot: async ({ chatId, clientData, continuation, previousRunId }) => {
|
|
53
|
+
const user = await db.user.findUnique({ where: { id: clientData.userId } });
|
|
54
|
+
userContext.init({ name: user.name, plan: user.plan });
|
|
55
|
+
|
|
56
|
+
if (continuation) {
|
|
57
|
+
// Re-hydrate per-chat in-memory state from your DB.
|
|
58
|
+
// `previousRunId` is the public id of the prior run (use it for
|
|
59
|
+
// logging or to look up persisted state keyed on run id).
|
|
60
|
+
const saved = await db.chatState.findUnique({ where: { chatId } });
|
|
61
|
+
if (saved) {
|
|
62
|
+
// Re-apply your saved per-chat state into wherever your
|
|
63
|
+
// run() reads it from (a chat.local slot, an in-memory map, etc.).
|
|
64
|
+
userContext.applySaved(saved);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
run: async ({ messages, signal }) => {
|
|
69
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Field | Type | Description |
|
|
75
|
+
| ----------------- | ----------------------------- | --------------------------------------------------------------------------- |
|
|
76
|
+
| `ctx` | `TaskRunContext` | Full task run context. See [reference](/ai-chat/reference#task-context-ctx). |
|
|
77
|
+
| `chatId` | `string` | Chat session ID |
|
|
78
|
+
| `runId` | `string` | The Trigger.dev run ID for this run boot |
|
|
79
|
+
| `chatAccessToken` | `string` | Scoped access token for this run |
|
|
80
|
+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
|
|
81
|
+
| `continuation` | `boolean` | `true` when this run is taking over from a prior dead run |
|
|
82
|
+
| `previousRunId` | `string \| undefined` | Public id of the prior run when `continuation` is true |
|
|
83
|
+
| `preloaded` | `boolean` | Whether this run was triggered as a preload |
|
|
84
|
+
|
|
85
|
+
<Tip>
|
|
86
|
+
`onBoot` and `onChatStart` are complementary — keep DB-row creation in `onChatStart` (it only needs to happen once per chat) and put process-level setup (`chat.local`, connections, caches) in `onBoot` (it needs to happen on every fresh worker).
|
|
87
|
+
</Tip>
|
|
88
|
+
|
|
89
|
+
## onRecoveryBoot
|
|
90
|
+
|
|
91
|
+
Fires once on a continuation boot when the dead predecessor was mid-stream — a partial assistant survives on `session.out`. The runtime reconstructs context automatically via a smart default; this hook is the override path for policies that need something different.
|
|
92
|
+
|
|
93
|
+
The hook does NOT fire when there's no partial — clean continuations after `chat.endRun()` or `chat.requestUpgrade()`, fresh chats, OOM retries on top of a complete snapshot. Those paths dispatch any in-flight user message as a normal turn on the new run without involving the hook. It also does NOT fire when [`hydrateMessages`](#hydratemessages) is registered (the customer owns persistence).
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
export const myChat = chat.agent({
|
|
97
|
+
id: "my-chat",
|
|
98
|
+
onRecoveryBoot: async ({ partialAssistant, inFlightUsers, writer, cause, previousRunId }) => {
|
|
99
|
+
writer.write({
|
|
100
|
+
type: "data-chat-recovery",
|
|
101
|
+
data: { cause, previousRunId, partialPresent: partialAssistant !== undefined },
|
|
102
|
+
transient: true,
|
|
103
|
+
});
|
|
104
|
+
// Return nothing → fall through to the smart default
|
|
105
|
+
// (splice partial + first user into chain, dispatch the rest).
|
|
106
|
+
},
|
|
107
|
+
run: async ({ messages, signal }) =>
|
|
108
|
+
streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Field | Type | Description |
|
|
113
|
+
| ------------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
114
|
+
| `ctx` | `TaskRunContext` | Full task run context |
|
|
115
|
+
| `chatId` | `string` | Chat session ID |
|
|
116
|
+
| `runId` | `string` | The Trigger.dev run ID for this run boot |
|
|
117
|
+
| `previousRunId` | `string` | Public id of the prior run that died |
|
|
118
|
+
| `cause` | `"cancelled" \| "crashed" \| "unknown"` | Best-effort cause. Currently always `"unknown"` — don't branch on it |
|
|
119
|
+
| `settledMessages` | `TUIMessage[]` | The chain persisted by the predecessor's last `onTurnComplete` |
|
|
120
|
+
| `inFlightUsers` | `TUIMessage[]` | User messages on `session.in` past the cursor — the message(s) the predecessor never acknowledged |
|
|
121
|
+
| `partialAssistant` | `TUIMessage \| undefined` | The trailing assistant message whose stream never received `finish` |
|
|
122
|
+
| `pendingToolCalls` | `Array<{ toolCallId, toolName, input, partIndex }>` | Tool calls in `input-available` state extracted from `partialAssistant` |
|
|
123
|
+
| `writer` | `ChatWriter` | Lazy session.out writer — write a recovery banner / signal here |
|
|
124
|
+
|
|
125
|
+
Returns `{ chain?, recoveredTurns?, beforeBoot? }` — every field optional. Omitted fields fall through to the smart default. See [Recovery boot](/ai-chat/patterns/recovery-boot) for the full guide, examples (drop partial, synthesize tool results, persist before boot), and interaction notes.
|
|
126
|
+
|
|
127
|
+
<Tip>
|
|
128
|
+
Don't put `chat.local` initialization in `onRecoveryBoot` — use [`onBoot`](#onboot). `onRecoveryBoot` is for recovery decisions, not per-process setup. `onBoot` fires first.
|
|
129
|
+
</Tip>
|
|
130
|
+
|
|
131
|
+
## onPreload
|
|
132
|
+
|
|
133
|
+
Fires when a **preloaded run** starts, before any messages arrive. Use it to eagerly create chat-scoped DB rows (the Chat row, the ChatSession row) while the user is still typing — so the very first message lands fast.
|
|
134
|
+
|
|
135
|
+
Preloaded runs are triggered by calling `transport.preload(chatId)` on the frontend. See [Preload](/ai-chat/fast-starts#preload) for details.
|
|
136
|
+
|
|
137
|
+
Per-process state (anything in [`chat.local`](/ai-chat/chat-local), DB connections, etc.) belongs in [`onBoot`](#onboot) — `onBoot` fires before `onPreload` on every fresh worker, including on continuation runs where `onPreload` never fires.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
export const myChat = chat.agent({
|
|
141
|
+
id: "my-chat",
|
|
142
|
+
clientDataSchema: z.object({ userId: z.string() }),
|
|
143
|
+
onBoot: async ({ clientData }) => {
|
|
144
|
+
// Per-process state — runs on every fresh worker (initial,
|
|
145
|
+
// preloaded, continuation). See onBoot above.
|
|
146
|
+
const user = await db.user.findUnique({ where: { id: clientData.userId } });
|
|
147
|
+
userContext.init({ name: user.name, plan: user.plan });
|
|
148
|
+
},
|
|
149
|
+
onPreload: async ({ chatId, clientData, runId, chatAccessToken }) => {
|
|
150
|
+
// Chat-scoped DB rows — only matters on preload (and onChatStart as
|
|
151
|
+
// a fallback when not preloaded).
|
|
152
|
+
await db.chat.create({ data: { id: chatId, userId: clientData.userId } });
|
|
153
|
+
await db.chatSession.upsert({
|
|
154
|
+
where: { id: chatId },
|
|
155
|
+
create: { id: chatId, runId, publicAccessToken: chatAccessToken },
|
|
156
|
+
update: { runId, publicAccessToken: chatAccessToken },
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
onChatStart: async ({ preloaded }) => {
|
|
160
|
+
if (preloaded) return; // Already initialized in onPreload
|
|
161
|
+
// ... non-preloaded chat-row initialization
|
|
162
|
+
},
|
|
163
|
+
run: async ({ messages, signal }) => {
|
|
164
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| Field | Type | Description |
|
|
170
|
+
| ----------------- | --------------------------------------------- | -------------------------------- |
|
|
171
|
+
| `ctx` | `TaskRunContext` | Full task run context. See [reference](/ai-chat/reference#task-context-ctx). |
|
|
172
|
+
| `chatId` | `string` | Chat session ID |
|
|
173
|
+
| `runId` | `string` | The Trigger.dev run ID |
|
|
174
|
+
| `chatAccessToken` | `string` | Scoped access token for this run |
|
|
175
|
+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
|
|
176
|
+
| `writer` | [`ChatWriter`](/ai-chat/reference#chatwriter) | Stream writer for custom chunks |
|
|
177
|
+
|
|
178
|
+
Every lifecycle callback receives a `writer`, a lazy stream writer that lets you send custom `UIMessageChunk` parts (like `data-*` parts) to the frontend. Non-transient `data-*` chunks written via the `writer` are automatically added to the response message and available in `onTurnComplete`. Add `transient: true` for ephemeral chunks (progress indicators, etc.) that should not persist. See [Custom data parts](/ai-chat/backend#custom-data-parts).
|
|
179
|
+
|
|
180
|
+
## onChatStart
|
|
181
|
+
|
|
182
|
+
Fires **exactly once per chat**, on the very first user message of the chat's lifetime, before `run()` executes. Use it for one-time chat-scoped setup — create the Chat DB row, mint resources tied to the chat's lifetime.
|
|
183
|
+
|
|
184
|
+
`onChatStart` does **not** fire on:
|
|
185
|
+
|
|
186
|
+
- **Continuation runs** — a new run picking up an existing session after the prior run ended (`chat.endRun`, waitpoint timeout, `chat.requestUpgrade`, cancel, crash). The chat already started.
|
|
187
|
+
- **OOM-retry attempts** — same chat, same conversation, just on a larger machine.
|
|
188
|
+
|
|
189
|
+
For per-process state that has to be initialized on every fresh worker (including continuation runs), use [`onBoot`](#onboot). For per-turn setup, use [`onTurnStart`](#onturnstart).
|
|
190
|
+
|
|
191
|
+
<Warning>
|
|
192
|
+
Do not initialize [`chat.local`](/ai-chat/chat-local) here. `chat.local` is per-process state that must survive continuation runs, but `onChatStart` only fires on the chat's very first message. Use [`onBoot`](#onboot) instead.
|
|
193
|
+
</Warning>
|
|
194
|
+
|
|
195
|
+
The `preloaded` field tells you whether [`onPreload`](#onpreload) already ran for this chat — useful for skipping setup work that's already done.
|
|
196
|
+
|
|
197
|
+
<Note>
|
|
198
|
+
Because `onChatStart` fires only on the chat's first ever message, `messages` is either empty (when no message exists yet — e.g. a preloaded run that hasn't received its first turn) or contains just the first user message. There's no prior history to load here.
|
|
199
|
+
</Note>
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
export const myChat = chat.agent({
|
|
203
|
+
id: "my-chat",
|
|
204
|
+
onChatStart: async ({ chatId, clientData, preloaded }) => {
|
|
205
|
+
if (preloaded) return; // Already set up in onPreload
|
|
206
|
+
|
|
207
|
+
const { userId } = clientData as { userId: string };
|
|
208
|
+
await db.chat.create({
|
|
209
|
+
data: { id: chatId, userId, title: "New chat" },
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
run: async ({ messages, signal }) => {
|
|
213
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
<Tip>
|
|
219
|
+
`clientData` contains custom data from the frontend: either the `clientData` option on the
|
|
220
|
+
transport constructor (sent with every message) or the `metadata` option on `sendMessage()`
|
|
221
|
+
(per-message). See [Client data and metadata](/ai-chat/frontend#client-data-and-metadata).
|
|
222
|
+
</Tip>
|
|
223
|
+
|
|
224
|
+
## onValidateMessages
|
|
225
|
+
|
|
226
|
+
Validate or transform incoming `UIMessage[]` before they are converted to model messages. Fires on turns that carry incoming messages, with the raw messages from the wire payload (after cleanup of aborted tool parts), **before** accumulation and `toModelMessages()`. Turns with no incoming messages — preload, close, and regenerate with nothing re-sent — skip it.
|
|
227
|
+
|
|
228
|
+
Return the validated messages array. Throw to abort the turn with an error.
|
|
229
|
+
|
|
230
|
+
This is the right place to call the AI SDK's [`validateUIMessages`](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-message-persistence#validating-messages-on-the-server) to catch malformed messages from storage or untrusted input before they reach the model, especially useful when persisting conversations to a database where tool schemas may drift between deploys.
|
|
231
|
+
|
|
232
|
+
| Field | Type | Description |
|
|
233
|
+
| --------- | --------------------------------------------------------------- | ---------------------------------------- |
|
|
234
|
+
| `messages` | `UIMessage[]` | Incoming UI messages for this turn |
|
|
235
|
+
| `chatId` | `string` | Chat session ID |
|
|
236
|
+
| `turn` | `number` | Turn number (0-indexed) |
|
|
237
|
+
| `trigger` | `"submit-message" \| "regenerate-message" \| "preload" \| "close"` | The trigger type for this turn |
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { validateUIMessages } from "ai";
|
|
241
|
+
|
|
242
|
+
export const myChat = chat.agent({
|
|
243
|
+
id: "my-chat",
|
|
244
|
+
onValidateMessages: async ({ messages }) => {
|
|
245
|
+
const userMessages = messages.filter((m) => m.role === "user");
|
|
246
|
+
if (userMessages.length > 0) {
|
|
247
|
+
await validateUIMessages({ messages: userMessages, tools: chatTools });
|
|
248
|
+
}
|
|
249
|
+
return messages;
|
|
250
|
+
},
|
|
251
|
+
run: async ({ messages, signal }) => {
|
|
252
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, tools: chatTools, abortSignal: signal });
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
<Warning>
|
|
258
|
+
On HITL continuations (`addToolOutput` / `addToolApproveResponse`) the assistant entry in `messages` is **slim** — `state` + `output` / `errorText` / `approval` only, no `input` or other parts. `validateUIMessages` against the AI SDK schema rejects that shape (the schema requires `input` on resolved tool parts), so filter to user messages first (or skip validation entirely on those turns). The example above does the filter.
|
|
259
|
+
</Warning>
|
|
260
|
+
|
|
261
|
+
<Note>
|
|
262
|
+
`onValidateMessages` fires **before** `onTurnStart` and message accumulation. If you need to validate messages loaded from a database, do the loading in `onChatStart` or `onPreload` and let `onValidateMessages` validate the full incoming set each turn.
|
|
263
|
+
</Note>
|
|
264
|
+
|
|
265
|
+
## hydrateMessages
|
|
266
|
+
|
|
267
|
+
Load the full message history from your backend on every turn, replacing the built-in linear accumulator. When set, the hook's return value becomes the accumulated state; the normal accumulation logic (append for submit, replace for regenerate) is skipped entirely.
|
|
268
|
+
|
|
269
|
+
Use this when the backend should be the source of truth for message history: abuse prevention, branching conversations (DAGs), or rollback/undo support.
|
|
270
|
+
|
|
271
|
+
| Field | Type | Description |
|
|
272
|
+
| ------------------ | ----------------------------------------------------- | --------------------------------------------------------- |
|
|
273
|
+
| `chatId` | `string` | Chat session ID |
|
|
274
|
+
| `turn` | `number` | Turn number (0-indexed) |
|
|
275
|
+
| `trigger` | `"submit-message" \| "regenerate-message" \| "action"` | The trigger type for this turn |
|
|
276
|
+
| `incomingMessages` | `UIMessage[]` | Validated incoming messages for this turn. Usually 0-or-1 (empty for actions, regenerates, and continuations; one element for normal `submit-message` and tool-approval responses). On a [Head Start](/ai-chat/fast-starts#with-hydratemessages) first turn, this can contain the route handler's first-turn history. |
|
|
277
|
+
| `previousMessages` | `UIMessage[]` | Accumulated UI messages before this turn (`[]` on turn 0) |
|
|
278
|
+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
|
|
279
|
+
| `continuation` | `boolean` | Whether this run is continuing an existing chat |
|
|
280
|
+
| `previousRunId` | `string \| undefined` | The previous run ID (if continuation) |
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { chat, upsertIncomingMessage } from "@trigger.dev/sdk/ai";
|
|
284
|
+
|
|
285
|
+
export const myChat = chat.agent({
|
|
286
|
+
id: "my-chat",
|
|
287
|
+
hydrateMessages: async ({ chatId, trigger, incomingMessages }) => {
|
|
288
|
+
const record = await db.chat.findUnique({ where: { id: chatId } });
|
|
289
|
+
const stored = record?.messages ?? [];
|
|
290
|
+
|
|
291
|
+
if (upsertIncomingMessage(stored, { trigger, incomingMessages })) {
|
|
292
|
+
// Upsert, not update: on a head-start first turn no preload ran,
|
|
293
|
+
// so the row may not exist yet when this hook fires.
|
|
294
|
+
await db.chat.upsert({
|
|
295
|
+
where: { id: chatId },
|
|
296
|
+
create: { id: chatId, messages: stored },
|
|
297
|
+
update: { messages: stored },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return stored;
|
|
302
|
+
},
|
|
303
|
+
run: async ({ messages, signal }) => {
|
|
304
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`upsertIncomingMessage` (exported from `@trigger.dev/sdk/ai`) handles the three cases that matter — fresh user messages get pushed, HITL continuations (`addToolOutput` / `addToolApproveResponse`) no-op because the incoming wire shares the existing assistant's id and the runtime overlays the new tool-state advance onto that entry, and non-`submit-message` triggers (`regenerate-message` / `action`) skip persistence. It returns `true` when it mutated `stored`, so the caller knows whether to persist.
|
|
310
|
+
|
|
311
|
+
If you need branching, rollback, or other custom hydrate logic, you can still write the upsert by hand — `upsertIncomingMessage` is a convenience for the common case, not the only supported shape.
|
|
312
|
+
|
|
313
|
+
**Lifecycle position:** `onValidateMessages` → **`hydrateMessages`** → `onChatStart` (chat's first message only) → `onTurnStart` → `run()`
|
|
314
|
+
|
|
315
|
+
After the hook returns, the runtime overlays the wire's tool-state advances (`output-available` / `output-error` / `approval-responded` / `output-denied`) onto matching hydrated entries by id. Everything else on the hydrated entry — text, reasoning, tool `input`, providerMetadata — stays put. This makes [tool approvals](/ai-chat/frontend#tool-approvals) and HITL `addToolOutput` continuations work transparently: ship a slim resolution on the wire, the agent merges the new state onto your DB-backed copy.
|
|
316
|
+
|
|
317
|
+
<Note>
|
|
318
|
+
`hydrateMessages` also fires for [action](/ai-chat/actions) turns (`trigger: "action"`) with empty `incomingMessages`. This lets the action handler work with the latest DB state.
|
|
319
|
+
</Note>
|
|
320
|
+
|
|
321
|
+
<Tip>
|
|
322
|
+
Registering `hydrateMessages` short-circuits the runtime's [snapshot + replay](/ai-chat/patterns/persistence-and-replay) reconstruction at run boot — your hook is the single source of truth for history, so the runtime skips reading or writing the snapshot entirely. No object storage traffic, no replay cost. The trade-off is that you own persistence end-to-end.
|
|
323
|
+
</Tip>
|
|
324
|
+
|
|
325
|
+
<Note>
|
|
326
|
+
`incomingMessages` is **usually 0-or-1-length**. `submit-message` and tool-approval responses ship a single message; `regenerate-message`, continuations, and actions ship none. The exception is a [Head Start](/ai-chat/fast-starts#with-hydratemessages) first turn, where it carries the route handler's first-turn history. Patterns like [tool-result auditing](/ai-chat/patterns/tool-result-auditing) work the same regardless — iterate the array rather than assuming a single element.
|
|
327
|
+
</Note>
|
|
328
|
+
|
|
329
|
+
## onTurnStart
|
|
330
|
+
|
|
331
|
+
Fires at the start of **every turn** — including the first turn of a continuation run, where `onChatStart` doesn't fire. Runs after message accumulation and (when applicable) `onChatStart`, but **before** `run()` executes. Use it to persist messages before streaming begins so a mid-stream page refresh still shows the user's message.
|
|
332
|
+
|
|
333
|
+
| Field | Type | Description |
|
|
334
|
+
| ----------------- | --------------------------------------------- | ----------------------------------------------- |
|
|
335
|
+
| `ctx` | `TaskRunContext` | Full task run context. See [reference](/ai-chat/reference#task-context-ctx). |
|
|
336
|
+
| `chatId` | `string` | Chat session ID |
|
|
337
|
+
| `messages` | `ModelMessage[]` | Full accumulated conversation (model format) |
|
|
338
|
+
| `uiMessages` | `UIMessage[]` | Full accumulated conversation (UI format) |
|
|
339
|
+
| `turn` | `number` | Turn number (0-indexed) |
|
|
340
|
+
| `runId` | `string` | The Trigger.dev run ID |
|
|
341
|
+
| `chatAccessToken` | `string` | Scoped access token for this run |
|
|
342
|
+
| `continuation` | `boolean` | Whether this run is continuing an existing chat |
|
|
343
|
+
| `preloaded` | `boolean` | Whether this run was preloaded |
|
|
344
|
+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
|
|
345
|
+
| `writer` | [`ChatWriter`](/ai-chat/reference#chatwriter) | Stream writer for custom chunks |
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
export const myChat = chat.agent({
|
|
349
|
+
id: "my-chat",
|
|
350
|
+
onTurnStart: async ({ chatId, uiMessages, runId, chatAccessToken }) => {
|
|
351
|
+
await db.chat.update({
|
|
352
|
+
where: { id: chatId },
|
|
353
|
+
data: { messages: uiMessages },
|
|
354
|
+
});
|
|
355
|
+
await db.chatSession.upsert({
|
|
356
|
+
where: { id: chatId },
|
|
357
|
+
create: { id: chatId, runId, publicAccessToken: chatAccessToken },
|
|
358
|
+
update: { runId, publicAccessToken: chatAccessToken },
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
run: async ({ messages, signal }) => {
|
|
362
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
<Tip>
|
|
368
|
+
By persisting in `onTurnStart`, the user's message is saved to your database before the AI starts
|
|
369
|
+
streaming. If the user refreshes mid-stream, the message is already there.
|
|
370
|
+
</Tip>
|
|
371
|
+
|
|
372
|
+
## onBeforeTurnComplete
|
|
373
|
+
|
|
374
|
+
Fires after the response is captured but **before** the stream closes. The `writer` can send custom chunks that appear in the current turn. Use this for post-processing indicators, compaction progress, or any data the user should see before the turn ends.
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
export const myChat = chat.agent({
|
|
378
|
+
id: "my-chat",
|
|
379
|
+
onBeforeTurnComplete: async ({ writer, usage, uiMessages }) => {
|
|
380
|
+
// Write a custom data part while the stream is still open
|
|
381
|
+
writer.write({
|
|
382
|
+
type: "data-usage-summary",
|
|
383
|
+
data: {
|
|
384
|
+
tokens: usage?.totalTokens,
|
|
385
|
+
messageCount: uiMessages.length,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// You can also compact messages here and write progress
|
|
390
|
+
if (usage?.totalTokens && usage.totalTokens > 50_000) {
|
|
391
|
+
writer.write({ type: "data-compaction", data: { status: "compacting" } });
|
|
392
|
+
chat.setMessages(compactedMessages);
|
|
393
|
+
writer.write({ type: "data-compaction", data: { status: "complete" } });
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
run: async ({ messages, signal }) => {
|
|
397
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Receives the same fields as [`TurnCompleteEvent`](/ai-chat/reference#turncompleteevent), plus a [`writer`](/ai-chat/reference#chatwriter).
|
|
403
|
+
|
|
404
|
+
## onTurnComplete
|
|
405
|
+
|
|
406
|
+
Fires after each turn completes, after the response is captured and the stream is closed. This is the primary hook for persisting the assistant's response. Does not include a `writer` since the stream is already closed.
|
|
407
|
+
|
|
408
|
+
| Field | Type | Description |
|
|
409
|
+
| -------------------- | ------------------------ | -------------------------------------------------------------------------------------------- |
|
|
410
|
+
| `ctx` | `TaskRunContext` | Full task run context. See [reference](/ai-chat/reference#task-context-ctx). |
|
|
411
|
+
| `chatId` | `string` | Chat session ID |
|
|
412
|
+
| `messages` | `ModelMessage[]` | Full accumulated conversation (model format) |
|
|
413
|
+
| `uiMessages` | `UIMessage[]` | Full accumulated conversation (UI format) |
|
|
414
|
+
| `newMessages` | `ModelMessage[]` | Only this turn's messages (model format) |
|
|
415
|
+
| `newUIMessages` | `UIMessage[]` | Only this turn's messages (UI format) |
|
|
416
|
+
| `responseMessage` | `UIMessage \| undefined` | The assistant's response for this turn |
|
|
417
|
+
| `turn` | `number` | Turn number (0-indexed) |
|
|
418
|
+
| `runId` | `string` | The Trigger.dev run ID |
|
|
419
|
+
| `chatAccessToken` | `string` | Scoped access token for this run |
|
|
420
|
+
| `lastEventId` | `string \| undefined` | Stream position for resumption. Persist this with the session. |
|
|
421
|
+
| `stopped` | `boolean` | Whether the user stopped generation during this turn |
|
|
422
|
+
| `continuation` | `boolean` | Whether this run is continuing an existing chat |
|
|
423
|
+
| `rawResponseMessage` | `UIMessage \| undefined` | The raw assistant response before abort cleanup (same as `responseMessage` when not stopped) |
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
export const myChat = chat.agent({
|
|
427
|
+
id: "my-chat",
|
|
428
|
+
onTurnComplete: async ({ chatId, uiMessages, runId, chatAccessToken, lastEventId }) => {
|
|
429
|
+
// Atomic write — see Database persistence for the race-condition rationale
|
|
430
|
+
await db.$transaction([
|
|
431
|
+
db.chat.update({
|
|
432
|
+
where: { id: chatId },
|
|
433
|
+
data: { messages: uiMessages },
|
|
434
|
+
}),
|
|
435
|
+
db.chatSession.upsert({
|
|
436
|
+
where: { id: chatId },
|
|
437
|
+
create: { id: chatId, runId, publicAccessToken: chatAccessToken, lastEventId },
|
|
438
|
+
update: { runId, publicAccessToken: chatAccessToken, lastEventId },
|
|
439
|
+
}),
|
|
440
|
+
]);
|
|
441
|
+
},
|
|
442
|
+
run: async ({ messages, signal }) => {
|
|
443
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
<Tip>
|
|
449
|
+
Use `uiMessages` to overwrite the full conversation each turn (simplest). Use `newUIMessages` if
|
|
450
|
+
you prefer to store messages individually, e.g. one database row per message.
|
|
451
|
+
</Tip>
|
|
452
|
+
|
|
453
|
+
<Tip>
|
|
454
|
+
Persist `lastEventId` alongside the session. When the transport reconnects after a page refresh,
|
|
455
|
+
it uses this to skip past already-seen events, preventing duplicate messages.
|
|
456
|
+
</Tip>
|
|
457
|
+
|
|
458
|
+
<Tip>
|
|
459
|
+
For a full **conversation + session** persistence pattern (including preload, continuation, and token renewal), see [Database persistence](/ai-chat/patterns/database-persistence).
|
|
460
|
+
</Tip>
|
|
461
|
+
|
|
462
|
+
## onChatSuspend / onChatResume
|
|
463
|
+
|
|
464
|
+
Chat-specific hooks that fire at the **idle-to-suspended** transition: the moment the run stops using compute and waits for the next message. These replace the need for the generic `onWait` / `onResume` task hooks for chat-specific work.
|
|
465
|
+
|
|
466
|
+
The `phase` discriminator tells you **when** the suspend/resume happened:
|
|
467
|
+
|
|
468
|
+
- `"preload"`: after `onPreload`, waiting for the first message
|
|
469
|
+
- `"turn"`: after `onTurnComplete`, waiting for the next message
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
export const myChat = chat.agent({
|
|
473
|
+
id: "my-chat",
|
|
474
|
+
onChatSuspend: async (event) => {
|
|
475
|
+
// Tear down expensive resources before suspending
|
|
476
|
+
await disposeCodeSandbox(event.ctx.run.id);
|
|
477
|
+
if (event.phase === "turn") {
|
|
478
|
+
logger.info("Suspending after turn", { turn: event.turn });
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
onChatResume: async (event) => {
|
|
482
|
+
// Re-initialize after waking up
|
|
483
|
+
logger.info("Resumed", { phase: event.phase });
|
|
484
|
+
},
|
|
485
|
+
run: async ({ messages, signal }) => {
|
|
486
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
| Field | Type | Description |
|
|
492
|
+
| ------------ | ---------------- | ------------------------------------------------------------ |
|
|
493
|
+
| `phase` | `"preload" \| "turn"` | Whether this is a preload or post-turn suspension |
|
|
494
|
+
| `ctx` | `TaskRunContext` | Full task run context |
|
|
495
|
+
| `chatId` | `string` | Chat session ID |
|
|
496
|
+
| `runId` | `string` | The Trigger.dev run ID |
|
|
497
|
+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
|
|
498
|
+
| `turn` | `number` | Turn number (**`"turn"` phase only**) |
|
|
499
|
+
| `messages` | `ModelMessage[]` | Accumulated model messages (**`"turn"` phase only**) |
|
|
500
|
+
| `uiMessages` | `UIMessage[]` | Accumulated UI messages (**`"turn"` phase only**) |
|
|
501
|
+
|
|
502
|
+
<Tip>
|
|
503
|
+
Unlike `onWait` (which fires for all wait types: duration, task, batch, token), `onChatSuspend` fires only at chat suspension points with full chat context. No need to filter on `wait.type`.
|
|
504
|
+
</Tip>
|
|
505
|
+
|
|
506
|
+
## exitAfterPreloadIdle
|
|
507
|
+
|
|
508
|
+
When set to `true`, a preloaded run completes successfully after the idle timeout elapses instead of suspending. Use this for "fire and forget" preloads. If the user doesn't send a message during the idle window, the run ends cleanly.
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
export const myChat = chat.agent({
|
|
512
|
+
id: "my-chat",
|
|
513
|
+
preloadIdleTimeoutInSeconds: 10,
|
|
514
|
+
exitAfterPreloadIdle: true,
|
|
515
|
+
onPreload: async ({ chatId, clientData }) => {
|
|
516
|
+
// Eagerly set up state. If no message comes, the run just ends.
|
|
517
|
+
await initializeChat(chatId, clientData);
|
|
518
|
+
},
|
|
519
|
+
run: async ({ messages, signal }) => {
|
|
520
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## See also
|
|
526
|
+
|
|
527
|
+
- [Reference](/ai-chat/reference) for full event-type definitions
|
|
528
|
+
- [Database persistence](/ai-chat/patterns/database-persistence) for the canonical persistence pattern
|
|
529
|
+
- [Code execution sandbox](/ai-chat/patterns/code-sandbox) for an `onChatSuspend` use case
|
|
530
|
+
- [Backend](/ai-chat/backend) for `chat.agent({ ... })` itself, prompts, stop signals, persistence overview, and runtime configuration
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "MCP Server"
|
|
3
|
+
sidebarTitle: "MCP Server"
|
|
4
|
+
description: "Chat with your agents from any AI coding tool using the Trigger.dev MCP server."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
|
|
8
|
+
|
|
9
|
+
<RcBanner />
|
|
10
|
+
|
|
11
|
+
The Trigger.dev MCP server includes tools for having conversations with your chat agents directly from AI coding tools like Claude Code, Cursor, Windsurf, and others. This lets your AI assistant interact with your agents without writing any code.
|
|
12
|
+
|
|
13
|
+
## Available tools
|
|
14
|
+
|
|
15
|
+
| Tool | Description |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `list_agents` | List all agents in the current worker |
|
|
18
|
+
| `start_agent_chat` | Start a conversation with an agent |
|
|
19
|
+
| `send_agent_message` | Send a message and get the response |
|
|
20
|
+
| `close_agent_chat` | Close a conversation |
|
|
21
|
+
|
|
22
|
+
See the [MCP Tools Reference](/mcp-tools#agent-chat-tools) for full details on each tool.
|
|
23
|
+
|
|
24
|
+
## Typical workflow
|
|
25
|
+
|
|
26
|
+
<Steps>
|
|
27
|
+
<Step title="List available agents">
|
|
28
|
+
Ask your AI assistant to list agents in your project. This calls `list_agents` which returns all tasks created with [`chat.agent()`](/ai-chat/backend#chat-agent) or [`chat.customAgent()`](/ai-chat/custom-agents).
|
|
29
|
+
</Step>
|
|
30
|
+
<Step title="Start a chat">
|
|
31
|
+
Start a conversation with an agent using `start_agent_chat`. This triggers a run and optionally preloads the agent so it's ready to respond immediately.
|
|
32
|
+
|
|
33
|
+
If the agent uses `withClientData`, pass the required fields via `clientData`.
|
|
34
|
+
</Step>
|
|
35
|
+
<Step title="Send messages">
|
|
36
|
+
Send messages with `send_agent_message`. The agent processes each message and returns the full response text, including which tools were used. The agent retains full context across messages in the same chat.
|
|
37
|
+
</Step>
|
|
38
|
+
<Step title="Close the chat">
|
|
39
|
+
When done, close the conversation with `close_agent_chat`. This signals the agent to exit gracefully. If you skip this step, the agent will close on its own when its idle timeout expires.
|
|
40
|
+
</Step>
|
|
41
|
+
</Steps>
|
|
42
|
+
|
|
43
|
+
## Example conversation
|
|
44
|
+
|
|
45
|
+
Here's what a typical MCP conversation with an agent looks like:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
You: List my agents
|
|
49
|
+
|
|
50
|
+
AI: Found 3 agents in worker 20250402.12 (dev):
|
|
51
|
+
- support-agent (src/trigger/support.ts)
|
|
52
|
+
- pr-review (src/trigger/pr-review.ts)
|
|
53
|
+
- research (src/trigger/research.ts)
|
|
54
|
+
|
|
55
|
+
You: Start a chat with the support agent
|
|
56
|
+
|
|
57
|
+
AI: Agent chat started and preloaded.
|
|
58
|
+
- Chat ID: abc-123
|
|
59
|
+
- Agent: support-agent
|
|
60
|
+
- Run ID: run_xyz
|
|
61
|
+
|
|
62
|
+
You: Ask it to look up order #4821
|
|
63
|
+
|
|
64
|
+
AI: I found order #4821. It was placed on March 15 and shipped via
|
|
65
|
+
FedEx (tracking: 7891234). Current status: In Transit, estimated
|
|
66
|
+
delivery March 19.
|
|
67
|
+
|
|
68
|
+
Tools used: lookupOrder, getShippingStatus
|
|
69
|
+
|
|
70
|
+
You: Close the chat
|
|
71
|
+
|
|
72
|
+
AI: Chat abc-123 closed.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Setup
|
|
76
|
+
|
|
77
|
+
If you haven't set up the MCP server yet, see the [MCP Server introduction](/mcp-introduction) for installation and client configuration.
|
|
78
|
+
|
|
79
|
+
Agent chat tools require:
|
|
80
|
+
- A running dev server (`trigger dev`) or a deployed worker
|
|
81
|
+
- At least one agent defined with [`chat.agent()`](/ai-chat/backend#chat-agent) or [`chat.customAgent()`](/ai-chat/custom-agents)
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
Under the hood, the MCP tools use the same protocol as the [frontend transport](/ai-chat/frontend) and [AgentChat SDK](/ai-chat/server-chat):
|
|
86
|
+
|
|
87
|
+
1. **`start_agent_chat`** triggers a task run with the `preload` trigger and stores the session (run ID, chat ID) in memory.
|
|
88
|
+
2. **`send_agent_message`** sends the message via the run's input stream and subscribes to the output SSE stream to collect the agent's full response.
|
|
89
|
+
3. **`close_agent_chat`** sends a close signal via the input stream and removes the session.
|
|
90
|
+
|
|
91
|
+
Sessions are held in-memory within the MCP server process. If the MCP server restarts, active sessions are lost — but the underlying agent runs continue until their idle timeout.
|
|
92
|
+
|
|
93
|
+
<Note>
|
|
94
|
+
The `get_current_worker` tool also labels agents with `[agent]` in its output, making it easy to identify which tasks are agents even when listing all tasks.
|
|
95
|
+
</Note>
|
|
96
|
+
|
|
97
|
+
## See also
|
|
98
|
+
|
|
99
|
+
- [AgentChat SDK](/ai-chat/server-chat) — programmatic server-side access to agents
|
|
100
|
+
- [Sub-Agents](/ai-chat/patterns/sub-agents) — agents calling other agents
|
|
101
|
+
- [MCP Tools Reference](/mcp-tools#agent-chat-tools) — full tool parameter reference
|