@librechat/agents 3.6.8 → 3.6.10

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 (158) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +46 -39
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/agents/projection.cjs +2 -2
  4. package/dist/cjs/agents/projection.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +28 -18
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +87 -16
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/llm/openai/index.cjs +10 -0
  10. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  11. package/dist/cjs/main.cjs +14 -0
  12. package/dist/cjs/messages/alternation.cjs +125 -20
  13. package/dist/cjs/messages/alternation.cjs.map +1 -1
  14. package/dist/cjs/messages/core.cjs +195 -18
  15. package/dist/cjs/messages/core.cjs.map +1 -1
  16. package/dist/cjs/messages/format.cjs +488 -118
  17. package/dist/cjs/messages/format.cjs.map +1 -1
  18. package/dist/cjs/messages/handoffCue.cjs +3 -2
  19. package/dist/cjs/messages/handoffCue.cjs.map +1 -1
  20. package/dist/cjs/messages/index.cjs +1 -0
  21. package/dist/cjs/messages/injected.cjs +5 -2
  22. package/dist/cjs/messages/injected.cjs.map +1 -1
  23. package/dist/cjs/messages/provenance.cjs +411 -0
  24. package/dist/cjs/messages/provenance.cjs.map +1 -0
  25. package/dist/cjs/messages/prune.cjs +18 -13
  26. package/dist/cjs/messages/prune.cjs.map +1 -1
  27. package/dist/cjs/messages/toolResultTypes.cjs +663 -0
  28. package/dist/cjs/messages/toolResultTypes.cjs.map +1 -0
  29. package/dist/cjs/run.cjs +7 -4
  30. package/dist/cjs/run.cjs.map +1 -1
  31. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +21 -4
  32. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  33. package/dist/cjs/tools/CallerCapabilities.cjs +48 -0
  34. package/dist/cjs/tools/CallerCapabilities.cjs.map +1 -0
  35. package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs +34 -0
  36. package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs.map +1 -0
  37. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +21 -4
  38. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +25 -24
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +12 -4
  42. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  43. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +13 -4
  44. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  45. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +47 -11
  46. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  47. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  48. package/dist/cjs/tools/search/search.cjs +19 -9
  49. package/dist/cjs/tools/search/search.cjs.map +1 -1
  50. package/dist/cjs/tools/search/tool.cjs +2 -1
  51. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  52. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  53. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  54. package/dist/cjs/utils/toolContent.cjs +2 -2
  55. package/dist/cjs/utils/toolContent.cjs.map +1 -1
  56. package/dist/esm/agents/AgentContext.mjs +46 -39
  57. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  58. package/dist/esm/agents/projection.mjs +2 -2
  59. package/dist/esm/agents/projection.mjs.map +1 -1
  60. package/dist/esm/graphs/Graph.mjs +30 -20
  61. package/dist/esm/graphs/Graph.mjs.map +1 -1
  62. package/dist/esm/graphs/MultiAgentGraph.mjs +87 -16
  63. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  64. package/dist/esm/llm/openai/index.mjs +10 -0
  65. package/dist/esm/llm/openai/index.mjs.map +1 -1
  66. package/dist/esm/main.mjs +4 -3
  67. package/dist/esm/messages/alternation.mjs +125 -20
  68. package/dist/esm/messages/alternation.mjs.map +1 -1
  69. package/dist/esm/messages/core.mjs +196 -19
  70. package/dist/esm/messages/core.mjs.map +1 -1
  71. package/dist/esm/messages/format.mjs +488 -119
  72. package/dist/esm/messages/format.mjs.map +1 -1
  73. package/dist/esm/messages/handoffCue.mjs +3 -2
  74. package/dist/esm/messages/handoffCue.mjs.map +1 -1
  75. package/dist/esm/messages/index.mjs +1 -0
  76. package/dist/esm/messages/injected.mjs +5 -2
  77. package/dist/esm/messages/injected.mjs.map +1 -1
  78. package/dist/esm/messages/provenance.mjs +401 -0
  79. package/dist/esm/messages/provenance.mjs.map +1 -0
  80. package/dist/esm/messages/prune.mjs +18 -13
  81. package/dist/esm/messages/prune.mjs.map +1 -1
  82. package/dist/esm/messages/toolResultTypes.mjs +657 -0
  83. package/dist/esm/messages/toolResultTypes.mjs.map +1 -0
  84. package/dist/esm/run.mjs +7 -4
  85. package/dist/esm/run.mjs.map +1 -1
  86. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +21 -4
  87. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  88. package/dist/esm/tools/CallerCapabilities.mjs +45 -0
  89. package/dist/esm/tools/CallerCapabilities.mjs.map +1 -0
  90. package/dist/esm/tools/ProgrammaticCallerPolicy.mjs +32 -0
  91. package/dist/esm/tools/ProgrammaticCallerPolicy.mjs.map +1 -0
  92. package/dist/esm/tools/ProgrammaticToolCalling.mjs +21 -4
  93. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  94. package/dist/esm/tools/ToolNode.mjs +25 -24
  95. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  96. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +14 -6
  97. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  98. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +15 -6
  99. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  100. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +47 -12
  101. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  102. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  103. package/dist/esm/tools/search/search.mjs +19 -9
  104. package/dist/esm/tools/search/search.mjs.map +1 -1
  105. package/dist/esm/tools/search/tool.mjs +2 -1
  106. package/dist/esm/tools/search/tool.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  108. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  109. package/dist/esm/utils/toolContent.mjs +2 -2
  110. package/dist/esm/utils/toolContent.mjs.map +1 -1
  111. package/dist/types/agents/AgentContext.d.ts +8 -13
  112. package/dist/types/agents/projection.d.ts +3 -1
  113. package/dist/types/graphs/Graph.d.ts +1 -1
  114. package/dist/types/llm/openai/index.d.ts +20 -0
  115. package/dist/types/messages/format.d.ts +5 -0
  116. package/dist/types/messages/index.d.ts +1 -0
  117. package/dist/types/messages/provenance.d.ts +85 -0
  118. package/dist/types/messages/toolResultTypes.d.ts +33 -0
  119. package/dist/types/tools/CallerCapabilities.d.ts +12 -0
  120. package/dist/types/tools/ProgrammaticCallerPolicy.d.ts +20 -0
  121. package/dist/types/tools/ToolNode.d.ts +4 -7
  122. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +4 -0
  123. package/dist/types/tools/ptcTimeout.d.ts +8 -0
  124. package/dist/types/tools/search/types.d.ts +15 -0
  125. package/dist/types/types/graph.d.ts +3 -1
  126. package/dist/types/types/tools.d.ts +13 -2
  127. package/package.json +1 -1
  128. package/src/agents/AgentContext.ts +109 -67
  129. package/src/agents/projection.ts +9 -1
  130. package/src/graphs/Graph.ts +56 -23
  131. package/src/graphs/MultiAgentGraph.ts +175 -33
  132. package/src/llm/openai/index.ts +43 -0
  133. package/src/messages/alternation.ts +217 -34
  134. package/src/messages/core.ts +358 -38
  135. package/src/messages/format.ts +958 -137
  136. package/src/messages/handoffCue.ts +7 -4
  137. package/src/messages/index.ts +1 -0
  138. package/src/messages/injected.ts +9 -6
  139. package/src/messages/provenance.ts +897 -0
  140. package/src/messages/prune.ts +28 -10
  141. package/src/messages/toolResultTypes.ts +1174 -0
  142. package/src/run.ts +9 -4
  143. package/src/tools/BashProgrammaticToolCalling.ts +46 -12
  144. package/src/tools/CallerCapabilities.ts +78 -0
  145. package/src/tools/ProgrammaticCallerPolicy.ts +97 -0
  146. package/src/tools/ProgrammaticToolCalling.ts +41 -7
  147. package/src/tools/ToolNode.ts +70 -52
  148. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +24 -26
  149. package/src/tools/local/LocalProgrammaticToolCalling.ts +28 -23
  150. package/src/tools/local/resolveLocalExecutionTools.ts +132 -13
  151. package/src/tools/ptcTimeout.ts +6 -0
  152. package/src/tools/search/search.ts +25 -8
  153. package/src/tools/search/tool.ts +2 -0
  154. package/src/tools/search/types.ts +16 -0
  155. package/src/tools/subagent/SubagentExecutor.ts +8 -4
  156. package/src/types/graph.ts +3 -0
  157. package/src/types/tools.ts +14 -3
  158. package/src/utils/toolContent.ts +2 -2
@@ -28,6 +28,26 @@ import type {
28
28
  TPayload,
29
29
  TMessage,
30
30
  } from '@/types';
