@@ -58,28 +58,14 @@ import { metadata } from "./metadata.js";
5858import type { ResolvedPrompt } from "./prompt.js" ;
5959import type { ResolvedSkill } from "./skill.js" ;
6060// Bash-skill runtime lives in `./agentSkillsRuntime.ts` (exposed as
61- // the `@trigger.dev/sdk/ai/skills-runtime` subpath) so `ai.ts`'s
62- // top-level graph stays free of `node:*` imports. The chat-agent
63- // surface in `@trigger.dev/sdk/ai` exports types like
64- // `ChatUiMessage` / `CompactionChunkData` that frontend code
65- // sometimes imports; Next.js + Webpack reject top-level node
66- // builtins in the client graph even when only the type is used.
67- //
68- // The load path uses a computed-string variable so bundlers skip
69- // static tracing — webpack emits a "Critical dependency" info-level
70- // warning and defers resolution to runtime. On a server worker the
71- // relative path lands next to `ai.js` in the emitted dist; on a
72- // client bundle the bash tool is never invoked so the dynamic
73- // import never fires.
74- type BashRuntimeModule = typeof import ( "./agentSkillsRuntime.js" ) ;
75-
76- let cachedBashRuntime : BashRuntimeModule | undefined ;
77- async function loadAgentSkillsRuntime ( ) : Promise < BashRuntimeModule > {
78- if ( cachedBashRuntime ) return cachedBashRuntime ;
79- const modulePath : string = "./agentSkillsRuntime.js" ;
80- cachedBashRuntime = ( await import ( modulePath ) ) as BashRuntimeModule ;
81- return cachedBashRuntime ;
82- }
61+ // the `@trigger.dev/sdk/ai/skills-runtime` subpath). It's a normal
62+ // static import — `ai.ts` is server-only by reachability now that
63+ // browser-side primitives (PENDING_MESSAGE_INJECTED_TYPE and the
64+ // chat-task wire types) live in `./ai-shared.ts`. Any browser bundle
65+ // that wants those primitives imports `./ai-shared.js` directly and
66+ // never touches `ai.ts`'s module graph, so the `node:*` builtins
67+ // pulled in transitively here never reach a client chunk.
68+ import { runBashInSkill , readFileInSkill } from "./agentSkillsRuntime.js" ;
8369import { streams } from "./streams.js" ;
8470import {
8571 sessions ,
@@ -801,69 +787,12 @@ async function withChatWriter<T>(fn: (writer: ChatWriter) => Promise<T> | T): Pr
801787 return result ;
802788}
803789
804- /**
805- * The wire payload shape sent by `TriggerChatTransport`.
806- * Uses `metadata` to match the AI SDK's `ChatRequestOptions` field name.
807- */
808- export type ChatTaskWirePayload < TMessage extends UIMessage = UIMessage , TMetadata = unknown > = {
809- messages : TMessage [ ] ;
810- chatId : string ;
811- trigger : "submit-message" | "regenerate-message" | "preload" | "close" | "action" ;
812- messageId ?: string ;
813- metadata ?: TMetadata ;
814- /** Custom action payload when `trigger` is `"action"`. Validated against `actionSchema` on the backend. */
815- action ?: unknown ;
816- /** Whether this run is continuing an existing chat whose previous run ended. */
817- continuation ?: boolean ;
818- /** The run ID of the previous run (only set when `continuation` is true). */
819- previousRunId ?: string ;
820- /** Override idle timeout for this run (seconds). Set by transport.preload(). */
821- idleTimeoutInSeconds ?: number ;
822- /**
823- * The friendlyId of the Session primitive backing this chat. The
824- * transport opens (or lazy-creates) the session with
825- * `externalId = chatId` on first message, then sends this friendlyId
826- * through to the run so the agent can attach to `.in` / `.out`
827- * without needing to round-trip through the control plane again.
828- * Optional for backward-compat while the migration is in flight;
829- * required once the legacy run-scoped stream path is removed.
830- */
831- sessionId ?: string ;
832- /**
833- * Client-side `chat.store` value sent by the transport. Applied at turn
834- * start before `run()` fires, overwriting any in-memory store value on the
835- * agent (last-write-wins).
836- *
837- * The transport queues this via `setStore` / `applyStorePatch` and flushes
838- * it with the next `sendMessage`. On the agent you typically don't read
839- * this directly — it's applied into `chat.store` transparently.
840- */
841- incomingStore ?: unknown ;
842- } ;
843-
844- /**
845- * A single record on a chat Session's `.in` channel. The transport and
846- * the agent agree on this tagged shape so one Session channel carries
847- * all the signals the old three-stream split did (`chat-messages`,
848- * `chat-stop`, plus action messages piggybacked on `chat-messages`).
849- *
850- * The agent subscribes via `session.in.on` / `.waitWithIdleTimeout`
851- * inside `chatAgent()` and dispatches on `kind`.
852- */
853- export type ChatInputChunk < TMessage extends UIMessage = UIMessage , TMetadata = unknown > =
854- | {
855- kind : "message" ;
856- /**
857- * Full wire payload for a new user message or regeneration. Mirrors
858- * what the legacy `chat-messages` input stream carried.
859- */
860- payload : ChatTaskWirePayload < TMessage , TMetadata > ;
861- }
862- | {
863- kind : "stop" ;
864- /** Optional human-readable reason. Maps to the legacy `chat-stop` record. */
865- message ?: string ;
866- } ;
790+ // `ChatTaskWirePayload` and `ChatInputChunk` live in `./ai-shared.ts` so
791+ // browser bundles (which import them via `chat-client.ts` / `chat.ts`)
792+ // can pull the types without dragging `ai.ts` into the client graph.
793+ // Re-exported here so `@trigger.dev/sdk/ai` consumers see them.
794+ import type { ChatTaskWirePayload , ChatInputChunk } from "./ai-shared.js" ;
795+ export type { ChatTaskWirePayload , ChatInputChunk } from "./ai-shared.js" ;
867796
868797/**
869798 * The payload shape passed to the `chatAgent` run function.
@@ -1547,7 +1476,12 @@ export type PendingMessagesOptions<TUIM extends UIMessage = UIMessage> = {
15471476 * between tool-call steps. The frontend can match on this to render
15481477 * injection points inline in the assistant response.
15491478 */
1550- export const PENDING_MESSAGE_INJECTED_TYPE = "data-pending-message-injected" as const ;
1479+ // `PENDING_MESSAGE_INJECTED_TYPE` lives in `./ai-shared.ts` so the chat
1480+ // React hooks (`@trigger.dev/sdk/chat/react`) can import it without
1481+ // dragging `ai.ts` into the browser graph. Re-exported here so
1482+ // `@trigger.dev/sdk/ai` consumers still see it.
1483+ export { PENDING_MESSAGE_INJECTED_TYPE } from "./ai-shared.js" ;
1484+ import { PENDING_MESSAGE_INJECTED_TYPE } from "./ai-shared.js" ;
15511485
15521486/** @internal */
15531487type SteeringQueueEntry = { uiMessage : UIMessage ; modelMessages : ModelMessage [ ] } ;
@@ -2240,7 +2174,6 @@ function getChatPrompt(): ChatPromptValue {
22402174/** @internal */
22412175const chatSkillsKey = locals . create < ResolvedSkill [ ] > ( "chat.skills" ) ;
22422176
2243-
22442177/**
22452178 * Store resolved skills for the current run. Call from any hook
22462179 * (`onPreload`, `onChatStart`, `onTurnStart`) or `run()`.
@@ -2336,7 +2269,6 @@ export function buildSkillTools(skills: ResolvedSkill[]): Record<string, Tool> {
23362269 return { error : `Skill "${ skillName } " not found.` } ;
23372270 }
23382271 try {
2339- const { readFileInSkill } = await loadAgentSkillsRuntime ( ) ;
23402272 return await readFileInSkill ( {
23412273 skillPath : skill . path ,
23422274 relativePath : relPath ,
@@ -2371,7 +2303,6 @@ export function buildSkillTools(skills: ResolvedSkill[]): Record<string, Tool> {
23712303 return { error : `Skill "${ skillName } " not found.` } ;
23722304 }
23732305 try {
2374- const { runBashInSkill } = await loadAgentSkillsRuntime ( ) ;
23752306 return await runBashInSkill ( {
23762307 skillPath : skill . path ,
23772308 command,
@@ -3595,7 +3526,7 @@ function chatCustomAgent<
35953526> (
35963527 options : ChatCustomAgentOptions < TIdentifier , TClientDataSchema , TUIMessage >
35973528) : Task < TIdentifier , ChatTaskWirePayload < TUIMessage , inferSchemaIn < TClientDataSchema > > , unknown > {
3598- const { clientDataSchema, ...restOptions } = options ;
3529+ const { clientDataSchema, run : userRun , ...restOptions } = options ;
35993530
36003531 const task = createTask <
36013532 TIdentifier ,
@@ -3605,6 +3536,20 @@ function chatCustomAgent<
36053536 ...restOptions ,
36063537 triggerSource : "agent" ,
36073538 agentConfig : { type : "ai-sdk-chat" } ,
3539+ run : async (
3540+ payload : ChatTaskWirePayload < TUIMessage , inferSchemaIn < TClientDataSchema > > ,
3541+ runOptions
3542+ ) => {
3543+ // Bind the run to its backing Session so module-level helpers
3544+ // (chat.messages, chat.stream, chat.createStopSignal, chat.createSession)
3545+ // resolve to this chat's `.in` / `.out` channels — same setup as
3546+ // chat.agent. Without this, any helper that calls getChatSession()
3547+ // throws "session handle is not initialized".
3548+ const sessionIdForHandle = payload . sessionId ?? payload . chatId ;
3549+ locals . set ( chatSessionHandleKey , sessions . open ( sessionIdForHandle ) ) ;
3550+ locals . set ( chatAgentRunContextKey , runOptions . ctx ) ;
3551+ return userRun ( payload , runOptions ) ;
3552+ } ,
36083553 } ) ;
36093554
36103555 // Register clientDataSchema so the CLI converts it to JSONSchema
@@ -4553,10 +4498,17 @@ function chatAgent<
45534498
45544499 // Capture token usage from the streamText result (if available).
45554500 // totalUsage is a PromiseLike that resolves after the stream is consumed.
4501+ // Race with a 2s timeout — on stop-abort the AI SDK's totalUsage
4502+ // promise can hang indefinitely (the underlying provider stream
4503+ // never reports final usage), which would block the turn loop
4504+ // from ever firing onTurnComplete / writeTurnComplete.
45564505 let turnUsage : LanguageModelUsage | undefined ;
45574506 if ( runResult != null && typeof ( runResult as any ) . totalUsage ?. then === "function" ) {
45584507 try {
4559- turnUsage = await ( runResult as any ) . totalUsage ;
4508+ turnUsage = ( await Promise . race ( [
4509+ ( runResult as any ) . totalUsage ,
4510+ new Promise < undefined > ( ( r ) => setTimeout ( ( ) => r ( undefined ) , 2_000 ) ) ,
4511+ ] ) ) as LanguageModelUsage | undefined ;
45604512 } catch {
45614513 /* non-fatal — usage capture failed */
45624514 }
@@ -6882,32 +6834,12 @@ function chatLocal<T extends Record<string, unknown>>(options: { id: string }):
68826834 * // { model?: string; userId: string }
68836835 * ```
68846836 */
6885- export type InferChatClientData < TTask extends AnyTask > = TTask extends Task <
6886- string ,
6887- ChatTaskWirePayload < any , infer TMetadata > ,
6888- any
6889- >
6890- ? TMetadata
6891- : unknown ;
6892-
6893- /**
6894- * Extracts the UI message type from a chat task (wire payload `messages` items).
6895- *
6896- * @example
6897- * ```ts
6898- * import type { InferChatUIMessage } from "@trigger.dev/sdk/ai";
6899- * import type { myChat } from "@/trigger/chat";
6900- *
6901- * type Msg = InferChatUIMessage<typeof myChat>;
6902- * ```
6903- */
6904- export type InferChatUIMessage < TTask extends AnyTask > = TTask extends Task <
6905- string ,
6906- ChatTaskWirePayload < infer TUIM extends UIMessage , any > ,
6907- any
6908- >
6909- ? TUIM
6910- : UIMessage ;
6837+ // `InferChatClientData` and `InferChatUIMessage` live in `./ai-shared.ts`
6838+ // so the chat React hooks can import them without dragging `ai.ts` into
6839+ // the browser graph. Re-exported here so `@trigger.dev/sdk/ai` consumers
6840+ // still see them.
6841+ import type { InferChatClientData , InferChatUIMessage } from "./ai-shared.js" ;
6842+ export type { InferChatClientData , InferChatUIMessage } from "./ai-shared.js" ;
69116843
69126844/**
69136845 * Options for {@link createChatTriggerAction}.
@@ -7124,6 +7056,14 @@ export const chat = {
71247056 compact : chatCompact ,
71257057 /** Read the current compaction state (summary + base message count). */
71267058 getCompactionState,
7059+ /**
7060+ * The friendlyId (`session_*`) of the backing Session for the current chat.agent run.
7061+ * Useful for persisting alongside `runId` so reloads can resume the same session.
7062+ * Throws if called outside a chat.agent `run()` or hook.
7063+ */
7064+ get sessionId ( ) : string {
7065+ return getChatSession ( ) . id ;
7066+ } ,
71277067} ;
71287068
71297069/**
0 commit comments