@trigger.dev/sdk 4.5.0-rc.5 → 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.
Files changed (213) hide show
  1. package/dist/commonjs/v3/ai.d.ts +178 -5
  2. package/dist/commonjs/v3/ai.js +603 -119
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat-client.js +3 -0
  5. package/dist/commonjs/v3/chat-client.js.map +1 -1
  6. package/dist/commonjs/v3/chat-react.js +10 -7
  7. package/dist/commonjs/v3/chat-react.js.map +1 -1
  8. package/dist/commonjs/v3/chat-server.d.ts +8 -0
  9. package/dist/commonjs/v3/chat-server.js +32 -10
  10. package/dist/commonjs/v3/chat-server.js.map +1 -1
  11. package/dist/commonjs/v3/chat-server.test.js +51 -0
  12. package/dist/commonjs/v3/chat-server.test.js.map +1 -1
  13. package/dist/commonjs/v3/chat.js +34 -6
  14. package/dist/commonjs/v3/chat.js.map +1 -1
  15. package/dist/commonjs/v3/chat.test.js +53 -0
  16. package/dist/commonjs/v3/chat.test.js.map +1 -1
  17. package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
  18. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
  19. package/dist/commonjs/v3/sessions.d.ts +11 -6
  20. package/dist/commonjs/v3/sessions.js +10 -5
  21. package/dist/commonjs/v3/sessions.js.map +1 -1
  22. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +6 -0
  23. package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
  24. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  25. package/dist/commonjs/version.js +1 -1
  26. package/dist/esm/v3/ai.d.ts +178 -5
  27. package/dist/esm/v3/ai.js +603 -120
  28. package/dist/esm/v3/ai.js.map +1 -1
  29. package/dist/esm/v3/chat-client.js +3 -0
  30. package/dist/esm/v3/chat-client.js.map +1 -1
  31. package/dist/esm/v3/chat-react.js +10 -7
  32. package/dist/esm/v3/chat-react.js.map +1 -1
  33. package/dist/esm/v3/chat-server.d.ts +8 -0
  34. package/dist/esm/v3/chat-server.js +32 -10
  35. package/dist/esm/v3/chat-server.js.map +1 -1
  36. package/dist/esm/v3/chat-server.test.js +51 -0
  37. package/dist/esm/v3/chat-server.test.js.map +1 -1
  38. package/dist/esm/v3/chat.js +34 -6
  39. package/dist/esm/v3/chat.js.map +1 -1
  40. package/dist/esm/v3/chat.test.js +53 -0
  41. package/dist/esm/v3/chat.test.js.map +1 -1
  42. package/dist/esm/v3/createStartSessionAction.test.js +30 -0
  43. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
  44. package/dist/esm/v3/sessions.d.ts +11 -6
  45. package/dist/esm/v3/sessions.js +10 -5
  46. package/dist/esm/v3/sessions.js.map +1 -1
  47. package/dist/esm/v3/test/mock-chat-agent.d.ts +6 -0
  48. package/dist/esm/v3/test/mock-chat-agent.js +1 -0
  49. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  50. package/dist/esm/version.js +1 -1
  51. package/docs/ai/prompts.mdx +430 -0
  52. package/docs/ai-chat/actions.mdx +115 -0
  53. package/docs/ai-chat/anatomy.mdx +71 -0
  54. package/docs/ai-chat/backend.mdx +817 -0
  55. package/docs/ai-chat/background-injection.mdx +221 -0
  56. package/docs/ai-chat/changelog.mdx +850 -0
  57. package/docs/ai-chat/chat-local.mdx +174 -0
  58. package/docs/ai-chat/client-protocol.mdx +1081 -0
  59. package/docs/ai-chat/compaction.mdx +411 -0
  60. package/docs/ai-chat/custom-agents.mdx +364 -0
  61. package/docs/ai-chat/error-handling.mdx +415 -0
  62. package/docs/ai-chat/fast-starts.mdx +672 -0
  63. package/docs/ai-chat/frontend.mdx +580 -0
  64. package/docs/ai-chat/how-it-works.mdx +230 -0
  65. package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
  66. package/docs/ai-chat/mcp.mdx +101 -0
  67. package/docs/ai-chat/overview.mdx +90 -0
  68. package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
  69. package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
  70. package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
  71. package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
  72. package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
  73. package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
  74. package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
  75. package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
  76. package/docs/ai-chat/patterns/skills.mdx +221 -0
  77. package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
  78. package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
  79. package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
  80. package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
  81. package/docs/ai-chat/pending-messages.mdx +343 -0
  82. package/docs/ai-chat/prompt-caching.mdx +206 -0
  83. package/docs/ai-chat/quick-start.mdx +161 -0
  84. package/docs/ai-chat/reference.mdx +909 -0
  85. package/docs/ai-chat/server-chat.mdx +263 -0
  86. package/docs/ai-chat/sessions.mdx +333 -0
  87. package/docs/ai-chat/testing.mdx +682 -0
  88. package/docs/ai-chat/tools.mdx +191 -0
  89. package/docs/ai-chat/types.mdx +242 -0
  90. package/docs/ai-chat/upgrade-guide.mdx +515 -0
  91. package/docs/apikeys.mdx +54 -0
  92. package/docs/building-with-ai.mdx +261 -0
  93. package/docs/bulk-actions.mdx +49 -0
  94. package/docs/changelog.mdx +6 -0
  95. package/docs/cli-deploy-commands.mdx +9 -0
  96. package/docs/cli-dev-commands.mdx +9 -0
  97. package/docs/cli-dev.mdx +8 -0
  98. package/docs/cli-init-commands.mdx +58 -0
  99. package/docs/cli-introduction.mdx +25 -0
  100. package/docs/cli-list-profiles-commands.mdx +42 -0
  101. package/docs/cli-login-commands.mdx +33 -0
  102. package/docs/cli-logout-commands.mdx +33 -0
  103. package/docs/cli-preview-archive.mdx +59 -0
  104. package/docs/cli-promote-commands.mdx +9 -0
  105. package/docs/cli-switch.mdx +43 -0
  106. package/docs/cli-update-commands.mdx +42 -0
  107. package/docs/cli-whoami-commands.mdx +33 -0
  108. package/docs/community.mdx +6 -0
  109. package/docs/config/config-file.mdx +602 -0
  110. package/docs/config/extensions/additionalFiles.mdx +38 -0
  111. package/docs/config/extensions/additionalPackages.mdx +40 -0
  112. package/docs/config/extensions/aptGet.mdx +34 -0
  113. package/docs/config/extensions/audioWaveform.mdx +20 -0
  114. package/docs/config/extensions/custom.mdx +380 -0
  115. package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
  116. package/docs/config/extensions/esbuildPlugin.mdx +31 -0
  117. package/docs/config/extensions/ffmpeg.mdx +45 -0
  118. package/docs/config/extensions/lightpanda.mdx +56 -0
  119. package/docs/config/extensions/overview.mdx +67 -0
  120. package/docs/config/extensions/playwright.mdx +195 -0
  121. package/docs/config/extensions/prismaExtension.mdx +1014 -0
  122. package/docs/config/extensions/puppeteer.mdx +30 -0
  123. package/docs/config/extensions/pythonExtension.mdx +182 -0
  124. package/docs/config/extensions/syncEnvVars.mdx +291 -0
  125. package/docs/context.mdx +235 -0
  126. package/docs/database-connections.mdx +213 -0
  127. package/docs/deploy-environment-variables.mdx +435 -0
  128. package/docs/deployment/atomic-deployment.mdx +172 -0
  129. package/docs/deployment/overview.mdx +257 -0
  130. package/docs/deployment/preview-branches.mdx +224 -0
  131. package/docs/errors-retrying.mdx +379 -0
  132. package/docs/github-actions.mdx +222 -0
  133. package/docs/github-integration.mdx +136 -0
  134. package/docs/github-repo.mdx +8 -0
  135. package/docs/help-email.mdx +6 -0
  136. package/docs/help-slack.mdx +11 -0
  137. package/docs/hidden-tasks.mdx +56 -0
  138. package/docs/how-it-works.mdx +454 -0
  139. package/docs/how-to-reduce-your-spend.mdx +217 -0
  140. package/docs/idempotency.mdx +504 -0
  141. package/docs/introduction.mdx +223 -0
  142. package/docs/limits.mdx +241 -0
  143. package/docs/logging.mdx +195 -0
  144. package/docs/machines.mdx +952 -0
  145. package/docs/manual-setup.mdx +632 -0
  146. package/docs/mcp-agent-rules.mdx +41 -0
  147. package/docs/mcp-introduction.mdx +385 -0
  148. package/docs/mcp-tools.mdx +273 -0
  149. package/docs/migrating-from-v3.mdx +334 -0
  150. package/docs/observability/dashboards.mdx +102 -0
  151. package/docs/observability/query.mdx +585 -0
  152. package/docs/open-source-contributing.mdx +16 -0
  153. package/docs/open-source-self-hosting.mdx +541 -0
  154. package/docs/private-networking/aws-console-setup.mdx +304 -0
  155. package/docs/private-networking/overview.mdx +144 -0
  156. package/docs/private-networking/troubleshooting.mdx +78 -0
  157. package/docs/queue-concurrency.mdx +354 -0
  158. package/docs/quick-start.mdx +97 -0
  159. package/docs/realtime/auth.mdx +208 -0
  160. package/docs/realtime/backend/overview.mdx +45 -0
  161. package/docs/realtime/backend/streams.mdx +418 -0
  162. package/docs/realtime/backend/subscribe.mdx +225 -0
  163. package/docs/realtime/how-it-works.mdx +94 -0
  164. package/docs/realtime/overview.mdx +63 -0
  165. package/docs/realtime/react-hooks/overview.mdx +73 -0
  166. package/docs/realtime/react-hooks/streams.mdx +449 -0
  167. package/docs/realtime/react-hooks/subscribe.mdx +674 -0
  168. package/docs/realtime/react-hooks/swr.mdx +87 -0
  169. package/docs/realtime/react-hooks/triggering.mdx +194 -0
  170. package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
  171. package/docs/realtime/run-object.mdx +174 -0
  172. package/docs/replaying.mdx +72 -0
  173. package/docs/request-feature.mdx +6 -0
  174. package/docs/roadmap.mdx +6 -0
  175. package/docs/run-tests.mdx +20 -0
  176. package/docs/run-usage.mdx +113 -0
  177. package/docs/runs/heartbeats.mdx +38 -0
  178. package/docs/runs/max-duration.mdx +139 -0
  179. package/docs/runs/metadata.mdx +734 -0
  180. package/docs/runs/priority.mdx +31 -0
  181. package/docs/runs.mdx +396 -0
  182. package/docs/self-hosting/docker.mdx +458 -0
  183. package/docs/self-hosting/env/supervisor.mdx +74 -0
  184. package/docs/self-hosting/env/webapp.mdx +276 -0
  185. package/docs/self-hosting/kubernetes.mdx +601 -0
  186. package/docs/self-hosting/overview.mdx +108 -0
  187. package/docs/skills.mdx +85 -0
  188. package/docs/tags.mdx +120 -0
  189. package/docs/tasks/overview.mdx +697 -0
  190. package/docs/tasks/scheduled.mdx +382 -0
  191. package/docs/tasks/schemaTask.mdx +413 -0
  192. package/docs/tasks/streams.mdx +884 -0
  193. package/docs/triggering.mdx +1320 -0
  194. package/docs/troubleshooting-alerts.mdx +385 -0
  195. package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
  196. package/docs/troubleshooting-github-issues.mdx +6 -0
  197. package/docs/troubleshooting-uptime-status.mdx +6 -0
  198. package/docs/troubleshooting.mdx +398 -0
  199. package/docs/upgrading-packages.mdx +80 -0
  200. package/docs/vercel-integration.mdx +207 -0
  201. package/docs/versioning.mdx +56 -0
  202. package/docs/video-walkthrough.mdx +23 -0
  203. package/docs/wait-for-token.mdx +540 -0
  204. package/docs/wait-for.mdx +42 -0
  205. package/docs/wait-until.mdx +53 -0
  206. package/docs/wait.mdx +18 -0
  207. package/docs/writing-tasks-introduction.mdx +33 -0
  208. package/package.json +10 -6
  209. package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
  210. package/skills/trigger-authoring-tasks/SKILL.md +254 -0
  211. package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
  212. package/skills/trigger-cost-savings/SKILL.md +116 -0
  213. package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
