@trigger.dev/sdk 0.0.0-prerelease-20260306162926 → 0.0.0-prerelease-20260309160514

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,24 @@
1
1
  import { AnyTask, Task, type inferSchemaIn, type inferSchemaOut, type TaskIdentifier, type TaskOptions, type TaskSchema, type TaskWithSchema } from "@trigger.dev/core/v3";
2
- import type { ModelMessage, UIMessage } from "ai";
3
- import { Tool, ToolCallOptions } from "ai";
2
+ import type { ModelMessage, UIMessage, UIMessageChunk } from "ai";
3
+ import { Tool } from "ai";
4
+ import { locals } from "./locals.js";
4
5
  import { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID } from "./chat-constants.js";
5
- export type ToolCallExecutionOptions = Omit<ToolCallOptions, "abortSignal">;
6
+ export type ToolCallExecutionOptions = {
7
+ toolCallId: string;
8
+ experimental_context?: unknown;
9
+ /** Chat context — only present when the tool runs inside a chat.task turn. */
10
+ chatId?: string;
11
+ turn?: number;
12
+ continuation?: boolean;
13
+ clientData?: unknown;
14
+ };
15
+ /** Chat context stored in locals during each chat.task turn for auto-detection. */
16
+ type ChatTurnContext<TClientData = unknown> = {
17
+ chatId: string;
18
+ turn: number;
19
+ continuation: boolean;
20
+ clientData?: TClientData;
21
+ };
6
22
  type ToolResultContent = Array<{
7
23
  type: "text";
8
24
  text: string;
@@ -17,9 +33,43 @@ export type ToolOptions<TResult> = {
17
33
  declare function toolFromTask<TIdentifier extends string, TInput = void, TOutput = unknown>(task: Task<TIdentifier, TInput, TOutput>, options?: ToolOptions<TOutput>): Tool<TInput, TOutput>;
18
34
  declare function toolFromTask<TIdentifier extends string, TTaskSchema extends TaskSchema | undefined = undefined, TOutput = unknown>(task: TaskWithSchema<TIdentifier, TTaskSchema, TOutput>, options?: ToolOptions<TOutput>): Tool<inferSchemaIn<TTaskSchema>, TOutput>;
19
35
  declare function getToolOptionsFromMetadata(): ToolCallExecutionOptions | undefined;
36
+ /**
37
+ * Get the current tool call ID from inside a subtask invoked via `ai.tool()`.
38
+ * Returns `undefined` if not running as a tool subtask.
39
+ */
40
+ declare function getToolCallId(): string | undefined;
41
+ /**
42
+ * Get the chat context from inside a subtask invoked via `ai.tool()` within a `chat.task`.
43
+ * Pass `typeof yourChatTask` as the type parameter to get typed `clientData`.
44
+ * Returns `undefined` if the parent is not a chat task.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const ctx = ai.chatContext<typeof myChat>();
49
+ * // ctx?.clientData is typed based on myChat's clientDataSchema
50
+ * ```
51
+ */
52
+ declare function getToolChatContext<TChatTask extends AnyTask = AnyTask>(): ChatTurnContext<InferChatClientData<TChatTask>> | undefined;
53
+ /**
54
+ * Get the chat context from inside a subtask, throwing if not in a chat context.
55
+ * Pass `typeof yourChatTask` as the type parameter to get typed `clientData`.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const ctx = ai.chatContextOrThrow<typeof myChat>();
60
+ * // ctx.chatId, ctx.clientData are guaranteed non-null
61
+ * ```
62
+ */
63
+ declare function getToolChatContextOrThrow<TChatTask extends AnyTask = AnyTask>(): ChatTurnContext<InferChatClientData<TChatTask>>;
20
64
  export declare const ai: {
21
65
  tool: typeof toolFromTask;
22
66
  currentToolOptions: typeof getToolOptionsFromMetadata;
67
+ /** Get the tool call ID from inside a subtask invoked via `ai.tool()`. */
68
+ toolCallId: typeof getToolCallId;
69
+ /** Get chat context (chatId, turn, clientData, etc.) from inside a subtask of a `chat.task`. Returns undefined if not in a chat context. */
70
+ chatContext: typeof getToolChatContext;
71
+ /** Get chat context or throw if not in a chat context. Pass `typeof yourChatTask` for typed clientData. */
72
+ chatContextOrThrow: typeof getToolChatContextOrThrow;
23
73
  };
24
74
  /**
25
75
  * Creates a public access token for a chat task.
@@ -46,6 +96,21 @@ declare function createChatAccessToken<TTask extends AnyTask>(taskId: TaskIdenti
46
96
  */
47
97
  export declare const CHAT_STREAM_KEY = "chat";
48
98
  export { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID };
99
+ /**
100
+ * The wire payload shape sent by `TriggerChatTransport`.
101
+ * Uses `metadata` to match the AI SDK's `ChatRequestOptions` field name.
102
+ */
103
+ export type ChatTaskWirePayload<TMessage extends UIMessage = UIMessage, TMetadata = unknown> = {
104
+ messages: TMessage[];
105
+ chatId: string;
106
+ trigger: "submit-message" | "regenerate-message" | "preload";
107
+ messageId?: string;
108
+ metadata?: TMetadata;
109
+ /** Whether this run is continuing an existing chat whose previous run ended. */
110
+ continuation?: boolean;
111
+ /** The run ID of the previous run (only set when `continuation` is true). */
112
+ previousRunId?: string;
113
+ };
49
114
  /**
50
115
  * The payload shape passed to the `chatTask` run function.
51
116
  *
@@ -65,12 +130,19 @@ export type ChatTaskPayload<TClientData = unknown> = {
65
130
  * The trigger type:
66
131
  * - `"submit-message"`: A new user message
67
132
  * - `"regenerate-message"`: Regenerate the last assistant response
133
+ * - `"preload"`: Run was preloaded before the first message (only on turn 0)
68
134
  */
69
- trigger: "submit-message" | "regenerate-message";
135
+ trigger: "submit-message" | "regenerate-message" | "preload";
70
136
  /** The ID of the message to regenerate (only for `"regenerate-message"`) */
71
137
  messageId?: string;
72
138
  /** Custom data from the frontend (passed via `metadata` on `sendMessage()` or the transport). */
73
139
  clientData?: TClientData;
140
+ /** Whether this run is continuing an existing chat (previous run timed out or was cancelled). False for brand new chats. */
141
+ continuation: boolean;
142
+ /** The run ID of the previous run (only set when `continuation` is true). */
143
+ previousRunId?: string;
144
+ /** Whether this run was preloaded before the first message. */
145
+ preloaded: boolean;
74
146
  };
75
147
  /**
76
148
  * Abort signals provided to the `chatTask` run function.
@@ -167,6 +239,19 @@ declare function pipeChat(source: UIMessageStreamable | AsyncIterable<unknown> |
167
239
  * emits a control chunk and suspends via `messagesInput.wait()`. The frontend
168
240
  * transport resumes the same run by sending the next message via input streams.
169
241
  */
242
+ /**
243
+ * Event passed to the `onPreload` callback.
244
+ */
245
+ export type PreloadEvent<TClientData = unknown> = {
246
+ /** The unique identifier for the chat session. */
247
+ chatId: string;
248
+ /** The Trigger.dev run ID for this conversation. */
249
+ runId: string;
250
+ /** A scoped access token for this chat run. */
251
+ chatAccessToken: string;
252
+ /** Custom data from the frontend. */
253
+ clientData?: TClientData;
254
+ };
170
255
  /**
171
256
  * Event passed to the `onChatStart` callback.
172
257
  */
@@ -181,6 +266,12 @@ export type ChatStartEvent<TClientData = unknown> = {
181
266
  runId: string;
182
267
  /** A scoped access token for this chat run. Persist this for frontend reconnection. */
183
268
  chatAccessToken: string;
269
+ /** Whether this run is continuing an existing chat (previous run timed out or was cancelled). False for brand new chats. */
270
+ continuation: boolean;
271
+ /** The run ID of the previous run (only set when `continuation` is true). */
272
+ previousRunId?: string;
273
+ /** Whether this run was preloaded before the first message. */
274
+ preloaded: boolean;
184
275
  };
185
276
  /**
186
277
  * Event passed to the `onTurnStart` callback.
@@ -200,6 +291,12 @@ export type TurnStartEvent<TClientData = unknown> = {
200
291
  chatAccessToken: string;
201
292
  /** Custom data from the frontend. */
202
293
  clientData?: TClientData;
294
+ /** Whether this run is continuing an existing chat (previous run timed out or was cancelled). False for brand new chats. */
295
+ continuation: boolean;
296
+ /** The run ID of the previous run (only set when `continuation` is true). */
297
+ previousRunId?: string;
298
+ /** Whether this run was preloaded before the first message. */
299
+ preloaded: boolean;
203
300
  };
