veryfront 0.1.1208 → 0.1.1209

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.
Files changed (27) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/agent/runtime/chat-stream-handler.d.ts +1 -0
  3. package/esm/src/agent/runtime/chat-stream-handler.d.ts.map +1 -1
  4. package/esm/src/agent/runtime/chat-stream-handler.js +536 -418
  5. package/esm/src/html/hydration-script-builder/hydration-runtime.generated.js +1 -1
  6. package/esm/src/html/utils.d.ts.map +1 -1
  7. package/esm/src/html/utils.js +6 -0
  8. package/esm/src/modules/import-map/default-import-map.d.ts.map +1 -1
  9. package/esm/src/modules/import-map/default-import-map.js +2 -0
  10. package/esm/src/platform/compat/http/pinned-fetch.d.ts +1 -1
  11. package/esm/src/react/components/ui/index.d.ts +1 -1
  12. package/esm/src/react/components/ui/index.d.ts.map +1 -1
  13. package/esm/src/react/components/ui/index.js +5 -1
  14. package/esm/src/release-assets/build-executor.d.ts.map +1 -1
  15. package/esm/src/release-assets/build-executor.js +64 -14
  16. package/esm/src/release-assets/manifest-schema.d.ts.map +1 -1
  17. package/esm/src/release-assets/manifest-schema.js +93 -0
  18. package/esm/src/security/http/platform-asset-origins.d.ts +19 -3
  19. package/esm/src/security/http/platform-asset-origins.d.ts.map +1 -1
  20. package/esm/src/security/http/platform-asset-origins.js +20 -2
  21. package/esm/src/security/http/response/security-handler.d.ts.map +1 -1
  22. package/esm/src/security/http/response/security-handler.js +7 -5
  23. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  24. package/esm/src/server/handlers/dev/framework-candidates.generated.js +6 -1
  25. package/esm/src/utils/version-constant.d.ts +1 -1
  26. package/esm/src/utils/version-constant.js +1 -1
  27. package/package.json +8 -8
@@ -297,14 +297,80 @@ export function processStreamInternal(resultOrSource, state, controller, encoder
297
297
  ? new Set(callbacks.availableToolNames)
298
298
  : null;
299
299
  const suppressedToolCallIds = new Set();
300
+ // Provider-executed calls whose input completed but whose result has not
301
+ // arrived yet. While any is outstanding the local-tool commit grace must not
302
+ // truncate the stream: the provider result can arrive after a separate HTTP
303
+ // continuation.
304
+ const pendingProviderExecutedToolCallIds = new Set();
300
305
  const isUnavailableTool = (toolName) => availableToolNames !== null && !availableToolNames.has(toolName);
301
306
  const suppressToolCall = (toolCallId, toolName) => {
302
307
  if (!toolCallId || suppressedToolCallIds.has(toolCallId)) {
303
308
  return;
304
309
  }
305
310
  suppressedToolCallIds.add(toolCallId);
311
+ pendingProviderExecutedToolCallIds.delete(toolCallId);
306
312
  state.suppressedToolCalls.push({ id: toolCallId, name: toolName });
307
313
  };
314
+ /**
315
+ * Record a provider-executed call as outstanding.
316
+ *
317
+ * Only call this where the tool call reaches `inputAvailable: true`. That
318
+ * keeps the invariant `id ∈ pending ⟹ toolCalls.get(id).inputAvailable`, so
319
+ * `finalizeUnresolvedProviderToolCalls` always drains the set. Tracking a
320
+ * call whose input never completed (or one already suppressed, which no
321
+ * later part will resolve) would leave an id nothing removes, disabling the
322
+ * local-tool commit grace for the rest of the step.
323
+ */
324
+ const trackProviderExecutedToolCall = (toolCallId, providerExecuted) => {
325
+ if (!toolCallId || providerExecuted !== true || suppressedToolCallIds.has(toolCallId)) {
326
+ return;
327
+ }
328
+ pendingProviderExecutedToolCallIds.add(toolCallId);
329
+ };
330
+ /**
331
+ * Close out provider-executed calls the provider never resolved.
332
+ *
333
+ * Without a terminal event the call stays `input-available` forever: the UI
334
+ * card spins and persistence judges the part incomplete and drops it.
335
+ * Emitting an error is honest: there genuinely is no content.
336
+ *
337
+ * The synthesized event is stream-only on purpose. Recording it in
338
+ * `state.toolResults` would make the runtime believe the provider answered:
339
+ * `shouldContinueAfterStreamStep()` would flip to true and bill another
340
+ * model call per unresolved call, and the fabricated result would be
341
+ * persisted into model history. The runtime keeps its unchanged view (no
342
+ * result arrived) while the client still gets a terminal part.
343
+ */
344
+ const finalizeUnresolvedProviderToolCalls = () => {
345
+ if (pendingProviderExecutedToolCallIds.size === 0) {
346
+ return;
347
+ }
348
+ // A preliminary result is recorded like any other, so it must not count as
349
+ // proof the provider answered. Reading it as terminal here would skip the
350
+ // synthesized event and strand the call, which is the failure this whole
351
+ // finalizer exists to close.
352
+ const terminalToolCallIds = new Set(state.toolResults
353
+ .filter((result) => result.preliminary !== true)
354
+ .map((result) => result.toolCallId));
355
+ for (const toolCall of state.toolCalls.values()) {
356
+ if (!pendingProviderExecutedToolCallIds.has(toolCall.id))
357
+ continue;
358
+ if (toolCall.providerExecuted !== true || toolCall.inputAvailable !== true)
359
+ continue;
360
+ if (terminalToolCallIds.has(toolCall.id))
361
+ continue;
362
+ if (suppressedToolCallIds.has(toolCall.id))
363
+ continue;
364
+ sendSSE(controller, encoder, {
365
+ type: "tool-output-error",
366
+ toolCallId: toolCall.id,
367
+ errorText: `Provider-executed tool "${toolCall.name}" returned no result before the model turn ended.`,
368
+ providerExecuted: true,
369
+ ...(toolCall.dynamic ? { dynamic: true } : {}),
370
+ });
371
+ }
372
+ pendingProviderExecutedToolCallIds.clear();
373
+ };
308
374
  const resolveProviderExecuted = (toolName, providerExecuted) => providerExecuted ?? (providerExecutedToolNames.has(toolName) ? true : undefined);
309
375
  const normalizeReasoningId = (part) => typeof part.id === "string" && part.id.length > 0 ? part.id : "reasoning";
310
376
  const openTextSegment = () => {
@@ -476,479 +542,531 @@ export function processStreamInternal(resultOrSource, state, controller, encoder
476
542
  });
477
543
  };
