@tangle-network/agent-app 0.47.4 → 0.47.6

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,47 @@ 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";
33
+ import {
34
+ ModelFailoverExhaustedError,
35
+ buildModelChain,
36
+ isUpstreamUnavailable,
37
+ readHttpStatusHint,
38
+ runWithModelFailover
39
+ } from "../chunk-DEXBRUZR.js";
31
40
  import {
32
- parseJsonObjectBody
33
- } from "../chunk-F7VH4G5C.js";
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 {
53
+ ProtectedModelSettlementError
54
+ } from "../chunk-KHRIPDW5.js";
55
+ import "../chunk-JML7WKWU.js";
56
+ import {
57
+ assistantRowIdForTurn,
58
+ createAssistantDraftWriter,
59
+ createChatTurnRoutes,
60
+ isDraftContentEvent,
61
+ rowIdOf,
62
+ storeSupportsDraftPersistence,
63
+ streamChatRouteAsSandboxEvents
64
+ } from "../chunk-LYAYNWDS.js";
65
+ import "../chunk-ZSSCVT6H.js";
66
+ import "../chunk-F7VH4G5C.js";
34
67
  import {
35
68
  attachmentInputToPart,
36
69
  attachmentKindForMime,
37
- mentionInputToPart,
38
70
  toChatMessageParts
39
71
  } from "../chunk-IRYULX3R.js";
40
72
  import "../chunk-ZVEEWGDK.js";
@@ -59,10 +91,7 @@ import {
59
91
  promptPartsByteSize,
60
92
  validateSandboxMentionPath
61
93
  } from "../chunk-DXHUQ5NO.js";