204
301
  /**
205
302
  * Event passed to the `onTurnComplete` callback.
@@ -224,8 +321,14 @@ export type TurnCompleteEvent<TClientData = unknown> = {
224
321
  * Useful for inserting individual message records instead of overwriting the full history.
225
322
  */
226
323
  newUIMessages: UIMessage[];
227
- /** The assistant's response for this turn (undefined if `pipeChat` was used manually). */
324
+ /** The assistant's response for this turn, with aborted parts cleaned up when `stopped` is true. Undefined if `pipeChat` was used manually. */
228
325
  responseMessage: UIMessage | undefined;
326
+ /**
327
+ * The raw assistant response before abort cleanup. Includes incomplete tool parts
328
+ * (`input-available`, `partial-call`) and streaming reasoning/text parts.
329
+ * Use this if you need custom cleanup logic. Same as `responseMessage` when not stopped.
330
+ */
331
+ rawResponseMessage: UIMessage | undefined;
229
332
  /** The turn number (0-indexed). */
230
333
  turn: number;
231
334
  /** The Trigger.dev run ID for this conversation. */
@@ -236,6 +339,14 @@ export type TurnCompleteEvent<TClientData = unknown> = {
236
339
  lastEventId?: string;
237
340
  /** Custom data from the frontend. */
238
341
  clientData?: TClientData;
342
+ /** Whether the user stopped generation during this turn. */
343
+ stopped: boolean;
344
+ /** Whether this run is continuing an existing chat (previous run timed out or was cancelled). False for brand new chats. */
345
+ continuation: boolean;
346
+ /** The run ID of the previous run (only set when `continuation` is true). */
347
+ previousRunId?: string;
348
+ /** Whether this run was preloaded before the first message. */
349
+ preloaded: boolean;
239
350
  };