31
+ import type {
32
+ ProviderMessageAttribution,
33
+ ProviderMessageProvenancePart,
34
+ } from './provenance';
35
+ import type { ProviderToolCallIndex } from './toolResultTypes';
36
+ import {
37
+ appendProviderToolCallDescriptor,
38
+ consumeProviderToolResultPair,
39
+ getBoundedProviderPairingArrayProperty,
40
+ getProviderToolCallPartDescriptor,
41
+ getProviderToolResultPartDescriptor,
42
+ hasStructurallyValidAnthropicWebSearchResultContent,
43
+ } from './toolResultTypes';
44
+ import {
45
+ hasBijectiveProviderContentPartMapping,
46
+ inspectProviderMessageProvenance,
47
+ inspectProviderSourceMessageIds,
48
+ setInvalidProviderMessageProvenance,
49
+ setProviderMessageProvenance,
50
+ } from './provenance';
31
51
  import {
32
52
  compactToolContent,
33
53
  getToolContentCharLength,
@@ -218,19 +238,27 @@ export const formatMessage = ({
218
238
  const mediaParts: MessageContentComplex[] = [];
219
239
 
220
240
  if (Array.isArray(documents) && documents.length > 0) {
221
- mediaParts.push(...documents);
241
+ for (const document of documents) {
242
+ mediaParts.push(document);
243
+ }
222
244
  }
223
245
 
224
246
  if (Array.isArray(videos) && videos.length > 0) {
225
- mediaParts.push(...videos);
247
+ for (const video of videos) {
248
+ mediaParts.push(video);
249
+ }
226
250
  }
227
251
 
228
252
  if (Array.isArray(audios) && audios.length > 0) {
229
- mediaParts.push(...audios);
253
+ for (const audio of audios) {
254
+ mediaParts.push(audio);
255
+ }
230
256
  }
231
257
 
232
258
  if (Array.isArray(image_urls) && image_urls.length > 0) {
233
- mediaParts.push(...image_urls);
259
+ for (const imageUrl of image_urls) {
260
+ mediaParts.push(imageUrl);
261
+ }
234
262
  }
235
263
 
236
264
  if (mediaParts.length > 0 && role === 'user') {
@@ -340,8 +368,13 @@ interface FormatAssistantMessageOptions {
340
368
  preserveReasoningContent?: boolean;
341
369
  provider?: Providers;
342
370
  sourceMessageId?: string;
371
+ sourceContentPartOffset?: number;
372
+ sourceContentPartIndices?: readonly SourceContentPartIndices[];
373
+ toolSourceContentPartIndices?: ReadonlySet<number>;
343
374
  }
344
375
 
376
+ type SourceContentPartIndices = number | readonly number[];
377
+
345
378
  interface FormatAgentMessagesOptions {
346
379
  provider?: Providers;
347
380
  /** Reconstruct hidden `reasoning_content` from `THINK` parts onto prior
@@ -434,21 +467,81 @@ function getToolUseId(part: MessageContentComplex): string | undefined {
434
467
  return part.tool_use_id;
435
468
  }
436
469
 
437
- function isValidServerToolResult(part: MessageContentComplex): boolean {
438
- const toolUseId = getToolUseId(part);
439
- if (
440
- toolUseId?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) !== true ||
441
- !('content' in part)
442
- ) {
470
+ function collectTrustedToolResultSourceContentPartIndices(
471
+ content: readonly unknown[] | undefined,
472
+ sourceContentPartOffset: number
473
+ ): Set<number> | undefined {
474
+ if (content == null) {
475
+ return undefined;
476
+ }
477
+ let calls: ProviderToolCallIndex | undefined;
478
+ let trusted: Set<number> | undefined;
479
+ let previousPart: MessageContentComplex | null | undefined;
480
+ for (let index = 0; index < content.length; index++) {
481
+ const part = content[index] as
482
+ | MessageContentComplex
483
+ | null
484
+ | undefined;
485
+ if (part == null) {
486
+ previousPart = part;
487
+ continue;
488
+ }
489
+ const call = getProviderToolCallPartDescriptor(part);
490
+ if (call != null) {
491
+ calls ??= new Map();
492
+ appendProviderToolCallDescriptor(calls, call);
493
+ }
494
+ const result = getProviderToolResultPartDescriptor(part);
495
+ if (result != null) {
496
+ calls ??= new Map();
497
+ if (consumeProviderToolResultPair(result, calls, previousPart)) {
498
+ trusted ??= new Set();
499
+ trusted.add(sourceContentPartOffset + index);
500
+ }
501
+ }
502
+ previousPart = part;
503
+ }
504
+ return trusted;
505
+ }
506
+
507
+ function sourceContentPartIndicesAreTrustedToolResult(
508
+ sourceContentPartIndices: SourceContentPartIndices,
509
+ trustedToolSourceContentPartIndices: ReadonlySet<number> | undefined
510
+ ): boolean {
511
+ if (typeof sourceContentPartIndices === 'number') {
512
+ return trustedToolSourceContentPartIndices?.has(sourceContentPartIndices) === true;
513
+ }
514
+ if (sourceContentPartIndices.length === 0) {
443
515
  return false;
444
516
  }
445
- const { content } = part as { content?: unknown };
517
+ for (const sourceContentPartIndex of sourceContentPartIndices) {
518
+ if (trustedToolSourceContentPartIndices?.has(sourceContentPartIndex) !== true) {
519
+ return false;
520
+ }
521
+ }
522
+ return true;
523
+ }
524
+
525
+ function isTrustedServerToolResult(
526
+ part: MessageContentComplex,
527
+ sourceContentPartIndices: SourceContentPartIndices,
528
+ trustedToolSourceContentPartIndices: ReadonlySet<number> | undefined
529
+ ): boolean {
530
+ const toolUseId = getToolUseId(part);
446
531
  return (
447
- Array.isArray(content) ||
448
- (content != null &&
449
- typeof content === 'object' &&
450
- 'type' in content &&
451
- (content as { type?: unknown }).type === 'web_search_tool_result_error')
532
+ toolUseId?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) === true &&
533
+ sourceContentPartIndicesAreTrustedToolResult(
534
+ sourceContentPartIndices,
535
+ trustedToolSourceContentPartIndices
536
+ )
537
+ );
538
+ }
539
+
540
+ function isServerToolResultForWire(part: MessageContentComplex): boolean {
541
+ const toolUseId = getToolUseId(part);
542
+ return (
543
+ toolUseId?.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) === true &&
544
+ hasStructurallyValidAnthropicWebSearchResultContent(part)
452
545
  );
453
546
  }
454
547
 
@@ -496,6 +589,155 @@ function endsWithSteerMessage(
496
589
  return formatted[formatted.length - 1].additional_kwargs.source === 'steer';
497
590
  }
498
591
 
592
+ interface MutableProviderMessageProvenancePart {
593
+ attribution: ProviderMessageAttribution;
594
+ sourceMessageId?: string;
595
+ sourceContentPartIndices?: number[];
596
+ }
597
+
598
+ interface ProviderMessageProvenanceBuilder {
599
+ readonly parts: MutableProviderMessageProvenancePart[];
600
+ lastSeenSourceContentPartIndices?: Set<number>;
601
+ }
602
+
603
+ function createProviderMessageProvenanceBuilder(): ProviderMessageProvenanceBuilder {
604
+ return { parts: [] };
605
+ }
606
+
607
+ function appendProviderProvenanceContribution(
608
+ builder: ProviderMessageProvenanceBuilder,
609
+ attribution: ProviderMessageAttribution,
610
+ sourceMessageId?: string,
611
+ sourceContentPartIndex?: number
612
+ ): void {
613
+ const last = builder.parts[builder.parts.length - 1];
614
+ if (
615
+ builder.parts.length === 0 ||
616
+ last.attribution !== attribution ||
617
+ last.sourceMessageId !== sourceMessageId ||
618
+ (last.sourceContentPartIndices == null) !== (sourceContentPartIndex == null)
619
+ ) {
620
+ builder.parts.push({
621
+ attribution,
622
+ ...(sourceMessageId != null && { sourceMessageId }),
623
+ ...(sourceContentPartIndex != null && {
624
+ sourceContentPartIndices: [sourceContentPartIndex],
625
+ }),
626
+ });
627
+ builder.lastSeenSourceContentPartIndices = undefined;
628
+ return;
629
+ }
630
+ if (sourceContentPartIndex == null) {
631
+ return;
632
+ }
633
+ const sourceContentPartIndices = (last.sourceContentPartIndices ??= []);
634
+ if (sourceContentPartIndices.length === 0) {
635
+ sourceContentPartIndices.push(sourceContentPartIndex);
636
+ return;
637
+ }
638
+ const seen = (builder.lastSeenSourceContentPartIndices ??= new Set(
639
+ sourceContentPartIndices
640
+ ));
641
+ if (!seen.has(sourceContentPartIndex)) {
642
+ seen.add(sourceContentPartIndex);
643
+ sourceContentPartIndices.push(sourceContentPartIndex);
644
+ }
645
+ }
646
+
647
+ function appendProviderProvenanceIndices(
648
+ builder: ProviderMessageProvenanceBuilder,
649
+ attribution: ProviderMessageAttribution,
650
+ sourceMessageId: string | undefined,
651
+ sourceContentPartIndices?: SourceContentPartIndices,
652
+ toolSourceContentPartIndices?: ReadonlySet<number>
653
+ ): void {
654
+ if (typeof sourceContentPartIndices === 'number') {
655
+ appendProviderProvenanceContribution(
656
+ builder,
657
+ toolSourceContentPartIndices?.has(sourceContentPartIndices) === true
658
+ ? 'tool'
659
+ : attribution,
660
+ sourceMessageId,
661
+ sourceContentPartIndices
662
+ );
663
+ return;
664
+ }
665
+ if (
666
+ sourceContentPartIndices == null ||
667
+ sourceContentPartIndices.length === 0
668
+ ) {
669
+ appendProviderProvenanceContribution(builder, attribution, sourceMessageId);
670
+ return;
671
+ }
672
+ for (const sourceContentPartIndex of sourceContentPartIndices) {
673
+ appendProviderProvenanceContribution(
674
+ builder,
675
+ toolSourceContentPartIndices?.has(sourceContentPartIndex) === true
676
+ ? 'tool'
677
+ : attribution,
678
+ sourceMessageId,
679
+ sourceContentPartIndex
680
+ );
681
+ }
682
+ }
683
+
684
+ function appendSourceContentPartIndices(
685
+ target: number[],
686
+ sourceContentPartIndices: SourceContentPartIndices
687
+ ): void {
688
+ if (typeof sourceContentPartIndices === 'number') {
689
+ target.push(sourceContentPartIndices);
690
+ } else {
691
+ for (const sourceContentPartIndex of sourceContentPartIndices) {
692
+ target.push(sourceContentPartIndex);
693
+ }
694
+ }
695
+ }
696
+
697
+ function appendProviderProvenanceParts(
698
+ builder: ProviderMessageProvenanceBuilder,
699
+ parts: readonly ProviderMessageProvenancePart[]
700
+ ): void {
701
+ for (const part of parts) {
702
+ appendProviderProvenanceIndices(
703
+ builder,
704
+ part.attribution,
705
+ part.sourceMessageId,
706
+ part.sourceContentPartIndices
707
+ );
708
+ }
709
+ }
710
+
711
+ function mergeAdjacentProviderProvenanceParts(
712
+ parts: readonly ProviderMessageProvenancePart[]
713
+ ): ProviderMessageProvenancePart[] {
714
+ const builder = createProviderMessageProvenanceBuilder();
715
+ appendProviderProvenanceParts(builder, parts);
716
+ return builder.parts;
717
+ }
718
+
719
+ function createProviderContentProvenanceParts(
720
+ content: readonly MessageContentComplex[],
721
+ sourceContentPartOffset: number,
722
+ sourceMessageId: string | undefined,
723
+ defaultAttribution: ProviderMessageAttribution
724
+ ): ProviderMessageProvenancePart[] {
725
+ const builder = createProviderMessageProvenanceBuilder();
726
+ for (let index = 0; index < content.length; index++) {
727
+ const part = content[index] as MessageContentComplex | null | undefined;
728
+ if (part == null) {
729
+ continue;
730
+ }
731
+ appendProviderProvenanceContribution(
732
+ builder,
733
+ defaultAttribution,
734
+ sourceMessageId,
735
+ sourceContentPartOffset + index
736
+ );
737
+ }
738
+ return builder.parts;
739
+ }
740
+
499
741
  /**
500
742
  * Helper function to format an assistant message
501
743
  * @param message The message to format
@@ -515,67 +757,196 @@ function formatAssistantMessage(
515
757
  | RoleBearingMessage<ToolMessage>
516
758
  | RoleBearingMessage<HumanMessage>
517
759
  > = [];
760
+ const formattedMessageProvenance = new Map<
761
+ BaseMessage,
762
+ ProviderMessageProvenanceBuilder
763
+ >();
518
764
  const appendFormattedMessage = (
519
- formattedMessage: (typeof formattedMessages)[number]
765
+ formattedMessage: (typeof formattedMessages)[number],
766
+ attribution: ProviderMessageAttribution,
767
+ sourceContentPartIndices?: SourceContentPartIndices,
768
+ provenanceBuilder?: ProviderMessageProvenanceBuilder
769
+ ): void => {
770
+ const builder =
771
+ provenanceBuilder ?? createProviderMessageProvenanceBuilder();
772
+ if (provenanceBuilder == null) {
773
+ appendProviderProvenanceIndices(
774
+ builder,
775
+ attribution,
776
+ options?.sourceMessageId,
777
+ sourceContentPartIndices
778
+ );
779
+ }
780
+ formattedMessageProvenance.set(formattedMessage, builder);
781
+ formattedMessages.push(formattedMessage);
782
+ };
783
+ const finalizeFormattedMessages = (): typeof formattedMessages => {
784
+ for (let index = 0; index < formattedMessages.length; index++) {
785
+ const formattedMessage = formattedMessages[index];
786
+ stampSourceMessageIdentity(
787
+ formattedMessage,
788
+ options?.sourceMessageId,
789
+ index,
790
+ 'model',
791
+ undefined,
792
+ formattedMessageProvenance.get(formattedMessage)?.parts
793
+ );
794
+ }
795
+ return formattedMessages;
796
+ };
797
+ const appendSourcePartIndices = (
798
+ formattedMessage: (typeof formattedMessages)[number],
799
+ attribution: ProviderMessageAttribution,
800
+ sourceContentPartIndices: SourceContentPartIndices
520
801
  ): void => {
521
- stampSourceMessageIdentity(
522
- formattedMessage,
802
+ const builder = formattedMessageProvenance.get(formattedMessage);
803
+ if (builder == null) {
804
+ return;
805
+ }
806
+ appendProviderProvenanceIndices(
807
+ builder,
808
+ attribution,
523
809
  options?.sourceMessageId,
524
- formattedMessages.length
810
+ sourceContentPartIndices
525
811
  );
526
- formattedMessages.push(formattedMessage);
812
+ };
813
+ const getSourcePartIndices = (partIndex: number): SourceContentPartIndices =>
814
+ options?.sourceContentPartIndices?.[partIndex] ??
815
+ (options?.sourceContentPartOffset ?? 0) + partIndex;
816
+ const createSourceProvenanceBuilder = (
817
+ sourceContentPartIndices: SourceContentPartIndices,
818
+ attribution: ProviderMessageAttribution,
819
+ applyToolOverrides = false
820
+ ): ProviderMessageProvenanceBuilder => {
821
+ const builder = createProviderMessageProvenanceBuilder();
822
+ appendProviderProvenanceIndices(
823
+ builder,
824
+ attribution,
825
+ options?.sourceMessageId,
826
+ sourceContentPartIndices,
827
+ applyToolOverrides ? options?.toolSourceContentPartIndices : undefined
828
+ );
829
+ return builder;
527
830
  };
528
831
  let currentContent: MessageContentComplex[] = [];
832
+ let currentContentProvenance = createProviderMessageProvenanceBuilder();
833
+ const appendCurrentContentProvenance = (
834
+ sourceContentPartIndices: SourceContentPartIndices,
835
+ attribution: ProviderMessageAttribution,
836
+ applyToolOverrides = false
837
+ ): void => {
838
+ appendProviderProvenanceIndices(
839
+ currentContentProvenance,
840
+ attribution,
841
+ options?.sourceMessageId,
842
+ sourceContentPartIndices,
843
+ applyToolOverrides ? options?.toolSourceContentPartIndices : undefined
844
+ );
845
+ };
529
846
  let lastAIMessage: RoleBearingMessage<AIMessage> | null = null;
530
847
  let hasReasoning = false;
531
848
  let pendingReasoningContent = '';
849
+ let pendingReasoningSourcePartIndices: number[] = [];
532
850
  const emittedServerToolUseIds = new Set<string>();
533
- const pendingServerToolUses = new Map<string, MessageContentComplex>();
851
+ const pendingServerToolUses = new Map<
852
+ string,
853
+ {
854
+ content: MessageContentComplex;
855
+ sourceContentPartIndices: SourceContentPartIndices;
856
+ }
857
+ >();
534
858
  const shouldPreserveReasoningContent =
535
859
  options?.preserveReasoningContent === true;
536
860
  const serverToolResultIds = new Set<string>();
537
861
  const preferredToolCallParts = new Map<string, MessageContentComplex>();
538
862
 
539
- const takePendingReasoningContent = (): string | undefined => {
863
+ const takePendingReasoningContent = (): {
864
+ content?: string;
865
+ sourceContentPartIndices: number[];
866
+ } => {
540
867
  if (!shouldPreserveReasoningContent || !pendingReasoningContent) {
541
- return undefined;
868
+ return { sourceContentPartIndices: [] };
542
869
  }
543
- const reasoningContent = pendingReasoningContent;
870
+ const content = pendingReasoningContent;
871
+ const sourceContentPartIndices = pendingReasoningSourcePartIndices;
544
872
  pendingReasoningContent = '';
545
- return reasoningContent;
873
+ pendingReasoningSourcePartIndices = [];
874
+ return { content, sourceContentPartIndices };
546
875
  };
547
876
 
548
877
  const createAIMessage = (
549
878
  content: MessageContent
550
- ): RoleBearingMessage<AIMessage> => {
551
- const reasoningContent = takePendingReasoningContent();
552
- return withMessageRole(
879
+ ): {
880
+ message: RoleBearingMessage<AIMessage>;
881
+ reasoningSourceContentPartIndices: number[];
882
+ } => {
883
+ const reasoning = takePendingReasoningContent();
884
+ const message = withMessageRole(
553
885
  new AIMessage({
554
886
  content,
555
- ...(reasoningContent != null && {
556
- additional_kwargs: { reasoning_content: reasoningContent },
887
+ ...(reasoning.content != null && {
888
+ additional_kwargs: { reasoning_content: reasoning.content },
557
889
  }),
558
890
  }),
559
891
  'assistant'
560
892
  );
893
+ return {
894
+ message,
895
+ reasoningSourceContentPartIndices: reasoning.sourceContentPartIndices,
896
+ };
897
+ };
898
+
899
+ const appendAIMessage = (
900
+ content: MessageContent,
901
+ provenance: ProviderMessageProvenanceBuilder
902
+ ): RoleBearingMessage<AIMessage> => {
903
+ const created = createAIMessage(content);
904
+ let combinedProvenance = provenance;
905
+ if (created.reasoningSourceContentPartIndices.length > 0) {
906
+ combinedProvenance = createProviderMessageProvenanceBuilder();
907
+ appendProviderProvenanceIndices(
908
+ combinedProvenance,
909
+ 'model',
910
+ options?.sourceMessageId,
911
+ created.reasoningSourceContentPartIndices
912
+ );
913
+ appendProviderProvenanceParts(combinedProvenance, provenance.parts);
914
+ }
915
+ appendFormattedMessage(
916
+ created.message,
917
+ 'model',
918
+ undefined,
919
+ combinedProvenance
920
+ );
921
+ return created.message;
561
922
  };
562
923
 
563
924
  const attachPendingReasoningContent = (aiMessage: AIMessage): void => {
564
- const reasoningContent = takePendingReasoningContent();
565
- if (reasoningContent == null) {
925
+ const reasoning = takePendingReasoningContent();
926
+ if (reasoning.content == null) {
566
927
  return;
567
928
  }
568
929
  aiMessage.additional_kwargs.reasoning_content =
569
930
  typeof aiMessage.additional_kwargs.reasoning_content === 'string'
570
- ? `${aiMessage.additional_kwargs.reasoning_content}${reasoningContent}`
571
- : reasoningContent;
931
+ ? `${aiMessage.additional_kwargs.reasoning_content}${reasoning.content}`
932
+ : reasoning.content;
933
+ appendSourcePartIndices(
934
+ aiMessage as (typeof formattedMessages)[number],
935
+ 'model',
936
+ reasoning.sourceContentPartIndices
937
+ );
572
938
  };
573
939
 
574
940
  const flushPendingServerToolUse = (toolUseId: string): void => {
575
- for (const [id, content] of pendingServerToolUses) {
941
+ for (const [id, pending] of pendingServerToolUses) {
576
942
  pendingServerToolUses.delete(id);
577
943
  if (id === toolUseId) {
578
- currentContent.push(content);
944
+ currentContent.push(pending.content);
945
+ appendCurrentContentProvenance(
946
+ pending.sourceContentPartIndices,
947
+ 'model',
948
+ true
949
+ );
579
950
  emittedServerToolUseIds.add(id);
580
951
  return;
581
952
  }
@@ -586,12 +957,26 @@ function formatAssistantMessage(
586
957
  const contentParts = message.content as Array<
587
958
  MessageContentComplex | undefined | null
588
959
  >;
960
+ let trustedServerToolResultPartIndices: Set<number> | undefined;
961
+ let wireServerToolResultPartIndices: Set<number> | undefined;
589
962
 
590
- for (const part of contentParts) {
963
+ for (let partIndex = 0; partIndex < contentParts.length; partIndex++) {
964
+ const part = contentParts[partIndex];
591
965
  if (part == null) {
592
966
  continue;
593
967
  }
594
- if (isValidServerToolResult(part)) {
968
+ const isTrustedResult = isTrustedServerToolResult(
969
+ part,
970
+ getSourcePartIndices(partIndex),
971
+ options?.toolSourceContentPartIndices
972
+ );
973
+ if (isTrustedResult) {
974
+ trustedServerToolResultPartIndices ??= new Set();
975
+ trustedServerToolResultPartIndices.add(partIndex);
976
+ }
977
+ if (isTrustedResult || isServerToolResultForWire(part)) {
978
+ wireServerToolResultPartIndices ??= new Set();
979
+ wireServerToolResultPartIndices.add(partIndex);
595
980
  serverToolResultIds.add(getToolUseId(part) ?? '');
596
981
  }
597
982
  if (options?.provider === Providers.ANTHROPIC) {
@@ -609,22 +994,26 @@ function formatAssistantMessage(
609
994
  }
610
995
  }
611
996
 
612
- for (const part of contentParts) {
997
+ for (let partIndex = 0; partIndex < contentParts.length; partIndex++) {
998
+ const part = contentParts[partIndex];
613
999
  if (part == null) {
614
1000
  continue;
615
1001
  }
1002
+ const sourcePartIndices = getSourcePartIndices(partIndex);
616
1003
  const toolUseId = getToolUseId(part);
617
1004
  if (toolUseId != null) {
618
- const isServerToolResult = isValidServerToolResult(part);
619
- if (
620
- toolUseId.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX) &&
621
- !isServerToolResult
622
- ) {
623
- continue;
624
- }
1005
+ const isServerToolResult =
1006
+ trustedServerToolResultPartIndices?.has(partIndex) === true;
1007
+ const isWireServerToolResult =
1008
+ wireServerToolResultPartIndices?.has(partIndex) === true;
625
1009
  flushPendingServerToolUse(toolUseId);
626
- if (isServerToolResult) {
1010
+ if (isWireServerToolResult) {
627
1011
  currentContent.push(part);
1012
+ appendCurrentContentProvenance(
1013
+ sourcePartIndices,
1014
+ isServerToolResult ? 'tool' : 'model',
1015
+ !isServerToolResult
1016
+ );
628
1017
  continue;
629
1018
  }
630
1019
  } else if (hasMeaningfulAssistantContent(part)) {
@@ -644,9 +1033,13 @@ function formatAssistantMessage(
644
1033
  currentContent.some((content) => content.type !== ContentTypes.TEXT)
645
1034
  ) {
646
1035
  currentContent.push(part);
647
- lastAIMessage = createAIMessage(toLangChainContent(currentContent));
648
- appendFormattedMessage(lastAIMessage);
1036
+ appendCurrentContentProvenance(sourcePartIndices, 'model', true);
1037
+ lastAIMessage = appendAIMessage(
1038
+ toLangChainContent(currentContent),
1039
+ currentContentProvenance
1040
+ );
649
1041
  currentContent = [];
1042
+ currentContentProvenance = createProviderMessageProvenanceBuilder();
650
1043
  continue;
651
1044
  }
652
1045
  let content = currentContent.reduce((acc, curr) => {
@@ -656,14 +1049,17 @@ function formatAssistantMessage(
656
1049
  return acc;
657
1050
  }, '');
658
1051
  content = `${content}\n${getTextContent(part)}`.trim();
659
- lastAIMessage = createAIMessage(content);
660
- appendFormattedMessage(lastAIMessage);
1052
+ appendCurrentContentProvenance(sourcePartIndices, 'model', true);
1053
+ lastAIMessage = appendAIMessage(content, currentContentProvenance);
661
1054
  currentContent = [];
1055
+ currentContentProvenance = createProviderMessageProvenanceBuilder();
662
1056
  continue;
663
1057
  }
664
1058
  // Create a new AIMessage with this text and prepare for tool calls
665
- lastAIMessage = createAIMessage(getTextContent(part));
666
- appendFormattedMessage(lastAIMessage);
1059
+ lastAIMessage = appendAIMessage(
1060
+ getTextContent(part),
1061
+ createSourceProvenanceBuilder(sourcePartIndices, 'model', true)
1062
+ );
667
1063
  } else if (part.type === ContentTypes.TOOL_CALL) {
668
1064
  // Skip malformed tool call entries without tool_call property
669
1065
  if (part.tool_call == null) {
@@ -711,20 +1107,26 @@ function formatAssistantMessage(
711
1107
  continue;
712
1108
  }
713
1109
  pendingServerToolUses.set(_tool_call.id, {
714
- type: 'server_tool_use',
715
- id: _tool_call.id,
716
- name: _tool_call.name,
717
- input: parseServerToolInput(_args),
718
- } as MessageContentComplex);
1110
+ content: {
1111
+ type: 'server_tool_use',
1112
+ id: _tool_call.id,
1113
+ name: _tool_call.name,
1114
+ input: parseServerToolInput(_args),
1115
+ } as MessageContentComplex,
1116
+ sourceContentPartIndices: sourcePartIndices,
1117
+ });
719
1118
  continue;
720
1119
  }
721
1120
 
722
1121
  if (!lastAIMessage) {
723
1122
  // "Heal" the payload by creating an AIMessage to precede the tool call
724
- lastAIMessage = createAIMessage('');
725
- appendFormattedMessage(lastAIMessage);
1123
+ lastAIMessage = appendAIMessage(
1124
+ '',
1125
+ createSourceProvenanceBuilder(sourcePartIndices, 'model')
1126
+ );
726
1127
  } else {
727
1128
  attachPendingReasoningContent(lastAIMessage);
1129
+ appendSourcePartIndices(lastAIMessage, 'model', sourcePartIndices);
728
1130
  }
729
1131
 
730
1132
  const tool_call: ToolCallPart = _tool_call;
@@ -768,7 +1170,9 @@ function formatAssistantMessage(
768
1170
  content: formatToolCallOutput(output),
769
1171
  }),
770
1172
  'tool'
771
- )
1173
+ ),
1174
+ 'tool',
1175
+ sourcePartIndices
772
1176
  );
773
1177
  } else if (
774
1178
  part.type === ContentTypes.THINK ||
@@ -779,6 +1183,10 @@ function formatAssistantMessage(
779
1183
  ) {
780
1184
  hasReasoning = true;
781
1185
  pendingReasoningContent += extractReasoningContent(part);
1186
+ appendSourceContentPartIndices(
1187
+ pendingReasoningSourcePartIndices,
1188
+ sourcePartIndices
1189
+ );
782
1190
  continue;
783
1191
  } else if (part.type === ContentTypes.STEER) {
784
1192
  /*
@@ -798,18 +1206,20 @@ function formatAssistantMessage(
798
1206
  if (
799
1207
  currentContent.some((content) => content.type !== ContentTypes.TEXT)
800
1208
  ) {
801
- lastAIMessage = createAIMessage(toLangChainContent(currentContent));
802
- appendFormattedMessage(lastAIMessage);
1209
+ appendAIMessage(
1210
+ toLangChainContent(currentContent),
1211
+ currentContentProvenance
1212
+ );
803
1213
  } else {
804
1214
  const flushed = currentContent
805
1215
  .reduce((acc, curr) => `${acc}${getTextContent(curr)}\n`, '')
806
1216
  .trim();
807
1217
  if (flushed.length > 0) {
808
- lastAIMessage = createAIMessage(flushed);
809
- appendFormattedMessage(lastAIMessage);
1218
+ appendAIMessage(flushed, currentContentProvenance);
810
1219
  }
811
1220
  }
812
1221
  currentContent = [];
1222
+ currentContentProvenance = createProviderMessageProvenanceBuilder();
813
1223
  } else if (shouldPreserveReasoningContent && pendingReasoningContent) {
814
1224
  /**
815
1225
  * Reasoning directly preceding a steer has no `currentContent`
@@ -818,8 +1228,7 @@ function formatAssistantMessage(
818
1228
  * `additional_kwargs.reasoning_content`) or the persisted
819
1229
  * assistant reasoning silently vanishes on replay.
820
1230
  */
821
- lastAIMessage = createAIMessage('');
822
- appendFormattedMessage(lastAIMessage);
1231
+ appendAIMessage('', createProviderMessageProvenanceBuilder());
823
1232
  }
824
1233
  const steerPart = part as {
825
1234
  steer?: string;
@@ -836,7 +1245,9 @@ function formatAssistantMessage(
836
1245
  additional_kwargs: { role: 'user', source: 'steer' },
837
1246
  }),
838
1247
  'user'
839
- )
1248
+ ),
1249
+ 'user',
1250
+ sourcePartIndices
840
1251
  );
841
1252
  lastAIMessage = null;
842
1253
  /** The steer splits the assistant message: the post-steer segment
@@ -844,6 +1255,7 @@ function formatAssistantMessage(
844
1255
  * flushed above or intentionally dropped when not preserving). */
845
1256
  hasReasoning = false;
846
1257
  pendingReasoningContent = '';
1258
+ pendingReasoningSourcePartIndices = [];
847
1259
  } else if (
848
1260
  part.type === ContentTypes.ERROR ||
849
1261
  part.type === ContentTypes.AGENT_UPDATE ||
@@ -856,10 +1268,16 @@ function formatAssistantMessage(
856
1268
  continue;
857
1269
  }
858
1270
  currentContent.push(part);
1271
+ appendCurrentContentProvenance(sourcePartIndices, 'model', true);
859
1272
  }
860
1273
  }
861
- for (const content of pendingServerToolUses.values()) {
862
- currentContent.push(content);
1274
+ for (const pending of pendingServerToolUses.values()) {
1275
+ currentContent.push(pending.content);
1276
+ appendCurrentContentProvenance(
1277
+ pending.sourceContentPartIndices,
1278
+ 'model',
1279
+ true
1280
+ );
863
1281
  }
864
1282
  }
865
1283
 
@@ -867,34 +1285,44 @@ function formatAssistantMessage(
867
1285
  let content = '';
868
1286
  for (const part of currentContent) {
869
1287
  if (part.type !== ContentTypes.TEXT) {
870
- appendFormattedMessage(
871
- createAIMessage(toLangChainContent(currentContent))
1288
+ appendAIMessage(
1289
+ toLangChainContent(currentContent),
1290
+ currentContentProvenance
872
1291
  );
873
- return formattedMessages;
1292
+ return finalizeFormattedMessages();
874
1293
  }
875
1294
  content += `${getTextContent(part)}\n`;
876
1295
  }
877
1296
  content = content.trim();
878
1297
 
879
1298
  if (content) {
880
- appendFormattedMessage(createAIMessage(content));
1299
+ appendAIMessage(content, currentContentProvenance);
881
1300
  }
882
1301
  } else if (currentContent.length > 0) {
883
- appendFormattedMessage(createAIMessage(toLangChainContent(currentContent)));
1302
+ appendAIMessage(
1303
+ toLangChainContent(currentContent),
1304
+ currentContentProvenance
1305
+ );
884
1306
  }
885
1307
 
886
- return formattedMessages;
1308
+ return finalizeFormattedMessages();
887
1309
  }
