Skip to main content

trigger-chat-agent-advanced

Advanced and operational chat.agent capabilities for Trigger.dev, loaded on demand. Load this when working on the raw Sessions primitive (sessions / SessionHandle), a custom chat transport or the realtime wire protocol, durable sub-agents (AgentChat, chat.stream.writer), human-in-the-loop, steering, actions, background injection (chat.defer / chat.inject), fast starts (preload, Head Start via @trigger.dev/sdk/chat-server), context resilience (compaction, recovery boot, OOM, large payloads), chat.local run-scoped state, offline testing with mockChatAgent, or prerelease/version upgrades. For the everyday chat.agent({...}) definition and the useTriggerChatTransport happy path, use the trigger-authoring-chat-agent skill instead.

معلومات المصدر

المستودع
triggerdotdev/trigger.dev
آخر نشاط في المصدر
٢٦ أكتوبر ٢٠٢٦ في ١٠:٤٠
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٦٬٧١٥
التفرعات
١٬٥٢٦

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
trigger-chat-agent-advanced
description
Advanced and operational chat.agent capabilities for Trigger.dev, loaded on demand. Load this when working on the raw Sessions primitive (sessions / SessionHandle), a custom chat transport or the realtime wire protocol, durable sub-agents (AgentChat, chat.stream.writer), human-in-the-loop, steering, actions, background injection (chat.defer / chat.inject), fast starts (preload, Head Start via @trigger.dev/sdk/chat-server), context resilience (compaction, recovery boot, OOM, large payloads), chat.local run-scoped state, offline testing with mockChatAgent, or prerelease/version upgrades. For the everyday chat.agent({...}) definition and the useTriggerChatTransport happy path, use the trigger-authoring-chat-agent skill instead.
type
core
library
trigger.dev
sources
["docs/ai-chat/sessions.mdx","docs/ai-chat/server-chat.mdx","docs/ai-chat/client-protocol.mdx","docs/ai-chat/pending-messages.mdx","docs/ai-chat/actions.mdx","docs/ai-chat/background-injection.mdx","docs/ai-chat/compaction.mdx","docs/ai-chat/fast-starts.mdx","docs/ai-chat/chat-local.mdx","docs/ai-chat/mcp.mdx","docs/ai-chat/testing.mdx","docs/ai-chat/upgrade-guide.mdx","docs/ai-chat/patterns/sub-agents.mdx","docs/ai-chat/patterns/human-in-the-loop.mdx","docs/ai-chat/patterns/persistence-and-replay.mdx","docs/ai-chat/patterns/recovery-boot.mdx","docs/ai-chat/patterns/oom-resilience.mdx","docs/ai-chat/patterns/large-payloads.mdx","docs/ai-chat/patterns/version-upgrades.mdx","docs/ai-chat/tools.mdx"]
# chat.agent: advanced and operational `chat.agent` is built on **Sessions**: a durable, task-bound, bi-directional I/O channel pair keyed on a stable `externalId` (e.g. `chatId`) that outlives any single run. This skill covers the layers beneath and around the everyday agent: the raw `sessions` API, server-side `AgentChat`, durable sub-agents, actions / background injection, fast starts, compaction and recovery, and the wire protocol for custom transports. Two `chat` namespaces are easy to confuse: the agent definition imports `chat` from `@trigger.dev/sdk/ai`; Head Start / Node-listener server entries import `chat` from `@trigger.dev/sdk/chat-server`. ## Setup Happy path: drive an agent from server-side code (task, webhook, or script) with `AgentChat`. ```ts import { AgentChat } from "@trigger.dev/sdk/chat"; import type { myAgent } from "./trigger/my-agent"; const chat = new AgentChat<typeof myAgent>({ agent: "my-chat", clientData: { userId: "user_123" } }); const stream = await chat.sendMessage("Review PR #42"); const text = await stream.text(); await chat.close(); ``` `sendMessage()` triggers a run on the first call, then reuses it via input streams. `ChatStream` exposes `text()`, `result()` (`{ text, toolCalls, toolResults }`), `messages()` (UIMessage snapshots), and the raw `.stream`. Other methods: `steer(text)`, `stop()`, `sendRaw(uiMessages)`, `sendAction(action)`, `preload()`, `reconnect()`. ## Core patterns ### 1. Raw Sessions for non-chat, bi-directional I/O Reach for `sessions` directly when the chat abstraction does not fit: agent inboxes, approval flows, server-to-server pipelines. `sessions.start` is idempotent on `(env, externalId)`; `externalId` cannot start with `session_`. ```ts import { sessions } from "@trigger.dev/sdk"; const { id, publicAccessToken } = await sessions.start({ type: "chat.agent", externalId: chatId, taskIdentifier: "my-chat", triggerConfig: { tags: [`chat:${chatId}`], basePayload: { chatId, trigger: "preload" } }, }); const session = sessions.open(chatId); // no network call; methods are lazy await session.out.append({ kind: "message", text: "hello" }); const next = await session.in.once<MyEvent>({ timeoutMs: 30_000 }); ``` `sessions.open(id).in` also has `send`, `on(handler)`, `peek`, `wait` (suspends the run, only inside `task.run()`), and `waitWithIdleTimeout`. `.out` has `append`, `pipe`, `writer`, `read`, `writeControl`, and `trimTo`. List with `sessions.list({ type, tag, status, ... })` (`for await`), mutate with `sessions.update`, end with `sessions.close` (terminal, idempotent). ### 2. Durable sub-agent as a streaming tool `AgentChat` inside an AI SDK `tool()` delegates to a durable sub-agent; `chat.forwardSubAgent` streams its reply to the frontend and returns the final message as the tool output. Give the tool a `toModelOutput` so the model sees a compact summary. ```ts import { tool } from "ai"; import { chat } from "@trigger.dev/sdk/ai"; import { AgentChat } from "@trigger.dev/sdk/chat"; import { z } from "zod"; const researchTool = tool({ description: "Delegate research to a specialist agent.", inputSchema: z.object({ topic: z.string() }), execute: async ({ topic }, { toolCallId, abortSignal }) => { const researcher = new AgentChat({ agent: "research-agent" }); try { const stream = await researcher.sendMessage(topic, { abortSignal }); return await chat.forwardSubAgent(stream, { toolCallId }); // chunks out once, final UIMessage back } finally { await researcher.close(); } }, toModelOutput: ({ output: message }) => { const lastText = message?.parts?.findLast((p: { type: string }) => p.type === "text") as | { text?: string } | undefined; return { type: "text", value: lastText?.text ?? "Done." }; }, }); ``` On the frontend, `useSubAgentMessages()` from `@trigger.dev/sdk/chat/react` rebuilds each sub-agent while it runs: pass its `onData` to `useChat` and render `subAgents.get(part.toolCallId) ?? part.output`. Do not `yield* stream.messages()` from a tool: every snapshot repeats the whole message, so the bytes written grow with the square of the reply length. For a subtask exposed via `execute: ai.toolExecute(task)`, stream progress to the agent's run with `chat.stream.writer({ target: "root" })`. `target` accepts `"self" | "parent" | "root" | <runId>`. Inside the subtask, read context with `ai.toolCallId()` and `ai.chatContextOrThrow<typeof myChat>()` (`{ chatId, turn, continuation, clientData }`). ```ts import { chat, ai } from "@trigger.dev/sdk/ai"; const { waitUntilComplete } = chat.stream.writer({ target: "root", execute: ({ write }) => write({ type: "data-research-status", id: partId, data: { query, status: "in-progress" } }), }); await waitUntilComplete(); ``` ### 3. Background injection: defer + inject `chat.defer(promise)` runs work in parallel with streaming (all deferred promises are awaited, with a 5s timeout, before `onTurnComplete`). `chat.inject(messages)` queues `ModelMessage[]` that drain at the next turn start or `prepareStep` boundary. Two lanes, decided by role. A `role: "system"` message goes to the model's instructions, where it is trusted like the system prompt, and applies to the next turn only. Any other role joins the conversation and is untrusted by construction, so put checkable facts there and directives in the system lane. The instructions lane reaches the model only through the managed `streamText` (or a `chat.toStreamTextOptions()` spread), since that is where the SDK can set instructions. ```ts export const myChat = chat.agent({ id: "my-chat", onTurnComplete: async ({ messages }) => { chat.defer( (async () => { const analysis = await analyzeConversation(messages); chat.inject([{ role: "system", content: `[Analysis]\n\n${analysis}` }]); })() ); }, registry, run: async ({ messages, signal, streamText }) => streamText({ messages, abortSignal: signal, stopWhen: stepCountIs(15) }), }); ``` ### 4. Compaction (threshold-based) `compaction.shouldCompact` decides when, `summarize` produces the summary that replaces the model messages. UI messages are preserved by default (customize via `compactUIMessages`). The `prepareStep` that performs inner-loop compaction rides the managed `streamText`, which composes a `prepareStep` you pass after it. Spreading `chat.toStreamTextOptions()` and then passing your own replaces it, switching compaction off. ```ts compaction: { shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000, summarize: async ({ messages }) => (await generateText({ model: anthropic("claude-haiku-4-5"), messages: [...messages, { role: "user", content: "Summarize concisely." }], })).text, }, ``` ### 5. Actions: edit state, and optionally answer `actionSchema` validates; `onAction` edits via `chat.history` (`slice`, `replace`, `rollbackTo`, `remove`, `getPendingToolCalls`, `extractNewToolResults`). An action fires `hydrateMessages` and `onAction` only. Return nothing for an edit-only action: no model call, and the turn counter does not advance. Return `chat.turn()` to answer after the edit: a turn runs on the edited history with everything a turn has (the agent's system prompt and tools, steering, compaction, injected instructions, `onTurnStart` and `onTurnComplete`, persistence), and `run()` receives it with `trigger: "action-turn"`. `onAction` has no `streamText` argument, and returning a `StreamTextResult`, string or `UIMessage` from it throws. Persistence splits by model. With transcript storage (`storage` on `chat.agent`; the platform snapshot by default), the runtime hands storage a changeset with `reason: "action"` after an action that changed the conversation: an undo is one `truncateAfter`, a regenerate is a `truncateAfter` followed by the new answer's `put` when the turn completes, and an edit is a `put` for the edited id. With the deprecated `hydrateMessages` your store is the source of truth and the runtime does not write, so mirror every mutation yourself: a regenerate is a delete and an insert, and the answer that follows `chat.turn()` arrives through `onTurnComplete` like any turn's answer. ```ts export const myChat = chat.agent({ id: "my-chat", actionSchema: z.discriminatedUnion("type", [ z.object({ type: z.literal("undo") }), z.object({ type: z.literal("regenerate") }), z.object({ type: z.literal("rollback"), targetMessageId: z.string() }), ]), onAction: async ({ action }) => { if (action.type === "undo") chat.history.slice(0, -2); // edit only if (action.type === "rollback") chat.history.rollbackTo(action.targetMessageId); if (action.type === "regenerate") { chat.history.slice(0, -1); return chat.turn(); // answer the edited history } }, run: async ({ messages, signal, streamText }) => streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }), }); ``` Send from the browser through `useChat`, so the answer a turn produces renders like any turn: `sendMessage(undefined, { body: { action: { type: "regenerate" } } })`, `regenerate({ body: { action } })`, or `useChatActions({ sendMessage })` from `@trigger.dev/sdk/chat/react`. For regeneration, use `regenerate()` from `useChat` (Vercel AI SDK) which removes the last assistant message before streaming the new one; calling `sendMessage` with a regenerate action appends without removal, leaving both answers visible. `transport.sendAction(chatId, action)` returns a raw stream the caller must read and apply itself. Server-side, use `agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" })`. ### 6. Fast starts: Head Start `chat.headStart` (from `@trigger.dev/sdk/chat-server`, NOT `/ai`) returns a Web Fetch handler that serves turn 1 from your own warm process, then hands off to the agent on turn 2+. Tools passed here must be **schema-only** (a module importing `ai` + `zod` only); heavy executes stay in the task. ```ts import { chat } from "@trigger.dev/sdk/chat-server"; import { anthropic } from "@ai-sdk/anthropic"; import { headStartTools } from "@/lib/chat-tools/schemas"; export const chatHandler = chat.headStart({ agentId: "my-chat", // `streamText` from the run argument owns `messages`, `prompt`, `stopWhen` // and `abortSignal`: the handover needs `stopWhen: stepCountIs(1)` so the agent, // not this handler, runs step 2 onward. Passing any of them is a type error. run: async ({ streamText }) => streamText({ model: anthropic("claude-sonnet-4-6"), system: "You are helpful.", tools: headStartTools, }), }); // Next.js: export const POST = chatHandler; Transport: headStart: "/api/chat" ``` Node-only frameworks wrap a Web Fetch handler with `chat.toNodeListener(handler)`. Use the **same model** on both sides to avoid a tone shift between turn 1 and turn 2+. ### 7. chat.local: init in onBoot, not onChatStart `chat.local<T>({ id })` is module-level, shallow-proxy, run-scoped state. Initialize it in `onBoot` (fires on every fresh worker, including continuation runs), never `onChatStart`. ```ts const userContext = chat.local<{ name: string; plan: "free" | "pro" }>({ id: "userContext" }); export const myChat = chat.agent({ id: "my-chat", onBoot: async ({ clientData }) => userContext.init({ name: "Alice", plan: "pro" }), run: async ({ messages, signal }) => streamText({ /* ... */ }), }); ``` ### 8. Pending messages (mid-stream user input) A message sent while a turn is streaming should NOT cancel the stream. Configure `pendingMessages` (`shouldInject`, `prepare`, `onReceived`, `onInjected`) on the agent so the managed `streamText`'s `prepareStep` folds them in at the next boundary. An injected steering message is part of the conversation your hooks see, so it arrives in `uiMessages` and `newUIMessages` at `onTurnComplete` and an app persisting from there stores it without extra work. On the frontend, `usePendingMessages` returns `pending`, `steer(text)`, `queue(text)`, and `promoteToSteering(id)`; send via `transport.sendPendingMessage(chatId, uiMessage, metadata?)`. ### 9. Recovery and version upgrades `onRecoveryBoot` fires only when a **partial assistant message exists on the tail** (interrupted deploy, crash, OOM retry). It does NOT fire on `chat.requestUpgrade()`, which is a graceful exit with
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub