Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
4bb1168
feat: add @trigger.dev/ai package with TriggerChatTransport
cursoragent Feb 15, 2026
3caf47b
test: add comprehensive unit tests for TriggerChatTransport
cursoragent Feb 15, 2026
8ba87a7
refactor: polish TriggerChatTransport implementation
cursoragent Feb 15, 2026
393bd79
test: add abort signal, multiple sessions, and body merging tests
cursoragent Feb 15, 2026
3e0711c
chore: add changeset for @trigger.dev/ai package
cursoragent Feb 15, 2026
f8f2f74
refactor: remove internal ChatSessionState from public exports
cursoragent Feb 15, 2026
4db5f07
feat: support dynamic accessToken function for token refresh
cursoragent Feb 15, 2026
b147407
refactor: avoid double-resolving accessToken in sendMessages
cursoragent Feb 15, 2026
b7a921b
feat: add chat transport and AI chat helpers to @trigger.dev/sdk
cursoragent Feb 15, 2026
77f0dc7
test: move chat transport tests to @trigger.dev/sdk
cursoragent Feb 15, 2026
edbd6d0
refactor: delete packages/ai/ — moved to @trigger.dev/sdk subpaths
cursoragent Feb 15, 2026
a888d67
chore: update changeset to target @trigger.dev/sdk
cursoragent Feb 15, 2026
9954b92
fix: address CodeRabbit review feedback
cursoragent Feb 15, 2026
61c766e
docs(ai): add AI Chat with useChat guide
cursoragent Feb 15, 2026
3e1ab7f
feat(reference): add ai-chat Next.js reference project
cursoragent Feb 15, 2026
296c525
fix(reference): use compatible @ai-sdk v3 packages, await convertToMo…
cursoragent Feb 15, 2026
61c9ccd
Use a single run with iterative waitpoint token completions
ericallam Feb 21, 2026
b3ef2d5
Added tool example
ericallam Feb 21, 2026
c87a321
expose a useTriggerChatTransport hook
ericallam Feb 21, 2026
c5032af
use input streams and rename chatTask and chatState to chat.task and …
ericallam Mar 3, 2026
ce73f9b
add stopping support and fix issue with the OpenAI responses API and …
ericallam Mar 4, 2026
206ea0f
Add warmTimeoutInSeconds option
ericallam Mar 4, 2026
7c8652c
Add clientData support
ericallam Mar 4, 2026
5ba385c
provide already converted UIMessages to the run function for better dx
ericallam Mar 4, 2026
7729b25
Added better telemetry support to view turns
ericallam Mar 4, 2026
23da303
Fix double looping when resuming from an input stream waitpoint
ericallam Mar 4, 2026
ad61bd8
Add some pending message support in the example
ericallam Mar 4, 2026
71e0d85
Accumulate messages in the task, allowing us to only have to send use…
ericallam Mar 5, 2026
72847a8
build full example with persisting messages, adding necessary hooks, …
ericallam Mar 5, 2026
963584f
Add ai chat to the sidebar for now
ericallam Mar 5, 2026
5988c24
remove postinstall hook
ericallam Mar 5, 2026
d32a66e
feat: add onTurnStart hook, lastEventId support, and stream resume de…
ericallam Mar 5, 2026
d6453fa
Minor fixes around reconnecting streams
ericallam Mar 6, 2026
8b36338
update pnpm link file
ericallam Mar 6, 2026
0ef2e2a
fixed chat tests
ericallam Mar 6, 2026
bd36b97
use locals for the chat pipe counter instead of a module global
ericallam Mar 6, 2026
52ed447
Add triggerOptions to the transport, auto-tag with the chat ID
ericallam Mar 6, 2026
d7e14e9
Make clientData typesafe and pass to all chat.task hooks
ericallam Mar 6, 2026
540d9bb
feat: add chat.local for per-run typed data with Proxy access and dir…
ericallam Mar 6, 2026
3b21f03
feat(chat): add stop handling, abort cleanup, continuation support, a…
ericallam Mar 7, 2026
4149e4e
Some improvements to the example ai-chat
ericallam Mar 7, 2026
1bdd70d
feat(chat): expose typed chat.stream, add deepResearch subtask exampl…
ericallam Mar 8, 2026
afd4f93
feat(ai): pass chat context and toolCallId to subtasks, add typed ai.…
ericallam Mar 8, 2026
6a0b063
feat(chat): add preload support, dynamic tools, and preload-specific …
ericallam Mar 9, 2026
af36799
docs: add mermaid architecture diagrams for ai-chat system
ericallam Mar 9, 2026
6a8e14c
docs: add sequence diagrams to ai-chat guide
ericallam Mar 9, 2026
7b9168e
feat(chat): auto-hydrate chat.local values in ai.tool subtasks
ericallam Mar 9, 2026
27ea804
feat(chat): add chat.defer(), preload toggle, TTFB measurement, and f…
ericallam Mar 9, 2026
903856e
fix(reference): replace hand-rolled HTML stripping with turndown
ericallam Mar 9, 2026
7448066
feat(streams): add inputStream.waitWithWarmup(), warm timeout config …
ericallam Mar 9, 2026
eba8c16
feat(chat): add composable primitives, raw task example, and task mod…
ericallam Mar 10, 2026
2f325ee
Introduce the chat session API and better docs organization
ericallam Mar 10, 2026
2a04f45
Add support for toUIMessageStream() options
ericallam Mar 10, 2026
827ad89
Add metadata to the streamText call
ericallam Mar 12, 2026
924c9ac
feat(chat): add chat.prompt API with provider registry support
ericallam Mar 23, 2026
2ebebed
refactor: rename warmTimeout to idleTimeout across chat APIs
ericallam Mar 23, 2026
149de37
feat: support message compaction
ericallam Mar 24, 2026
45cfd40
better compaction support in our other chat variants
ericallam Mar 24, 2026
61878ec
feat(chat): add compaction option, pendingMessages steering, and useP…
ericallam Mar 25, 2026
a33f294
Add a writer to easily write chunks in callbacks
ericallam Mar 26, 2026
518dc5d
feat(ai): add triggerAndSubscribe method and use it in ai.tool
ericallam Mar 26, 2026
516c21e
feat(chat): add chat.inject() for background context injection and ch…
ericallam Mar 26, 2026
21fa332
feat(sdk): ToolSet typing for toolFromTask, ai.toolExecute, deprecate…
ericallam Mar 27, 2026
ce728f4
Add run-scoped PAT renewal for chat transport
ericallam Mar 27, 2026
2ee97a1
feat(sdk): ctx on chat.task hooks; ai-chat E2B sandbox; docs patterns
ericallam Mar 27, 2026
92b30f7
feat(sdk): add onChatSuspend/onChatResume hooks, exitAfterPreloadIdle…
ericallam Mar 28, 2026
7000a75
Add support for triggering from the backend
ericallam Mar 28, 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
6 changes: 6 additions & 0 deletions .changeset/ai-chat-sandbox-and-ctx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
---

Add `TaskRunContext` (`ctx`) to all `chat.task` lifecycle events, `CompactedEvent`, and `ChatTaskRunPayload`. Export `TaskRunContext` from `@trigger.dev/sdk`.

42 changes: 42 additions & 0 deletions .changeset/ai-sdk-chat-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
"@trigger.dev/sdk": minor
---

Add AI SDK chat transport integration via two new subpath exports:

**`@trigger.dev/sdk/chat`** (frontend, browser-safe):
- `TriggerChatTransport` — custom `ChatTransport` for the AI SDK's `useChat` hook that runs chat completions as durable Trigger.dev tasks
- `createChatTransport()` — factory function

```tsx
import { useChat } from "@ai-sdk/react";
import { TriggerChatTransport } from "@trigger.dev/sdk/chat";

const { messages, sendMessage } = useChat({
transport: new TriggerChatTransport({
task: "my-chat-task",
accessToken,
}),
});
```

**`@trigger.dev/sdk/ai`** (backend, extends existing `ai.tool`/`ai.currentToolOptions`):
- `chatTask()` — pre-typed task wrapper with auto-pipe support
- `pipeChat()` — pipe a `StreamTextResult` or stream to the frontend
- `CHAT_STREAM_KEY` — the default stream key constant
- `ChatTaskPayload` type