888
1310
 
889
1311
  function getSourceMessageId(message: Partial<TMessage>): string | undefined {
890
- const candidate =
891
- (message as { messageId?: string }).messageId ??
892
- (message as { id?: string }).id;
893
- if (typeof candidate !== 'string') {
894
- return undefined;
1312
+ const candidates = [
1313
+ (message as { messageId?: unknown }).messageId,
1314
+ (message as { id?: unknown }).id,
1315
+ ];
1316
+ for (const candidate of candidates) {
1317
+ if (typeof candidate !== 'string') {
1318
+ continue;
1319
+ }
1320
+ const normalized = candidate.trim();
1321
+ if (normalized.length > 0) {
1322
+ return normalized;
1323
+ }
895
1324
  }
896
- const normalized = candidate.trim();
897
- return normalized.length > 0 ? normalized : undefined;
1325
+ return undefined;
898
1326
  }
899
1327
 
900
1328
  /**
@@ -907,13 +1335,53 @@ function getSourceMessageId(message: Partial<TMessage>): string | undefined {
907
1335
  function stampSourceMessageIdentity(
908
1336
  message: RoleBearingMessage<BaseMessage>,
909
1337
  sourceMessageId: string | undefined,
910
- derivedIndex = 0
1338
+ derivedIndex = 0,
1339
+ attribution: ProviderMessageAttribution = 'model',
1340
+ sourceContentPartIndices?: readonly number[],
1341
+ provenanceParts?: readonly ProviderMessageProvenancePart[]
911
1342
  ): void {
912
- if (sourceMessageId == null) {
913
- return;
1343
+ if (sourceMessageId != null) {
1344
+ message.additional_kwargs.sourceMessageId = sourceMessageId;
1345
+ }
1346
+ let partsToStamp: readonly ProviderMessageProvenancePart[];
1347
+ if (provenanceParts != null && provenanceParts.length > 0) {
1348
+ let needsSourceMessageId = false;
1349
+ if (sourceMessageId != null) {
1350
+ for (const part of provenanceParts) {
1351
+ if (part.sourceMessageId == null) {
1352
+ needsSourceMessageId = true;
1353
+ break;
1354
+ }
1355
+ }
1356
+ }
1357
+ if (needsSourceMessageId) {
1358
+ const completedParts: ProviderMessageProvenancePart[] = [];
1359
+ for (const part of provenanceParts) {
1360
+ completedParts.push({
1361
+ ...part,
1362
+ ...(part.sourceMessageId == null && { sourceMessageId }),
1363
+ });
1364
+ }
1365
+ partsToStamp = completedParts;
1366
+ } else {
1367
+ partsToStamp = provenanceParts;
1368
+ }
1369
+ } else {
1370
+ partsToStamp = [
1371
+ {
1372
+ attribution,
1373
+ ...(sourceMessageId != null && { sourceMessageId }),
1374
+ ...(sourceContentPartIndices != null && {
1375
+ sourceContentPartIndices,
1376
+ }),
1377
+ },
1378
+ ];
914
1379
  }
915
- message.additional_kwargs.sourceMessageId = sourceMessageId;
916
- if (derivedIndex !== 0) {
1380
+ setProviderMessageProvenance(
1381
+ message,
1382
+ partsToStamp
1383
+ );
1384
+ if (sourceMessageId == null || derivedIndex !== 0) {
917
1385
  return;
918
1386
  }
919
1387
  message.id = sourceMessageId;
@@ -979,7 +1447,9 @@ function labelAllAgentContent(
979
1447
  } as MessageContentComplex);
980
1448
  } else {
981
1449
  // No agent ID, pass through as-is
982
- result.push(...agentContentBuffer);
1450
+ for (const part of agentContentBuffer) {
1451
+ result.push(part);
1452
+ }
983
1453
  }
984
1454
 
985
1455
  agentContentBuffer = [];
@@ -1108,7 +1578,9 @@ export const labelContentByAgent = (
1108
1578
  }
1109
1579
  } else {
1110
1580
  // Not from a transfer, add as-is
1111
- result.push(...agentContentBuffer);
1581
+ for (const part of agentContentBuffer) {
1582
+ result.push(part);
1583
+ }
1112
1584
  }
1113
1585
 
1114
1586
  agentContentBuffer = [];
@@ -1541,12 +2013,12 @@ export const formatAgentMessages = (
1541
2013
  if (next.role === 'assistant') {
1542
2014
  return;
1543
2015
  }
1544
- messages.push(
1545
- withMessageRole(
1546
- new AIMessage({ content: STEER_ANCHOR_PLACEHOLDER }),
1547
- 'assistant'
1548
- )
2016
+ const anchor = withMessageRole(
2017
+ new AIMessage({ content: STEER_ANCHOR_PLACEHOLDER }),
2018
+ 'assistant'
1549
2019
  );
2020
+ stampSourceMessageIdentity(anchor, undefined, 0, 'synthetic');
2021
+ messages.push(anchor);
1550
2022
  };
1551
2023
  // If indexTokenCountMap is provided, create a new map to track the updated indices
1552
2024
  const updatedIndexTokenCountMap: Record<number, number> = {};
@@ -1577,6 +2049,12 @@ export const formatAgentMessages = (
1577
2049
  continue;
1578
2050
  }
1579
2051
 
2052
+ const sourceContentPartOffset =
2053
+ summaryBoundary?.mode === 'positional' &&
2054
+ summaryBoundary.messageIndex === i
2055
+ ? summaryBoundary.contentIndex + 1
2056
+ : 0;
2057
+
1580
2058
  // Q: Store the current length of messages to track where this payload message starts in the result?
1581
2059
  // const startIndex = messages.length;
1582
2060
  if (typeof message.content === 'string') {
@@ -1599,7 +2077,26 @@ export const formatAgentMessages = (
1599
2077
  | RoleBearingMessage<HumanMessage>
1600
2078
  | RoleBearingMessage<AIMessage>
1601
2079
  | RoleBearingMessage<SystemMessage>;
1602
- stampSourceMessageIdentity(formattedMessage, sourceMessageId);
2080
+ let attribution: ProviderMessageAttribution = 'synthetic';
2081
+ if (formattedMessage.role === 'user') {
2082
+ attribution = 'user';
2083
+ } else if (formattedMessage.role === 'assistant') {
2084
+ attribution = 'model';
2085
+ }
2086
+ const provenanceParts = createProviderContentProvenanceParts(
2087
+ message.content as MessageContentComplex[],
2088
+ sourceContentPartOffset,
2089
+ sourceMessageId,
2090
+ attribution
2091
+ );
2092
+ stampSourceMessageIdentity(
2093
+ formattedMessage,
2094
+ sourceMessageId,
2095
+ 0,
2096
+ attribution,
2097
+ undefined,
2098
+ provenanceParts
2099
+ );
1603
2100
  flushSteerAnchor(formattedMessage);
1604
2101
  messages.push(formattedMessage);
1605
2102
 
@@ -1618,17 +2115,36 @@ export const formatAgentMessages = (
1618
2115
  * - Dynamically expand the set when tool_search results are encountered
1619
2116
  */
1620
2117
  let processedMessage = message;
2118
+ let processedSourceContentPartIndices:
2119
+ | SourceContentPartIndices[]
2120
+ | undefined;
2121
+ const processedToolSourceContentPartIndices =
2122
+ collectTrustedToolResultSourceContentPartIndices(
2123
+ getBoundedProviderPairingArrayProperty(message, 'content'),
2124
+ sourceContentPartOffset
2125
+ );
1621
2126
  let pendingSkillNames: Set<string> | undefined;
1622
2127
  if (discoveredTools) {
1623
2128
  const content = message.content;
1624
2129
  if (content != null && Array.isArray(content)) {
1625
2130
  const filteredContent: typeof content = [];
2131
+ const filteredSourceContentPartIndices: SourceContentPartIndices[] = [];
1626
2132
  const invalidToolCallIds = new Set<string>();
1627
2133
  const invalidToolStrings: string[] = [];
1628
-
1629
- for (const part of content) {
2134
+ const invalidToolSourceContentPartIndices: number[] = [];
2135
+
2136
+ for (let partIndex = 0; partIndex < content.length; partIndex++) {
2137
+ const part = content[partIndex] as
2138
+ | MessageContentComplex
2139
+ | null
2140
+ | undefined;
2141
+ const partSourceContentIndices = sourceContentPartOffset + partIndex;
2142
+ if (part == null || typeof part !== 'object') {
2143
+ continue;
2144
+ }
1630
2145
  if (part.type !== ContentTypes.TOOL_CALL) {
1631
2146
  filteredContent.push(part);
2147
+ filteredSourceContentPartIndices.push(partSourceContentIndices);
1632
2148
  continue;
1633
2149
  }
1634
2150
 
@@ -1668,6 +2184,7 @@ export const formatAgentMessages = (
1668
2184
 
1669
2185
  if (discoveredTools.has(toolName)) {
1670
2186
  filteredContent.push(part);
2187
+ filteredSourceContentPartIndices.push(partSourceContentIndices);
1671
2188
  if (
1672
2189
  toolName === Constants.SKILL_TOOL &&
1673
2190
  skills?.size != null &&
@@ -1688,6 +2205,10 @@ export const formatAgentMessages = (
1688
2205
  }
1689
2206
  const output = part.tool_call.output ?? '';
1690
2207
  invalidToolStrings.push(`Tool: ${toolName}, ${output}`);
2208
+ appendSourceContentPartIndices(
2209
+ invalidToolSourceContentPartIndices,
2210
+ partSourceContentIndices
2211
+ );
1691
2212
  }
1692
2213
  }
1693
2214
 
@@ -1734,12 +2255,23 @@ export const formatAgentMessages = (
1734
2255
  ? `${existingText}\n${invalidToolText}`
1735
2256
  : invalidToolText,
1736
2257
  };
2258
+ const lastTextSourceContentPartIndices =
2259
+ filteredSourceContentPartIndices[lastTextPartIndex];
2260
+ filteredSourceContentPartIndices[lastTextPartIndex] = [
2261
+ ...(typeof lastTextSourceContentPartIndices === 'number'
2262
+ ? [lastTextSourceContentPartIndices]
2263
+ : lastTextSourceContentPartIndices),
2264
+ ...invalidToolSourceContentPartIndices,
2265
+ ];
1737
2266
  } else {
1738
2267
  /** No text part exists, create one */
1739
2268
  filteredContent.push({
1740
2269
  type: ContentTypes.TEXT,
1741
2270
  [ContentTypes.TEXT]: invalidToolText,
1742
2271
  });
2272
+ filteredSourceContentPartIndices.push(
2273
+ invalidToolSourceContentPartIndices
2274
+ );
1743
2275
  }
1744
2276
  }
1745
2277
 
@@ -1749,6 +2281,7 @@ export const formatAgentMessages = (
1749
2281
  invalidToolStrings.length > 0
1750
2282
  ) {
1751
2283
  processedMessage = { ...message, content: filteredContent };
2284
+ processedSourceContentPartIndices = filteredSourceContentPartIndices;
1752
2285
  }
1753
2286
  }
1754
2287
  }
@@ -1757,8 +2290,12 @@ export const formatAgentMessages = (
1757
2290
  if (!discoveredTools && skills?.size != null && skills.size > 0) {
1758
2291
  const content = processedMessage.content;
1759
2292
  if (Array.isArray(content)) {
1760
- for (const part of content) {
2293
+ for (const part of content as Array<
2294
+ MessageContentComplex | null | undefined
2295
+ >) {
1761
2296
  if (
2297
+ part == null ||
2298
+ typeof part !== 'object' ||
1762
2299
  part.type !== ContentTypes.TOOL_CALL ||
1763
2300
  part.tool_call?.name !== Constants.SKILL_TOOL
1764
2301
  ) {
@@ -1779,6 +2316,9 @@ export const formatAgentMessages = (
1779
2316
  options?.provider === Providers.DEEPSEEK,
1780
2317
  provider: options?.provider,
1781
2318
  sourceMessageId,
2319
+ sourceContentPartOffset,
2320
+ sourceContentPartIndices: processedSourceContentPartIndices,
2321
+ toolSourceContentPartIndices: processedToolSourceContentPartIndices,
1782
2322
  });
1783
2323
  /**
1784
2324
  * A steer that ends an assistant message leaves the replay on a
@@ -1818,7 +2358,9 @@ export const formatAgentMessages = (
1818
2358
  if (formattedMessages.length > 0) {
1819
2359
  flushSteerAnchor(formattedMessages[0]);
1820
2360
  }
1821
- messages.push(...formattedMessages);
2361
+ for (const formattedMessage of formattedMessages) {
2362
+ messages.push(formattedMessage);
2363
+ }
1822
2364
  if (endsWithSteerMessage(formattedMessages)) {
1823
2365
  pendingSteerAnchor = true;
1824
2366
  }
@@ -1835,20 +2377,20 @@ export const formatAgentMessages = (
1835
2377
  }
1836
2378
  const body = skills?.get(skillName) ?? '';
1837
2379
  if (body) {
1838
- messages.push(
1839
- withMessageRole(
1840
- new HumanMessage({
1841
- content: body,
1842
- additional_kwargs: {
1843
- role: 'user',
1844
- isMeta: true,
1845
- source: 'skill',
1846
- skillName,
1847
- },
1848
- }),
1849
- 'user'
1850
- )
2380
+ const skillMessage = withMessageRole(
2381
+ new HumanMessage({
2382
+ content: body,
2383
+ additional_kwargs: {
2384
+ role: 'user',
2385
+ isMeta: true,
2386
+ source: 'skill',
2387
+ skillName,
2388
+ },
2389
+ }),
2390
+ 'user'
1851
2391
  );
2392
+ stampSourceMessageIdentity(skillMessage, undefined, 0, 'synthetic');
2393
+ messages.push(skillMessage);
1852
2394
  }
1853
2395
  }
1854
2396
  }
@@ -2188,14 +2730,160 @@ function serializeFoldedValue(value: unknown): string {
2188
2730
  .content;
2189
2731
  }
2190
2732
 
2191
- function markSyntheticProviderContext<T extends BaseMessage>(message: T): T {
2733
+ interface FoldedSourceMessage {
2734
+ readonly message: BaseMessage;
2735
+ /** Provider-content positions retained from an array message. Omitted when
2736
+ * string content or tool-call metadata contributed as a whole. */
2737
+ readonly retainedContentPartIndices?: ReadonlySet<number>;
2738
+ readonly mappingAmbiguous?: boolean;
2739
+ }
2740
+
2741
+ function getFoldedSourceAttribution(
2742
+ message: BaseMessage
2743
+ ): ProviderMessageAttribution {
2744
+ const messageType = message.getType();
2745
+ if (messageType === 'ai') {
2746
+ return 'model';
2747
+ }
2748
+ if (messageType === 'tool') {
2749
+ return 'tool';
2750
+ }
2751
+ if (messageType === 'system') {
2752
+ return 'synthetic';
2753
+ }
2754
+ return 'user';
2755
+ }
2756
+
2757
+ function getSyntheticProviderContextProvenanceParts(
2758
+ sourceMessages: readonly FoldedSourceMessage[]
2759
+ ): ProviderMessageProvenancePart[] | null {
2760
+ /** Fold labels are generated context, while retained source bytes keep their
2761
+ * original attribution so downstream policy can still route them exactly. */
2762
+ const parts: ProviderMessageProvenancePart[] = [
2763
+ { attribution: 'synthetic' },
2764
+ ];
2765
+ for (const source of sourceMessages) {
2766
+ const {
2767
+ message: sourceMessage,
2768
+ retainedContentPartIndices,
2769
+ mappingAmbiguous,
2770
+ } = source;
2771
+ const provenanceState = inspectProviderMessageProvenance(sourceMessage);
2772
+ const sourceMessageIdsState =
2773
+ inspectProviderSourceMessageIds(sourceMessage);
2774
+ if (
2775
+ provenanceState.status === 'invalid' ||
2776
+ sourceMessageIdsState.status === 'invalid'
2777
+ ) {
2778
+ return null;
2779
+ }
2780
+ const explicit =
2781
+ provenanceState.status === 'valid'
2782
+ ? provenanceState.provenance
2783
+ : undefined;
2784
+ const sourceMessageIds =
2785
+ sourceMessageIdsState.status === 'valid'
2786
+ ? sourceMessageIdsState.sourceMessageIds
2787
+ : [];
2788
+ const fallbackAttribution = getFoldedSourceAttribution(sourceMessage);
2789
+ const sourcePartStart = parts.length;
2790
+ const retainedSourceIds = new Set<string>();
2791
+ const contentLength = Array.isArray(sourceMessage.content)
2792
+ ? sourceMessage.content.length
2793
+ : undefined;
2794
+ const mapsOneToOne =
2795
+ mappingAmbiguous !== true &&
2796
+ retainedContentPartIndices != null &&
2797
+ contentLength != null &&
2798
+ explicit != null &&
2799
+ hasBijectiveProviderContentPartMapping(explicit.parts, contentLength);
2800
+ const retainedAllContentParts =
2801
+ mappingAmbiguous !== true &&
2802
+ retainedContentPartIndices != null &&
2803
+ contentLength != null &&
2804
+ retainedContentPartIndices.size === contentLength;
2805
+ const retainUnindexedSourceIds =
2806
+ (mappingAmbiguous !== true && retainedContentPartIndices == null) ||
2807
+ retainedAllContentParts ||
2808
+ sourceMessageIds.length <= 1;
2809
+ for (const part of explicit?.parts ?? []) {
2810
+ let sourceContentPartIndices = part.sourceContentPartIndices;
2811
+ if (mappingAmbiguous === true && sourceContentPartIndices != null) {
2812
+ sourceContentPartIndices = undefined;
2813
+ }
2814
+ if (
2815
+ retainedContentPartIndices != null &&
2816
+ sourceContentPartIndices != null &&
2817
+ !retainedAllContentParts
2818
+ ) {
2819
+ if (!mapsOneToOne) {
2820
+ sourceContentPartIndices = undefined;
2821
+ } else {
2822
+ const retainedSourceContentPartIndices: number[] = [];
2823
+ for (const sourceContentPartIndex of sourceContentPartIndices) {
2824
+ if (retainedContentPartIndices.has(sourceContentPartIndex)) {
2825
+ retainedSourceContentPartIndices.push(sourceContentPartIndex);
2826
+ }
2827
+ }
2828
+ if (retainedSourceContentPartIndices.length === 0) {
2829
+ continue;
2830
+ }
2831
+ sourceContentPartIndices = retainedSourceContentPartIndices;
2832
+ }
2833
+ }
2834
+ const sourceMessageId =
2835
+ sourceContentPartIndices != null || retainUnindexedSourceIds
2836
+ ? part.sourceMessageId
2837
+ : undefined;
2838
+ parts.push({
2839
+ attribution: part.attribution,
2840
+ ...(sourceMessageId != null && {
2841
+ sourceMessageId,
2842
+ }),
2843
+ ...(sourceContentPartIndices != null && {
2844
+ sourceContentPartIndices,
2845
+ }),
2846
+ });
2847
+ if (sourceMessageId != null) {
2848
+ retainedSourceIds.add(sourceMessageId);
2849
+ }
2850
+ }
2851
+ for (const sourceMessageId of sourceMessageIds) {
2852
+ if (!retainUnindexedSourceIds) {
2853
+ break;
2854
+ }
2855
+ if (!retainedSourceIds.has(sourceMessageId)) {
2856
+ parts.push({ attribution: fallbackAttribution, sourceMessageId });
2857
+ }
2858
+ }
2859
+ if (parts.length === sourcePartStart) {
2860
+ parts.push({ attribution: fallbackAttribution });
2861
+ }
2862
+ }
2863
+ return mergeAdjacentProviderProvenanceParts(parts);
2864
+ }
2865
+
2866
+ function markSyntheticProviderContext<T extends BaseMessage>(
2867
+ message: T,
2868
+ sourceMessages: readonly FoldedSourceMessage[] = []
2869
+ ): T {
2192
2870
  syntheticProviderContextMessages.add(message);
2871
+ const parts = getSyntheticProviderContextProvenanceParts(sourceMessages);
2872
+ if (parts == null) {
2873
+ setInvalidProviderMessageProvenance(message);
2874
+ return message;
2875
+ }
2876
+ setProviderMessageProvenance(
2877
+ message,
2878
+ parts.length > 0 ? parts : [{ attribution: 'synthetic' }]
2879
+ );
2193
2880
  return message;
2194
2881
  }
2195
2882
 
2196
2883
  function appendSyntheticProviderContextMessage(
2197
2884
  result: BaseMessage[],
2198
- parts: MessageContentComplex[]
2885
+ parts: MessageContentComplex[],
2886
+ sourceMessages: readonly FoldedSourceMessage[] = []
2199
2887
  ): boolean {
2200
2888
  if (parts.length === 0) {
2201
2889
  return false;
@@ -2205,7 +2893,8 @@ function appendSyntheticProviderContextMessage(
2205
2893
  withMessageRole(
2206
2894
  new HumanMessage({ content: toLangChainContent(parts) }),
2207
2895
  'user'
2208
- )
2896
+ ),
2897
+ sourceMessages
2209
2898
  )
2210
2899
  );
2211
2900
  return true;
@@ -2222,6 +2911,34 @@ export function isSyntheticProviderContextMessage(
2222
2911
  return syntheticProviderContextMessages.has(message);
2223
2912
  }
2224
2913
 
2914
+ /** Compacts a folded provider-context message without retaining source claims
2915
+ * for bytes whose mapping cannot survive the lossy compaction. The unsourced
2916
+ * user/tool parts intentionally make security consumers inspect the exact
2917
+ * compacted wire under both external trust domains. */
2918
+ export function compactSyntheticProviderContextMessage(
2919
+ message: HumanMessage,
2920
+ maxChars: number
2921
+ ): HumanMessage {
2922
+ const compactedContent = compactToolContent(message.content, maxChars);
2923
+ if (!compactedContent.changed) {
2924
+ return message;
2925
+ }
2926
+ const compacted = new HumanMessage({
2927
+ content: compactedContent.content,
2928
+ id: message.id,
2929
+ name: message.name,
2930
+ additional_kwargs: { ...message.additional_kwargs },
2931
+ response_metadata: message.response_metadata,
2932
+ });
2933
+ syntheticProviderContextMessages.add(compacted);
2934
+ setProviderMessageProvenance(compacted, [
2935
+ { attribution: 'synthetic' },
2936
+ { attribution: 'user' },
2937
+ { attribution: 'tool' },
2938
+ ]);
2939
+ return compacted;
2940
+ }
2941
+
2225
2942
  /** Flushes accumulated text chunks into `parts` as a single text block. */
2226
2943
  function flushTextChunks(
2227
2944
  textChunks: string[],
@@ -2254,29 +2971,53 @@ function appendMessageContent(
2254
2971
  textChunks: string[],
2255
2972
  parts: MessageContentComplex[],
2256
2973
  budget: FoldContextBudget
2257
- ): void {
2974
+ ): FoldedSourceMessage | undefined {
2258
2975
  const { content } = msg;
2259
2976
 
2260
2977
  if (typeof content === 'string') {
2261
- if (content && consumeFoldedWork(textChunks, budget)) {
2262
- appendFoldedLine(textChunks, budget, [`${role}: `, content]);
2978
+ const remainingCharsBefore = budget.remainingChars;
2979
+ let contentComplete = true;
2980
+ if (content) {
2981
+ contentComplete =
2982
+ consumeFoldedWork(textChunks, budget) &&
2983
+ appendFoldedLine(textChunks, budget, [`${role}: `, content]);
2263
2984
  }
2264
- appendToolCalls(msg, role, textChunks, budget);
2265
- return;
2985
+ const toolCalls = appendToolCalls(msg, role, textChunks, budget);
2986
+ return budget.remainingChars < remainingCharsBefore || toolCalls.contributed
2987
+ ? {
2988
+ message: msg,
2989
+ ...((!contentComplete || !toolCalls.complete) && {
2990
+ mappingAmbiguous: true,
2991
+ }),
2992
+ }
2993
+ : undefined;
2266
2994
  }
2267
2995
 
2268
2996
  if (!Array.isArray(content)) {
2269
- appendToolCalls(msg, role, textChunks, budget);
2270
- return;
2997
+ const toolCalls = appendToolCalls(msg, role, textChunks, budget);
2998
+ return toolCalls.contributed
2999
+ ? {
3000
+ message: msg,
3001
+ ...(!toolCalls.complete && { mappingAmbiguous: true }),
3002
+ }
3003
+ : undefined;
2271
3004
  }
2272
3005
 
2273
3006
  let hasToolUseBlock = false;
3007
+ const retainedContentPartIndices = new Set<number>();
2274
3008
 
2275
- for (const block of content as ExtendedMessageContent[]) {
3009
+ for (let blockIndex = 0; blockIndex < content.length; blockIndex++) {
3010
+ const block = content[blockIndex] as ExtendedMessageContent;
2276
3011
  if (!consumeFoldedWork(textChunks, budget)) {
2277
3012
  hasToolUseBlock = true;
2278
3013
  break;
2279
3014
  }
3015
+ const remainingCharsBefore = budget.remainingChars;
3016
+ const markBlockRetained = (): void => {
3017
+ if (budget.remainingChars < remainingCharsBefore) {
3018
+ retainedContentPartIndices.add(blockIndex);
3019
+ }
3020
+ };
2280
3021
  const blockTypeValue = readFoldedDataProperty(block, 'type');
2281
3022
  const blockType =
2282
3023
  typeof blockTypeValue === 'string' ? blockTypeValue : undefined;
@@ -2293,6 +3034,7 @@ function appendMessageContent(
2293
3034
  appendFoldedLine(textChunks, budget, [
2294
3035
  `${role}: [${blockType ?? 'media'} omitted: folded context limit]`,
2295
3036
  ]);
3037
+ markBlockRetained();
2296
3038
  continue;
2297
3039
  }
2298
3040
  flushTextChunks(textChunks, parts);
@@ -2304,6 +3046,7 @@ function appendMessageContent(
2304
3046
  serializeFoldedValue(block),
2305
3047
  ]);
2306
3048
  }
3049
+ markBlockRetained();
2307
3050
  continue;
2308
3051
  }
2309
3052
 
@@ -2315,6 +3058,7 @@ function appendMessageContent(
2315
3058
  `${role}: [tool_use] ${typeof name === 'string' ? name : ''} `,
2316
3059
  serializeFoldedValue(input ?? {}),
2317
3060
  ]);
3061
+ markBlockRetained();
2318
3062
  continue;
2319
3063
  }
2320
3064
 
@@ -2350,6 +3094,7 @@ function appendMessageContent(
2350
3094
  typeof output === 'string' ? output : serializeFoldedValue(output),
2351
3095
  ]);
2352
3096
  }
3097
+ markBlockRetained();
2353
3098
  continue;
2354
3099
  }
2355
3100
 
@@ -2419,6 +3164,7 @@ function appendMessageContent(
2419
3164
  serializeFoldedValue(inner),
2420
3165
  ]);
2421
3166
  }
3167
+ markBlockRetained();
2422
3168
  continue;
2423
3169
  }
