refactor: delete packages/ai/ — moved to @trigger.dev/sdk subpaths
All functionality now lives in: - @trigger.dev/sdk/chat (frontend transport) - @trigger.dev/sdk/ai (backend chatTask, pipeChat) Co-authored-by: Eric Allam <eric@trigger.dev>
This commit is contained in:
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"name": "@trigger.dev/ai",
|
||||
"version": "4.3.3",
|
||||
"description": "AI SDK integration for Trigger.dev - Custom ChatTransport for running AI chat as durable tasks",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/triggerdotdev/trigger.dev",
|
||||
"directory": "packages/ai"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"tshy": {
|
||||
"selfLink": false,
|
||||
"main": true,
|
||||
"module": true,
|
||||
"project": "./tsconfig.json",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"sourceDialects": [
|
||||
"@triggerdotdev/source"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .tshy .tshy-build .turbo",
|
||||
"build": "tshy && pnpm run update-version",
|
||||
"dev": "tshy --watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"update-version": "tsx ../../scripts/updateVersion.ts",
|
||||
"check-exports": "attw --pack ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/core": "workspace:4.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ai": "^5.0.0 || ^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.15.4",
|
||||
"ai": "^6.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"tshy": "^3.0.2",
|
||||
"tsx": "4.17.0",
|
||||
"vitest": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.0"
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"@triggerdotdev/source": "./src/index.ts",
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"default": "./dist/commonjs/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./dist/commonjs/index.js",
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"module": "./dist/esm/index.js"
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { task as createTask } from "@trigger.dev/sdk";
|
||||
import type { Task } from "@trigger.dev/core/v3";
|
||||
import type { ChatTaskPayload } from "./types.js";
|
||||
import { pipeChat } from "./pipeChat.js";
|
||||
|
||||
/**
|
||||
* Options for defining a chat task.
|
||||
*
|
||||
* This is a simplified version of the standard task options with the payload
|
||||
* pre-typed as `ChatTaskPayload`.
|
||||
*/
|
||||
export type ChatTaskOptions<TIdentifier extends string> = {
|
||||
/** Unique identifier for the task */
|
||||
id: TIdentifier;
|
||||
|
||||
/** Optional description of the task */
|
||||
description?: string;
|
||||
|
||||
/** Retry configuration */
|
||||
retry?: {
|
||||
maxAttempts?: number;
|
||||
factor?: number;
|
||||
minTimeoutInMs?: number;
|
||||
maxTimeoutInMs?: number;
|
||||
randomize?: boolean;
|
||||
};
|
||||
|
||||
/** Queue configuration */
|
||||
queue?: {
|
||||
name?: string;
|
||||
concurrencyLimit?: number;
|
||||
};
|
||||
|
||||
/** Machine preset for the task */
|
||||
machine?: {
|
||||
preset?: string;
|
||||
};
|
||||
|
||||
/** Maximum duration in seconds */
|
||||
maxDuration?: number;
|
||||
|
||||
/**
|
||||
* The main run function for the chat task.
|
||||
*
|
||||
* Receives a `ChatTaskPayload` with the conversation messages, chat session ID,
|
||||
* and trigger type.
|
||||
*
|
||||
* **Auto-piping:** If this function returns a value that has a `.toUIMessageStream()` method
|
||||
* (like a `StreamTextResult` from `streamText()`), the stream will automatically be piped
|
||||
* to the frontend via the chat realtime stream. If you need to pipe from deeper in your
|
||||
* code, use `pipeChat()` instead and don't return the result.
|
||||
*/
|
||||
run: (payload: ChatTaskPayload) => Promise<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* An object that has a `toUIMessageStream()` method, like the result of `streamText()`.
|
||||
*/
|
||||
type UIMessageStreamable = {
|
||||
toUIMessageStream: (...args: any[]) => AsyncIterable<unknown> | ReadableStream<unknown>;
|
||||
};
|
||||
|
||||
function isUIMessageStreamable(value: unknown): value is UIMessageStreamable {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"toUIMessageStream" in value &&
|
||||
typeof (value as any).toUIMessageStream === "function"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Trigger.dev task pre-configured for AI SDK chat.
|
||||
*
|
||||
* This is a convenience wrapper around `task()` from `@trigger.dev/sdk` that:
|
||||
* - **Pre-types the payload** as `ChatTaskPayload` — no manual typing needed
|
||||
* - **Auto-pipes the stream** if the `run` function returns a `StreamTextResult`
|
||||
*
|
||||
* Requires `@trigger.dev/sdk` to be installed (it's a peer dependency).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { chatTask } from "@trigger.dev/ai";
|
||||
* import { streamText, convertToModelMessages } from "ai";
|
||||
* import { openai } from "@ai-sdk/openai";
|
||||
*
|
||||
* // Simple: return streamText result — auto-piped to the frontend
|
||||
* export const myChatTask = chatTask({
|
||||
* id: "my-chat-task",
|
||||
* run: async ({ messages }) => {
|
||||
* return streamText({
|
||||
* model: openai("gpt-4o"),
|
||||
* messages: convertToModelMessages(messages),
|
||||
* });
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { chatTask, pipeChat } from "@trigger.dev/ai";
|
||||
*
|
||||
* // Complex: use pipeChat() from deep inside your agent code
|
||||
* export const myAgentTask = chatTask({
|
||||
* id: "my-agent-task",
|
||||
* run: async ({ messages }) => {
|
||||
* await runComplexAgentLoop(messages);
|
||||
* // pipeChat() called internally by the agent loop
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function chatTask<TIdentifier extends string>(
|
||||
options: ChatTaskOptions<TIdentifier>
|
||||
): Task<TIdentifier, ChatTaskPayload, unknown> {
|
||||
const { run: userRun, ...restOptions } = options;
|
||||
|
||||
return createTask<TIdentifier, ChatTaskPayload, unknown>({
|
||||
...restOptions,
|
||||
run: async (payload: ChatTaskPayload) => {
|
||||
const result = await userRun(payload);
|
||||
|
||||
// If the run function returned a StreamTextResult or similar,
|
||||
// automatically pipe it to the chat stream
|
||||
if (isUIMessageStreamable(result)) {
|
||||
await pipeChat(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { TriggerChatTransport, createChatTransport } from "./transport.js";
|
||||
export type { TriggerChatTransportOptions, ChatTaskPayload } from "./types.js";
|
||||
export { VERSION } from "./version.js";
|
||||
@@ -1,137 +0,0 @@
|
||||
import { realtimeStreams } from "@trigger.dev/core/v3";
|
||||
|
||||
/**
|
||||
* The default stream key used for chat transport communication.
|
||||
*
|
||||
* Both `TriggerChatTransport` (frontend) and `pipeChat` (backend) use this key
|
||||
* by default to ensure they communicate over the same stream.
|
||||
*/
|
||||
export const CHAT_STREAM_KEY = "chat";
|
||||
|
||||
/**
|
||||
* Options for `pipeChat`.
|
||||
*/
|
||||
export type PipeChatOptions = {
|
||||
/**
|
||||
* Override the stream key to pipe to.
|
||||
* Must match the `streamKey` option on `TriggerChatTransport`.
|
||||
*
|
||||
* @default "chat"
|
||||
*/
|
||||
streamKey?: string;
|
||||
|
||||
/**
|
||||
* An AbortSignal to cancel the stream.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
|
||||
/**
|
||||
* The target run ID to pipe the stream to.
|
||||
* @default "self" (current run)
|
||||
*/
|
||||
target?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An object that has a `toUIMessageStream()` method, like the result of `streamText()` from the AI SDK.
|
||||
*/
|
||||
type UIMessageStreamable = {
|
||||
toUIMessageStream: (...args: any[]) => AsyncIterable<unknown> | ReadableStream<unknown>;
|
||||
};
|
||||
|
||||
function isUIMessageStreamable(value: unknown): value is UIMessageStreamable {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"toUIMessageStream" in value &&
|
||||
typeof (value as any).toUIMessageStream === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
Symbol.asyncIterator in value
|
||||
);
|
||||
}
|
||||
|
||||
function isReadableStream(value: unknown): value is ReadableStream<unknown> {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
typeof (value as any).getReader === "function"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipes a chat stream to the realtime stream, making it available to the
|
||||
* `TriggerChatTransport` on the frontend.
|
||||
*
|
||||
* Accepts any of:
|
||||
* - A `StreamTextResult` from the AI SDK (has `.toUIMessageStream()`)
|
||||
* - An `AsyncIterable` of `UIMessageChunk`s
|
||||
* - A `ReadableStream` of `UIMessageChunk`s
|
||||
*
|
||||
* This must be called from inside a Trigger.dev task's `run` function.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { task } from "@trigger.dev/sdk";
|
||||
* import { pipeChat, type ChatTaskPayload } from "@trigger.dev/ai";
|
||||
* import { streamText, convertToModelMessages } from "ai";
|
||||
*
|
||||
* export const myChatTask = task({
|
||||
* id: "my-chat-task",
|
||||
* run: async (payload: ChatTaskPayload) => {
|
||||
* const result = streamText({
|
||||
* model: openai("gpt-4o"),
|
||||
* messages: convertToModelMessages(payload.messages),
|
||||
* });
|
||||
*
|
||||
* await pipeChat(result);
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Deep inside your agent library — pipeChat works from anywhere inside a task
|
||||
* async function runAgentLoop(messages: CoreMessage[]) {
|
||||
* const result = streamText({ model, messages });
|
||||
* await pipeChat(result);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param source - A StreamTextResult, AsyncIterable, or ReadableStream of UIMessageChunks
|
||||
* @param options - Optional configuration
|
||||
* @returns A promise that resolves when the stream has been fully piped
|
||||
*/
|
||||
export async function pipeChat(
|
||||
source: UIMessageStreamable | AsyncIterable<unknown> | ReadableStream<unknown>,
|
||||
options?: PipeChatOptions
|
||||
): Promise<void> {
|
||||
const streamKey = options?.streamKey ?? CHAT_STREAM_KEY;
|
||||
|
||||
// Resolve the source to an AsyncIterable or ReadableStream
|
||||
let stream: AsyncIterable<unknown> | ReadableStream<unknown>;
|
||||
|
||||
if (isUIMessageStreamable(source)) {
|
||||
stream = source.toUIMessageStream();
|
||||
} else if (isAsyncIterable(source) || isReadableStream(source)) {
|
||||
stream = source;
|
||||
} else {
|
||||
throw new Error(
|
||||
"pipeChat: source must be a StreamTextResult (with .toUIMessageStream()), " +
|
||||
"an AsyncIterable, or a ReadableStream"
|
||||
);
|
||||
}
|
||||
|
||||
// Pipe to the realtime stream
|
||||
const instance = realtimeStreams.pipe(streamKey, stream, {
|
||||
signal: options?.signal,
|
||||
target: options?.target,
|
||||
});
|
||||
|
||||
await instance.wait();
|
||||
}
|
||||
@@ -1,842 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import type { UIMessage, UIMessageChunk } from "ai";
|
||||
import { TriggerChatTransport, createChatTransport } from "./transport.js";
|
||||
|
||||
// Helper: encode text as SSE format
|
||||
function sseEncode(chunks: UIMessageChunk[]): string {
|
||||
return chunks.map((chunk, i) => `id: ${i}\ndata: ${JSON.stringify(chunk)}\n\n`).join("");
|
||||
}
|
||||
|
||||
// Helper: create a ReadableStream from SSE text
|
||||
function createSSEStream(sseText: string): ReadableStream<Uint8Array> {
|
||||
const encoder = new TextEncoder();
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(sseText));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: create test UIMessages
|
||||
function createUserMessage(text: string): UIMessage {
|
||||
return {
|
||||
id: `msg-${Date.now()}`,
|
||||
role: "user",
|
||||
parts: [{ type: "text", text }],
|
||||
};
|
||||
}
|
||||
|
||||
function createAssistantMessage(text: string): UIMessage {
|
||||
return {
|
||||
id: `msg-${Date.now()}`,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text }],
|
||||
};
|
||||
}
|
||||
|
||||
// Sample UIMessageChunks as the AI SDK would produce
|
||||
const sampleChunks: UIMessageChunk[] = [
|
||||
{ type: "text-start", id: "part-1" },
|
||||
{ type: "text-delta", id: "part-1", delta: "Hello" },
|
||||
{ type: "text-delta", id: "part-1", delta: " world" },
|
||||
{ type: "text-delta", id: "part-1", delta: "!" },
|
||||
{ type: "text-end", id: "part-1" },
|
||||
];
|
||||
|
||||
describe("TriggerChatTransport", () => {
|
||||
let originalFetch: typeof global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = global.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create transport with required options", () => {
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-chat-task",
|
||||
accessToken: "test-token",
|
||||
});
|
||||
|
||||
expect(transport).toBeInstanceOf(TriggerChatTransport);
|
||||
});
|
||||
|
||||
it("should accept optional configuration", () => {
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-chat-task",
|
||||
accessToken: "test-token",
|
||||
baseURL: "https://custom.trigger.dev",
|
||||
streamKey: "custom-stream",
|
||||
headers: { "X-Custom": "value" },
|
||||
});
|
||||
|
||||
expect(transport).toBeInstanceOf(TriggerChatTransport);
|
||||
});
|
||||
|
||||
it("should accept a function for accessToken", () => {
|
||||
let tokenCallCount = 0;
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-chat-task",
|
||||
accessToken: () => {
|
||||
tokenCallCount++;
|
||||
return `dynamic-token-${tokenCallCount}`;
|
||||
},
|
||||
});
|
||||
|
||||
expect(transport).toBeInstanceOf(TriggerChatTransport);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessages", () => {
|
||||
it("should trigger the task and return a ReadableStream of UIMessageChunks", async () => {
|
||||
const triggerRunId = "run_abc123";
|
||||
const publicToken = "pub_token_xyz";
|
||||
|
||||
// Mock fetch to handle both the trigger request and the SSE stream request
|
||||
global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
// Handle the task trigger request
|
||||
if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: triggerRunId }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": publicToken,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the SSE stream request
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
const sseText = sseEncode(sampleChunks);
|
||||
return new Response(createSSEStream(sseText), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-chat-task",
|
||||
accessToken: "test-token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
const messages: UIMessage[] = [createUserMessage("Hello!")];
|
||||
|
||||
const stream = await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-1",
|
||||
messageId: undefined,
|
||||
messages,
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
expect(stream).toBeInstanceOf(ReadableStream);
|
||||
|
||||
// Read all chunks from the stream
|
||||
const reader = stream.getReader();
|
||||
const receivedChunks: UIMessageChunk[] = [];
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
receivedChunks.push(value);
|
||||
}
|
||||
|
||||
expect(receivedChunks).toHaveLength(sampleChunks.length);
|
||||
expect(receivedChunks[0]).toEqual({ type: "text-start", id: "part-1" });
|
||||
expect(receivedChunks[1]).toEqual({ type: "text-delta", id: "part-1", delta: "Hello" });
|
||||
expect(receivedChunks[4]).toEqual({ type: "text-end", id: "part-1" });
|
||||
});
|
||||
|
||||
it("should send the correct payload to the trigger API", async () => {
|
||||
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: "run_test" }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": "pub_token",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
return new Response(createSSEStream(""), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-chat-task",
|
||||
accessToken: "test-token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
const messages: UIMessage[] = [createUserMessage("Hello!")];
|
||||
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-123",
|
||||
messageId: undefined,
|
||||
messages,
|
||||
abortSignal: undefined,
|
||||
metadata: { custom: "data" },
|
||||
});
|
||||
|
||||
// Verify the trigger fetch call
|
||||
const triggerCall = fetchSpy.mock.calls.find((call: any[]) =>
|
||||
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger")
|
||||
);
|
||||
|
||||
expect(triggerCall).toBeDefined();
|
||||
const triggerUrl = typeof triggerCall![0] === "string" ? triggerCall![0] : triggerCall![0].toString();
|
||||
expect(triggerUrl).toContain("/api/v1/tasks/my-chat-task/trigger");
|
||||
|
||||
const triggerBody = JSON.parse(triggerCall![1]?.body as string);
|
||||
const payload = JSON.parse(triggerBody.payload);
|
||||
expect(payload.messages).toEqual(messages);
|
||||
expect(payload.chatId).toBe("chat-123");
|
||||
expect(payload.trigger).toBe("submit-message");
|
||||
expect(payload.metadata).toEqual({ custom: "data" });
|
||||
});
|
||||
|
||||
it("should use the correct stream URL with custom streamKey", async () => {
|
||||
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: "run_custom" }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": "token",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
return new Response(createSSEStream(""), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
streamKey: "my-custom-stream",
|
||||
});
|
||||
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-1",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("test")],
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
// Verify the stream URL uses the custom stream key
|
||||
const streamCall = fetchSpy.mock.calls.find((call: any[]) =>
|
||||
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/")
|
||||
);
|
||||
|
||||
expect(streamCall).toBeDefined();
|
||||
const streamUrl = typeof streamCall![0] === "string" ? streamCall![0] : streamCall![0].toString();
|
||||
expect(streamUrl).toContain("/realtime/v1/streams/run_custom/my-custom-stream");
|
||||
});
|
||||
|
||||
it("should include extra headers in stream requests", async () => {
|
||||
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: "run_hdrs" }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": "token",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
return new Response(createSSEStream(""), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
headers: { "X-Custom-Header": "custom-value" },
|
||||
});
|
||||
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-1",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("test")],
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
// Verify the stream request includes custom headers
|
||||
const streamCall = fetchSpy.mock.calls.find((call: any[]) =>
|
||||
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/realtime/v1/streams/")
|
||||
);
|
||||
|
||||
expect(streamCall).toBeDefined();
|
||||
const requestHeaders = streamCall![1]?.headers as Record<string, string>;
|
||||
expect(requestHeaders["X-Custom-Header"]).toBe("custom-value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("reconnectToStream", () => {
|
||||
it("should return null when no session exists for chatId", async () => {
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
});
|
||||
|
||||
const result = await transport.reconnectToStream({
|
||||
chatId: "nonexistent-chat",
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should reconnect to an existing session", async () => {
|
||||
const triggerRunId = "run_reconnect";
|
||||
const publicToken = "pub_reconnect_token";
|
||||
|
||||
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: triggerRunId }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": publicToken,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
const chunks: UIMessageChunk[] = [
|
||||
{ type: "text-start", id: "part-1" },
|
||||
{ type: "text-delta", id: "part-1", delta: "Reconnected!" },
|
||||
{ type: "text-end", id: "part-1" },
|
||||
];
|
||||
return new Response(createSSEStream(sseEncode(chunks)), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
// First, send messages to establish a session
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-reconnect",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("Hello")],
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
// Now reconnect
|
||||
const stream = await transport.reconnectToStream({
|
||||
chatId: "chat-reconnect",
|
||||
});
|
||||
|
||||
expect(stream).toBeInstanceOf(ReadableStream);
|
||||
|
||||
// Read the stream
|
||||
const reader = stream!.getReader();
|
||||
const receivedChunks: UIMessageChunk[] = [];
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
receivedChunks.push(value);
|
||||
}
|
||||
|
||||
expect(receivedChunks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createChatTransport", () => {
|
||||
it("should create a TriggerChatTransport instance", () => {
|
||||
const transport = createChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
});
|
||||
|
||||
expect(transport).toBeInstanceOf(TriggerChatTransport);
|
||||
});
|
||||
|
||||
it("should pass options through to the transport", () => {
|
||||
const transport = createChatTransport({
|
||||
taskId: "custom-task",
|
||||
accessToken: "custom-token",
|
||||
baseURL: "https://custom.example.com",
|
||||
streamKey: "custom-key",
|
||||
headers: { "X-Test": "value" },
|
||||
});
|
||||
|
||||
expect(transport).toBeInstanceOf(TriggerChatTransport);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should propagate trigger API errors", async () => {
|
||||
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Task not found" }),
|
||||
{
|
||||
status: 404,
|
||||
headers: { "content-type": "application/json" },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "nonexistent-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
await expect(
|
||||
transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-error",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("test")],
|
||||
abortSignal: undefined,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("abort signal", () => {
|
||||
it("should close the stream gracefully when aborted", async () => {
|
||||
let streamResolve: (() => void) | undefined;
|
||||
const streamWait = new Promise<void>((resolve) => {
|
||||
streamResolve = resolve;
|
||||
});
|
||||
|
||||
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: "run_abort" }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": "token",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
// Create a slow stream that waits before sending data
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
controller.enqueue(
|
||||
encoder.encode(`id: 0\ndata: ${JSON.stringify({ type: "text-start", id: "p1" })}\n\n`)
|
||||
);
|
||||
// Wait for the test to signal it's done
|
||||
await streamWait;
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
const stream = await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-abort",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("test")],
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
// Read the first chunk
|
||||
const reader = stream.getReader();
|
||||
const first = await reader.read();
|
||||
expect(first.done).toBe(false);
|
||||
|
||||
// Abort and clean up
|
||||
abortController.abort();
|
||||
streamResolve?.();
|
||||
|
||||
// The stream should close — reading should return done
|
||||
const next = await reader.read();
|
||||
expect(next.done).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple sessions", () => {
|
||||
it("should track multiple chat sessions independently", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
callCount++;
|
||||
return new Response(
|
||||
JSON.stringify({ id: `run_multi_${callCount}` }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": `token_${callCount}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
return new Response(createSSEStream(""), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
// Start two independent chat sessions
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "session-a",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("Hello A")],
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "session-b",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("Hello B")],
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
// Both sessions should be independently reconnectable
|
||||
const streamA = await transport.reconnectToStream({ chatId: "session-a" });
|
||||
const streamB = await transport.reconnectToStream({ chatId: "session-b" });
|
||||
const streamC = await transport.reconnectToStream({ chatId: "nonexistent" });
|
||||
|
||||
expect(streamA).toBeInstanceOf(ReadableStream);
|
||||
expect(streamB).toBeInstanceOf(ReadableStream);
|
||||
expect(streamC).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dynamic accessToken", () => {
|
||||
it("should call the accessToken function for each sendMessages call", async () => {
|
||||
let tokenCallCount = 0;
|
||||
|
||||
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: `run_dyn_${tokenCallCount}` }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": "stream-token",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
const chunks: UIMessageChunk[] = [
|
||||
{ type: "text-start", id: "p1" },
|
||||
{ type: "text-end", id: "p1" },
|
||||
];
|
||||
return new Response(createSSEStream(sseEncode(chunks)), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: () => {
|
||||
tokenCallCount++;
|
||||
return `dynamic-token-${tokenCallCount}`;
|
||||
},
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
// First call — the token function should be invoked
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-dyn-1",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("first")],
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
const firstCount = tokenCallCount;
|
||||
expect(firstCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Second call — the token function should be invoked again
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-dyn-2",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("second")],
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
// Token function was called at least once more
|
||||
expect(tokenCallCount).toBeGreaterThan(firstCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe("body merging", () => {
|
||||
it("should merge ChatRequestOptions.body into the task payload", async () => {
|
||||
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: "run_body" }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": "token",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
return new Response(createSSEStream(""), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
await transport.sendMessages({
|
||||
trigger: "submit-message",
|
||||
chatId: "chat-body",
|
||||
messageId: undefined,
|
||||
messages: [createUserMessage("test")],
|
||||
abortSignal: undefined,
|
||||
body: { systemPrompt: "You are helpful", temperature: 0.7 },
|
||||
});
|
||||
|
||||
const triggerCall = fetchSpy.mock.calls.find((call: any[]) =>
|
||||
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger")
|
||||
);
|
||||
|
||||
const triggerBody = JSON.parse(triggerCall![1]?.body as string);
|
||||
const payload = JSON.parse(triggerBody.payload);
|
||||
|
||||
// body properties should be merged into the payload
|
||||
expect(payload.systemPrompt).toBe("You are helpful");
|
||||
expect(payload.temperature).toBe(0.7);
|
||||
// Standard fields should still be present
|
||||
expect(payload.chatId).toBe("chat-body");
|
||||
expect(payload.trigger).toBe("submit-message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("message types", () => {
|
||||
it("should handle regenerate-message trigger", async () => {
|
||||
const fetchSpy = vi.fn().mockImplementation(async (url: string | URL) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
|
||||
if (urlStr.includes("/trigger")) {
|
||||
return new Response(
|
||||
JSON.stringify({ id: "run_regen" }),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trigger-jwt": "token",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (urlStr.includes("/realtime/v1/streams/")) {
|
||||
return new Response(createSSEStream(""), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
"X-Stream-Version": "v1",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch URL: ${urlStr}`);
|
||||
});
|
||||
|
||||
global.fetch = fetchSpy;
|
||||
|
||||
const transport = new TriggerChatTransport({
|
||||
taskId: "my-task",
|
||||
accessToken: "token",
|
||||
baseURL: "https://api.test.trigger.dev",
|
||||
});
|
||||
|
||||
const messages: UIMessage[] = [
|
||||
createUserMessage("Hello!"),
|
||||
createAssistantMessage("Hi there!"),
|
||||
];
|
||||
|
||||
await transport.sendMessages({
|
||||
trigger: "regenerate-message",
|
||||
chatId: "chat-regen",
|
||||
messageId: "msg-to-regen",
|
||||
messages,
|
||||
abortSignal: undefined,
|
||||
});
|
||||
|
||||
// Verify the payload includes the regenerate trigger type and messageId
|
||||
const triggerCall = fetchSpy.mock.calls.find((call: any[]) =>
|
||||
(typeof call[0] === "string" ? call[0] : call[0].toString()).includes("/trigger")
|
||||
);
|
||||
|
||||
const triggerBody = JSON.parse(triggerCall![1]?.body as string);
|
||||
const payload = JSON.parse(triggerBody.payload);
|
||||
expect(payload.trigger).toBe("regenerate-message");
|
||||
expect(payload.messageId).toBe("msg-to-regen");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,256 +0,0 @@
|
||||
import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai";
|
||||
import {
|
||||
ApiClient,
|
||||
SSEStreamSubscription,
|
||||
type SSEStreamPart,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import type { TriggerChatTransportOptions, ChatSessionState } from "./types.js";
|
||||
|
||||
const DEFAULT_STREAM_KEY = "chat";
|
||||
const DEFAULT_BASE_URL = "https://api.trigger.dev";
|
||||
const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;
|
||||
|
||||
/**
|
||||
* A custom AI SDK `ChatTransport` implementation that bridges the Vercel AI SDK's
|
||||
* `useChat` hook with Trigger.dev's durable task execution and realtime streams.
|
||||
*
|
||||
* When `sendMessages` is called, the transport:
|
||||
* 1. Triggers a Trigger.dev task with the chat messages as payload
|
||||
* 2. Subscribes to the task's realtime stream to receive `UIMessageChunk` data
|
||||
* 3. Returns a `ReadableStream<UIMessageChunk>` that the AI SDK processes natively
|
||||
*
|
||||
* The task receives a `ChatTaskPayload` containing the conversation messages,
|
||||
* chat session ID, trigger type, and any custom metadata. Your task should use
|
||||
* the AI SDK's `streamText` (or similar) to generate a response, then pipe
|
||||
* the resulting `UIMessageStream` to the `"chat"` realtime stream key
|
||||
* (or a custom key matching the `streamKey` option).
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Frontend — use with AI SDK's useChat hook
|
||||
* import { useChat } from "@ai-sdk/react";
|
||||
* import { TriggerChatTransport } from "@trigger.dev/ai";
|
||||
*
|
||||
* function Chat({ accessToken }: { accessToken: string }) {
|
||||
* const { messages, sendMessage, status } = useChat({
|
||||
* transport: new TriggerChatTransport({
|
||||
* accessToken,
|
||||
* taskId: "my-chat-task",
|
||||
* }),
|
||||
* });
|
||||
*
|
||||
* // ... render messages
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Backend — Trigger.dev task that handles chat
|
||||
* import { task, streams } from "@trigger.dev/sdk";
|
||||
* import { streamText, convertToModelMessages } from "ai";
|
||||
* import type { ChatTaskPayload } from "@trigger.dev/ai";
|
||||
*
|
||||
* export const myChatTask = task({
|
||||
* id: "my-chat-task",
|
||||
* run: async (payload: ChatTaskPayload) => {
|
||||
* const result = streamText({
|
||||
* model: openai("gpt-4o"),
|
||||
* messages: convertToModelMessages(payload.messages),
|
||||
* });
|
||||
*
|
||||
* const { waitUntilComplete } = streams.pipe("chat", result.toUIMessageStream());
|
||||
* await waitUntilComplete();
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
private readonly taskId: string;
|
||||
private readonly resolveAccessToken: () => string;
|
||||
private readonly baseURL: string;
|
||||
private readonly streamKey: string;
|
||||
private readonly extraHeaders: Record<string, string>;
|
||||
private readonly streamTimeoutSeconds: number;
|
||||
|
||||
/**
|
||||
* Tracks active chat sessions for reconnection support.
|
||||
* Maps chatId → session state (runId, publicAccessToken).
|
||||
*/
|
||||
private sessions: Map<string, ChatSessionState> = new Map();
|
||||
|
||||
constructor(options: TriggerChatTransportOptions) {
|
||||
this.taskId = options.taskId;
|
||||
this.resolveAccessToken =
|
||||
typeof options.accessToken === "function"
|
||||
? options.accessToken
|
||||
: () => options.accessToken as string;
|
||||
this.baseURL = options.baseURL ?? DEFAULT_BASE_URL;
|
||||
this.streamKey = options.streamKey ?? DEFAULT_STREAM_KEY;
|
||||
this.extraHeaders = options.headers ?? {};
|
||||
this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends messages to a Trigger.dev task and returns a streaming response.
|
||||
*
|
||||
* This method:
|
||||
* 1. Triggers the configured task with the chat messages as payload
|
||||
* 2. Subscribes to the task's realtime stream for UIMessageChunk events
|
||||
* 3. Returns a ReadableStream that the AI SDK's useChat hook processes
|
||||
*/
|
||||
sendMessages = async (
|
||||
options: {
|
||||
trigger: "submit-message" | "regenerate-message";
|
||||
chatId: string;
|
||||
messageId: string | undefined;
|
||||
messages: UIMessage[];
|
||||
abortSignal: AbortSignal | undefined;
|
||||
} & ChatRequestOptions
|
||||
): Promise<ReadableStream<UIMessageChunk>> => {
|
||||
const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options;
|
||||
|
||||
// Build the payload for the task — this becomes the ChatTaskPayload
|
||||
const payload = {
|
||||
messages,
|
||||
chatId,
|
||||
trigger,
|
||||
messageId,
|
||||
metadata,
|
||||
...(body ?? {}),
|
||||
};
|
||||
|
||||
const currentToken = this.resolveAccessToken();
|
||||
|
||||
// Trigger the task — use the already-resolved token directly
|
||||
const apiClient = new ApiClient(this.baseURL, currentToken);
|
||||
const triggerResponse = await apiClient.triggerTask(this.taskId, {
|
||||
payload: JSON.stringify(payload),
|
||||
options: {
|
||||
payloadType: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const runId = triggerResponse.id;
|
||||
const publicAccessToken =
|
||||
"publicAccessToken" in triggerResponse
|
||||
? (triggerResponse as { publicAccessToken?: string }).publicAccessToken
|
||||
: undefined;
|
||||
|
||||
// Store session state for reconnection
|
||||
this.sessions.set(chatId, {
|
||||
runId,
|
||||
publicAccessToken: publicAccessToken ?? currentToken,
|
||||
});
|
||||
|
||||
// Subscribe to the realtime stream for this run
|
||||
return this.subscribeToStream(runId, publicAccessToken ?? currentToken, abortSignal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reconnects to an existing streaming response for the specified chat session.
|
||||
*
|
||||
* Returns a ReadableStream if an active session exists, or null if no session is found.
|
||||
*/
|
||||
reconnectToStream = async (
|
||||
options: {
|
||||
chatId: string;
|
||||
} & ChatRequestOptions
|
||||
): Promise<ReadableStream<UIMessageChunk> | null> => {
|
||||
const session = this.sessions.get(options.chatId);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.subscribeToStream(session.runId, session.publicAccessToken, undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a ReadableStream<UIMessageChunk> by subscribing to the realtime SSE stream
|
||||
* for a given run.
|
||||
*/
|
||||
private subscribeToStream(
|
||||
runId: string,
|
||||
accessToken: string,
|
||||
abortSignal: AbortSignal | undefined
|
||||
): ReadableStream<UIMessageChunk> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
...this.extraHeaders,
|
||||
};
|
||||
|
||||
const subscription = new SSEStreamSubscription(
|
||||
`${this.baseURL}/realtime/v1/streams/${runId}/${this.streamKey}`,
|
||||
{
|
||||
headers,
|
||||
signal: abortSignal,
|
||||
timeoutInSeconds: this.streamTimeoutSeconds,
|
||||
}
|
||||
);
|
||||
|
||||
return new ReadableStream<UIMessageChunk>({
|
||||
start: async (controller) => {
|
||||
try {
|
||||
const sseStream = await subscription.subscribe();
|
||||
const reader = sseStream.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
reader.cancel();
|
||||
reader.releaseLock();
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Each SSE part's chunk is a UIMessageChunk
|
||||
controller.enqueue(value.chunk as UIMessageChunk);
|
||||
}
|
||||
} catch (readError) {
|
||||
reader.releaseLock();
|
||||
throw readError;
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't error the stream for abort errors — just close gracefully
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `TriggerChatTransport` instance.
|
||||
*
|
||||
* This is a convenience factory function equivalent to `new TriggerChatTransport(options)`.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useChat } from "@ai-sdk/react";
|
||||
* import { createChatTransport } from "@trigger.dev/ai";
|
||||
*
|
||||
* const transport = createChatTransport({
|
||||
* taskId: "my-chat-task",
|
||||
* accessToken: publicAccessToken,
|
||||
* });
|
||||
*
|
||||
* function Chat() {
|
||||
* const { messages, sendMessage } = useChat({ transport });
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function createChatTransport(options: TriggerChatTransportOptions): TriggerChatTransport {
|
||||
return new TriggerChatTransport(options);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import type { UIMessage } from "ai";
|
||||
|
||||
/**
|
||||
* Options for creating a TriggerChatTransport.
|
||||
*/
|
||||
export type TriggerChatTransportOptions = {
|
||||
/**
|
||||
* The Trigger.dev task ID to trigger for chat completions.
|
||||
* This task will receive the chat messages as its payload.
|
||||
*/
|
||||
task: string;
|
||||
|
||||
/**
|
||||
* An access token for authenticating with the Trigger.dev API.
|
||||
*
|
||||
* This must be a token with permission to trigger the task. You can use:
|
||||
* - A **trigger public token** created via `auth.createTriggerPublicToken(taskId)` (recommended for frontend use)
|
||||
* - A **secret API key** (for server-side use only — never expose in the browser)
|
||||
*
|
||||
* The token returned from triggering the task (`publicAccessToken`) is automatically
|
||||
* used for subscribing to the realtime stream.
|
||||
*
|
||||
* Can also be a function that returns a token string, useful for dynamic token refresh:
|
||||
* ```ts
|
||||
* accessToken: () => getLatestToken()
|
||||
* ```
|
||||
*/
|
||||
accessToken: string | (() => string);
|
||||
|
||||
/**
|
||||
* Base URL for the Trigger.dev API.
|
||||
*
|
||||
* @default "https://api.trigger.dev"
|
||||
*/
|
||||
baseURL?: string;
|
||||
|
||||
/**
|
||||
* The stream key where the task pipes UIMessageChunk data.
|
||||
* When using `chatTask()` or `pipeChat()`, this is handled automatically.
|
||||
* Only set this if you're using a custom stream key.
|
||||
*
|
||||
* @default "chat"
|
||||
*/
|
||||
streamKey?: string;
|
||||
|
||||
/**
|
||||
* Additional headers to include in API requests to Trigger.dev.
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* The number of seconds to wait for the realtime stream to produce data
|
||||
* before timing out. If no data arrives within this period, the stream
|
||||
* will be closed.
|
||||
*
|
||||
* @default 120
|
||||
*/
|
||||
streamTimeoutSeconds?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The payload shape that the transport sends to the triggered task.
|
||||
*
|
||||
* When using `chatTask()`, the payload is automatically typed — you don't need
|
||||
* to import this type. When using `task()` directly, use this type to annotate
|
||||
* your payload:
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { task } from "@trigger.dev/sdk";
|
||||
* import { pipeChat, type ChatTaskPayload } from "@trigger.dev/ai";
|
||||
*
|
||||
* export const myChatTask = task({
|
||||
* id: "my-chat-task",
|
||||
* run: async (payload: ChatTaskPayload) => {
|
||||
* const result = streamText({
|
||||
* model: openai("gpt-4o"),
|
||||
* messages: convertToModelMessages(payload.messages),
|
||||
* });
|
||||
* await pipeChat(result);
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export type ChatTaskPayload<TMessage extends UIMessage = UIMessage> = {
|
||||
/** The array of UI messages representing the conversation history */
|
||||
messages: TMessage[];
|
||||
|
||||
/** The unique identifier for the chat session */
|
||||
chatId: string;
|
||||
|
||||
/**
|
||||
* The type of message submission:
|
||||
* - `"submit-message"`: A new user message was submitted
|
||||
* - `"regenerate-message"`: The user wants to regenerate the last assistant response
|
||||
*/
|
||||
trigger: "submit-message" | "regenerate-message";
|
||||
|
||||
/**
|
||||
* The ID of the message to regenerate (only present for `"regenerate-message"` trigger).
|
||||
*/
|
||||
messageId?: string;
|
||||
|
||||
/**
|
||||
* Custom metadata attached to the chat request by the frontend.
|
||||
*/
|
||||
metadata?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal state for tracking active chat sessions, used for stream reconnection.
|
||||
* @internal
|
||||
*/
|
||||
export type ChatSessionState = {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export const VERSION = "0.0.0";
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../.configs/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedDeclarations": false,
|
||||
"composite": true,
|
||||
"sourceMap": true,
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["./src/**/*.ts"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.test.ts"],
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
Generated
+6
-182
@@ -1147,7 +1147,7 @@ importers:
|
||||
version: 18.3.1
|
||||
react-email:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0)
|
||||
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0)
|
||||
resend:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
@@ -1435,31 +1435,6 @@ importers:
|
||||
specifier: 8.6.6
|
||||
version: 8.6.6
|
||||
|
||||
packages/ai:
|
||||
dependencies:
|
||||
'@trigger.dev/core':
|
||||
specifier: workspace:4.3.3
|
||||
version: link:../core
|
||||
devDependencies:
|
||||
'@arethetypeswrong/cli':
|
||||
specifier: ^0.15.4
|
||||
version: 0.15.4
|
||||
ai:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.116(zod@3.25.76)
|
||||
rimraf:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
tshy:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
tsx:
|
||||
specifier: 4.17.0
|
||||
version: 4.17.0
|
||||
vitest:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
|
||||
|
||||
packages/build:
|
||||
dependencies:
|
||||
'@prisma/config':
|
||||
@@ -11167,23 +11142,9 @@ packages:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||
|
||||
'@vitest/expect@3.1.4':
|
||||
resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==}
|
||||
|
||||
'@vitest/mocker@2.1.9':
|
||||
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.4.2
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@3.1.4':
|
||||
resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==}
|
||||
peerDependencies:
|
||||
@@ -11207,15 +11168,9 @@ packages:
|
||||
'@vitest/runner@3.1.4':
|
||||
resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==}
|
||||
|
||||
'@vitest/snapshot@2.1.9':
|
||||
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
|
||||
|
||||
'@vitest/snapshot@3.1.4':
|
||||
resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==}
|
||||
|
||||
'@vitest/spy@2.1.9':
|
||||
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
||||
|
||||
'@vitest/spy@3.1.4':
|
||||
resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==}
|
||||
|
||||
@@ -19753,11 +19708,6 @@ packages:
|
||||
engines: {node: '>=v14.16.0'}
|
||||
hasBin: true
|
||||
|
||||
vite-node@2.1.9:
|
||||
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite-node@3.1.4:
|
||||
resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@@ -19834,31 +19784,6 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@2.1.9:
|
||||
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': 20.14.14
|
||||
'@vitest/browser': 2.1.9
|
||||
'@vitest/ui': 2.1.9
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vitest@3.1.4:
|
||||
resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@@ -31573,13 +31498,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
'@vitest/utils': 2.1.9
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/expect@3.1.4':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.1.4
|
||||
@@ -31587,14 +31505,6 @@ snapshots:
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@2.1.9(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
|
||||
|
||||
'@vitest/mocker@3.1.4(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.1.4
|
||||
@@ -31621,22 +31531,12 @@ snapshots:
|
||||
'@vitest/utils': 3.1.4
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
magic-string: 0.30.21
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/snapshot@3.1.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.1.4
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@2.1.9':
|
||||
dependencies:
|
||||
tinyspy: 3.0.2
|
||||
|
||||
'@vitest/spy@3.1.4':
|
||||
dependencies:
|
||||
tinyspy: 3.0.2
|
||||
@@ -39157,7 +39057,7 @@ snapshots:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0):
|
||||
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0):
|
||||
dependencies:
|
||||
'@babel/parser': 7.24.1
|
||||
'@radix-ui/colors': 1.0.1
|
||||
@@ -39194,8 +39094,8 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.2.0(react@18.3.1)
|
||||
shelljs: 0.8.5
|
||||
socket.io: 4.7.3(bufferutil@4.0.9)
|
||||
socket.io-client: 4.7.3(bufferutil@4.0.9)
|
||||
socket.io: 4.7.3
|
||||
socket.io-client: 4.7.3
|
||||
sonner: 1.3.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1)
|
||||
source-map-js: 1.0.2
|
||||
stacktrace-parser: 0.1.10
|
||||
@@ -40422,7 +40322,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
socket.io-client@4.7.3(bufferutil@4.0.9):
|
||||
socket.io-client@4.7.3:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.0
|
||||
debug: 4.3.7(supports-color@10.0.0)
|
||||
@@ -40451,7 +40351,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
socket.io@4.7.3(bufferutil@4.0.9):
|
||||
socket.io@4.7.3:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
@@ -42018,27 +41918,6 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 1.1.2
|
||||
vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-node@3.1.4(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -42118,23 +41997,6 @@ snapshots:
|
||||
tsx: 3.12.2
|
||||
yaml: 2.8.3
|
||||
|
||||
vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.25.1
|
||||
fdir: 6.4.4(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.10
|
||||
rollup: 4.60.1
|
||||
tinyglobby: 0.2.13
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.14
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.29.2
|
||||
terser: 5.44.1
|
||||
tsx: 4.17.0
|
||||
yaml: 2.8.3
|
||||
|
||||
vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.25.1
|
||||
@@ -42152,44 +42014,6 @@ snapshots:
|
||||
tsx: 4.20.6
|
||||
yaml: 2.8.3
|
||||
|
||||
vitest@2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3))
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
'@vitest/runner': 2.1.9
|
||||
'@vitest/snapshot': 2.1.9
|
||||
'@vitest/spy': 2.1.9
|
||||
'@vitest/utils': 2.1.9
|
||||
chai: 5.2.0
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
expect-type: 1.2.1
|
||||
magic-string: 0.30.21
|
||||
pathe: 1.1.2
|
||||
std-env: 3.9.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 6.4.2(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
|
||||
vite-node: 2.1.9(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@4.17.0)(yaml@2.8.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.14
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@3.1.4(@types/debug@4.1.12)(@types/node@20.14.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.44.1)(tsx@3.12.2)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@vitest/expect': 3.1.4
|
||||
|
||||
Reference in New Issue
Block a user