Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c9a6180
docs: ai chat.task
ericallam Mar 16, 2026
092d25e
docs: rename warmTimeout to idleTimeout in ai-chat docs
ericallam Mar 23, 2026
979a4fb
add docs for prompts
ericallam Mar 24, 2026
969d14b
better compaction support in createSession and manual tasks
ericallam Mar 24, 2026
e86877f
docs: add prompts, compaction, and pending messages docs
ericallam Mar 25, 2026
c7bbddd
document the writer stuff
ericallam Mar 26, 2026
330b664
Add background injection docs
ericallam Mar 26, 2026
8c134d2
docs(ai-chat): add Types page, link toolExecute and withUIMessage, fi…
ericallam Mar 27, 2026
9a8a2d0
Add run-scoped PAT renewal for chat transport
ericallam Mar 27, 2026
be94422
patterns and the ctx thing
ericallam Mar 27, 2026
8405d67
docs: add onChatSuspend/onChatResume, exitAfterPreloadIdle, withClien…
ericallam Mar 28, 2026
28ecfd2
code sandbox and database patterns
ericallam Mar 28, 2026
ddae077
docs: rename chat.task to chat.agent across all AI docs
ericallam Mar 30, 2026
1d33d3d
subagents and AgentChat docs
ericallam Apr 2, 2026
013d619
remove references to the ai chat reference project from the docs
ericallam Apr 2, 2026
40e4cc1
agent mcp tools docs
ericallam Apr 2, 2026
83861bc
docs for validating ui messages
ericallam Apr 2, 2026
0b7b248
version upgrades
ericallam Apr 3, 2026
f9ac744
docs for stopping chats after resume
ericallam Apr 11, 2026
6b9c1b1
docs: add tool approvals and stop-after-resume documentation
ericallam Apr 14, 2026
c40056b
cover tool approvals in the client protocol
ericallam Apr 14, 2026
ad346fd
Cover passing a custom message ID generator
ericallam Apr 14, 2026
b7d3431
docs: add chat.response API, persistent data parts, transient flag, t…
ericallam Apr 14, 2026
b21e82f
add agent prerelease changelog
ericallam Apr 14, 2026
00b312f
docs: add hydrateMessages, chat.history, and actions documentation
ericallam Apr 15, 2026
9ed2aa7
Add branching pattern
ericallam Apr 15, 2026
68a075f
new changelog for 0.0.0-chat-prerelease-20260415164455
ericallam Apr 15, 2026
2441148
docs: multi-tab coordination, error stack truncation changelog
ericallam Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,396 changes: 1,396 additions & 0 deletions docs/ai-chat/backend.mdx

Large diffs are not rendered by default.

192 changes: 192 additions & 0 deletions docs/ai-chat/background-injection.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
---
title: "Background injection"
sidebarTitle: "Background injection"
description: "Inject context from background work into the agent's conversation — self-review, RAG augmentation, or any async analysis."
---

## Overview

`chat.inject()` queues model messages for injection into the conversation. Messages are picked up at the start of the next turn or at the next `prepareStep` boundary (between tool-call steps).

This is the backend counterpart to [pending messages](/ai-chat/pending-messages) — pending messages come from the user via the frontend, while `chat.inject()` comes from your task code.

## Basic usage

```ts
import { chat } from "@trigger.dev/sdk/ai";

// Queue a system message for injection
chat.inject([
{
role: "system",
content: "The user's account was just upgraded to Pro.",
},
]);
```

Messages are appended to the model messages before the next LLM inference call. The LLM sees them as part of the conversation context.

## Common pattern: defer + inject

The most powerful pattern combines `chat.defer()` (background work) with `chat.inject()` (inject results). Background work runs in parallel with the idle wait between turns, and results are injected before the next response.