240
351
  export type ChatTaskOptions<TIdentifier extends string, TClientDataSchema extends TaskSchema | undefined = undefined> = Omit<TaskOptions<TIdentifier, ChatTaskWirePayload, unknown>, "run"> & {
241
352
  /**
@@ -267,6 +378,21 @@ export type ChatTaskOptions<TIdentifier extends string, TClientDataSchema extend
267
378
  * the stream is automatically piped to the frontend.
268
379
  */
269
380
  run: (payload: ChatTaskRunPayload<inferSchemaOut<TClientDataSchema>>) => Promise<unknown>;
381
+ /**
382
+ * Called when a preloaded run starts, before the first message arrives.
383
+ *
384
+ * Use this to initialize state, create DB records, and load context early —
385
+ * so everything is ready when the user's first message comes through.
386
+ *
387
+ * @example
388
+ * ```ts
389
+ * onPreload: async ({ chatId, clientData }) => {
390
+ * await db.chat.create({ data: { id: chatId } });
391
+ * userContext.init(await loadUser(clientData.userId));
392
+ * }
393
+ * ```
394
+ */
395
+ onPreload?: (event: PreloadEvent<inferSchemaOut<TClientDataSchema>>) => Promise<void> | void;
270
396
  /**
271
397
  * Called on the first turn (turn 0) of a new run, before the `run` function executes.
272
398
  *
@@ -345,6 +471,24 @@ export type ChatTaskOptions<TIdentifier extends string, TClientDataSchema extend
345
471
  * @default "1h"
346
472
  */
347
473
  chatAccessTokenTTL?: string;
474
+ /**
475
+ * How long (in seconds) to keep the run warm after `onPreload` fires,
476
+ * waiting for the first message before suspending.
477
+ *
478
+ * Only applies to preloaded runs (triggered via `transport.preload()`).
479
+ *
480
+ * @default Same as `warmTimeoutInSeconds`
481
+ */
482
+ preloadWarmTimeoutInSeconds?: number;
483
+ /**
484
+ * How long to wait (suspended) for the first message after a preloaded run starts.
485
+ * If no message arrives within this time, the run ends.
486
+ *
487
+ * Only applies to preloaded runs.
488
+ *
489
+ * @default Same as `turnTimeout`
490
+ */
491
+ preloadTimeout?: string;
348
492
  };
