Skip to main content

ai-chat-agents

Build durable AI chat agents on Trigger.dev with chat.agent, sessions and the useTriggerChatTransport React transport (requires server 4.5.0 or newer and AI SDK 5+). Use when the user asks about "chat.agent", "trigger.dev chat agent", "AI chat on trigger.dev", "useTriggerChatTransport", "trigger.dev sessions", "start_agent_chat", "human in the loop chat", or migrating a streamText route to Trigger.dev.

Source facts

Repository
Agents-Store/opencode-plugins
Last source activity
October 3, 2026 at 23:16
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
ai-chat-agents
description
Build durable AI chat agents on Trigger.dev with chat.agent, sessions and the useTriggerChatTransport React transport (requires server 4.5.0 or newer and AI SDK 5+). Use when the user asks about "chat.agent", "trigger.dev chat agent", "AI chat on trigger.dev", "useTriggerChatTransport", "trigger.dev sessions", "start_agent_chat", "human in the loop chat", or migrating a streamText route to Trigger.dev.
# AI Chat Agents (chat.agent) A `chat.agent` runs a whole conversation as one long-lived Trigger.dev task: it wakes when a message arrives, is suspended when none do, and keeps its state across page refreshes, deploys and idle gaps. There are no API routes: the browser talks to the agent through a transport, history accumulates server-side, and the client sends only the new message each turn. > **Version requirements (decision D8: the plugin baseline stays server 4.4.4).** Chat agents and Sessions need **server ≥ 4.5.0** (the Session tables first appeared in 4.4.5) and `@trigger.dev/sdk` ≥ 4.5.0. They work with the Vercel AI SDK `ai` v5, v6 or v7 only (v4 is not supported). 4.6.x changes: Zod 4 by default (peer `^3.25.56 || ^4`), reading a session's `.in` stream needs the secret key (public tokens get 403), `hydrateMessages` is deprecated in favour of `loadContext` / transcript storage, and 4.6.0-4.6.3 have a warm-start defect with Zod 3 (use 4.6.4 or later). Self-hosted: realtime streams run on the bundled `s2` service (see the **deployment** skill). Model ids below are placeholders. ## 1. Define the agent ```ts // trigger/chat.ts import { chat } from "@trigger.dev/sdk/ai"; import { stepCountIs } from "ai"; // helpers come from "ai"; streamText does not import { anthropic } from "@ai-sdk/anthropic"; export const myChat = chat.agent({ id: "my-chat", // Take `streamText` from the run argument, NOT from "ai" (see Common mistakes) run: async ({ messages, signal, streamText }) => streamText({ model: anthropic("<model id>"), messages, abortSignal: signal, // makes the Stop button actually stop generation stopWhen: stepCountIs(15), }), }); ``` `messages` arrive already converted to `ModelMessage[]`. Returning the `StreamTextResult` pipes it to the frontend; when `streamText` is called deep in a helper, call `await chat.pipe(result)` instead. Add `trigger.config.ts` with `maxDuration` like any task; an agent is a task and deploys the same way. ## 2. Two server actions Both run on your server so the browser never holds the environment secret key. Both are also public endpoints: the browser sends any chat id it likes, and the token `mintChatAccessToken` returns reads and writes that whole conversation. So each action authenticates the caller and checks that the chat is theirs. The owner is recorded on the Session row (`metadata.ownerId`) by server code, never taken from the browser. This is the same pattern as `refreshRunToken` in the **realtime** skill. ```ts // app/actions.ts "use server"; import { auth, NotFoundError, sessions } from "@trigger.dev/sdk"; import { chat } from "@trigger.dev/sdk/ai"; import { requireUserId } from "@/lib/session"; // your session library: throws when nobody is signed in import type { myChat } from "@/trigger/chat"; // type only const start = chat.createStartSessionAction<typeof myChat>("my-chat"); // The chat's Session, or null when nobody has started it yet; throws when it belongs to someone else. async function findOwnedSession(userId: string, chatId: string) { const session = await sessions.retrieve(chatId).catch((error) => { if (error instanceof NotFoundError) return null; throw error; }); if (session && session.metadata?.ownerId !== userId) throw new Error("Forbidden"); return session; } // Idempotent per (environment, chatId). Pass on only `chatId` and `clientData`: a browser-supplied // `triggerConfig` or `metadata` would let the caller choose the queue, the machine or the owner. export async function startChatSession(params: { chatId: string; clientData?: Parameters<typeof start>[0]["clientData"]; }) { const userId = await requireUserId(); await findOwnedSession(userId, params.chatId); // a repeat start for a known chat id returns that session's token return start({ chatId: params.chatId, clientData: params.clientData, metadata: { ownerId: userId } }); } // The transport calls this to refresh an expired token export async function mintChatAccessToken(chatId: string) { const userId = await requireUserId(); // The transport starts the session first, so a refresh always finds it if (!(await findOwnedSession(userId, chatId))) throw new Error("Forbidden"); return auth.createPublicToken({ scopes: { read: { sessions: chatId }, write: { sessions: chatId } }, expirationTime: "1h", }); } ``` `clientData` also arrives from the browser (the transport sends it on every turn), so the agent must not treat an identity inside it as proof; the owner on the Session row is the one set here. Prefer random chat ids (`crypto.randomUUID()`), which also makes the start-time race between two users on one id practically impossible. ## 3. Frontend ```tsx "use client"; import { useChat } from "@ai-sdk/react"; import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; import type { myChat } from "@/trigger/chat"; import { mintChatAccessToken, startChatSession } from "@/app/actions"; export function Chat() { const transport = useTriggerChatTransport<typeof myChat>({ task: "my-chat", accessToken: ({ chatId }) => mintChatAccessToken(chatId), startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }), }); const { messages, sendMessage, stop, status } = useChat({ transport }); // render messages; sendMessage({ text }); a Stop button calling stop() while status === "streaming" } ``` ## Tools Declare tools on the config and pass the typed set back, so each tool's `toModelOutput` is re-applied when history is converted on later turns. A Trigger.dev task can be the tool: `tool({ description, inputSchema, execute: ai.toolExecute(task) })` (see **ai-agent-patterns**, `references/ai-tool.md`). ```ts export const myChat = chat.agent({ id: "my-chat", tools, // declared once run: async ({ messages, tools, signal, streamText }) => streamText({ model, messages, tools, abortSignal: signal, stopWhen: stepCountIs(15) }), }); ``` ## Lifecycle Per turn the hooks fire in order: `onValidateMessages`, storage load, `onChatStart` (first message only), `onTurnStart`, `run()`, `onBeforeTurnComplete`, `onTurnComplete`, storage save. `onBoot` fires once per worker process (put `chat.local`, DB connections there). Options include `maxTurns` (100), `turnTimeout` (`"1h"`), `idleTimeoutInSeconds` (30). `chat.agent` runs with `maxAttempts: 1`; there is no generic `retry`. Managed prompts plug in with `chat.prompt.set(await myPrompt.resolve(vars))` in `onChatStart` and `...chat.toStreamTextOptions({ registry })` (see **managed-prompts**). ## Sessions, concurrency and MCP - A **Session** is a stateful execution of an agent with a durable input stream (`.in`) and output stream (`.out`); one Session can span many runs. `chat.agent` is built on them; `sessions.start()`, `sessions.open(id)`, `session.in.send/wait`, `session.out.append/read` are the raw primitives. - Per-tenant concurrency for chats (server ≥ 4.7.0): pass `concurrencyKey` (chat or tenant id) through `chat.createStartSessionAction("my-chat", { triggerConfig: { concurrencyKey: userId } })`. Without a key a session shares the task's keyless pool. - From an MCP client use `list_agents`, `start_agent_chat`, `send_agent_message`, `close_agent_chat` and, for side channels, `read_session_channel` / `write_session_channel` (see **mcp-patterns**). Chats started that way live in the MCP server process. ## Common mistakes - **Calling the `streamText` imported from `ai`.** It silently skips compaction, mid-turn steering, background injection, the managed system prompt, registry model resolution and telemetry. Use the one on the `run` argument, or spread `chat.toStreamTextOptions()` in a custom agent. - **Not forwarding `signal`.** Without `abortSignal: signal`, Stop updates the UI but the model keeps generating. - **Tools only on `streamText`.** Also declare them on `chat.agent({ tools })`. - **Initialising `chat.local` in `onChatStart`.** Do it in `onBoot`; continuation runs skip `onChatStart`. - **Minting tokens in the browser.** Only the two server actions touch the secret key. - **Minting a token without an ownership check.** `mintChatAccessToken(chatId)` and `startChatSession` are public Server Actions; without `requireUserId()` and the owner check, anyone who knows a chat id can read and write that conversation. - **Returning the raw error from `uiMessageStreamOptions.onError`.** It leaks internals; return a sanitized string. ## Sources The SDK package ships version-exact docs and skills: `node_modules/@trigger.dev/sdk/docs/ai-chat/` and `node_modules/@trigger.dev/sdk/skills/` (`npx trigger.dev@latest skills` installs them as agent skills: trigger-authoring-chat-agent, trigger-chat-agent-advanced, ...). Online: https://trigger.dev/docs/ai-chat/overview and `/quick-start`.
View on GitHub