Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@
},
"pages": [
"guides/ai-agents/overview",
"guides/ai-agents/chat-agent",
"guides/ai-agents/generate-translate-copy",
"guides/ai-agents/route-question",
"guides/ai-agents/respond-and-check-content",
Expand Down
119 changes: 119 additions & 0 deletions docs/guides/ai-agents/chat-agent.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: "Build a chat agent"
sidebarTitle: "Chat agent"
description: "Create a durable, multi-turn chat agent with chat.agent(), then add tools to it like any AI SDK agent."
---

## Overview

Build a **durable, multi-turn chat agent**. One Trigger.dev run holds the whole conversation, streams tokens to your UI, and stays alive across many back-and-forth messages. The other guides in this section are one-shot workflows (trigger a task, run a fixed sequence of LLM calls, return a result); a chat agent instead owns the session for its whole lifetime.
Comment thread
D-K-P marked this conversation as resolved.
Outdated

[`chat.agent()`](/ai-chat/overview) handles the queuing, retries, resumability and streaming for you. You write the model call, Trigger.dev owns the session. For the full feature set (sessions, fast starts, compaction, sub-agents, the frontend transport), see the [AI chat docs](/ai-chat/overview).

## A minimal agent

Define an agent with `chat.agent()`. The `run` function receives the conversation `messages` (already converted from the frontend's `UIMessage[]`) and an abort `signal`. Return a `StreamTextResult` and it's piped to the frontend automatically.

```ts trigger/chat.ts
Comment thread
D-K-P marked this conversation as resolved.
Outdated
import { chat } from "@trigger.dev/sdk/ai";
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, stepCountIs } from "ai";

export const myChat = chat.agent({
id: "my-chat",
run: async ({ messages, signal }) => {
return streamText({
// Spread chat.toStreamTextOptions() FIRST: it wires up prepareStep
// (compaction, steering, background injection) and telemetry.
...chat.toStreamTextOptions(),
model: anthropic("claude-sonnet-4-5"),
messages,
abortSignal: signal,
stopWhen: stepCountIs(15),
});
},
});
```

<Warning>
Always spread `chat.toStreamTextOptions()` into your `streamText` call, and spread it first. It
wires up the `prepareStep` callback that drives compaction, mid-turn steering and background
injection. Those features silently no-op if the spread is missing.
</Warning>

## Add tools

A chat agent uses tools exactly like any other AI SDK agent. Declare them on the config so their results survive across turns, then pass the `tools` you receive in `run` straight to `streamText`:

```ts trigger/chat.ts
import { chat } from "@trigger.dev/sdk/ai";
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, stepCountIs, tool } from "ai";
import { z } from "zod";

const getCurrentTime = tool({
description: "Get the current server time as an ISO string.",
inputSchema: z.object({}),
execute: async () => ({ now: new Date().toISOString() }),
});

export const myChat = chat.agent({
id: "my-chat",
// Declared here so tool results survive history re-conversion across turns.
tools: { getCurrentTime },
run: async ({ messages, tools, signal }) => {
return streamText({
...chat.toStreamTextOptions(),
model: anthropic("claude-sonnet-4-5"),
messages,
tools,
stopWhen: stepCountIs(15),
abortSignal: signal,
});
},
Comment thread
D-K-P marked this conversation as resolved.
});
```

Swap `getCurrentTime` for whatever your agent needs to do: query a database, call an API, or trigger another Trigger.dev task. See [Tools](/ai-chat/tools) for how tool results are persisted and replayed across turns.

## Wire up the frontend

The browser talks to Trigger.dev directly through the [chat transport](/ai-chat/frontend), so there's no API route to maintain. Expose two server actions (one to start the session, one to mint a session-scoped token) and pass them to `useTriggerChatTransport`, then hand the transport to the AI SDK's `useChat`:

```ts app/actions.ts
"use server";

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

export const startChatSession = chat.createStartSessionAction("my-chat");

export async function mintChatAccessToken(chatId: string) {
return auth.createPublicToken({
scopes: { read: { sessions: chatId }, write: { sessions: chatId } },
expirationTime: "1h",
});
Comment thread
D-K-P marked this conversation as resolved.
}
```

See the [Quick Start](/ai-chat/quick-start) for the complete frontend component.

## A full example

For a complete, real-world chat agent, see the ClickHouse chat agent example. It builds on everything above with generative UI, a versioned system prompt, and real tools against a live database.

<CardGroup cols={2}>
<Card title="ClickHouse chat agent" icon="chart-column" href="/guides/example-projects/clickhouse-chat-agent">
A full example project: a chat agent that answers questions about your data with charts, tables
and maps.
</Card>
<Card title="AI chat overview" icon="message-bot" href="/ai-chat/overview">
How chat agents, sessions and the turn loop work.
</Card>
<Card title="Tools" icon="wrench" href="/ai-chat/tools">
Declaring tools on your agent and how they persist across turns.
</Card>
<Card title="Fast starts" icon="bolt" href="/ai-chat/fast-starts">
Cut first-turn latency with preload and head start.
</Card>
</CardGroup>
16 changes: 8 additions & 8 deletions docs/guides/ai-agents/generate-translate-copy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ In this example, we'll create a workflow that generates and translates copy. Thi

**This task:**

- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
- Uses `experimental_telemetry` to provide LLM logs
- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models
- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard
- Generates marketing copy based on subject and target word count
- Validates the generated copy meets word count requirements (±10 words)
- Translates the validated copy to the target language while preserving tone

```typescript
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { task } from "@trigger.dev/sdk";
import { generateText } from "ai";

Expand All @@ -39,7 +39,7 @@ export const generateAndTranslateTask = task({
run: async (payload: TranslatePayload) => {
// Step 1: Generate marketing copy
const generatedCopy = await generateText({
model: openai("o1-mini"),
model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
Expand Down Expand Up @@ -72,7 +72,7 @@ export const generateAndTranslateTask = task({

// Step 2: Translate to target language
const translatedCopy = await generateText({
model: openai("o1-mini"),
model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
Expand Down Expand Up @@ -103,9 +103,9 @@ On the Test page in the dashboard, select the `generate-and-translate-copy` task

```json
{
marketingSubject: "The controversial new Jaguar electric concept car",
targetLanguage: "Spanish",
targetWordCount: 100,
"marketingSubject": "The controversial new Jaguar electric concept car",
"targetLanguage": "Spanish",
"targetWordCount": 100
}
```

Expand Down
22 changes: 22 additions & 0 deletions docs/guides/ai-agents/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ description: "Real world AI agent example tasks using Trigger.dev"
Generate and maintain GitHub wiki documentation with Claude-powered analysis.
</Card>

<Card
icon="chart-column"
title="ClickHouse chat agent"
href="/guides/example-projects/clickhouse-chat-agent"
>
Build a chat agent that answers questions about your ClickHouse data with charts, tables and maps
using `chat.agent()` and generative UI.
</Card>
<Card
icon="hand"
title="Human-in-the-loop workflow"
Expand Down Expand Up @@ -68,6 +76,20 @@ description: "Real world AI agent example tasks using Trigger.dev"
</Card>
</CardGroup>

## Chat agents

Build a durable, multi-turn chat agent with [`chat.agent()`](/ai-chat/overview). One run per conversation, with streaming, sessions and resumability handled for you.

<CardGroup cols={2}>
<Card
title="Chat agent"
icon="message-bot"
href="/guides/ai-agents/chat-agent"
>
Create a durable, multi-turn chat agent with `chat.agent()`, then add tools to it.
</Card>
</CardGroup>

## Agent fundamentals

These guides will show you how to set up different types of AI agent workflows with Trigger.dev. The examples take inspiration from Anthropic's blog post on [building effective agents](https://www.anthropic.com/research/building-effective-agents).
Expand Down
12 changes: 6 additions & 6 deletions docs/guides/ai-agents/respond-and-check-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ In this example, we'll create a workflow that simultaneously checks content for

**This task:**

- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
- Uses `experimental_telemetry` to provide LLM logs
- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models
- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard
- Uses [`batch.triggerByTaskAndWait`](/triggering#batch-triggerbytaskandwait) to run customer response and content moderation tasks in parallel
- Generates customer service responses using an AI model
- Answers with `claude-sonnet-4-5` and moderates with the faster, cheaper `claude-haiku-4-5`
- Simultaneously checks for inappropriate content while generating responses

```typescript
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { batch, task } from "@trigger.dev/sdk";
import { generateText } from "ai";

Expand All @@ -31,7 +31,7 @@ export const generateCustomerResponse = task({
id: "generate-customer-response",
run: async (payload: { question: string }) => {
const response = await generateText({
model: openai("o1-mini"),
model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
Expand All @@ -54,7 +54,7 @@ export const checkInappropriateContent = task({
id: "check-inappropriate-content",
run: async (payload: { text: string }) => {
const response = await generateText({
model: openai("o1-mini"),
model: anthropic("claude-haiku-4-5"),
messages: [
{
role: "system",
Expand Down
83 changes: 31 additions & 52 deletions docs/guides/ai-agents/route-question.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,78 +16,57 @@ In this example, we'll create a workflow that routes a question to a different A

**This task:**

- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
- Uses `experimental_telemetry` in the source verification and historical analysis tasks to provide LLM logs
- Routes questions using a lightweight model (`o1-mini`) to classify complexity
- Directs simple questions to `gpt-4o` and complex ones to `gpt-o3-mini`
- Uses `generateObject` from the [AI SDK](https://ai-sdk.dev/) to classify the question into a typed routing decision
- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard
- Classifies complexity with a fast, cheap model (`claude-haiku-4-5`)
- Directs simple questions to `claude-haiku-4-5` and complex ones to `claude-sonnet-4-5`
- Returns both the answer and metadata about the routing decision

```typescript
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { task } from "@trigger.dev/sdk";
import { generateText } from "ai";
import { generateObject, generateText } from "ai";
import { z } from "zod";

// Schema for router response
// The router's structured decision. generateObject validates the model
// output against this schema, so there's no manual JSON parsing.
const routingSchema = z.object({
model: z.enum(["gpt-4o", "gpt-o3-mini"]),
model: z.enum(["claude-haiku-4-5", "claude-sonnet-4-5"]),
reason: z.string(),
});

// Router prompt template
const ROUTER_PROMPT = `You are a routing assistant that determines the complexity of questions.
Analyze the following question and route it to the appropriate model:

- Use "gpt-4o" for simple, common, or straightforward questions
- Use "gpt-o3-mini" for complex, unusual, or questions requiring deep reasoning

Respond with a JSON object in this exact format:
{"model": "gpt-4o" or "gpt-o3-mini", "reason": "your reasoning here"}

Question: `;

export const routeAndAnswerQuestion = task({
id: "route-and-answer-question",
run: async (payload: { question: string }) => {
// Step 1: Route the question
const routingResponse = await generateText({
model: openai("o1-mini"),
messages: [
{
role: "system",
content:
"You must respond with a valid JSON object containing only 'model' and 'reason' fields. No markdown, no backticks, no explanation.",
},
{
role: "user",
content: ROUTER_PROMPT + payload.question,
},
],
temperature: 0.1,
// Step 1: Classify the question and pick a model
const { object: routing } = await generateObject({
model: anthropic("claude-haiku-4-5"),
schema: routingSchema,
system:
"You are a routing assistant. Pick the model best suited to answer the question:\n" +
"- claude-haiku-4-5 for simple, common, or straightforward questions\n" +
"- claude-sonnet-4-5 for complex, unusual, or questions needing deep reasoning",
prompt: payload.question,
experimental_telemetry: {
isEnabled: true,
functionId: "route-and-answer-question",
functionId: "route-question",
},
});

// Add error handling and cleanup
let jsonText = routingResponse.text.trim();
if (jsonText.startsWith("```")) {
jsonText = jsonText.replace(/```json\n|\n```/g, "");
}

const routingResult = routingSchema.parse(JSON.parse(jsonText));

// Step 2: Get the answer using the selected model
const answerResult = await generateText({
model: openai(routingResult.model),
messages: [{ role: "user", content: payload.question }],
// Step 2: Answer with the selected model
const answer = await generateText({
model: anthropic(routing.model),
prompt: payload.question,
experimental_telemetry: {
isEnabled: true,
functionId: "answer-question",
},
});

return {
answer: answerResult.text,
selectedModel: routingResult.model,
routingReason: routingResult.reason,
answer: answer.text,
selectedModel: routing.model,
routingReason: routing.reason,
};
},
});
Expand All @@ -97,7 +76,7 @@ export const routeAndAnswerQuestion = task({

## Run a test

Triggering our task with a simple question shows it routing to the gpt-4o model and returning the answer with reasoning:
Triggering our task with a simple question shows it routing to the `claude-haiku-4-5` model and returning the answer with reasoning:

```json
{
Expand Down
Loading
Loading