```ts
export const myChat = chat.agent({
id: "my-chat",
onTurnComplete: async ({ messages }) => {
// Kick off background analysis — doesn't block the turn
chat.defer(
(async () => {
const analysis = await analyzeConversation(messages);
chat.inject([
{
role: "system",
content: `[Analysis of conversation so far]\n\n${analysis}`,
},
]);
})()
);
},
run: async ({ messages, signal }) => {
return streamText({
...chat.toStreamTextOptions({ registry }),
messages,
abortSignal: signal,
});
},
});
```

### Timing

1. Turn completes, `onTurnComplete` fires
2. `chat.defer()` registers the background work
3. The run immediately starts waiting for the next message (no blocking)
4. Background work completes, `chat.inject()` queues the messages
5. User sends next message, turn starts
6. Injected messages are appended before `run()` executes
7. The LLM sees the injected context alongside the new user message

If the background work finishes *during* a tool-call loop (not between turns), the messages are picked up at the next `prepareStep` boundary instead.

## Example: self-review

A cheap model reviews the agent's response after each turn and injects coaching for the next one. Uses [Prompts](/ai/prompts) for the review prompt and `generateObject` for structured output.

```ts
import { chat } from "@trigger.dev/sdk/ai";
import { prompts } from "@trigger.dev/sdk";
import { streamText, generateObject, createProviderRegistry } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const registry = createProviderRegistry({ openai });

const selfReviewPrompt = prompts.define({
id: "self-review",
model: "openai:gpt-4o-mini",
content: `You are a conversation quality reviewer. Analyze the assistant's most recent response.

Focus on:
- Whether the response answered the user's question
- Missed opportunities to use tools or provide more detail
- Tone mismatches

Be concise. Only flag issues worth fixing.`,
});

export const myChat = chat.agent({
id: "my-chat",
onTurnComplete: async ({ messages }) => {
chat.defer(
(async () => {
const resolved = await selfReviewPrompt.resolve({});

const review = await generateObject({
model: registry.languageModel(resolved.model ?? "openai:gpt-4o-mini"),
...resolved.toAISDKTelemetry(),
system: resolved.text,
prompt: messages
.filter((m) => m.role === "user" || m.role === "assistant")
.map((m) => {
const text =
typeof m.content === "string"
? m.content
: Array.isArray(m.content)
? m.content
.filter((p: any) => p.type === "text")
.map((p: any) => p.text)
.join("")
: "";
return `${m.role}: ${text}`;
})
.join("\n\n"),
schema: z.object({
needsImprovement: z.boolean(),
suggestions: z.array(z.string()),
}),
});

if (review.object.needsImprovement) {
chat.inject([
{
role: "system",
content: `[Self-review]\n\n${review.object.suggestions.map((s) => `- ${s}`).join("\n")}\n\nApply these naturally.`,
},
]);
}
})()
);
},
run: async ({ messages, signal }) => {
return streamText({
...chat.toStreamTextOptions({ registry }),
messages,
abortSignal: signal,
});
},
});
```

The self-review runs on `gpt-4o-mini` (fast, cheap) in the background. If the user sends another message before it completes, the coaching is still injected — `chat.inject()` persists across the idle wait.

## Other use cases

- **RAG augmentation**: After each turn, fetch relevant documents and inject them as context for the next response
- **Safety checks**: Run a moderation model on the response, inject warnings if issues are detected
- **Fact-checking**: Verify claims in the response using search tools, inject corrections
- **Context enrichment**: Look up user/account data based on what was discussed, inject it as system context

## How it differs from pending messages

| | `chat.inject()` | [Pending messages](/ai-chat/pending-messages) |
|---|---|---|
| **Source** | Backend task code | Frontend user input |
| **Triggered by** | Your code (e.g. `onTurnComplete` + `chat.defer()`) | User sending a message during streaming |
| **Injection point** | Start of next turn, or next `prepareStep` boundary | Next `prepareStep` boundary only |
| **Message role** | Any (`system`, `user`, `assistant`) | Typically `user` |
| **Frontend visibility** | Not visible unless you write custom `data-*` chunks | Visible via `usePendingMessages` hook |