2424
3170
 
@@ -2427,6 +3173,7 @@ function appendMessageContent(
2427
3173
  readFoldedDataProperty(block, 'input');
2428
3174
  if (typeof text === 'string' && text) {
2429
3175
  appendFoldedLine(textChunks, budget, [`${role}: `, text]);
3176
+ markBlockRetained();
2430
3177
  continue;
2431
3178
  }
2432
3179
 
@@ -2437,13 +3184,23 @@ function appendMessageContent(
2437
3184
  serializeFoldedValue(block),
2438
3185
  ]);
2439
3186
  }
3187
+ markBlockRetained();
2440
3188
  }
2441
3189
 
2442
3190
  // If content array had no tool_use blocks, fall back to tool_calls metadata
2443
3191
  // (handles edge case: empty content array with tool_calls populated)
2444
- if (!hasToolUseBlock) {
2445
- appendToolCalls(msg, role, textChunks, budget);
3192
+ const toolCalls = !hasToolUseBlock
3193
+ ? appendToolCalls(msg, role, textChunks, budget)
3194
+ : { contributed: false, complete: true };
3195
+ if (toolCalls.contributed) {
3196
+ return {
3197
+ message: msg,
3198
+ ...(!toolCalls.complete && { mappingAmbiguous: true }),
3199
+ };
2446
3200
  }
3201
+ return retainedContentPartIndices.size > 0
3202
+ ? { message: msg, retainedContentPartIndices }
3203
+ : undefined;
2447
3204
  }
