@trigger.dev/sdk 4.5.0-rc.6 → 4.5.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +171 -5
- package/dist/commonjs/v3/ai.js +309 -22
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +8 -0
- package/dist/commonjs/v3/chat-server.js +32 -10
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat-server.test.js +51 -0
- package/dist/commonjs/v3/chat-server.test.js.map +1 -1
- package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
- package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +3 -2
- package/dist/commonjs/v3/sessions.js +3 -2
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +171 -5
- package/dist/esm/v3/ai.js +309 -22
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +8 -0
- package/dist/esm/v3/chat-server.js +32 -10
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat-server.test.js +51 -0
- package/dist/esm/v3/chat-server.test.js.map +1 -1
- package/dist/esm/v3/createStartSessionAction.test.js +30 -0
- package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +3 -2
- package/dist/esm/v3/sessions.js +3 -2
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/prompts.mdx +430 -0
- package/docs/ai-chat/actions.mdx +115 -0
- package/docs/ai-chat/anatomy.mdx +71 -0
- package/docs/ai-chat/backend.mdx +817 -0
- package/docs/ai-chat/background-injection.mdx +221 -0
- package/docs/ai-chat/changelog.mdx +850 -0
- package/docs/ai-chat/chat-local.mdx +174 -0
- package/docs/ai-chat/client-protocol.mdx +1081 -0
- package/docs/ai-chat/compaction.mdx +411 -0
- package/docs/ai-chat/custom-agents.mdx +364 -0
- package/docs/ai-chat/error-handling.mdx +415 -0
- package/docs/ai-chat/fast-starts.mdx +672 -0
- package/docs/ai-chat/frontend.mdx +580 -0
- package/docs/ai-chat/how-it-works.mdx +230 -0
- package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
- package/docs/ai-chat/mcp.mdx +101 -0
- package/docs/ai-chat/overview.mdx +90 -0
- package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
- package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
- package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
- package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
- package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
- package/docs/ai-chat/patterns/skills.mdx +221 -0
- package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
- package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
- package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
- package/docs/ai-chat/pending-messages.mdx +343 -0
- package/docs/ai-chat/prompt-caching.mdx +206 -0
- package/docs/ai-chat/quick-start.mdx +161 -0
- package/docs/ai-chat/reference.mdx +909 -0
- package/docs/ai-chat/server-chat.mdx +263 -0
- package/docs/ai-chat/sessions.mdx +333 -0
- package/docs/ai-chat/testing.mdx +682 -0
- package/docs/ai-chat/tools.mdx +191 -0
- package/docs/ai-chat/types.mdx +242 -0
- package/docs/ai-chat/upgrade-guide.mdx +515 -0
- package/docs/apikeys.mdx +54 -0
- package/docs/building-with-ai.mdx +261 -0
- package/docs/bulk-actions.mdx +49 -0
- package/docs/changelog.mdx +6 -0
- package/docs/cli-deploy-commands.mdx +9 -0
- package/docs/cli-dev-commands.mdx +9 -0
- package/docs/cli-dev.mdx +8 -0
- package/docs/cli-init-commands.mdx +58 -0
- package/docs/cli-introduction.mdx +25 -0
- package/docs/cli-list-profiles-commands.mdx +42 -0
- package/docs/cli-login-commands.mdx +33 -0
- package/docs/cli-logout-commands.mdx +33 -0
- package/docs/cli-preview-archive.mdx +59 -0
- package/docs/cli-promote-commands.mdx +9 -0
- package/docs/cli-switch.mdx +43 -0
- package/docs/cli-update-commands.mdx +42 -0
- package/docs/cli-whoami-commands.mdx +33 -0
- package/docs/community.mdx +6 -0
- package/docs/config/config-file.mdx +602 -0
- package/docs/config/extensions/additionalFiles.mdx +38 -0
- package/docs/config/extensions/additionalPackages.mdx +40 -0
- package/docs/config/extensions/aptGet.mdx +34 -0
- package/docs/config/extensions/audioWaveform.mdx +20 -0
- package/docs/config/extensions/custom.mdx +380 -0
- package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
- package/docs/config/extensions/esbuildPlugin.mdx +31 -0
- package/docs/config/extensions/ffmpeg.mdx +45 -0
- package/docs/config/extensions/lightpanda.mdx +56 -0
- package/docs/config/extensions/overview.mdx +67 -0
- package/docs/config/extensions/playwright.mdx +195 -0
- package/docs/config/extensions/prismaExtension.mdx +1014 -0
- package/docs/config/extensions/puppeteer.mdx +30 -0
- package/docs/config/extensions/pythonExtension.mdx +182 -0
- package/docs/config/extensions/syncEnvVars.mdx +291 -0
- package/docs/context.mdx +235 -0
- package/docs/database-connections.mdx +213 -0
- package/docs/deploy-environment-variables.mdx +435 -0
- package/docs/deployment/atomic-deployment.mdx +172 -0
- package/docs/deployment/overview.mdx +257 -0
- package/docs/deployment/preview-branches.mdx +224 -0
- package/docs/errors-retrying.mdx +379 -0
- package/docs/github-actions.mdx +222 -0
- package/docs/github-integration.mdx +136 -0
- package/docs/github-repo.mdx +8 -0
- package/docs/help-email.mdx +6 -0
- package/docs/help-slack.mdx +11 -0
- package/docs/hidden-tasks.mdx +56 -0
- package/docs/how-it-works.mdx +454 -0
- package/docs/how-to-reduce-your-spend.mdx +217 -0
- package/docs/idempotency.mdx +504 -0
- package/docs/introduction.mdx +223 -0
- package/docs/limits.mdx +241 -0
- package/docs/logging.mdx +195 -0
- package/docs/machines.mdx +952 -0
- package/docs/manual-setup.mdx +632 -0
- package/docs/mcp-agent-rules.mdx +41 -0
- package/docs/mcp-introduction.mdx +385 -0
- package/docs/mcp-tools.mdx +273 -0
- package/docs/migrating-from-v3.mdx +334 -0
- package/docs/observability/dashboards.mdx +102 -0
- package/docs/observability/query.mdx +585 -0
- package/docs/open-source-contributing.mdx +16 -0
- package/docs/open-source-self-hosting.mdx +541 -0
- package/docs/private-networking/aws-console-setup.mdx +304 -0
- package/docs/private-networking/overview.mdx +144 -0
- package/docs/private-networking/troubleshooting.mdx +78 -0
- package/docs/queue-concurrency.mdx +354 -0
- package/docs/quick-start.mdx +97 -0
- package/docs/realtime/auth.mdx +208 -0
- package/docs/realtime/backend/overview.mdx +45 -0
- package/docs/realtime/backend/streams.mdx +418 -0
- package/docs/realtime/backend/subscribe.mdx +225 -0
- package/docs/realtime/how-it-works.mdx +94 -0
- package/docs/realtime/overview.mdx +63 -0
- package/docs/realtime/react-hooks/overview.mdx +73 -0
- package/docs/realtime/react-hooks/streams.mdx +449 -0
- package/docs/realtime/react-hooks/subscribe.mdx +674 -0
- package/docs/realtime/react-hooks/swr.mdx +87 -0
- package/docs/realtime/react-hooks/triggering.mdx +194 -0
- package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
- package/docs/realtime/run-object.mdx +174 -0
- package/docs/replaying.mdx +72 -0
- package/docs/request-feature.mdx +6 -0
- package/docs/roadmap.mdx +6 -0
- package/docs/run-tests.mdx +20 -0
- package/docs/run-usage.mdx +113 -0
- package/docs/runs/heartbeats.mdx +38 -0
- package/docs/runs/max-duration.mdx +139 -0
- package/docs/runs/metadata.mdx +734 -0
- package/docs/runs/priority.mdx +31 -0
- package/docs/runs.mdx +396 -0
- package/docs/self-hosting/docker.mdx +458 -0
- package/docs/self-hosting/env/supervisor.mdx +74 -0
- package/docs/self-hosting/env/webapp.mdx +276 -0
- package/docs/self-hosting/kubernetes.mdx +601 -0
- package/docs/self-hosting/overview.mdx +108 -0
- package/docs/skills.mdx +85 -0
- package/docs/tags.mdx +120 -0
- package/docs/tasks/overview.mdx +697 -0
- package/docs/tasks/scheduled.mdx +382 -0
- package/docs/tasks/schemaTask.mdx +413 -0
- package/docs/tasks/streams.mdx +884 -0
- package/docs/triggering.mdx +1320 -0
- package/docs/troubleshooting-alerts.mdx +385 -0
- package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
- package/docs/troubleshooting-github-issues.mdx +6 -0
- package/docs/troubleshooting-uptime-status.mdx +6 -0
- package/docs/troubleshooting.mdx +398 -0
- package/docs/upgrading-packages.mdx +80 -0
- package/docs/vercel-integration.mdx +207 -0
- package/docs/versioning.mdx +56 -0
- package/docs/video-walkthrough.mdx +23 -0
- package/docs/wait-for-token.mdx +540 -0
- package/docs/wait-for.mdx +42 -0
- package/docs/wait-until.mdx +53 -0
- package/docs/wait.mdx +18 -0
- package/docs/writing-tasks-introduction.mdx +33 -0
- package/package.json +8 -5
- package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
- package/skills/trigger-authoring-tasks/SKILL.md +254 -0
- package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
- package/skills/trigger-cost-savings/SKILL.md +116 -0
- package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Recovery boot"
|
|
3
|
+
sidebarTitle: "Recovery boot"
|
|
4
|
+
description: "Recover from cancel-mid-stream, crashes, and OOM kills with full conversational context. The smart default Just Works; the onRecoveryBoot hook is the override path for advanced policies."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
|
|
8
|
+
|
|
9
|
+
<RcBanner />
|
|
10
|
+
|
|
11
|
+
When a `chat.agent` run dies in the middle of streaming a response — the user cancels, the worker OOMs, or an unhandled exception kills the process — the durable streams hold what was in flight. The next run boots as a continuation, reads both stream tails, and reconstructs a chain that preserves the partial response so any follow-up (`keep going`, `actually do X instead`, a new question) has full context.
|
|
12
|
+
|
|
13
|
+
The behavior is automatic. The `onRecoveryBoot` hook is opt-in for policies that need something different.
|
|
14
|
+
|
|
15
|
+
## The scenario
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Turn 1 is mid-essay when the user clicks Cancel.
|
|
19
|
+
window.__chat.send("Write me a long essay about espresso");
|
|
20
|
+
// ... assistant has written 3000 characters ...
|
|
21
|
+
window.__chat.stop(); // OR: server-side cancel_run
|
|
22
|
+
|
|
23
|
+
// User decides what they want next.
|
|
24
|
+
window.__chat.send("keep going"); // OR: "what's 7+8?", or anything
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The cancelled run never wrote `onTurnComplete`. The snapshot is stale or absent. `session.out` has a half-written assistant message. `session.in` has the original user message (the run consumed it but never marked the turn complete) plus the new follow-up.
|
|
28
|
+
|
|
29
|
+
A naive continuation would either re-run the cancelled essay (the user already chose to stop) or drop everything (no context for the follow-up). Recovery boot handles this without either failure mode.
|
|
30
|
+
|
|
31
|
+
## The smart default
|
|
32
|
+
|
|
33
|
+
On a continuation boot, the runtime reads:
|
|
34
|
+
|
|
35
|
+
- **Snapshot** — settled turns persisted by the last successful `onTurnComplete`.
|
|
36
|
+
- **`session.out` tail past the snapshot cursor** — closed assistant turns plus, optionally, a `partialAssistant` (the trailing message whose stream never received a `finish` chunk). `cleanupAbortedParts` has already stripped streaming-in-progress fragments.
|
|
37
|
+
- **`session.in` tail past the last `turn-complete` cursor** — user messages the dead run hadn't acknowledged.
|
|
38
|
+
|
|
39
|
+
If both `partialAssistant` and `inFlightUsers` are non-empty, the runtime splices `[firstInFlightUser, partialAssistant]` onto the chain. The remaining in-flight users dispatch as fresh turns. The model sees:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
[ ...settledMessages, // chain through the last completed turn
|
|
43
|
+
firstInFlightUser, // the question the dead run was answering
|
|
44
|
+
partialAssistant, // the dead run's incomplete response
|
|
45
|
+
followUpUser ] // the new turn the customer just sent
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Modern instruction-following models prioritize the latest user message. The follow-up determines the response:
|
|
49
|
+
|
|
50
|
+
| Follow-up | Model behavior |
|
|
51
|
+
|---|---|
|
|
52
|
+
| "keep going" / "continue" / "more" | Continues the partial essay from where it stopped. |
|
|
53
|
+
| "actually, what's 7+8?" | Answers the new question. Prior context doesn't derail it. |
|
|
54
|
+
| "scrap that, do something else" | Abandons the partial work and follows the new direction. |
|
|
55
|
+
|
|
56
|
+
No customer code needed for any of these.
|
|
57
|
+
|
|
58
|
+
## When to register `onRecoveryBoot`
|
|
59
|
+
|
|
60
|
+
The hook fires when recovery state is non-empty (either `partialAssistant` is defined or there's at least one in-flight user). Register it when you need a policy different from "preserve context":
|
|
61
|
+
|
|
62
|
+
- **Drop the partial entirely.** Your UX means "cancel discards the work — start fresh from the follow-up."
|
|
63
|
+
- **Synthesize tool results.** The partial has tool calls in `input-available` state (HITL was mid-call when the run died). Return a chain that has fabricated `output-available` results so the model can continue.
|
|
64
|
+
- **Emit a recovery banner.** Write a `data-chat-recovery` UIMessage chunk via `ctx.writer` so the frontend can render "Recovering interrupted response..." before the model speaks.
|
|
65
|
+
- **Persist recovered state.** Use `beforeBoot` to flush the partial to your own database before the next turn starts.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
69
|
+
|
|
70
|
+
export const myChat = chat.agent({
|
|
71
|
+
id: "my-chat",
|
|
72
|
+
onRecoveryBoot: async ({ partialAssistant, inFlightUsers, writer, cause, previousRunId }) => {
|
|
73
|
+
writer.write({
|
|
74
|
+
type: "data-chat-recovery",
|
|
75
|
+
data: { cause, previousRunId, partialPresent: partialAssistant !== undefined },
|
|
76
|
+
transient: true,
|
|
77
|
+
});
|
|
78
|
+
// Return nothing → fall through to smart default.
|
|
79
|
+
},
|
|
80
|
+
run: async ({ messages, signal }) =>
|
|
81
|
+
streamText({ model, messages, abortSignal: signal }),
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Hook reference
|
|
86
|
+
|
|
87
|
+
### Fires when
|
|
88
|
+
|
|
89
|
+
The hook fires once on a continuation boot, AFTER both stream tails have been read, AND only when there's a partial assistant — the mid-stream-died signal:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const shouldFire = partialAssistant !== undefined;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
In-flight users alone don't fire the hook. Graceful exits like `chat.requestUpgrade()` and `chat.endRun()` may leave an unacknowledged user on `session.in` (the message that triggered the upgrade, the next message after endRun), but no partial — that's a normal continuation, not recovery. The next message just dispatches as turn 1 on the new run via the normal session.in pump.
|
|
96
|
+
|
|
97
|
+
Skipped scenarios (where the hook does NOT fire):
|
|
98
|
+
|
|
99
|
+
- A clean continuation after `chat.endRun()` with no buffered follow-up.
|
|
100
|
+
- A fresh chat (no continuation, attempt 1).
|
|
101
|
+
- An OOM retry that booted onto a complete snapshot (no partial on the tail).
|
|
102
|
+
- `chat.requestUpgrade()` graceful exit — predecessor ended cleanly before processing, no partial.
|
|
103
|
+
- An agent with [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) registered. Customers using `hydrateMessages` own persistence — recovery decisions live in their own DB query.
|
|
104
|
+
|
|
105
|
+
### Event shape
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
type RecoveryBootEvent<TUIM extends UIMessage = UIMessage> = {
|
|
109
|
+
ctx: TaskRunContext;
|
|
110
|
+
chatId: string;
|
|
111
|
+
runId: string;
|
|
112
|
+
previousRunId: string;
|
|
113
|
+
cause: "cancelled" | "crashed" | "unknown";
|
|
114
|
+
settledMessages: TUIM[];
|
|
115
|
+
inFlightUsers: TUIM[];
|
|
116
|
+
partialAssistant: TUIM | undefined;
|
|
117
|
+
pendingToolCalls: Array<{
|
|
118
|
+
toolCallId: string;
|
|
119
|
+
toolName: string;
|
|
120
|
+
input: unknown;
|
|
121
|
+
partIndex: number;
|
|
122
|
+
}>;
|
|
123
|
+
writer: ChatWriter;
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
<Note>
|
|
128
|
+
`cause` is currently always `"unknown"` — the run engine doesn't yet plumb the
|
|
129
|
+
real reason into the continuation payload. The enum is forward-looking; don't
|
|
130
|
+
branch behavior on it for now.
|
|
131
|
+
</Note>
|
|
132
|
+
|
|
133
|
+
### Return shape
|
|
134
|
+
|
|
135
|
+
Every field is optional. Returning `undefined` (or nothing) accepts the smart default for every field.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
type RecoveryBootResult<TUIM extends UIMessage = UIMessage> = {
|
|
139
|
+
chain?: TUIM[];
|
|
140
|
+
recoveredTurns?: TUIM[];
|
|
141
|
+
beforeBoot?: () => Promise<void>;
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- **`chain`** — replaces the seed chain. Defaults to `[...settledMessages, firstInFlightUser, partialAssistant]` when both partial and in-flight users exist, otherwise `settledMessages` alone.
|
|
146
|
+
- **`recoveredTurns`** — user messages to dispatch as fresh turns after the chain is restored. Defaults to `inFlightUsers.slice(1)` when the smart default consumed the first user, otherwise `inFlightUsers`.
|
|
147
|
+
- **`beforeBoot`** — runs after the writer flushes and before the first recovered turn fires. Use for blocking persistence (write the partial to your DB so a later turn can reference it). Errors bubble — wrap your own try/catch if you want to soft-fail.
|
|
148
|
+
|
|
149
|
+
## Examples
|
|
150
|
+
|
|
151
|
+
### Drop the partial — strict "cancel means discard"
|
|
152
|
+
|
|
153
|
+
The customer's UX treats cancel as "throw the work away":
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
onRecoveryBoot: async ({ inFlightUsers, partialAssistant }) => {
|
|
157
|
+
if (!partialAssistant) return; // No partial → nothing to drop
|
|
158
|
+
return {
|
|
159
|
+
chain: undefined, // Use settledMessages, don't splice partial
|
|
160
|
+
recoveredTurns: inFlightUsers.slice(1) // Still skip the first user (the dead run was answering it)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Synthesize tool results for a mid-call interruption
|
|
166
|
+
|
|
167
|
+
The dead run was processing a tool call when it died. The partial has tool parts in `input-available` state with no `output-available`. Synthesize a result so the model can keep going:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
onRecoveryBoot: async ({ partialAssistant, pendingToolCalls, settledMessages, inFlightUsers }) => {
|
|
171
|
+
if (pendingToolCalls.length === 0) return;
|
|
172
|
+
|
|
173
|
+
// Rebuild the partial with synthetic outputs for any input-available tool call.
|
|
174
|
+
const repaired = {
|
|
175
|
+
...partialAssistant!,
|
|
176
|
+
parts: partialAssistant!.parts!.map((part, i) => {
|
|
177
|
+
const pending = pendingToolCalls.find(p => p.partIndex === i);
|
|
178
|
+
if (!pending) return part;
|
|
179
|
+
return {
|
|
180
|
+
...part,
|
|
181
|
+
state: "output-available" as const,
|
|
182
|
+
output: { interrupted: true, reason: "previous run was cancelled" },
|
|
183
|
+
};
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
chain: [...settledMessages, inFlightUsers[0]!, repaired],
|
|
189
|
+
recoveredTurns: inFlightUsers.slice(1),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Persist the partial before the next turn fires
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
onRecoveryBoot: async ({ chatId, partialAssistant }) => {
|
|
198
|
+
return {
|
|
199
|
+
beforeBoot: async () => {
|
|
200
|
+
if (partialAssistant) {
|
|
201
|
+
await db.partial.create({
|
|
202
|
+
data: { chatId, partialJson: JSON.stringify(partialAssistant) },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Interaction with other features
|
|
211
|
+
|
|
212
|
+
### `hydrateMessages`
|
|
213
|
+
|
|
214
|
+
If your agent registers [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages), the runtime skips snapshot read, `session.out` replay, `session.in` replay, AND `onRecoveryBoot`. Your DB is the source of truth — recovery decisions live in your own query. To detect a cancel-recovery scenario yourself, persist a `runState: "in-progress"` flag in `onTurnStart` and check for it in `hydrateMessages`.
|
|
215
|
+
|
|
216
|
+
### `chat.requestUpgrade()`
|
|
217
|
+
|
|
218
|
+
[`chat.requestUpgrade()`](/ai-chat/patterns/version-upgrades) is a graceful exit — the old run doesn't crash, it returns cleanly. The new continuation run boots with a clean `session.out` tail (`partialAssistant` is undefined) and the upgrade-trigger message on `session.in` (one in-flight user). The smart default doesn't splice (it requires both partial AND in-flight users), so the chain is just `settledMessages` and the in-flight user dispatches as a fresh turn. `onRecoveryBoot` still fires (there's an in-flight user) — use it to emit an "upgraded" signal to the UI if you want.
|
|
219
|
+
|
|
220
|
+
### Hooks throwing
|
|
221
|
+
|
|
222
|
+
If the body of `onRecoveryBoot` throws (or rejects), the runtime logs a warning and falls back to the smart default — the run does not fail. Wrap your own try/catch if you want stricter handling.
|
|
223
|
+
|
|
224
|
+
`beforeBoot` is the exception: it's the contract you opted into for blocking persistence, so errors thrown there **bubble** and fail the run rather than dispatch recovered turns against half-persisted state. Wrap it yourself if you want to soft-fail.
|
|
225
|
+
|
|
226
|
+
## See also
|
|
227
|
+
|
|
228
|
+
- [OOM resilience](/ai-chat/patterns/oom-resilience) — `oomMachine` opt-in for automatic memory-driven recovery; uses the same recovery boot path.
|
|
229
|
+
- [Persistence and replay](/ai-chat/patterns/persistence-and-replay) — the snapshot + dual-tail replay model that recovery boot sits on top of.
|
|
230
|
+
- [Lifecycle hooks](/ai-chat/lifecycle-hooks) — where `onRecoveryBoot` sits in the broader hook taxonomy.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Agent Skills"
|
|
3
|
+
sidebarTitle: "Agent Skills"
|
|
4
|
+
description: "Ship reusable capabilities (folders with SKILL.md + scripts) that a chat agent discovers and invokes on demand."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
|
|
8
|
+
|
|
9
|
+
<RcBanner />
|
|
10
|
+
|
|
11
|
+
Agent skills are reusable capabilities you ship as folders — a `SKILL.md` describing when and how to use them, plus optional scripts, references, and assets. The chat agent sees a short description of each skill in its system prompt, loads the full instructions on demand via a `loadSkill` tool, and invokes the bundled scripts via `bash` — all without you wiring anything up manually.
|
|
12
|
+
|
|
13
|
+
Built on the [AI SDK cookbook pattern](https://ai-sdk.dev/cookbook/guides/agent-skills). Works with any provider (OpenAI, Anthropic, Gemini, etc.) — not tied to Anthropic's server-side skills.
|
|
14
|
+
|
|
15
|
+
## Why skills?
|
|
16
|
+
|
|
17
|
+
Compared to regular AI SDK tools:
|
|
18
|
+
|
|
19
|
+
- **Tools** are typed functions you pre-declare. Great when you know up-front exactly what capability the agent needs.
|
|
20
|
+
- **Skills** are folders the model discovers and reads on demand. Great when the capability is a bundle of instructions + helper scripts that would be awkward to encode as a single tool.
|
|
21
|
+
|
|
22
|
+
PDFs are the canonical example: you don't want to ask the LLM to parse PDF bytes inline. You want it to `bash scripts/extract.py report.pdf` using a bundled `pdfplumber` wrapper. A skill ships the script, the instructions, and any reference notes together.
|
|
23
|
+
|
|
24
|
+
Dashboard-editable `SKILL.md` is on the roadmap so a platform team can tighten a skill's description or "when to use" text without a redeploy. Today, skills are SDK-only — defined in your task code and shipped with each deploy.
|
|
25
|
+
|
|
26
|
+
## Trust model
|
|
27
|
+
|
|
28
|
+
Skills are **developer-authored code**, not end-user-supplied. The same developer who writes the `chat.agent()` writes the skill bundle. The trust boundary is identical to any `tool.execute` handler the developer writes — scripts run directly in the Trigger.dev worker container, no sandboxing required.
|
|
29
|
+
|
|
30
|
+
This makes skills different from the Claude Code / end-user model where arbitrary user-provided skills need isolation. Don't accept skill paths from untrusted input.
|
|
31
|
+
|
|
32
|
+
## Skill folder layout
|
|
33
|
+
|
|
34
|
+
A skill is a directory under your project (conventionally `trigger/skills/{id}/`):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
trigger/skills/time-utils/
|
|
38
|
+
├── SKILL.md # Required — frontmatter + instructions
|
|
39
|
+
├── scripts/
|
|
40
|
+
│ ├── now.sh
|
|
41
|
+
│ └── add.sh
|
|
42
|
+
├── references/
|
|
43
|
+
│ └── timezones.txt
|
|
44
|
+
└── assets/ # Optional — templates, data files, etc.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### SKILL.md
|
|
48
|
+
|
|
49
|
+
Frontmatter is YAML-subset — only `name` and `description` are required:
|
|
50
|
+
|
|
51
|
+
```md
|
|
52
|
+
---
|
|
53
|
+
name: time-utils
|
|
54
|
+
description: Compute and format dates/times in arbitrary timezones. Use when the user asks "what time is it", timezone conversions, or date math.
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Time utilities
|
|
58
|
+
|
|
59
|
+
## When to use
|
|
60
|
+
|
|
61
|
+
- The user asks for the current time in a timezone
|
|
62
|
+
- The user wants date math ("3 days from now")
|
|
63
|
+
|
|
64
|
+
## Scripts
|
|
65
|
+
|
|
66
|
+
### `scripts/now.sh [TZ]`
|
|
67
|
+
Prints the current time in the given IANA timezone (default `UTC`).
|
|
68
|
+
|
|
69
|
+
### `scripts/add.sh DAYS [TZ]`
|
|
70
|
+
Prints a date `DAYS` days from now.
|
|
71
|
+
|
|
72
|
+
## Tips
|
|
73
|
+
- IANA timezone names only (`America/New_York`, not `EST`).
|
|
74
|
+
- See `references/timezones.txt` for a cheat-sheet.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The **description** is what the model sees in its system prompt — write it like you're explaining to the agent when to reach for the skill.
|
|
78
|
+
|
|
79
|
+
The **body** is loaded on demand via the `loadSkill` tool when the agent decides to use the skill. Write it like documentation for the agent.
|
|
80
|
+
|
|
81
|
+
## Defining and using a skill
|
|
82
|
+
|
|
83
|
+
```ts trigger/chat.ts
|
|
84
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
85
|
+
import { skills } from "@trigger.dev/sdk";
|
|
86
|
+
import { streamText, stepCountIs } from "ai";
|
|
87
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
88
|
+
|
|
89
|
+
const timeUtilsSkill = skills.define({
|
|
90
|
+
id: "time-utils",
|
|
91
|
+
path: "./skills/time-utils",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const agent = chat.agent({
|
|
95
|
+
id: "docs-chat",
|
|
96
|
+
onChatStart: async () => {
|
|
97
|
+
chat.skills.set([await timeUtilsSkill.local()]);
|
|
98
|
+
},
|
|
99
|
+
run: async ({ messages, signal }) => {
|
|
100
|
+
return streamText({
|
|
101
|
+
model: anthropic("claude-sonnet-4-5"),
|
|
102
|
+
messages,
|
|
103
|
+
abortSignal: signal,
|
|
104
|
+
...chat.toStreamTextOptions(),
|
|
105
|
+
stopWhen: stepCountIs(15),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`skills.define({ id, path })` does two things:
|
|
112
|
+
|
|
113
|
+
1. Registers the skill with the Trigger.dev build system so the CLI **automatically bundles the folder** into your deploy image at `/app/.trigger/skills/{id}/`. No `trigger.config.ts` changes, no build extension — it just works.
|
|
114
|
+
2. Returns a `SkillHandle` you use at runtime.
|
|
115
|
+
|
|
116
|
+
`skill.local()` reads the bundled `SKILL.md` from disk and returns a `ResolvedSkill` with the parsed frontmatter + body + on-disk path.
|
|
117
|
+
|
|
118
|
+
`chat.skills.set([...])` stores the resolved skills for the current run. `chat.toStreamTextOptions()` spreads them into `streamText` automatically:
|
|
119
|
+
|
|
120
|
+
- The frontmatter `description` lands in the system prompt under "Available skills:".
|
|
121
|
+
- Three tools are added: `loadSkill`, `readFile`, `bash` — scoped per skill.
|
|
122
|
+
|
|
123
|
+
## What gets auto-injected
|
|
124
|
+
|
|
125
|
+
When you spread `chat.toStreamTextOptions()` with skills set, the AI SDK call receives three tools:
|
|
126
|
+
|
|
127
|
+
### `loadSkill({ name })`
|
|
128
|
+
|
|
129
|
+
Returns the full `SKILL.md` body for the named skill. The model calls this first when it decides a skill is relevant, to load the full instructions.
|
|
130
|
+
|
|
131
|
+
### `readFile({ skill, path })`
|
|
132
|
+
|
|
133
|
+
Reads a file inside the skill's bundled folder. Paths are relative to the skill's root and are rejected if they attempt to escape via `..` or absolute paths. Output is capped at 1 MB per call.
|
|
134
|
+
|
|
135
|
+
Use for reference files and templates that the model should read literally:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
readFile({ skill: "time-utils", path: "references/timezones.txt" })
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `bash({ skill, command })`
|
|
142
|
+
|
|
143
|
+
Runs a bash command with `cwd` set to the skill's root. Stdout and stderr are captured and returned (each capped at 64 KB per call, with tail truncation). The turn's abort signal propagates — cancelling the run kills the child process.
|
|
144
|
+
|
|
145
|
+
Use to invoke the skill's bundled scripts:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
bash({ skill: "time-utils", command: "bash scripts/now.sh America/Los_Angeles" })
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Script runtime expectations are yours to manage. If your skill uses `extract.py`, your deploy image needs Python — add it via your build config the same way you would for any other task dependency.
|
|
152
|
+
|
|
153
|
+
## How discovery works in the model
|
|
154
|
+
|
|
155
|
+
The model sees a short preamble appended to your system prompt:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Available skills (call `loadSkill` to read the full instructions before using one):
|
|
159
|
+
- time-utils: Compute and format dates/times in arbitrary timezones...
|
|
160
|
+
- pdf-processing: Extract text from PDFs, fill forms...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
When the user asks something that matches a description, the model calls `loadSkill({ name: "time-utils" })` to load the body, then follows the body's instructions — typically by calling `bash` or `readFile` on the bundled scripts.
|
|
164
|
+
|
|
165
|
+
This is **progressive disclosure**: each skill costs ~100 tokens up front (its one-line description), and only the ones the model actually uses pay the full context cost.
|
|
166
|
+
|
|
167
|
+
## Mixing skills with custom tools
|
|
168
|
+
|
|
169
|
+
If you also define your own AI SDK tools, pass them through `chat.toStreamTextOptions()` so the merge is explicit:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
return streamText({
|
|
173
|
+
model: anthropic("claude-sonnet-4-5"),
|
|
174
|
+
messages,
|
|
175
|
+
abortSignal: signal,
|
|
176
|
+
...chat.toStreamTextOptions({
|
|
177
|
+
tools: {
|
|
178
|
+
webFetch, // your tool
|
|
179
|
+
deepResearch, // your tool
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
stopWhen: stepCountIs(15),
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Your tools win on name conflicts. (Pick names that don't collide with `loadSkill` / `readFile` / `bash` to keep things predictable.)
|
|
187
|
+
|
|
188
|
+
Also declare those same tools on the agent's [`tools`](/ai-chat/tools) config. `toStreamTextOptions` merges them with the skill tools for the model call, while the config option threads them into history re-conversion so any `toModelOutput` survives across turns. The auto-injected skill tools (`loadSkill` / `readFile` / `bash`) don't define `toModelOutput`, so they don't need to be on the config.
|
|
189
|
+
|
|
190
|
+
## Bundling
|
|
191
|
+
|
|
192
|
+
Bundling is **built-in to the CLI** — there's no extension to import. When you run `trigger deploy` or `trigger dev`:
|
|
193
|
+
|
|
194
|
+
1. esbuild bundles your task code as usual.
|
|
195
|
+
2. The CLI forks the indexer locally against the bundled output, collects every `skills.define({ path })` registration.
|
|
196
|
+
3. Each skill's folder is copied to `{outputPath}/.trigger/skills/{id}/` via a recursive copy.
|
|
197
|
+
4. The existing Dockerfile `COPY` picks up `.trigger/skills/` along with the rest of the bundle — no Dockerfile changes.
|
|
198
|
+
|
|
199
|
+
If you're running `trigger dev`, the same layout appears in the local dev output directory, so `skill.local()` works the same way.
|
|
200
|
+
|
|
201
|
+
## Path scoping rules
|
|
202
|
+
|
|
203
|
+
- `skill.path` always resolves to `${process.cwd()}/.trigger/skills/{id}/` at runtime. Don't hardcode paths elsewhere.
|
|
204
|
+
- `readFile` rejects `..` segments and absolute paths — the tool only exposes files inside the skill's own directory.
|
|
205
|
+
- `bash` runs with `cwd` set to the skill's root. Inside the script, relative paths resolve against the skill directory.
|
|
206
|
+
- Cross-skill access isn't provided — each skill is isolated by design. If two skills need to share data, either duplicate the shared file or consolidate the skills.
|
|
207
|
+
|
|
208
|
+
## Current limitations
|
|
209
|
+
|
|
210
|
+
- `skill.resolve()` (backend-managed overrides) is not available yet — use `.local()` for now. Dashboard-editable `SKILL.md` is on the roadmap.
|
|
211
|
+
- No per-skill metrics in the dashboard yet.
|
|
212
|
+
- No Anthropic `/v1/skills` integration — use the portable path today; we're tracking the Anthropic optimization separately.
|
|
213
|
+
|
|
214
|
+
## Full example
|
|
215
|
+
|
|
216
|
+
See [`projects/ai-chat/src/trigger/skills/time-utils/`](https://github.com/triggerdotdev/references/tree/main/projects/ai-chat/src/trigger/skills/time-utils) in the [references repo](https://github.com/triggerdotdev/references) for a working skill that bundles two bash scripts and a reference cheat-sheet, wired into a `chat.agent` that answers timezone questions.
|
|
217
|
+
|
|
218
|
+
## Related
|
|
219
|
+
|
|
220
|
+
- [AI SDK cookbook — Agent Skills](https://ai-sdk.dev/cookbook/guides/agent-skills) — the userland pattern we build on
|
|
221
|
+
- [Anthropic Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) — Anthropic's codified version (server-side, optional future integration)
|