diff --git a/.changeset/ai-sdk-chat-transport.md b/.changeset/ai-sdk-chat-transport.md new file mode 100644 index 0000000000..f5cdb9187d --- /dev/null +++ b/.changeset/ai-sdk-chat-transport.md @@ -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), + }); + }, +}); +``` diff --git a/.scratch/plan-graceful-oversized-batch-items.md b/.scratch/plan-graceful-oversized-batch-items.md new file mode 100644 index 0000000000..cb463b9625 --- /dev/null +++ b/.scratch/plan-graceful-oversized-batch-items.md @@ -0,0 +1,257 @@ +# Graceful handling of oversized batch items + +## Prerequisites + +This plan builds on top of PR #2980 which provides: +- `TriggerFailedTaskService` at `apps/webapp/app/runEngine/services/triggerFailedTask.server.ts` - creates pre-failed TaskRuns with proper trace events, waitpoint connections, and parent run associations +- `engine.createFailedTaskRun()` on RunEngine - creates a SYSTEM_FAILURE run with associated waitpoints +- Retry support in `processItemCallback` with `attempt` and `isFinalAttempt` params +- The callback already uses `TriggerFailedTaskService` for items that fail after retries + +## Problem + +When the NDJSON parser in `createNdjsonParserStream` detects an oversized line, it throws inside the TransformStream's `transform()` method. This aborts the request body stream (due to `pipeThrough` coupling), causing the client's `fetch()` to see `TypeError: fetch failed` instead of the server's 400 response. The SDK treats this as a connection error and retries with exponential backoff (~25s wasted). + +## Goal + +Instead of throwing, treat oversized items as per-item failures that flow through the existing batch failure pipeline. The batch seals normally, other items process fine, and the user sees a clear failure for the specific oversized item(s). + +## Approach + +The NDJSON parser emits an error marker object instead of throwing. `StreamBatchItemsService` detects these markers and enqueues the item to the FairQueue with error metadata in its options. The `processItemCallback` (already enhanced with `TriggerFailedTaskService` in PR #2980) detects the error metadata and creates a pre-failed run via `TriggerFailedTaskService`, which handles all the waitpoint/trace machinery. + +## Changes + +### 1. Byte-level key extractor for oversized lines + +**`apps/webapp/app/runEngine/services/streamBatchItems.server.ts`** - new function + +Add `extractIndexAndTask(bytes: Uint8Array): { index: number; task: string }` - a state machine that extracts top-level `"index"` and `"task"` values from raw bytes without decoding the full line. + +How it works: +- Scan bytes tracking JSON nesting depth (count `{`/`[` vs `}`/`]`) +- At depth 1 (inside the top-level object), look for byte sequences matching `"index"` and `"task"` key patterns +- For `"index"`: after the `:`, parse the digit sequence as a number +- For `"task"`: after the `:`, find opening `"`, read bytes until closing `"`, decode just that slice +- Stop when both found, or after scanning 512 bytes (whichever comes first) +- Fallback: `index = -1`, `task = "unknown"` if not found + +This avoids decoding/allocating the full 3MB line - only the first few hundred bytes are examined. + +### 2. Modify `createNdjsonParserStream` to emit error markers + +**`apps/webapp/app/runEngine/services/streamBatchItems.server.ts`** + +Define a marker type: +```typescript +type OversizedItemMarker = { + __batchItemError: "OVERSIZED"; + index: number; + task: string; + actualSize: number; + maxSize: number; +}; +``` + +**Case 1 - Complete line exceeds limit** (newline found, `newlineIndex > maxItemBytes`): +- Call `extractLine(newlineIndex)` to consume the line from the buffer +- Call `extractIndexAndTask(lineBytes)` on the extracted bytes +- `controller.enqueue(marker)` instead of throwing +- Increment `lineNumber` and continue + +**Case 2 - Incomplete line exceeds limit** (no newline, `totalBytes > maxItemBytes`): +- Call `extractIndexAndTask(concatenateChunks())` on current buffer +- `controller.enqueue(marker)` +- Clear the buffer (`chunks = []; totalBytes = 0`) +- Return from transform (don't throw) + +**Case 3 - Flush with oversized remaining** (`totalBytes > maxItemBytes` in flush): +- Same as case 2 but in `flush()`. + +### 3. Handle markers in `StreamBatchItemsService` + +**`apps/webapp/app/runEngine/services/streamBatchItems.server.ts`** - in the `for await` loop + +Before the existing `BatchItemNDJSONSchema.safeParse(rawItem)`, check for the marker: + +```typescript +if (rawItem && typeof rawItem === "object" && (rawItem as any).__batchItemError === "OVERSIZED") { + const marker = rawItem as OversizedItemMarker; + const itemIndex = marker.index >= 0 ? marker.index : lastIndex + 1; + + const errorMessage = `Batch item payload is too large (${(marker.actualSize / 1024).toFixed(1)} KB). Maximum allowed size is ${(marker.maxSize / 1024).toFixed(1)} KB. Reduce the payload size or offload large data to external storage.`; + + // Enqueue the item normally but with error metadata in options. + // The processItemCallback will detect __error and use TriggerFailedTaskService + // to create a pre-failed run with proper waitpoint connections. + const batchItem: BatchItem = { + task: marker.task, + payload: "{}", + payloadType: "application/json", + options: { + __error: errorMessage, + __errorCode: "PAYLOAD_TOO_LARGE", + }, + }; + + const result = await this._engine.enqueueBatchItem( + batchId, environment.id, itemIndex, batchItem + ); + + if (result.enqueued) { + itemsAccepted++; + } else { + itemsDeduplicated++; + } + lastIndex = itemIndex; + continue; +} +``` + +### 4. Handle `__error` items in `processItemCallback` + +**`apps/webapp/app/v3/runEngineHandlers.server.ts`** - in the `setupBatchQueueCallbacks` function + +In the `processItemCallback`, before the `TriggerTaskService.call()`, check for `__error` in `item.options`: + +```typescript +const itemError = item.options?.__error as string | undefined; +if (itemError) { + const errorCode = (item.options?.__errorCode as string) ?? "ITEM_ERROR"; + + // Use TriggerFailedTaskService to create a pre-failed run. + // This creates a proper TaskRun with waitpoint connections so the + // parent's batchTriggerAndWait resolves correctly for this item. + const failedRunId = await triggerFailedTaskService.call({ + taskId: item.task, + environment, + payload: item.payload ?? "{}", + payloadType: item.payloadType, + errorMessage: itemError, + errorCode: errorCode as TaskRunErrorCodes, + parentRunId: meta.parentRunId, + resumeParentOnCompletion: meta.resumeParentOnCompletion, + batch: { id: batchId, index: itemIndex }, + traceContext: meta.traceContext as Record | undefined, + spanParentAsLink: meta.spanParentAsLink, + }); + + if (failedRunId) { + span.setAttribute("batch.result.pre_failed", true); + span.setAttribute("batch.result.run_id", failedRunId); + span.end(); + return { success: true as const, runId: failedRunId }; + } + + // Fallback if TriggerFailedTaskService fails + span.end(); + return { success: false as const, error: itemError, errorCode }; +} +``` + +Note: this returns `{ success: true, runId }` because the pre-failed run IS a real run. The BatchQueue records it as a success (run was created). The run itself is already in SYSTEM_FAILURE status, so the batch completion flow handles it correctly. + +If `environment` is null (environment not found), fall through to the existing environment-not-found handling which already uses `triggerFailedTaskService.callWithoutTraceEvents()` on `isFinalAttempt`. + +### 5. Handle undefined/null payload in BatchQueue serialization + +**`internal-packages/run-engine/src/batch-queue/index.ts`** - in `#handleMessage` + +Both payload serialization blocks (in the `success: false` branch and the `catch` block) do: +```typescript +const str = typeof item.payload === "string" ? item.payload : JSON.stringify(item.payload); +innerSpan?.setAttribute("batch.payloadSize", str.length); +``` + +`JSON.stringify(undefined)` returns `undefined`, causing `str.length` to crash. Fix both: +```typescript +const str = + item.payload === undefined || item.payload === null + ? "{}" + : typeof item.payload === "string" + ? item.payload + : JSON.stringify(item.payload); +``` + +### 6. Remove stale error handling in route + +**`apps/webapp/app/routes/api.v3.batches.$batchId.items.ts`** + +The `error.message.includes("exceeds maximum size")` branch is no longer reachable since oversized items don't throw. Remove that condition, keep the `"Invalid JSON"` check. + +### 7. Remove `BatchItemTooLargeError` and SDK pre-validation + +**`packages/core/src/v3/apiClient/errors.ts`** - remove `BatchItemTooLargeError` class + +**`packages/core/src/v3/apiClient/index.ts`**: +- Remove `BatchItemTooLargeError` import +- Remove `instanceof BatchItemTooLargeError` check in the retry catch block +- Remove `MAX_BATCH_ITEM_BYTES` constant +- Remove size validation from `createNdjsonStream` (revert `encodeAndValidate` to simple encode) + +**`packages/trigger-sdk/src/v3/shared.ts`** - remove `BatchItemTooLargeError` import and handling in `buildBatchErrorMessage` + +**`packages/trigger-sdk/src/v3/index.ts`** - remove `BatchItemTooLargeError` re-export + +### 8. Update tests + +**`apps/webapp/test/engine/streamBatchItems.test.ts`**: +- Update "should reject lines exceeding maxItemBytes" to assert `OversizedItemMarker` emission instead of throw +- Update "should reject unbounded accumulation without newlines" similarly +- Update the emoji byte-size test to assert marker instead of throw + +### 9. Update reference project test task + +**`references/hello-world/src/trigger/batches.ts`**: +- Remove `BatchItemTooLargeError` import +- Update `batchSealFailureOversizedPayload` task to test the new behavior: + - Send 2 items: one normal, one oversized (~3.2MB) + - Assert `batchTriggerAndWait` returns (doesn't throw) + - Assert `results.runs[0].ok === true` (normal item succeeded) + - Assert `results.runs[1].ok === false` (oversized item failed) + - Assert error message contains "too large" + +## Data flow + +``` +NDJSON bytes arrive + | +createNdjsonParserStream + |-- Line <= limit --> parse JSON --> enqueue object + `-- Line > limit --> extractIndexAndTask(bytes) --> enqueue OversizedItemMarker + | +StreamBatchItemsService for-await loop + |-- OversizedItemMarker --> engine.enqueueBatchItem() with __error in options + `-- Normal item --> validate --> engine.enqueueBatchItem() + | +FairQueue consumer (#handleMessage) + |-- __error in options --> processItemCallback detects it + | --> TriggerFailedTaskService.call() + | --> Creates pre-failed TaskRun with SYSTEM_FAILURE status + | --> Proper waitpoint + TaskRunWaitpoint connections created + | --> Returns { success: true, runId: failedRunFriendlyId } + `-- Normal item --> TriggerTaskService.call() --> creates normal run + | +Batch sealing: enqueuedCount === runCount (all items go through enqueueBatchItem) +Batch completion: all items have runs (real or pre-failed), waitpoints resolve normally +Parent run: batchTriggerAndWait resolves with per-item results +``` + +## Why this works + +The key insight is that `TriggerFailedTaskService` (from PR #2980) creates a real `TaskRun` in `SYSTEM_FAILURE` status. This means: +1. A RUN waitpoint is created and connected to the parent via `TaskRunWaitpoint` with correct `batchId`/`batchIndex` +2. The run is immediately completed, which completes the waitpoint +3. The SDK's `waitForBatch` resolver for that index fires with the error result +4. The batch completion flow counts this as a processed item (it's a real run) +5. No special-casing needed in the batch completion callback + +## Verification + +1. Rebuild `@trigger.dev/core`, `@trigger.dev/sdk`, `@internal/run-engine` +2. Restart webapp + trigger dev +3. Trigger `batch-seal-failure-oversized` task - should complete in ~2-3s with: + - Normal item: `ok: true` + - Oversized item: `ok: false` with "too large" error +4. Run NDJSON parser tests: updated tests assert marker emission instead of throws +5. Run `pnpm run build --filter @internal/run-engine --filter @trigger.dev/core --filter @trigger.dev/sdk` diff --git a/docs/docs.json b/docs/docs.json index 14d728e2db..911c912711 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -74,6 +74,7 @@ "tags", "runs/metadata", "tasks/streams", + "guides/ai-chat", "run-usage", "context", "runs/priority", diff --git a/docs/guides/ai-chat.mdx b/docs/guides/ai-chat.mdx new file mode 100644 index 0000000000..e549226b14 --- /dev/null +++ b/docs/guides/ai-chat.mdx @@ -0,0 +1,268 @@ +--- +title: "AI Chat with useChat" +sidebarTitle: "AI Chat (useChat)" +description: "Run AI SDK chat completions as durable Trigger.dev tasks with built-in realtime streaming." +--- + +## Overview + +The `@trigger.dev/sdk` provides a custom [ChatTransport](https://sdk.vercel.ai/docs/ai-sdk-ui/transport) for the Vercel AI SDK's `useChat` hook. This lets you run chat completions as **durable Trigger.dev tasks** instead of fragile API routes — with automatic retries, observability, and realtime streaming built in. + +**How it works:** +1. The frontend sends messages via `useChat` → `TriggerChatTransport` +2. The transport triggers a Trigger.dev task with the conversation as payload +3. The task streams `UIMessageChunk` events back via Trigger.dev's realtime streams +4. The AI SDK's `useChat` processes the stream natively — text, tool calls, reasoning, etc. + +No custom API routes needed. Your chat backend is a Trigger.dev task. + + + Requires `@trigger.dev/sdk` version **4.4.0 or later** and the `ai` package **v5.0.0 or later**. + + +## Quick start + +### 1. Define a chat task + +Use `chatTask` from `@trigger.dev/sdk/ai` to define a task that handles chat messages. The payload is automatically typed as `ChatTaskPayload`. + +If you return a `StreamTextResult` from `run`, it's **automatically piped** to the frontend. + +```ts trigger/chat.ts +import { chatTask } from "@trigger.dev/sdk/ai"; +import { streamText, convertToModelMessages } from "ai"; +import { openai } from "@ai-sdk/openai"; + +export const myChat = chatTask({ + id: "my-chat", + run: async ({ messages }) => { + // messages is UIMessage[] from the frontend + return streamText({ + model: openai("gpt-4o"), + messages: convertToModelMessages(messages), + }); + // Returning a StreamTextResult auto-pipes it to the frontend + }, +}); +``` + +### 2. Generate an access token + +On your server (e.g. a Next.js API route or server action), create a trigger public token: + +```ts app/actions.ts +"use server"; + +import { auth } from "@trigger.dev/sdk"; + +export async function getChatToken() { + return await auth.createTriggerPublicToken("my-chat"); +} +``` + +### 3. Use in the frontend + +Import `TriggerChatTransport` from `@trigger.dev/sdk/chat` (browser-safe — no server dependencies). + +```tsx app/components/chat.tsx +"use client"; + +import { useChat } from "@ai-sdk/react"; +import { TriggerChatTransport } from "@trigger.dev/sdk/chat"; + +export function Chat({ accessToken }: { accessToken: string }) { + const { messages, sendMessage, status, error } = useChat({ + transport: new TriggerChatTransport({ + task: "my-chat", + accessToken, + }), + }); + + return ( +
+ {messages.map((m) => ( +
+ {m.role}: + {m.parts.map((part, i) => + part.type === "text" ? {part.text} : null + )} +
+ ))} + +
{ + e.preventDefault(); + const input = e.currentTarget.querySelector("input"); + if (input?.value) { + sendMessage({ text: input.value }); + input.value = ""; + } + }} + > + + +
+
+ ); +} +``` + +## Backend patterns + +### Simple: return a StreamTextResult + +The easiest approach — return the `streamText` result from `run` and it's automatically piped to the frontend: + +```ts +import { chatTask } from "@trigger.dev/sdk/ai"; +import { streamText, convertToModelMessages } from "ai"; +import { openai } from "@ai-sdk/openai"; + +export const simpleChat = chatTask({ + id: "simple-chat", + run: async ({ messages }) => { + return streamText({ + model: openai("gpt-4o"), + system: "You are a helpful assistant.", + messages: convertToModelMessages(messages), + }); + }, +}); +``` + +### Complex: use pipeChat() from anywhere + +For complex agent flows where `streamText` is called deep inside your code, use `pipeChat()`. It works from **anywhere inside a task** — even nested function calls. + +```ts trigger/agent-chat.ts +import { chatTask, pipeChat } from "@trigger.dev/sdk/ai"; +import { streamText, convertToModelMessages } from "ai"; +import { openai } from "@ai-sdk/openai"; + +export const agentChat = chatTask({ + id: "agent-chat", + run: async ({ messages }) => { + // Don't return anything — pipeChat is called inside + await runAgentLoop(convertToModelMessages(messages)); + }, +}); + +// This could be deep inside your agent library +async function runAgentLoop(messages: CoreMessage[]) { + // ... agent logic, tool calls, etc. + + const result = streamText({ + model: openai("gpt-4o"), + messages, + }); + + // Pipe from anywhere — no need to return it + await pipeChat(result); +} +``` + +### Manual: use task() with pipeChat() + +If you need full control over task options, use the standard `task()` with `ChatTaskPayload` and `pipeChat()`: + +```ts +import { task } from "@trigger.dev/sdk"; +import { pipeChat, type ChatTaskPayload } from "@trigger.dev/sdk/ai"; +import { streamText, convertToModelMessages } from "ai"; +import { openai } from "@ai-sdk/openai"; + +export const manualChat = task({ + id: "manual-chat", + retry: { maxAttempts: 3 }, + queue: { concurrencyLimit: 10 }, + run: async (payload: ChatTaskPayload) => { + const result = streamText({ + model: openai("gpt-4o"), + messages: convertToModelMessages(payload.messages), + }); + + await pipeChat(result); + }, +}); +``` + +## Frontend options + +### TriggerChatTransport options + +```ts +new TriggerChatTransport({ + // Required + task: "my-chat", // Task ID to trigger + accessToken: token, // Trigger public token or secret key + + // Optional + baseURL: "https://...", // Custom API URL (self-hosted) + streamKey: "chat", // Custom stream key (default: "chat") + headers: { ... }, // Extra headers for API requests + streamTimeoutSeconds: 120, // Stream timeout (default: 120s) +}); +``` + +### Dynamic access tokens + +For token refresh patterns, pass a function: + +```ts +new TriggerChatTransport({ + task: "my-chat", + accessToken: () => getLatestToken(), // Called on each sendMessage +}); +``` + +### Passing extra data + +Use the `body` option on `sendMessage` to pass additional data to the task: + +```ts +sendMessage({ + text: "Hello", +}, { + body: { + systemPrompt: "You are a pirate.", + temperature: 0.9, + }, +}); +``` + +The `body` fields are merged into the `ChatTaskPayload` and available in your task's `run` function. + +## ChatTaskPayload + +The payload sent to the task has this shape: + +| Field | Type | Description | +|-------|------|-------------| +| `messages` | `UIMessage[]` | The conversation history | +| `chatId` | `string` | Unique chat session ID | +| `trigger` | `"submit-message" \| "regenerate-message"` | What triggered the request | +| `messageId` | `string \| undefined` | Message ID to regenerate (if applicable) | +| `metadata` | `unknown` | Custom metadata from the frontend | + +Plus any extra fields from the `body` option. + +## Self-hosting + +If you're self-hosting Trigger.dev, pass the `baseURL` option: + +```ts +new TriggerChatTransport({ + task: "my-chat", + accessToken, + baseURL: "https://your-trigger-instance.com", +}); +``` + +## Related + +- [Realtime Streams](/tasks/streams) — How streams work under the hood +- [Using the Vercel AI SDK](/guides/examples/vercel-ai-sdk) — Basic AI SDK usage with Trigger.dev +- [Realtime React Hooks](/realtime/react-hooks/overview) — Lower-level realtime hooks +- [Authentication](/realtime/auth) — Public access tokens and trigger tokens diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index f5f840bc1e..2db05da7fc 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -24,7 +24,9 @@ "./package.json": "./package.json", ".": "./src/v3/index.ts", "./v3": "./src/v3/index.ts", - "./ai": "./src/v3/ai.ts" + "./ai": "./src/v3/ai.ts", + "./chat": "./src/v3/chat.ts", + "./chat/react": "./src/v3/chat-react.ts" }, "sourceDialects": [ "@triggerdotdev/source" @@ -37,6 +39,12 @@ ], "ai": [ "dist/commonjs/v3/ai.d.ts" + ], + "chat": [ + "dist/commonjs/v3/chat.d.ts" + ], + "chat/react": [ + "dist/commonjs/v3/chat-react.d.ts" ] } }, @@ -66,6 +74,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.4", "@types/debug": "^4.1.7", + "@types/react": "^19.2.14", "@types/slug": "^5.0.3", "@types/uuid": "^9.0.0", "@types/ws": "^8.5.3", @@ -78,12 +87,16 @@ "zod": "3.25.76" }, "peerDependencies": { - "zod": "^3.0.0 || ^4.0.0", - "ai": "^4.2.0 || ^5.0.0 || ^6.0.0" + "ai": "^5.0.0 || ^6.0.0", + "react": "^18.0 || ^19.0", + "zod": "^3.0.0 || ^4.0.0" }, "peerDependenciesMeta": { "ai": { "optional": true + }, + "react": { + "optional": true } }, "engines": { @@ -123,6 +136,28 @@ "types": "./dist/commonjs/v3/ai.d.ts", "default": "./dist/commonjs/v3/ai.js" } + }, + "./chat": { + "import": { + "@triggerdotdev/source": "./src/v3/chat.ts", + "types": "./dist/esm/v3/chat.d.ts", + "default": "./dist/esm/v3/chat.js" + }, + "require": { + "types": "./dist/commonjs/v3/chat.d.ts", + "default": "./dist/commonjs/v3/chat.js" + } + }, + "./chat/react": { + "import": { + "@triggerdotdev/source": "./src/v3/chat-react.ts", + "types": "./dist/esm/v3/chat-react.d.ts", + "default": "./dist/esm/v3/chat-react.js" + }, + "require": { + "types": "./dist/commonjs/v3/chat-react.d.ts", + "default": "./dist/commonjs/v3/chat-react.js" + } } }, "main": "./dist/commonjs/v3/index.js", diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index 59afa2fe21..8bec798e98 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -3,11 +3,19 @@ import { isSchemaZodEsque, Task, type inferSchemaIn, + type PipeStreamOptions, + type TaskIdentifier, + type TaskOptions, type TaskSchema, type TaskWithSchema, } from "@trigger.dev/core/v3"; +import type { UIMessage } from "ai"; import { dynamicTool, jsonSchema, JSONSchema7, Schema, Tool, ToolCallOptions, zodSchema } from "ai"; +import { auth } from "./auth.js"; import { metadata } from "./metadata.js"; +import { streams } from "./streams.js"; +import { createTask } from "./shared.js"; +import { wait } from "./wait.js"; const METADATA_KEY = "tool.execute.options"; @@ -116,3 +124,324 @@ export const ai = { tool: toolFromTask, currentToolOptions: getToolOptionsFromMetadata, }; + +/** + * Creates a public access token for a chat task. + * + * This is a convenience helper that creates a multi-use trigger public token + * scoped to the given task. Use it in a server action to provide the frontend + * `TriggerChatTransport` with an `accessToken`. + * + * @example + * ```ts + * // actions.ts + * "use server"; + * import { createChatAccessToken } from "@trigger.dev/sdk/ai"; + * import type { chat } from "@/trigger/chat"; + * + * export const getChatToken = () => createChatAccessToken("ai-chat"); + * ``` + */ +export async function createChatAccessToken( + taskId: TaskIdentifier +): Promise { + return auth.createTriggerPublicToken(taskId as string, { multipleUse: true }); +} + +// --------------------------------------------------------------------------- +// Chat transport helpers — backend side +// --------------------------------------------------------------------------- + +/** + * The default stream key used for chat transport communication. + * Both `TriggerChatTransport` (frontend) and `pipeChat`/`chatTask` (backend) + * use this key by default. + */ +export const CHAT_STREAM_KEY = "chat"; + +/** + * The payload shape that the chat transport sends to the triggered task. + * + * When using `chatTask()`, the payload is automatically typed — you don't need + * to import this type. Use this type only if you're using `task()` directly + * with `pipeChat()`. + */ +export type ChatTaskPayload = { + /** The conversation messages */ + messages: TMessage[]; + + /** The unique identifier for the chat session */ + chatId: string; + + /** + * The trigger type: + * - `"submit-message"`: A new user message + * - `"regenerate-message"`: Regenerate the last assistant response + */ + trigger: "submit-message" | "regenerate-message"; + + /** The ID of the message to regenerate (only for `"regenerate-message"`) */ + messageId?: string; + + /** Custom metadata from the frontend */ + metadata?: unknown; +}; + +/** + * Tracks how many times `pipeChat` has been called in the current `chatTask` run. + * Used to prevent double-piping when a user both calls `pipeChat()` manually + * and returns a streamable from their `run` function. + * @internal + */ +let _chatPipeCount = 0; + +/** + * Options for `pipeChat`. + */ +export type PipeChatOptions = { + /** + * Override the stream key. Must match the `streamKey` on `TriggerChatTransport`. + * @default "chat" + */ + streamKey?: string; + + /** An AbortSignal to cancel the stream. */ + signal?: AbortSignal; + + /** + * The target run ID to pipe to. + * @default "self" (current run) + */ + target?: string; +}; + +/** + * An object with a `toUIMessageStream()` method (e.g. `StreamTextResult` from `streamText()`). + */ +type UIMessageStreamable = { + toUIMessageStream: (...args: any[]) => AsyncIterable | ReadableStream; +}; + +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 { + return typeof value === "object" && value !== null && Symbol.asyncIterator in value; +} + +function isReadableStream(value: unknown): value is ReadableStream { + 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: + * - A `StreamTextResult` from `streamText()` (has `.toUIMessageStream()`) + * - An `AsyncIterable` of `UIMessageChunk`s + * - A `ReadableStream` of `UIMessageChunk`s + * + * 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/sdk/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 + * // Works from anywhere inside a task — even deep in your agent code + * async function runAgentLoop(messages: CoreMessage[]) { + * const result = streamText({ model, messages }); + * await pipeChat(result); + * } + * ``` + */ +export async function pipeChat( + source: UIMessageStreamable | AsyncIterable | ReadableStream, + options?: PipeChatOptions +): Promise { + _chatPipeCount++; + const streamKey = options?.streamKey ?? CHAT_STREAM_KEY; + + let stream: AsyncIterable | ReadableStream; + + 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" + ); + } + + const pipeOptions: PipeStreamOptions = {}; + if (options?.signal) { + pipeOptions.signal = options.signal; + } + if (options?.target) { + pipeOptions.target = options.target; + } + + const { waitUntilComplete } = streams.pipe(streamKey, stream, pipeOptions); + await waitUntilComplete(); +} + +/** + * Options for defining a chat task. + * + * Extends the standard `TaskOptions` but pre-types the payload as `ChatTaskPayload` + * and overrides `run` to accept `ChatTaskPayload` directly. + * + * **Auto-piping:** If the `run` function returns a value with `.toUIMessageStream()` + * (like a `StreamTextResult`), the stream is automatically piped to the frontend. + * For complex flows, use `pipeChat()` manually from anywhere in your code. + * + * **Single-run mode:** By default, the task runs a waitpoint loop so that the + * entire conversation lives inside one run. After each AI response, the task + * emits a control chunk and pauses via `wait.forToken`. The frontend transport + * resumes the same run by completing the token with the next set of messages. + */ +export type ChatTaskOptions = Omit< + TaskOptions, + "run" +> & { + /** + * The 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 with `.toUIMessageStream()`, + * the stream is automatically piped to the frontend. + */ + run: (payload: ChatTaskPayload) => Promise; + + /** + * Maximum number of conversational turns (message round-trips) a single run + * will handle before ending. After this many turns the run completes + * normally and the next message will start a fresh run. + * + * @default 100 + */ + maxTurns?: number; + + /** + * How long to wait for the next message before timing out and ending the run. + * Accepts any duration string recognised by `wait.createToken` (e.g. `"1h"`, `"30m"`). + * + * @default "1h" + */ + turnTimeout?: string; +}; + +/** + * Creates a Trigger.dev task pre-configured for AI SDK chat. + * + * - **Pre-types the payload** as `ChatTaskPayload` — no manual typing needed + * - **Auto-pipes the stream** if `run` returns a `StreamTextResult` + * - For complex flows, use `pipeChat()` from anywhere inside your task code + * + * @example + * ```ts + * import { chatTask } from "@trigger.dev/sdk/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/sdk/ai"; + * + * // Complex: pipeChat() from deep in your agent code + * export const myAgentTask = chatTask({ + * id: "my-agent-task", + * run: async ({ messages }) => { + * await runComplexAgentLoop(messages); + * }, + * }); + * ``` + */ +export function chatTask( + options: ChatTaskOptions +): Task { + const { run: userRun, maxTurns = 100, turnTimeout = "1h", ...restOptions } = options; + + return createTask({ + ...restOptions, + run: async (payload: ChatTaskPayload) => { + let currentPayload = payload; + + for (let turn = 0; turn < maxTurns; turn++) { + _chatPipeCount = 0; + + const result = await userRun(currentPayload); + + // Auto-pipe if the run function returned a StreamTextResult or similar, + // but only if pipeChat() wasn't already called manually during this turn + if (_chatPipeCount === 0 && isUIMessageStreamable(result)) { + await pipeChat(result); + } + + // Create a waitpoint token and emit a control chunk so the frontend + // knows to resume this run instead of triggering a new one. + const token = await wait.createToken({ timeout: turnTimeout }); + + const { waitUntilComplete } = streams.writer(CHAT_STREAM_KEY, { + execute: ({ write }) => { + write({ + type: "__trigger_waitpoint_ready", + tokenId: token.id, + publicAccessToken: token.publicAccessToken, + }); + }, + }); + await waitUntilComplete(); + + // Pause until the frontend completes the token with the next message + const next = await wait.forToken(token); + + if (!next.ok) { + // Timed out waiting for the next message — end the conversation + return; + } + + currentPayload = next.output; + } + }, + }); +} diff --git a/packages/trigger-sdk/src/v3/chat-react.ts b/packages/trigger-sdk/src/v3/chat-react.ts new file mode 100644 index 0000000000..a62496463a --- /dev/null +++ b/packages/trigger-sdk/src/v3/chat-react.ts @@ -0,0 +1,84 @@ +"use client"; + +/** + * @module @trigger.dev/sdk/chat/react + * + * React hooks for AI SDK chat transport integration. + * Use alongside `@trigger.dev/sdk/chat` for a type-safe, ergonomic DX. + * + * @example + * ```tsx + * import { useChat } from "@ai-sdk/react"; + * import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; + * import type { chat } from "@/trigger/chat"; + * + * function Chat() { + * const transport = useTriggerChatTransport({ + * task: "ai-chat", + * accessToken: () => fetchToken(), + * }); + * + * const { messages, sendMessage } = useChat({ transport }); + * } + * ``` + */ + +import { useRef } from "react"; +import { + TriggerChatTransport, + type TriggerChatTransportOptions, +} from "./chat.js"; +import type { AnyTask, TaskIdentifier } from "@trigger.dev/core/v3"; + +/** + * Options for `useTriggerChatTransport`, with a type-safe `task` field. + * + * Pass a task type parameter to get compile-time validation of the task ID: + * ```ts + * useTriggerChatTransport({ task: "my-task", ... }) + * ``` + */ +export type UseTriggerChatTransportOptions = Omit< + TriggerChatTransportOptions, + "task" +> & { + /** The task ID. Strongly typed when a task type parameter is provided. */ + task: TaskIdentifier; +}; + +/** + * React hook that creates and memoizes a `TriggerChatTransport` instance. + * + * The transport is created once on first render and reused for the lifetime + * of the component. This avoids the need for `useMemo` and ensures the + * transport's internal session state (waitpoint tokens, lastEventId, etc.) + * is preserved across re-renders. + * + * For dynamic access tokens, pass a function — it will be called on each + * request without needing to recreate the transport. + * + * @example + * ```tsx + * import { useChat } from "@ai-sdk/react"; + * import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; + * import type { chat } from "@/trigger/chat"; + * + * function Chat() { + * const transport = useTriggerChatTransport({ + * task: "ai-chat", + * accessToken: () => fetchToken(), + * }); + * + * const { messages, sendMessage } = useChat({ transport }); + * } + * ``` + */ +export function useTriggerChatTransport( + options: UseTriggerChatTransportOptions +): TriggerChatTransport { + const ref = useRef(null); + if (ref.current === null) { + ref.current = new TriggerChatTransport(options); + } + return ref.current; +} diff --git a/packages/trigger-sdk/src/v3/chat.test.ts b/packages/trigger-sdk/src/v3/chat.test.ts new file mode 100644 index 0000000000..03eceb1a8f --- /dev/null +++ b/packages/trigger-sdk/src/v3/chat.test.ts @@ -0,0 +1,1615 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import type { UIMessage, UIMessageChunk } from "ai"; +import { TriggerChatTransport, createChatTransport } from "./chat.js"; + +// Helper: encode text as SSE format +function sseEncode(chunks: (UIMessageChunk | Record)[]): 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 { + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(sseText)); + controller.close(); + }, + }); +} + +// Helper: create test UIMessages with unique IDs +let messageIdCounter = 0; + +function createUserMessage(text: string): UIMessage { + return { + id: `msg-user-${++messageIdCounter}`, + role: "user", + parts: [{ type: "text", text }], + }; +} + +function createAssistantMessage(text: string): UIMessage { + return { + id: `msg-assistant-${++messageIdCounter}`, + 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({ + task: "my-chat-task", + accessToken: "test-token", + }); + + expect(transport).toBeInstanceOf(TriggerChatTransport); + }); + + it("should accept optional configuration", () => { + const transport = new TriggerChatTransport({ + task: "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({ + task: "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({ + task: "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({ + task: "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 = 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({ + task: "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({ + task: "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; + 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({ + task: "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({ + task: "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({ + task: "my-task", + accessToken: "token", + }); + + expect(transport).toBeInstanceOf(TriggerChatTransport); + }); + + it("should pass options through to the transport", () => { + const transport = createChatTransport({ + task: "custom-task", + accessToken: "custom-token", + baseURL: "https://custom.example.com", + streamKey: "custom-key", + headers: { "X-Test": "value" }, + }); + + expect(transport).toBeInstanceOf(TriggerChatTransport); + }); + }); + + describe("publicAccessToken from trigger response", () => { + it("should use x-trigger-jwt from trigger response as the stream auth token", async () => { + const fetchSpy = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/trigger")) { + // Return with x-trigger-jwt header — this public token should be + // used for the subsequent stream subscription request. + return new Response( + JSON.stringify({ id: "run_pat" }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "server-generated-public-token", + }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + // Verify the Authorization header uses the server-generated token + const authHeader = (init?.headers as Record)?.["Authorization"]; + expect(authHeader).toBe("Bearer server-generated-public-token"); + + 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}`); + }); + + global.fetch = fetchSpy; + + const transport = new TriggerChatTransport({ + task: "my-task", + accessToken: "caller-token", + baseURL: "https://api.test.trigger.dev", + }); + + const stream = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-pat", + messageId: undefined, + messages: [createUserMessage("test")], + abortSignal: undefined, + }); + + // Consume the stream + const reader = stream.getReader(); + while (true) { + const { done } = await reader.read(); + if (done) break; + } + + // Verify the stream subscription used the public token, not the caller token + 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 streamHeaders = streamCall![1]?.headers as Record; + expect(streamHeaders["Authorization"]).toBe("Bearer server-generated-public-token"); + }); + }); + + 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({ + task: "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((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({ + 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({ + task: "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({ + task: "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({ + task: "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({ + task: "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 = 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({ + task: "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 = triggerBody.payload; + expect(payload.trigger).toBe("regenerate-message"); + expect(payload.messageId).toBe("msg-to-regen"); + }); + }); + + describe("lastEventId tracking", () => { + it("should pass lastEventId to SSE subscription on subsequent turns", async () => { + const controlChunk = { + type: "__trigger_waitpoint_ready", + tokenId: "wp_token_eid", + publicAccessToken: "wp_access_eid", + }; + + let triggerCallCount = 0; + const streamFetchCalls: { url: string; headers: Record }[] = []; + + global.fetch = 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")) { + triggerCallCount++; + return new Response( + JSON.stringify({ id: "run_eid" }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_token_eid", + }, + } + ); + } + + if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) { + return new Response( + JSON.stringify({ success: true }), + { + status: 200, + headers: { "content-type": "application/json" }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + streamFetchCalls.push({ + url: urlStr, + headers: (init?.headers as Record) ?? {}, + }); + + const chunks = [ + ...sampleChunks, + { type: "finish" as const, id: "part-1" } as UIMessageChunk, + controlChunk, + ]; + 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({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + }); + + // First message — triggers a new run + const stream1 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-eid", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + const reader1 = stream1.getReader(); + while (true) { + const { done } = await reader1.read(); + if (done) break; + } + + // Second message — completes the waitpoint + const stream2 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-eid", + messageId: undefined, + messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("What's up?")], + abortSignal: undefined, + }); + + const reader2 = stream2.getReader(); + while (true) { + const { done } = await reader2.read(); + if (done) break; + } + + // The second stream subscription should include a Last-Event-ID header + expect(streamFetchCalls.length).toBe(2); + const secondStreamHeaders = streamFetchCalls[1]!.headers; + // SSEStreamSubscription passes lastEventId as the Last-Event-ID header + expect(secondStreamHeaders["Last-Event-ID"]).toBeDefined(); + }); + }); + + describe("AbortController cleanup", () => { + it("should terminate SSE connection after intercepting control chunk", async () => { + const controlChunk = { + type: "__trigger_waitpoint_ready", + tokenId: "wp_token_abort", + publicAccessToken: "wp_access_abort", + }; + + let streamAborted = false; + + global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/trigger")) { + return new Response( + JSON.stringify({ id: "run_abort_cleanup" }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_token", + }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + // Track abort signal + const signal = init?.signal; + if (signal) { + signal.addEventListener("abort", () => { + streamAborted = true; + }); + } + + const chunks = [ + ...sampleChunks, + { type: "finish" as const, id: "part-1" } as UIMessageChunk, + controlChunk, + ]; + 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({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + }); + + const stream = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-abort-cleanup", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + // Consume all chunks + const reader = stream.getReader(); + while (true) { + const { done } = await reader.read(); + if (done) break; + } + + // The internal AbortController should have aborted the fetch + expect(streamAborted).toBe(true); + }); + }); + + describe("async accessToken", () => { + it("should accept an async function for accessToken", 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_async_${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({ + task: "my-task", + accessToken: async () => { + tokenCallCount++; + // Simulate async work (e.g. server action) + await new Promise((r) => setTimeout(r, 1)); + return `async-token-${tokenCallCount}`; + }, + baseURL: "https://api.test.trigger.dev", + }); + + await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-async", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + expect(tokenCallCount).toBe(1); + }); + + it("should resolve async token for waitpoint completion flow", async () => { + const controlChunk = { + type: "__trigger_waitpoint_ready", + tokenId: "wp_token_async", + publicAccessToken: "wp_access_async", + }; + + let tokenCallCount = 0; + let completeWaitpointCalled = false; + + global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) { + return new Response( + JSON.stringify({ id: "run_async_wp" }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "stream-token", + }, + } + ); + } + + if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) { + completeWaitpointCalled = true; + return new Response( + JSON.stringify({ success: true }), + { + status: 200, + headers: { "content-type": "application/json" }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + const chunks = [ + ...sampleChunks, + { type: "finish" as const, id: "part-1" } as UIMessageChunk, + controlChunk, + ]; + 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({ + task: "my-task", + accessToken: async () => { + tokenCallCount++; + await new Promise((r) => setTimeout(r, 1)); + return `async-wp-token-${tokenCallCount}`; + }, + baseURL: "https://api.test.trigger.dev", + }); + + // First message — triggers a new run (calls async token) + const stream1 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-async-wp", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + const reader1 = stream1.getReader(); + while (true) { + const { done } = await reader1.read(); + if (done) break; + } + + const firstTokenCount = tokenCallCount; + + // Second message — should complete waitpoint (does NOT call async token) + const stream2 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-async-wp", + messageId: undefined, + messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("More")], + abortSignal: undefined, + }); + + const reader2 = stream2.getReader(); + while (true) { + const { done } = await reader2.read(); + if (done) break; + } + + // Token function should NOT have been called again for the waitpoint path + expect(tokenCallCount).toBe(firstTokenCount); + expect(completeWaitpointCalled).toBe(true); + }); + }); + + describe("single-run mode (waitpoint loop)", () => { + it("should store waitpoint token from control chunk and not forward it to consumer", async () => { + const controlChunk = { + type: "__trigger_waitpoint_ready", + tokenId: "wp_token_123", + publicAccessToken: "wp_access_abc", + }; + + 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_single" }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_token", + }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + const chunks = [ + ...sampleChunks, + { type: "finish" as const, id: "part-1" } as UIMessageChunk, + controlChunk, + ]; + 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({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + }); + + const stream = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-single", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + // Read all chunks — the control chunk should NOT appear + const reader = stream.getReader(); + const receivedChunks: UIMessageChunk[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + receivedChunks.push(value); + } + + // All AI SDK chunks should be forwarded + expect(receivedChunks.length).toBe(sampleChunks.length + 1); // +1 for the finish chunk + // Control chunk should not be in the output + expect(receivedChunks.every((c) => c.type !== ("__trigger_waitpoint_ready" as any))).toBe(true); + }); + + it("should complete waitpoint token on second message instead of triggering a new run", async () => { + const controlChunk = { + type: "__trigger_waitpoint_ready", + tokenId: "wp_token_456", + publicAccessToken: "wp_access_def", + }; + + let triggerCallCount = 0; + let completeWaitpointCalled = false; + + global.fetch = 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")) { + triggerCallCount++; + return new Response( + JSON.stringify({ id: "run_resume" }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_token", + }, + } + ); + } + + // Handle waitpoint token completion + if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) { + completeWaitpointCalled = true; + return new Response( + JSON.stringify({ success: true }), + { + status: 200, + headers: { "content-type": "application/json" }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + const chunks = [ + ...sampleChunks, + { type: "finish" as const, id: "part-1" } as UIMessageChunk, + controlChunk, + ]; + 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({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + }); + + // First message — triggers a new run + const stream1 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-resume", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + // Consume stream to capture the control chunk + const reader1 = stream1.getReader(); + while (true) { + const { done } = await reader1.read(); + if (done) break; + } + + expect(triggerCallCount).toBe(1); + + // Second message — should complete the waitpoint instead of triggering + const stream2 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-resume", + messageId: undefined, + messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("How are you?")], + abortSignal: undefined, + }); + + // Consume second stream + const reader2 = stream2.getReader(); + while (true) { + const { done } = await reader2.read(); + if (done) break; + } + + // Should NOT have triggered a second run + expect(triggerCallCount).toBe(1); + // Should have completed the waitpoint + expect(completeWaitpointCalled).toBe(true); + }); + + it("should fall back to triggering a new run if stream closes without control chunk", async () => { + let triggerCallCount = 0; + + global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) { + triggerCallCount++; + return new Response( + JSON.stringify({ id: `run_fallback_${triggerCallCount}` }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_token", + }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + // No control chunk — stream just ends after the finish + const chunks: UIMessageChunk[] = [ + { type: "text-start", id: "p1" }, + { type: "text-delta", id: "p1", delta: "Hello" }, + { 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({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + }); + + // First message + const stream1 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-fallback", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + const reader1 = stream1.getReader(); + while (true) { + const { done } = await reader1.read(); + if (done) break; + } + + expect(triggerCallCount).toBe(1); + + // Second message — no waitpoint token stored, should trigger a new run + await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-fallback", + messageId: undefined, + messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("Again")], + abortSignal: undefined, + }); + + // Should have triggered a second run + expect(triggerCallCount).toBe(2); + }); + + it("should fall back to new run when completing waitpoint fails", async () => { + const controlChunk = { + type: "__trigger_waitpoint_ready", + tokenId: "wp_token_fail", + publicAccessToken: "wp_access_fail", + }; + + let triggerCallCount = 0; + + global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/api/v1/tasks/") && urlStr.includes("/trigger")) { + triggerCallCount++; + return new Response( + JSON.stringify({ id: `run_fail_${triggerCallCount}` }), + { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_token", + }, + } + ); + } + + // Waitpoint completion fails + if (urlStr.includes("/api/v1/waitpoints/tokens/") && urlStr.includes("/complete")) { + return new Response( + JSON.stringify({ error: "Token expired" }), + { + status: 400, + headers: { "content-type": "application/json" }, + } + ); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + // First call has control chunk, subsequent calls don't + const chunks: (UIMessageChunk | Record)[] = [ + ...sampleChunks, + { type: "finish" as const, id: "part-1" } as UIMessageChunk, + ]; + + if (triggerCallCount <= 1) { + chunks.push(controlChunk); + } + + 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({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + }); + + // First message + const stream1 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-fail", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + const reader1 = stream1.getReader(); + while (true) { + const { done } = await reader1.read(); + if (done) break; + } + + expect(triggerCallCount).toBe(1); + + // Second message — waitpoint completion will fail, should fall back to new run + const stream2 = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-fail", + messageId: undefined, + messages: [createUserMessage("Hello"), createAssistantMessage("Hi!"), createUserMessage("Again")], + abortSignal: undefined, + }); + + const reader2 = stream2.getReader(); + while (true) { + const { done } = await reader2.read(); + if (done) break; + } + + // Should have triggered a second run as fallback + expect(triggerCallCount).toBe(2); + }); + }); +}); diff --git a/packages/trigger-sdk/src/v3/chat.ts b/packages/trigger-sdk/src/v3/chat.ts new file mode 100644 index 0000000000..e36c576187 --- /dev/null +++ b/packages/trigger-sdk/src/v3/chat.ts @@ -0,0 +1,394 @@ +/** + * @module @trigger.dev/sdk/chat + * + * Browser-safe module for AI SDK chat transport integration. + * Use this on the frontend with the AI SDK's `useChat` hook. + * + * For backend helpers (`chatTask`, `pipeChat`), use `@trigger.dev/sdk/ai` instead. + * + * @example + * ```tsx + * import { useChat } from "@ai-sdk/react"; + * import { TriggerChatTransport } from "@trigger.dev/sdk/chat"; + * + * function Chat({ accessToken }: { accessToken: string }) { + * const { messages, sendMessage, status } = useChat({ + * transport: new TriggerChatTransport({ + * task: "my-chat-task", + * accessToken, + * }), + * }); + * } + * ``` + */ + +import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai"; +import { ApiClient, SSEStreamSubscription } from "@trigger.dev/core/v3"; + +const DEFAULT_STREAM_KEY = "chat"; +const DEFAULT_BASE_URL = "https://api.trigger.dev"; +const DEFAULT_STREAM_TIMEOUT_SECONDS = 120; + +/** + * Options for creating a TriggerChatTransport. + */ +export type TriggerChatTransportOptions = { + /** + * The Trigger.dev task ID to trigger for chat completions. + * This task should be defined using `chatTask()` from `@trigger.dev/sdk/ai`, + * or a regular `task()` that uses `pipeChat()`. + */ + 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) + * + * Can also be a function that returns a token string (sync or async), + * useful for dynamic token refresh or passing a Next.js server action directly. + */ + accessToken: string | (() => string | Promise); + + /** + * 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; + + /** + * The number of seconds to wait for the realtime stream to produce data + * before timing out. + * + * @default 120 + */ + streamTimeoutSeconds?: number; +}; + +/** + * Internal state for tracking active chat sessions. + * @internal + */ +type ChatSessionState = { + runId: string; + publicAccessToken: string; + /** Token ID from the `__trigger_waitpoint_ready` control chunk. */ + waitpointTokenId?: string; + /** Access token scoped to complete the waitpoint (separate from the run's PAT). */ + waitpointAccessToken?: string; + /** Last SSE event ID — used to resume the stream without replaying old events. */ + lastEventId?: string; +}; + +/** + * A custom AI SDK `ChatTransport` that runs chat completions as durable Trigger.dev tasks. + * + * 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` that the AI SDK processes natively + * + * @example + * ```tsx + * import { useChat } from "@ai-sdk/react"; + * import { TriggerChatTransport } from "@trigger.dev/sdk/chat"; + * + * function Chat({ accessToken }: { accessToken: string }) { + * const { messages, sendMessage, status } = useChat({ + * transport: new TriggerChatTransport({ + * task: "my-chat-task", + * accessToken, + * }), + * }); + * + * // ... render messages + * } + * ``` + * + * On the backend, define the task using `chatTask` from `@trigger.dev/sdk/ai`: + * + * @example + * ```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), + * }); + * }, + * }); + * ``` + */ +export class TriggerChatTransport implements ChatTransport { + private readonly taskId: string; + private readonly resolveAccessToken: () => string | Promise; + private readonly baseURL: string; + private readonly streamKey: string; + private readonly extraHeaders: Record; + private readonly streamTimeoutSeconds: number; + + private sessions: Map = new Map(); + + constructor(options: TriggerChatTransportOptions) { + this.taskId = options.task; + 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; + } + + sendMessages = async ( + options: { + trigger: "submit-message" | "regenerate-message"; + chatId: string; + messageId: string | undefined; + messages: UIMessage[]; + abortSignal: AbortSignal | undefined; + } & ChatRequestOptions + ): Promise> => { + const { trigger, chatId, messageId, messages, abortSignal, body, metadata } = options; + + const payload = { + ...(body ?? {}), + messages, + chatId, + trigger, + messageId, + metadata, + }; + + const session = this.sessions.get(chatId); + + // If we have a waitpoint token from a previous turn, complete it to + // resume the existing run instead of triggering a new one. + if (session?.waitpointTokenId && session.waitpointAccessToken) { + const tokenId = session.waitpointTokenId; + const tokenAccessToken = session.waitpointAccessToken; + + // Clear the used waitpoint so we don't try to reuse it + session.waitpointTokenId = undefined; + session.waitpointAccessToken = undefined; + + try { + const wpClient = new ApiClient(this.baseURL, tokenAccessToken); + await wpClient.completeWaitpointToken(tokenId, { data: payload }); + + return this.subscribeToStream( + session.runId, + session.publicAccessToken, + abortSignal, + chatId + ); + } catch { + // If completing the waitpoint fails (run died, token expired, etc.), + // fall through to trigger a new run. + this.sessions.delete(chatId); + } + } + + const currentToken = await this.resolveAccessToken(); + const apiClient = new ApiClient(this.baseURL, currentToken); + + const triggerResponse = await apiClient.triggerTask(this.taskId, { + payload, + options: { + payloadType: "application/json", + }, + }); + + const runId = triggerResponse.id; + const publicAccessToken = + "publicAccessToken" in triggerResponse + ? (triggerResponse as { publicAccessToken?: string }).publicAccessToken + : undefined; + + this.sessions.set(chatId, { + runId, + publicAccessToken: publicAccessToken ?? currentToken, + }); + + return this.subscribeToStream( + runId, + publicAccessToken ?? currentToken, + abortSignal, + chatId + ); + }; + + reconnectToStream = async ( + options: { + chatId: string; + } & ChatRequestOptions + ): Promise | null> => { + const session = this.sessions.get(options.chatId); + if (!session) { + return null; + } + + return this.subscribeToStream(session.runId, session.publicAccessToken, undefined, options.chatId); + }; + + private subscribeToStream( + runId: string, + accessToken: string, + abortSignal: AbortSignal | undefined, + chatId?: string + ): ReadableStream { + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + ...this.extraHeaders, + }; + + // When resuming a run via waitpoint, skip past previously-seen events + // so we only receive the new turn's response. + const session = chatId ? this.sessions.get(chatId) : undefined; + + // Create an internal AbortController so we can terminate the underlying + // fetch connection when we're done reading (e.g. after intercepting the + // control chunk). Without this, the SSE connection stays open and leaks. + const internalAbort = new AbortController(); + const combinedSignal = abortSignal + ? AbortSignal.any([abortSignal, internalAbort.signal]) + : internalAbort.signal; + + const subscription = new SSEStreamSubscription( + `${this.baseURL}/realtime/v1/streams/${runId}/${this.streamKey}`, + { + headers, + signal: combinedSignal, + timeoutInSeconds: this.streamTimeoutSeconds, + lastEventId: session?.lastEventId, + } + ); + + return new ReadableStream({ + start: async (controller) => { + try { + const sseStream = await subscription.subscribe(); + const reader = sseStream.getReader(); + + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) { + // Stream closed without a control chunk — the run has + // ended (or was killed). Clear the session so that the + // next message triggers a fresh run. + if (chatId) { + const s = this.sessions.get(chatId); + if (s) { + s.waitpointTokenId = undefined; + s.waitpointAccessToken = undefined; + } + } + controller.close(); + return; + } + + if (combinedSignal.aborted) { + internalAbort.abort(); + await reader.cancel(); + controller.close(); + return; + } + + // Track the last event ID so we can resume from here + if (value.id && session) { + session.lastEventId = value.id; + } + + // Guard against heartbeat or malformed SSE events + if (value.chunk != null && typeof value.chunk === "object") { + const chunk = value.chunk as Record; + + // Intercept the waitpoint-ready control chunk emitted by + // `chatTask` after the AI response stream completes. This + // chunk is never forwarded to the AI SDK consumer. + if (chunk.type === "__trigger_waitpoint_ready" && chatId) { + const s = this.sessions.get(chatId); + if (s) { + s.waitpointTokenId = chunk.tokenId as string; + s.waitpointAccessToken = chunk.publicAccessToken as string; + } + + // Abort the underlying fetch to close the SSE connection + internalAbort.abort(); + try { + controller.close(); + } catch { + // Controller may already be closed + } + return; + } + + controller.enqueue(chunk as unknown as UIMessageChunk); + } + } + } catch (readError) { + reader.releaseLock(); + throw readError; + } + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + try { + controller.close(); + } catch { + // Controller may already be closed + } + return; + } + + controller.error(error); + } + }, + }); + } +} + +/** + * Creates a new `TriggerChatTransport` instance. + * + * @example + * ```tsx + * import { useChat } from "@ai-sdk/react"; + * import { createChatTransport } from "@trigger.dev/sdk/chat"; + * + * const transport = createChatTransport({ + * task: "my-chat-task", + * accessToken: publicAccessToken, + * }); + * + * function Chat() { + * const { messages, sendMessage } = useChat({ transport }); + * } + * ``` + */ +export function createChatTransport(options: TriggerChatTransportOptions): TriggerChatTransport { + return new TriggerChatTransport(options); +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbf25048b6..81b6bcc57e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -456,8 +456,8 @@ importers: specifier: ^0.1.3 version: 0.1.3(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4)) '@s2-dev/streamstore': - specifier: ^0.22.5 - version: 0.22.5(supports-color@10.0.0) + specifier: ^0.17.2 + version: 0.17.3(typescript@5.5.4) '@sentry/remix': specifier: 9.46.0 version: 9.46.0(patch_hash=146126b032581925294aaed63ab53ce3f5e0356a755f1763d7a9a76b9846943b)(@remix-run/node@2.1.0(typescript@5.5.4))(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(encoding@0.1.13)(react@18.2.0) @@ -1452,8 +1452,8 @@ importers: specifier: 1.36.0 version: 1.36.0 '@s2-dev/streamstore': - specifier: ^0.22.5 - version: 0.22.5(supports-color@10.0.0) + specifier: ^0.17.6 + version: 0.17.6 '@trigger.dev/build': specifier: workspace:4.4.1 version: link:../build @@ -1729,8 +1729,8 @@ importers: specifier: 1.36.0 version: 1.36.0 '@s2-dev/streamstore': - specifier: 0.22.5 - version: 0.22.5(supports-color@10.0.0) + specifier: 0.17.3 + version: 0.17.3(typescript@5.5.4) dequal: specifier: ^2.0.3 version: 2.0.3 @@ -2077,6 +2077,9 @@ importers: '@types/debug': specifier: ^4.1.7 version: 4.1.7 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 '@types/slug': specifier: ^5.0.3 version: 5.0.3 @@ -2108,6 +2111,58 @@ importers: specifier: 3.25.76 version: 3.25.76 + references/ai-chat: + dependencies: + '@ai-sdk/openai': + specifier: ^3.0.0 + version: 3.0.19(zod@3.25.76) + '@ai-sdk/react': + specifier: ^3.0.0 + version: 3.0.51(react@19.1.0)(zod@3.25.76) + '@trigger.dev/sdk': + specifier: workspace:* + version: link:../../packages/trigger-sdk + ai: + specifier: ^6.0.0 + version: 6.0.3(zod@3.25.76) + next: + specifier: 15.3.3 + version: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.37.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.0.0 + version: 19.1.0 + react-dom: + specifier: ^19.0.0 + version: 19.1.0(react@19.1.0) + zod: + specifier: 3.25.76 + version: 3.25.76 + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.0.17 + '@trigger.dev/build': + specifier: workspace:* + version: link:../../packages/build + '@types/node': + specifier: 20.14.14 + version: 20.14.14 + '@types/react': + specifier: ^19 + version: 19.0.12 + '@types/react-dom': + specifier: ^19 + version: 19.0.4(@types/react@19.0.12) + tailwindcss: + specifier: ^4 + version: 4.0.17 + trigger.dev: + specifier: workspace:* + version: link:../../packages/cli-v3 + typescript: + specifier: 5.5.4 + version: 5.5.4 + references/bun-catalog: dependencies: '@trigger.dev/sdk': @@ -2628,7 +2683,7 @@ importers: version: 6.20.0-integration-next.8 '@prisma/client': specifier: 6.20.0-integration-next.8 - version: 6.20.0-integration-next.8(prisma@6.20.0-integration-next.8(@types/react@19.0.12)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4) + version: 6.20.0-integration-next.8(prisma@6.20.0-integration-next.8(@types/react@19.2.14)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4) '@trigger.dev/build': specifier: workspace:* version: link:../../packages/build @@ -2641,7 +2696,7 @@ importers: devDependencies: prisma: specifier: 6.20.0-integration-next.8 - version: 6.20.0-integration-next.8(@types/react@19.0.12)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4) + version: 6.20.0-integration-next.8(@types/react@19.2.14)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4) trigger.dev: specifier: workspace:* version: link:../../packages/cli-v3 @@ -2867,6 +2922,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/gateway@3.0.22': + resolution: {integrity: sha512-NgnlY73JNuooACHqUIz5uMOEWvqR1MMVbb2soGLMozLY1fgwEIF5iJFDAGa5/YArlzw2ATVU7zQu7HkR/FUjgA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/openai@1.0.1': resolution: {integrity: sha512-snZge8457afWlosVNUn+BG60MrxAPOOm3zmIMxJZih8tneNSiRbTVCbSzAtq/9vsnOHDe5RR83PRl85juOYEnA==} engines: {node: '>=18'} @@ -2897,6 +2958,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/openai@3.0.19': + resolution: {integrity: sha512-qpMGKV6eYfW8IzErk/OppchQwVui3GPc4BEfg/sQGRzR89vf2Sa8qvSavXeZi5w/oUF56d+VtobwSH0FRooFCQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@1.0.22': resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==} engines: {node: '>=18'} @@ -2945,6 +3012,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.9': + resolution: {integrity: sha512-bB4r6nfhBOpmoS9mePxjRoCy+LnzP3AfhyMGCkGL4Mn9clVNlqEeKj26zEKEtB6yoSVcT1IQ0Zh9fytwMCDnow==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@0.0.26': resolution: {integrity: sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==} engines: {node: '>=18'} @@ -2969,6 +3042,10 @@ packages: resolution: {integrity: sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ==} engines: {node: '>=18'} + '@ai-sdk/provider@3.0.5': + resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} + engines: {node: '>=18'} + '@ai-sdk/react@1.0.0': resolution: {integrity: sha512-BDrZqQA07Btg64JCuhFvBgYV+tt2B8cXINzEqWknGoxqcwgdE8wSLG2gkXoLzyC2Rnj7oj0HHpOhLUxDCmoKZg==} engines: {node: '>=18'} @@ -3001,6 +3078,12 @@ packages: zod: optional: true + '@ai-sdk/react@3.0.51': + resolution: {integrity: sha512-7nmCwEJM52NQZB4/ED8qJ4wbDg7EEWh94qJ7K9GSJxD6sWF3GOKrRZ5ivm4qNmKhY+JfCxCAxfghGY5mTKOsxw==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@ai-sdk/ui-utils@1.0.0': resolution: {integrity: sha512-oXBDIM/0niWeTWyw77RVl505dNxBUDLLple7bTsqo2d3i1UKwGlzBUX8XqZsh7GbY7I6V05nlG0Y8iGlWxv1Aw==} engines: {node: '>=18'} @@ -5920,6 +6003,9 @@ packages: '@next/env@15.2.4': resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==} + '@next/env@15.3.3': + resolution: {integrity: sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==} + '@next/env@15.4.8': resolution: {integrity: sha512-LydLa2MDI1NMrOFSkO54mTc8iIHSttj6R6dthITky9ylXV2gCGi0bHQjVCtLGRshdRPjyh2kXbxJukDtBWQZtQ==} @@ -5944,6 +6030,12 @@ packages: cpu: [arm64] os: [darwin] + '@next/swc-darwin-arm64@15.3.3': + resolution: {integrity: sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-arm64@15.4.8': resolution: {integrity: sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==} engines: {node: '>= 10'} @@ -5974,6 +6066,12 @@ packages: cpu: [x64] os: [darwin] + '@next/swc-darwin-x64@15.3.3': + resolution: {integrity: sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-darwin-x64@15.4.8': resolution: {integrity: sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw==} engines: {node: '>= 10'} @@ -6007,6 +6105,13 @@ packages: os: [linux] libc: [glibc] + '@next/swc-linux-arm64-gnu@15.3.3': + resolution: {integrity: sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@next/swc-linux-arm64-gnu@15.4.8': resolution: {integrity: sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw==} engines: {node: '>= 10'} @@ -6042,6 +6147,13 @@ packages: os: [linux] libc: [musl] + '@next/swc-linux-arm64-musl@15.3.3': + resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + '@next/swc-linux-arm64-musl@15.4.8': resolution: {integrity: sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==} engines: {node: '>= 10'} @@ -6077,6 +6189,13 @@ packages: os: [linux] libc: [glibc] + '@next/swc-linux-x64-gnu@15.3.3': + resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@next/swc-linux-x64-gnu@15.4.8': resolution: {integrity: sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==} engines: {node: '>= 10'} @@ -6112,6 +6231,13 @@ packages: os: [linux] libc: [musl] + '@next/swc-linux-x64-musl@15.3.3': + resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + '@next/swc-linux-x64-musl@15.4.8': resolution: {integrity: sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==} engines: {node: '>= 10'} @@ -6144,6 +6270,12 @@ packages: cpu: [arm64] os: [win32] + '@next/swc-win32-arm64-msvc@15.3.3': + resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@next/swc-win32-arm64-msvc@15.4.8': resolution: {integrity: sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==} engines: {node: '>= 10'} @@ -6186,6 +6318,12 @@ packages: cpu: [x64] os: [win32] + '@next/swc-win32-x64-msvc@15.3.3': + resolution: {integrity: sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@next/swc-win32-x64-msvc@15.4.8': resolution: {integrity: sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg==} engines: {node: '>= 10'} @@ -9412,8 +9550,13 @@ packages: '@rushstack/eslint-patch@1.2.0': resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} - '@s2-dev/streamstore@0.22.5': - resolution: {integrity: sha512-GqdOKIbIoIxT+40fnKzHbrsHB6gBqKdECmFe7D3Ojk4FoN1Hu0LhFzZv6ZmVMjoHHU+55debS1xSWjZwQmbIyQ==} + '@s2-dev/streamstore@0.17.3': + resolution: {integrity: sha512-UeXL5+MgZQfNkbhCgEDVm7PrV5B3bxh6Zp4C5pUzQQwaoA+iGh2QiiIptRZynWgayzRv4vh0PYfnKpTzJEXegQ==} + peerDependencies: + typescript: 5.5.4 + + '@s2-dev/streamstore@0.17.6': + resolution: {integrity: sha512-ocjZfKaPKmo2yhudM58zVNHv3rBLSbTKkabVoLFn9nAxU6iLrR2CO3QmSo7/waohI3EZHAWxF/Pw8kA8d6QH2g==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -10871,6 +11014,9 @@ packages: '@types/react@19.0.12': resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/readable-stream@4.0.14': resolution: {integrity: sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==} @@ -11121,6 +11267,10 @@ packages: resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} engines: {node: '>= 20'} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@vercel/otel@1.13.0': resolution: {integrity: sha512-esRkt470Y2jRK1B1g7S1vkt4Csu44gp83Zpu8rIyPoqy2BKgk4z7ik1uSMswzi45UogLHFl6yR5TauDurBQi4Q==} engines: {node: '>=18'} @@ -11468,6 +11618,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ai@6.0.49: + resolution: {integrity: sha512-LABniBX/0R6Tv+iUK5keUZhZLaZUe4YjP5M2rZ4wAdZ8iKV3EfTAoJxuL1aaWTSJKIilKa9QUEkCgnp89/32bw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -12515,6 +12671,9 @@ packages: csstype@3.2.0: resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -14251,6 +14410,7 @@ packages: glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -16256,6 +16416,28 @@ packages: sass: optional: true + next@15.3.3: + resolution: {integrity: sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + next@15.4.8: resolution: {integrity: sha512-jwOXTz/bo0Pvlf20FSb6VXVeWRssA2vbvq9SdrOPEg9x8E1B27C2rQtvriAn600o9hH61kjrVRexEffv3JybuA==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -17206,10 +17388,6 @@ packages: resolution: {integrity: sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.4: resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} engines: {node: ^10 || ^12 || >=14} @@ -18962,22 +19140,21 @@ packages: tar@6.1.13: resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.5.6: resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} @@ -20260,6 +20437,13 @@ snapshots: '@vercel/oidc': 3.0.5 zod: 3.25.76 + '@ai-sdk/gateway@3.0.22(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + '@ai-sdk/openai@1.0.1(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.0.0 @@ -20290,6 +20474,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.12(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/openai@3.0.19(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/provider-utils@1.0.22(zod@3.25.76)': dependencies: '@ai-sdk/provider': 0.0.26 @@ -20344,6 +20534,13 @@ snapshots: eventsource-parser: 3.0.6 zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.9(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + '@ai-sdk/provider@0.0.26': dependencies: json-schema: 0.4.0 @@ -20368,6 +20565,10 @@ snapshots: dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@3.0.5': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/react@1.0.0(react@18.3.1)(zod@3.25.76)': dependencies: '@ai-sdk/provider-utils': 2.0.0(zod@3.25.76) @@ -20398,6 +20599,16 @@ snapshots: optionalDependencies: zod: 3.25.76 + '@ai-sdk/react@3.0.51(react@19.1.0)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + ai: 6.0.49(zod@3.25.76) + react: 19.1.0 + swr: 2.2.5(react@19.1.0) + throttleit: 2.1.0 + transitivePeerDependencies: + - zod + '@ai-sdk/ui-utils@1.0.0(zod@3.25.76)': dependencies: '@ai-sdk/provider': 1.0.0 @@ -20444,7 +20655,7 @@ snapshots: commander: 10.0.1 marked: 9.1.6 marked-terminal: 7.1.0(marked@9.1.6) - semver: 7.6.3 + semver: 7.7.3 '@arethetypeswrong/core@0.15.1': dependencies: @@ -23125,7 +23336,7 @@ snapshots: '@epic-web/test-server@0.1.0(bufferutil@4.0.9)': dependencies: - '@hono/node-server': 1.12.2(hono@4.11.8) + '@hono/node-server': 1.12.2(hono@4.5.11) '@hono/node-ws': 1.0.4(@hono/node-server@1.12.2(hono@4.5.11))(bufferutil@4.0.9) '@open-draft/deferred-promise': 2.2.0 '@types/ws': 8.5.12 @@ -23873,9 +24084,9 @@ snapshots: dependencies: react: 18.2.0 - '@hono/node-server@1.12.2(hono@4.11.8)': + '@hono/node-server@1.12.2(hono@4.5.11)': dependencies: - hono: 4.11.8 + hono: 4.5.11 '@hono/node-server@1.19.9(hono@4.11.8)': dependencies: @@ -23883,7 +24094,7 @@ snapshots: '@hono/node-ws@1.0.4(@hono/node-server@1.12.2(hono@4.5.11))(bufferutil@4.0.9)': dependencies: - '@hono/node-server': 1.12.2(hono@4.11.8) + '@hono/node-server': 1.12.2(hono@4.5.11) ws: 8.18.3(bufferutil@4.0.9) transitivePeerDependencies: - bufferutil @@ -24404,6 +24615,8 @@ snapshots: '@next/env@15.2.4': {} + '@next/env@15.3.3': {} + '@next/env@15.4.8': {} '@next/env@15.5.6': {} @@ -24417,6 +24630,9 @@ snapshots: '@next/swc-darwin-arm64@15.2.4': optional: true + '@next/swc-darwin-arm64@15.3.3': + optional: true + '@next/swc-darwin-arm64@15.4.8': optional: true @@ -24432,6 +24648,9 @@ snapshots: '@next/swc-darwin-x64@15.2.4': optional: true + '@next/swc-darwin-x64@15.3.3': + optional: true + '@next/swc-darwin-x64@15.4.8': optional: true @@ -24447,6 +24666,9 @@ snapshots: '@next/swc-linux-arm64-gnu@15.2.4': optional: true + '@next/swc-linux-arm64-gnu@15.3.3': + optional: true + '@next/swc-linux-arm64-gnu@15.4.8': optional: true @@ -24462,6 +24684,9 @@ snapshots: '@next/swc-linux-arm64-musl@15.2.4': optional: true + '@next/swc-linux-arm64-musl@15.3.3': + optional: true + '@next/swc-linux-arm64-musl@15.4.8': optional: true @@ -24477,6 +24702,9 @@ snapshots: '@next/swc-linux-x64-gnu@15.2.4': optional: true + '@next/swc-linux-x64-gnu@15.3.3': + optional: true + '@next/swc-linux-x64-gnu@15.4.8': optional: true @@ -24492,6 +24720,9 @@ snapshots: '@next/swc-linux-x64-musl@15.2.4': optional: true + '@next/swc-linux-x64-musl@15.3.3': + optional: true + '@next/swc-linux-x64-musl@15.4.8': optional: true @@ -24507,6 +24738,9 @@ snapshots: '@next/swc-win32-arm64-msvc@15.2.4': optional: true + '@next/swc-win32-arm64-msvc@15.3.3': + optional: true + '@next/swc-win32-arm64-msvc@15.4.8': optional: true @@ -24528,6 +24762,9 @@ snapshots: '@next/swc-win32-x64-msvc@15.2.4': optional: true + '@next/swc-win32-x64-msvc@15.3.3': + optional: true + '@next/swc-win32-x64-msvc@15.4.8': optional: true @@ -25440,11 +25677,11 @@ snapshots: prisma: 6.19.0(typescript@5.5.4) typescript: 5.5.4 - '@prisma/client@6.20.0-integration-next.8(prisma@6.20.0-integration-next.8(@types/react@19.0.12)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4)': + '@prisma/client@6.20.0-integration-next.8(prisma@6.20.0-integration-next.8(@types/react@19.2.14)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4)': dependencies: '@prisma/client-runtime-utils': 6.20.0-integration-next.8 optionalDependencies: - prisma: 6.20.0-integration-next.8(@types/react@19.0.12)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4) + prisma: 6.20.0-integration-next.8(@types/react@19.2.14)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4) typescript: 5.5.4 '@prisma/config@6.14.0(magicast@0.3.5)': @@ -25608,9 +25845,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@prisma/studio-core-licensed@0.6.0(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@prisma/studio-core-licensed@0.6.0(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@types/react': 19.0.12 + '@types/react': 19.2.14 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -25641,7 +25878,7 @@ snapshots: '@puppeteer/browsers@2.10.6': dependencies: - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -25716,14 +25953,14 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-avatar@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': @@ -25738,21 +25975,21 @@ snapshots: '@types/react': 19.0.12 '@types/react-dom': 19.0.4(@types/react@19.0.12) - '@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-collapsible@1.1.11(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -25804,17 +26041,17 @@ snapshots: '@types/react': 18.2.69 '@types/react-dom': 18.2.7 - '@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-collection@1.1.7(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -25907,6 +26144,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + '@radix-ui/react-context@1.0.1(@types/react@18.2.69)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.7 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.69 + '@radix-ui/react-context@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 @@ -26017,12 +26261,12 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 - '@radix-ui/react-direction@1.0.1(@types/react@18.3.1)(react@18.3.1)': + '@radix-ui/react-direction@1.0.1(@types/react@18.2.69)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@radix-ui/react-direction@1.1.0(@types/react@18.3.1)(react@18.3.1)': dependencies: @@ -26061,6 +26305,20 @@ snapshots: '@types/react': 18.2.69 '@types/react-dom': 18.2.7 + '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.69)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.69 + '@types/react-dom': 18.2.7 + '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -26075,18 +26333,18 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.2.7 - '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-focus-guards@1.0.0(react@18.2.0)': @@ -26101,6 +26359,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.69)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.69 + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -26141,16 +26406,16 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.2.7 - '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-icons@1.3.0(react@18.3.1)': @@ -26177,6 +26442,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + '@radix-ui/react-id@1.0.1(@types/react@18.2.69)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.69)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.69 + '@radix-ui/react-id@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -26222,28 +26495,28 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.69)(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.2.0(react@18.3.1) - react-remove-scroll: 2.5.5(@types/react@18.3.1)(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.2.69)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-popper@1.1.1(@types/react@18.2.69)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -26264,42 +26537,42 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@floating-ui/react-dom': 2.0.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-rect': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.69)(react@18.3.1) '@radix-ui/rect': 1.0.1 react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 - '@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@floating-ui/react-dom': 2.0.9(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-rect': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.69)(react@18.3.1) '@radix-ui/rect': 1.0.1 react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-portal@1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -26319,6 +26592,16 @@ snapshots: '@types/react': 18.2.69 '@types/react-dom': 18.2.7 + '@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.69 + '@types/react-dom': 18.2.7 + '@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -26329,14 +26612,14 @@ snapshots: '@types/react': 18.3.1 '@types/react-dom': 18.2.7 - '@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-portal@1.1.9(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -26376,6 +26659,17 @@ snapshots: '@types/react': 18.2.69 '@types/react-dom': 18.2.7 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.7 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.69)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.69 + '@types/react-dom': 18.2.7 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 @@ -26431,6 +26725,16 @@ snapshots: '@types/react': 18.2.69 '@types/react-dom': 18.2.7 + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.7 + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.69)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.69 + '@types/react-dom': 18.2.7 + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 @@ -26554,22 +26858,22 @@ snapshots: '@types/react': 18.2.69 '@types/react-dom': 18.2.7 - '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-direction': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-scroll-area@1.2.0(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': @@ -26746,32 +27050,32 @@ snapshots: react: 18.3.1 react-dom: 18.2.0(react@18.3.1) - '@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-direction': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 - '@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-tooltip@1.0.5(@types/react@18.2.69)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -26794,25 +27098,25 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@radix-ui/react-tooltip@1.0.6(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.0.6(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0)': @@ -26832,6 +27136,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.69)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.69 + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -26871,6 +27182,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.69)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.69 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.4 @@ -26908,6 +27227,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.69)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.69)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.69 + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 @@ -26933,6 +27260,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.69)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.4 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.69 + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.1)(react@18.3.1)': dependencies: '@babel/runtime': 7.27.4 @@ -26976,13 +27310,13 @@ snapshots: '@radix-ui/rect': 1.0.0 react: 18.2.0 - '@radix-ui/react-use-rect@1.0.1(@types/react@18.3.1)(react@18.3.1)': + '@radix-ui/react-use-rect@1.0.1(@types/react@18.2.69)(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 '@radix-ui/rect': 1.0.1 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@radix-ui/react-use-size@1.0.0(react@18.2.0)': dependencies: @@ -26998,13 +27332,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 - '@radix-ui/react-use-size@1.0.1(@types/react@18.3.1)(react@18.3.1)': + '@radix-ui/react-use-size@1.0.1(@types/react@18.2.69)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.7 - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@radix-ui/react-visually-hidden@1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: @@ -27013,14 +27347,14 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.4 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.2.0(react@18.3.1) optionalDependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@radix-ui/rect@1.0.0': @@ -28090,7 +28424,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@react-email/components@0.0.17(@types/react@18.3.1)(react@18.3.1)': + '@react-email/components@0.0.17(@types/react@18.2.69)(react@18.3.1)': dependencies: '@react-email/body': 0.0.8(react@18.3.1) '@react-email/button': 0.0.15(react@18.3.1) @@ -28100,7 +28434,7 @@ snapshots: '@react-email/container': 0.0.12(react@18.3.1) '@react-email/font': 0.0.6(react@18.3.1) '@react-email/head': 0.0.8(react@18.3.1) - '@react-email/heading': 0.0.12(@types/react@18.3.1)(react@18.3.1) + '@react-email/heading': 0.0.12(@types/react@18.2.69)(react@18.3.1) '@react-email/hr': 0.0.8(react@18.3.1) '@react-email/html': 0.0.8(react@18.3.1) '@react-email/img': 0.0.8(react@18.3.1) @@ -28147,9 +28481,9 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@react-email/heading@0.0.12(@types/react@18.3.1)(react@18.3.1)': + '@react-email/heading@0.0.12(@types/react@18.2.69)(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.69)(react@18.3.1) react: 18.3.1 transitivePeerDependencies: - '@types/react' @@ -29204,12 +29538,14 @@ snapshots: '@rushstack/eslint-patch@1.2.0': {} - '@s2-dev/streamstore@0.22.5(supports-color@10.0.0)': + '@s2-dev/streamstore@0.17.3(typescript@5.5.4)': + dependencies: + '@protobuf-ts/runtime': 2.11.1 + typescript: 5.5.4 + + '@s2-dev/streamstore@0.17.6': dependencies: '@protobuf-ts/runtime': 2.11.1 - debug: 4.4.3(supports-color@10.0.0) - transitivePeerDependencies: - - supports-color '@sec-ant/readable-stream@0.4.1': {} @@ -30571,7 +30907,7 @@ snapshots: '@tailwindcss/node': 4.0.17 '@tailwindcss/oxide': 4.0.17 lightningcss: 1.29.2 - postcss: 8.5.3 + postcss: 8.5.6 tailwindcss: 4.0.17 '@tailwindcss/typography@0.5.9(tailwindcss@3.4.1)': @@ -31089,7 +31425,7 @@ snapshots: '@types/react-dom@18.2.7': dependencies: - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom@19.0.4(@types/react@19.0.12)': dependencies: @@ -31114,7 +31450,11 @@ snapshots: '@types/react@19.0.12': dependencies: - csstype: 3.1.3 + csstype: 3.2.0 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 '@types/readable-stream@4.0.14': dependencies: @@ -31290,7 +31630,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) eslint: 8.31.0 tsutils: 3.21.0(typescript@5.5.4) optionalDependencies: @@ -31304,7 +31644,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.59.6 '@typescript-eslint/visitor-keys': 5.59.6 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.3 @@ -31453,6 +31793,8 @@ snapshots: '@vercel/oidc@3.0.5': {} + '@vercel/oidc@3.1.0': {} + '@vercel/otel@1.13.0(@opentelemetry/api-logs@0.203.0)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0))': dependencies: '@opentelemetry/api': 1.9.0 @@ -31896,6 +32238,14 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.76 + ai@6.0.49(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.22(zod@3.25.76) + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -33048,6 +33398,8 @@ snapshots: csstype@3.2.0: {} + csstype@3.2.3: {} + csv-generate@3.4.3: {} csv-parse@4.16.3: {} @@ -33315,9 +33667,11 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1: + debug@4.4.1(supports-color@10.0.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.0.0 debug@4.4.3(supports-color@10.0.0): dependencies: @@ -33677,7 +34031,7 @@ snapshots: enhanced-resolve@5.15.0: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 + tapable: 2.3.0 enhanced-resolve@5.18.3: dependencies: @@ -34169,7 +34523,7 @@ snapshots: eslint: 8.31.0 eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - get-tsconfig: 4.7.2 + get-tsconfig: 4.7.6 globby: 13.2.2 is-core-module: 2.14.0 is-glob: 4.0.3 @@ -34685,7 +35039,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -35119,7 +35473,7 @@ snapshots: dependencies: basic-ftp: 5.0.3 data-uri-to-buffer: 5.0.1 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -35278,7 +35632,7 @@ snapshots: '@types/node': 20.14.14 '@types/semver': 7.5.1 chalk: 4.1.2 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) interpret: 3.1.1 semver: 7.7.3 tslib: 2.8.1 @@ -35562,7 +35916,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -35582,7 +35936,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) transitivePeerDependencies: - supports-color @@ -35947,7 +36301,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -37624,6 +37978,33 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.37.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@next/env': 15.3.3 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001754 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.3.3 + '@next/swc-darwin-x64': 15.3.3 + '@next/swc-linux-arm64-gnu': 15.3.3 + '@next/swc-linux-arm64-musl': 15.3.3 + '@next/swc-linux-x64-gnu': 15.3.3 + '@next/swc-linux-x64-musl': 15.3.3 + '@next/swc-win32-arm64-msvc': 15.3.3 + '@next/swc-win32-x64-msvc': 15.3.3 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.37.0 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@15.4.8(@opentelemetry/api@1.9.0)(@playwright/test@1.37.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.4.8 @@ -38153,7 +38534,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) get-uri: 6.0.1 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -38672,12 +39053,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.0 - postcss@8.5.3: - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.4: dependencies: nanoid: 3.3.11 @@ -38823,11 +39198,11 @@ snapshots: transitivePeerDependencies: - magicast - prisma@6.20.0-integration-next.8(@types/react@19.0.12)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4): + prisma@6.20.0-integration-next.8(@types/react@19.2.14)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4): dependencies: '@prisma/config': 6.20.0-integration-next.8(magicast@0.3.5) '@prisma/engines': 6.20.0-integration-next.8 - '@prisma/studio-core-licensed': 0.6.0(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@prisma/studio-core-licensed': 0.6.0(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) postgres: 3.4.7 optionalDependencies: typescript: 5.5.4 @@ -38912,7 +39287,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -38952,7 +39327,7 @@ snapshots: dependencies: '@puppeteer/browsers': 2.10.6 chromium-bidi: 7.2.0(devtools-protocol@0.0.1464554) - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) devtools-protocol: 0.0.1464554 typed-query-selector: 2.12.0 ws: 8.18.3(bufferutil@4.0.9) @@ -39171,15 +39546,15 @@ snapshots: dependencies: '@babel/parser': 7.24.1 '@radix-ui/colors': 1.0.1 - '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.1)(react@18.3.1) - '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@radix-ui/react-tooltip': 1.0.6(@types/react-dom@18.2.7)(@types/react@18.3.1)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) - '@react-email/components': 0.0.17(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-collapsible': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': 1.0.7(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.69)(react@18.3.1) + '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.0.6(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@react-email/components': 0.0.17(@types/react@18.2.69)(react@18.3.1) '@react-email/render': 0.0.13 '@swc/core': 1.3.101(@swc/helpers@0.5.15) - '@types/react': 18.3.1 + '@types/react': 18.2.69 '@types/react-dom': 18.2.7 '@types/webpack': 5.28.5(@swc/core@1.3.101(@swc/helpers@0.5.15))(esbuild@0.19.11) autoprefixer: 10.4.14(postcss@8.4.35) @@ -39322,6 +39697,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + react-remove-scroll-bar@2.3.8(@types/react@18.2.69)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.2.69)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.2.69 + react-remove-scroll-bar@2.3.8(@types/react@18.3.1)(react@18.3.1): dependencies: react: 18.3.1 @@ -39341,6 +39724,17 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + react-remove-scroll@2.5.5(@types/react@18.2.69)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.2.69)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.2.69)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.2.69)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.2.69)(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.69 + react-remove-scroll@2.5.5(@types/react@18.3.1)(react@18.3.1): dependencies: react: 18.3.1 @@ -39419,6 +39813,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + react-style-singleton@2.2.3(@types/react@18.2.69)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.2.69 + react-style-singleton@2.2.3(@types/react@18.3.1)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -39837,7 +40239,7 @@ snapshots: require-in-the-middle@7.1.1(supports-color@10.0.0): dependencies: - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -40465,7 +40867,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -40837,7 +41239,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3(supports-color@10.0.0) + debug: 4.4.1(supports-color@10.0.0) fast-safe-stringify: 2.1.1 form-data: 4.0.4 formidable: 3.5.1 @@ -40903,6 +41305,12 @@ snapshots: react: 19.0.0 use-sync-external-store: 1.2.2(react@19.0.0) + swr@2.2.5(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + use-sync-external-store: 1.2.2(react@19.1.0) + sync-content@2.0.1: dependencies: glob: 11.0.0 @@ -41804,6 +42212,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + use-callback-ref@1.3.3(@types/react@18.2.69)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.2.69 + use-callback-ref@1.3.3(@types/react@18.3.1)(react@18.3.1): dependencies: react: 18.3.1 @@ -41825,6 +42240,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.69 + use-sidecar@1.1.3(@types/react@18.2.69)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.2.69 + use-sidecar@1.1.3(@types/react@18.3.1)(react@18.3.1): dependencies: detect-node-es: 1.1.0 @@ -41845,6 +42268,10 @@ snapshots: dependencies: react: 19.0.0 + use-sync-external-store@1.2.2(react@19.1.0): + dependencies: + react: 19.1.0 + util-deprecate@1.0.2: {} util@0.12.5: @@ -42033,7 +42460,7 @@ snapshots: '@vitest/spy': 3.1.4 '@vitest/utils': 3.1.4 chai: 5.2.0 - debug: 4.4.1 + debug: 4.4.1(supports-color@10.0.0) expect-type: 1.2.1 magic-string: 0.30.21 pathe: 2.0.3 diff --git a/references/ai-chat/next-env.d.ts b/references/ai-chat/next-env.d.ts new file mode 100644 index 0000000000..1b3be0840f --- /dev/null +++ b/references/ai-chat/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/references/ai-chat/next.config.ts b/references/ai-chat/next.config.ts new file mode 100644 index 0000000000..cb651cdc00 --- /dev/null +++ b/references/ai-chat/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/references/ai-chat/package.json b/references/ai-chat/package.json new file mode 100644 index 0000000000..9dcab80046 --- /dev/null +++ b/references/ai-chat/package.json @@ -0,0 +1,31 @@ +{ + "name": "references-ai-chat", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "dev:trigger": "trigger dev" + }, + "dependencies": { + "@ai-sdk/openai": "^3.0.0", + "@ai-sdk/react": "^3.0.0", + "@trigger.dev/sdk": "workspace:*", + "ai": "^6.0.0", + "next": "15.3.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "zod": "3.25.76" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@trigger.dev/build": "workspace:*", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "trigger.dev": "workspace:*", + "typescript": "^5" + } +} diff --git a/references/ai-chat/postcss.config.mjs b/references/ai-chat/postcss.config.mjs new file mode 100644 index 0000000000..79bcf135dc --- /dev/null +++ b/references/ai-chat/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/references/ai-chat/src/app/actions.ts b/references/ai-chat/src/app/actions.ts new file mode 100644 index 0000000000..6d230e271a --- /dev/null +++ b/references/ai-chat/src/app/actions.ts @@ -0,0 +1,6 @@ +"use server"; + +import { createChatAccessToken } from "@trigger.dev/sdk/ai"; +import type { chat } from "@/trigger/chat"; + +export const getChatToken = async () => createChatAccessToken("ai-chat"); diff --git a/references/ai-chat/src/app/globals.css b/references/ai-chat/src/app/globals.css new file mode 100644 index 0000000000..f1d8c73cdc --- /dev/null +++ b/references/ai-chat/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/references/ai-chat/src/app/layout.tsx b/references/ai-chat/src/app/layout.tsx new file mode 100644 index 0000000000..f507028583 --- /dev/null +++ b/references/ai-chat/src/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "AI Chat — Trigger.dev", + description: "AI SDK useChat powered by Trigger.dev durable tasks", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/references/ai-chat/src/app/page.tsx b/references/ai-chat/src/app/page.tsx new file mode 100644 index 0000000000..185d84b5e9 --- /dev/null +++ b/references/ai-chat/src/app/page.tsx @@ -0,0 +1,14 @@ +import { Chat } from "@/components/chat"; + +export default function Home() { + return ( +
+
+

+ AI Chat — powered by Trigger.dev +

+ +
+
+ ); +} diff --git a/references/ai-chat/src/components/chat.tsx b/references/ai-chat/src/components/chat.tsx new file mode 100644 index 0000000000..9755e15f6e --- /dev/null +++ b/references/ai-chat/src/components/chat.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useChat } from "@ai-sdk/react"; +import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; +import { useState } from "react"; +import { getChatToken } from "@/app/actions"; +import type { chat } from "@/trigger/chat"; + +function ToolInvocation({ part }: { part: any }) { + const [expanded, setExpanded] = useState(false); + // Static tools: type is "tool-{name}", dynamic tools have toolName property + const toolName = + part.type === "dynamic-tool" + ? (part.toolName ?? "tool") + : part.type.startsWith("tool-") + ? part.type.slice(5) + : "tool"; + const state = part.state ?? "input-available"; + const args = part.input; + const result = part.output; + + const isLoading = state === "input-streaming" || state === "input-available"; + const isError = state === "output-error"; + + return ( +
+ + + {expanded && ( +
+ {args && Object.keys(args).length > 0 && ( +
+
Input
+
+                {JSON.stringify(args, null, 2)}
+              
+
+ )} + {state === "output-available" && result !== undefined && ( +
+
Output
+
+                {JSON.stringify(result, null, 2)}
+              
+
+ )} + {isError && result !== undefined && ( +
+
Error
+
+                {typeof result === "string" ? result : JSON.stringify(result, null, 2)}
+              
+
+ )} +
+ )} +
+ ); +} + +export function Chat() { + const [input, setInput] = useState(""); + + const transport = useTriggerChatTransport({ + task: "ai-chat", + accessToken: getChatToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + }); + + const { messages, sendMessage, status, error } = useChat({ + transport, + }); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!input.trim() || status === "streaming") return; + + sendMessage({ text: input }); + setInput(""); + } + + return ( +
+ {/* Messages */} +
+ {messages.length === 0 && ( +

Send a message to start chatting.

+ )} + + {messages.map((message) => ( +
+
+ {message.parts.map((part, i) => { + if (part.type === "text") { + return {part.text}; + } + + // Static tools: "tool-{toolName}", dynamic tools: "dynamic-tool" + if (part.type.startsWith("tool-") || part.type === "dynamic-tool") { + return ; + } + + return null; + })} +
+
+ ))} + + {status === "streaming" && ( +
+
+ Thinking… +
+
+ )} +
+ + {/* Error */} + {error && ( +
+ {error.message} +
+ )} + + {/* Input */} +
+ setInput(e.target.value)} + placeholder="Type a message…" + className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + /> + +
+
+ ); +} diff --git a/references/ai-chat/src/trigger/chat.ts b/references/ai-chat/src/trigger/chat.ts new file mode 100644 index 0000000000..b600977c59 --- /dev/null +++ b/references/ai-chat/src/trigger/chat.ts @@ -0,0 +1,76 @@ +import { chatTask } from "@trigger.dev/sdk/ai"; +import { streamText, convertToModelMessages, tool } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { z } from "zod"; +import os from "node:os"; + +const inspectEnvironment = tool({ + description: + "Inspect the current execution environment. Returns runtime info (Node.js/Bun/Deno version), " + + "OS details, CPU architecture, memory usage, environment variables, and platform metadata.", + inputSchema: z.object({}), + execute: async () => { + const memUsage = process.memoryUsage(); + + return { + runtime: { + name: typeof Bun !== "undefined" ? "bun" : typeof Deno !== "undefined" ? "deno" : "node", + version: process.version, + versions: { + v8: process.versions.v8, + openssl: process.versions.openssl, + modules: process.versions.modules, + }, + }, + os: { + platform: process.platform, + arch: process.arch, + release: os.release(), + type: os.type(), + hostname: os.hostname(), + uptime: `${Math.floor(os.uptime())}s`, + }, + cpus: { + count: os.cpus().length, + model: os.cpus()[0]?.model, + }, + memory: { + total: `${Math.round(os.totalmem() / 1024 / 1024)}MB`, + free: `${Math.round(os.freemem() / 1024 / 1024)}MB`, + process: { + rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`, + heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, + heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, + }, + }, + env: { + NODE_ENV: process.env.NODE_ENV, + TZ: process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone, + LANG: process.env.LANG, + }, + process: { + pid: process.pid, + cwd: process.cwd(), + execPath: process.execPath, + argv: process.argv.slice(0, 3), + }, + }; + }, +}); + +// Silence TS errors for Bun/Deno global checks +declare const Bun: unknown; +declare const Deno: unknown; + +export const chat = chatTask({ + id: "ai-chat", + run: async ({ messages }) => { + return streamText({ + model: openai("gpt-4o-mini"), + system: "You are a helpful assistant. Be concise and friendly.", + messages: await convertToModelMessages(messages), + tools: { inspectEnvironment }, + maxSteps: 3, + }); + }, +}); diff --git a/references/ai-chat/trigger.config.ts b/references/ai-chat/trigger.config.ts new file mode 100644 index 0000000000..4412bfc932 --- /dev/null +++ b/references/ai-chat/trigger.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "@trigger.dev/sdk"; + +export default defineConfig({ + project: process.env.TRIGGER_PROJECT_REF!, + dirs: ["./src/trigger"], + maxDuration: 300, +}); diff --git a/references/ai-chat/tsconfig.json b/references/ai-chat/tsconfig.json new file mode 100644 index 0000000000..c1334095f8 --- /dev/null +++ b/references/ai-chat/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}