@trigger.dev/sdk 4.5.0-rc.6 → 4.5.0

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.
Files changed (247) hide show
  1. package/dist/commonjs/v3/ai-shared.js +1 -4
  2. package/dist/commonjs/v3/ai-shared.js.map +1 -1
  3. package/dist/commonjs/v3/ai.d.ts +178 -6
  4. package/dist/commonjs/v3/ai.js +417 -64
  5. package/dist/commonjs/v3/ai.js.map +1 -1
  6. package/dist/commonjs/v3/chat-client.js +18 -14
  7. package/dist/commonjs/v3/chat-client.js.map +1 -1
  8. package/dist/commonjs/v3/chat-react.js +1 -3
  9. package/dist/commonjs/v3/chat-react.js.map +1 -1
  10. package/dist/commonjs/v3/chat-server.d.ts +76 -0
  11. package/dist/commonjs/v3/chat-server.js +194 -24
  12. package/dist/commonjs/v3/chat-server.js.map +1 -1
  13. package/dist/commonjs/v3/chat-server.test.js +201 -6
  14. package/dist/commonjs/v3/chat-server.test.js.map +1 -1
  15. package/dist/commonjs/v3/chat.js +4 -3
  16. package/dist/commonjs/v3/chat.js.map +1 -1
  17. package/dist/commonjs/v3/chat.test.js +6 -18
  18. package/dist/commonjs/v3/chat.test.js.map +1 -1
  19. package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
  20. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
  21. package/dist/commonjs/v3/idempotencyKeys.js.map +1 -1
  22. package/dist/commonjs/v3/prompt.js +2 -6
  23. package/dist/commonjs/v3/prompt.js.map +1 -1
  24. package/dist/commonjs/v3/promptManagement.js.map +1 -1
  25. package/dist/commonjs/v3/retry.js.map +1 -1
  26. package/dist/commonjs/v3/runs.d.ts +1 -1
  27. package/dist/commonjs/v3/sessions.d.ts +3 -2
  28. package/dist/commonjs/v3/sessions.js +4 -2
  29. package/dist/commonjs/v3/sessions.js.map +1 -1
  30. package/dist/commonjs/v3/shared.js.map +1 -1
  31. package/dist/commonjs/v3/skill.js.map +1 -1
  32. package/dist/commonjs/v3/streams.js +2 -3
  33. package/dist/commonjs/v3/streams.js.map +1 -1
  34. package/dist/commonjs/v3/streams.test.js.map +1 -1
  35. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -4
  36. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  37. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  38. package/dist/commonjs/v3/triggerClient.test.js +1 -1
  39. package/dist/commonjs/v3/triggerClient.test.js.map +1 -1
  40. package/dist/commonjs/v3/triggerClient.types.test.js +11 -11
  41. package/dist/commonjs/v3/triggerClient.types.test.js.map +1 -1
  42. package/dist/commonjs/version.js +1 -1
  43. package/dist/esm/v3/ai-shared.js +1 -4
  44. package/dist/esm/v3/ai-shared.js.map +1 -1
  45. package/dist/esm/v3/ai.d.ts +178 -6
  46. package/dist/esm/v3/ai.js +417 -64
  47. package/dist/esm/v3/ai.js.map +1 -1
  48. package/dist/esm/v3/chat-client.js +18 -14
  49. package/dist/esm/v3/chat-client.js.map +1 -1
  50. package/dist/esm/v3/chat-react.js +1 -3
  51. package/dist/esm/v3/chat-react.js.map +1 -1
  52. package/dist/esm/v3/chat-server.d.ts +76 -0
  53. package/dist/esm/v3/chat-server.js +194 -24
  54. package/dist/esm/v3/chat-server.js.map +1 -1
  55. package/dist/esm/v3/chat-server.test.js +201 -6
  56. package/dist/esm/v3/chat-server.test.js.map +1 -1
  57. package/dist/esm/v3/chat.js +4 -3
  58. package/dist/esm/v3/chat.js.map +1 -1
  59. package/dist/esm/v3/chat.test.js +6 -18
  60. package/dist/esm/v3/chat.test.js.map +1 -1
  61. package/dist/esm/v3/createStartSessionAction.test.js +30 -0
  62. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
  63. package/dist/esm/v3/idempotencyKeys.js +1 -1
  64. package/dist/esm/v3/idempotencyKeys.js.map +1 -1
  65. package/dist/esm/v3/prompt.js +2 -6
  66. package/dist/esm/v3/prompt.js.map +1 -1
  67. package/dist/esm/v3/promptManagement.js.map +1 -1
  68. package/dist/esm/v3/retry.js.map +1 -1
  69. package/dist/esm/v3/sessions.d.ts +3 -2
  70. package/dist/esm/v3/sessions.js +4 -2
  71. package/dist/esm/v3/sessions.js.map +1 -1
  72. package/dist/esm/v3/shared.js.map +1 -1
  73. package/dist/esm/v3/skill.js.map +1 -1
  74. package/dist/esm/v3/streams.js +2 -3
  75. package/dist/esm/v3/streams.js.map +1 -1
  76. package/dist/esm/v3/streams.test.js.map +1 -1
  77. package/dist/esm/v3/test/mock-chat-agent.js +4 -7
  78. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  79. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  80. package/dist/esm/v3/triggerClient.test.js +1 -1
  81. package/dist/esm/v3/triggerClient.test.js.map +1 -1
  82. package/dist/esm/v3/triggerClient.types.test.js +11 -11
  83. package/dist/esm/v3/triggerClient.types.test.js.map +1 -1
  84. package/dist/esm/version.js +1 -1
  85. package/docs/ai/prompts.mdx +426 -0
  86. package/docs/ai-chat/actions.mdx +111 -0
  87. package/docs/ai-chat/anatomy.mdx +67 -0
  88. package/docs/ai-chat/backend.mdx +813 -0
  89. package/docs/ai-chat/background-injection.mdx +217 -0
  90. package/docs/ai-chat/changelog.mdx +958 -0
  91. package/docs/ai-chat/chat-local.mdx +170 -0
  92. package/docs/ai-chat/client-protocol.mdx +1077 -0
  93. package/docs/ai-chat/compaction.mdx +407 -0
  94. package/docs/ai-chat/custom-agents.mdx +392 -0
  95. package/docs/ai-chat/error-handling.mdx +411 -0
  96. package/docs/ai-chat/fast-starts.mdx +754 -0
  97. package/docs/ai-chat/frontend.mdx +576 -0
  98. package/docs/ai-chat/how-it-works.mdx +226 -0
  99. package/docs/ai-chat/lifecycle-hooks.mdx +526 -0
  100. package/docs/ai-chat/mcp.mdx +97 -0
  101. package/docs/ai-chat/overview.mdx +86 -0
  102. package/docs/ai-chat/patterns/branching-conversations.mdx +280 -0
  103. package/docs/ai-chat/patterns/code-sandbox.mdx +122 -0
  104. package/docs/ai-chat/patterns/database-persistence.mdx +410 -0
  105. package/docs/ai-chat/patterns/human-in-the-loop.mdx +279 -0
  106. package/docs/ai-chat/patterns/large-payloads.mdx +165 -0
  107. package/docs/ai-chat/patterns/oom-resilience.mdx +116 -0
  108. package/docs/ai-chat/patterns/persistence-and-replay.mdx +207 -0
  109. package/docs/ai-chat/patterns/recovery-boot.mdx +226 -0
  110. package/docs/ai-chat/patterns/skills.mdx +217 -0
  111. package/docs/ai-chat/patterns/sub-agents.mdx +379 -0
  112. package/docs/ai-chat/patterns/tool-result-auditing.mdx +144 -0
  113. package/docs/ai-chat/patterns/trusted-edge-signals.mdx +333 -0
  114. package/docs/ai-chat/patterns/version-upgrades.mdx +168 -0
  115. package/docs/ai-chat/pending-messages.mdx +339 -0
  116. package/docs/ai-chat/prompt-caching.mdx +206 -0
  117. package/docs/ai-chat/quick-start.mdx +157 -0
  118. package/docs/ai-chat/reference.mdx +905 -0
  119. package/docs/ai-chat/server-chat.mdx +259 -0
  120. package/docs/ai-chat/sessions.mdx +329 -0
  121. package/docs/ai-chat/testing.mdx +678 -0
  122. package/docs/ai-chat/tools.mdx +187 -0
  123. package/docs/ai-chat/types.mdx +238 -0
  124. package/docs/ai-chat/upgrade-guide.mdx +511 -0
  125. package/docs/apikeys.mdx +54 -0
  126. package/docs/building-with-ai.mdx +261 -0
  127. package/docs/bulk-actions.mdx +49 -0
  128. package/docs/changelog.mdx +6 -0
  129. package/docs/cli-deploy-commands.mdx +9 -0
  130. package/docs/cli-dev-commands.mdx +9 -0
  131. package/docs/cli-init-commands.mdx +58 -0
  132. package/docs/cli-introduction.mdx +25 -0
  133. package/docs/cli-list-profiles-commands.mdx +42 -0
  134. package/docs/cli-login-commands.mdx +33 -0
  135. package/docs/cli-logout-commands.mdx +33 -0
  136. package/docs/cli-preview-archive.mdx +59 -0
  137. package/docs/cli-promote-commands.mdx +9 -0
  138. package/docs/cli-switch.mdx +43 -0
  139. package/docs/cli-update-commands.mdx +42 -0
  140. package/docs/cli-whoami-commands.mdx +33 -0
  141. package/docs/community.mdx +6 -0
  142. package/docs/config/config-file.mdx +602 -0
  143. package/docs/config/extensions/additionalFiles.mdx +38 -0
  144. package/docs/config/extensions/additionalPackages.mdx +40 -0
  145. package/docs/config/extensions/aptGet.mdx +34 -0
  146. package/docs/config/extensions/audioWaveform.mdx +20 -0
  147. package/docs/config/extensions/custom.mdx +380 -0
  148. package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
  149. package/docs/config/extensions/esbuildPlugin.mdx +31 -0
  150. package/docs/config/extensions/ffmpeg.mdx +45 -0
  151. package/docs/config/extensions/lightpanda.mdx +56 -0
  152. package/docs/config/extensions/overview.mdx +67 -0
  153. package/docs/config/extensions/playwright.mdx +195 -0
  154. package/docs/config/extensions/prismaExtension.mdx +1014 -0
  155. package/docs/config/extensions/puppeteer.mdx +30 -0
  156. package/docs/config/extensions/pythonExtension.mdx +182 -0
  157. package/docs/config/extensions/syncEnvVars.mdx +291 -0
  158. package/docs/context.mdx +235 -0
  159. package/docs/database-connections.mdx +213 -0
  160. package/docs/deploy-environment-variables.mdx +435 -0
  161. package/docs/deployment/atomic-deployment.mdx +172 -0
  162. package/docs/deployment/dev-branches.mdx +92 -0
  163. package/docs/deployment/overview.mdx +257 -0
  164. package/docs/deployment/preview-branches.mdx +224 -0
  165. package/docs/errors-retrying.mdx +379 -0
  166. package/docs/github-actions.mdx +222 -0
  167. package/docs/github-integration.mdx +136 -0
  168. package/docs/github-repo.mdx +8 -0
  169. package/docs/help-email.mdx +6 -0
  170. package/docs/help-slack.mdx +12 -0
  171. package/docs/hidden-tasks.mdx +56 -0
  172. package/docs/how-it-works.mdx +454 -0
  173. package/docs/how-to-reduce-your-spend.mdx +224 -0
  174. package/docs/idempotency.mdx +504 -0
  175. package/docs/introduction.mdx +223 -0
  176. package/docs/limits.mdx +241 -0
  177. package/docs/logging.mdx +195 -0
  178. package/docs/machines.mdx +952 -0
  179. package/docs/manual-setup.mdx +632 -0
  180. package/docs/mcp-agent-rules.mdx +41 -0
  181. package/docs/mcp-introduction.mdx +385 -0
  182. package/docs/mcp-tools.mdx +273 -0
  183. package/docs/migrating-from-v3.mdx +334 -0
  184. package/docs/observability/dashboards.mdx +102 -0
  185. package/docs/observability/query.mdx +585 -0
  186. package/docs/open-source-contributing.mdx +16 -0
  187. package/docs/open-source-self-hosting.mdx +541 -0
  188. package/docs/private-networking/aws-console-setup.mdx +304 -0
  189. package/docs/private-networking/overview.mdx +144 -0
  190. package/docs/private-networking/troubleshooting.mdx +78 -0
  191. package/docs/queue-concurrency.mdx +354 -0
  192. package/docs/quick-start.mdx +97 -0
  193. package/docs/realtime/auth.mdx +208 -0
  194. package/docs/realtime/backend/overview.mdx +45 -0
  195. package/docs/realtime/backend/streams.mdx +418 -0
  196. package/docs/realtime/backend/subscribe.mdx +225 -0
  197. package/docs/realtime/how-it-works.mdx +94 -0
  198. package/docs/realtime/overview.mdx +63 -0
  199. package/docs/realtime/react-hooks/overview.mdx +73 -0
  200. package/docs/realtime/react-hooks/streams.mdx +449 -0
  201. package/docs/realtime/react-hooks/subscribe.mdx +674 -0
  202. package/docs/realtime/react-hooks/swr.mdx +87 -0
  203. package/docs/realtime/react-hooks/triggering.mdx +194 -0
  204. package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
  205. package/docs/realtime/run-object.mdx +174 -0
  206. package/docs/replaying.mdx +72 -0
  207. package/docs/request-feature.mdx +6 -0
  208. package/docs/roadmap.mdx +6 -0
  209. package/docs/run-tests.mdx +20 -0
  210. package/docs/run-usage.mdx +113 -0
  211. package/docs/runs/heartbeats.mdx +38 -0
  212. package/docs/runs/max-duration.mdx +139 -0
  213. package/docs/runs/metadata.mdx +734 -0
  214. package/docs/runs/priority.mdx +31 -0
  215. package/docs/runs.mdx +396 -0
  216. package/docs/self-hosting/docker.mdx +458 -0
  217. package/docs/self-hosting/env/supervisor.mdx +74 -0
  218. package/docs/self-hosting/env/webapp.mdx +276 -0
  219. package/docs/self-hosting/kubernetes.mdx +601 -0
  220. package/docs/self-hosting/overview.mdx +109 -0
  221. package/docs/skills.mdx +85 -0
  222. package/docs/tags.mdx +120 -0
  223. package/docs/tasks/overview.mdx +697 -0
  224. package/docs/tasks/scheduled.mdx +382 -0
  225. package/docs/tasks/schemaTask.mdx +413 -0
  226. package/docs/tasks/streams.mdx +884 -0
  227. package/docs/triggering.mdx +1320 -0
  228. package/docs/troubleshooting-alerts.mdx +385 -0
  229. package/docs/troubleshooting-debugging-in-vscode.mdx +9 -0
  230. package/docs/troubleshooting-github-issues.mdx +6 -0
  231. package/docs/troubleshooting-uptime-status.mdx +7 -0
  232. package/docs/troubleshooting.mdx +398 -0
  233. package/docs/upgrading-packages.mdx +80 -0
  234. package/docs/vercel-integration.mdx +207 -0
  235. package/docs/versioning.mdx +56 -0
  236. package/docs/video-walkthrough.mdx +23 -0
  237. package/docs/wait-for-token.mdx +540 -0
  238. package/docs/wait-for.mdx +42 -0
  239. package/docs/wait-until.mdx +53 -0
  240. package/docs/wait.mdx +18 -0
  241. package/docs/writing-tasks-introduction.mdx +33 -0
  242. package/package.json +8 -5
  243. package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
  244. package/skills/trigger-authoring-tasks/SKILL.md +254 -0
  245. package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
  246. package/skills/trigger-cost-savings/SKILL.md +116 -0
  247. package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
