diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index e9994711d..134ec99c1 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -9,8 +9,8 @@ import { 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 type { ModelMessage, UIMessage } from "ai"; +import { convertToModelMessages, dynamicTool, jsonSchema, JSONSchema7, Schema, Tool, ToolCallOptions, zodSchema } from "ai"; import { auth } from "./auth.js"; import { metadata } from "./metadata.js"; import { streams } from "./streams.js"; @@ -182,12 +182,17 @@ type ChatTaskWirePayload = { /** * The payload shape passed to the `chatTask` run function. * - * The `metadata` field from the AI SDK transport is exposed as `clientData` - * to avoid confusion with Trigger.dev's run metadata. + * - `messages` contains model-ready messages (converted via `convertToModelMessages`) — + * pass these directly to `streamText`. + * - `uiMessages` contains the raw `UIMessage[]` from the frontend. + * - `clientData` contains custom data from the frontend (the `metadata` field from `sendMessage()`). */ -export type ChatTaskPayload = { - /** The conversation messages */ - messages: TMessage[]; +export type ChatTaskPayload = { + /** Model-ready messages — pass directly to `streamText({ messages })`. */ + messages: ModelMessage[]; + + /** Raw UI messages from the frontend. */ + uiMessages: UIMessage[]; /** The unique identifier for the chat session */ chatId: string; @@ -324,7 +329,7 @@ function isReadableStream(value: unknown): value is ReadableStream { * run: async (payload: ChatTaskPayload) => { * const result = streamText({ * model: openai("gpt-4o"), - * messages: convertToModelMessages(payload.messages), + * messages: payload.messages, * }); * * await chat.pipe(result); @@ -388,7 +393,7 @@ async function pipeChat( * transport resumes the same run by sending the next message via input streams. */ export type ChatTaskOptions = Omit< - TaskOptions, + TaskOptions, "run" > & { /** @@ -452,8 +457,8 @@ export type ChatTaskOptions = Omit< * run: async ({ messages, signal }) => { * return streamText({ * model: openai("gpt-4o"), - * messages: convertToModelMessages(messages), - * abortSignal: signal, // fires on stop or run cancel + * messages, // already converted via convertToModelMessages + * abortSignal: signal, * }); * }, * }); @@ -503,14 +508,17 @@ function chatTask( pendingMessages.push(msg); }); - // Remap wire payload to user-facing payload (metadata -> clientData) - const { metadata: wireMetadata, ...restWire } = currentWirePayload; + // Convert wire payload to user-facing payload + const { metadata: wireMetadata, messages: uiMessages, ...restWire } = currentWirePayload; + const sanitized = sanitizeMessages(uiMessages); + const modelMessages = await convertToModelMessages(sanitized); try { const result = await userRun({ ...restWire, + messages: modelMessages, + uiMessages: sanitized, clientData: wireMetadata, - messages: sanitizeMessages(currentWirePayload.messages), signal: combinedSignal, cancelSignal, stopSignal, diff --git a/references/ai-chat/src/trigger/chat.ts b/references/ai-chat/src/trigger/chat.ts index 94a9d66be..bd7069f1a 100644 --- a/references/ai-chat/src/trigger/chat.ts +++ b/references/ai-chat/src/trigger/chat.ts @@ -1,5 +1,5 @@ import { chat } from "@trigger.dev/sdk/ai"; -import { streamText, convertToModelMessages, tool, stepCountIs } from "ai"; +import { streamText, tool, stepCountIs } from "ai"; import type { LanguageModel } from "ai"; import { openai } from "@ai-sdk/openai"; import { anthropic } from "@ai-sdk/anthropic"; @@ -91,7 +91,7 @@ export const aiChat = chat.task({ return streamText({ model: getModel(modelId), system: "You are a helpful assistant. Be concise and friendly.", - messages: await convertToModelMessages(messages), + messages, tools: { inspectEnvironment }, stopWhen: stepCountIs(10), abortSignal: stopSignal,