-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(sdk): chat.agent — runtime + browser transport (2/4) #3543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| --- | ||
| "@trigger.dev/sdk": patch | ||
| "@trigger.dev/core": patch | ||
| "@trigger.dev/build": patch | ||
| "trigger.dev": patch | ||
| --- | ||
|
|
||
| Add Agent Skills for `chat.agent`. Drop a folder with a `SKILL.md` and any helper scripts/references next to your task code, register it with `skills.define({ id, path })`, and the CLI bundles it into the deploy image automatically — no `trigger.config.ts` changes. The agent gets a one-line summary in its system prompt and discovers full instructions on demand via `loadSkill`, with `bash` and `readFile` tools scoped per-skill (path-traversal guards, output caps, abort-signal propagation). | ||
|
|
||
| ```ts | ||
| const pdfSkill = skills.define({ id: "pdf-extract", path: "./skills/pdf-extract" }); | ||
|
|
||
| chat.skills.set([await pdfSkill.local()]); | ||
| ``` | ||
|
|
||
| Built on the [AI SDK cookbook pattern](https://ai-sdk.dev/cookbook/guides/agent-skills) — portable across providers. SDK + CLI only for now; dashboard-editable `SKILL.md` text is on the roadmap. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| --- | ||
| "@trigger.dev/sdk": minor | ||
| --- | ||
|
|
||
| `chat.agent` actions are no longer treated as turns. They fire `hydrateMessages` and `onAction` only — no `onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete`, no `run()`, no turn-counter increment. The trace span is named `chat action` instead of `chat turn N`. | ||
|
|
||
| `onAction` can now return a `StreamTextResult`, `string`, or `UIMessage` to produce a model response from the action; returning `void` (the previous and now default) is side-effect-only. | ||
|
|
||
| **Migration**: if you previously had `run()` branching on `payload.trigger === "action"`, return your `streamText(...)` from `onAction` instead. If you persisted in `onTurnComplete`, do that work inside `onAction`. For any other state-only action, just remove your skip-the-model workaround — the default is now correct. | ||
|
|
||
| ```ts | ||
| // before | ||
| onAction: async ({ action }) => { | ||
| if (action.type === "regenerate") { | ||
| chat.store.set({ skipModelCall: false }); | ||
| chat.history.slice(0, -1); | ||
| } | ||
| }, | ||
| run: async ({ messages, signal }) => { | ||
| if (chat.store.get()?.skipModelCall) return; | ||
| return streamText({ model, messages, abortSignal: signal }); | ||
| }, | ||
|
|
||
| // after | ||
| onAction: async ({ action, messages, signal }) => { | ||
| if (action.type === "regenerate") { | ||
| chat.history.slice(0, -1); | ||
| return streamText({ model, messages, abortSignal: signal }); | ||
| } | ||
| }, | ||
| run: async ({ messages, signal }) => | ||
| streamText({ model, messages, abortSignal: signal }), | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "@trigger.dev/sdk": patch | ||
| "@trigger.dev/core": patch | ||
| --- | ||
|
|
||
| `chat.agent` wire is now delta-only — clients ship at most one new message per `.in/append` instead of the full `UIMessage[]` history. The agent rebuilds prior history at run boot from a JSON snapshot in object storage plus a `wait=0` replay of the `session.out` tail. Long chats stop hitting the 512 KiB body cap on `/realtime/v1/sessions/{id}/in/append`. Snapshot writes happen after every `onTurnComplete`, awaited so they survive idle suspend; reads happen only at run boot. Registering a `hydrateMessages` hook short-circuits both the snapshot read/write and the replay — the customer is the source of truth for history. | ||
|
|
||
| Custom transports that constructed `ChatTaskWirePayload` directly need to drop the `messages: UIMessage[]` field and use `message?: UIMessage` (singular). Built-in transports (`TriggerChatTransport`, `AgentChat`) handle the change below the customer-facing surface — most apps need no changes. Configure object-store env vars (`OBJECT_STORE_*`) on your webapp deployment if you haven't already; without an object store and without `hydrateMessages`, conversations don't survive run boundaries. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| "@trigger.dev/sdk": minor | ||
| --- | ||
|
|
||
| Adds `onBoot` to `chat.agent` — a lifecycle hook that fires once per worker process picking up the chat. Runs for the initial run, preloaded runs, AND reactive continuation runs (post-cancel, crash, `endRun`, `requestUpgrade`, OOM retry), before any other hook. Use it to initialize `chat.local`, open per-process resources, or re-hydrate state from your DB on continuation — anywhere the SAME run picking up after suspend/resume isn't enough. | ||
|
|
||
| ```ts | ||
| const userContext = chat.local<{ name: string; plan: string }>({ id: "userContext" }); | ||
|
|
||
| export const myChat = chat.agent({ | ||
| id: "my-chat", | ||
| onBoot: async ({ clientData, continuation }) => { | ||
| const user = await db.user.findUnique({ where: { id: clientData.userId } }); | ||
| userContext.init({ name: user.name, plan: user.plan }); | ||
| }, | ||
| run: async ({ messages, signal }) => | ||
| streamText({ model: openai("gpt-4o"), messages, abortSignal: signal }), | ||
| }); | ||
| ``` | ||
|
|
||
| If you previously initialized `chat.local` in `onChatStart`, move it to `onBoot` — `onChatStart` is once-per-chat and won't fire on a continuation, leaving `chat.local` uninitialized when `run()` tries to use it. See the upgrade guide for the migration pattern. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| --- | ||
| "@trigger.dev/sdk": minor | ||
| "@trigger.dev/core": patch | ||
| --- | ||
|
|
||
| Run AI chats as durable Trigger.dev tasks. Define the agent in one function, wire `useChat` to it from React, and the conversation survives page refreshes, network blips, and process restarts — with built-in support for tools, HITL approvals, multi-turn state, and stop-mid-stream cancellation. | ||
|
|
||
| ```ts | ||
| import { chat } from "@trigger.dev/sdk/ai"; | ||
| import { streamText } from "ai"; | ||
| import { openai } from "@ai-sdk/openai"; | ||
|
|
||
| export const myChat = chat.agent({ | ||
| id: "my-chat", | ||
| run: async ({ messages, signal }) => | ||
| streamText({ model: openai("gpt-4o"), messages, abortSignal: signal }), | ||
| }); | ||
| ``` | ||
|
|
||
| ```tsx | ||
| import { useChat } from "@ai-sdk/react"; | ||
| import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; | ||
|
|
||
| const transport = useTriggerChatTransport({ task: "my-chat", accessToken }); | ||
| const { messages, sendMessage } = useChat({ transport }); | ||
| ``` | ||
|
|
||
| Lifecycle hooks (`onPreload`, `onTurnStart`, `onTurnComplete`, etc.) cover the common needs around persistence, validation, and post-turn work. `chat.store` gives you a typed shared-data slot the agent and client both read and write. `chat.endRun()` exits cleanly when the agent decides it's done. The transport's `watch` mode lets a dashboard tab observe a run without driving it. | ||
|
|
||
| Drops the pre-Sessions chat stream constants (`CHAT_STREAM_KEY`, `CHAT_MESSAGES_STREAM_ID`, `CHAT_STOP_STREAM_ID`) — migrate to `sessions.open(id).out` / `.in`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| --- | ||
| "@trigger.dev/sdk": minor | ||
| --- | ||
|
|
||
| Add `chat.headStart` — an opt-in fast-path that runs the first turn's `streamText` step in your warm Next.js / Hono / Workers / Express handler while the trigger agent run boots in parallel. Cold-start TTFC drops by ~50% on the first message; the agent owns step 2+ (tool execution, persistence, hooks) so heavy deps stay where they belong. | ||
|
|
||
| ```ts | ||
| // app/api/chat/route.ts (Next.js / any Web Fetch framework) | ||
| import { chat } from "@trigger.dev/sdk/chat-server"; | ||
| import { streamText } from "ai"; | ||
| import { openai } from "@ai-sdk/openai"; | ||
| import { headStartTools } from "@/lib/chat-tools-schemas"; // schema-only | ||
|
|
||
| export const POST = chat.headStart({ | ||
| agentId: "ai-chat", | ||
| run: async ({ chat: chatHelper }) => | ||
| streamText({ | ||
| ...chatHelper.toStreamTextOptions({ tools: headStartTools }), | ||
| model: openai("gpt-4o-mini"), | ||
| system: "You are a helpful AI assistant.", | ||
| }), | ||
| }); | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // browser — opt in by pointing the transport at your handler | ||
| const transport = useTriggerChatTransport({ | ||
| task: "ai-chat", | ||
| accessToken, | ||
| headStart: "/api/chat", // first-turn-only; turn 2+ bypasses the endpoint | ||
| }); | ||
| ``` | ||
|
|
||
| For Node-only frameworks (Express, Fastify, Koa, raw `node:http`) use `chat.toNodeListener(handler)` to bridge the Web Fetch handler to `(req, res)`. Adds a new `@trigger.dev/sdk/chat-server` subpath; bundle stays Web Fetch–only with no `node:*` imports. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| "@trigger.dev/sdk": minor | ||
| --- | ||
|
|
||
| Add read primitives to `chat.history` for HITL flows: `getPendingToolCalls()`, `getResolvedToolCalls()`, `extractNewToolResults(message)`, `getChain()`, and `findMessage(messageId)`. These lift the accumulator-walking logic that customers building human-in-the-loop tools were re-implementing into the SDK. | ||
|
|
||
| Use `getPendingToolCalls()` to gate fresh user turns while a tool call is awaiting an answer. Use `extractNewToolResults(message)` to dedup tool results when persisting to your own store — the helper returns only the parts whose `toolCallId` is not already resolved on the chain. | ||
|
|
||
| ```ts | ||
| const pending = chat.history.getPendingToolCalls(); | ||
| if (pending.length > 0) { | ||
| // an addToolOutput is expected before a new user message | ||
| } | ||
|
|
||
| onTurnComplete: async ({ responseMessage }) => { | ||
| const newResults = chat.history.extractNewToolResults(responseMessage); | ||
| for (const r of newResults) { | ||
| await db.toolResults.upsert({ id: r.toolCallId, output: r.output, errorText: r.errorText }); | ||
| } | ||
| }; | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@trigger.dev/sdk": patch | ||
| "@trigger.dev/core": patch | ||
| --- | ||
|
|
||
| Stamp `gen_ai.conversation.id` (the chat id) on every span and metric emitted from inside a `chat.task` or `chat.agent` run. Lets you filter dashboard spans, runs, and metrics by the chat conversation that produced them — independent of the run boundary, so multi-run chats correlate cleanly. No code changes required on the user side. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "@trigger.dev/sdk": patch | ||
| "@trigger.dev/core": patch | ||
| --- | ||
|
|
||
| Unit-test `chat.agent` definitions offline with `mockChatAgent` from `@trigger.dev/sdk/ai/test`. Drives a real agent's turn loop in-process — no network, no task runtime — so you can send messages, actions, and stop signals via driver methods, inspect captured output chunks, and verify hooks fire. Pairs with `MockLanguageModelV3` from `ai/test` for model mocking. `setupLocals` lets you pre-seed `locals` (DB clients, service stubs) before `run()` starts. | ||
|
|
||
| The broader `runInMockTaskContext` harness it's built on lives at `@trigger.dev/core/v3/test` — useful for unit-testing any task code, not just chat. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { lazy } from "react"; | ||
| import type { CodeHighlighterPlugin } from "streamdown"; | ||
|
|
||
| export const StreamdownRenderer = lazy(() => | ||
| Promise.all([import("streamdown"), import("@streamdown/code"), import("./shikiTheme")]).then( | ||
| ([{ Streamdown }, { createCodePlugin }, { triggerDarkTheme }]) => { | ||
| // Type assertion needed: @streamdown/code and streamdown resolve different shiki | ||
| // versions under pnpm, causing structurally-identical CodeHighlighterPlugin types | ||
| // to be considered incompatible (different BundledLanguage string unions). | ||
| const codePlugin = createCodePlugin({ | ||
| themes: [triggerDarkTheme, triggerDarkTheme], | ||
| }) as unknown as CodeHighlighterPlugin; | ||
|
|
||
| return { | ||
| default: ({ | ||
| children, | ||
| isAnimating = false, | ||
| }: { | ||
| children: string; | ||
| isAnimating?: boolean; | ||
| }) => ( | ||
| <Streamdown isAnimating={isAnimating} plugins={{ code: codePlugin }}> | ||
| {children} | ||
| </Streamdown> | ||
| ), | ||
| }; | ||
| } | ||
| ) | ||
| ); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.