478
544
  throwIfAborted(abortSignal);
545
+ // Terminal events for unresolved provider-executed calls must reach the
546
+ // client even when the stream aborts or throws, so the finalizer runs in a
547
+ // `finally`. It runs after the shadow compare so a synthesized event can
548
+ // never perturb the legacy-vs-reducer snapshot the rollout gate reads.
479
549
  const streamIterator = result.fullStream[Symbol.asyncIterator]();
480
- while (true) {
481
- const shouldStopForIdleOutput = !hasActiveLocalToolInput &&
482
- !shouldStopForCommittedLocalToolCall && hasStreamOutput(state);
483
- const shouldStopForIdleStart = !hasActiveLocalToolInput &&
484
- !shouldStopForCommittedLocalToolCall && !hasStreamOutput(state);
485
- const next = hasActiveLocalToolInput
486
- ? await readNextStreamPartWithTimeout(streamIterator, state, callbacks?.localToolInputIdleTimeoutMs ?? LOCAL_TOOL_INPUT_IDLE_MS)
487
- : shouldStopForCommittedLocalToolCall
488
- ? await readNextStreamPartWithTimeout(streamIterator, state, LOCAL_TOOL_COMMIT_GRACE_MS)
489
- : shouldStopForIdleOutput
490
- ? await readNextStreamPartWithTimeout(streamIterator, state, callbacks?.streamIdleTimeoutMs ?? STREAM_OUTPUT_IDLE_MS)
491
- : shouldStopForIdleStart
492
- ? await readNextStreamPartWithTimeout(streamIterator, state, callbacks?.streamIdleTimeoutMs ?? STREAM_START_IDLE_MS)
493
- : await readNextStreamPart(streamIterator, state);
494
- if (next === "timeout") {
495
- state.finishReason ??= shouldStopForIdleOutput || shouldStopForIdleStart
496
- ? "stop"
497
- : "tool-calls";
498
- requestStreamIteratorReturn(streamIterator);
499
- break;
500
- }
501
- if (next.done) {
502
- break;
503
- }
504
- const part = next.value;
505
- throwIfAborted(abortSignal);
506
- try {
507
- shadowLifecycle?.observePart(part);
508
- }
509
- catch {
510
- shadowLifecycleFailed = true;
511
- shadowLifecycle = null;
512
- }
513
- eventCount++;
514
- if (!isRecord(part) || typeof part.type !== "string") {
515
- continue;
516
- }
517
- const typedPart = part;
518
- if (typedPart.type.startsWith("data-")) {
519
- sendSSE(controller, encoder, {
520
- type: typedPart.type,
521
- data: "data" in typedPart ? typedPart.data : undefined,
522
- });
523
- continue;
550
+ let streamIteratorReturned = false;
551
+ /** Release the upstream iterator exactly once, whichever exit is taken. */
552
+ const returnStreamIteratorOnce = () => {
553
+ if (streamIteratorReturned) {
554
+ return;
524
555
  }
525
- switch (typedPart.type) {
526
- case "text-delta": {
527
- closeReasoningSegment();
528
- openTextSegment();
529
- state.accumulatedText += typedPart.text;
530
- sendSSE(controller, encoder, {
531
- type: "text-delta",
532
- id: textPartId,
533
- delta: typedPart.text,
534
- });
535
- callbacks?.onChunk?.(typedPart.text);
556
+ streamIteratorReturned = true;
557
+ requestStreamIteratorReturn(streamIterator);
558
+ };
559
+ try {
560
+ while (true) {
561
+ // A pending provider-executed call outranks the local commit grace: its
562
+ // result can arrive on a later provider continuation request. This picks
563
+ // a longer timeout only. It must not change what a timeout *means*, so
564
+ // the finish-reason classification below stays on the ungated flag.
565
+ const shouldStopForCommittedLocalToolCallNow = shouldStopForCommittedLocalToolCall &&
566
+ pendingProviderExecutedToolCallIds.size === 0;
567
+ const shouldStopForIdleOutput = !hasActiveLocalToolInput &&
568
+ !shouldStopForCommittedLocalToolCallNow && hasStreamOutput(state);
569
+ const shouldStopForIdleStart = !hasActiveLocalToolInput &&
570
+ !shouldStopForCommittedLocalToolCallNow && !hasStreamOutput(state);
571
+ // A timeout means "the turn is over" only when nothing local is in flight
572
+ // and nothing local was committed. Classifying a committed local tool call
573
+ // as "stop" makes shouldContinueAfterStreamStep() bail, so the tool never
574
+ // executes and its card is stranded at input-available.
575
+ const wouldTimeOutIdle = !hasActiveLocalToolInput && !shouldStopForCommittedLocalToolCall;
576
+ const next = hasActiveLocalToolInput
577
+ ? await readNextStreamPartWithTimeout(streamIterator, state, callbacks?.localToolInputIdleTimeoutMs ?? LOCAL_TOOL_INPUT_IDLE_MS)
578
+ : shouldStopForCommittedLocalToolCallNow
579
+ ? await readNextStreamPartWithTimeout(streamIterator, state, callbacks?.localToolCommitGraceMs ?? LOCAL_TOOL_COMMIT_GRACE_MS)
580
+ : shouldStopForIdleOutput
581
+ ? await readNextStreamPartWithTimeout(streamIterator, state, callbacks?.streamIdleTimeoutMs ?? STREAM_OUTPUT_IDLE_MS)
582
+ : shouldStopForIdleStart
583
+ ? await readNextStreamPartWithTimeout(streamIterator, state, callbacks?.streamIdleTimeoutMs ?? STREAM_START_IDLE_MS)
584
+ : await readNextStreamPart(streamIterator, state);
585
+ if (next === "timeout") {
586
+ state.finishReason ??= wouldTimeOutIdle ? "stop" : "tool-calls";
587
+ returnStreamIteratorOnce();
536
588
  break;
537
589
  }
538
- case "reasoning-start": {
539
- closeTextSegment();
540
- openReasoningSegment(normalizeReasoningId(typedPart));
590
+ if (next.done) {
541
591
  break;
542
592
  }
543
- case "reasoning-delta": {
544
- closeTextSegment();
545
- const reasoningId = normalizeReasoningId(typedPart);
546
- openReasoningSegment(reasoningId);
547
- const reasoningPart = reasoningParts.get(reasoningId);
548
- if (reasoningPart) {
549
- reasoningPart.text += typeof typedPart.delta === "string" ? typedPart.delta : "";
550
- }
551
- sendSSE(controller, encoder, {
552
- type: "reasoning-delta",
553
- id: reasoningId,
554
- delta: typeof typedPart.delta === "string" ? typedPart.delta : "",
555
- });
556
- break;
593
+ const part = next.value;
594
+ throwIfAborted(abortSignal);
595
+ try {
596
+ shadowLifecycle?.observePart(part);
557
597
  }
558
- case "reasoning-end": {
559
- closeTextSegment();
560
- if (activeReasoningId === null) {
561
- activeReasoningId = normalizeReasoningId(typedPart);
562
- }
563
- const reasoningPart = reasoningParts.get(activeReasoningId);
564
- if (reasoningPart) {
565
- if (typeof typedPart.signature === "string") {
566
- reasoningPart.signature = typedPart.signature;
567
- }
568
- if (typeof typedPart.redactedData === "string") {
569
- reasoningPart.redactedData = typedPart.redactedData;
570
- }
571
- }
572
- closeReasoningSegment();
573
- break;
598
+ catch {
599
+ shadowLifecycleFailed = true;
600
+ shadowLifecycle = null;
574
601
  }
575
- case "tool-input-start": {
576
- closeTextSegment();
577
- closeReasoningSegment();
578
- shouldStopForCommittedLocalToolCall = false;
579
- const toolId = typedPart.id;
580
- if (isUnavailableTool(typedPart.toolName)) {
581
- suppressToolCall(toolId, typedPart.toolName);
582
- hasActiveLocalToolInput = false;
583
- break;
584
- }
585
- const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
586
- hasActiveLocalToolInput = providerExecuted !== true;
587
- state.toolCalls.set(toolId, {
588
- id: toolId,
589
- name: typedPart.toolName,
590
- arguments: "",
591
- inputAvailable: false,
592
- providerExecuted,
593
- dynamic: typedPart.dynamic,
594
- inputDeltas: [],
595
- inputAnnounced: false,
602
+ eventCount++;
603
+ if (!isRecord(part) || typeof part.type !== "string") {
604
+ continue;
605
+ }
606
+ const typedPart = part;
607
+ if (typedPart.type.startsWith("data-")) {
608
+ sendSSE(controller, encoder, {
609
+ type: typedPart.type,
610
+ data: "data" in typedPart ? typedPart.data : undefined,
596
611
  });
597
- break;
612
+ continue;
598
613
  }
599
- case "tool-input-delta": {
600
- closeReasoningSegment();
601
- const toolId = typedPart.id;
602
- if (suppressedToolCallIds.has(toolId))
603
- break;
604
- const tc = state.toolCalls.get(toolId);
605
- if (!tc)
614
+ switch (typedPart.type) {
615
+ case "text-delta": {
616
+ closeReasoningSegment();
617
+ openTextSegment();
618
+ state.accumulatedText += typedPart.text;
619
+ sendSSE(controller, encoder, {
620
+ type: "text-delta",
621
+ id: textPartId,
622
+ delta: typedPart.text,
623
+ });
624
+ callbacks?.onChunk?.(typedPart.text);
606
625
  break;
607
- tc.arguments = mergeToolInputDelta(tc.arguments, typedPart.delta);
608
- tc.inputDeltas ??= [];
609
- tc.inputDeltas.push(typedPart.delta);
610
- break;
611
- }
612
- case "tool-input-end": {
613
- closeTextSegment();
614
- closeReasoningSegment();
615
- const toolId = typedPart.id;
616
- if (suppressedToolCallIds.has(toolId)) {
617
- hasActiveLocalToolInput = false;
626
+ }
627
+ case "reasoning-start": {
628
+ closeTextSegment();
629
+ openReasoningSegment(normalizeReasoningId(typedPart));
618
630
  break;
619
631
  }
620
- const tc = state.toolCalls.get(toolId);
621
- if (!tc)
632
+ case "reasoning-delta": {
633
+ closeTextSegment();
634
+ const reasoningId = normalizeReasoningId(typedPart);
635
+ openReasoningSegment(reasoningId);
636
+ const reasoningPart = reasoningParts.get(reasoningId);
637
+ if (reasoningPart) {
638
+ reasoningPart.text += typeof typedPart.delta === "string" ? typedPart.delta : "";
639
+ }
640
+ sendSSE(controller, encoder, {
641
+ type: "reasoning-delta",
642
+ id: reasoningId,
643
+ delta: typeof typedPart.delta === "string" ? typedPart.delta : "",
644
+ });
622
645
  break;
623
- tc.inputAvailable = true;
624
- hasActiveLocalToolInput = false;
625
- const dynamic = tc.dynamic ?? isDynamicTool(tc.name);
626
- if (dynamic) {
627
- tc.dynamic = true;
628
646
  }
629
- announceToolInputStart(tc);
630
- sendSSE(controller, encoder, {
631
- type: "tool-input-available",
632
- toolCallId: toolId,
633
- toolName: tc.name,
634
- input: parseToolInputObject(tc.arguments),
635
- ...(tc.providerExecuted !== undefined ? { providerExecuted: tc.providerExecuted } : {}),
636
- ...(dynamic ? { dynamic: true } : {}),
637
- });
638
- if (tc.providerExecuted !== true) {
639
- shouldStopForCommittedLocalToolCall = true;
647
+ case "reasoning-end": {
648
+ closeTextSegment();
649
+ if (activeReasoningId === null) {
650
+ activeReasoningId = normalizeReasoningId(typedPart);
651
+ }
652
+ const reasoningPart = reasoningParts.get(activeReasoningId);
653
+ if (reasoningPart) {
654
+ if (typeof typedPart.signature === "string") {
655
+ reasoningPart.signature = typedPart.signature;
656
+ }
657
+ if (typeof typedPart.redactedData === "string") {
658
+ reasoningPart.redactedData = typedPart.redactedData;
659
+ }
660
+ }
661
+ closeReasoningSegment();
662
+ break;
640
663
  }
641
- break;
642
- }
643
- case "tool-input-available": {
644
- closeTextSegment();
645
- closeReasoningSegment();
646
- const toolId = typedPart.toolCallId ?? typedPart.id;
647
- if (!toolId) {
664
+ case "tool-input-start": {
665
+ closeTextSegment();
666
+ closeReasoningSegment();
667
+ shouldStopForCommittedLocalToolCall = false;
668
+ const toolId = typedPart.id;
669
+ // A restart drops the call back to `inputAvailable: false`, so an
670
+ // earlier tracking of the same id no longer holds. Leaving it pending
671
+ // would disable the local commit grace for the rest of the step.
672
+ pendingProviderExecutedToolCallIds.delete(toolId);
673
+ if (isUnavailableTool(typedPart.toolName)) {
674
+ suppressToolCall(toolId, typedPart.toolName);
675
+ hasActiveLocalToolInput = false;
676
+ break;
677
+ }
678
+ const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
679
+ hasActiveLocalToolInput = providerExecuted !== true;
680
+ state.toolCalls.set(toolId, {
681
+ id: toolId,
682
+ name: typedPart.toolName,
683
+ arguments: "",
684
+ inputAvailable: false,
685
+ providerExecuted,
686
+ dynamic: typedPart.dynamic,
687
+ inputDeltas: [],
688
+ inputAnnounced: false,
689
+ });
648
690
  break;
649
691
  }
650
- if (isUnavailableTool(typedPart.toolName)) {
651
- suppressToolCall(toolId, typedPart.toolName);
652
- hasActiveLocalToolInput = false;
692
+ case "tool-input-delta": {
693
+ closeReasoningSegment();
694
+ const toolId = typedPart.id;
695
+ if (suppressedToolCallIds.has(toolId))
696
+ break;
697
+ const tc = state.toolCalls.get(toolId);
698
+ if (!tc)
699
+ break;
700
+ tc.arguments = mergeToolInputDelta(tc.arguments, typedPart.delta);
701
+ tc.inputDeltas ??= [];
702
+ tc.inputDeltas.push(typedPart.delta);
653
703
  break;
654
704
  }
655
- const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
656
- hasActiveLocalToolInput = false;
657
- const inputStr = normalizeToolInputString(typedPart.input);
658
- const previous = state.toolCalls.get(toolId);
659
- const previousArguments = previous?.arguments ?? "";
660
- const resolvedArguments = mergeToolCallInput(previousArguments, inputStr);
661
- const wasInputAvailable = previous?.inputAvailable === true;
662
- const dynamic = typedPart.dynamic ?? isDynamicTool(typedPart.toolName);
663
- state.toolCalls.set(toolId, {
664
- id: toolId,
665
- name: typedPart.toolName,
666
- arguments: resolvedArguments,
667
- inputAvailable: true,
668
- providerExecuted,
669
- dynamic,
670
- });
671
- if (!wasInputAvailable) {
705
+ case "tool-input-end": {
706
+ closeTextSegment();
707
+ closeReasoningSegment();
708
+ const toolId = typedPart.id;
709
+ if (suppressedToolCallIds.has(toolId)) {
710
+ hasActiveLocalToolInput = false;
711
+ break;
712
+ }
713
+ const tc = state.toolCalls.get(toolId);
714
+ if (!tc)
715
+ break;
716
+ tc.inputAvailable = true;
717
+ trackProviderExecutedToolCall(toolId, tc.providerExecuted);
718
+ hasActiveLocalToolInput = false;
719
+ const dynamic = tc.dynamic ?? isDynamicTool(tc.name);
720
+ if (dynamic) {
721
+ tc.dynamic = true;
722
+ }
723
+ announceToolInputStart(tc);
672
724
  sendSSE(controller, encoder, {
673
725
  type: "tool-input-available",
674
726
  toolCallId: toolId,
675
- toolName: typedPart.toolName,
676
- input: parseToolInputObject(resolvedArguments),
677
- ...(providerExecuted !== undefined ? { providerExecuted } : {}),
727
+ toolName: tc.name,
728
+ input: parseToolInputObject(tc.arguments),
729
+ ...(tc.providerExecuted !== undefined
730
+ ? { providerExecuted: tc.providerExecuted }
731
+ : {}),
678
732
  ...(dynamic ? { dynamic: true } : {}),
679
733
  });
734
+ if (tc.providerExecuted !== true) {
735
+ shouldStopForCommittedLocalToolCall = true;
736
+ }
737
+ break;
680
738
  }
681
- if (providerExecuted !== true) {
682
- shouldStopForCommittedLocalToolCall = true;
739
+ case "tool-input-available": {
740
+ closeTextSegment();
741
+ closeReasoningSegment();
742
+ const toolId = typedPart.toolCallId ?? typedPart.id;
743
+ if (!toolId) {
744
+ break;
745
+ }
746
+ if (isUnavailableTool(typedPart.toolName)) {
747
+ suppressToolCall(toolId, typedPart.toolName);
748
+ hasActiveLocalToolInput = false;
749
+ break;
750
+ }
751
+ const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
752
+ hasActiveLocalToolInput = false;
753
+ trackProviderExecutedToolCall(toolId, providerExecuted);
754
+ const inputStr = normalizeToolInputString(typedPart.input);
755
+ const previous = state.toolCalls.get(toolId);
756
+ const previousArguments = previous?.arguments ?? "";
757
+ const resolvedArguments = mergeToolCallInput(previousArguments, inputStr);
758
+ const wasInputAvailable = previous?.inputAvailable === true;
759
+ const dynamic = typedPart.dynamic ?? isDynamicTool(typedPart.toolName);
760
+ state.toolCalls.set(toolId, {
761
+ id: toolId,
762
+ name: typedPart.toolName,
763
+ arguments: resolvedArguments,
764
+ inputAvailable: true,
765
+ providerExecuted,
766
+ dynamic,
767
+ });
768
+ if (!wasInputAvailable) {
769
+ sendSSE(controller, encoder, {
770
+ type: "tool-input-available",
771
+ toolCallId: toolId,
772
+ toolName: typedPart.toolName,
773
+ input: parseToolInputObject(resolvedArguments),
774
+ ...(providerExecuted !== undefined ? { providerExecuted } : {}),
775
+ ...(dynamic ? { dynamic: true } : {}),
776
+ });
777
+ }
778
+ if (providerExecuted !== true) {
779
+ shouldStopForCommittedLocalToolCall = true;
780
+ }
781
+ break;
683
782
  }
684
- break;
685
- }
686
- case "tool-call": {
687
- closeTextSegment();
688
- closeReasoningSegment();
689
- // tool-call fires when the full tool call is available
690
- const toolId = typedPart.toolCallId;
691
- if (isUnavailableTool(typedPart.toolName)) {
692
- suppressToolCall(toolId, typedPart.toolName);
783
+ case "tool-call": {
784
+ closeTextSegment();
785
+ closeReasoningSegment();
786
+ // tool-call fires when the full tool call is available
787
+ const toolId = typedPart.toolCallId;
788
+ if (isUnavailableTool(typedPart.toolName)) {
789
+ suppressToolCall(toolId, typedPart.toolName);
790
+ hasActiveLocalToolInput = false;
791
+ break;
792
+ }
793
+ const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
693
794
  hasActiveLocalToolInput = false;
795
+ trackProviderExecutedToolCall(toolId, providerExecuted);
796
+ const inputStr = normalizeToolInputString(typedPart.input);
797
+ const previous = state.toolCalls.get(toolId);
798
+ const previousArguments = previous?.arguments ?? "";
799
+ const resolvedArguments = mergeToolCallInput(previousArguments, inputStr);
800
+ const wasInputAvailable = previous?.inputAvailable === true;
801
+ const toolCall = {
802
+ id: toolId,
803
+ name: typedPart.toolName,
804
+ arguments: resolvedArguments,
805
+ inputDeltas: previous?.inputDeltas ?? [],
806
+ inputAnnounced: previous?.inputAnnounced ?? false,
807
+ inputAvailable: true,
808
+ providerExecuted,
809
+ dynamic: typedPart.dynamic,
810
+ };
811
+ state.toolCalls.set(toolId, toolCall);
812
+ const dynamic = isDynamicTool(typedPart.toolName);
813
+ const inputObj = parseToolInputObject(typedPart.input);
814
+ announceToolInputStart(toolCall);
815
+ if (!wasInputAvailable) {
816
+ sendSSE(controller, encoder, {
817
+ type: "tool-input-available",
818
+ toolCallId: toolId,
819
+ toolName: typedPart.toolName,
820
+ input: inputObj,
821
+ ...(providerExecuted !== undefined ? { providerExecuted } : {}),
822
+ ...(dynamic ? { dynamic: true } : {}),
823
+ });
824
+ }
825
+ if (providerExecuted !== true) {
826
+ shouldStopForCommittedLocalToolCall = true;
827
+ }
694
828
  break;
695
829
  }
696
- const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
697
- hasActiveLocalToolInput = false;
698
- const inputStr = normalizeToolInputString(typedPart.input);
699
- const previous = state.toolCalls.get(toolId);
700
- const previousArguments = previous?.arguments ?? "";
701
- const resolvedArguments = mergeToolCallInput(previousArguments, inputStr);
702
- const wasInputAvailable = previous?.inputAvailable === true;
703
- const toolCall = {
704
- id: toolId,
705
- name: typedPart.toolName,
706
- arguments: resolvedArguments,
707
- inputDeltas: previous?.inputDeltas ?? [],
708
- inputAnnounced: previous?.inputAnnounced ?? false,
709
- inputAvailable: true,
710
- providerExecuted,
711
- dynamic: typedPart.dynamic,
712
- };
713
- state.toolCalls.set(toolId, toolCall);
714
- const dynamic = isDynamicTool(typedPart.toolName);
715
- const inputObj = parseToolInputObject(typedPart.input);
716
- announceToolInputStart(toolCall);
717
- if (!wasInputAvailable) {
718
- sendSSE(controller, encoder, {
719
- type: "tool-input-available",
720
- toolCallId: toolId,
830
+ case "tool-result": {
831
+ closeTextSegment();
832
+ closeReasoningSegment();
833
+ if (suppressedToolCallIds.has(typedPart.toolCallId) ||
834
+ isUnavailableTool(typedPart.toolName)) {
835
+ suppressToolCall(typedPart.toolCallId, typedPart.toolName);
836
+ break;
837
+ }
838
+ const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
839
+ // A preliminary result is not terminal: the provider is still
840
+ // working. Releasing the call here would re-arm the local commit
841
+ // grace and truncate the stream before the final result arrives,
842
+ // which is the failure this tracking exists to prevent.
843
+ if (typedPart.preliminary !== true) {
844
+ pendingProviderExecutedToolCallIds.delete(typedPart.toolCallId);
845
+ }
846
+ ensureToolLifecycle({
847
+ toolCallId: typedPart.toolCallId,
848
+ toolName: typedPart.toolName,
849
+ input: typedPart.input,
850
+ providerExecuted,
851
+ dynamic: typedPart.dynamic,
852
+ });
853
+ const toolResultOutput = resolveToolResultOutput(typedPart);
854
+ const inferredToolError = getToolResultError(toolResultOutput);
855
+ const isExplicitError = typedPart.isError === true &&
856
+ !isIntegrationAuthenticationActionResult(toolResultOutput);
857
+ const isError = isExplicitError || inferredToolError !== undefined;
858
+ const toolResultError = isExplicitError
859
+ ? toolResultOutput
860
+ : inferredToolError ?? toolResultOutput;
861
+ logProviderToolPart("tool-result", {
862
+ toolCallId: typedPart.toolCallId,
863
+ toolName: typedPart.toolName,
864
+ providerExecuted,
865
+ dynamic: typedPart.dynamic,
866
+ output: toolResultOutput,
867
+ input: typedPart.input,
868
+ preliminary: typedPart.preliminary,
869
+ isError,
870
+ });
871
+ if (isError) {
872
+ state.toolResults.push({
873
+ toolCallId: typedPart.toolCallId,
874
+ toolName: typedPart.toolName,
875
+ error: toolResultError,
876
+ ...(providerExecuted !== undefined ? { providerExecuted } : {}),
877
+ ...(typedPart.dynamic ? { dynamic: true } : {}),
878
+ });
879
+ sendSSE(controller, encoder, {
880
+ type: "tool-output-error",
881
+ toolCallId: typedPart.toolCallId,
882
+ errorText: stringifyToolError(toolResultError),
883
+ ...(providerExecuted !== undefined ? { providerExecuted } : {}),
884
+ ...(typedPart.dynamic ? { dynamic: true } : {}),
885
+ });
886
+ break;
887
+ }
888
+ state.toolResults.push({
889
+ toolCallId: typedPart.toolCallId,
721
890
  toolName: typedPart.toolName,
722
- input: inputObj,
891
+ output: toolResultOutput,
723
892
  ...(providerExecuted !== undefined ? { providerExecuted } : {}),
724
- ...(dynamic ? { dynamic: true } : {}),
893
+ ...(typedPart.dynamic ? { dynamic: true } : {}),
894
+ ...(typedPart.preliminary !== undefined
895
+ ? { preliminary: typedPart.preliminary }
896
+ : {}),
897
+ });
898
+ sendSSE(controller, encoder, {
899
+ type: "tool-output-available",
900
+ toolCallId: typedPart.toolCallId,
901
+ output: toolResultOutput,
902
+ ...(providerExecuted !== undefined ? { providerExecuted } : {}),
903
+ ...(typedPart.dynamic ? { dynamic: true } : {}),
904
+ ...(typedPart.preliminary !== undefined
905
+ ? { preliminary: typedPart.preliminary }
906
+ : {}),
725
907
  });
726
- }
727
- if (providerExecuted !== true) {
728
- shouldStopForCommittedLocalToolCall = true;
729
- }
730
- break;
731
- }
732
- case "tool-result": {
733
- closeTextSegment();
734
- closeReasoningSegment();
735
- if (suppressedToolCallIds.has(typedPart.toolCallId) ||
736
- isUnavailableTool(typedPart.toolName)) {
737
- suppressToolCall(typedPart.toolCallId, typedPart.toolName);
738
908
  break;
739
909
  }
740
- const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
741
- ensureToolLifecycle({
742
- toolCallId: typedPart.toolCallId,
743
- toolName: typedPart.toolName,
744
- input: typedPart.input,
745
- providerExecuted,
746
- dynamic: typedPart.dynamic,
747
- });
748
- const toolResultOutput = resolveToolResultOutput(typedPart);
749
- const inferredToolError = getToolResultError(toolResultOutput);
750
- const isExplicitError = typedPart.isError === true &&
751
- !isIntegrationAuthenticationActionResult(toolResultOutput);
752
- const isError = isExplicitError || inferredToolError !== undefined;
753
- const toolResultError = isExplicitError
754
- ? toolResultOutput
755
- : inferredToolError ?? toolResultOutput;
756
- logProviderToolPart("tool-result", {
757
- toolCallId: typedPart.toolCallId,
758
- toolName: typedPart.toolName,
759
- providerExecuted,
760
- dynamic: typedPart.dynamic,
761
- output: toolResultOutput,
762
- input: typedPart.input,
763
- preliminary: typedPart.preliminary,
764
- isError,
765
- });
766
- if (isError) {
910
+ case "tool-error": {
911
+ closeTextSegment();
912
+ closeReasoningSegment();
913
+ const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
914
+ pendingProviderExecutedToolCallIds.delete(typedPart.toolCallId);
915
+ ensureToolLifecycle({
916
+ toolCallId: typedPart.toolCallId,
917
+ toolName: typedPart.toolName,
918
+ input: typedPart.input,
919
+ providerExecuted,
920
+ dynamic: typedPart.dynamic,
921
+ });
922
+ logProviderToolPart("tool-error", {
923
+ toolCallId: typedPart.toolCallId,
924
+ toolName: typedPart.toolName,
925
+ providerExecuted,
926
+ dynamic: typedPart.dynamic,
927
+ error: typedPart.error,
928
+ input: typedPart.input,
929
+ });
767
930
  state.toolResults.push({
768
931
  toolCallId: typedPart.toolCallId,
769
932
  toolName: typedPart.toolName,
770
- error: toolResultError,
933
+ error: typedPart.error,
771
934
  ...(providerExecuted !== undefined ? { providerExecuted } : {}),
772
935
  ...(typedPart.dynamic ? { dynamic: true } : {}),
773
936
  });
774
937
  sendSSE(controller, encoder, {
775
938
  type: "tool-output-error",
776
939
  toolCallId: typedPart.toolCallId,
777
- errorText: stringifyToolError(toolResultError),
940
+ errorText: stringifyToolError(typedPart.error),
778
941
  ...(providerExecuted !== undefined ? { providerExecuted } : {}),
779
942
  ...(typedPart.dynamic ? { dynamic: true } : {}),
780
943
  });
781
944
  break;
782
945
  }
783
- state.toolResults.push({
784
- toolCallId: typedPart.toolCallId,
785
- toolName: typedPart.toolName,
786
- output: toolResultOutput,
787
- ...(providerExecuted !== undefined ? { providerExecuted } : {}),
788
- ...(typedPart.dynamic ? { dynamic: true } : {}),
789
- ...(typedPart.preliminary !== undefined ? { preliminary: typedPart.preliminary } : {}),
790
- });
791
- sendSSE(controller, encoder, {
792
- type: "tool-output-available",
793
- toolCallId: typedPart.toolCallId,
794
- output: toolResultOutput,
795
- ...(providerExecuted !== undefined ? { providerExecuted } : {}),
796
- ...(typedPart.dynamic ? { dynamic: true } : {}),
797
- ...(typedPart.preliminary !== undefined ? { preliminary: typedPart.preliminary } : {}),
798
- });
799
- break;
800
- }
801
- case "tool-error": {
802
- closeTextSegment();
803
- closeReasoningSegment();
804
- const providerExecuted = resolveProviderExecuted(typedPart.toolName, typedPart.providerExecuted);
805
- ensureToolLifecycle({
806
- toolCallId: typedPart.toolCallId,
807
- toolName: typedPart.toolName,
808
- input: typedPart.input,
809
- providerExecuted,
810
- dynamic: typedPart.dynamic,
811
- });
812
- logProviderToolPart("tool-error", {
813
- toolCallId: typedPart.toolCallId,
814
- toolName: typedPart.toolName,
815
- providerExecuted,
816
- dynamic: typedPart.dynamic,
817
- error: typedPart.error,
818
- input: typedPart.input,
819
- });
820
- state.toolResults.push({
821
- toolCallId: typedPart.toolCallId,
822
- toolName: typedPart.toolName,
823
- error: typedPart.error,
824
- ...(providerExecuted !== undefined ? { providerExecuted } : {}),
825
- ...(typedPart.dynamic ? { dynamic: true } : {}),
826
- });
827
- sendSSE(controller, encoder, {
828
- type: "tool-output-error",
829
- toolCallId: typedPart.toolCallId,
830
- errorText: stringifyToolError(typedPart.error),
831
- ...(providerExecuted !== undefined ? { providerExecuted } : {}),
832
- ...(typedPart.dynamic ? { dynamic: true } : {}),
833
- });
834
- break;
835
- }
836
- case "finish": {
837
- closeTextSegment();
838
- closeReasoningSegment();
839
- state.finishReason = typedPart.finishReason ?? null;
840
- if (state.finishReason) {
841
- setActiveSpanAttributes({
842
- "gen_ai.response.finish_reasons": [state.finishReason],
843
- });
844
- }
845
- if (state.finishReason === "tool-calls") {
846
- commitParseablePendingToolInputs();
946
+ case "finish": {
947
+ closeTextSegment();
948
+ closeReasoningSegment();
949
+ state.finishReason = typedPart.finishReason ?? null;
950
+ if (state.finishReason) {
951
+ setActiveSpanAttributes({
952
+ "gen_ai.response.finish_reasons": [state.finishReason],
953
+ });
954
+ }
955
+ if (state.finishReason === "tool-calls") {
956
+ commitParseablePendingToolInputs();
957
+ }
958
+ if (typedPart.totalUsage) {
959
+ const input = typedPart.totalUsage.inputTokens ?? 0;
960
+ const output = typedPart.totalUsage.outputTokens ?? 0;
961
+ const cacheReadInputTokens = typedPart.totalUsage.cacheReadInputTokens;
962
+ const cacheCreationInputTokens = typedPart.totalUsage.cacheCreationInputTokens;
963
+ const cachedInputTokens = typedPart.totalUsage.cachedInputTokens ??
964
+ cacheReadInputTokens;
965
+ state.usage = {
966
+ promptTokens: input,
967
+ completionTokens: output,
968
+ totalTokens: typedPart.totalUsage.totalTokens ?? input + output,
969
+ ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
970
+ ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
971
+ ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
972
+ ...(typedPart.totalUsage.reasoningTokens !== undefined
973
+ ? { reasoningTokens: typedPart.totalUsage.reasoningTokens }
974
+ : {}),
975
+ ...(typedPart.totalUsage.billableInputTokens !== undefined
976
+ ? { billableInputTokens: typedPart.totalUsage.billableInputTokens }
977
+ : {}),
978
+ ...(typedPart.totalUsage.billableOutputTokens !== undefined
979
+ ? { billableOutputTokens: typedPart.totalUsage.billableOutputTokens }
980
+ : {}),
981
+ ...(typedPart.totalUsage.costUsd !== undefined
982
+ ? { costUsd: typedPart.totalUsage.costUsd }
983
+ : {}),
984
+ ...(typedPart.totalUsage.providerInputCostUsd !== undefined
985
+ ? { providerInputCostUsd: typedPart.totalUsage.providerInputCostUsd }
986
+ : {}),
987
+ ...(typedPart.totalUsage.providerOutputCostUsd !== undefined
988
+ ? { providerOutputCostUsd: typedPart.totalUsage.providerOutputCostUsd }
989
+ : {}),
990
+ ...(typedPart.totalUsage.providerCostUsd !== undefined
991
+ ? { providerCostUsd: typedPart.totalUsage.providerCostUsd }
992
+ : {}),
993
+ ...(typedPart.totalUsage.veryfrontInputChargeUsd !== undefined
994
+ ? { veryfrontInputChargeUsd: typedPart.totalUsage.veryfrontInputChargeUsd }
995
+ : {}),
996
+ ...(typedPart.totalUsage.veryfrontOutputChargeUsd !== undefined
997
+ ? { veryfrontOutputChargeUsd: typedPart.totalUsage.veryfrontOutputChargeUsd }
998
+ : {}),
999
+ ...(typedPart.totalUsage.veryfrontChargeUsd !== undefined
1000
+ ? { veryfrontChargeUsd: typedPart.totalUsage.veryfrontChargeUsd }
1001
+ : {}),
1002
+ ...(typedPart.totalUsage.veryfrontBilledUsd !== undefined
1003
+ ? { veryfrontBilledUsd: typedPart.totalUsage.veryfrontBilledUsd }
1004
+ : {}),
1005
+ ...(typedPart.totalUsage.costCredits !== undefined
1006
+ ? { costCredits: typedPart.totalUsage.costCredits }
1007
+ : {}),
1008
+ ...(typedPart.totalUsage.costSource !== undefined
1009
+ ? { costSource: typedPart.totalUsage.costSource }
1010
+ : {}),
1011
+ ...(typedPart.totalUsage.billingMode !== undefined
1012
+ ? { billingMode: typedPart.totalUsage.billingMode }
1013
+ : {}),
1014
+ ...(typedPart.totalUsage.usageCaptureStatus !== undefined
1015
+ ? { usageCaptureStatus: typedPart.totalUsage.usageCaptureStatus }
1016
+ : {}),
1017
+ };
1018
+ callbacks?.onUsage?.(state.usage);
1019
+ setActiveSpanAttributes(buildRuntimeUsageTraceAttributes(state.usage));
1020
+ }
1021
+ break;
847
1022
  }
848
- if (typedPart.totalUsage) {
849
- const input = typedPart.totalUsage.inputTokens ?? 0;
850
- const output = typedPart.totalUsage.outputTokens ?? 0;
851
- const cacheReadInputTokens = typedPart.totalUsage.cacheReadInputTokens;
852
- const cacheCreationInputTokens = typedPart.totalUsage.cacheCreationInputTokens;
853
- const cachedInputTokens = typedPart.totalUsage.cachedInputTokens ??
854
- cacheReadInputTokens;
855
- state.usage = {
856
- promptTokens: input,
857
- completionTokens: output,
858
- totalTokens: typedPart.totalUsage.totalTokens ?? input + output,
859
- ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
860
- ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
861
- ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
862
- ...(typedPart.totalUsage.reasoningTokens !== undefined
863
- ? { reasoningTokens: typedPart.totalUsage.reasoningTokens }
864
- : {}),
865
- ...(typedPart.totalUsage.billableInputTokens !== undefined
866
- ? { billableInputTokens: typedPart.totalUsage.billableInputTokens }
867
- : {}),
868
- ...(typedPart.totalUsage.billableOutputTokens !== undefined
869
- ? { billableOutputTokens: typedPart.totalUsage.billableOutputTokens }
870
- : {}),
871
- ...(typedPart.totalUsage.costUsd !== undefined
872
- ? { costUsd: typedPart.totalUsage.costUsd }
873
- : {}),
874
- ...(typedPart.totalUsage.providerInputCostUsd !== undefined
875
- ? { providerInputCostUsd: typedPart.totalUsage.providerInputCostUsd }
876
- : {}),
877
- ...(typedPart.totalUsage.providerOutputCostUsd !== undefined
878
- ? { providerOutputCostUsd: typedPart.totalUsage.providerOutputCostUsd }
879
- : {}),
880
- ...(typedPart.totalUsage.providerCostUsd !== undefined
881
- ? { providerCostUsd: typedPart.totalUsage.providerCostUsd }
882
- : {}),
883
- ...(typedPart.totalUsage.veryfrontInputChargeUsd !== undefined
884
- ? { veryfrontInputChargeUsd: typedPart.totalUsage.veryfrontInputChargeUsd }
885
- : {}),
886
- ...(typedPart.totalUsage.veryfrontOutputChargeUsd !== undefined
887
- ? { veryfrontOutputChargeUsd: typedPart.totalUsage.veryfrontOutputChargeUsd }
888
- : {}),
889
- ...(typedPart.totalUsage.veryfrontChargeUsd !== undefined
890
- ? { veryfrontChargeUsd: typedPart.totalUsage.veryfrontChargeUsd }
891
- : {}),
892
- ...(typedPart.totalUsage.veryfrontBilledUsd !== undefined
893
- ? { veryfrontBilledUsd: typedPart.totalUsage.veryfrontBilledUsd }
894
- : {}),
895
- ...(typedPart.totalUsage.costCredits !== undefined
896
- ? { costCredits: typedPart.totalUsage.costCredits }
897
- : {}),
898
- ...(typedPart.totalUsage.costSource !== undefined
899
- ? { costSource: typedPart.totalUsage.costSource }
900
- : {}),
901
- ...(typedPart.totalUsage.billingMode !== undefined
902
- ? { billingMode: typedPart.totalUsage.billingMode }
903
- : {}),
904
- ...(typedPart.totalUsage.usageCaptureStatus !== undefined
905
- ? { usageCaptureStatus: typedPart.totalUsage.usageCaptureStatus }
906
- : {}),
907
- };
908
- callbacks?.onUsage?.(state.usage);
909
- setActiveSpanAttributes(buildRuntimeUsageTraceAttributes(state.usage));
1023
+ case "error": {
1024
+ closeTextSegment();
1025
+ closeReasoningSegment();
1026
+ logger.warn("Runtime stream error:", typedPart.error);
1027
+ sendSSE(controller, encoder, {
1028
+ type: "error",
1029
+ error: typedPart.error instanceof Error
1030
+ ? typedPart.error.message
1031
+ : String(typedPart.error),
1032
+ });
1033
+ break;
910
1034
  }
911
- break;
912
- }
913
- case "error": {
914
- closeTextSegment();
915
- closeReasoningSegment();
916
- logger.warn("Runtime stream error:", typedPart.error);
917
- sendSSE(controller, encoder, {
918
- type: "error",
919
- error: typedPart.error instanceof Error
920
- ? typedPart.error.message
921
- : String(typedPart.error),
922
- });
923
- break;
1035
+ default:
1036
+ // Ignore other stream parts (source, file, reasoning-*, etc.)
1037
+ break;
924
1038
  }
925
- default:
926
- // Ignore other stream parts (source, file, reasoning-*, etc.)
927
- break;
1039
+ throwIfAborted(abortSignal);
928
1040
  }
929
1041
  throwIfAborted(abortSignal);
930
- }
931
- throwIfAborted(abortSignal);
932
- if (callbacks?.streamLifecycleMode === "shadow") {
933
- let observed = { count: 0, categories: [] };
934
- try {
935
- observed = shadowLifecycle?.compareLegacySnapshot(state) ?? observed;
936
- }
937
- catch {
938
- shadowLifecycleFailed = true;
1042
+ if (callbacks?.streamLifecycleMode === "shadow") {
1043
+ let observed = { count: 0, categories: [] };
1044
+ try {
1045
+ observed = shadowLifecycle?.compareLegacySnapshot(state) ?? observed;
1046
+ }
1047
+ catch {
1048
+ shadowLifecycleFailed = true;
1049
+ }
1050
+ const categories = new Set(observed.categories);
1051
+ if (shadowLifecycleFailed)
1052
+ categories.add("shadow_error");
1053
+ const report = {
1054
+ count: categories.size,
1055
+ categories: [...categories].sort(),
1056
+ };
1057
+ callbacks.onLifecycleShadowReport?.(report);
1058
+ setActiveSpanAttributes({
1059
+ "stream.lifecycle_shadow.divergence_count": report.count,
1060
+ "stream.lifecycle_shadow.divergence_categories": [...report.categories],
1061
+ });
939
1062
  }
940
- const categories = new Set(observed.categories);
941
- if (shadowLifecycleFailed)
942
- categories.add("shadow_error");
943
- const report = {
944
- count: categories.size,
945
- categories: [...categories].sort(),
946
- };
947
- callbacks.onLifecycleShadowReport?.(report);
948
- setActiveSpanAttributes({
949
- "stream.lifecycle_shadow.divergence_count": report.count,
950
- "stream.lifecycle_shadow.divergence_categories": [...report.categories],
951
- });
1063
+ }
1064
+ finally {
1065
+ finalizeUnresolvedProviderToolCalls();
1066
+ // `throwIfAborted` and `streamIterator.next()` can both throw past the
1067
+ // loop, so the upstream iterator is released here rather than only on the
1068
+ // timeout path.
1069
+ returnStreamIteratorOnce();
952
1070
  }
953
1071
  setActiveSpanAttributes({
954
1072
  "stream.event_count": eventCount,