@tangle-network/agent-app 0.47.3 → 0.47.5

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.
@@ -26,15 +26,44 @@ import {
26
26
  sniffBinary
27
27
  } from "../chunk-SUO2R2ET.js";
28
28
  import {
29
- createInteractionAnswerRoute
30
- } from "../chunk-ZSSCVT6H.js";
29
+ DEFAULT_STALE_TURN_LOCK_GRACE_MS,
30
+ DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
31
+ reconcileStaleTurnLock
32
+ } from "../chunk-CCVWR33L.js";
31
33
  import {
32
- parseJsonObjectBody
33
- } from "../chunk-F7VH4G5C.js";
34
+ ModelFailoverExhaustedError,
35
+ buildModelChain,
36
+ isUpstreamUnavailable,
37
+ readHttpStatusHint,
38
+ runWithModelFailover
39
+ } from "../chunk-DEXBRUZR.js";
40
+ import {
41
+ flattenHistory,
42
+ readSandboxBinaryBytes,
43
+ statSandboxFileSize
44
+ } from "../chunk-P6YXUGNA.js";
45
+ import "../chunk-MCYJON3F.js";
46
+ import "../chunk-LWSJK546.js";
47
+ import "../chunk-P5PIAQVS.js";
48
+ import "../chunk-KBJL6LIW.js";
49
+ import "../chunk-TX6S7XXU.js";
50
+ import "../chunk-UGWQLQDS.js";
51
+ import "../chunk-S5SRJJQG.js";
52
+ import "../chunk-JML7WKWU.js";
53
+ import {
54
+ assistantRowIdForTurn,
55
+ createAssistantDraftWriter,
56
+ createChatTurnRoutes,
57
+ isDraftContentEvent,
58
+ rowIdOf,
59
+ storeSupportsDraftPersistence,
60
+ streamChatRouteAsSandboxEvents
61
+ } from "../chunk-LYAYNWDS.js";
62
+ import "../chunk-ZSSCVT6H.js";
63
+ import "../chunk-F7VH4G5C.js";
34
64
  import {
35
65
  attachmentInputToPart,
36
66
  attachmentKindForMime,
37
- mentionInputToPart,
38
67
  toChatMessageParts
39
68
  } from "../chunk-IRYULX3R.js";
40
69
  import "../chunk-ZVEEWGDK.js";
@@ -59,10 +88,7 @@ import {
59
88
  promptPartsByteSize,
60
89
  validateSandboxMentionPath
61
90
  } from "../chunk-DXHUQ5NO.js";