@@ -0,0 +1,337 @@
1
+ ---
2
+ title: "Trusted edge signals"
3
+ sidebarTitle: "Trusted edge signals"
4
+ description: "How to safely deliver server-trusted signals (bot scores, JA4, ASN, ReCAPTCHA verdicts) to a chat.agent run via an edge proxy."
5
+ ---
6
+
7
+ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8
+
9
+ <RcBanner />
10
+
11
+ A common need for chat-style endpoints is to drive agent behavior from **server-trusted signals** that the browser cannot be allowed to declare itself — bot management scores, JA4 fingerprints, ASN, ReCAPTCHA verdicts, or any other anti-abuse data only the edge can see. The agent's [`clientData`](/ai-chat/reference#withclientdata) channel is the right delivery mechanism, but `clientData` set in the browser is by definition spoofable. The fix is to move the value population out of the browser and into a trusted edge proxy.
12
+
13
+ This page documents the pattern using Cloudflare Workers as the proxy. The same shape applies to any edge layer (custom reverse proxy, Vercel Edge Middleware, AWS Lambda@Edge) — the trust comes from the deployment topology, not from Trigger.dev validating the source.
14
+
15
+ ## Why headers don't work
16
+
17
+ It's tempting to ask whether `POST /realtime/v1/sessions/{id}/in/append` could carry the signal as an HTTP header. It cannot. The realtime route reads only `Authorization` and `X-Part-Id`; the remaining headers are dropped at the route boundary and the body is persisted to the durable stream as opaque bytes. There is no `headers → run payload` channel.
18
+
19
+ The trigger.dev wire payload, on the other hand, has a typed per-turn metadata channel ([`ChatTaskWirePayload.metadata`](/ai-chat/client-protocol#chattaskwirepayload)). It already flows from the wire into [`clientData`](/ai-chat/reference#withclientdata) on every hook (`onBoot`, `onChatStart`, `onTurnStart`, `run`, `onTurnComplete`). That field is where signals must land.
20
+
21
+ ## The trust boundary
22
+
23
+ The pattern has one architectural requirement and one wire-shape convention.
24
+
25
+ **Topology**: the browser must not be able to reach `trigger.dev` directly. All four chat-related requests (`POST /api/v1/sessions`, `GET /realtime/v1/sessions/{id}/out`, `POST /realtime/v1/sessions/{id}/in/append`, `POST /api/v1/auth/jwt/claims`) flow through your edge proxy. The proxy holds the trust; trigger.dev simply persists whatever the proxy writes.
26
+
27
+ **Namespace**: pick a key your edge proxy owns exclusively — e.g. `__cf`, `__edge`, `__trust`. The proxy **strips** anything in that key on the way in and **injects** its own value on every request. Nothing else in your system should write that key. This is the convention that converts deployment topology into a guarantee the agent can rely on.
28
+
29
+ ```mermaid
30
+ sequenceDiagram
31
+ participant Browser
32
+ participant Edge as Edge Proxy (CF Worker)
33
+ participant Trigger as trigger.dev API
34
+ participant Agent as chat.agent run
35
+
36
+ Browser->>Edge: POST /api/v1/sessions { triggerConfig.basePayload.metadata: {...} }
37
+ Edge->>Edge: strip body.triggerConfig.basePayload.metadata.__cf<br/>inject body.triggerConfig.basePayload.metadata.__cf = { botScore, ja4, asn }
38
+ Edge->>Trigger: POST /api/v1/sessions (rewritten body)
39
+ Trigger-->>Agent: run boots with payload.metadata.__cf
40
+ Browser->>Edge: POST /realtime/v1/sessions/{id}/in/append { kind: "message", payload: {...} }
41
+ Edge->>Edge: strip payload.metadata.__cf<br/>inject payload.metadata.__cf
42
+ Edge->>Trigger: POST /in/append (rewritten body)
43
+ Trigger-->>Agent: chat.messages.wait() resolves with payload.metadata.__cf
44
+ ```
45
+
46
+ ## Wire payload — the two endpoints to rewrite
47
+
48
+ The signal needs to land in **two** places. Both bodies are JSON; the edge proxy parses, mutates the namespaced key, and re-serializes.
49
+
50
+ ### `POST /api/v1/sessions` — session create
51
+
52
+ The browser's session-create call carries the first-turn metadata under `triggerConfig.basePayload.metadata`. The proxy mutates that:
53
+
54
+ ```ts
55
+ // Before
56
+ {
57
+ "type": "chat.agent",
58
+ "externalId": "conv-123",
59
+ "taskIdentifier": "my-agent",
60
+ "triggerConfig": {
61
+ "basePayload": {
62
+ "chatId": "conv-123",
63
+ "trigger": "preload",
64
+ "metadata": { "userId": "user-456" }
65
+ }
66
+ }
67
+ }
68
+
69
+ // After
70
+ {
71
+ "type": "chat.agent",
72
+ "externalId": "conv-123",
73
+ "taskIdentifier": "my-agent",
74
+ "triggerConfig": {
75
+ "basePayload": {
76
+ "chatId": "conv-123",
77
+ "trigger": "preload",
78
+ "metadata": {
79
+ "userId": "user-456",
80
+ "__cf": { "botScore": 95, "ja4": "...", "asn": 13335, "country": "US" }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### `POST /realtime/v1/sessions/{id}/in/append` — every follow-up turn
88
+
89
+ The body is a JSON-serialized `ChatInputChunk`. The proxy parses it, checks `kind === "message"`, and mutates `payload.metadata`:
90
+
91
+ ```ts
92
+ // Before
93
+ {
94
+ "kind": "message",
95
+ "payload": {
96
+ "message": { "id": "u-2", "role": "user", "parts": [{ "type": "text", "text": "..." }] },
97
+ "chatId": "conv-123",
98
+ "trigger": "submit-message",
99
+ "metadata": { "userId": "user-456" }
100
+ }
101
+ }
102
+
103
+ // After
104
+ {
105
+ "kind": "message",
106
+ "payload": {
107
+ "message": { ... },
108
+ "chatId": "conv-123",
109
+ "trigger": "submit-message",
110
+ "metadata": {
111
+ "userId": "user-456",
112
+ "__cf": { "botScore": 95, "ja4": "...", "asn": 13335, "country": "US" }
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ Both bodies stay well under the [per-record cap on `/in/append`](/ai-chat/client-protocol#step-3-send-messages-stops-and-actions) — a typical trust object is ~200 bytes.
119
+
120
+ Other paths — `.out` SSE, `/api/v1/auth/jwt/claims`, anything else — pass through the proxy untouched. The SSE stream in particular must not be buffered; preserve the response body as-is.
121
+
122
+ ## Cloudflare Worker reference implementation
123
+
124
+ A complete worker that proxies all paths to `TRIGGER_API_UPSTREAM` and injects `__cf` on the two body-write endpoints:
125
+
126
+ ```ts
127
+ export interface Env {
128
+ TRIGGER_API_UPSTREAM: string; // e.g. "https://api.trigger.dev"
129
+ }
130
+
131
+ type CfTrustData = {
132
+ botScore: number;
133
+ ja4: string;
134
+ asn: number;
135
+ country: string;
136
+ };
137
+
138
+ function readCfTrustData(request: Request): CfTrustData {
139
+ const cf = (request as Request & { cf?: Record<string, unknown> }).cf;
140
+ const bm = cf?.botManagement as Record<string, unknown> | undefined;
141
+ return {
142
+ botScore: (bm?.score as number) ?? 0,
143
+ ja4: (bm?.ja4 as string) ?? "",
144
+ asn: (cf?.asn as number) ?? 0,
145
+ country: (cf?.country as string) ?? "",
146
+ };
147
+ }
148
+
149
+ function injectCf(metadata: Record<string, unknown> | undefined, cf: CfTrustData) {
150
+ // Strip anything the client tried to send under our namespace,
151
+ // then inject the edge-trusted value. Topology + convention =
152
+ // trust.
153
+ const stripped = { ...(metadata ?? {}) };
154
+ delete stripped.__cf;
155
+ return { ...stripped, __cf: cf };
156
+ }
157
+
158
+ function rewriteSessionsCreate(body: string, cf: CfTrustData) {
159
+ const parsed = JSON.parse(body) as Record<string, unknown>;
160
+ const tc = (parsed.triggerConfig as Record<string, unknown>) ?? {};
161
+ const bp = (tc.basePayload as Record<string, unknown>) ?? {};
162
+ parsed.triggerConfig = {
163
+ ...tc,
164
+ basePayload: { ...bp, metadata: injectCf(bp.metadata as Record<string, unknown>, cf) },
165
+ };
166
+ return JSON.stringify(parsed);
167
+ }
168
+
169
+ function rewriteAppend(body: string, cf: CfTrustData) {
170
+ let parsed: Record<string, unknown>;
171
+ try {
172
+ parsed = JSON.parse(body);
173
+ } catch {
174
+ return body;
175
+ }
176
+ if (parsed.kind !== "message") return body;
177
+ const payload = (parsed.payload as Record<string, unknown>) ?? {};
178
+ parsed.payload = { ...payload, metadata: injectCf(payload.metadata as Record<string, unknown>, cf) };
179
+ return JSON.stringify(parsed);
180
+ }
181
+
182
+ export default {
183
+ async fetch(request: Request, env: Env): Promise<Response> {
184
+ const incoming = new URL(request.url);
185
+ const target = new URL(incoming.pathname + incoming.search, env.TRIGGER_API_UPSTREAM);
186
+ const cf = readCfTrustData(request);
187
+
188
+ const isSessionsCreate =
189
+ request.method === "POST" && incoming.pathname === "/api/v1/sessions";
190
+ const isAppend =
191
+ request.method === "POST" &&
192
+ /^\/realtime\/v1\/sessions\/[^/]+\/in\/append$/.test(incoming.pathname);
193
+
194
+ let body: BodyInit | null = null;
195
+ if (request.method !== "GET" && request.method !== "HEAD") {
196
+ const raw = await request.text();
197
+ if (isSessionsCreate && raw) body = rewriteSessionsCreate(raw, cf);
198
+ else if (isAppend && raw) body = rewriteAppend(raw, cf);
199
+ else body = raw;
200
+ }
201
+
202
+ const headers = new Headers(request.headers);
203
+ headers.delete("host");
204
+ headers.delete("content-length");
205
+
206
+ return fetch(target.toString(), {
207
+ method: request.method,
208
+ headers,
209
+ body,
210
+ redirect: "manual",
211
+ });
212
+ },
213
+ };
214
+ ```
215
+
216
+ Browser-only deployments also need CORS on the worker — echo `Access-Control-Request-Headers` on preflight and set `Access-Control-Allow-Origin` to your frontend origin. The trigger.dev route itself allows all origins, but the worker becomes the visible cross-origin endpoint to the browser.
217
+
218
+ ### Streaming and latency
219
+
220
+ The SDK's `baseURL` accepts a function (see [Browser transport configuration](#browser-transport-configuration)), so the recommended setup routes `.in/append` and session-create through the worker but lets `.out` SSE go direct to `api.trigger.dev`. Body-mutation only happens on the POST paths; the SSE stream is read-only, doesn't need rewriting, and routing it direct saves an edge hop on every reconnect.
221
+
222
+ If you do route `.out` through the proxy (e.g. you want a single origin in front of `api.trigger.dev` and don't care about the extra hop), the template above handles it correctly because the worker returns `response.body` as a `ReadableStream`. **Do not replace that with `await response.text()`** anywhere in your fork; doing so converts the streaming SSE response into a buffered read and breaks per-chunk delivery.
223
+
224
+ [Cloudflare Workers HTTP requests](https://developers.cloudflare.com/workers/platform/limits/) have no wall-clock duration limit while the client stays connected — the 60-second long-poll runs to completion on every plan, including Free. CPU-time limits (10 ms on Free, 30 s default on Paid) only apply to active computation; relaying bytes through `fetch` doesn't burn CPU. The two body-rewrite paths use sub-millisecond CPU for typical message sizes, well under either ceiling.
225
+
226
+ Network-wise the proxy adds one edge hop: roughly 10–50 ms per request round trip versus talking to `api.trigger.dev` directly. Routing SSE direct via the function-form `baseURL` eliminates that hop on the long-lived path.
227
+
228
+ ## Agent side — declare the namespace in `clientDataSchema`
229
+
230
+ Mirror the namespace in the agent so every turn lands typed:
231
+
232
+ ```ts
233
+ import { chat } from "@trigger.dev/sdk/ai";
234
+ import { z } from "zod";
235
+
236
+ export const myAgent = chat
237
+ .withClientData({
238
+ schema: z.object({
239
+ userId: z.string(),
240
+ __cf: z.object({
241
+ botScore: z.number(),
242
+ ja4: z.string(),
243
+ asn: z.number(),
244
+ country: z.string(),
245
+ }),
246
+ }),
247
+ })
248
+ .agent({
249
+ id: "my-agent",
250
+ run: async ({ messages, clientData, signal }) => {
251
+ // Score-based routing. The values arrive from the edge proxy.
252
+ if (clientData.__cf.botScore < 30) {
253
+ return streamText({
254
+ model: anthropic("claude-haiku-4-5"),
255
+ messages: [{ role: "system", content: "Reject politely; do not engage." }],
256
+ abortSignal: signal,
257
+ stopWhen: stepCountIs(15),
258
+ });
259
+ }
260
+
261
+ return streamText({
262
+ model: anthropic("claude-sonnet-4-5"),
263
+ messages,
264
+ abortSignal: signal,
265
+ // ...
266
+ stopWhen: stepCountIs(15),
267
+ });
268
+ },
269
+ });
270
+ ```
271
+
272
+ Because the schema requires `__cf` on every turn, any request that *doesn't* go through the proxy fails at the agent boundary — the turn produces a `[ERROR]` span on the trace and an empty `turn-complete` on the wire (see [the client protocol error-detection note](/ai-chat/client-protocol#step-3-send-messages-stops-and-actions)). That gives you a server-side enforcement check for "did this request actually come through the trusted path?"
273
+
274
+ ## Browser transport configuration
275
+
276
+ Point the `TriggerChatTransport` at the worker, not at `api.trigger.dev`:
277
+
278
+ `baseURL` accepts a function so you can route `.in/append` through the worker while keeping `.out` SSE direct to `api.trigger.dev`. The append path is where the body-mutation matters; the SSE stream is a read-only one-way channel that doesn't need to be proxied. Routing it direct saves an edge hop on every long-poll.
279
+
280
+ ```tsx
281
+ import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
282
+
283
+ const WORKER = "https://worker.your-domain.com";
284
+ const DIRECT = "https://api.trigger.dev";
285
+
286
+ const transport = useTriggerChatTransport({
287
+ task: "my-agent",
288
+ baseURL: ({ endpoint }) => (endpoint === "out" ? DIRECT : WORKER),
289
+ // ... accessToken, startSession, etc.
290
+ // NOTE: do not set __cf in clientData here. The browser cannot be
291
+ // trusted to populate it — the worker is the source of truth.
292
+ clientData: { userId: currentUserId },
293
+ });
294
+ ```
295
+
296
+ If you'd rather route everything through the worker, pass a single string:
297
+
298
+ ```tsx
299
+ baseURL: "https://worker.your-domain.com",
300
+ ```
301
+
302
+ `baseURL` accepts the same string-or-function shape on `chat.createStartSessionAction`, so the Next.js server action that creates the session also flows through the worker — that's how the very first run's `basePayload.metadata.__cf` gets injected before reaching `api.trigger.dev`:
303
+
304
+ ```ts
305
+ // actions.ts — server-only
306
+ import { chat } from "@trigger.dev/sdk/ai";
307
+
308
+ export const startSession = chat.createStartSessionAction("my-agent", {
309
+ tokenTTL: "1h",
310
+ baseURL: ({ endpoint }) =>
311
+ endpoint === "sessions" ? WORKER : DIRECT,
312
+ });
313
+ ```
314
+
315
+ The session-create endpoint discriminator is `"sessions"` (POST `/api/v1/sessions`) or `"auth"` (POST `/api/v1/auth/jwt/claims`) — distinct from the chat transport's `"in"` / `"out"`. If you want everything proxied, pass a string.
316
+
317
+ ## Threat model
318
+
319
+ Two important invariants follow from this design:
320
+
321
+ 1. **Direct browser-to-trigger.dev requests cannot succeed**. As long as your agent's `clientDataSchema` requires the namespaced field, any request that doesn't go through the proxy fails schema validation and produces an empty turn. This is your gate.
322
+ 2. **Anything inside the namespaced key is trusted only as far as the proxy is the sole writer**. If a client could obtain the public access token and bypass the proxy, they could send arbitrary values under `__cf`. The schema would still validate (it only checks shape, not provenance). The mitigation is operational: the public access token must only be served to clients that reach trigger.dev through the proxy. In practice this means your Next.js server actions and your browser are both behind the same edge layer, and the worker is the only fetch destination for `trigger.dev` baked into either of them.
323
+
324
+ You can harden further with a shared-secret header the worker injects (e.g. `X-Edge-Signature`) and an agent-side check, but in most CDN deployments the deployment topology is already sufficient.
325
+
326
+ ## Recipe summary
327
+
328
+ 1. Pick a namespaced key the edge proxy owns (`__cf`, `__edge`, `__trust`).
329
+ 2. Deploy a proxy in front of `trigger.dev` that rewrites POST `/api/v1/sessions` and POST `/realtime/v1/sessions/{id}/in/append` to inject your trusted values under that key.
330
+ 3. Declare the namespace in the agent's `clientDataSchema` so missing or malformed signals fail at the agent boundary.
331
+ 4. Point your transport's `baseURL` at the proxy. Never expose `api.trigger.dev` directly to the browser.
332
+
333
+ ## See also
334
+
335
+ - [Client Protocol](/ai-chat/client-protocol) — the full wire shape the proxy is rewriting.
336
+ - [`withClientData`](/ai-chat/reference#withclientdata) — agent-side typed metadata channel.
337
+ - [Large payloads](/ai-chat/patterns/large-payloads) — for when injected signals or hooks need to ship more than the 1 MiB stream cap allows.
@@ -0,0 +1,172 @@
1
+ ---
2
+ title: "Version upgrades"
3
+ sidebarTitle: "Version upgrades"
4
+ description: "Gracefully migrate suspended chat agents to a new deployment using chat.requestUpgrade() and the continuation mechanism."
5
+ ---
6
+
7
+ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8
+
9
+ <RcBanner />
10
+
11
+ Chat agent runs are pinned to the worker version they started on. When you deploy a new version, suspended runs resume on the **old** code. If your deploy includes breaking changes (new tools, changed schemas, updated API contracts), this can cause issues.
12
+
13
+ `chat.requestUpgrade()` lets the agent opt out of the current run so the transport triggers a new one on the latest version.
14
+
15
+ ## How it works
16
+
17
+ When `chat.requestUpgrade()` is called in `onTurnStart` or `onValidateMessages`:
18
+
19
+ 1. `run()` is **skipped** — no response is generated on old code
20
+ 2. The agent calls the server-side `endAndContinueSession` endpoint, which atomically swaps the Session's `currentRunId` to a freshly-triggered run on the latest deployment (optimistic-claim against `currentRunVersion`)
21
+ 3. The new run picks up the conversation and produces the response
22
+ 4. The transport's existing SSE subscription to `session.out` keeps receiving chunks across the swap — no client-side reconnect
23
+
24
+ The new run lives on the **same Session** as the old one. `chatId` is the durable identity; only the underlying `currentRunId` rotates. The audit log records the new run with `reason: "upgrade"`.
25
+
26
+ When called from inside `run()` or `chat.defer()`, the current turn completes normally first and the run exits afterward. The next message triggers the continuation on the same session.
27
+
28
+ ```mermaid
29
+ sequenceDiagram
30
+ participant User
31
+ participant Transport
32
+ participant RunV1 as Run (v1)
33
+ participant RunV2 as Run (v2)
34
+
35
+ User->>Transport: send message
36
+ Transport->>RunV1: input stream
37
+ RunV1->>RunV1: onTurnStart → requestUpgrade()
38
+ RunV1-->>Transport: trigger:upgrade-required
39
+ RunV1->>RunV1: exit (run() never called)
40
+ Transport->>RunV2: trigger new run (continuation, same message)
41
+ RunV2-->>Transport: response stream
42
+ Transport-->>User: response (seamless)
43
+ ```
44
+
45
+ ## Contract versioning
46
+
47
+ Define an explicit version for the contract between your frontend and agent. The frontend sends a `protocolVersion` via `clientData`, and the agent declares which versions it supports. When a breaking change ships (new tools, changed data parts, updated response format), bump the version.
48
+
49
+ This gives you full control — the frontend can be backwards-compatible across multiple agent versions, and the agent only upgrades when it sees a version it doesn't support.
50
+
51
+ ```tsx title="app/components/Chat.tsx"
52
+ import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
53
+ import { useChat } from "@ai-sdk/react";
54
+
55
+ export function Chat() {
56
+ const transport = useTriggerChatTransport({
57
+ task: "my-chat",
58
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
59
+ startSession: ({ chatId, clientData }) =>
60
+ startChatSession({ chatId, clientData }),
61
+ // Bump this when you ship a breaking change to the chat UI or tools
62
+ clientData: { userId: user.id, protocolVersion: "v2" },
63
+ });
64
+
65
+ const { messages, sendMessage } = useChat({ transport });
66
+ // ...
67
+ }
68
+ ```
69
+
70
+ On the agent side, declare which versions the current code supports:
71
+
72
+ ```ts
73
+ import { chat } from "@trigger.dev/sdk/ai";
74
+ import { streamText } from "ai";
75
+ import { anthropic } from "@ai-sdk/anthropic";
76
+
77
+ // The set of frontend protocol versions this agent code supports.
78
+ // When you deploy a breaking change, remove old versions from this set.
79
+ const SUPPORTED_VERSIONS = new Set(["v2", "v3"]);
80
+
81
+ export const myChat = chat
82
+ .withClientData({
83
+ schema: z.object({
84
+ userId: z.string(),
85
+ protocolVersion: z.string(),
86
+ }),
87
+ })
88
+ .agent({
89
+ id: "my-chat",
90
+ onTurnStart: async ({ clientData }) => {
91
+ if (clientData?.protocolVersion && !SUPPORTED_VERSIONS.has(clientData.protocolVersion)) {
92
+ chat.requestUpgrade();
93
+ }
94
+ },
95
+ run: async ({ messages, signal }) => {
96
+ return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
97
+ },
98
+ });
99
+ ```
100
+
101
+ The transport includes `clientData` in every payload — both the initial trigger and subsequent records on the session's `.in` channel — so the agent always has the current value.
102
+
103
+ This pattern is useful when:
104
+ - Your frontend is backwards-compatible across several agent versions, but occasionally ships breaking changes
105
+ - You want explicit control over when upgrades happen rather than upgrading on every deploy
106
+ - Multiple frontend versions may be active at the same time (e.g., users with cached tabs)
107
+
108
+ ## Auto-detect from build ID (Next.js / Vercel)
109
+
110
+ For automatic upgrade on every deploy, pass your platform's build ID via `clientData` instead of a manual version. The agent stores the ID from the first message and upgrades when it changes:
111
+
112
+ ```tsx title="app/components/Chat.tsx"
113
+ // Vercel sets this at build time, or use your own build ID
114
+ const APP_VERSION = process.env.NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID
115
+ ?? process.env.NEXT_PUBLIC_BUILD_ID
116
+ ?? "dev";
117
+
118
+ export function Chat() {
119
+ const transport = useTriggerChatTransport({
120
+ task: "my-chat",
121
+ accessToken: ({ chatId }) => mintChatAccessToken(chatId),
122
+ startSession: ({ chatId, clientData }) =>
123
+ startChatSession({ chatId, clientData }),
124
+ clientData: { userId: user.id, appVersion: APP_VERSION },
125
+ });
126
+ // ...
127
+ }
128
+ ```
129
+
130
+ ```ts title="trigger/chat.ts"
131
+ const initialAppVersion = chat.local<{ version: string }>({ id: "appVersion" });
132
+
133
+ export const myChat = chat
134
+ .withClientData({
135
+ schema: z.object({
136
+ userId: z.string(),
137
+ appVersion: z.string(),
138
+ }),
139
+ })
140
+ .agent({
141
+ id: "my-chat",
142
+ onBoot: async ({ clientData }) => {
143
+ initialAppVersion.init({ version: clientData.appVersion });
144
+ },
145
+ onTurnStart: async ({ clientData }) => {
146
+ if (clientData?.appVersion && clientData.appVersion !== initialAppVersion.version) {
147
+ chat.requestUpgrade();
148
+ }
149
+ },
150
+ run: async ({ messages, signal }) => {
151
+ return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
152
+ },
153
+ });
154
+ ```
155
+
156
+ This upgrades on **every** deploy, not just breaking changes. Good for fast-moving projects where you always want the latest code.
157
+
158
+ ## Other agent types
159
+
160
+ - **`chat.agent()`** and **`chat.createSession()`** — use `chat.requestUpgrade()` as shown above
161
+ - **`chat.customAgent()`** — you control the turn loop, so just `return` from `run()` when you want to exit
162
+
163
+ ## Interaction with recovery boot
164
+
165
+ `chat.requestUpgrade()` is a graceful exit — the old run returns cleanly, never writing a partial assistant. The new continuation run boots with an empty `session.out` tail and the upgrade-trigger message on `session.in`. The trigger message dispatches as turn 1 on the new version via the normal continuation-wait path. [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot) does NOT fire on this path — the hook is reserved for mid-stream interruptions (cancel / crash / OOM) where a partial assistant exists on the tail.
166
+
167
+ ## See also
168
+
169
+ - [Lifecycle hooks](/ai-chat/lifecycle-hooks) — where `onTurnStart` and `onChatResume` fit in the turn cycle
170
+ - [Recovery boot](/ai-chat/patterns/recovery-boot) — the sibling hook for mid-stream interruptions (does NOT fire on `requestUpgrade`)
171
+ - [Database persistence](/ai-chat/patterns/database-persistence) — how continuations interact with session state
172
+ - [Client Protocol](/ai-chat/client-protocol#step-4-handle-continuations) — how clients handle continuations at the wire level