@@ -0,0 +1,576 @@
1
+ ---
2
+ title: "Frontend"
3
+ sidebarTitle: "Frontend"
4
+ description: "Transport setup, session management, client data, and frontend patterns for AI Chat."
5
+ ---
6
+
7
+ ## How the transport works
8
+
9
+ Vanilla `useChat` expects an `api` URL — it POSTs the conversation to your own Next.js route handler, which terminates the stream. `useTriggerChatTransport` replaces that round-trip: instead of an `api` URL, you pass a custom [`ChatTransport`](https://ai-sdk.dev/docs/ai-sdk-ui/transport) that talks directly to the Trigger.dev cloud (or your self-hosted webapp) on behalf of `useChat`.
10
+
11
+ There's no API route to maintain. The browser uses a short-lived session-scoped PAT (minted by your `accessToken` server action) to:
12
+
13
+ - **Create the session** via your `startSession` action on the first message (or `transport.preload(chatId)`).
14
+ - **Append the new user message** to the session's durable `.in` stream.
15
+ - **Subscribe to the `.out` SSE stream** for the agent's response chunks (text, tool calls, reasoning, custom `data-*` parts).
16
+
17
+ The transport handles the auth refresh, reconnect, `Last-Event-ID` resume, and stop-signal plumbing transparently. `useChat` sees the result as `UIMessageChunk`s and renders them unchanged.
18
+
19
+ ## Transport setup
20
+
21
+ Use the `useTriggerChatTransport` hook from `@trigger.dev/sdk/chat/react` to create a memoized transport instance, then pass it to `useChat`:
22
+
23
+ ```tsx
24
+ import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
25
+ import { useChat } from "@ai-sdk/react";
26
+ import type { myChat } from "@/trigger/chat";
27
+ import { mintChatAccessToken, startChatSession } from "@/app/actions";
28
+
29
+ export function Chat() {
30
+ const transport = useTriggerChatTransport<typeof myChat>({
31
+ task: "my-chat",
32
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
33
+ startSession: ({ chatId, clientData }) =>
34
+ startChatSession({ chatId, clientData }),
35
+ });
36
+
37
+ const { messages, sendMessage, stop, status } = useChat({ transport });
38
+ // ... render UI
39
+ }
40
+ ```
41
+
42
+ The transport is created once on first render and reused across re-renders. Pass a type parameter for compile-time validation of the task ID.
43
+
44
+ The two callbacks have distinct responsibilities:
45
+
46
+ - **`accessToken`** is a *pure* PAT mint — the transport invokes it on a 401/403 to refresh the session-scoped token. Customer wraps `auth.createPublicToken({ scopes: { read: { sessions: chatId }, write: { sessions: chatId } } })`, which resolves to a `Promise<string>` (the JWT). Return that string from your `accessToken` callback.
47
+ - **`startSession`** wraps `chat.createStartSessionAction(taskId)` and is called when the transport needs to *create* the session (`transport.preload(chatId)`, or lazily on the first `sendMessage` for a chatId without a cached PAT). The customer's server controls authorization here, alongside any DB writes paired with session creation.
48
+
49
+ See [Quick start](/ai-chat/quick-start) for the matching server actions.
50
+
51
+ <Tip>
52
+ The hook keeps `onSessionChange` and `clientData` up to date via internal refs, so you don't need
53
+ to memoize callbacks or worry about stale closures when those options change between renders.
54
+ </Tip>
55
+
56
+ ## Typed messages (`chat.withUIMessage`)
57
+
58
+ If your chat agent is defined with [`chat.withUIMessage<YourUIMessage>()`](/ai-chat/types) (custom `data-*` parts, typed tools, etc.), pass the same message type through `useChat` so `messages` and `message.parts` are narrowed on the client:
59
+
60
+ ```tsx
61
+ import { useChat } from "@ai-sdk/react";
62
+ import { useTriggerChatTransport, type InferChatUIMessage } from "@trigger.dev/sdk/chat/react";
63
+ import type { myChat } from "./myChat";
64
+
65
+ type Msg = InferChatUIMessage<typeof myChat>;
66
+
67
+ const transport = useTriggerChatTransport<typeof myChat>({
68
+ task: "my-chat",
69
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
70
+ startSession: ({ chatId, clientData }) =>
71
+ startChatSession({ chatId, clientData }),
72
+ });
73
+ const { messages } = useChat<Msg>({ transport });
74
+ ```
75
+
76
+ See the [Types](/ai-chat/types) guide for defining `YourUIMessage`, default stream options, and backend examples.
77
+
78
+ ### Calling a fetch endpoint instead of a server action
79
+
80
+ If you want to mint tokens via a REST endpoint instead of a Next.js server action, the same callbacks accept any async function. Import `AccessTokenParams` and `StartSessionParams` from `@trigger.dev/sdk/chat` to type your fetch handler.
81
+
82
+ ```ts
83
+ import type { AccessTokenParams, StartSessionParams } from "@trigger.dev/sdk/chat";
84
+
85
+ const transport = useTriggerChatTransport({
86
+ task: "my-chat",
87
+ accessToken: async ({ chatId }: AccessTokenParams) => {
88
+ const res = await fetch(`/api/chat/${chatId}/access-token`, { method: "POST" });
89
+ return res.text();
90
+ },
91
+ startSession: async ({ chatId, taskId, clientData }: StartSessionParams) => {
92
+ const res = await fetch(`/api/chat/${chatId}/start`, {
93
+ method: "POST",
94
+ body: JSON.stringify({ taskId, clientData }),
95
+ });
96
+ return res.json(); // { publicAccessToken: string }
97
+ },
98
+ });
99
+ ```
100
+
101
+ The fetch handlers on the server side wrap the same SDK helpers as the server-action variant: `auth.createPublicToken({ scopes: { read: { sessions: chatId }, write: { sessions: chatId } } })` for refresh and `chat.createStartSessionAction(taskId)` for create.
102
+
103
+ ## Session management
104
+
105
+ Every chat is backed by a durable Session — the row that owns the chat's runs, persists across run lifecycles, and orchestrates handoffs. The transport manages the session for you; what you persist on your side is a small piece of state per chat that lets a fresh tab resume without a round-trip to create a new session.
106
+
107
+ ### What the transport persists per chat
108
+
109
+ | Field | Type | Notes |
110
+ | --- | --- | --- |
111
+ | `publicAccessToken` | `string` | Session-scoped JWT (`read:sessions:{chatId} + write:sessions:{chatId}`). Refreshed automatically on 401/403 via `accessToken`. |
112
+ | `lastEventId` | `string \| undefined` | Last SSE event received on `.out`. **Valid for the lifetime of the Session** — keep it across `endRun` / `requestUpgrade` / continuation-run boundaries; only clear when the Session itself closes. The cursor lets the next subscription open past the prior turn's stale `turn-complete` record. |
113
+ | `isStreaming` | `boolean \| undefined` | **Optional.** The transport sets it internally, but you don't have to persist it — the server decides "nothing is streaming" via the session's [`X-Session-Settled`](/ai-chat/client-protocol#x-session-settled-fast-close-on-idle-reconnects) signal on reconnect. If you do persist it, the transport keeps the fast-path short-circuit. If you drop it, reconnects open the SSE and close fast on settled sessions. |
114
+
115
+ ### Session cleanup (frontend)
116
+
117
+ Since session creation and updates are handled server-side, the frontend only needs to handle session deletion when a run ends:
118
+
119
+ ```tsx
120
+ const transport = useTriggerChatTransport<typeof myChat>({
121
+ task: "my-chat",
122
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
123
+ startSession: ({ chatId, clientData }) =>
124
+ startChatSession({ chatId, clientData }),
125
+ sessions: loadedSessions, // Restored from DB on page load
126
+ onSessionChange: (chatId, session) => {
127
+ if (!session) {
128
+ deleteSession(chatId); // Server action — run ended
129
+ }
130
+ },
131
+ });
132
+ ```
133
+
134
+ ### Restoring on page load
135
+
136
+ On page load, fetch both the messages and the session state from your database, then pass them to `useChat` and the transport. Pass `resume: true` to `useChat` when there's an existing conversation — this tells the AI SDK to reconnect to the stream via the transport.
137
+
138
+ Because the underlying Session row outlives individual runs, a chat you were in yesterday resumes against the same chat — even if the original run has long since exited. The transport hydrates from the persisted state and uses `lastEventId` to resubscribe; if the client tries to send a new message and no run is alive, the server triggers a fresh continuation run on the same session before the message is appended.
139
+
140
+ ```tsx app/chat/[chatId]/ChatPage.tsx
141
+ "use client";
142
+
143
+ import { useEffect, useState } from "react";
144
+ import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
145
+ import { useChat } from "@ai-sdk/react";
146
+ import {
147
+ mintChatAccessToken,
148
+ startChatSession,
149
+ getChatMessages,
150
+ getSession,
151
+ deleteSession,
152
+ } from "@/app/actions";
153
+
154
+ // Rendered from `app/chat/[chatId]/page.tsx`, which awaits `params`
155
+ // and forwards `chatId` into this client component:
156
+ //
157
+ // export default async function Page({ params }: { params: Promise<{ chatId: string }> }) {
158
+ // const { chatId } = await params;
159
+ // return <ChatPage chatId={chatId} />;
160
+ // }
161
+ export default function ChatPage({ chatId }: { chatId: string }) {
162
+ const [initialMessages, setInitialMessages] = useState([]);
163
+ const [initialSession, setInitialSession] = useState(undefined);
164
+ const [loaded, setLoaded] = useState(false);
165
+
166
+ useEffect(() => {
167
+ async function load() {
168
+ const [messages, session] = await Promise.all([getChatMessages(chatId), getSession(chatId)]);
169
+ setInitialMessages(messages);
170
+ setInitialSession(session ? { [chatId]: session } : undefined);
171
+ setLoaded(true);
172
+ }
173
+ load();
174
+ }, [chatId]);
175
+
176
+ if (!loaded) return null;
177
+
178
+ return (
179
+ <ChatClient
180
+ chatId={chatId}
181
+ initialMessages={initialMessages}
182
+ initialSessions={initialSession}
183
+ />
184
+ );
185
+ }
186
+
187
+ function ChatClient({ chatId, initialMessages, initialSessions }) {
188
+ const transport = useTriggerChatTransport({
189
+ task: "my-chat",
190
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
191
+ startSession: ({ chatId, clientData }) =>
192
+ startChatSession({ chatId, clientData }),
193
+ sessions: initialSessions,
194
+ onSessionChange: (id, session) => {
195
+ if (!session) deleteSession(id);
196
+ },
197
+ });
198
+
199
+ const { messages, sendMessage, stop, status } = useChat({
200
+ id: chatId,
201
+ messages: initialMessages,
202
+ transport,
203
+ resume: initialMessages.length > 0, // Resume if there's an existing conversation
204
+ });
205
+
206
+ // ... render UI
207
+ }
208
+ ```
209
+
210
+ <Info>
211
+ `resume: true` causes `useChat` to call `reconnectToStream` on the transport when the component
212
+ mounts. The transport uses the session's `lastEventId` to skip past already-seen stream events, so
213
+ the frontend only receives new data. Only enable `resume` when there are existing messages — for
214
+ brand new chats, there's nothing to reconnect to.
215
+ </Info>
216
+
217
+ <Note>
218
+ After resuming, `useChat`'s built-in `stop()` won't send the stop signal to the backend because
219
+ the AI SDK doesn't pass its abort signal through `reconnectToStream`. Use
220
+ `transport.stopGeneration(chatId)` for reliable stop behavior after resume — see
221
+ [Stop generation](#stop-generation) for the recommended pattern.
222
+ </Note>
223
+
224
+ <Warning>
225
+ In React strict mode (enabled by default in Next.js dev), you may see a `TypeError: Cannot read
226
+ properties of undefined (reading 'state')` in the console when using `resume`. This is a [known
227
+ bug in the AI SDK](https://github.com/vercel/ai/issues/8477) caused by React strict mode
228
+ double-firing the resume effect. The error is caught internally and **does not affect
229
+ functionality** — streaming and message display work correctly. It only appears in development and
230
+ will not occur in production builds.
231
+ </Warning>
232
+
233
+ ### Network resilience
234
+
235
+ You don't need to handle network drops, mobile background-kills, or Safari bfcache restores. The transport retries indefinitely with bounded backoff, reconnects on `online` / tab refocus / `pageshow` with `event.persisted`, and uses `Last-Event-ID` to resume without dropping chunks. See the [changelog entry](/ai-chat/changelog) for the gory details.
236
+
237
+ ## Client data and metadata
238
+
239
+ ### Transport-level client data
240
+
241
+ Set default client data on the transport that's included in every request. When the task uses `clientDataSchema`, this is type-checked to match:
242
+
243
+ ```ts
244
+ const transport = useTriggerChatTransport<typeof myChat>({
245
+ task: "my-chat",
246
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
247
+ startSession: ({ chatId, clientData }) =>
248
+ startChatSession({ chatId, clientData }),
249
+ clientData: { userId: currentUser.id },
250
+ });
251
+ ```
252
+
253
+ The transport threads `clientData` through three places automatically: into `startSession`'s `params.clientData` for the first run's `payload.metadata`, into per-turn `metadata` on every `.in/append` chunk, and live-updates if the option value changes between renders (so React-driven values like the current user work without reconstructing the transport).
254
+
255
+ ### Per-message metadata
256
+
257
+ Pass metadata with individual messages via `sendMessage`. Per-message values are merged with transport-level client data (per-message wins on conflicts):
258
+
259
+ ```ts
260
+ sendMessage({ text: "Hello" }, { metadata: { model: "gpt-4o", priority: "high" } });
261
+ ```
262
+
263
+ ### Typed client data with clientDataSchema
264
+
265
+ Instead of manually parsing `clientData` with Zod in every hook, pass a `clientDataSchema` to `chat.agent`. The schema validates the data once per turn, and `clientData` is typed in all hooks and `run`:
266
+
267
+ ```ts
268
+ import { chat } from "@trigger.dev/sdk/ai";
269
+ import { streamText, stepCountIs } from "ai";
270
+ import { anthropic } from "@ai-sdk/anthropic";
271
+ import { z } from "zod";
272
+
273
+ export const myChat = chat.agent({
274
+ id: "my-chat",
275
+ clientDataSchema: z.object({
276
+ model: z.string().optional(),
277
+ userId: z.string(),
278
+ }),
279
+ onChatStart: async ({ chatId, clientData }) => {
280
+ // clientData is typed as { model?: string; userId: string }
281
+ await db.chat.create({
282
+ data: { id: chatId, userId: clientData.userId },
283
+ });
284
+ },
285
+ run: async ({ messages, clientData, signal }) => {
286
+ // Same typed clientData — no manual parsing needed
287
+ return streamText({
288
+ model: openai(clientData?.model ?? "gpt-4o"),
289
+ messages,
290
+ abortSignal: signal,
291
+ stopWhen: stepCountIs(15),
292
+ });
293
+ },
294
+ });
295
+ ```
296
+
297
+ The schema also types the `clientData` option on the frontend transport:
298
+
299
+ ```ts
300
+ // TypeScript enforces that clientData matches the schema
301
+ const transport = useTriggerChatTransport<typeof myChat>({
302
+ task: "my-chat",
303
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
304
+ startSession: ({ chatId, clientData }) =>
305
+ startChatSession({ chatId, clientData }),
306
+ clientData: { userId: currentUser.id },
307
+ });
308
+ ```
309
+
310
+ Supports Zod, ArkType, Valibot, and other schema libraries supported by the SDK.
311
+
312
+ ## Stop generation
313
+
314
+ Use `transport.stopGeneration(chatId)` to stop the current generation. This sends a stop signal to the running task via input streams, aborting the current `streamText` call while keeping the run alive for the next message.
315
+
316
+ `stopGeneration` works in all scenarios — including after a page refresh when the stream was reconnected via `resume`. Call it alongside `useChat`'s `stop()` to also update the frontend state:
317
+
318
+ ```tsx
319
+ const { messages, sendMessage, stop: aiStop, status } = useChat({ transport });
320
+
321
+ // Wrap both calls in a single stop handler
322
+ const stop = useCallback(() => {
323
+ transport.stopGeneration(chatId);
324
+ aiStop();
325
+ }, [transport, chatId, aiStop]);
326
+
327
+ {
328
+ status === "streaming" && (
329
+ <button type="button" onClick={stop}>
330
+ Stop
331
+ </button>
332
+ );
333
+ }
334
+ ```
335
+
336
+ <Info>
337
+ `transport.stopGeneration(chatId)` handles the backend stop signal and closes
338
+ the SSE connection, while `aiStop()` (from `useChat`) updates the frontend
339
+ status to `"ready"` and fires the `onFinish` callback.
340
+ </Info>
341
+
342
+ <Tip>
343
+ A [PR to the AI SDK](https://github.com/vercel/ai/pull/14350) has been
344
+ submitted to pass `abortSignal` through `reconnectToStream`, which would make
345
+ `useChat`'s built-in `stop()` work after resume without needing
346
+ `stopGeneration`. Until that lands, use the pattern above for reliable stop
347
+ behavior after page refresh.
348
+ </Tip>
349
+
350
+ See [Stop generation](/ai-chat/backend#stop-generation) in the backend docs for how to handle stop signals in your task.
351
+
352
+ ## Tool approvals
353
+
354
+ The AI SDK supports tools that require human approval before execution. To use this with `chat.agent`, define a tool with `needsApproval: true` on the backend, then handle the approval UI and configure `sendAutomaticallyWhen` on the frontend.
355
+
356
+ ### Backend: define an approval-required tool
357
+
358
+ ```ts
359
+ import { tool } from "ai";
360
+ import { z } from "zod";
361
+
362
+ const sendEmail = tool({
363
+ description: "Send an email. Requires human approval before sending.",
364
+ inputSchema: z.object({
365
+ to: z.string(),
366
+ subject: z.string(),
367
+ body: z.string(),
368
+ }),
369
+ needsApproval: true,
370
+ execute: async ({ to, subject, body }) => {
371
+ await emailService.send({ to, subject, body });
372
+ return { sent: true, to, subject };
373
+ },
374
+ });
375
+ ```
376
+
377
+ Pass the tool to `streamText` in your `run` function as usual. When the model calls the tool, `chat.agent` streams a `tool-approval-request` chunk. The turn completes and the run waits for the next message.
378
+
379
+ ### Frontend: approval UI
380
+
381
+ Import `lastAssistantMessageIsCompleteWithApprovalResponses` from the AI SDK and pass it to `sendAutomaticallyWhen`. This tells `useChat` to automatically re-send messages once all approvals have been responded to.
382
+
383
+ Destructure `addToolApprovalResponse` from `useChat` and wire it to your approval buttons:
384
+
385
+ ```tsx
386
+ import { useChat } from "@ai-sdk/react";
387
+ import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
388
+
389
+ function Chat({ chatId, transport }) {
390
+ const { messages, sendMessage, addToolApprovalResponse, status } = useChat({
391
+ id: chatId,
392
+ transport,
393
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
394
+ });
395
+
396
+ const handleApprove = (approvalId: string) => {
397
+ addToolApprovalResponse({ id: approvalId, approved: true });
398
+ };
399
+
400
+ const handleDeny = (approvalId: string) => {
401
+ addToolApprovalResponse({ id: approvalId, approved: false, reason: "User denied" });
402
+ };
403
+
404
+ return (
405
+ <div>
406
+ {messages.map((msg) =>
407
+ msg.parts.map((part, i) => {
408
+ if (part.state === "approval-requested") {
409
+ return (
410
+ <div key={i}>
411
+ <p>Tool "{part.type}" wants to run with input:</p>
412
+ <pre>{JSON.stringify(part.input, null, 2)}</pre>
413
+ <button onClick={() => handleApprove(part.approval.id)}>Approve</button>
414
+ <button onClick={() => handleDeny(part.approval.id)}>Deny</button>
415
+ </div>
416
+ );
417
+ }
418
+ // ... render other parts
419
+ })
420
+ )}
421
+ </div>
422
+ );
423
+ }
424
+ ```
425
+
426
+ ### How it works
427
+
428
+ 1. Model calls a tool with `needsApproval: true` — the turn completes with the tool in `approval-requested` state
429
+ 2. Frontend shows Approve/Deny buttons
430
+ 3. User clicks Approve — `addToolApprovalResponse` updates the tool part to `approval-responded`
431
+ 4. `sendAutomaticallyWhen` returns `true` — `useChat` re-sends the updated assistant message
432
+ 5. The transport sends the message via input streams — the backend matches it by ID and replaces the existing assistant message in the accumulator
433
+ 6. `streamText` sees the approved tool, executes it, and streams the result
434
+
435
+ <Info>
436
+ Message IDs are kept in sync between frontend and backend automatically. The backend always
437
+ includes a `generateMessageId` function when streaming responses, ensuring the `start` chunk
438
+ carries a `messageId` that the frontend uses. This makes the ID-based matching reliable
439
+ for tool approval updates.
440
+ </Info>
441
+
442
+ ## Sending actions
443
+
444
+ Send custom actions (undo, rollback, edit) to the agent via `transport.sendAction()`. Actions wake the agent and fire only `hydrateMessages` (if configured) and `onAction` — they're not turns, so `onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete` and `run()` do not fire.
445
+
446
+ For optimistic UI, mirror the action's effect on the `useChat` state via `setMessages` while the request is in flight:
447
+
448
+ ```tsx
449
+ function ChatControls({ chatId }: { chatId: string }) {
450
+ const transport = useTriggerChatTransport({
451
+ task: "my-chat",
452
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
453
+ startSession: ({ chatId, clientData }) =>
454
+ startChatSession({ chatId, clientData }),
455
+ });
456
+
457
+ const { setMessages } = useChat({ transport });
458
+
459
+ return (
460
+ <div>
461
+ <button
462
+ onClick={() => {
463
+ void transport.sendAction(chatId, { type: "undo" });
464
+ setMessages((prev) => prev.slice(0, -2));
465
+ }}
466
+ >
467
+ Undo last exchange
468
+ </button>
469
+ <button
470
+ onClick={() => transport.sendAction(chatId, { type: "rollback", targetMessageId: "msg-5" })}
471
+ >
472
+ Rollback to message
473
+ </button>
474
+ </div>
475
+ );
476
+ }
477
+ ```
478
+
479
+ The action payload is validated against the agent's `actionSchema` on the backend — invalid actions are rejected. See [Actions](/ai-chat/actions) for the backend setup.
480
+
481
+ <Note>
482
+ `sendAction` returns a `ReadableStream<UIMessageChunk>`. For side-effect-only actions (where `onAction` returns `void`), the stream completes immediately with `trigger:turn-complete`. For actions where `onAction` returns a `StreamTextResult`, the stream carries the assistant chunks the same way `sendMessages` does — `useChat` consumes them automatically.
483
+ </Note>
484
+
485
+ For server-to-server usage, `AgentChat` has the same method:
486
+
487
+ ```ts
488
+ const stream = await agentChat.sendAction({ type: "undo" });
489
+ for await (const chunk of stream) {
490
+ if (chunk.type === "text-delta") process.stdout.write(chunk.delta);
491
+ }
492
+ ```
493
+
494
+ ## Multi-tab coordination
495
+
496
+ When the same chat is open in multiple browser tabs, `multiTab: true` prevents duplicate messages and syncs conversation state across tabs. Only one tab can send at a time. Other tabs enter read-only mode with real-time message updates.
497
+
498
+ ```tsx
499
+ import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
500
+ import { useMultiTabChat } from "@trigger.dev/sdk/chat/react";
501
+ import { useChat } from "@ai-sdk/react";
502
+
503
+ function Chat({ chatId }: { chatId: string }) {
504
+ const transport = useTriggerChatTransport({
505
+ task: "my-chat",
506
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
507
+ startSession: ({ chatId, clientData }) =>
508
+ startChatSession({ chatId, clientData }),
509
+ multiTab: true,
510
+ });
511
+
512
+ const { messages, setMessages, sendMessage } = useChat({
513
+ id: chatId,
514
+ transport,
515
+ });
516
+
517
+ const { isReadOnly } = useMultiTabChat(transport, chatId, messages, setMessages);
518
+
519
+ return (
520
+ <div>
521
+ {isReadOnly && (
522
+ <div className="bg-amber-50 text-amber-700 p-2 text-sm">
523
+ This chat is active in another tab. Messages are read-only.
524
+ </div>
525
+ )}
526
+ {/* message list */}
527
+ <input
528
+ disabled={isReadOnly}
529
+ placeholder={isReadOnly ? "Active in another tab" : "Type a message..."}
530
+ />
531
+ </div>
532
+ );
533
+ }
534
+ ```
535
+
536
+ ### How it works
537
+
538
+ 1. When a tab sends a message, the transport "claims" the chatId via `BroadcastChannel`
539
+ 2. Other tabs detect the claim and enter read-only mode (`isReadOnly: true`)
540
+ 3. The active tab broadcasts its messages so read-only tabs see updates in real-time
541
+ 4. When the turn completes, the claim is released. Any tab can send next.
542
+ 5. Heartbeats detect crashed tabs (10s timeout clears stale claims)
543
+
544
+ ### What `useMultiTabChat` does
545
+
546
+ - Returns `{ isReadOnly }` for disabling the input UI
547
+ - Broadcasts `messages` from the active tab to other tabs
548
+ - Calls `setMessages` on read-only tabs when messages arrive from the active tab
549
+ - Tracks read-only state via the transport's `BroadcastChannel` coordinator
550
+
551
+ <Note>
552
+ Multi-tab coordination is same-browser only (`BroadcastChannel` is a browser API). It gracefully degrades to a no-op in Node.js, SSR, or browsers without `BroadcastChannel` support. Cross-device coordination requires server-side involvement.
553
+ </Note>
554
+
555
+ ## Self-hosting
556
+
557
+ If you're self-hosting Trigger.dev, pass the `baseURL` option:
558
+
559
+ ```ts
560
+ const transport = useTriggerChatTransport({
561
+ task: "my-chat",
562
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
563
+ startSession: ({ chatId, clientData }) =>
564
+ startChatSession({ chatId, clientData }),
565
+ baseURL: "https://your-trigger-instance.com",
566
+ });
567
+ ```
568
+
569
+ `baseURL` also accepts a function so you can route per endpoint — useful when fronting `.in/append` with an edge proxy (e.g. to inject server-trusted signal into the wire) while keeping `.out` SSE direct:
570
+
571
+ ```ts
572
+ baseURL: ({ endpoint }) =>
573
+ endpoint === "out" ? "https://api.trigger.dev" : "https://chat-proxy.example.com",
574
+ ```
575
+
576
+ For per-request control beyond URL routing (header injection, custom retries, tracing), pass a `fetch` override. See [Trusted edge signals](/ai-chat/patterns/trusted-edge-signals) for a full proxy walkthrough.