62
- import {
63
- normalizeClientTurnId,
64
- resolveChatTurn
65
- } from "../chunk-JEZJ6HTF.js";
91
+ import "../chunk-JEZJ6HTF.js";
66
92
  import {
67
93
  asRecord,
68
94
  asString,
@@ -90,1050 +116,10 @@ import {
90
116
  parsePlanSubmittedEvent,
91
117
  planToPersistedPart
92
118
  } from "../chunk-YJMCRXQQ.js";
93
- import {
94
- DEFAULT_STALE_TURN_LOCK_GRACE_MS,
95
- DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
96
- reconcileStaleTurnLock
97
- } from "../chunk-CCVWR33L.js";
98
119
  import {
99
120
  coalesceDeltas,
100
- createBufferedTurnTap,
101
- replayTurnEvents,
102
- stampReplaySeq
121
+ createBufferedTurnTap
103
122
  } from "../chunk-ES3I3UGP.js";
104
- import {
105
- ModelFailoverExhaustedError,
106
- buildModelChain,
107
- isUpstreamUnavailable,
108
- readHttpStatusHint,
109
- runWithModelFailover
110
- } from "../chunk-DEXBRUZR.js";
111
- import {
112
- flattenHistory,
113
- readSandboxBinaryBytes,
114
- statSandboxFileSize
115
- } from "../chunk-P6YXUGNA.js";
116
- import "../chunk-MCYJON3F.js";
117
- import "../chunk-LWSJK546.js";
118
- import "../chunk-P5PIAQVS.js";
119
- import "../chunk-KBJL6LIW.js";
120
- import "../chunk-TX6S7XXU.js";
121
- import "../chunk-UGWQLQDS.js";
122
- import "../chunk-S5SRJJQG.js";
123
- import "../chunk-JML7WKWU.js";
124
-
125
- // src/chat-routes/turn-routes.ts
126
- import {
127
- deriveExecutionId,
128
- handleChatTurn
129
- } from "@tangle-network/agent-runtime/durable";
130
-
131
- // src/chat-routes/draft-persistence.ts
132
- var CONTENT_EVENT_TYPES = /* @__PURE__ */ new Set([
133
- "text",
134
- "reasoning",
135
- "tool_call",
136
- "tool_result",
137
- "usage",
138
- "notice",
139
- "error",
140
- "file",
141
- "interaction",
142
- "interaction.cancel",
143
- "plan.submitted",
144
- "message.part.updated"
145
- ]);
146
- var DEFAULT_INTERVAL_MS = 2e3;
147
- var DEFAULT_BACKOFF_BYTES = 262144;
148
- var DEFAULT_MAX_DRAFT_TOOL_OUTPUT_BYTES = 32768;
149
- function isDraftContentEvent(event) {
150
- return typeof event?.type === "string" && CONTENT_EVENT_TYPES.has(event.type);
151
- }
152
- function capDraftToolOutput(part, maxBytes) {
153
- if (maxBytes <= 0) return part;
154
- if (String(part.type ?? "") !== "tool") return part;
155
- const state = part.state;
156
- if (!state || typeof state !== "object") return part;
157
- const record = state;
158
- const output = record.output;
159
- if (output === void 0 || output === null) return part;
160
- const serialized = typeof output === "string" ? output : safeStringify(output);
161
- if (serialized.length <= maxBytes) return part;
162
- const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
163
- return {
164
- ...part,
165
- state: {
166
- ...record,
167
- output: `${serialized.slice(0, maxBytes)}\u2026[draft-truncated ${serialized.length - maxBytes} chars]`,
168
- metadata: { ...metadata, draftTruncated: true }
169
- }
170
- };
171
- }
172
- function safeStringify(value) {
173
- try {
174
- return JSON.stringify(value) ?? "";
175
- } catch {
176
- return String(value);
177
- }
178
- }
179
- function createAssistantDraftWriter(options) {
180
- const log = options.log ?? (() => {
181
- });
182
- const baseIntervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
183
- const backoffBytes = options.backoffBytes ?? DEFAULT_BACKOFF_BYTES;
184
- const maxToolOutput = options.maxDraftToolOutputBytes ?? DEFAULT_MAX_DRAFT_TOOL_OUTPUT_BYTES;
185
- let dirty = false;
186
- let closed = false;
187
- let inFlight;
188
- let lastWriteAt = 0;
189
- let lastBlobBytes = 0;
190
- let rowId;
191
- let writes = 0;
192
- function currentIntervalMs() {
193
- if (lastBlobBytes >= backoffBytes * 10) return baseIntervalMs * 5;
194
- if (lastBlobBytes >= backoffBytes) return Math.round(baseIntervalMs * 2.5);
195
- return baseIntervalMs;
196
- }
197
- async function projectValues(snapshot) {
198
- const transform = options.transformText;
199
- const content = transform ? await transform(snapshot.content) : snapshot.content;
200
- let parts;
201
- if (snapshot.parts) {
202
- const capped = snapshot.parts.map((part) => capDraftToolOutput(part, maxToolOutput));
203
- const redacted = transform ? await Promise.all(
204
- capped.map(
205
- async (part) => String(part.type ?? "") === "text" ? { ...part, text: await transform(String(part.text ?? "")) } : part
206
- )
207
- ) : capped;
208
- parts = toChatMessageParts(redacted);
209
- }
210
- const usage = snapshot.usage ?? {};
211
- return {
212
- content,
213
- ...parts && parts.length > 0 ? { parts } : {},
214
- ...snapshot.model ? { model: snapshot.model } : {},
215
- ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
216
- ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
217
- ...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {},
218
- ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
219
- ...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
220
- ...usage.costUsd !== void 0 ? { costUsd: usage.costUsd } : {}
221
- };
222
- }
223
- async function adoptRow() {
224
- if (rowId) return rowId;
225
- const existing = (await options.store.listMessages(options.threadId)).find(
226
- (message) => message.id === options.messageId
227
- );
228
- if (existing) rowId = existing.id;
229
- return rowId;
230
- }
231
- async function writeOnce(values) {
232
- if (await adoptRow()) {
233
- await options.store.updateMessage(rowId, values);
234
- writes += 1;
235
- return;
236
- }
237
- const inserted = await options.store.appendMessage({
238
- id: options.messageId,
239
- threadId: options.threadId,
240
- role: "assistant",
241
- ...values
242
- });
243
- rowId = rowIdOf(inserted) ?? options.messageId;
244
- writes += 1;
245
- }
246
- function trigger() {
247
- if (closed) return;
248
- if (!dirty) return;
249
- if (inFlight) return;
250
- const now = Date.now();
251
- if (lastWriteAt !== 0 && now - lastWriteAt < currentIntervalMs()) return;
252
- const snapshot = options.snapshot();
253
- if (!snapshot) return;
254
- if (!snapshot.content && (!snapshot.parts || snapshot.parts.length === 0)) return;
255
- dirty = false;
256
- lastWriteAt = now;
257
- inFlight = (async () => {
258
- try {
259
- const values = await projectValues(snapshot);
260
- lastBlobBytes = values.parts ? safeStringify(values.parts).length : 0;
261
- await writeOnce(values);
262
- } catch (err) {
263
- log("[chat-routes] incremental assistant persistence failed", {
264
- messageId: options.messageId,
265
- error: err instanceof Error ? err.message : String(err)
266
- });
267
- } finally {
268
- inFlight = void 0;
269
- if (dirty && !closed) trigger();
270
- }
271
- })();
272
- }
273
- return {
274
- notify(event) {
275
- if (closed) return;
276
- if (isDraftContentEvent(event)) dirty = true;
277
- trigger();
278
- },
279
- async close() {
280
- closed = true;
281
- if (inFlight) await inFlight;
282
- },
283
- async finalize(values) {
284
- closed = true;
285
- if (inFlight) await inFlight;
286
- await writeOnce(values);
287
- },
288
- rowId: () => rowId,
289
- async discard() {
290
- closed = true;
291
- if (inFlight) await inFlight;
292
- if (!options.store.deleteMessage) return;
293
- try {
294
- if (!await adoptRow()) return;
295
- await options.store.deleteMessage(rowId);
296
- rowId = void 0;
297
- } catch (err) {
298
- log("[chat-routes] draft assistant row discard failed", {
299
- messageId: options.messageId,
300
- error: err instanceof Error ? err.message : String(err)
301
- });
302
- }
303
- },
304
- writeCount: () => writes
305
- };
306
- }
307
- function storeSupportsDraftPersistence(store) {
308
- return typeof store.updateMessage === "function";
309
- }
310
- function rowIdOf(inserted) {
311
- const id = inserted?.id;
312
- return typeof id === "string" && id ? id : null;
313
- }
314
- function assistantRowIdForTurn(turnKey) {
315
- return `assistant:${turnKey}`;
316
- }
317
-
318
- // src/chat-routes/turn-routes.ts
319
- function failureReasonOf(data) {
320
- if (!data) return void 0;
321
- const message = data.message ?? data.error ?? data.reason;
322
- if (typeof message === "string" && message.length > 0) return message;
323
- return void 0;
324
- }
325
- function errorResponse(err) {
326
- return Response.json({ code: err.code, error: err.message }, { status: err.status });
327
- }
328
- function validateTurnBody(body, maxInlinePartBytes) {
329
- const threadId = typeof body.threadId === "string" ? body.threadId.trim() : "";
330
- if (!threadId) throw new ChatTurnInputError("Missing threadId");
331
- const rawContent = body.content ?? body.message ?? "";
332
- if (typeof rawContent !== "string") throw new ChatTurnInputError("content must be a string");
333
- const content = rawContent.trim();
334
- const fileParts = parseChatTurnParts(body.parts);
335
- const mentions = parseFileMentions(body.mentions);
336
- const hasAttachments = Array.isArray(body.attachments) && body.attachments.length > 0;
337
- if (!content && fileParts.length === 0 && mentions.length === 0 && !hasAttachments) {
338
- throw new ChatTurnInputError("Missing content (send text, parts, mentions, attachments, or any combination)");
339
- }
340
- assertPromptPartsWithinCap(fileParts, maxInlinePartBytes);
341
- let turnId;
342
- try {
343
- turnId = normalizeClientTurnId(body.turnId);
344
- } catch (err) {
345
- throw new ChatTurnInputError(err instanceof Error ? err.message : "Invalid turnId");
346
- }
347
- return {
348
- // The VALIDATED, deduped mention list replaces the raw one on the payload,
349
- // so every downstream seam (`authorize`, `contextGate`, `beforeTurn`,
350
- // `produce`) reads checked paths and never the request's own.
351
- payload: { ...body, threadId, content, mentions },
352
- content,
353
- fileParts,
354
- mentions,
355
- turnId
356
- };
357
- }
358
- function userPartsWithFiles(userParts, fileParts, mentions) {
359
- return toChatMessageParts([
360
- ...userParts,
361
- ...fileParts.map((part) => ({ ...part })),
362
- ...mentions.map((mention) => ({ ...mentionInputToPart(mention) }))
363
- ]);
364
- }
365
- async function* tapRawEvents(source, onRawEvent, log) {
366
- for await (const event of source) {
367
- try {
368
- await onRawEvent(event);
369
- } catch (err) {
370
- log("[chat-routes] onRawEvent failed", { error: err instanceof Error ? err.message : String(err) });
371
- }
372
- yield event;
373
- }
374
- }
375
- async function* withStreamHeartbeat(source, intervalMs, makeEvent) {
376
- const iterator = source[Symbol.asyncIterator]();
377
- try {
378
- let pending = iterator.next();
379
- let windowStart = Date.now();
380
- let tick = 0;
381
- for (; ; ) {
382
- let timer;
383
- let winner;
384
- try {
385
- const heartbeat = new Promise((resolve) => {
386
- timer = setTimeout(() => resolve("heartbeat"), intervalMs);
387
- });
388
- winner = await Promise.race([pending.then(() => "event"), heartbeat]);
389
- } finally {
390
- if (timer !== void 0) clearTimeout(timer);
391
- }
392
- if (winner === "heartbeat") {
393
- tick += 1;
394
- yield makeEvent({ elapsedMs: Date.now() - windowStart, tick });
395
- continue;
396
- }
397
- const result = await pending;
398
- if (result.done) return;
399
- yield result.value;
400
- pending = iterator.next();
401
- windowStart = Date.now();
402
- tick = 0;
403
- }
404
- } finally {
405
- await iterator.return?.();
406
- }
407
- }
408
- function createChatTurnRoutes(options) {
409
- const log = options.log ?? ((message, meta) => console.error(message, meta ?? ""));
410
- const draftStore = options.store;
411
- if (options.incrementalPersistence && !storeSupportsDraftPersistence(draftStore)) {
412
- throw new Error(
413
- "incrementalPersistence requires a store with updateMessage() \u2014 `/chat-store`'s createChatStore has it; a product store must implement it or omit the option"
414
- );
415
- }
416
- const draftTuning = options.incrementalPersistence === false || !storeSupportsDraftPersistence(draftStore) ? null : options.incrementalPersistence ?? {};
417
- async function turn(request, ctx) {
418
- const [rawBody, badBody] = await parseJsonObjectBody(request);
419
- if (badBody) return badBody;
420
- let parsed;
421
- try {
422
- parsed = validateTurnBody(rawBody, options.maxInlinePartBytes);
423
- } catch (err) {
424
- if (err instanceof ChatTurnInputError) return errorResponse(err);
425
- throw err;
426
- }
427
- const { payload, content, fileParts, mentions, turnId } = parsed;
428
- const auth = await options.authorize({ request, intent: "turn", body: payload });
429
- if (!auth.ok) return auth.response;
430
- const { tenantId, userId, context } = auth;
431
- const existingMessages = (await options.store.listMessages(payload.threadId)).map((m) => ({
432
- id: m.id,
433
- role: m.role,
434
- content: m.content,
435
- parts: m.parts ?? null
436
- }));
437
- let hasRunningTurn = false;
438
- if (draftTuning && existingMessages.at(-1)?.role === "assistant") {
439
- try {
440
- hasRunningTurn = (await options.turnStore.listRunning?.(payload.threadId) ?? []).length > 0;
441
- } catch (err) {
442
- log("[chat-routes] listRunning probe failed; treating the thread as idle", {
443
- threadId: payload.threadId,
444
- error: err instanceof Error ? err.message : String(err)
445
- });
446
- }
447
- }
448
- const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId, hasRunningTurn });
449
- const identity = {
450
- tenantId,
451
- sessionId: payload.threadId,
452
- userId,
453
- turnIndex: chatTurn.turnIndex
454
- };
455
- const executionId = deriveExecutionId({
456
- projectId: options.projectId,
457
- sessionId: payload.threadId,
458
- turnIndex: chatTurn.turnIndex
459
- });
460
- const turnStreamId = crypto.randomUUID();
461
- const prompt = fileParts.length === 0 ? content : content ? [{ type: "text", text: content }, ...fileParts] : [...fileParts];
462
- let produceArgs = {
463
- request,
464
- body: payload,
465
- identity,
466
- context,
467
- prompt,
468
- executionId,
469
- turnStreamId,
470
- priorMessages: chatTurn.priorMessages,
471
- ...ctx?.executionLimits ? { executionLimits: ctx.executionLimits } : {}
472
- };
473
- let lockAcquired = false;
474
- let lockHandle;
475
- let lockReleased = false;
476
- const releaseLock = async () => {
477
- if (!lockAcquired || lockReleased) return;
478
- lockReleased = true;
479
- try {
480
- await options.turnLock.release(lockHandle);
481
- } catch (err) {
482
- log("[chat-routes] turnLock.release failed", {
483
- turnId: turnStreamId,
484
- error: err instanceof Error ? err.message : String(err)
485
- });
486
- }
487
- };
488
- if (options.turnLock) {
489
- const acquired = await options.turnLock.acquire(produceArgs);
490
- if (!acquired.acquired) return acquired.response;
491
- lockAcquired = true;
492
- lockHandle = acquired.handle;
493
- }
494
- let producer;
495
- let draft;
496
- let assistantMessageId;
497
- const assistantRowId = () => assistantMessageId ?? draft?.rowId() ?? null;
498
- let runFailed = false;
499
- let lastFailureData;
500
- let turnStartedAtMs = 0;
501
- let turnStarted = false;
502
- let lifecycleSettled = false;
503
- let gatedTurn = false;
504
- const fireTerminalLifecycle = async (failed, terminalError) => {
505
- if (lifecycleSettled) return;
506
- lifecycleSettled = true;
507
- const lifecycle = options.lifecycle;
508
- if (!lifecycle) return;
509
- const durationMs = Date.now() - turnStartedAtMs;
510
- try {
511
- if (failed) {
512
- await lifecycle.onTurnError?.({
513
- identity,
514
- executionId,
515
- turnStreamId,
516
- context,
517
- durationMs,
518
- error: terminalError ?? lastFailureData ?? new Error("chat turn failed")
519
- });
520
- } else {
521
- const failoverInfo = producer?.modelFailover?.();
522
- const attribution = producer?.modelAttribution?.();
523
- await lifecycle.onTurnComplete?.({
524
- identity,
525
- executionId,
526
- turnStreamId,
527
- context,
528
- durationMs,
529
- finalText: producer?.finalText() ?? "",
530
- usage: producer?.usage?.() ?? {},
531
- assistantMessageId: assistantRowId(),
532
- ...gatedTurn ? { gated: true } : {},
533
- ...producer?.model ? { model: producer.model } : {},
534
- ...attribution?.requestedModel ? { requestedModel: attribution.requestedModel } : {},
535
- ...attribution?.servedModel ? { servedModel: attribution.servedModel } : {},
536
- ...attribution?.servedProvider ? { servedProvider: attribution.servedProvider } : {},
537
- ...attribution?.servedSource ? { servedSource: attribution.servedSource } : {},
538
- ...failoverInfo ? { modelFailover: failoverInfo } : {}
539
- });
540
- }
541
- } catch (err) {
542
- log("[chat-routes] lifecycle terminal hook failed", {
543
- turnId: turnStreamId,
544
- error: err instanceof Error ? err.message : String(err)
545
- });
546
- }
547
- };
548
- try {
549
- const insertUserMessage = chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true);
550
- let userMessageId = chatTurn.reusedUserMessageId ?? null;
551
- if (insertUserMessage) {
552
- userMessageId = rowIdOf(
553
- await options.store.appendMessage({
554
- threadId: payload.threadId,
555
- role: "user",
556
- content,
557
- parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions)
558
- })
559
- );
560
- }
561
- produceArgs = { ...produceArgs, userMessageId };
562
- if (options.contextGate) {
563
- const gate = await options.contextGate(produceArgs);
564
- if (gate.proceed === "handoff") {
565
- await releaseLock();
566
- return gate.response;
567
- }
568
- if (!gate.proceed) {
569
- await releaseLock();
570
- gatedTurn = true;
571
- turnStartedAtMs = Date.now();
572
- if (options.lifecycle?.onTurnStart) {
573
- try {
574
- await options.lifecycle.onTurnStart({
575
- identity,
576
- executionId,
577
- turnStreamId,
578
- context,
579
- startedAt: turnStartedAtMs
580
- });
581
- } catch (err) {
582
- log("[chat-routes] lifecycle.onTurnStart failed", {
583
- turnId: turnStreamId,
584
- error: err instanceof Error ? err.message : String(err)
585
- });
586
- }
587
- }
588
- await fireTerminalLifecycle(false, void 0);
589
- return gate.response;
590
- }
591
- }
592
- if (options.beforeTurn) {
593
- const patch = await options.beforeTurn(produceArgs);
594
- if (patch) produceArgs = { ...produceArgs, ...patch };
595
- }
596
- const tap = createBufferedTurnTap({
597
- store: options.turnStore,
598
- turnId: turnStreamId,
599
- scopeId: payload.threadId,
600
- coalesce: options.coalesceTurnEvents ?? coalesceDeltas
601
- });
602
- const turnMarker = { type: "turn", turnId: turnStreamId };
603
- await tap.onEvent(turnMarker);
604
- if (draftTuning) {
605
- draft = createAssistantDraftWriter({
606
- ...draftTuning,
607
- store: draftStore,
608
- threadId: payload.threadId,
609
- messageId: options.draftMessageId ? options.draftMessageId({ identity, executionId, threadId: payload.threadId }) : assistantRowIdForTurn(executionId),
610
- snapshot: () => {
611
- if (!producer) return null;
612
- return {
613
- content: producer.finalText(),
614
- ...producer.draftParts ? { parts: producer.draftParts() } : {},
615
- ...producer.usage ? { usage: producer.usage() } : {},
616
- ...producer.model ? { model: producer.model } : {}
617
- };
618
- },
619
- ...options.transformFinalText ? { transformText: options.transformFinalText } : {},
620
- log
621
- });
622
- }
623
- turnStartedAtMs = Date.now();
624
- turnStarted = true;
625
- if (options.lifecycle?.onTurnStart) {
626
- try {
627
- await options.lifecycle.onTurnStart({
628
- identity,
629
- executionId,
630
- turnStreamId,
631
- context,
632
- startedAt: turnStartedAtMs
633
- });
634
- } catch (err) {
635
- log("[chat-routes] lifecycle.onTurnStart failed", {
636
- turnId: turnStreamId,
637
- error: err instanceof Error ? err.message : String(err)
638
- });
639
- }
640
- }
641
- const result = handleChatTurn({
642
- identity,
643
- waitUntil: ctx?.waitUntil,
644
- log,
645
- hooks: {
646
- // The engine wants a synchronous producer; box resolution is async —
647
- // defer it into the generator's first pull.
648
- produce: () => ({
649
- stream: (async function* () {
650
- producer = await options.produce(produceArgs);
651
- let source = producer.stream;
652
- if (ctx?.cancelOnDisconnect) {
653
- source = abortOnSignal(source, request.signal);
654
- }
655
- if (options.onRawEvent) {
656
- source = tapRawEvents(source, (event) => options.onRawEvent(event, context), log);
657
- }
658
- if (options.heartbeat) {
659
- source = withStreamHeartbeat(source, options.heartbeat.intervalMs, options.heartbeat.event);
660
- }
661
- for await (const event of source) yield event;
662
- })(),
663
- finalText: () => producer?.finalText() ?? ""
664
- }),
665
- onEvent: async (event) => {
666
- if (event.type === "session.run.failed" || event.type === "error") {
667
- runFailed = true;
668
- lastFailureData = event.data;
669
- }
670
- await tap.onEvent(event);
671
- draft?.notify(event);
672
- if (options.onEvent) await options.onEvent(event, context);
673
- },
674
- ...options.transformFinalText ? { transformFinalText: options.transformFinalText } : {},
675
- persistAssistantMessage: async ({ finalText }) => {
676
- if (ctx?.cancelOnDisconnect && request.signal.aborted) {
677
- await draft?.discard();
678
- assistantMessageId = null;
679
- return;
680
- }
681
- const rawParts = producer?.assistantParts ? producer.assistantParts() : void 0;
682
- const projected = rawParts && options.transformFinalText ? await Promise.all(
683
- rawParts.map(
684
- async (part) => String(part.type ?? "") === "text" ? { ...part, text: await options.transformFinalText(String(part.text ?? "")) } : part
685
- )
686
- ) : rawParts;
687
- const parts = projected ? toChatMessageParts(projected) : void 0;
688
- if (!finalText.trim() && (!parts || parts.length === 0)) {
689
- await draft?.discard();
690
- assistantMessageId = draft?.rowId() ?? null;
691
- return;
692
- }
693
- const usage = producer?.usage?.() ?? {};
694
- const attribution = producer?.modelAttribution?.();
695
- const values = {
696
- content: finalText,
697
- ...parts && parts.length > 0 ? { parts } : {},
698
- ...producer?.model ? { model: producer.model } : {},
699
- ...attribution?.requestedModel ? { requestedModel: attribution.requestedModel } : {},
700
- ...attribution?.servedModel ? { servedModel: attribution.servedModel } : {},
701
- ...attribution?.servedProvider ? { servedProvider: attribution.servedProvider } : {},
702
- ...attribution?.servedSource ? { servedSource: attribution.servedSource } : {},
703
- ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
704
- ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
705
- ...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {},
706
- ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
707
- ...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
708
- ...usage.costUsd !== void 0 ? { costUsd: usage.costUsd } : {}
709
- };
710
- if (draft) {
711
- await draft.finalize(values);
712
- assistantMessageId = draft.rowId() ?? null;
713
- return;
714
- }
715
- assistantMessageId = rowIdOf(
716
- await options.store.appendMessage({
717
- threadId: payload.threadId,
718
- role: "assistant",
719
- ...values
720
- })
721
- );
722
- },
723
- ...options.onTurnComplete ? {
724
- // Wired into the engine's completion hook, which fires only when
725
- // the stream ended without throwing. A terminal error EVENT
726
- // (not a throw) still lands here — so surface `runFailed` so the
727
- // product skips billing an errored turn instead of marking it
728
- // complete with empty text.
729
- onTurnComplete: ({ identity: turnIdentity, finalText }) => {
730
- const failoverInfo = producer?.modelFailover?.();
731
- const attribution = producer?.modelAttribution?.();
732
- return options.onTurnComplete({
733
- identity: turnIdentity,
734
- finalText,
735
- context,
736
- failed: runFailed,
737
- assistantMessageId: assistantRowId(),
738
- ...runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {},
739
- ...producer?.model ? { model: producer.model } : {},
740
- ...attribution?.requestedModel ? { requestedModel: attribution.requestedModel } : {},
741
- ...attribution?.servedModel ? { servedModel: attribution.servedModel } : {},
742
- ...attribution?.servedProvider ? { servedProvider: attribution.servedProvider } : {},
743
- ...attribution?.servedSource ? { servedSource: attribution.servedSource } : {},
744
- ...failoverInfo ? { modelFailover: failoverInfo } : {}
745
- });
746
- }
747
- } : {},
748
- ...options.traceFlush ? { traceFlush: () => options.traceFlush(context) } : {}
749
- }
750
- });
751
- const [clientBody, drainBody] = result.body.tee();
752
- const drained = (async () => {
753
- const reader = drainBody.getReader();
754
- let drainError;
755
- try {
756
- for (; ; ) {
757
- const { done } = await reader.read();
758
- if (done) break;
759
- }
760
- } catch (err) {
761
- drainError = err;
762
- log("[chat-routes] turn drain failed", {
763
- turnId: turnStreamId,
764
- error: err instanceof Error ? err.message : String(err)
765
- });
766
- }
767
- const failed = runFailed || drainError !== void 0;
768
- if (ctx?.cancelOnDisconnect && request.signal.aborted) {
769
- await draft?.discard();
770
- } else {
771
- await draft?.close();
772
- }
773
- try {
774
- await tap.done(failed ? "error" : "complete");
775
- } catch (err) {
776
- log("[chat-routes] turn buffer finalize failed", {
777
- turnId: turnStreamId,
778
- error: err instanceof Error ? err.message : String(err)
779
- });
780
- }
781
- await fireTerminalLifecycle(failed, drainError);
782
- await releaseLock();
783
- })();
784
- if (ctx?.waitUntil) ctx.waitUntil(drained);
785
- else void drained.catch(() => {
786
- });
787
- const encoder = new TextEncoder();
788
- const marker = new ReadableStream({
789
- start(controller) {
790
- controller.enqueue(encoder.encode(`${JSON.stringify(turnMarker)}
791
- `));
792
- controller.close();
793
- }
794
- });
795
- const body = concatStreams([marker, clientBody]);
796
- return new Response(body, {
797
- headers: {
798
- "Content-Type": result.contentType,
799
- "Cache-Control": "no-cache",
800
- "X-Turn-Id": turnStreamId
801
- }
802
- });
803
- } catch (err) {
804
- if (turnStarted) await fireTerminalLifecycle(true, err);
805
- await releaseLock();
806
- throw err;
807
- }
808
- }
809
- async function replay(request, params) {
810
- const turnId = params.turnId?.trim();
811
- if (!turnId) return Response.json({ error: "Missing turnId" }, { status: 400 });
812
- const auth = await options.authorize({ request, intent: "replay", turnId });
813
- if (!auth.ok) return auth.response;
814
- const fromSeqRaw = new URL(request.url).searchParams.get("fromSeq");
815
- const fromSeq = fromSeqRaw ? Math.max(0, Math.trunc(Number(fromSeqRaw)) || 0) : 0;
816
- const encoder = new TextEncoder();
817
- const events = replayTurnEvents({
818
- store: options.turnStore,
819
- turnId,
820
- fromSeq,
821
- ...options.replay?.pollMs !== void 0 ? { pollMs: options.replay.pollMs } : {},
822
- ...options.replay?.timeoutMs !== void 0 ? { timeoutMs: options.replay.timeoutMs } : {}
823
- });
824
- const body = new ReadableStream({
825
- async pull(controller) {
826
- const { done, value } = await events.next();
827
- if (done) {
828
- controller.close();
829
- return;
830
- }
831
- controller.enqueue(encoder.encode(`${stampReplaySeq(value)}
832
- `));
833
- },
834
- cancel() {
835
- void events.return(void 0);
836
- }
837
- });
838
- return new Response(body, {
839
- headers: {
840
- "Content-Type": "application/x-ndjson",
841
- "Cache-Control": "no-cache"
842
- }
843
- });
844
- }
845
- async function running(request) {
846
- const threadId = new URL(request.url).searchParams.get("threadId")?.trim();
847
- if (!threadId) return Response.json({ error: "Missing threadId" }, { status: 400 });
848
- const auth = await options.authorize({ request, intent: "running", threadId });
849
- if (!auth.ok) return auth.response;
850
- const ids = await options.turnStore.listRunning?.(threadId) ?? [];
851
- return Response.json({ running: ids });
852
- }
853
- return {
854
- turn,
855
- replay,
856
- running,
857
- interactions: options.interactions ? createInteractionAnswerRoute(options.interactions) : null
858
- };
859
- }
860
- function concatStreams(streams) {
861
- let index = 0;
862
- let reader = null;
863
- return new ReadableStream({
864
- async pull(controller) {
865
- for (; ; ) {
866
- if (!reader) {
867
- const next = streams[index++];
868
- if (!next) {
869
- controller.close();
870
- return;
871
- }
872
- reader = next.getReader();
873
- }
874
- const { done, value } = await reader.read();
875
- if (done) {
876
- reader = null;
877
- continue;
878
- }
879
- controller.enqueue(value);
880
- return;
881
- }
882
- },
883
- async cancel(reason) {
884
- await reader?.cancel(reason);
885
- for (const stream of streams.slice(index)) await stream.cancel(reason);
886
- }
887
- });
888
- }
889
- async function* abortOnSignal(source, signal) {
890
- const iterator = source[Symbol.asyncIterator]();
891
- try {
892
- for (; ; ) {
893
- const next = await nextWithAbort(iterator, signal);
894
- if (next.done) return;
895
- yield next.value;
896
- }
897
- } finally {
898
- await closeIteratorAfterAbort(iterator);
899
- }
900
- }
901
- function nextWithAbort(iterator, signal) {
902
- if (signal.aborted) return Promise.reject(new Error("chat turn cancelled"));
903
- return new Promise((resolve, reject) => {
904
- let settled = false;
905
- const cleanup = () => signal.removeEventListener("abort", onAbort);
906
- const onAbort = () => {
907
- if (settled) return;
908
- settled = true;
909
- cleanup();
910
- reject(new Error("chat turn cancelled"));
911
- };
912
- signal.addEventListener("abort", onAbort, { once: true });
913
- if (signal.aborted) {
914
- onAbort();
915
- return;
916
- }
917
- Promise.resolve().then(() => iterator.next()).then(
918
- (result) => {
919
- if (settled) return;
920
- settled = true;
921
- cleanup();
922
- resolve(result);
923
- },
924
- (error) => {
925
- if (settled) return;
926
- settled = true;
927
- cleanup();
928
- reject(error);
929
- }
930
- );
931
- });
932
- }
933
- var ABORT_CLEANUP_TIMEOUT_MS = 50;
934
- async function closeIteratorAfterAbort(iterator) {
935
- let closing;
936
- try {
937
- const result = iterator.return?.();
938
- if (result) closing = Promise.resolve(result);
939
- } catch {
940
- return;
941
- }
942
- if (!closing) return;
943
- let timeout;
944
- try {
945
- await Promise.race([
946
- Promise.resolve(closing).catch(() => void 0),
947
- new Promise((resolve) => {
948
- timeout = setTimeout(resolve, ABORT_CLEANUP_TIMEOUT_MS);
949
- })
950
- ]);
951
- } finally {
952
- if (timeout !== void 0) clearTimeout(timeout);
953
- }
954
- }
955
-
956
- // src/chat-routes/gateway-adapter.ts
957
- async function* streamChatRouteAsSandboxEvents(options) {
958
- const parentSignal = options.signal ?? options.request.signal;
959
- if (parentSignal.aborted) return;
960
- let reader;
961
- const stopForwarding = () => void reader?.cancel();
962
- parentSignal.addEventListener("abort", stopForwarding, { once: true });
963
- const request = new Request(options.request.url, {
964
- ...chatTurnRequestInit(options.payload),
965
- headers: options.request.headers
966
- });
967
- request.headers.set("Content-Type", "application/json");
968
- let complete = false;
969
- try {
970
- const response = await options.routes.turn(
971
- request,
972
- {
973
- ...options.waitUntil ? { waitUntil: options.waitUntil } : {},
974
- ...options.executionLimits ? { executionLimits: options.executionLimits } : {}
975
- }
976
- );
977
- if (!response.ok) {
978
- await response.body?.cancel().catch(() => void 0);
979
- throw new Error(`chat route rejected the gateway turn (${response.status})`);
980
- }
981
- if (!response.body) throw new Error("chat route returned no stream");
982
- reader = response.body.getReader();
983
- if (parentSignal.aborted) {
984
- await reader.cancel().catch(() => void 0);
985
- return;
986
- }
987
- const decoder = new TextDecoder();
988
- let buffer = "";
989
- for (; ; ) {
990
- const chunk = await reader.read();
991
- if (chunk.done) {
992
- complete = true;
993
- buffer += decoder.decode();
994
- const event = toSandboxEvent(parseEvent(buffer));
995
- if (event) yield event;
996
- break;
997
- }
998
- buffer += decoder.decode(chunk.value, { stream: true });
999
- const lines = buffer.split("\n");
1000
- buffer = lines.pop() ?? "";
1001
- for (const line of lines) {
1002
- const event = toSandboxEvent(parseEvent(line));
1003
- if (event) yield event;
1004
- }
1005
- }
1006
- } finally {
1007
- if (!complete) await reader?.cancel().catch(() => void 0);
1008
- reader?.releaseLock();
1009
- parentSignal.removeEventListener("abort", stopForwarding);
1010
- }
1011
- }
1012
- function parseEvent(line) {
1013
- if (!line.trim()) return null;
1014
- let value;
1015
- try {
1016
- value = JSON.parse(line);
1017
- } catch {
1018
- throw new Error("chat route returned invalid NDJSON");
1019
- }
1020
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1021
- throw new Error("chat route returned a non-object event");
1022
- }
1023
- const event = value;
1024
- if (event.type !== void 0 && typeof event.type !== "string") {
1025
- throw new Error("chat route returned an invalid event type");
1026
- }
1027
- const data = event.data;
1028
- if (data !== void 0 && (!data || typeof data !== "object" || Array.isArray(data))) {
1029
- throw new Error("chat route returned invalid event data");
1030
- }
1031
- const dataUsage = data && data.usage;
1032
- if (dataUsage !== void 0 && (!dataUsage || typeof dataUsage !== "object" || Array.isArray(dataUsage))) {
1033
- throw new Error("chat route returned invalid data.usage");
1034
- }
1035
- if (event.text !== void 0 && typeof event.text !== "string") {
1036
- throw new Error("chat route returned an invalid text event");
1037
- }
1038
- const usage = event.usage;
1039
- if (usage !== void 0 && (!usage || typeof usage !== "object" || Array.isArray(usage))) {
1040
- throw new Error("chat route returned invalid usage");
1041
- }
1042
- if (event.toolName !== void 0 && typeof event.toolName !== "string") {
1043
- throw new Error("chat route returned an invalid tool name");
1044
- }
1045
- return {
1046
- ...typeof event.type === "string" ? { type: event.type } : {},
1047
- ...data ? { data } : {},
1048
- ...typeof event.text === "string" ? { text: event.text } : {},
1049
- ...usage ? { usage } : {},
1050
- ...typeof event.toolName === "string" ? { toolName: event.toolName } : {}
1051
- };
1052
- }
1053
- function isNonNegativeSafeInteger(value) {
1054
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1055
- }
1056
- function aliasedSafeInteger(usage, field, alias) {
1057
- const value = usage[field];
1058
- const aliasValue = alias ? usage[alias] : void 0;
1059
- for (const candidate of [value, aliasValue]) {
1060
- if (candidate !== void 0 && !isNonNegativeSafeInteger(candidate)) {
1061
- throw new Error(`chat route returned invalid usage.${field}`);
1062
- }
1063
- }
1064
- if (value !== void 0 && aliasValue !== void 0 && value !== aliasValue) {
1065
- throw new Error(`chat route returned conflicting usage.${field}`);
1066
- }
1067
- return value ?? aliasValue;
1068
- }
1069
- function aliasedNonNegativeNumber(usage, field, alias) {
1070
- const value = usage[field];
1071
- const aliasValue = alias ? usage[alias] : void 0;
1072
- for (const candidate of [value, aliasValue]) {
1073
- if (candidate !== void 0 && (typeof candidate !== "number" || !Number.isFinite(candidate) || candidate < 0)) {
1074
- throw new Error(`chat route returned invalid usage.${field}`);
1075
- }
1076
- }
1077
- if (value !== void 0 && aliasValue !== void 0 && value !== aliasValue) {
1078
- throw new Error(`chat route returned conflicting usage.${field}`);
1079
- }
1080
- return value ?? aliasValue;
1081
- }
1082
- function providerUsageUpdate(value) {
1083
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1084
- const source = value;
1085
- const usage = {};
1086
- const inputTokens = aliasedSafeInteger(source, "inputTokens", "promptTokens");
1087
- const outputTokens = aliasedSafeInteger(source, "outputTokens", "completionTokens");
1088
- const reasoningTokens = aliasedSafeInteger(source, "reasoningTokens");
1089
- const toolTokens = aliasedSafeInteger(source, "toolTokens");
1090
- const toolCallCount = aliasedSafeInteger(source, "toolCallCount");
1091
- const providerCostUsd = aliasedNonNegativeNumber(source, "providerCostUsd", "costUsd");
1092
- if (inputTokens !== void 0) usage.inputTokens = inputTokens;
1093
- if (outputTokens !== void 0) usage.outputTokens = outputTokens;
1094
- if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
1095
- if (toolTokens !== void 0) usage.toolTokens = toolTokens;
1096
- if (toolCallCount !== void 0) usage.toolCallCount = toolCallCount;
1097
- if (providerCostUsd !== void 0) usage.providerCostUsd = providerCostUsd;
1098
- if (source.budgetEnforced !== void 0) {
1099
- if (typeof source.budgetEnforced !== "boolean") {
1100
- throw new Error("chat route returned invalid usage.budgetEnforced");
1101
- }
1102
- usage.budgetEnforced = source.budgetEnforced;
1103
- }
1104
- return Object.keys(usage).length > 0 ? usage : null;
1105
- }
1106
- function toSandboxEvent(event) {
1107
- if (!event) return null;
1108
- if (event.type === "text") {
1109
- if (event.text === void 0) throw new Error("chat route text event had no text");
1110
- return {
1111
- type: "message.part.updated",
1112
- data: { part: { type: "text" }, delta: event.text }
1113
- };
1114
- }
1115
- if (event.type === "reasoning") return null;
1116
- if (event.type === "usage" || event.usage || event.data?.usage) {
1117
- const usage = providerUsageUpdate(event.usage ?? event.data?.usage);
1118
- if (!usage) return null;
1119
- return { type: "usage", data: { usage } };
1120
- }
1121
- if (event.type === "tool_result") {
1122
- if (!event.toolName) throw new Error("chat route tool result had no tool name");
1123
- return { type: "tool_result", data: { tool: { name: event.toolName } } };
1124
- }
1125
- if (event.type === "input-required" && !event.data?.inputRequired) {
1126
- const prompt = typeof event.data?.prompt === "string" ? event.data.prompt : void 0;
1127
- return {
1128
- type: "input-required",
1129
- data: { inputRequired: prompt ? { prompt } : {} }
1130
- };
1131
- }
1132
- return {
1133
- ...event.type ? { type: event.type } : {},
1134
- ...event.data ? { data: event.data } : {}
1135
- };
1136
- }
1137
123
 
1138
124
  // src/chat-routes/model-failover-stream.ts
1139
125
  var TERMINAL_FAILURE_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);