## API reference

### chat.inject()

```ts
chat.inject(messages: ModelMessage[]): void
```

Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns — they are not reset when a new turn starts.

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `ModelMessage[]` | Model messages to inject (from the `ai` package) |

Messages are drained (consumed) when:
1. A new turn starts — before `run()` executes
2. A `prepareStep` boundary is reached — between tool-call steps during streaming

<Note>
`chat.inject()` writes to an in-memory queue in the current process. It works from any code running in the same task — lifecycle hooks, deferred work, tool execute functions, etc. It does not work from subtasks or other runs.
</Note>
161 changes: 161 additions & 0 deletions docs/ai-chat/changelog.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
title: "Changelog"
sidebarTitle: "Changelog"
description: "Pre-release updates for AI chat agents."
---

<Update label="April 17, 2026" description="0.0.0-chat-prerelease-20260417152143" tags={["SDK"]}>

## Multi-tab coordination

Prevent duplicate messages when the same chat is open in multiple browser tabs. Enable with `multiTab: true` on the transport.

```tsx
const transport = useTriggerChatTransport({ task: "my-chat", multiTab: true, accessToken });
const { messages, setMessages } = useChat({ id: chatId, transport });
const { isReadOnly } = useMultiTabChat(transport, chatId, messages, setMessages);
```

Only one tab can send at a time. Other tabs enter read-only mode with real-time message updates via `BroadcastChannel`. When the active tab's turn completes, any tab can send next. Crashed tabs are detected via heartbeat timeout (10s).

