Skip to content

Commit e092919

Browse files
authored
feat(sdk,cli): bundle agent skills + docs in the SDK for zero-drift (#3937)
## Summary `@trigger.dev/sdk` now ships the Trigger.dev agent skills and a curated snapshot of the docs those skills cite. The skills that `trigger skills` installs into your coding agent are thin pointers that read this bundled content from `node_modules`, so the guidance always matches the SDK version installed in your project. Previously the full skill text was copied into your repo at install time and went stale until you reinstalled after an upgrade. ## How it works The SDK's `files[]` now includes `skills/` (the full skill text) and `docs/` (a curated snapshot generated at build time). The docs manifest is derived from each skill's own `sources:` frontmatter, so a skill only ships the docs it references, and a skill that cites a missing doc fails the build. The CLI installs thin skills whose body points the agent at `node_modules/@trigger.dev/sdk/skills/<name>/SKILL.md` and `node_modules/@trigger.dev/sdk/docs/`. They keep the high-value "Common mistakes" anti-patterns inline so the trigger and the guardrails survive even if the agent does not follow the pointer. `getting-started` stays self-contained in the CLI because it runs before the SDK is installed.
1 parent 1f1a366 commit e092919

12 files changed

Lines changed: 1339 additions & 979 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"trigger.dev": patch
4+
---
5+
6+
`@trigger.dev/sdk` now bundles the Trigger.dev agent skills and a curated snapshot of the docs those skills reference. The skills that `trigger skills` installs into your coding agent read this content from node_modules, so the guidance your AI assistant follows is pinned to the SDK version installed in your project and stays current across upgrades instead of going stale until the next reinstall.

packages/cli-v3/skills/authoring-chat-agent/SKILL.md

Lines changed: 6 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -10,242 +10,16 @@ description: >
1010
streamText route to chat.agent.
1111
type: core
1212
library: trigger.dev
13-
library_version: "{{TRIGGER_SDK_VERSION}}"
14-
sources:
15-
- docs/ai-chat/overview.mdx
16-
- docs/ai-chat/quick-start.mdx
17-
- docs/ai-chat/how-it-works.mdx
18-
- docs/ai-chat/backend.mdx
19-
- docs/ai-chat/frontend.mdx
20-
- docs/ai-chat/reference.mdx
21-
- docs/ai-chat/types.mdx
22-
- docs/ai-chat/tools.mdx
23-
- docs/ai-chat/lifecycle-hooks.mdx
24-
- docs/ai-chat/error-handling.mdx
2513
---
2614

27-
# Authoring a chat agent
15+
# Authoring a chat.agent
2816

29-
A `chat.agent` runs an entire conversation as one long-lived Trigger.dev task. It wakes when a
30-
message arrives, freezes when none do, and in-memory state survives page refreshes, deploys, idle
31-
gaps, and crashes. Your code is the loop you would write anyway: messages in, `streamText` out.
32-
There are no API routes. The frontend talks to the agent through a `TriggerChatTransport`, so
33-
history accumulates server-side and the client ships only the new message each turn.
17+
The full, version-pinned reference ships **inside your installed `@trigger.dev/sdk`**. Read it before writing code — it always matches the SDK version in this project, so it never drifts:
3418

35-
Works with Vercel AI SDK v5, v6, or v7. On v7 also install `@ai-sdk/otel` so model calls are traced
36-
(the SDK registers it for you).
19+
- **Skill:** `node_modules/@trigger.dev/sdk/skills/authoring-chat-agent/SKILL.md` — the per-turn run loop, `chat.toStreamTextOptions()`, the two server actions, typed tools/data parts, and the React transport.
20+
- **Docs:** the full, version-pinned docs ship bundled at `node_modules/@trigger.dev/sdk/docs/ai-chat/`; the skill above lists the exact pages it draws from in its `sources:` frontmatter. Grep for an API, e.g. `grep -rl "toStreamTextOptions" node_modules/@trigger.dev/sdk/docs/`.
3721

38-
## Setup
39-
40-
Three pieces: the agent task, two server actions, and the frontend transport.
41-
42-
### 1. Define the agent
43-
44-
```ts trigger/chat.ts
45-
import { chat } from "@trigger.dev/sdk/ai";
46-
import { streamText, stepCountIs } from "ai";
47-
import { anthropic } from "@ai-sdk/anthropic";
48-
49-
export const myChat = chat.agent({
50-
id: "my-chat",
51-
run: async ({ messages, signal }) =>
52-
streamText({
53-
// Spread this FIRST. See "Common mistakes".
54-
...chat.toStreamTextOptions(),
55-
model: anthropic("claude-sonnet-4-5"),
56-
messages,
57-
abortSignal: signal,
58-
stopWhen: stepCountIs(15),
59-
}),
60-
});
61-
```
62-
63-
`run` receives `messages` already converted to `ModelMessage[]` (the SDK converts the frontend's
64-
`UIMessage[]` for you) plus a `signal` that aborts on stop or cancel. Returning the
65-
`StreamTextResult` auto-pipes it to the frontend.
66-
67-
### 2. Add two server actions
68-
69-
Both run on your server, so the browser never holds your environment secret key. This is also
70-
where per-user / per-plan authorization and any paired DB writes live.
71-
72-
```ts app/actions.ts
73-
"use server";
74-
import { auth } from "@trigger.dev/sdk";
75-
import { chat } from "@trigger.dev/sdk/ai";
76-
77-
// Creates the Session + first run, returns a session PAT. Idempotent on (env, chatId).
78-
export const startChatSession = chat.createStartSessionAction("my-chat");
79-
80-
// Pure mint. The transport calls this on 401/403 to refresh an expired token.
81-
export async function mintChatAccessToken(chatId: string) {
82-
return auth.createPublicToken({
83-
scopes: { read: { sessions: chatId }, write: { sessions: chatId } },
84-
expirationTime: "1h",
85-
});
86-
}
87-
```
88-
89-
### 3. Wire the frontend
90-
91-
```tsx app/components/chat.tsx
92-
"use client";
93-
import { useState } from "react";
94-
import { useChat } from "@ai-sdk/react";
95-
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
96-
import type { myChat } from "@/trigger/chat";
97-
import { mintChatAccessToken, startChatSession } from "@/app/actions";
98-
99-
export function Chat() {
100-
const transport = useTriggerChatTransport<typeof myChat>({
101-
task: "my-chat", // typeof myChat gives compile-time task-id validation
102-
accessToken: ({ chatId }) => mintChatAccessToken(chatId),
103-
startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
104-
});
105-
106-
const { messages, sendMessage, stop, status } = useChat({ transport });
107-
const [input, setInput] = useState("");
108-
// render messages, a form that calls sendMessage({ text: input }),
109-
// and a Stop button (onClick={stop}) while status === "streaming".
110-
}
111-
```
112-
113-
The transport is memoized (created once, reused across renders). Passing `typeof myChat` flows the
114-
agent's message type through `useChat`.
115-
116-
## Core patterns
117-
118-
### 1. Return vs pipe
119-
120-
Return the `streamText` result from `run` for the simple case. When `streamText` is called deep
121-
inside nested helpers, call `await chat.pipe(result)` from anywhere in the task instead, and let
122-
`run` resolve `void`.
123-
124-
```ts
125-
export const agentChat = chat.agent({
126-
id: "agent-chat",
127-
run: async ({ messages }) => {
128-
await runAgentLoop(messages); // don't return; pipe inside
129-
},
130-
});
131-
132-
async function runAgentLoop(messages: ModelMessage[]) {
133-
const result = streamText({
134-
...chat.toStreamTextOptions(),
135-
model: anthropic("claude-sonnet-4-5"),
136-
messages,
137-
});
138-
await chat.pipe(result); // works from anywhere in the task
139-
}
140-
```
141-
142-
### 2. Typed tools (declare on config AND spread back)
143-
144-
Declare tools on `chat.agent({ tools })`, read them back typed from the `run()` payload, and pass
145-
that set to `chat.toStreamTextOptions({ tools })`. One declaration flows everywhere.
146-
147-
```ts
148-
import { tool, stepCountIs } from "ai";
149-
import { z } from "zod";
150-
151-
const tools = {
152-
searchDocs: tool({
153-
description: "Search the docs.",
154-
inputSchema: z.object({ query: z.string() }),
155-
execute: async ({ query }) => searchIndex(query),
156-
}),
157-
};
158-
159-
export const myChat = chat.agent({
160-
id: "my-chat",
161-
tools, // so toModelOutput survives across turns
162-
run: async ({ messages, tools, signal }) =>
163-
streamText({
164-
...chat.toStreamTextOptions({ tools }), // same set, handed back typed
165-
model: anthropic("claude-sonnet-4-5"),
166-
messages,
167-
abortSignal: signal,
168-
stopWhen: stepCountIs(15),
169-
}),
170-
});
171-
```
172-
173-
`tools` also accepts a function `(event) => ToolSet` resolved per turn, where `event` carries
174-
`chatId`, `turn`, `continuation`, and `clientData`.
175-
176-
### 3. Custom data parts (persisted vs transient)
177-
178-
`data-*` parts written via `chat.response.write()` in `run()` (or `writer.write()` in hooks)
179-
persist into `responseMessage.parts` and surface in `onTurnComplete`. Add `transient: true` to
180-
stream them without persisting. Writes via `chat.stream` are always ephemeral.
181-
182-
```ts
183-
// In run() - persists, surfaces in onTurnComplete's responseMessage
184-
chat.response.write({ type: "data-context", data: { searchResults } });
185-
186-
// In a hook via writer - streams but does NOT persist
187-
writer.write({ type: "data-progress", id: "search", data: { percent: 50 }, transient: true });
188-
```
189-
190-
### 4. Custom UIMessage type, client data, and builder hooks
191-
192-
For typed `data-*` parts or a tool map, build the agent through `chat.withUIMessage<T>()` and
193-
`chat.withClientData({ schema })`. Builder methods chain in any order; builder hooks run before the
194-
matching task hook. `streamOptions` becomes the default `uiMessageStreamOptions` (shallow-merged,
195-
agent wins).
196-
197-
```ts
198-
export const myChat = chat
199-
.withUIMessage<MyChatUIMessage>({ streamOptions: { sendReasoning: true } })
200-
.withClientData({ schema: z.object({ userId: z.string() }) })
201-
.agent({
202-
id: "my-chat",
203-
tools: myTools,
204-
onTurnStart: async ({ uiMessages, writer }) => {
205-
writer.write({ type: "data-turn-status", data: { status: "preparing" } });
206-
},
207-
run: async ({ messages, tools, signal }) =>
208-
streamText({ ...chat.toStreamTextOptions({ tools }), model, messages, abortSignal: signal }),
209-
});
210-
```
211-
212-
Build `MyChatUIMessage` as `UIMessage<unknown, MyDataTypes, InferUITools<typeof tools>>` (or, for
213-
tools only, `InferChatUIMessageFromTools<typeof tools>` from `@trigger.dev/sdk/ai`). On the
214-
frontend, narrow `useChat` with `InferChatUIMessage<typeof myChat>` from `@trigger.dev/sdk/chat/react`.
215-
216-
### 5. Lifecycle hooks and stop
217-
218-
`chat.agent` accepts hooks that fire in a fixed per-turn order:
219-
220-
```text
221-
onValidateMessages -> hydrateMessages -> onChatStart (chat's first message only)
222-
-> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnComplete
223-
```
224-
225-
`onBoot` fires once per worker process (every fresh boot, including continuation runs) and is where
226-
`chat.local`, DB connections, and per-process state belong. `onChatStart` fires only on the chat's
227-
first message. Suspend/resume use `onChatSuspend` / `onChatResume`. Config options include
228-
`tools`, `clientDataSchema`, `maxTurns` (100), `turnTimeout` ("1h"), `idleTimeoutInSeconds` (30),
229-
`uiMessageStreamOptions`, and `exitAfterPreloadIdle`. There is no generic `retry`; `chat.agent`
230-
runs with `maxAttempts: 1` internally.
231-
232-
Stop is load-bearing: the `signal` passed to `run` aborts on stop or cancel. Forward it as
233-
`abortSignal` to `streamText`, or the Stop button updates the UI while the model keeps generating
234-
server-side.
235-
236-
```ts
237-
run: async ({ messages, signal }) =>
238-
streamText({ ...chat.toStreamTextOptions(), model, messages, abortSignal: signal, stopWhen: stepCountIs(15) });
239-
```
240-
241-
### 6. Migrating from a plain AI SDK `streamText` route
242-
243-
There is no API route in this model. The transport replaces the route round-trip, so:
244-
245-
- Delete the route handler. Move per-request auth into the two server actions from Setup step 2.
246-
- Move the `streamText` call into `run`. It already receives pre-converted `ModelMessage[]`.
247-
- Return the `StreamTextResult` (it auto-pipes) and add `...chat.toStreamTextOptions()` first.
248-
- On the client, swap the `api` URL for `useTriggerChatTransport`; `useChat` stays the same shape.
22+
If those paths don't exist, `@trigger.dev/sdk` isn't installed yet — install it first. In a non-hoisted layout, resolve the package with `node -p "require.resolve('@trigger.dev/sdk/package.json')"` and read `skills/` + `docs/` beside it.
24923

25024
## Common mistakes
25125

@@ -283,13 +57,4 @@ There is no API route in this model. The transport replaces the route round-trip
28357

28458
## References
28559

286-
- `chat-agent-advanced` skill - lifecycle hooks in depth, sessions, raw-task primitives
287-
(`chat.createSession`, `chat.customAgent`, `chat.stream`), compaction, HITL approvals, recovery.
288-
- `realtime-and-frontend` skill - Realtime hooks and frontend streaming beyond the chat transport.
289-
- `authoring-tasks` skill - base `task()` semantics, `ctx`, and standard lifecycle hooks.
290-
- Docs: /ai-chat/quick-start, /ai-chat/backend, /ai-chat/tools, /ai-chat/types, /ai-chat/frontend
291-
292-
## Version
293-
294-
Generated for `@trigger.dev/sdk` `{{TRIGGER_SDK_VERSION}}`. Re-run the trigger.dev skills installer
295-
after upgrading.
60+
Sibling skills: **chat-agent-advanced** (Sessions primitive, custom transports, sub-agents, HITL, fast starts, resilience, testing, upgrades), **authoring-tasks** and **realtime-and-frontend** (the task + frontend foundations chat builds on).

0 commit comments

Comments
 (0)