```ts
import { chatTask } from "@trigger.dev/sdk/ai";
import { streamText, convertToModelMessages } from "ai";

export const myChatTask = chatTask({
id: "my-chat-task",
run: async ({ messages }) => {
return streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
});
},
});
```
5 changes: 5 additions & 0 deletions .changeset/ai-tool-execute-helper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

Add `ai.toolExecute(task)` so you can pass Trigger's subtask/metadata wiring as the `execute` handler to AI SDK `tool()` while defining `description` and `inputSchema` yourself. Refactors `ai.tool()` to share the same internal handler.
6 changes: 6 additions & 0 deletions .changeset/ai-tool-toolset-typing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
---

Align `ai.tool()` (`toolFromTask`) with the AI SDK `ToolSet` shape: Zod-backed tasks use static `tool()`; returns are asserted as `Tool & ToolSet[string]`. Raise the SDK's minimum `ai` devDependency to `^6.0.116` so emitted types resolve the same `ToolSet` as apps on AI SDK 6.0.x (avoids cross-version `ToolSet` mismatches in monorepos).

6 changes: 6 additions & 0 deletions .changeset/chat-run-pat-renewal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
---

Add run-scoped PAT renewal for chat transport (`renewRunAccessToken`), fail fast on 401/403 for SSE without retry backoff, and export `isTriggerRealtimeAuthError` for auth-error detection.
5 changes: 5 additions & 0 deletions .changeset/dry-sloths-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

Add `chat.withUIMessage<TUIMessage>()` for typed AI SDK `UIMessage` in chat task hooks, optional factory `streamOptions` merged with `uiMessageStreamOptions`, and `InferChatUIMessage` helper. Generic `ChatUIMessageStreamOptions`, compaction, and pending-message event types. `usePendingMessages` accepts a UI message type parameter; re-export `InferChatUIMessage` from `@trigger.dev/sdk/chat/react`.
22 changes: 22 additions & 0 deletions .claude/rules/package-installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
paths:
- "**/package.json"
---

# Installing Packages

When adding a new dependency to any package.json in the monorepo:

1. **Look up the latest version** on npm before adding:
```bash
pnpm view <package-name> version
```
If unsure which version to use (e.g. major version compatibility), confirm with the user.

2. **Edit the package.json directly** — do NOT use `pnpm add` as it can cause issues in the monorepo. Add the dependency with the correct version range (typically `^x.y.z`).

3. **Run `pnpm i` from the repo root** after editing to install and update the lockfile:
```bash
pnpm i
```
Always run from the repo root, not from the package directory.
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This file provides guidance to Claude Code when working with this repository. Su

This is a pnpm 10.23.0 monorepo using Turborepo. Run commands from root with `pnpm run`.

**Adding dependencies:** Edit `package.json` directly instead of using `pnpm add`, then run `pnpm i` from the repo root. See `.claude/rules/package-installation.md` for the full process.