62
- import {
63
- normalizeClientTurnId,
64
- resolveChatTurn
65
- } from "../chunk-JEZJ6HTF.js";
94
+ import "../chunk-JEZJ6HTF.js";
66
95
  import {
67
96
  asRecord,
68
97
  asString,
@@ -90,1050 +119,10 @@ import {
90
119
  parsePlanSubmittedEvent,
91
120
  planToPersistedPart
92
121
  } 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
122
  import {
99
123
  coalesceDeltas,
100
- createBufferedTurnTap,
101
- replayTurnEvents,
102
- stampReplaySeq
124
+ createBufferedTurnTap
103
125
  } 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
126
 
1138
127
  // src/chat-routes/model-failover-stream.ts
1139
128
  var TERMINAL_FAILURE_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
@@ -2051,6 +1040,184 @@ ${diagnostic.userMessage}` : diagnostic.userMessage;
2051
1040
  };
2052
1041
  }
2053
1042
 
1043
+ // src/chat-routes/protected-runtime-producer.ts
1044
+ import { createExecutor, streamAgentTurn } from "@tangle-network/agent-runtime/kernel";
1045
+ import {
1046
+ runProtectedAgentCandidateModelGrant
1047
+ } from "@tangle-network/agent-runtime/candidate-execution";
1048
+ function createProtectedRuntimeChatProducer(options) {
1049
+ if (!Number.isSafeInteger(options.maxToolCalls) || options.maxToolCalls < 0) {
1050
+ throw new Error("maxToolCalls must be a non-negative integer");
1051
+ }
1052
+ options = {
1053
+ ...options,
1054
+ grant: { ...options.grant, resolve: structuredClone(options.grant.resolve), reserve: structuredClone(options.grant.reserve) },
1055
+ tools: options.tools.map((tool) => ({ ...tool, inputSchema: structuredClone(tool.inputSchema) })),
1056
+ priorMessages: options.priorMessages ? structuredClone(options.priorMessages) : void 0
1057
+ };
1058
+ const tools = new Map(options.tools.map((tool) => [tool.name, tool]));
1059
+ if (tools.size !== options.tools.length) throw new Error("Duplicate protected tool name");
1060
+ const profile = structuredClone(options.profile);
1061
+ let toolCalls = 0;
1062
+ let usage = {};
1063
+ let started = false;
1064
+ let model;
1065
+ let budgetEnforced = false;
1066
+ let toolTokens;
1067
+ async function* events() {
1068
+ if (started) throw new Error("A protected chat producer can only execute once");
1069
+ started = true;
1070
+ const controller = new AbortController();
1071
+ options.signal?.throwIfAborted();
1072
+ if (Date.now() >= options.grant.deadlineAtMs) throw new Error("Protected execution deadline expired");
1073
+ const signal = AbortSignal.any([
1074
+ controller.signal,
1075
+ AbortSignal.timeout(Math.max(1, options.grant.deadlineAtMs - Date.now())),
1076
+ ...options.signal ? [options.signal] : []
1077
+ ]);
1078
+ const channel = new TransformStream();
1079
+ const writer = channel.writable.getWriter();
1080
+ const reader = channel.readable.getReader();
1081
+ const emit = (event) => writer.write(event);
1082
+ const execution = runProtectedAgentCandidateModelGrant({
1083
+ ...options.grant,
1084
+ port: {
1085
+ resolve: (input) => options.grant.port.resolve(input),
1086
+ reserveGrant: (input) => options.grant.port.reserveGrant(input),
1087
+ activateGrant: (input) => options.grant.port.activateGrant(input),
1088
+ // Runtime settles failed executions before rethrowing. Retain that receipt too.
1089
+ settleGrant: async (input) => {
1090
+ let auditError;
1091
+ const settlement = await options.grant.port.settleGrant(input).catch((error) => {
1092
+ if (!(error instanceof ProtectedModelSettlementError)) throw error;
1093
+ auditError = error;
1094
+ return error.settlement;
1095
+ });
1096
+ usage = {
1097
+ inputTokens: settlement.calls.reduce((sum, call) => sum + call.accountedInputTokens, 0),
1098
+ outputTokens: settlement.calls.reduce((sum, call) => sum + call.outputTokens, 0),
1099
+ reasoningTokens: settlement.calls.reduce((sum, call) => sum + call.reasoningTokens, 0),
1100
+ costUsd: settlement.calls.reduce((sum, call) => sum + call.costUsdNanos, 0) / 1e9
1101
+ };
1102
+ budgetEnforced = settlement.usageWithinLimits;
1103
+ if (!budgetEnforced) throw new Error("Provider settlement exceeded execution limits");
1104
+ if (auditError) throw auditError;
1105
+ return settlement;
1106
+ }
1107
+ },
1108
+ execute: async ({ activation, resolved }) => {
1109
+ signal.throwIfAborted();
1110
+ if (Date.now() >= options.grant.deadlineAtMs) throw new Error("Protected execution deadline expired");
1111
+ model = resolved.model;
1112
+ if (resolved.provider !== "anthropic" || resolved.reasoningEffort !== "none") {
1113
+ throw new Error("Protected chat currently requires Anthropic without reasoning");
1114
+ }
1115
+ toolTokens = 0;
1116
+ const apiKey = activation.env.OPENAI_API_KEY;
1117
+ const baseUrl = activation.env.OPENAI_BASE_URL;
1118
+ if (!apiKey || !baseUrl) throw new Error("Protected Router activation is missing");
1119
+ const factory = createExecutor({
1120
+ backend: "router-tools",
1121
+ routerBaseUrl: baseUrl,
1122
+ routerKey: apiKey,
1123
+ tools: options.tools.map((tool) => ({
1124
+ type: "function",
1125
+ function: { name: tool.name, description: tool.description, parameters: tool.inputSchema }
1126
+ })),
1127
+ ...options.priorMessages?.length ? { initialMessages: options.priorMessages } : {},
1128
+ executeToolCall: async (name, args) => {
1129
+ signal.throwIfAborted();
1130
+ const tool = tools.get(name);
1131
+ if (!tool) throw new Error("Tool is not authorized for this execution");
1132
+ if (toolCalls >= options.maxToolCalls) {
1133
+ controller.abort(new Error("Execution tool-call limit reached"));
1134
+ signal.throwIfAborted();
1135
+ }
1136
+ toolCalls += 1;
1137
+ const id = `tool-${toolCalls}`;
1138
+ await emit({ type: "tool_call", data: { id, name, arguments: args } });
1139
+ try {
1140
+ const result = await tool.run(args, { signal });
1141
+ await emit({ type: "tool_result", data: { id, name, output: result } });
1142
+ return JSON.stringify(result);
1143
+ } catch (error) {
1144
+ const message = error instanceof Error ? error.message : "Tool failed";
1145
+ await emit({ type: "tool_result", data: { id, name, error: message } });
1146
+ throw error;
1147
+ }
1148
+ }
1149
+ });
1150
+ let completed = false;
1151
+ for await (const event of streamAgentTurn(
1152
+ { kind: "executor", factory, profile },
1153
+ { prompt: options.prompt },
1154
+ {
1155
+ signal,
1156
+ callId: options.grant.reserve.executionId,
1157
+ timeoutMs: Math.max(1, options.grant.deadlineAtMs - Date.now())
1158
+ }
1159
+ )) {
1160
+ if (event.type === "text_delta" || event.type === "reasoning_delta") {
1161
+ await emit({ type: "message.part.updated", data: {
1162
+ part: { id: event.type, type: event.type === "text_delta" ? "text" : "reasoning" },
1163
+ delta: event.text
1164
+ } });
1165
+ }
1166
+ if (event.type === "final") {
1167
+ if (event.status !== "completed") throw new Error(`Protected execution ${event.status}`);
1168
+ completed = true;
1169
+ await emit({ type: "result", data: { finalText: event.text } });
1170
+ }
1171
+ }
1172
+ if (!completed) throw new Error("Protected execution ended without a terminal result");
1173
+ }
1174
+ }).then(async () => {
1175
+ await writer.close();
1176
+ }).catch((error) => writer.abort(error));
1177
+ try {
1178
+ for (; ; ) {
1179
+ const next = await reader.read();
1180
+ if (next.done) break;
1181
+ yield next.value;
1182
+ }
1183
+ await execution;
1184
+ } finally {
1185
+ controller.abort();
1186
+ await reader.cancel().catch(() => void 0);
1187
+ await execution;
1188
+ reader.releaseLock();
1189
+ writer.releaseLock();
1190
+ }
1191
+ }
1192
+ const producer = createSandboxChatProducer({ events: events(), model: profile.model?.default });
1193
+ return {
1194
+ ...producer,
1195
+ stream: (async function* () {
1196
+ let failure;
1197
+ for await (const event of producer.stream) {
1198
+ if (event.type === "error") failure = event;
1199
+ else yield event;
1200
+ }
1201
+ if (usage.inputTokens !== void 0 && usage.outputTokens !== void 0 && usage.costUsd !== void 0) {
1202
+ yield { type: "usage", usage: {
1203
+ promptTokens: usage.inputTokens,
1204
+ completionTokens: usage.outputTokens,
1205
+ providerCostUsd: usage.costUsd,
1206
+ reasoningTokens: usage.reasoningTokens,
1207
+ toolTokens,
1208
+ toolCallCount: toolCalls,
1209
+ budgetEnforced
1210
+ } };
1211
+ }
1212
+ if (failure) yield failure;
1213
+ })(),
1214
+ get model() {
1215
+ return model ?? profile.model?.default;
1216
+ },
1217
+ usage: () => ({ ...usage })
1218
+ };
1219
+ }
1220
+
2054
1221
  // src/chat-routes/detached-turn.ts
2055
1222
  var TERMINAL_ERROR_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
2056
1223
  function errorMessageOf(ev) {
@@ -3597,6 +2764,7 @@ export {
3597
2764
  createAtomicAttachmentWriter,
3598
2765
  createAttachmentUploadRoute,
3599
2766
  createChatTurnRoutes,
2767
+ createProtectedRuntimeChatProducer,
3600
2768
  createSandboxChatProducer,
3601
2769
  createSandboxFileIndexRoute,
3602
2770
  createUploadRoute,