See [Multi-tab coordination](/ai-chat/frontend#multi-tab-coordination) and [`useMultiTabChat`](/ai-chat/reference#usemultitabchat).

## Error stack truncation

Large error stacks no longer OOM the worker process. Stacks are capped at 50 frames (top 5 + bottom 45), individual lines at 1024 chars, messages at 1000 chars. Applied in `parseError`, `sanitizeError`, and OTel span recording.

</Update>

<Update label="April 15, 2026" description="0.0.0-chat-prerelease-20260415164455" tags={["SDK"]}>

## Fix: `resume: true` hangs on completed turns

When refreshing a page after a turn completed, `useChat` with `resume: true` would hang indefinitely — `reconnectToStream` opened an SSE connection that never received data.

Added `isStreaming` to session state. The transport sets it to `true` when streaming starts and `false` on `trigger:turn-complete`. `reconnectToStream` returns `null` immediately when `isStreaming` is false, so `resume: initialMessages.length > 0` is now safe to pass unconditionally.

The flag flows through `onSessionChange` and is restored from `sessions` — no extra persistence code needed.

</Update>

<Update label="April 15, 2026" description="0.0.0-chat-prerelease-20260415152704" tags={["SDK"]}>

## `hydrateMessages` — backend-controlled message history

Load message history from your database on every turn instead of trusting the frontend accumulator. The hook replaces the built-in linear accumulation entirely — the backend is the source of truth.

```ts
chat.agent({
id: "my-chat",
hydrateMessages: async ({ chatId, trigger, incomingMessages }) => {
const stored = await db.getMessages(chatId);
if (trigger === "submit-message" && incomingMessages.length > 0) {
stored.push(incomingMessages[incomingMessages.length - 1]!);
await db.persistMessages(chatId, stored);
}
return stored;
},
});
```

Tool approval updates are auto-merged after hydration — no extra handling needed.

See [hydrateMessages](/ai-chat/backend#hydratemessages).

## `chat.history` — imperative message mutations

Modify the accumulated message history from any hook or `run()`:

```ts
chat.history.rollbackTo(messageId); // Undo — keep up to this message
chat.history.remove(messageId); // Remove one message
chat.history.replace(id, newMsg); // Edit a message
chat.history.slice(0, -2); // Remove last 2 messages
chat.history.all(); // Read current state
```

See [chat.history](/ai-chat/backend#chat-history).

## Custom actions — `actionSchema` + `onAction`

Send typed actions (undo, rollback, edit) from the frontend via `transport.sendAction()`. Actions wake the agent, fire `onAction`, then trigger a normal `run()` turn.

```ts
chat.agent({
id: "my-chat",
actionSchema: z.discriminatedUnion("type", [
z.object({ type: z.literal("undo") }),
z.object({ type: z.literal("rollback"), targetMessageId: z.string() }),
]),
onAction: async ({ action }) => {
if (action.type === "undo") chat.history.slice(0, -2);
if (action.type === "rollback") chat.history.rollbackTo(action.targetMessageId);
},
});
```

Frontend: `transport.sendAction(chatId, { type: "undo" })`
Server: `agentChat.sendAction({ type: "undo" })`

See [Actions](/ai-chat/backend#actions) and [Sending actions](/ai-chat/frontend#sending-actions).

</Update>

<Update label="April 14, 2026" description="0.0.0-chat-prerelease-20260414181032" tags={["SDK"]}>

## `chat.response` — persistent data parts

Added `chat.response.write()` for writing data parts that both stream to the frontend AND persist in `onTurnComplete`'s `responseMessage` and `uiMessages`.

```ts
// Persists to responseMessage.parts — available in onTurnComplete
chat.response.write({ type: "data-handover", data: { context: summary } });

// Transient — streams to frontend only, not in responseMessage
writer.write({ type: "data-progress", data: { percent: 50 }, transient: true });
```

Non-transient `data-*` chunks written via lifecycle hook `writer.write()` now automatically persist to the response message, matching the AI SDK's default semantics. Add `transient: true` for ephemeral chunks (progress indicators, status updates).

See [Custom data parts](/ai-chat/features#custom-data-parts).

## Tool approvals

Added support for AI SDK tool approvals (`needsApproval: true`). When the model calls a tool that needs approval, the turn completes and the frontend shows approve/deny buttons. After approval, the updated assistant message is sent back and matched by ID in the accumulator.

```ts
const sendEmail = tool({
description: "Send an email. Requires human approval.",
inputSchema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
needsApproval: true,
execute: async ({ to, subject, body }) => { /* ... */ },
});
```

Frontend setup requires `sendAutomaticallyWhen` and `addToolApprovalResponse` from `useChat`. See [Tool approvals](/ai-chat/frontend#tool-approvals).

## `transport.stopGeneration(chatId)`

Added `stopGeneration` method to `TriggerChatTransport` for reliable stop after page refresh / stream reconnect. Works regardless of whether the AI SDK passes `abortSignal` through `reconnectToStream`.

```tsx
const stop = useCallback(() => {
transport.stopGeneration(chatId);
aiStop(); // also update useChat state
}, [transport, chatId, aiStop]);
```

See [Stop generation](/ai-chat/frontend#stop-generation).

## `generateMessageId` support

`generateMessageId` can now be passed via `uiMessageStreamOptions` to control response message ID generation (e.g. UUID-v7). The backend automatically passes `originalMessages` to `toUIMessageStream` so message IDs are consistent between frontend and backend.

## Bug fixes

- **`onTurnComplete` not called**: Fixed `turnCompleteResult?.lastEventId` TypeError that silently skipped `onTurnComplete` when `writeTurnCompleteChunk` returned undefined in dev.
- **Stop during streaming**: Added 2s timeout on `onFinishPromise` so `onBeforeTurnComplete` and `onTurnComplete` fire even when the AI SDK's `onFinish` doesn't fire after abort.
- **`toStreamTextOptions` without `chat.prompt.set()`**: `prepareStep` injection (compaction, steering, background context) now works even when the user passes `system` directly to `streamText` instead of using `chat.prompt.set()`.
- **Background queue vs tool approvals**: Background context injection is now skipped when the last accumulated message is a `tool` message, preventing it from breaking `streamText`'s `collectToolApprovals`.

</Update>
Loading
Loading