refactor(sdk): extract browser-safe chat types into ai-shared

Pulls PENDING_MESSAGE_INJECTED_TYPE, ChatTaskWirePayload, and the
client-data inference helpers out of ai.ts (~7000 lines, statically
imports node:* via the skills runtime) into a new ai-shared.ts that
stays free of node-only imports. chat.ts and chat-react.ts now reach
for these via ai-shared so browser bundlers don't trace ai.ts's entire
module graph (Turbopack rejected the node: builtins outright).
This commit is contained in:
Eric Allam
2026-04-25 21:20:00 +01:00
parent 3f62880da4
commit 3d2483a4de
3 changed files with 126 additions and 2 deletions
+124
View File
@@ -0,0 +1,124 @@
/**
* Browser-safe primitives shared between `@trigger.dev/sdk/ai` (server) and
* `@trigger.dev/sdk/chat` / `@trigger.dev/sdk/chat/react` (client).
*
* This module exists to keep `ai.ts` reachable only from the server graph.
* `ai.ts` weighs in at ~7000 lines and statically imports the agent-skills
* runtime (which uses `node:child_process` / `node:fs/promises`). When a
* browser bundle imports a runtime value from `ai.ts` — historically the
* `PENDING_MESSAGE_INJECTED_TYPE` constant in `chat-react.ts` — the bundler
* traces `ai.ts`'s entire module graph into the client chunk and hits the
* `node:` builtins, which Turbopack rejects outright (and webpack flags as
* a "Critical dependency" warning).
*
* Anything in this file MUST stay free of `node:*` imports and free of any
* import from `ai.ts`.
*/
import type { Task, AnyTask } from "@trigger.dev/core/v3";
import type { UIMessage } from "ai";
/**
* Message-part `type` value for the pending-message data part the agent
* injects when a follow-up message arrives mid-turn.
*/
export const PENDING_MESSAGE_INJECTED_TYPE = "data-pending-message-injected" as const;
/**
* The wire payload shape sent by `TriggerChatTransport`.
* Uses `metadata` to match the AI SDK's `ChatRequestOptions` field name.
*/
export type ChatTaskWirePayload<TMessage extends UIMessage = UIMessage, TMetadata = unknown> = {
messages: TMessage[];
chatId: string;
trigger: "submit-message" | "regenerate-message" | "preload" | "close" | "action";
messageId?: string;
metadata?: TMetadata;
/** Custom action payload when `trigger` is `"action"`. Validated against `actionSchema` on the backend. */
action?: unknown;
/** Whether this run is continuing an existing chat whose previous run ended. */
continuation?: boolean;
/** The run ID of the previous run (only set when `continuation` is true). */
previousRunId?: string;
/** Override idle timeout for this run (seconds). Set by transport.preload(). */
idleTimeoutInSeconds?: number;
/**
* The friendlyId of the Session primitive backing this chat. The
* transport opens (or lazy-creates) the session with
* `externalId = chatId` on first message, then sends this friendlyId
* through to the run so the agent can attach to `.in` / `.out`
* without needing to round-trip through the control plane again.
* Optional for backward-compat while the migration is in flight;
* required once the legacy run-scoped stream path is removed.
*/
sessionId?: string;
/**
* Client-side `chat.store` value sent by the transport. Applied at turn
* start before `run()` fires, overwriting any in-memory store value on the
* agent (last-write-wins).
*
* The transport queues this via `setStore` / `applyStorePatch` and flushes
* it with the next `sendMessage`. On the agent you typically don't read
* this directly — it's applied into `chat.store` transparently.
*/
incomingStore?: unknown;
};
/**
* One chunk on the chat input stream. `kind` discriminates the variants —
* a single ordered stream now carries all the signals the old three-stream
* split did (`chat-messages`, `chat-stop`, plus action messages piggybacked
* on `chat-messages`).
*/
export type ChatInputChunk<TMessage extends UIMessage = UIMessage, TMetadata = unknown> =
| {
kind: "message";
/**
* Full wire payload for a new user message or regeneration. Mirrors
* what the legacy `chat-messages` input stream carried.
*/
payload: ChatTaskWirePayload<TMessage, TMetadata>;
}
| {
kind: "stop";
/** Optional human-readable reason. Maps to the legacy `chat-stop` record. */
message?: string;
};
/**
* Extracts the client-data (`metadata`) type from a chat task.
*
* @example
* ```ts
* import type { InferChatClientData } from "@trigger.dev/sdk/ai";
* import type { myChat } from "@/trigger/chat";
*
* type MyClientData = InferChatClientData<typeof myChat>;
* ```
*/
export type InferChatClientData<TTask extends AnyTask> = TTask extends Task<
string,
ChatTaskWirePayload<any, infer TMetadata>,
any
>
? TMetadata
: unknown;
/**
* Extracts the UI message type from a chat task (wire payload `messages` items).
*
* @example
* ```ts
* import type { InferChatUIMessage } from "@trigger.dev/sdk/ai";
* import type { myChat } from "@/trigger/chat";
*
* type Msg = InferChatUIMessage<typeof myChat>;
* ```
*/
export type InferChatUIMessage<TTask extends AnyTask> = TTask extends Task<
string,
ChatTaskWirePayload<infer TUIM extends UIMessage, any>,
any
>
? TUIM
: UIMessage;
+1 -1
View File
@@ -20,7 +20,7 @@ import type { Task } from "@trigger.dev/core/v3";
import type { UIMessage, UIMessageChunk } from "ai";
import { readUIMessageStream } from "ai";
import { ApiClient, SSEStreamSubscription, apiClientManager } from "@trigger.dev/core/v3";
import type { ChatInputChunk, ChatTaskWirePayload } from "./ai.js";
import type { ChatInputChunk, ChatTaskWirePayload } from "./ai-shared.js";
import { sessions } from "./sessions.js";
import { trigger } from "./shared.js";
+1 -1
View File
@@ -30,7 +30,7 @@ import {
PENDING_MESSAGE_INJECTED_TYPE,
type InferChatClientData,
type InferChatUIMessage,
} from "./ai.js";
} from "./ai-shared.js";
import type { UIMessage, ChatRequestOptions } from "ai";
/**