349
493
  /**
350
494
  * Creates a Trigger.dev task pre-configured for AI SDK chat.
@@ -426,6 +570,131 @@ declare function setTurnTimeoutInSeconds(seconds: number): void;
426
570
  * ```
427
571
  */
428
572
  declare function setWarmTimeoutInSeconds(seconds: number): void;
573
+ /**
574
+ * Check whether the user stopped generation during the current turn.
575
+ *
576
+ * Works from **anywhere** inside a `chat.task` run — including inside
577
+ * `streamText`'s `onFinish` callback — without needing to thread the
578
+ * `stopSignal` through closures.
579
+ *
580
+ * This is especially useful when the AI SDK's `isAborted` flag is unreliable
581
+ * (e.g. when using `createUIMessageStream` + `writer.merge()`).
582
+ *
583
+ * @example
584
+ * ```ts
585
+ * onFinish: ({ isAborted }) => {
586
+ * const wasStopped = isAborted || chat.isStopped();
587
+ * if (wasStopped) {
588
+ * // handle stop
589
+ * }
590
+ * }
591
+ * ```
592
+ */
593
+ declare function isStopped(): boolean;
594
+ /**
595
+ * Register a promise that runs in the background during the current turn.
596
+ *
597
+ * Use this to move non-blocking work (DB writes, analytics, etc.) out of
598
+ * the critical path. The promise runs in parallel with streaming and is
599
+ * awaited (with a 5 s timeout) before `onTurnComplete` fires.
600
+ *
601
+ * @example
602
+ * ```ts
603
+ * onTurnStart: async ({ chatId, uiMessages }) => {
604
+ * // Persist messages without blocking the LLM call
605
+ * chat.defer(db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } }));
606
+ * },
607
+ * ```
608
+ */
609
+ declare function chatDefer(promise: Promise<unknown>): void;
610
+ /**
611
+ * Clean up a UIMessage that was captured during an aborted/stopped turn.
612
+ *
613
+ * When generation is stopped mid-stream, the captured message may contain:
614
+ * - Tool parts stuck in incomplete states (`partial-call`, `input-available`,
615
+ * `input-streaming`) that cause permanent UI spinners
616
+ * - Reasoning parts with `state: "streaming"` instead of `"done"`
617
+ * - Text parts with `state: "streaming"` instead of `"done"`
618
+ *
619
+ * This function returns a cleaned copy with:
620
+ * - Incomplete tool parts removed entirely
621
+ * - Reasoning and text parts marked as `"done"`
622
+ *
623
+ * `chat.task` calls this automatically when stop is detected before passing
624
+ * the response to `onTurnComplete`. Use this manually when calling `pipeChat`
625
+ * directly and capturing response messages yourself.
626
+ *
627
+ * @example
628
+ * ```ts
629
+ * onTurnComplete: async ({ responseMessage, stopped }) => {
630
+ * // Already cleaned automatically by chat.task — but if you captured
631
+ * // your own message via pipeChat, clean it manually:
632
+ * const cleaned = chat.cleanupAbortedParts(myMessage);
633
+ * await db.messages.save(cleaned);
634
+ * }
635
+ * ```
636
+ */
637
+ declare function cleanupAbortedParts(message: UIMessage): UIMessage;
638
+ /**
639
+ * A Proxy-backed, run-scoped data object that appears as `T` to users.
640
+ * Includes helper methods for initialization, dirty tracking, and serialization.
641
+ * Internal metadata is stored behind Symbols and invisible to
642
+ * `Object.keys()`, `JSON.stringify()`, and spread.
643
+ */
644
+ export type ChatLocal<T extends Record<string, unknown>> = T & {
645
+ /** Initialize the local with a value. Call in `onChatStart` or `run()`. */
646
+ init(value: T): void;
647
+ /** Returns `true` if any property was set since the last check. Resets the dirty flag. */
648
+ hasChanged(): boolean;
649
+ /** Returns a plain object copy of the current value. Useful for persistence. */
650
+ get(): T;
651
+ readonly [CHAT_LOCAL_KEY]: ReturnType<typeof locals.create<T>>;
652
+ readonly [CHAT_LOCAL_DIRTY_KEY]: ReturnType<typeof locals.create<boolean>>;
653
+ };
654
+ /**
655
+ * Creates a per-run typed data object accessible from anywhere during task execution.
656
+ *
657
+ * Declare at module level, then initialize inside a lifecycle hook (e.g. `onChatStart`)
658
+ * using `chat.initLocal()`. Properties are accessible directly via the Proxy.
659
+ *
660
+ * Multiple locals can coexist — each gets its own isolated run-scoped storage.
661
+ *
662
+ * The `id` is required and must be unique across all `chat.local()` calls in
663
+ * your project. It's used to serialize values into subtask metadata so that
664
+ * `ai.tool()` subtasks can auto-hydrate parent locals (read-only).
665
+ *
666
+ * @example
667
+ * ```ts
668
+ * import { chat } from "@trigger.dev/sdk/ai";
669
+ *
670
+ * const userPrefs = chat.local<{ theme: string; language: string }>({ id: "userPrefs" });
671
+ * const gameState = chat.local<{ score: number; streak: number }>({ id: "gameState" });
672
+ *
673
+ * export const myChat = chat.task({
674
+ * id: "my-chat",
675
+ * onChatStart: async ({ clientData }) => {
676
+ * const prefs = await db.prefs.findUnique({ where: { userId: clientData.userId } });
677
+ * userPrefs.init(prefs ?? { theme: "dark", language: "en" });
678
+ * gameState.init({ score: 0, streak: 0 });
679
+ * },
680
+ * onTurnComplete: async ({ chatId }) => {
681
+ * if (gameState.hasChanged()) {
682
+ * await db.save({ where: { chatId }, data: gameState.get() });
683
+ * }
684
+ * },
685
+ * run: async ({ messages }) => {
686
+ * gameState.score++;
687
+ * return streamText({
688
+ * system: `User prefers ${userPrefs.theme} theme. Score: ${gameState.score}`,
689
+ * messages,
690
+ * });
691
+ * },
692
+ * });
693
+ * ```
694
+ */
695
+ declare function chatLocal<T extends Record<string, unknown>>(options: {
696
+ id: string;
697
+ }): ChatLocal<T>;
429
698
  /**
430
699
  * Extracts the client data (metadata) type from a chat task.
431
700
  * Use this to type the `metadata` option on the transport.
@@ -445,6 +714,8 @@ export declare const chat: {
445
714
  task: typeof chatTask;
446
715
  /** Pipe a stream to the chat transport. See {@link pipeChat}. */