2448
3205
 
2449
3206
  function appendToolCalls(
@@ -2451,14 +3208,37 @@ function appendToolCalls(
2451
3208
  role: string,
2452
3209
  textChunks: string[],
2453
3210
  budget: FoldContextBudget
2454
- ): void {
3211
+ ): { contributed: boolean; complete: boolean } {
2455
3212
  if (role !== 'AI') {
2456
- return;
3213
+ return { contributed: false, complete: true };
2457
3214
  }
3215
+ const remainingCharsBefore = budget.remainingChars;
3216
+ let complete = true;
2458
3217
  const aiMsg = msg as AIMessage;
2459
3218
  if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
3219
+ const rawToolCalls = aiMsg.additional_kwargs.tool_calls;
3220
+ if (Array.isArray(rawToolCalls)) {
3221
+ if (rawToolCalls.length !== aiMsg.tool_calls.length) {
3222
+ complete = false;
3223
+ } else {
3224
+ for (let index = 0; index < rawToolCalls.length; index++) {
3225
+ const rawToolCall = rawToolCalls[index];
3226
+ const parsedToolCall = aiMsg.tool_calls[index];
3227
+ const fn = readFoldedDataProperty(rawToolCall, 'function');
3228
+ const rawId = readFoldedDataProperty(rawToolCall, 'id');
3229
+ const rawName = readFoldedDataProperty(fn, 'name');
3230
+ const parsedId = readFoldedDataProperty(parsedToolCall, 'id');
3231
+ const parsedName = readFoldedDataProperty(parsedToolCall, 'name');
3232
+ if (fn == null || rawId !== parsedId || rawName !== parsedName) {
3233
+ complete = false;
3234
+ break;
3235
+ }
3236
+ }
3237
+ }
3238
+ }
2460
3239
  for (const tc of aiMsg.tool_calls) {
2461
3240
  if (!consumeFoldedWork(textChunks, budget)) {
3241
+ complete = false;
2462
3242
  break;
2463
3243
  }
2464
3244
  const name = readFoldedDataProperty(tc, 'name');
@@ -2469,19 +3249,24 @@ function appendToolCalls(
2469
3249
  ')',
2470
3250
  ]);
2471
3251
  }
2472
- return;
3252
+ return {
3253
+ contributed: budget.remainingChars < remainingCharsBefore,
3254
+ complete,
3255
+ };
2473
3256
  }
