use input streams and rename chatTask and chatState to chat.task and chat.state
This commit is contained in:
@@ -15,7 +15,11 @@ 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";
|
||||
import {
|
||||
CHAT_STREAM_KEY as _CHAT_STREAM_KEY,
|
||||
CHAT_MESSAGES_STREAM_ID,
|
||||
CHAT_STOP_STREAM_ID,
|
||||
} from "./chat-constants.js";
|
||||
|
||||
const METADATA_KEY = "tool.execute.options";
|
||||
|
||||
@@ -136,13 +140,13 @@ export const ai = {
|
||||
* ```ts
|
||||
* // actions.ts
|
||||
* "use server";
|
||||
* import { createChatAccessToken } from "@trigger.dev/sdk/ai";
|
||||
* import type { chat } from "@/trigger/chat";
|
||||
* import { chat } from "@trigger.dev/sdk/ai";
|
||||
* import type { myChat } from "@/trigger/chat";
|
||||
*
|
||||
* export const getChatToken = () => createChatAccessToken<typeof chat>("ai-chat");
|
||||
* export const getChatToken = () => chat.createAccessToken<typeof myChat>("my-chat");
|
||||
* ```
|
||||
*/
|
||||
export async function createChatAccessToken<TTask extends AnyTask>(
|
||||
function createChatAccessToken<TTask extends AnyTask>(
|
||||
taskId: TaskIdentifier<TTask>
|
||||
): Promise<string> {
|
||||
return auth.createTriggerPublicToken(taskId as string, { multipleUse: true });
|
||||
@@ -157,7 +161,10 @@ export async function createChatAccessToken<TTask extends AnyTask>(
|
||||
* Both `TriggerChatTransport` (frontend) and `pipeChat`/`chatTask` (backend)
|
||||
* use this key by default.
|
||||
*/
|
||||
export const CHAT_STREAM_KEY = "chat";
|
||||
export const CHAT_STREAM_KEY = _CHAT_STREAM_KEY;
|
||||
|
||||
// Re-export input stream IDs for advanced usage
|
||||
export { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID };
|
||||
|
||||
/**
|
||||
* The payload shape that the chat transport sends to the triggered task.
|
||||
@@ -187,6 +194,28 @@ export type ChatTaskPayload<TMessage extends UIMessage = UIMessage> = {
|
||||
metadata?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Abort signals provided to the `chatTask` run function.
|
||||
*/
|
||||
export type ChatTaskSignals = {
|
||||
/** Combined signal — fires on run cancel OR stop generation. Pass to `streamText`. */
|
||||
signal: AbortSignal;
|
||||
/** Fires only when the run is cancelled, expired, or exceeds maxDuration. */
|
||||
cancelSignal: AbortSignal;
|
||||
/** Fires only when the frontend stops generation for this turn (per-turn, reset each turn). */
|
||||
stopSignal: AbortSignal;
|
||||
};
|
||||
|
||||
/**
|
||||
* The full payload passed to a `chatTask` run function.
|
||||
* Extends `ChatTaskPayload` (the wire payload) with abort signals.
|
||||
*/
|
||||
export type ChatTaskRunPayload = ChatTaskPayload & ChatTaskSignals;
|
||||
|
||||
// Input streams for bidirectional chat communication
|
||||
const messagesInput = streams.input<ChatTaskPayload>({ id: CHAT_MESSAGES_STREAM_ID });
|
||||
const stopInput = streams.input<{ stop: true; message?: string }>({ id: CHAT_STOP_STREAM_ID });
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -253,7 +282,7 @@ function isReadableStream(value: unknown): value is ReadableStream<unknown> {
|
||||
* @example
|
||||
* ```ts
|
||||
* import { task } from "@trigger.dev/sdk";
|
||||
* import { pipeChat, type ChatTaskPayload } from "@trigger.dev/sdk/ai";
|
||||
* import { chat, type ChatTaskPayload } from "@trigger.dev/sdk/ai";
|
||||
* import { streamText, convertToModelMessages } from "ai";
|
||||
*
|
||||
* export const myChatTask = task({
|
||||
@@ -264,7 +293,7 @@ function isReadableStream(value: unknown): value is ReadableStream<unknown> {
|
||||
* messages: convertToModelMessages(payload.messages),
|
||||
* });
|
||||
*
|
||||
* await pipeChat(result);
|
||||
* await chat.pipe(result);
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
@@ -274,11 +303,11 @@ function isReadableStream(value: unknown): value is ReadableStream<unknown> {
|
||||
* // 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);
|
||||
* await chat.pipe(result);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function pipeChat(
|
||||
async function pipeChat(
|
||||
source: UIMessageStreamable | AsyncIterable<unknown> | ReadableStream<unknown>,
|
||||
options?: PipeChatOptions
|
||||
): Promise<void> {
|
||||
@@ -314,16 +343,15 @@ export async function pipeChat(
|
||||
* Options for defining a chat task.
|
||||
*
|
||||
* Extends the standard `TaskOptions` but pre-types the payload as `ChatTaskPayload`
|
||||
* and overrides `run` to accept `ChatTaskPayload` directly.
|
||||
* and overrides `run` to accept `ChatTaskRunPayload` (with abort signals).
|
||||
*
|
||||
* **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
|
||||
* **Single-run mode:** By default, the task uses input streams 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.
|
||||
* emits a control chunk and suspends via `messagesInput.wait()`. The frontend
|
||||
* transport resumes the same run by sending the next message via input streams.
|
||||
*/
|
||||
export type ChatTaskOptions<TIdentifier extends string> = Omit<
|
||||
TaskOptions<TIdentifier, ChatTaskPayload, unknown>,
|
||||
@@ -332,13 +360,13 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<
|
||||
/**
|
||||
* The run function for the chat task.
|
||||
*
|
||||
* Receives a `ChatTaskPayload` with the conversation messages, chat session ID,
|
||||
* and trigger type.
|
||||
* Receives a `ChatTaskRunPayload` with the conversation messages, chat session ID,
|
||||
* trigger type, and abort signals (`signal`, `cancelSignal`, `stopSignal`).
|
||||
*
|
||||
* **Auto-piping:** If this function returns a value with `.toUIMessageStream()`,
|
||||
* the stream is automatically piped to the frontend.
|
||||
*/
|
||||
run: (payload: ChatTaskPayload) => Promise<unknown>;
|
||||
run: (payload: ChatTaskRunPayload) => Promise<unknown>;
|
||||
|
||||
/**
|
||||
* Maximum number of conversational turns (message round-trips) a single run
|
||||
@@ -351,7 +379,7 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<
|
||||
|
||||
/**
|
||||
* 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"`).
|
||||
* Accepts any duration string (e.g. `"1h"`, `"30m"`).
|
||||
*
|
||||
* @default "1h"
|
||||
*/
|
||||
@@ -361,87 +389,164 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<
|
||||
/**
|
||||
* Creates a Trigger.dev task pre-configured for AI SDK chat.
|
||||
*
|
||||
* - **Pre-types the payload** as `ChatTaskPayload` — no manual typing needed
|
||||
* - **Pre-types the payload** as `ChatTaskRunPayload` — includes abort signals
|
||||
* - **Auto-pipes the stream** if `run` returns a `StreamTextResult`
|
||||
* - **Multi-turn**: keeps the conversation in a single run using input streams
|
||||
* - **Stop support**: frontend can stop generation mid-stream via the stop input stream
|
||||
* - For complex flows, use `pipeChat()` from anywhere inside your task code
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { chatTask } from "@trigger.dev/sdk/ai";
|
||||
* import { chat } 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 }) => {
|
||||
* export const myChat = chat.task({
|
||||
* id: "my-chat",
|
||||
* run: async ({ messages, signal }) => {
|
||||
* return streamText({
|
||||
* model: openai("gpt-4o"),
|
||||
* messages: convertToModelMessages(messages),
|
||||
* abortSignal: signal, // fires on stop or run cancel
|
||||
* });
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @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<TIdentifier extends string>(
|
||||
function chatTask<TIdentifier extends string>(
|
||||
options: ChatTaskOptions<TIdentifier>
|
||||
): Task<TIdentifier, ChatTaskPayload, unknown> {
|
||||
const { run: userRun, maxTurns = 100, turnTimeout = "1h", ...restOptions } = options;
|
||||
|
||||
return createTask<TIdentifier, ChatTaskPayload, unknown>({
|
||||
...restOptions,
|
||||
run: async (payload: ChatTaskPayload) => {
|
||||
run: async (payload: ChatTaskPayload, { signal: runSignal }) => {
|
||||
let currentPayload = payload;
|
||||
|
||||
for (let turn = 0; turn < maxTurns; turn++) {
|
||||
_chatPipeCount = 0;
|
||||
// Mutable reference to the current turn's stop controller so the
|
||||
// stop input stream listener (registered once) can abort the right turn.
|
||||
let currentStopController: AbortController | undefined;
|
||||
|
||||
const result = await userRun(currentPayload);
|
||||
// Listen for stop signals for the lifetime of the run
|
||||
const stopSub = stopInput.on((data) => {
|
||||
currentStopController?.abort(data?.message || "stopped");
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
try {
|
||||
for (let turn = 0; turn < maxTurns; turn++) {
|
||||
_chatPipeCount = 0;
|
||||
|
||||
// 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 });
|
||||
// Per-turn stop controller (reset each turn)
|
||||
const stopController = new AbortController();
|
||||
currentStopController = stopController;
|
||||
|
||||
const { waitUntilComplete } = streams.writer(CHAT_STREAM_KEY, {
|
||||
execute: ({ write }) => {
|
||||
write({
|
||||
type: "__trigger_waitpoint_ready",
|
||||
tokenId: token.id,
|
||||
publicAccessToken: token.publicAccessToken,
|
||||
// Three signals for the user's run function
|
||||
const stopSignal = stopController.signal;
|
||||
const cancelSignal = runSignal;
|
||||
const combinedSignal = AbortSignal.any([runSignal, stopController.signal]);
|
||||
|
||||
// Buffer messages that arrive during streaming
|
||||
const pendingMessages: ChatTaskPayload[] = [];
|
||||
const msgSub = messagesInput.on((msg) => {
|
||||
pendingMessages.push(msg as ChatTaskPayload);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await userRun({
|
||||
...currentPayload,
|
||||
signal: combinedSignal,
|
||||
cancelSignal,
|
||||
stopSignal,
|
||||
});
|
||||
},
|
||||
});
|
||||
await waitUntilComplete();
|
||||
|
||||
// Pause until the frontend completes the token with the next message
|
||||
const next = await wait.forToken<ChatTaskPayload>(token);
|
||||
// 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, { signal: combinedSignal });
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle AbortError from streamText gracefully
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
if (runSignal.aborted) {
|
||||
return; // Full run cancellation — exit
|
||||
}
|
||||
// Stop generation — fall through to continue the loop
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
msgSub.off();
|
||||
}
|
||||
|
||||
if (!next.ok) {
|
||||
// Timed out waiting for the next message — end the conversation
|
||||
return;
|
||||
if (runSignal.aborted) return;
|
||||
|
||||
// Write turn-complete control chunk so frontend closes its stream
|
||||
await writeTurnCompleteChunk();
|
||||
|
||||
// If messages arrived during streaming, use the first one immediately
|
||||
if (pendingMessages.length > 0) {
|
||||
currentPayload = pendingMessages[0]!;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Suspend the task (frees compute) until the next message arrives
|
||||
const next = await messagesInput.wait({ timeout: turnTimeout });
|
||||
|
||||
if (!next.ok) {
|
||||
// Timed out waiting for the next message — end the conversation
|
||||
return;
|
||||
}
|
||||
|
||||
currentPayload = next.output as ChatTaskPayload;
|
||||
}
|
||||
|
||||
currentPayload = next.output;
|
||||
} finally {
|
||||
stopSub.off();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespace for AI SDK chat integration.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { chat } from "@trigger.dev/sdk/ai";
|
||||
*
|
||||
* // Define a chat task
|
||||
* export const myChat = chat.task({
|
||||
* id: "my-chat",
|
||||
* run: async ({ messages, signal }) => {
|
||||
* return streamText({ model, messages, abortSignal: signal });
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Pipe a stream manually (from inside a task)
|
||||
* await chat.pipe(streamTextResult);
|
||||
*
|
||||
* // Create an access token (from a server action)
|
||||
* const token = await chat.createAccessToken("my-chat");
|
||||
* ```
|
||||
*/
|
||||
export const chat = {
|
||||
/** Create a chat task. See {@link chatTask}. */
|
||||
task: chatTask,
|
||||
/** Pipe a stream to the chat transport. See {@link pipeChat}. */
|
||||
pipe: pipeChat,
|
||||
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
|
||||
createAccessToken: createChatAccessToken,
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a turn-complete control chunk to the chat output stream.
|
||||
* The frontend transport intercepts this to close the ReadableStream for the current turn.
|
||||
* @internal
|
||||
*/
|
||||
async function writeTurnCompleteChunk(): Promise<void> {
|
||||
const { waitUntilComplete } = streams.writer(CHAT_STREAM_KEY, {
|
||||
execute: ({ write }) => {
|
||||
write({ type: "__trigger_turn_complete" });
|
||||
},
|
||||
});
|
||||
await waitUntilComplete();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Stream IDs used for bidirectional chat communication.
|
||||
* Shared between backend (ai.ts) and frontend (chat.ts).
|
||||
*/
|
||||
|
||||
/** The output stream key where UIMessageChunks are written. */
|
||||
export const CHAT_STREAM_KEY = "chat";
|
||||
|
||||
/** Input stream ID for sending chat messages to the running task. */
|
||||
export const CHAT_MESSAGES_STREAM_ID = "chat-messages";
|
||||
|
||||
/** Input stream ID for sending stop signals to abort the current generation. */
|
||||
export const CHAT_STOP_STREAM_ID = "chat-stop";
|
||||
@@ -51,7 +51,7 @@ export type UseTriggerChatTransportOptions<TTask extends AnyTask = AnyTask> = Om
|
||||
*
|
||||
* 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.)
|
||||
* transport's internal session state (run IDs, lastEventId, etc.)
|
||||
* is preserved across re-renders.
|
||||
*
|
||||
* For dynamic access tokens, pass a function — it will be called on each
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai";
|
||||
import { ApiClient, SSEStreamSubscription } from "@trigger.dev/core/v3";
|
||||
import { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID } from "./chat-constants.js";
|
||||
|
||||
const DEFAULT_STREAM_KEY = "chat";
|
||||
const DEFAULT_BASE_URL = "https://api.trigger.dev";
|
||||
@@ -88,10 +89,6 @@ export type TriggerChatTransportOptions = {
|
||||
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;
|
||||
};
|
||||
@@ -100,44 +97,29 @@ type ChatSessionState = {
|
||||
* 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
|
||||
* 1. Triggers a Trigger.dev task (or sends to an existing run via input streams)
|
||||
* 2. Subscribes to the task's realtime stream to receive `UIMessageChunk` data
|
||||
* 3. Returns a `ReadableStream<UIMessageChunk>` that the AI SDK processes natively
|
||||
*
|
||||
* Calling `stop()` from `useChat` sends a stop signal via input streams, which
|
||||
* aborts the current `streamText` call in the task without ending the run.
|
||||
*
|
||||
* @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({
|
||||
* const { messages, sendMessage, stop, status } = useChat({
|
||||
* transport: new TriggerChatTransport({
|
||||
* task: "my-chat-task",
|
||||
* accessToken,
|
||||
* }),
|
||||
* });
|
||||
*
|
||||
* // ... render messages
|
||||
* // stop() sends a stop signal — the task aborts streamText but keeps the run alive
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 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<UIMessage> {
|
||||
private readonly taskId: string;
|
||||
@@ -183,19 +165,12 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
|
||||
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;
|
||||
|
||||
// If we have an existing run, send the message via input stream
|
||||
// to resume the conversation in the same run.
|
||||
if (session?.runId) {
|
||||
try {
|
||||
const wpClient = new ApiClient(this.baseURL, tokenAccessToken);
|
||||
await wpClient.completeWaitpointToken(tokenId, { data: payload });
|
||||
const apiClient = new ApiClient(this.baseURL, session.publicAccessToken);
|
||||
await apiClient.sendInputStream(session.runId, CHAT_MESSAGES_STREAM_ID, payload);
|
||||
|
||||
return this.subscribeToStream(
|
||||
session.runId,
|
||||
@@ -204,12 +179,12 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
chatId
|
||||
);
|
||||
} catch {
|
||||
// If completing the waitpoint fails (run died, token expired, etc.),
|
||||
// fall through to trigger a new run.
|
||||
// If sending fails (run died, etc.), fall through to trigger a new run.
|
||||
this.sessions.delete(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
// First message or run has ended — trigger a new run
|
||||
const currentToken = await this.resolveAccessToken();
|
||||
const apiClient = new ApiClient(this.baseURL, currentToken);
|
||||
|
||||
@@ -263,7 +238,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
...this.extraHeaders,
|
||||
};
|
||||
|
||||
// When resuming a run via waitpoint, skip past previously-seen events
|
||||
// When resuming a run, skip past previously-seen events
|
||||
// so we only receive the new turn's response.
|
||||
const session = chatId ? this.sessions.get(chatId) : undefined;
|
||||
|
||||
@@ -275,6 +250,24 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
? AbortSignal.any([abortSignal, internalAbort.signal])
|
||||
: internalAbort.signal;
|
||||
|
||||
// When the caller aborts (user calls stop()), send a stop signal to the
|
||||
// running task via input streams, then close the SSE connection.
|
||||
if (abortSignal) {
|
||||
abortSignal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
if (session?.runId) {
|
||||
const api = new ApiClient(this.baseURL, session.publicAccessToken);
|
||||
api
|
||||
.sendInputStream(session.runId, CHAT_STOP_STREAM_ID, { stop: true })
|
||||
.catch(() => {}); // Best-effort
|
||||
}
|
||||
internalAbort.abort();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
const subscription = new SSEStreamSubscription(
|
||||
`${this.baseURL}/realtime/v1/streams/${runId}/${this.streamKey}`,
|
||||
{
|
||||
@@ -300,11 +293,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
// 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;
|
||||
}
|
||||
this.sessions.delete(chatId);
|
||||
}
|
||||
controller.close();
|
||||
return;
|
||||
@@ -326,16 +315,10 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
if (value.chunk != null && typeof value.chunk === "object") {
|
||||
const chunk = value.chunk as Record<string, unknown>;
|
||||
|
||||
// Intercept the waitpoint-ready control chunk emitted by
|
||||
// Intercept the turn-complete 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;
|
||||
}
|
||||
|
||||
if (chunk.type === "__trigger_turn_complete" && chatId) {
|
||||
// Abort the underlying fetch to close the SSE connection
|
||||
internalAbort.abort();
|
||||
try {
|
||||
@@ -391,4 +374,3 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
export function createChatTransport(options: TriggerChatTransportOptions): TriggerChatTransport {
|
||||
return new TriggerChatTransport(options);
|
||||
}
|
||||
|
||||
|
||||
Generated
+37
-43
@@ -477,8 +477,8 @@ importers:
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3(@remix-run/react@2.17.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.17.4(typescript@5.5.4))
|
||||
'@s2-dev/streamstore':
|
||||
specifier: ^0.17.2
|
||||
version: 0.17.3(typescript@5.5.4)
|
||||
specifier: ^0.22.5
|
||||
version: 0.22.5(supports-color@10.0.0)
|
||||
'@sentry/remix':
|
||||
specifier: 9.46.0
|
||||
version: 9.46.0(patch_hash=146126b032581925294aaed63ab53ce3f5e0356a755f1763d7a9a76b9846943b)(@remix-run/node@2.17.4(typescript@5.5.4))(@remix-run/react@2.17.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.17.4(typescript@5.5.4))(encoding@0.1.13)(react@18.2.0)
|
||||
@@ -1138,7 +1138,7 @@ importers:
|
||||
version: 18.3.1
|
||||
react-email:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0)
|
||||
version: 2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0)
|
||||
resend:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
@@ -1505,8 +1505,8 @@ importers:
|
||||
specifier: 1.36.0
|
||||
version: 1.36.0
|
||||
'@s2-dev/streamstore':
|
||||
specifier: ^0.17.6
|
||||
version: 0.17.6
|
||||
specifier: ^0.22.5
|
||||
version: 0.22.5(supports-color@10.0.0)
|
||||
'@trigger.dev/build':
|
||||
specifier: workspace:4.4.4
|
||||
version: link:../build
|
||||
@@ -1782,8 +1782,8 @@ importers:
|
||||
specifier: 1.36.0
|
||||
version: 1.36.0
|
||||
'@s2-dev/streamstore':
|
||||
specifier: 0.17.3
|
||||
version: 0.17.3(typescript@5.5.4)
|
||||
specifier: 0.22.5
|
||||
version: 0.22.5(supports-color@10.0.0)
|
||||
dequal:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
@@ -2108,6 +2108,9 @@ importers:
|
||||
evt:
|
||||
specifier: ^2.4.13
|
||||
version: 2.4.13
|
||||
react:
|
||||
specifier: ^18.0 || ^19.0
|
||||
version: 18.3.1
|
||||
slug:
|
||||
specifier: ^6.0.0
|
||||
version: 6.1.0
|
||||
@@ -9554,13 +9557,8 @@ packages:
|
||||
'@rushstack/eslint-patch@1.2.0':
|
||||
resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==}
|
||||
|
||||
'@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==}
|
||||
'@s2-dev/streamstore@0.22.5':
|
||||
resolution: {integrity: sha512-GqdOKIbIoIxT+40fnKzHbrsHB6gBqKdECmFe7D3Ojk4FoN1Hu0LhFzZv6ZmVMjoHHU+55debS1xSWjZwQmbIyQ==}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
@@ -25817,7 +25815,7 @@ snapshots:
|
||||
|
||||
'@puppeteer/browsers@2.10.6':
|
||||
dependencies:
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
extract-zip: 2.0.1
|
||||
progress: 2.0.3
|
||||
proxy-agent: 6.5.0
|
||||
@@ -29487,14 +29485,12 @@ snapshots:
|
||||
|
||||
'@rushstack/eslint-patch@1.2.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':
|
||||
'@s2-dev/streamstore@0.22.5(supports-color@10.0.0)':
|
||||
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': {}
|
||||
|
||||
@@ -31579,7 +31575,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.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
eslint: 8.31.0
|
||||
tsutils: 3.21.0(typescript@5.5.4)
|
||||
optionalDependencies:
|
||||
@@ -31593,7 +31589,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 5.59.6
|
||||
'@typescript-eslint/visitor-keys': 5.59.6
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.7.3
|
||||
@@ -33621,11 +33617,9 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.1(supports-color@10.0.0):
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.0.0
|
||||
|
||||
debug@4.4.3(supports-color@10.0.0):
|
||||
dependencies:
|
||||
@@ -34972,7 +34966,7 @@ snapshots:
|
||||
|
||||
extract-zip@2.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
get-stream: 5.2.0
|
||||
yauzl: 2.10.0
|
||||
optionalDependencies:
|
||||
@@ -35398,7 +35392,7 @@ snapshots:
|
||||
dependencies:
|
||||
basic-ftp: 5.0.3
|
||||
data-uri-to-buffer: 5.0.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
fs-extra: 8.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -35557,7 +35551,7 @@ snapshots:
|
||||
'@types/node': 20.14.14
|
||||
'@types/semver': 7.5.1
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
interpret: 3.1.1
|
||||
semver: 7.7.3
|
||||
tslib: 2.8.1
|
||||
@@ -35841,7 +35835,7 @@ snapshots:
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -35861,7 +35855,7 @@ snapshots:
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -38459,7 +38453,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
get-uri: 6.0.1
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
@@ -39210,7 +39204,7 @@ snapshots:
|
||||
proxy-agent@6.5.0:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
lru-cache: 7.18.3
|
||||
@@ -39250,7 +39244,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@puppeteer/browsers': 2.10.6
|
||||
chromium-bidi: 7.2.0(devtools-protocol@0.0.1464554)
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.1
|
||||
devtools-protocol: 0.0.1464554
|
||||
typed-query-selector: 2.12.0
|
||||
ws: 8.18.3(bufferutil@4.0.9)
|
||||
@@ -39465,7 +39459,7 @@ snapshots:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(bufferutil@4.0.9)(eslint@8.31.0):
|
||||
react-email@2.1.2(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.15)(eslint@8.31.0):
|
||||
dependencies:
|
||||
'@babel/parser': 7.24.1
|
||||
'@radix-ui/colors': 1.0.1
|
||||
@@ -39502,8 +39496,8 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.2.0(react@18.3.1)
|
||||
shelljs: 0.8.5
|
||||
socket.io: 4.7.3(bufferutil@4.0.9)
|
||||
socket.io-client: 4.7.3(bufferutil@4.0.9)
|
||||
socket.io: 4.7.3
|
||||
socket.io-client: 4.7.3
|
||||
sonner: 1.3.1(react-dom@18.2.0(react@18.3.1))(react@18.3.1)
|
||||
source-map-js: 1.0.2
|
||||
stacktrace-parser: 0.1.10
|
||||
@@ -40162,7 +40156,7 @@ snapshots:
|
||||
|
||||
require-in-the-middle@7.1.1(supports-color@10.0.0):
|
||||
dependencies:
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
module-details-from-path: 1.0.3
|
||||
resolve: 1.22.8
|
||||
transitivePeerDependencies:
|
||||
@@ -40728,7 +40722,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
socket.io-client@4.7.3(bufferutil@4.0.9):
|
||||
socket.io-client@4.7.3:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.0
|
||||
debug: 4.3.7(supports-color@10.0.0)
|
||||
@@ -40757,7 +40751,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
socket.io@4.7.3(bufferutil@4.0.9):
|
||||
socket.io@4.7.3:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
@@ -40788,7 +40782,7 @@ snapshots:
|
||||
socks-proxy-agent@8.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
socks: 2.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -41162,7 +41156,7 @@ snapshots:
|
||||
dependencies:
|
||||
component-emitter: 1.3.1
|
||||
cookiejar: 2.1.4
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.3(supports-color@10.0.0)
|
||||
fast-safe-stringify: 2.1.1
|
||||
form-data: 4.0.4
|
||||
formidable: 3.5.1
|
||||
@@ -42398,7 +42392,7 @@ snapshots:
|
||||
'@vitest/spy': 3.1.4
|
||||
'@vitest/utils': 3.1.4
|
||||
chai: 5.2.0
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
debug: 4.4.1
|
||||
expect-type: 1.2.1
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { createChatAccessToken } from "@trigger.dev/sdk/ai";
|
||||
import type { chat } from "@/trigger/chat";
|
||||
import { chat } from "@trigger.dev/sdk/ai";
|
||||
import type { aiChat } from "@/trigger/chat";
|
||||
|
||||
export const getChatToken = async () => createChatAccessToken<typeof chat>("ai-chat");
|
||||
export const getChatToken = async () => chat.createAccessToken<typeof aiChat>("ai-chat");
|
||||
|
||||
@@ -4,7 +4,7 @@ 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";
|
||||
import type { aiChat } from "@/trigger/chat";
|
||||
|
||||
function ToolInvocation({ part }: { part: any }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@@ -73,7 +73,7 @@ function ToolInvocation({ part }: { part: any }) {
|
||||
export function Chat() {
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
const transport = useTriggerChatTransport<typeof chat>({
|
||||
const transport = useTriggerChatTransport<typeof aiChat>({
|
||||
task: "ai-chat",
|
||||
accessToken: getChatToken,
|
||||
baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { chatTask } from "@trigger.dev/sdk/ai";
|
||||
import { chat } from "@trigger.dev/sdk/ai";
|
||||
import { streamText, convertToModelMessages, tool } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { z } from "zod";
|
||||
@@ -62,15 +62,16 @@ const inspectEnvironment = tool({
|
||||
declare const Bun: unknown;
|
||||
declare const Deno: unknown;
|
||||
|
||||
export const chat = chatTask({
|
||||
export const aiChat = chat.task({
|
||||
id: "ai-chat",
|
||||
run: async ({ messages }) => {
|
||||
run: async ({ messages, signal }) => {
|
||||
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,
|
||||
abortSignal: signal,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user