```bash
pnpm run docker # Start Docker services (PostgreSQL, Redis, Electric)
pnpm run db:migrate # Run database migrations
Expand Down
17 changes: 16 additions & 1 deletion packages/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"./extensions/typescript": "./src/extensions/typescript.ts",
"./extensions/puppeteer": "./src/extensions/puppeteer.ts",
"./extensions/playwright": "./src/extensions/playwright.ts",
"./extensions/lightpanda": "./src/extensions/lightpanda.ts"
"./extensions/lightpanda": "./src/extensions/lightpanda.ts",
"./extensions/secureExec": "./src/extensions/secureExec.ts"
},
"sourceDialects": [
"@triggerdotdev/source"
Expand Down Expand Up @@ -65,6 +66,9 @@
],
"extensions/lightpanda": [
"dist/commonjs/extensions/lightpanda.d.ts"
],
"extensions/secureExec": [
"dist/commonjs/extensions/secureExec.d.ts"
]
}
},
Expand Down Expand Up @@ -207,6 +211,17 @@
"types": "./dist/commonjs/extensions/lightpanda.d.ts",
"default": "./dist/commonjs/extensions/lightpanda.js"
}
},
"./extensions/secureExec": {
"import": {
"@triggerdotdev/source": "./src/extensions/secureExec.ts",
"types": "./dist/esm/extensions/secureExec.d.ts",
"default": "./dist/esm/extensions/secureExec.js"
},
"require": {
"types": "./dist/commonjs/extensions/secureExec.d.ts",
"default": "./dist/commonjs/extensions/secureExec.js"
}
}
},
"main": "./dist/commonjs/index.js",
Expand Down
172 changes: 172 additions & 0 deletions packages/build/src/extensions/secureExec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { BuildTarget } from "@trigger.dev/core/v3";
import { BuildManifest } from "@trigger.dev/core/v3/schemas";
import { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build";
import { dirname, resolve, join } from "node:path";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { readPackageJSON } from "pkg-types";

export type SecureExecOptions = {
/**
* Packages available inside the sandbox at runtime.
*
* These are `require()`'d inside the V8 isolate at runtime — the bundler
* never sees them statically. They are marked external and installed as
* deploy dependencies.
*
* @example
* ```ts
* secureExec({ packages: ["jszip", "lodash"] })
* ```
*/
packages?: string[];
};

/**
* Build extension for [secure-exec](https://secureexec.dev) — run untrusted
* JavaScript/TypeScript in V8 isolates with configurable permissions.
*
* Handles the esbuild workarounds needed for secure-exec's runtime
* `require.resolve` calls, native binaries, and module-scope resolution.
*
* @example
* ```ts
* import { secureExec } from "@trigger.dev/build/extensions/secureExec";
*
* export default defineConfig({
* build: {
* extensions: [secureExec()],
* },
* });
* ```
*/
export function secureExec(options?: SecureExecOptions): BuildExtension {
return new SecureExecExtension(options ?? {});
}

class SecureExecExtension implements BuildExtension {
public readonly name = "SecureExecExtension";

private userPackages: string[];

constructor(options: SecureExecOptions) {
this.userPackages = options.packages ?? [];
}

externalsForTarget(_target: BuildTarget) {
return [
// esbuild must not be bundled — it locates its native binary via a
// relative path from its JS API entry point. secure-exec uses esbuild
// at runtime to bundle polyfills for sandbox code.
"esbuild",
// User-specified packages are require()'d inside the V8 sandbox at
// runtime — the bundler never sees them statically.
...this.userPackages,
];
}

onBuildStart(context: BuildContext) {
context.logger.debug(`Adding ${this.name} esbuild plugins`);

// Plugin 1: Replace node-stdlib-browser with pre-resolved paths.
//
// Trigger's ESM shim anchors require.resolve() to the chunk path, so
// node-stdlib-browser's runtime require.resolve("./mock/empty.js") breaks.
// Fix: load the real node-stdlib-browser at build time (where require.resolve
// works), capture the resolved path map, and inline it as a static export.
const workingDir = context.workingDir;
context.registerPlugin({
name: "secure-exec-stdlib-resolver",
setup(build) {
build.onResolve({ filter: /^node-stdlib-browser$/ }, () => ({
path: "node-stdlib-browser",
namespace: "secure-exec-nsb-resolved",
}));
build.onLoad({ filter: /.*/, namespace: "secure-exec-nsb-resolved" }, () => {
const buildRequire = createRequire(join(workingDir, "package.json"));
const resolved = buildRequire("node-stdlib-browser");
return {
contents: `export default ${JSON.stringify(resolved)};`,
loader: "js",
};
});
},
});

// Plugin 2: Inline bridge.js at build time.
//
// bridge-loader.js in @secure-exec/node(js) uses __dirname and
// require.resolve("@secure-exec/core") at module scope to locate
// dist/bridge.js on disk. This fails in Trigger's bundled output.
// Fix: read bridge.js content at build time and inline it as a
// string literal so no runtime filesystem resolution is needed.
//
context.registerPlugin({
name: "secure-exec-bridge-inline",
setup(build) {
build.onLoad(
{ filter: /[\\/]@secure-exec[\\/]node[\\/]dist[\\/]bridge-loader\.js$/ },
(args) => {
try {
const buildRequire = createRequire(args.path);
const coreEntry = buildRequire.resolve("@secure-exec/core");
const coreRoot = resolve(dirname(coreEntry), "..");
const bridgeCode = readFileSync(join(coreRoot, "dist", "bridge.js"), "utf8");

return {
contents: [
`import { getIsolateRuntimeSource } from "@secure-exec/core";`,
`const bridgeCodeCache = ${JSON.stringify(bridgeCode)};`,
`export function getRawBridgeCode() { return bridgeCodeCache; }`,
`export function getBridgeAttachCode() { return getIsolateRuntimeSource("bridgeAttach"); }`,
].join("\n"),
loader: "js",
};
} catch {
// If we can't inline the bridge, let the normal loader handle it.
return undefined;
}
}
);
},
});
}

async onBuildComplete(context: BuildContext, _manifest: BuildManifest) {
if (context.target === "dev") {
return;
}

context.logger.debug(`Adding ${this.name} deploy dependencies`);

const dependencies: Record<string, string> = {};

// Resolve versions for user-specified sandbox packages
for (const pkg of this.userPackages) {
try {
const modulePath = await context.resolvePath(pkg);
if (!modulePath) {
dependencies[pkg] = "latest";
continue;
}

const packageJSON = await readPackageJSON(dirname(modulePath));
dependencies[pkg] = packageJSON.version ?? "latest";
} catch {
context.logger.warn(
`Could not resolve version for sandbox package ${pkg}, defaulting to latest`
);
dependencies[pkg] = "latest";
}
}

context.addLayer({
id: "secureExec",
dependencies,
image: {
// isolated-vm requires native compilation tools
pkgs: ["python3", "make", "g++"],
},
});
}
}
12 changes: 12 additions & 0 deletions packages/core/src/v3/apiClient/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ export class PermissionDeniedError extends ApiError {
override readonly status: 403 = 403;
}

/**
* True when `error` is a 401/403 from the Trigger API (e.g. expired run-scoped PAT on realtime streams).
* Uses structural checks so it works even if multiple copies of `@trigger.dev/core` are bundled (subclass `instanceof` can fail).
*/
export function isTriggerRealtimeAuthError(error: unknown): boolean {
if (error === null || typeof error !== "object") {
return false;
}
const e = error as ApiError;
return e.name === "TriggerApiError" && (e.status === 401 || e.status === 403);
}

export class NotFoundError extends ApiError {
override readonly status: 404 = 404;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/v3/apiClient/runStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
IOPacket,
parsePacket,
} from "../utils/ioSerialization.js";
import { ApiError } from "./errors.js";
import { ApiError, isTriggerRealtimeAuthError } from "./errors.js";
import { ApiClient } from "./index.js";
import { zodShapeStream } from "./stream.js";

Expand Down Expand Up @@ -344,6 +344,12 @@ export class SSEStreamSubscription implements StreamSubscription {
return;
}

if (isTriggerRealtimeAuthError(error)) {
this.options.onError?.(error as Error);
controller.error(error as Error);
return;
}

// Retry on error
await this.retryConnection(controller, error as Error);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/v3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export {
getSchemaParseFn,
type AnySchemaParseFn,
type SchemaParseFn,
type inferSchemaOut,
isSchemaZodEsque,
isSchemaValibotEsque,
isSchemaArkTypeEsque,
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/v3/inputStreams/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ export class InputStreamsAPI implements InputStreamManager {
return this.#getManager().lastSeqNum(streamId);
}

public setLastSeqNum(streamId: string, seqNum: number): void {
this.#getManager().setLastSeqNum(streamId, seqNum);
}

public shiftBuffer(streamId: string): boolean {
return this.#getManager().shiftBuffer(streamId);
}

public disconnectStream(streamId: string): void {
this.#getManager().disconnectStream(streamId);
}

public clearHandlers(): void {
this.#getManager().clearHandlers();
}
Expand Down
Loading
Loading