2474
3257
  // Fall back to raw provider tool calls kept only in additional_kwargs.
2475
3258
  const rawToolCalls = aiMsg.additional_kwargs.tool_calls;
2476
3259
  if (!Array.isArray(rawToolCalls)) {
2477
- return;
3260
+ return { contributed: false, complete: true };
2478
3261
  }
2479
3262
  for (const tc of rawToolCalls) {
2480
3263
  if (!consumeFoldedWork(textChunks, budget)) {
3264
+ complete = false;
2481
3265
  break;
2482
3266
  }
2483
3267
  const fn = readFoldedDataProperty(tc, 'function');
2484
3268
  if (fn == null) {
3269
+ complete = false;
2485
3270
  continue;
2486
3271
  }
2487
3272
  const name = readFoldedDataProperty(fn, 'name');
@@ -2492,6 +3277,10 @@ function appendToolCalls(
2492
3277
  ')',
2493
3278
  ]);
2494
3279
  }
3280
+ return {
3281
+ contributed: budget.remainingChars < remainingCharsBefore,
3282
+ complete,
3283
+ };
2495
3284
  }
2496
3285
 
2497
3286
  /**
@@ -2644,24 +3433,43 @@ export function ensureThinkingBlockInMessages(
2644
3433
  // binary data as text (which caused 174× token amplification).
2645
3434
  const parts: MessageContentComplex[] = [];
2646
3435
  const textChunks: string[] = [];
3436
+ const foldedSourceMessages: FoldedSourceMessage[] = [];
2647
3437
  appendFoldedLine(textChunks, foldBudget, ['[Previous agent context]']);
2648
3438
 
2649
- appendMessageContent(msg, 'AI', textChunks, parts, foldBudget);
3439
+ const aiSource = appendMessageContent(
3440
+ msg,
3441
+ 'AI',
3442
+ textChunks,
3443
+ parts,
3444
+ foldBudget
3445
+ );
3446
+ if (aiSource != null) {
3447
+ foldedSourceMessages.push(aiSource);
3448
+ }
2650
3449
 
2651
3450
  let j = i + 1;
2652
3451
  while (j < messages.length && isToolMessage(messages[j])) {
2653
- appendMessageContent(
3452
+ const toolSource = appendMessageContent(
2654
3453
  messages[j],
2655
3454
  'Tool',
2656
3455
  textChunks,
2657
3456
  parts,
2658
3457
  foldBudget
2659
3458
  );
3459
+ if (toolSource != null) {
3460
+ foldedSourceMessages.push(toolSource);
3461
+ }
2660
3462
  j++;
2661
3463
  }
2662
3464
 
2663
3465
  flushTextChunks(textChunks, parts);
2664
- if (appendSyntheticProviderContextMessage(result, parts)) {
3466
+ if (
3467
+ appendSyntheticProviderContextMessage(
3468
+ result,
3469
+ parts,
3470
+ foldedSourceMessages
3471
+ )
3472
+ ) {
2665
3473
  emitAgentLog(
2666
3474
  config,
2667
3475
  'warn',
@@ -2779,25 +3587,38 @@ export function foldToolBlocksForToollessAgent(
2779
3587
 
2780
3588
  const parts: MessageContentComplex[] = [];
2781
3589
  const textChunks: string[] = [];
3590
+ const foldedSourceMessages: FoldedSourceMessage[] = [];
2782
3591
  appendFoldedLine(textChunks, foldBudget, ['[Previous tool interaction]']);
2783
- appendMessageContent(
3592
+ const initialSource = appendMessageContent(
2784
3593
  msg,
2785
3594
  isToolResultMessage(msg) ? 'Tool' : 'AI',
2786
3595
  textChunks,
2787
3596
  parts,
2788
3597
  foldBudget
2789
3598
  );
3599
+ if (initialSource != null) {
3600
+ foldedSourceMessages.push(initialSource);
3601
+ }
2790
3602
  foldedCount++;
2791
3603
 
2792
3604
  let j = i + 1;
2793
3605
  while (j < messages.length && isToolResultMessage(messages[j])) {
2794
- appendMessageContent(messages[j], 'Tool', textChunks, parts, foldBudget);
3606
+ const toolSource = appendMessageContent(
3607
+ messages[j],
3608
+ 'Tool',
3609
+ textChunks,
3610
+ parts,
3611
+ foldBudget
3612
+ );
3613
+ if (toolSource != null) {
3614
+ foldedSourceMessages.push(toolSource);
3615
+ }
2795
3616
  foldedCount++;
2796
3617
  j++;
2797
3618
  }
2798
3619
 
2799
3620
  flushTextChunks(textChunks, parts);
2800
- appendSyntheticProviderContextMessage(result, parts);
3621
+ appendSyntheticProviderContextMessage(result, parts, foldedSourceMessages);
2801
3622
  i = j;
2802
3623
  }
2803
3624