@trigger.dev/sdk 4.5.9 → 4.5.11
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/package.json +2 -1
- package/dist/commonjs/v3/agentSkillsRuntime.js.map +1 -1
- package/dist/commonjs/v3/ai-shared.js.map +1 -1
- package/dist/commonjs/v3/ai.js +15 -1
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/aiAutoTelemetry.js.map +1 -1
- package/dist/commonjs/v3/auth.js.map +1 -1
- package/dist/commonjs/v3/batchDebounce.test.d.ts +1 -0
- package/dist/commonjs/v3/batchDebounce.test.js +189 -0
- package/dist/commonjs/v3/batchDebounce.test.js.map +1 -0
- package/dist/commonjs/v3/cache.js.map +1 -1
- package/dist/commonjs/v3/chat-client.d.ts +14 -1
- package/dist/commonjs/v3/chat-client.js +19 -2
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.js +1 -1
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-tab-coordinator.js.map +1 -1
- package/dist/commonjs/v3/chat-tab-coordinator.test.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +7 -0
- package/dist/commonjs/v3/chat.js +139 -35
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +637 -2
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/config.js.map +1 -1
- package/dist/commonjs/v3/envvars.js.map +1 -1
- package/dist/commonjs/v3/hooks.js.map +1 -1
- package/dist/commonjs/v3/index.js.map +1 -1
- package/dist/commonjs/v3/locals.js.map +1 -1
- package/dist/commonjs/v3/metadata.js.map +1 -1
- package/dist/commonjs/v3/prompt.js.map +1 -1
- package/dist/commonjs/v3/promptManagement.js.map +1 -1
- package/dist/commonjs/v3/queues.js.map +1 -1
- package/dist/commonjs/v3/retry.js.map +1 -1
- package/dist/commonjs/v3/runs.d.ts +81 -81
- package/dist/commonjs/v3/runs.js.map +1 -1
- package/dist/commonjs/v3/schedules/index.js.map +1 -1
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/shared.d.ts +2 -1
- package/dist/commonjs/v3/shared.js +7 -0
- package/dist/commonjs/v3/shared.js.map +1 -1
- package/dist/commonjs/v3/shared.test.js +9 -0
- package/dist/commonjs/v3/shared.test.js.map +1 -1
- package/dist/commonjs/v3/skill.js.map +1 -1
- package/dist/commonjs/v3/streams.js.map +1 -1
- package/dist/commonjs/v3/tasks.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
- package/dist/commonjs/v3/triggerClient.js.map +1 -1
- package/dist/commonjs/v3/triggerClient.types.test.js.map +1 -1
- package/dist/commonjs/v3/wait.d.ts +6 -1
- package/dist/commonjs/v3/waitUntil.js.map +1 -1
- package/dist/commonjs/v3/webhooks.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/package.json +2 -1
- package/dist/esm/v3/ai.js +15 -1
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/auth.js.map +1 -1
- package/dist/esm/v3/batchDebounce.test.d.ts +1 -0
- package/dist/esm/v3/batchDebounce.test.js +187 -0
- package/dist/esm/v3/batchDebounce.test.js.map +1 -0
- package/dist/esm/v3/chat-client.d.ts +14 -1
- package/dist/esm/v3/chat-client.js +19 -2
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-tab-coordinator.js.map +1 -1
- package/dist/esm/v3/chat-tab-coordinator.test.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +7 -0
- package/dist/esm/v3/chat.js +139 -35
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +637 -2
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/metadata.js.map +1 -1
- package/dist/esm/v3/retry.js.map +1 -1
- package/dist/esm/v3/runs.d.ts +80 -80
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/shared.d.ts +2 -1
- package/dist/esm/v3/shared.js +6 -0
- package/dist/esm/v3/shared.js.map +1 -1
- package/dist/esm/v3/shared.test.js +10 -1
- package/dist/esm/v3/shared.test.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/test/test-session-handle.js.map +1 -1
- package/dist/esm/v3/triggerClient.js.map +1 -1
- package/dist/esm/v3/wait.d.ts +6 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/observability.mdx +174 -0
- package/docs/ai-chat/client-protocol.mdx +4 -2
- package/docs/ai-chat/fast-starts.mdx +4 -1
- package/docs/ai-chat/frontend.mdx +10 -9
- package/docs/ai-chat/migrating-from-a-route-handler.mdx +592 -0
- package/docs/ai-chat/overview.mdx +1 -1
- package/docs/config/extensions/emitDecoratorMetadata.mdx +27 -4
- package/docs/mcp-tools.mdx +1 -1
- package/docs/self-hosting/docker.mdx +9 -0
- package/docs/self-hosting/env/supervisor.mdx +2 -1
- package/docs/self-hosting/env/webapp.mdx +12 -1
- package/docs/self-hosting/kubernetes.mdx +281 -221
- package/docs/triggering.mdx +16 -4
- package/package.json +4 -3
package/dist/esm/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "4.5.
|
|
1
|
+
export const VERSION = "4.5.11";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "LLM observability"
|
|
3
|
+
sidebarTitle: "LLM observability"
|
|
4
|
+
description: "Capture Vercel AI SDK calls in a task as spans in the run trace, with model, token usage, cost, and latency. Opt in per call, link calls to prompt versions, and query usage across runs."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**LLM observability turns a Vercel AI SDK call inside a task into its own span in the run trace, next to your logs and other spans.** Each span carries the model, provider, input, output, and total token counts, cost, and latency, so you can see what each generation did and what it cost without leaving the run.
|
|
8
|
+
|
|
9
|
+
Everything shows up inline in the run trace you already use to debug runs. There is no separate product and no dashboard to set up.
|
|
10
|
+
|
|
11
|
+
<Note>
|
|
12
|
+
Observability is opt-in per call and only covers [Vercel AI SDK](https://ai-sdk.dev) functions (`generateText`, `streamText`, `generateObject`). Calls you make with a raw `fetch`, a provider's own SDK, or any other HTTP client are not captured automatically.
|
|
13
|
+
</Note>
|
|
14
|
+
|
|
15
|
+
## Turn it on
|
|
16
|
+
|
|
17
|
+
Set `experimental_telemetry: { isEnabled: true }` on the AI SDK call. There is nothing to install for AI SDK 6, and nothing to configure on the Trigger.dev side.
|
|
18
|
+
|
|
19
|
+
```ts /trigger/summarize.ts
|
|
20
|
+
import { task } from "@trigger.dev/sdk";
|
|
21
|
+
import { generateText } from "ai";
|
|
22
|
+
import { openai } from "@ai-sdk/openai";
|
|
23
|
+
|
|
24
|
+
export const summarize = task({
|
|
25
|
+
id: "summarize",
|
|
26
|
+
run: async (payload: { text: string }) => {
|
|
27
|
+
const result = await generateText({
|
|
28
|
+
model: openai("gpt-4o"),
|
|
29
|
+
prompt: `Summarize the following text:\n\n${payload.text}`,
|
|
30
|
+
experimental_telemetry: { isEnabled: true },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return { summary: result.text };
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Trigger the task and open the run. The `generateText` call appears as a span in the trace. `streamText` and `generateObject` work the same way: add the same `experimental_telemetry` flag to each call you want captured.
|
|
39
|
+
|
|
40
|
+
<Note>
|
|
41
|
+
**AI SDK 7** moved span emission out of `ai` core into the `@ai-sdk/otel` adapter. In a task, install `@ai-sdk/otel` and register it once yourself, for example at the top of your task file:
|
|
42
|
+
|
|
43
|
+
```ts /trigger/summarize.ts
|
|
44
|
+
import { registerTelemetry } from "ai";
|
|
45
|
+
import { OpenTelemetry } from "@ai-sdk/otel";
|
|
46
|
+
|
|
47
|
+
registerTelemetry(new OpenTelemetry());
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A [`chat.agent()`](/ai-chat/overview) run registers the adapter for you at run start, so chat agents need only the install. On AI SDK 5 and 6, `ai` core emits spans directly and no adapter is needed.
|
|
51
|
+
</Note>
|
|
52
|
+
|
|
53
|
+
## What each span shows
|
|
54
|
+
|
|
55
|
+
Open an AI generation span in the run trace to get a dedicated inspector with three tabs:
|
|
56
|
+
|
|
57
|
+
- **Overview**: model, provider, token usage, cost, and a preview of the input and output.
|
|
58
|
+
- **Messages**: the full message thread, including the system prompt and any tool results.
|
|
59
|
+
- **Tools**: the tool definitions passed to the model, plus every tool call the model made with its arguments.
|
|
60
|
+
|
|
61
|
+
A fourth **Prompt** tab appears when the call is linked to an [AI Prompt](/ai/prompts) (see below).
|
|
62
|
+
|
|
63
|
+
## Link a call to its prompt
|
|
64
|
+
|
|
65
|
+
If you manage prompts with [AI Prompts](/ai/prompts), resolve the prompt and spread `toAISDKTelemetry()` into the call. This sets `experimental_telemetry` for you and links the span back to the exact prompt version that produced it.
|
|
66
|
+
|
|
67
|
+
```ts /trigger/support.ts
|
|
68
|
+
import { task, prompts } from "@trigger.dev/sdk";
|
|
69
|
+
import { generateText } from "ai";
|
|
70
|
+
import { openai } from "@ai-sdk/openai";
|
|
71
|
+
import type { supportPrompt } from "./prompts";
|
|
72
|
+
|
|
73
|
+
export const handleSupport = task({
|
|
74
|
+
id: "handle-support",
|
|
75
|
+
run: async (payload: { name: string; plan: string; issue: string }) => {
|
|
76
|
+
const resolved = await prompts.resolve<typeof supportPrompt>("customer-support", {
|
|
77
|
+
customerName: payload.name,
|
|
78
|
+
plan: payload.plan,
|
|
79
|
+
issue: payload.issue,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const result = await generateText({
|
|
83
|
+
model: openai(resolved.model ?? "gpt-4o"),
|
|
84
|
+
system: resolved.text,
|
|
85
|
+
prompt: payload.issue,
|
|
86
|
+
...resolved.toAISDKTelemetry(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return { response: result.text };
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The span's **Prompt** tab now shows the linked template, its version, and the input variables the prompt was resolved with.
|
|
95
|
+
|
|
96
|
+
Pass custom attributes to `toAISDKTelemetry()` to tag the span with your own metadata:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const result = await generateText({
|
|
100
|
+
model: openai(resolved.model ?? "gpt-4o"),
|
|
101
|
+
system: resolved.text,
|
|
102
|
+
prompt: payload.issue,
|
|
103
|
+
...resolved.toAISDKTelemetry({
|
|
104
|
+
"task.type": "summarization",
|
|
105
|
+
"customer.tier": "enterprise",
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Custom attributes are stored on the span's `metadata`, so you can filter or group by them in TRQL, for example `metadata['task.type']`.
|
|
111
|
+
|
|
112
|
+
<Note>
|
|
113
|
+
When you build an agent with `chat.agent()` and store a prompt with `chat.prompt.set()`, `chat.toStreamTextOptions()` sets `experimental_telemetry` for you, so those generations are captured without adding the flag by hand. Without a stored prompt, set `experimental_telemetry` on the call yourself. See [Prompts](/ai/prompts#using-with-chatagent).
|
|
114
|
+
</Note>
|
|
115
|
+
|
|
116
|
+
## Query usage across runs
|
|
117
|
+
|
|
118
|
+
Every captured generation is also written to the `llm_metrics` table, which you can query with [TRQL](/observability/query). This lets you aggregate token usage, cost, and latency across many runs rather than inspecting one span at a time.
|
|
119
|
+
|
|
120
|
+
Cost and token usage by model:
|
|
121
|
+
|
|
122
|
+
```sql
|
|
123
|
+
SELECT
|
|
124
|
+
response_model,
|
|
125
|
+
gen_ai_system AS provider,
|
|
126
|
+
count() AS calls,
|
|
127
|
+
sum(total_tokens) AS tokens,
|
|
128
|
+
round(sum(total_cost), 4) AS cost_usd
|
|
129
|
+
FROM llm_metrics
|
|
130
|
+
GROUP BY response_model, gen_ai_system
|
|
131
|
+
ORDER BY cost_usd DESC
|
|
132
|
+
LIMIT 20
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Spend per task:
|
|
136
|
+
|
|
137
|
+
```sql
|
|
138
|
+
SELECT
|
|
139
|
+
task_identifier,
|
|
140
|
+
sum(input_tokens) AS input_tokens,
|
|
141
|
+
sum(output_tokens) AS output_tokens,
|
|
142
|
+
round(sum(total_cost), 4) AS cost_usd
|
|
143
|
+
FROM llm_metrics
|
|
144
|
+
GROUP BY task_identifier
|
|
145
|
+
ORDER BY cost_usd DESC
|
|
146
|
+
LIMIT 20
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Cost by prompt version, when calls are linked to an [AI Prompt](/ai/prompts):
|
|
150
|
+
|
|
151
|
+
```sql
|
|
152
|
+
SELECT
|
|
153
|
+
prompt_slug,
|
|
154
|
+
prompt_version,
|
|
155
|
+
count() AS calls,
|
|
156
|
+
round(sum(total_cost), 4) AS cost_usd
|
|
157
|
+
FROM llm_metrics
|
|
158
|
+
WHERE prompt_slug != ''
|
|
159
|
+
GROUP BY prompt_slug, prompt_version
|
|
160
|
+
ORDER BY prompt_slug, prompt_version
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Set the time window with the query's [period filter](/observability/query#time-ranges) rather than in the SQL itself. Run these from the [Query dashboard](/observability/query#using-the-query-dashboard), the SDK with `query.execute()`, or the REST API. `llm_metrics` also exposes `ms_to_first_chunk` and `tokens_per_second` for latency and throughput, plus `finish_reason`, `request_model`, `cached_read_tokens`, `reasoning_tokens`, and per-direction `input_cost` / `output_cost` for finer breakdowns.
|
|
164
|
+
|
|
165
|
+
## Next steps
|
|
166
|
+
|
|
167
|
+
<CardGroup cols={2}>
|
|
168
|
+
<Card title="Prompts" icon="message-lines" href="/ai/prompts">
|
|
169
|
+
Version prompts as code and link generations to the exact prompt version that produced them.
|
|
170
|
+
</Card>
|
|
171
|
+
<Card title="Query (TRQL)" icon="magnifying-glass-chart" href="/observability/query">
|
|
172
|
+
Write custom queries against your runs, metrics, and LLM usage.
|
|
173
|
+
</Card>
|
|
174
|
+
</CardGroup>
|
|
@@ -320,7 +320,7 @@ The output stream uses [S2](https://s2.dev) under the hood and follows the stand
|
|
|
320
320
|
| Event | Meaning |
|
|
321
321
|
| --- | --- |
|
|
322
322
|
| `batch` | One or more records. The records you actually care about. |
|
|
323
|
-
| `ping` | Keepalive (~every 5s on idle). Body is `{"timestamp": <ms>}
|
|
323
|
+
| `ping` | Keepalive (~every 5s on idle). Body is `{"timestamp": <ms>}`, and on backends that report it a `tail` (same `{seq_num, timestamp}` shape as a batch tail). The tail is what lets you tell you have caught up to the live edge; if you don't need caught-up detection, ignore the ping. |
|
|
324
324
|
| _(no `event:`, just `data: [DONE]`)_ | Stream is closing — server sends this once before EOF. |
|
|
325
325
|
|
|
326
326
|
A `batch` event in raw SSE format looks like this — note the `data` is a single line of JSON, no embedded newlines (per the SSE spec):
|
|
@@ -359,7 +359,7 @@ Decoded `data` payload:
|
|
|
359
359
|
| `records[].timestamp` | Unix ms when the record was written to S2. |
|
|
360
360
|
| `records[].body` | For data records: a JSON-encoded **string** wrapping `{ data: UIMessageChunk, id: string }`. For control records: an empty string (semantics live in `headers`). For S2 command records: opaque bytes. See [Records on session.out](#records-on-session-out). |
|
|
361
361
|
| `records[].headers` | Optional `[name, value]` pairs. Empty for data records; a `trigger-control` entry for control records; a single empty-name `["", "<op>"]` entry for S2 command records. |
|
|
362
|
-
| `tail.seq_num` | Latest known tail of the S2 stream
|
|
362
|
+
| `tail.seq_num` | Latest known tail of the S2 stream, useful for detecting how far behind the live edge you are. Track the **highest `seq_num` you have received**, counting every record in the batch (including the command records you skip, see below), not just the last application-visible one. When `highest received seq_num + 1 === tail.seq_num` you have drained the backlog and are caught up to the live edge. The same `tail` also rides on `ping` events. Skip if you don't need it. |
|
|
363
363
|
| `tail.timestamp` | Timestamp of `tail.seq_num`. |
|
|
364
364
|
|
|
365
365
|
### Records on `session.out`
|
|
@@ -652,6 +652,8 @@ On **reconnect-on-reload** paths (resuming a chat where nothing may be streaming
|
|
|
652
652
|
|
|
653
653
|
**Do not send `X-Peek-Settled` on the active-send response-stream path.** The peek would race the newly-triggered turn's first chunk — if the agent hasn't written the new turn's first record yet, the peek sees the prior turn's `turn-complete` and closes the SSE before the response lands on S2. The built-in `TriggerChatTransport.reconnectToStream` sets the header; `sendMessages → subscribeToStream` does not.
|
|
654
654
|
|
|
655
|
+
If you use `TriggerChatTransport` (or `useChat`, which builds on it), this is handled for you: the reconnect path sends `X-Peek-Settled`, so a settled idle reconnect closes promptly on the server's `wait=0` fast-close instead of waiting out the long poll. Consuming `.out` directly, send the same header on your reconnect reads and treat an `X-Session-Settled: true` response as a terminal close.
|
|
656
|
+
|
|
655
657
|
```ts
|
|
656
658
|
// Reconnect path (page reload)
|
|
657
659
|
const response = await fetch(sseUrl, {
|
|
@@ -266,11 +266,14 @@ This is an **import-chain** problem, not a runtime one. A "we'll strip the execu
|
|
|
266
266
|
...helper.toStreamTextOptions({ tools: headStartTools }),
|
|
267
267
|
model: anthropic("claude-sonnet-4-6"),
|
|
268
268
|
system: "You are a helpful assistant.",
|
|
269
|
-
stopWhen: stepCountIs(15),
|
|
270
269
|
}),
|
|
271
270
|
});
|
|
272
271
|
```
|
|
273
272
|
|
|
273
|
+
<Warning>
|
|
274
|
+
Don't set `stopWhen` here. The spread pins it to `stepCountIs(1)`, and overriding it makes the handler run steps the agent is supposed to own — the handover then splices a stream that has already moved past step 1.
|
|
275
|
+
</Warning>
|
|
276
|
+
|
|
274
277
|
<Tip>
|
|
275
278
|
Use the **same model** on both sides (route handler and `chat.agent`) to avoid a tone or style shift between step 1 and step 2+. Your LLM provider keys stay server-side in your warm process — Trigger.dev never holds them in this design.
|
|
276
279
|
</Tip>
|
|
@@ -215,10 +215,12 @@ function ChatClient({ chatId, initialMessages, initialSessions }) {
|
|
|
215
215
|
</Info>
|
|
216
216
|
|
|
217
217
|
<Note>
|
|
218
|
-
After resuming, `useChat`'s built-in `stop()` won't send the stop signal to the backend
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
After resuming, `useChat`'s built-in `stop()` won't send the stop signal to the backend. The
|
|
219
|
+
transport accepts an abort signal on `reconnectToStream` but doesn't treat it as owning the turn,
|
|
220
|
+
so aborting a resumed subscription only closes your local stream while the run keeps generating.
|
|
221
|
+
Use `transport.stopGeneration(chatId)` for reliable stop behavior after resume — see
|
|
222
|
+
[Stop generation](#stop-generation) for the recommended pattern. Pass `stopOnAbort: true` to
|
|
223
|
+
`reconnectToStream` only when that subscriber owns the turn.
|
|
222
224
|
</Note>
|
|
223
225
|
|
|
224
226
|
<Warning>
|
|
@@ -340,11 +342,10 @@ const stop = useCallback(() => {
|
|
|
340
342
|
</Info>
|
|
341
343
|
|
|
342
344
|
<Tip>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
`
|
|
346
|
-
|
|
347
|
-
behavior after page refresh.
|
|
345
|
+
Aborting a resumed stream never stops the run by itself, so `useChat`'s
|
|
346
|
+
built-in `stop()` isn't enough after a page refresh. Use the pattern above, or
|
|
347
|
+
pass `stopOnAbort: true` to `reconnectToStream` when that subscriber owns the
|
|
348
|
+
turn.
|
|
348
349
|
</Tip>
|
|
349
350
|
|
|
350
351
|
See [Stop generation](/ai-chat/backend#stop-generation) in the backend docs for how to handle stop signals in your task.
|