447
716
  pipe: typeof pipeChat;
717
+ /** Create a per-run typed local. See {@link chatLocal}. */
718
+ local: typeof chatLocal;
448
719
  /** Create a public access token for a chat task. See {@link createChatAccessToken}. */
449
720
  createAccessToken: typeof createChatAccessToken;
450
721
  /** Override the turn timeout at runtime (duration string). See {@link setTurnTimeout}. */
@@ -453,4 +724,12 @@ export declare const chat: {
453
724
  setTurnTimeoutInSeconds: typeof setTurnTimeoutInSeconds;
454
725
  /** Override the warm timeout at runtime. See {@link setWarmTimeoutInSeconds}. */
455
726
  setWarmTimeoutInSeconds: typeof setWarmTimeoutInSeconds;
727
+ /** Check if the current turn was stopped by the user. See {@link isStopped}. */
728
+ isStopped: typeof isStopped;
729
+ /** Clean up aborted parts from a UIMessage. See {@link cleanupAbortedParts}. */
730
+ cleanupAbortedParts: typeof cleanupAbortedParts;
731
+ /** Register background work that runs in parallel with streaming. See {@link chatDefer}. */
732
+ defer: typeof chatDefer;
733
+ /** Typed chat output stream for writing custom chunks or piping from subtasks. */
734
+ stream: import("@trigger.dev/core/v3").RealtimeDefinedStream<UIMessageChunk>;
456
735
  };