@trigger.dev/sdk 0.0.0-prerelease-20260304181730 → 0.0.0-prerelease-20260305183215
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.
- package/dist/commonjs/v3/ai.d.ts +170 -16
- package/dist/commonjs/v3/ai.js +270 -39
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-react.d.ts +3 -0
- package/dist/commonjs/v3/chat-react.js +8 -0
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +86 -0
- package/dist/commonjs/v3/chat.js +90 -4
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +325 -72
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/streams.js +3 -1
- package/dist/commonjs/v3/streams.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +170 -16
- package/dist/esm/v3/ai.js +272 -41
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-react.d.ts +3 -0
- package/dist/esm/v3/chat-react.js +9 -1
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +86 -0
- package/dist/esm/v3/chat.js +90 -4
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +325 -72
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/streams.js +3 -1
- package/dist/esm/v3/streams.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
package/dist/commonjs/v3/ai.d.ts
CHANGED
|
@@ -51,14 +51,14 @@ export { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID };
|
|
|
51
51
|
*
|
|
52
52
|
* - `messages` contains model-ready messages (converted via `convertToModelMessages`) —
|
|
53
53
|
* pass these directly to `streamText`.
|
|
54
|
-
* - `uiMessages` contains the raw `UIMessage[]` from the frontend.
|
|
55
54
|
* - `clientData` contains custom data from the frontend (the `metadata` field from `sendMessage()`).
|
|
55
|
+
*
|
|
56
|
+
* The backend accumulates the full conversation history across turns, so the frontend
|
|
57
|
+
* only needs to send new messages after the first turn.
|
|
56
58
|
*/
|
|
57
59
|
export type ChatTaskPayload = {
|
|
58
60
|
/** Model-ready messages — pass directly to `streamText({ messages })`. */
|
|
59
61
|
messages: ModelMessage[];
|
|
60
|
-
/** Raw UI messages from the frontend. */
|
|
61
|
-
uiMessages: UIMessage[];
|
|
62
62
|
/** The unique identifier for the chat session */
|
|
63
63
|
chatId: string;
|
|
64
64
|
/**
|
|
@@ -167,6 +167,72 @@ declare function pipeChat(source: UIMessageStreamable | AsyncIterable<unknown> |
|
|
|
167
167
|
* emits a control chunk and suspends via `messagesInput.wait()`. The frontend
|
|
168
168
|
* transport resumes the same run by sending the next message via input streams.
|
|
169
169
|
*/
|
|
170
|
+
/**
|
|
171
|
+
* Event passed to the `onChatStart` callback.
|
|
172
|
+
*/
|
|
173
|
+
export type ChatStartEvent = {
|
|
174
|
+
/** The unique identifier for the chat session. */
|
|
175
|
+
chatId: string;
|
|
176
|
+
/** The initial model-ready messages for this conversation. */
|
|
177
|
+
messages: ModelMessage[];
|
|
178
|
+
/** Custom data from the frontend (passed via `metadata` on `sendMessage()` or the transport). */
|
|
179
|
+
clientData: unknown;
|
|
180
|
+
/** The Trigger.dev run ID for this conversation. */
|
|
181
|
+
runId: string;
|
|
182
|
+
/** A scoped access token for this chat run. Persist this for frontend reconnection. */
|
|
183
|
+
chatAccessToken: string;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Event passed to the `onTurnStart` callback.
|
|
187
|
+
*/
|
|
188
|
+
export type TurnStartEvent = {
|
|
189
|
+
/** The unique identifier for the chat session. */
|
|
190
|
+
chatId: string;
|
|
191
|
+
/** The accumulated model-ready messages (all turns so far, including new user message). */
|
|
192
|
+
messages: ModelMessage[];
|
|
193
|
+
/** The accumulated UI messages (all turns so far, including new user message). */
|
|
194
|
+
uiMessages: UIMessage[];
|
|
195
|
+
/** The turn number (0-indexed). */
|
|
196
|
+
turn: number;
|
|
197
|
+
/** The Trigger.dev run ID for this conversation. */
|
|
198
|
+
runId: string;
|
|
199
|
+
/** A scoped access token for this chat run. */
|
|
200
|
+
chatAccessToken: string;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Event passed to the `onTurnComplete` callback.
|
|
204
|
+
*/
|
|
205
|
+
export type TurnCompleteEvent = {
|
|
206
|
+
/** The unique identifier for the chat session. */
|
|
207
|
+
chatId: string;
|
|
208
|
+
/** The full accumulated conversation in model format (all turns so far). */
|
|
209
|
+
messages: ModelMessage[];
|
|
210
|
+
/**
|
|
211
|
+
* The full accumulated conversation in UI format (all turns so far).
|
|
212
|
+
* This is the format expected by `useChat` — store this for persistence.
|
|
213
|
+
*/
|
|
214
|
+
uiMessages: UIMessage[];
|
|
215
|
+
/**
|
|
216
|
+
* Only the new model messages from this turn (user message(s) + assistant response).
|
|
217
|
+
* Useful for appending to an existing conversation record.
|
|
218
|
+
*/
|
|
219
|
+
newMessages: ModelMessage[];
|
|
220
|
+
/**
|
|
221
|
+
* Only the new UI messages from this turn (user message(s) + assistant response).
|
|
222
|
+
* Useful for inserting individual message records instead of overwriting the full history.
|
|
223
|
+
*/
|
|
224
|
+
newUIMessages: UIMessage[];
|
|
225
|
+
/** The assistant's response for this turn (undefined if `pipeChat` was used manually). */
|
|
226
|
+
responseMessage: UIMessage | undefined;
|
|
227
|
+
/** The turn number (0-indexed). */
|
|
228
|
+
turn: number;
|
|
229
|
+
/** The Trigger.dev run ID for this conversation. */
|
|
230
|
+
runId: string;
|
|
231
|
+
/** A fresh scoped access token for this chat run (renewed each turn). Persist this for frontend reconnection. */
|
|
232
|
+
chatAccessToken: string;
|
|
233
|
+
/** The last event ID from the stream writer. Use this with `resume: true` to avoid replaying events after refresh. */
|
|
234
|
+
lastEventId?: string;
|
|
235
|
+
};
|
|
170
236
|
export type ChatTaskOptions<TIdentifier extends string> = Omit<TaskOptions<TIdentifier, ChatTaskWirePayload, unknown>, "run"> & {
|
|
171
237
|
/**
|
|
172
238
|
* The run function for the chat task.
|
|
@@ -178,6 +244,48 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<TaskOptions<TIden
|
|
|
178
244
|
* the stream is automatically piped to the frontend.
|
|
179
245
|
*/
|
|
180
246
|
run: (payload: ChatTaskRunPayload) => Promise<unknown>;
|
|
247
|
+
/**
|
|
248
|
+
* Called on the first turn (turn 0) of a new run, before the `run` function executes.
|
|
249
|
+
*
|
|
250
|
+
* Use this to create the chat record in your database when a new conversation starts.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* onChatStart: async ({ chatId, messages, clientData }) => {
|
|
255
|
+
* await db.chat.create({ data: { id: chatId, userId: clientData.userId } });
|
|
256
|
+
* }
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
onChatStart?: (event: ChatStartEvent) => Promise<void> | void;
|
|
260
|
+
/**
|
|
261
|
+
* Called at the start of every turn, after message accumulation and `onChatStart` (turn 0),
|
|
262
|
+
* but before the `run` function executes.
|
|
263
|
+
*
|
|
264
|
+
* Use this to persist messages before streaming begins, so a mid-stream page refresh
|
|
265
|
+
* still shows the user's message.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* onTurnStart: async ({ chatId, uiMessages }) => {
|
|
270
|
+
* await db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } });
|
|
271
|
+
* }
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
onTurnStart?: (event: TurnStartEvent) => Promise<void> | void;
|
|
275
|
+
/**
|
|
276
|
+
* Called after each turn completes (after the response is captured, before waiting
|
|
277
|
+
* for the next message). Also fires on the final turn.
|
|
278
|
+
*
|
|
279
|
+
* Use this to persist the conversation to your database after each assistant response.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```ts
|
|
283
|
+
* onTurnComplete: async ({ chatId, messages }) => {
|
|
284
|
+
* await db.chat.update({ where: { id: chatId }, data: { messages } });
|
|
285
|
+
* }
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
onTurnComplete?: (event: TurnCompleteEvent) => Promise<void> | void;
|
|
181
289
|
/**
|
|
182
290
|
* Maximum number of conversational turns (message round-trips) a single run
|
|
183
291
|
* will handle before ending. After this many turns the run completes
|
|
@@ -204,6 +312,16 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<TaskOptions<TIden
|
|
|
204
312
|
* @default 30
|
|
205
313
|
*/
|
|
206
314
|
warmTimeoutInSeconds?: number;
|
|
315
|
+
/**
|
|
316
|
+
* How long the `chatAccessToken` (scoped to this run) remains valid.
|
|
317
|
+
* A fresh token is minted after each turn, so this only needs to cover
|
|
318
|
+
* the gap between turns.
|
|
319
|
+
*
|
|
320
|
+
* Accepts a duration string (e.g. `"1h"`, `"30m"`, `"2h"`).
|
|
321
|
+
*
|
|
322
|
+
* @default "1h"
|
|
323
|
+
*/
|
|
324
|
+
chatAccessTokenTTL?: string;
|
|
207
325
|
};
|
|
208
326
|
/**
|
|
209
327
|
* Creates a Trigger.dev task pre-configured for AI SDK chat.
|
|
@@ -234,27 +352,57 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<TaskOptions<TIden
|
|
|
234
352
|
*/
|
|
235
353
|
declare function chatTask<TIdentifier extends string>(options: ChatTaskOptions<TIdentifier>): Task<TIdentifier, ChatTaskWirePayload, unknown>;
|
|
236
354
|
/**
|
|
237
|
-
*
|
|
355
|
+
* Override the turn timeout for subsequent turns in the current run.
|
|
356
|
+
*
|
|
357
|
+
* The turn timeout controls how long the run stays suspended (freeing compute)
|
|
358
|
+
* waiting for the next user message. When it expires, the run completes
|
|
359
|
+
* gracefully and the next message starts a fresh run.
|
|
360
|
+
*
|
|
361
|
+
* Call from inside a `chatTask` run function to adjust based on context.
|
|
362
|
+
*
|
|
363
|
+
* @param duration - A duration string (e.g. `"5m"`, `"1h"`, `"30s"`)
|
|
238
364
|
*
|
|
239
365
|
* @example
|
|
240
366
|
* ```ts
|
|
241
|
-
*
|
|
367
|
+
* run: async ({ messages, signal }) => {
|
|
368
|
+
* chat.setTurnTimeout("2h");
|
|
369
|
+
* return streamText({ model, messages, abortSignal: signal });
|
|
370
|
+
* }
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
declare function setTurnTimeout(duration: string): void;
|
|
374
|
+
/**
|
|
375
|
+
* Override the turn timeout in seconds for subsequent turns in the current run.
|
|
242
376
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* });
|
|
377
|
+
* @param seconds - Number of seconds to wait for the next message before ending the run
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts
|
|
381
|
+
* run: async ({ messages, signal }) => {
|
|
382
|
+
* chat.setTurnTimeoutInSeconds(3600); // 1 hour
|
|
383
|
+
* return streamText({ model, messages, abortSignal: signal });
|
|
384
|
+
* }
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
declare function setTurnTimeoutInSeconds(seconds: number): void;
|
|
388
|
+
/**
|
|
389
|
+
* Override the warm timeout for subsequent turns in the current run.
|
|
250
390
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
391
|
+
* The warm timeout controls how long the run stays active (using compute)
|
|
392
|
+
* after each turn, waiting for the next message. During this window,
|
|
393
|
+
* responses are instant. After it expires, the run suspends.
|
|
253
394
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
395
|
+
* @param seconds - Number of seconds to stay warm (0 to suspend immediately)
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```ts
|
|
399
|
+
* run: async ({ messages, signal }) => {
|
|
400
|
+
* chat.setWarmTimeoutInSeconds(60);
|
|
401
|
+
* return streamText({ model, messages, abortSignal: signal });
|
|
402
|
+
* }
|
|
256
403
|
* ```
|
|
257
404
|
*/
|
|
405
|
+
declare function setWarmTimeoutInSeconds(seconds: number): void;
|
|
258
406
|
export declare const chat: {
|
|
259
407
|
/** Create a chat task. See {@link chatTask}. */
|
|
260
408
|
task: typeof chatTask;
|
|
@@ -262,4 +410,10 @@ export declare const chat: {
|
|
|
262
410
|
pipe: typeof pipeChat;
|
|
263
411
|
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
|
|
264
412
|
createAccessToken: typeof createChatAccessToken;
|
|
413
|
+
/** Override the turn timeout at runtime (duration string). See {@link setTurnTimeout}. */
|
|
414
|
+
setTurnTimeout: typeof setTurnTimeout;
|
|
415
|
+
/** Override the turn timeout at runtime (seconds). See {@link setTurnTimeoutInSeconds}. */
|
|
416
|
+
setTurnTimeoutInSeconds: typeof setTurnTimeoutInSeconds;
|
|
417
|
+
/** Override the warm timeout at runtime. See {@link setWarmTimeoutInSeconds}. */
|
|
418
|
+
setWarmTimeoutInSeconds: typeof setWarmTimeoutInSeconds;
|
|
265
419
|
};
|
package/dist/commonjs/v3/ai.js
CHANGED
|
@@ -79,7 +79,7 @@ exports.ai = {
|
|
|
79
79
|
* ```
|
|
80
80
|
*/
|
|
81
81
|
function createChatAccessToken(taskId) {
|
|
82
|
-
return auth_js_1.auth.createTriggerPublicToken(taskId, {
|
|
82
|
+
return auth_js_1.auth.createTriggerPublicToken(taskId, { expirationTime: "24h" });
|
|
83
83
|
}
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
85
|
// Chat transport helpers — backend side
|
|
@@ -93,28 +93,6 @@ exports.CHAT_STREAM_KEY = chat_constants_js_1.CHAT_STREAM_KEY;
|
|
|
93
93
|
// Input streams for bidirectional chat communication
|
|
94
94
|
const messagesInput = streams_js_1.streams.input({ id: chat_constants_js_1.CHAT_MESSAGES_STREAM_ID });
|
|
95
95
|
const stopInput = streams_js_1.streams.input({ id: chat_constants_js_1.CHAT_STOP_STREAM_ID });
|
|
96
|
-
/**
|
|
97
|
-
* Strips provider-specific IDs from message parts so that partial/stopped
|
|
98
|
-
* assistant responses don't cause 404s when sent back to the provider
|
|
99
|
-
* (e.g. OpenAI Responses API message IDs).
|
|
100
|
-
* @internal
|
|
101
|
-
*/
|
|
102
|
-
function sanitizeMessages(messages) {
|
|
103
|
-
return messages.map((msg) => {
|
|
104
|
-
if (msg.role !== "assistant" || !msg.parts)
|
|
105
|
-
return msg;
|
|
106
|
-
return {
|
|
107
|
-
...msg,
|
|
108
|
-
parts: msg.parts.map((part) => {
|
|
109
|
-
// Strip provider-specific metadata (e.g. OpenAI Responses API itemId)
|
|
110
|
-
// and streaming state from assistant message parts. These cause 404s
|
|
111
|
-
// when partial/stopped responses are sent back to the provider.
|
|
112
|
-
const { providerMetadata, state, id, ...rest } = part;
|
|
113
|
-
return rest;
|
|
114
|
-
}),
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
96
|
/**
|
|
119
97
|
* Tracks how many times `pipeChat` has been called in the current `chatTask` run.
|
|
120
98
|
* Used to prevent double-piping when a user both calls `pipeChat()` manually
|
|
@@ -228,7 +206,7 @@ async function pipeChat(source, options) {
|
|
|
228
206
|
* ```
|
|
229
207
|
*/
|
|
230
208
|
function chatTask(options) {
|
|
231
|
-
const { run: userRun, maxTurns = 100, turnTimeout = "1h", warmTimeoutInSeconds = 30, ...restOptions } = options;
|
|
209
|
+
const { run: userRun, onChatStart, onTurnStart, onTurnComplete, maxTurns = 100, turnTimeout = "1h", warmTimeoutInSeconds = 30, chatAccessTokenTTL = "1h", ...restOptions } = options;
|
|
232
210
|
return (0, shared_js_1.createTask)({
|
|
233
211
|
...restOptions,
|
|
234
212
|
run: async (payload, { signal: runSignal }) => {
|
|
@@ -238,6 +216,13 @@ function chatTask(options) {
|
|
|
238
216
|
activeSpan.setAttribute("gen_ai.conversation.id", payload.chatId);
|
|
239
217
|
}
|
|
240
218
|
let currentWirePayload = payload;
|
|
219
|
+
// Accumulated model messages across turns. Turn 1 initialises from the
|
|
220
|
+
// full history the frontend sends; subsequent turns append only the new
|
|
221
|
+
// user message(s) and the captured assistant response.
|
|
222
|
+
let accumulatedMessages = [];
|
|
223
|
+
// Accumulated UI messages for persistence. Mirrors the model accumulator
|
|
224
|
+
// but in frontend-friendly UIMessage format (with parts, id, etc.).
|
|
225
|
+
let accumulatedUIMessages = [];
|
|
241
226
|
// Mutable reference to the current turn's stop controller so the
|
|
242
227
|
// stop input stream listener (registered once) can abort the right turn.
|
|
243
228
|
let currentStopController;
|
|
@@ -287,23 +272,114 @@ function chatTask(options) {
|
|
|
287
272
|
const msgSub = messagesInput.on((msg) => {
|
|
288
273
|
pendingMessages.push(msg);
|
|
289
274
|
});
|
|
290
|
-
// Convert
|
|
291
|
-
|
|
292
|
-
|
|
275
|
+
// Convert the incoming UIMessages to model messages and update the accumulator.
|
|
276
|
+
// Turn 1: full history from the frontend → replaces the accumulator.
|
|
277
|
+
// Turn 2+: only the new message(s) → appended to the accumulator.
|
|
278
|
+
const incomingModelMessages = await (0, ai_1.convertToModelMessages)(uiMessages);
|
|
279
|
+
// Track new messages for this turn (user input + assistant response).
|
|
280
|
+
const turnNewModelMessages = [];
|
|
281
|
+
const turnNewUIMessages = [];
|
|
282
|
+
if (turn === 0) {
|
|
283
|
+
accumulatedMessages = incomingModelMessages;
|
|
284
|
+
accumulatedUIMessages = [...uiMessages];
|
|
285
|
+
// On first turn, the "new" messages are just the last user message
|
|
286
|
+
// (the rest is history). We'll add the response after streaming.
|
|
287
|
+
if (uiMessages.length > 0) {
|
|
288
|
+
turnNewUIMessages.push(uiMessages[uiMessages.length - 1]);
|
|
289
|
+
const lastModel = incomingModelMessages[incomingModelMessages.length - 1];
|
|
290
|
+
if (lastModel)
|
|
291
|
+
turnNewModelMessages.push(lastModel);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (currentWirePayload.trigger === "regenerate-message") {
|
|
295
|
+
// Regenerate: frontend sent full history with last assistant message
|
|
296
|
+
// removed. Reset the accumulator to match.
|
|
297
|
+
accumulatedMessages = incomingModelMessages;
|
|
298
|
+
accumulatedUIMessages = [...uiMessages];
|
|
299
|
+
// No new user messages for regenerate — just the response (added below)
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Submit: frontend sent only the new user message(s). Append to accumulator.
|
|
303
|
+
accumulatedMessages.push(...incomingModelMessages);
|
|
304
|
+
accumulatedUIMessages.push(...uiMessages);
|
|
305
|
+
turnNewModelMessages.push(...incomingModelMessages);
|
|
306
|
+
turnNewUIMessages.push(...uiMessages);
|
|
307
|
+
}
|
|
308
|
+
// Mint a scoped public access token once per turn, reused for
|
|
309
|
+
// onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk.
|
|
310
|
+
const currentRunId = v3_1.taskContext.ctx?.run.id ?? "";
|
|
311
|
+
let turnAccessToken = "";
|
|
312
|
+
if (currentRunId) {
|
|
313
|
+
try {
|
|
314
|
+
turnAccessToken = await auth_js_1.auth.createPublicToken({
|
|
315
|
+
scopes: {
|
|
316
|
+
read: { runs: currentRunId },
|
|
317
|
+
write: { inputStreams: currentRunId },
|
|
318
|
+
},
|
|
319
|
+
expirationTime: chatAccessTokenTTL,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Token creation failed
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Fire onChatStart on the first turn
|
|
327
|
+
if (turn === 0 && onChatStart) {
|
|
328
|
+
await tracer_js_1.tracer.startActiveSpan("onChatStart()", async () => {
|
|
329
|
+
await onChatStart({
|
|
330
|
+
chatId: currentWirePayload.chatId,
|
|
331
|
+
messages: accumulatedMessages,
|
|
332
|
+
clientData: wireMetadata,
|
|
333
|
+
runId: currentRunId,
|
|
334
|
+
chatAccessToken: turnAccessToken,
|
|
335
|
+
});
|
|
336
|
+
}, {
|
|
337
|
+
attributes: {
|
|
338
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
339
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// Fire onTurnStart before running user code — persist messages
|
|
344
|
+
// so a mid-stream page refresh still shows the user's message.
|
|
345
|
+
if (onTurnStart) {
|
|
346
|
+
await tracer_js_1.tracer.startActiveSpan("onTurnStart()", async () => {
|
|
347
|
+
await onTurnStart({
|
|
348
|
+
chatId: currentWirePayload.chatId,
|
|
349
|
+
messages: accumulatedMessages,
|
|
350
|
+
uiMessages: accumulatedUIMessages,
|
|
351
|
+
turn,
|
|
352
|
+
runId: currentRunId,
|
|
353
|
+
chatAccessToken: turnAccessToken,
|
|
354
|
+
});
|
|
355
|
+
}, {
|
|
356
|
+
attributes: {
|
|
357
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart",
|
|
358
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// Captured by the onFinish callback below — works even on abort/stop.
|
|
363
|
+
let capturedResponseMessage;
|
|
293
364
|
try {
|
|
294
365
|
const result = await userRun({
|
|
295
366
|
...restWire,
|
|
296
|
-
messages:
|
|
297
|
-
uiMessages: sanitized,
|
|
367
|
+
messages: accumulatedMessages,
|
|
298
368
|
clientData: wireMetadata,
|
|
299
369
|
signal: combinedSignal,
|
|
300
370
|
cancelSignal,
|
|
301
371
|
stopSignal,
|
|
302
372
|
});
|
|
303
373
|
// Auto-pipe if the run function returned a StreamTextResult or similar,
|
|
304
|
-
// but only if pipeChat() wasn't already called manually during this turn
|
|
374
|
+
// but only if pipeChat() wasn't already called manually during this turn.
|
|
375
|
+
// We call toUIMessageStream ourselves to attach onFinish for response capture.
|
|
305
376
|
if (_chatPipeCount === 0 && isUIMessageStreamable(result)) {
|
|
306
|
-
|
|
377
|
+
const uiStream = result.toUIMessageStream({
|
|
378
|
+
onFinish: ({ responseMessage }) => {
|
|
379
|
+
capturedResponseMessage = responseMessage;
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" });
|
|
307
383
|
}
|
|
308
384
|
}
|
|
309
385
|
catch (error) {
|
|
@@ -321,10 +397,60 @@ function chatTask(options) {
|
|
|
321
397
|
finally {
|
|
322
398
|
msgSub.off();
|
|
323
399
|
}
|
|
400
|
+
// Append the assistant's response (partial or complete) to the accumulator.
|
|
401
|
+
// The onFinish callback fires even on abort/stop, so partial responses
|
|
402
|
+
// from stopped generation are captured correctly.
|
|
403
|
+
if (capturedResponseMessage) {
|
|
404
|
+
// Ensure the response message has an ID (the stream's onFinish
|
|
405
|
+
// may produce a message with an empty ID since IDs are normally
|
|
406
|
+
// assigned by the frontend's useChat).
|
|
407
|
+
if (!capturedResponseMessage.id) {
|
|
408
|
+
capturedResponseMessage = { ...capturedResponseMessage, id: (0, ai_1.generateId)() };
|
|
409
|
+
}
|
|
410
|
+
accumulatedUIMessages.push(capturedResponseMessage);
|
|
411
|
+
turnNewUIMessages.push(capturedResponseMessage);
|
|
412
|
+
try {
|
|
413
|
+
const responseModelMessages = await (0, ai_1.convertToModelMessages)([
|
|
414
|
+
stripProviderMetadata(capturedResponseMessage),
|
|
415
|
+
]);
|
|
416
|
+
accumulatedMessages.push(...responseModelMessages);
|
|
417
|
+
turnNewModelMessages.push(...responseModelMessages);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Conversion failed — skip accumulation for this turn
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// TODO: When the user calls `pipeChat` manually instead of returning a
|
|
424
|
+
// StreamTextResult, we don't have access to onFinish. A future iteration
|
|
425
|
+
// should let manual-mode users report back response messages for
|
|
426
|
+
// accumulation (e.g. via a `chat.addMessages()` helper).
|
|
324
427
|
if (runSignal.aborted)
|
|
325
428
|
return "exit";
|
|
326
|
-
// Write turn-complete control chunk so frontend closes its stream
|
|
327
|
-
|
|
429
|
+
// Write turn-complete control chunk so frontend closes its stream.
|
|
430
|
+
// Capture the lastEventId from the stream writer for resume support.
|
|
431
|
+
const turnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId, turnAccessToken);
|
|
432
|
+
// Fire onTurnComplete after response capture
|
|
433
|
+
if (onTurnComplete) {
|
|
434
|
+
await tracer_js_1.tracer.startActiveSpan("onTurnComplete()", async () => {
|
|
435
|
+
await onTurnComplete({
|
|
436
|
+
chatId: currentWirePayload.chatId,
|
|
437
|
+
messages: accumulatedMessages,
|
|
438
|
+
uiMessages: accumulatedUIMessages,
|
|
439
|
+
newMessages: turnNewModelMessages,
|
|
440
|
+
newUIMessages: turnNewUIMessages,
|
|
441
|
+
responseMessage: capturedResponseMessage,
|
|
442
|
+
turn,
|
|
443
|
+
runId: currentRunId,
|
|
444
|
+
chatAccessToken: turnAccessToken,
|
|
445
|
+
lastEventId: turnCompleteResult.lastEventId,
|
|
446
|
+
});
|
|
447
|
+
}, {
|
|
448
|
+
attributes: {
|
|
449
|
+
[v3_1.SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete",
|
|
450
|
+
[v3_1.SemanticInternalAttributes.COLLAPSED]: true,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
328
454
|
// If messages arrived during streaming, use the first one immediately
|
|
329
455
|
if (pendingMessages.length > 0) {
|
|
330
456
|
currentWirePayload = pendingMessages[0];
|
|
@@ -332,9 +458,10 @@ function chatTask(options) {
|
|
|
332
458
|
}
|
|
333
459
|
// Phase 1: Keep the run warm for quick response to the next message.
|
|
334
460
|
// The run stays active (using compute) during this window.
|
|
335
|
-
|
|
461
|
+
const effectiveWarmTimeout = metadata_js_1.metadata.get(WARM_TIMEOUT_METADATA_KEY) ?? warmTimeoutInSeconds;
|
|
462
|
+
if (effectiveWarmTimeout > 0) {
|
|
336
463
|
const warm = await messagesInput.once({
|
|
337
|
-
timeoutMs:
|
|
464
|
+
timeoutMs: effectiveWarmTimeout * 1000,
|
|
338
465
|
spanName: "waiting (warm)",
|
|
339
466
|
});
|
|
340
467
|
if (warm.ok) {
|
|
@@ -344,8 +471,9 @@ function chatTask(options) {
|
|
|
344
471
|
}
|
|
345
472
|
}
|
|
346
473
|
// Phase 2: Suspend the task (frees compute) until the next message arrives
|
|
474
|
+
const effectiveTurnTimeout = metadata_js_1.metadata.get(TURN_TIMEOUT_METADATA_KEY) ?? turnTimeout;
|
|
347
475
|
const next = await messagesInput.wait({
|
|
348
|
-
timeout:
|
|
476
|
+
timeout: effectiveTurnTimeout,
|
|
349
477
|
spanName: "waiting (suspended)",
|
|
350
478
|
});
|
|
351
479
|
if (!next.ok) {
|
|
@@ -390,6 +518,69 @@ function chatTask(options) {
|
|
|
390
518
|
* const token = await chat.createAccessToken("my-chat");
|
|
391
519
|
* ```
|
|
392
520
|
*/
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Runtime configuration helpers
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
const TURN_TIMEOUT_METADATA_KEY = "chat.turnTimeout";
|
|
525
|
+
const WARM_TIMEOUT_METADATA_KEY = "chat.warmTimeout";
|
|
526
|
+
/**
|
|
527
|
+
* Override the turn timeout for subsequent turns in the current run.
|
|
528
|
+
*
|
|
529
|
+
* The turn timeout controls how long the run stays suspended (freeing compute)
|
|
530
|
+
* waiting for the next user message. When it expires, the run completes
|
|
531
|
+
* gracefully and the next message starts a fresh run.
|
|
532
|
+
*
|
|
533
|
+
* Call from inside a `chatTask` run function to adjust based on context.
|
|
534
|
+
*
|
|
535
|
+
* @param duration - A duration string (e.g. `"5m"`, `"1h"`, `"30s"`)
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```ts
|
|
539
|
+
* run: async ({ messages, signal }) => {
|
|
540
|
+
* chat.setTurnTimeout("2h");
|
|
541
|
+
* return streamText({ model, messages, abortSignal: signal });
|
|
542
|
+
* }
|
|
543
|
+
* ```
|
|
544
|
+
*/
|
|
545
|
+
function setTurnTimeout(duration) {
|
|
546
|
+
metadata_js_1.metadata.set(TURN_TIMEOUT_METADATA_KEY, duration);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Override the turn timeout in seconds for subsequent turns in the current run.
|
|
550
|
+
*
|
|
551
|
+
* @param seconds - Number of seconds to wait for the next message before ending the run
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* ```ts
|
|
555
|
+
* run: async ({ messages, signal }) => {
|
|
556
|
+
* chat.setTurnTimeoutInSeconds(3600); // 1 hour
|
|
557
|
+
* return streamText({ model, messages, abortSignal: signal });
|
|
558
|
+
* }
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
function setTurnTimeoutInSeconds(seconds) {
|
|
562
|
+
metadata_js_1.metadata.set(TURN_TIMEOUT_METADATA_KEY, `${seconds}s`);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Override the warm timeout for subsequent turns in the current run.
|
|
566
|
+
*
|
|
567
|
+
* The warm timeout controls how long the run stays active (using compute)
|
|
568
|
+
* after each turn, waiting for the next message. During this window,
|
|
569
|
+
* responses are instant. After it expires, the run suspends.
|
|
570
|
+
*
|
|
571
|
+
* @param seconds - Number of seconds to stay warm (0 to suspend immediately)
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```ts
|
|
575
|
+
* run: async ({ messages, signal }) => {
|
|
576
|
+
* chat.setWarmTimeoutInSeconds(60);
|
|
577
|
+
* return streamText({ model, messages, abortSignal: signal });
|
|
578
|
+
* }
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
581
|
+
function setWarmTimeoutInSeconds(seconds) {
|
|
582
|
+
metadata_js_1.metadata.set(WARM_TIMEOUT_METADATA_KEY, seconds);
|
|
583
|
+
}
|
|
393
584
|
exports.chat = {
|
|
394
585
|
/** Create a chat task. See {@link chatTask}. */
|
|
395
586
|
task: chatTask,
|
|
@@ -397,21 +588,30 @@ exports.chat = {
|
|
|
397
588
|
pipe: pipeChat,
|
|
398
589
|
/** Create a public access token for a chat task. See {@link createChatAccessToken}. */
|
|
399
590
|
createAccessToken: createChatAccessToken,
|
|
591
|
+
/** Override the turn timeout at runtime (duration string). See {@link setTurnTimeout}. */
|
|
592
|
+
setTurnTimeout,
|
|
593
|
+
/** Override the turn timeout at runtime (seconds). See {@link setTurnTimeoutInSeconds}. */
|
|
594
|
+
setTurnTimeoutInSeconds,
|
|
595
|
+
/** Override the warm timeout at runtime. See {@link setWarmTimeoutInSeconds}. */
|
|
596
|
+
setWarmTimeoutInSeconds,
|
|
400
597
|
};
|
|
401
598
|
/**
|
|
402
599
|
* Writes a turn-complete control chunk to the chat output stream.
|
|
403
600
|
* The frontend transport intercepts this to close the ReadableStream for the current turn.
|
|
404
601
|
* @internal
|
|
405
602
|
*/
|
|
406
|
-
async function writeTurnCompleteChunk(chatId) {
|
|
603
|
+
async function writeTurnCompleteChunk(chatId, publicAccessToken) {
|
|
407
604
|
const { waitUntilComplete } = streams_js_1.streams.writer(exports.CHAT_STREAM_KEY, {
|
|
408
605
|
spanName: "turn complete",
|
|
409
606
|
collapsed: true,
|
|
410
607
|
execute: ({ write }) => {
|
|
411
|
-
write({
|
|
608
|
+
write({
|
|
609
|
+
type: "__trigger_turn_complete",
|
|
610
|
+
...(publicAccessToken ? { publicAccessToken } : {}),
|
|
611
|
+
});
|
|
412
612
|
},
|
|
413
613
|
});
|
|
414
|
-
await waitUntilComplete();
|
|
614
|
+
return await waitUntilComplete();
|
|
415
615
|
}
|
|
416
616
|
/**
|
|
417
617
|
* Extracts the text content of the last user message from a UIMessage array.
|
|
@@ -436,4 +636,35 @@ function extractLastUserMessageText(messages) {
|
|
|
436
636
|
}
|
|
437
637
|
return undefined;
|
|
438
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Strips ephemeral OpenAI Responses API `itemId` from a UIMessage's parts.
|
|
641
|
+
*
|
|
642
|
+
* The OpenAI Responses provider attaches `itemId` to message parts via
|
|
643
|
+
* `providerMetadata.openai.itemId`. These IDs are ephemeral — sending them
|
|
644
|
+
* back in a subsequent `streamText` call causes 404s because the provider
|
|
645
|
+
* can't find the referenced item (especially for stopped/partial responses).
|
|
646
|
+
*
|
|
647
|
+
* @internal
|
|
648
|
+
*/
|
|
649
|
+
function stripProviderMetadata(message) {
|
|
650
|
+
if (!message.parts)
|
|
651
|
+
return message;
|
|
652
|
+
return {
|
|
653
|
+
...message,
|
|
654
|
+
parts: message.parts.map((part) => {
|
|
655
|
+
const openai = part.providerMetadata?.openai;
|
|
656
|
+
if (!openai?.itemId)
|
|
657
|
+
return part;
|
|
658
|
+
const { itemId, ...restOpenai } = openai;
|
|
659
|
+
const { openai: _, ...restProviders } = part.providerMetadata;
|
|
660
|
+
return {
|
|
661
|
+
...part,
|
|
662
|
+
providerMetadata: {
|
|
663
|
+
...restProviders,
|
|
664
|
+
...(Object.keys(restOpenai).length > 0 ? { openai: restOpenai } : {}),
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
439
670
|
//# sourceMappingURL=ai.js.map
|