Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 16 additions & 0 deletions .changeset/agent-skills.md
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.
33 changes: 33 additions & 0 deletions .changeset/chat-actions-no-turn.md
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 }),
```
8 changes: 8 additions & 0 deletions .changeset/chat-agent-delta-wire-snapshots.md
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.
30 changes: 30 additions & 0 deletions .changeset/chat-agent.md
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`.
34 changes: 34 additions & 0 deletions .changeset/chat-head-start.md
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.
21 changes: 21 additions & 0 deletions .changeset/chat-history-read-primitives.md
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 });
}
};
```
6 changes: 6 additions & 0 deletions .changeset/chat-session-attributes.md
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.
8 changes: 8 additions & 0 deletions .changeset/mock-chat-agent-test-harness.md
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.
14 changes: 2 additions & 12 deletions apps/webapp/app/components/code/AIQueryInput.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
import { CheckIcon, PencilSquareIcon, PlusIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { AnimatePresence, motion } from "framer-motion";
import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react";
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
import { Button } from "~/components/primitives/Buttons";
import { Spinner } from "~/components/primitives/Spinner";
import { StreamdownRenderer } from "~/components/code/StreamdownRenderer";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import type { AITimeFilter } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/types";
import { cn } from "~/utils/cn";

// Lazy load streamdown components to avoid SSR issues
const StreamdownRenderer = lazy(() =>
import("streamdown").then((mod) => ({
default: ({ children, isAnimating }: { children: string; isAnimating: boolean }) => (
<mod.ShikiThemeContext.Provider value={["one-dark-pro", "one-dark-pro"]}>
<mod.Streamdown isAnimating={isAnimating}>{children}</mod.Streamdown>
</mod.ShikiThemeContext.Provider>
),
}))
);

type StreamEventType =
| { type: "thinking"; content: string }
| { type: "tool_call"; tool: string; args: unknown }
Expand Down
29 changes: 29 additions & 0 deletions apps/webapp/app/components/code/StreamdownRenderer.tsx
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>
),
};
}
)
);
Loading
Loading