@librechat/agents 3.3.7 → 3.3.9

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 (168) hide show
  1. package/dist/cjs/graphs/Graph.cjs +47 -13
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +56 -6
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
  6. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +18 -48
  8. package/dist/cjs/instrumentation.cjs.map +1 -1
  9. package/dist/cjs/langfuse.cjs +174 -29
  10. package/dist/cjs/langfuse.cjs.map +1 -1
  11. package/dist/cjs/langfuseConfig.cjs +12 -0
  12. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  13. package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
  14. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
  16. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  17. package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
  18. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
  19. package/dist/cjs/langfuseTraceShaping.cjs +121 -4
  20. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  21. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
  22. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
  24. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  25. package/dist/cjs/llm/init.cjs +3 -3
  26. package/dist/cjs/llm/invoke.cjs +5 -5
  27. package/dist/cjs/llm/openai/index.cjs +1 -1
  28. package/dist/cjs/main.cjs +10 -10
  29. package/dist/cjs/messages/format.cjs +124 -15
  30. package/dist/cjs/messages/format.cjs.map +1 -1
  31. package/dist/cjs/messages/injected.cjs +10 -1
  32. package/dist/cjs/messages/injected.cjs.map +1 -1
  33. package/dist/cjs/messages/prune.cjs +13 -1
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/prompts/activityLabel.cjs +51 -11
  36. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  37. package/dist/cjs/run.cjs +54 -24
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/session/messageSerialization.cjs +6 -0
  40. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  41. package/dist/cjs/stream.cjs +21 -10
  42. package/dist/cjs/stream.cjs.map +1 -1
  43. package/dist/cjs/summarization/node.cjs +60 -0
  44. package/dist/cjs/summarization/node.cjs.map +1 -1
  45. package/dist/cjs/tools/ToolNode.cjs +253 -24
  46. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  47. package/dist/cjs/tools/handlers.cjs +1 -1
  48. package/dist/cjs/tools/search/tool.cjs +1 -1
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  50. package/dist/cjs/utils/index.cjs +2 -2
  51. package/dist/esm/graphs/Graph.mjs +48 -14
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +56 -6
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/hitl/askUserQuestion.mjs +3 -2
  56. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  57. package/dist/esm/instrumentation.mjs +18 -48
  58. package/dist/esm/instrumentation.mjs.map +1 -1
  59. package/dist/esm/langfuse.mjs +176 -28
  60. package/dist/esm/langfuse.mjs.map +1 -1
  61. package/dist/esm/langfuseConfig.mjs +10 -1
  62. package/dist/esm/langfuseConfig.mjs.map +1 -1
  63. package/dist/esm/langfuseRuntimeContext.mjs +21 -3
  64. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  65. package/dist/esm/langfuseRuntimeScope.mjs +39 -10
  66. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  67. package/dist/esm/langfuseSpanRegistry.mjs +91 -0
  68. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
  69. package/dist/esm/langfuseTraceShaping.mjs +121 -4
  70. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  71. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
  72. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
  74. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  75. package/dist/esm/llm/init.mjs +2 -2
  76. package/dist/esm/llm/invoke.mjs +5 -5
  77. package/dist/esm/llm/openai/index.mjs +1 -1
  78. package/dist/esm/main.mjs +8 -8
  79. package/dist/esm/messages/format.mjs +124 -15
  80. package/dist/esm/messages/format.mjs.map +1 -1
  81. package/dist/esm/messages/injected.mjs +10 -1
  82. package/dist/esm/messages/injected.mjs.map +1 -1
  83. package/dist/esm/messages/prune.mjs +13 -1
  84. package/dist/esm/messages/prune.mjs.map +1 -1
  85. package/dist/esm/prompts/activityLabel.mjs +51 -11
  86. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  87. package/dist/esm/run.mjs +54 -24
  88. package/dist/esm/run.mjs.map +1 -1
  89. package/dist/esm/session/messageSerialization.mjs +6 -0
  90. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  91. package/dist/esm/stream.mjs +21 -10
  92. package/dist/esm/stream.mjs.map +1 -1
  93. package/dist/esm/summarization/node.mjs +60 -0
  94. package/dist/esm/summarization/node.mjs.map +1 -1
  95. package/dist/esm/tools/ToolNode.mjs +254 -25
  96. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  97. package/dist/esm/tools/handlers.mjs +1 -1
  98. package/dist/esm/tools/search/tool.mjs +1 -1
  99. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  100. package/dist/esm/utils/index.mjs +2 -2
  101. package/dist/types/graphs/Graph.d.ts +19 -0
  102. package/dist/types/hitl/askUserQuestion.d.ts +11 -1
  103. package/dist/types/langfuse.d.ts +16 -8
  104. package/dist/types/langfuseConfig.d.ts +6 -0
  105. package/dist/types/langfuseRuntimeContext.d.ts +27 -1
  106. package/dist/types/langfuseRuntimeScope.d.ts +17 -2
  107. package/dist/types/langfuseSpanRegistry.d.ts +17 -0
  108. package/dist/types/langfuseTraceShaping.d.ts +2 -1
  109. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
  110. package/dist/types/messages/format.d.ts +9 -8
  111. package/dist/types/prompts/activityLabel.d.ts +8 -1
  112. package/dist/types/run.d.ts +1 -1
  113. package/dist/types/session/types.d.ts +1 -0
  114. package/dist/types/tools/ToolNode.d.ts +7 -1
  115. package/dist/types/types/activityLabel.d.ts +8 -0
  116. package/dist/types/types/hitl.d.ts +8 -0
  117. package/dist/types/types/stream.d.ts +19 -0
  118. package/dist/types/types/tools.d.ts +30 -0
  119. package/package.json +7 -4
  120. package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
  121. package/src/graphs/Graph.ts +69 -20
  122. package/src/graphs/MultiAgentGraph.ts +74 -6
  123. package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
  124. package/src/hitl/askUserQuestion.ts +14 -1
  125. package/src/instrumentation.ts +35 -77
  126. package/src/langfuse.ts +320 -43
  127. package/src/langfuseConfig.ts +24 -0
  128. package/src/langfuseRuntimeContext.ts +43 -1
  129. package/src/langfuseRuntimeScope.ts +94 -21
  130. package/src/langfuseSpanRegistry.ts +131 -0
  131. package/src/langfuseTraceShaping.ts +194 -7
  132. package/src/llm/anthropic/utils/message_inputs.ts +70 -19
  133. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
  134. package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
  135. package/src/llm/bedrock/utils/message_inputs.ts +32 -7
  136. package/src/messages/format.ts +222 -50
  137. package/src/messages/formatAgentMessages.test.ts +308 -6
  138. package/src/messages/injected.test.ts +18 -1
  139. package/src/messages/injected.ts +8 -1
  140. package/src/messages/prune.ts +12 -1
  141. package/src/prompts/activityLabel.ts +67 -2
  142. package/src/run.ts +86 -46
  143. package/src/scripts/activity-labels/captured.json +56 -0
  144. package/src/scripts/activity-labels/checks.cjs +205 -0
  145. package/src/scripts/activity-labels/corpus.cjs +473 -0
  146. package/src/scripts/activity-labels/report.cjs +203 -0
  147. package/src/scripts/activity-labels/rescore.cjs +102 -0
  148. package/src/scripts/activity-labels/run.ts +705 -0
  149. package/src/scripts/activity-labels/variants.ts +71 -0
  150. package/src/session/messageSerialization.ts +12 -1
  151. package/src/session/types.ts +1 -0
  152. package/src/specs/activity-label-prompt.test.ts +109 -0
  153. package/src/specs/agent-handoffs.test.ts +306 -0
  154. package/src/specs/langfuse-callbacks.test.ts +456 -0
  155. package/src/specs/langfuse-routing.integration.test.ts +138 -1
  156. package/src/specs/langfuse-span-registry.test.ts +70 -0
  157. package/src/specs/langfuse-trace-shaping.test.ts +294 -0
  158. package/src/specs/prune.test.ts +38 -1
  159. package/src/stream.ts +70 -6
  160. package/src/summarization/__tests__/node.test.ts +188 -0
  161. package/src/summarization/node.ts +72 -0
  162. package/src/tools/ToolNode.ts +400 -9
  163. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
  164. package/src/tools/__tests__/hitl.test.ts +58 -0
  165. package/src/types/activityLabel.ts +8 -0
  166. package/src/types/hitl.ts +8 -0
  167. package/src/types/stream.ts +20 -0
  168. package/src/types/tools.ts +35 -1
@@ -1,11 +1,13 @@
1
1
  import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
2
2
  import type { RunnableConfig } from '@langchain/core/runnables';
3
+ import type { BaseMessage } from '@langchain/core/messages';
3
4
  import type * as t from '@/types';
4
5
  import {
5
6
  createSummarizeNode,
6
7
  DEFAULT_SUMMARIZATION_PROMPT,
7
8
  DEFAULT_UPDATE_SUMMARIZATION_PROMPT,
8
9
  } from '@/summarization/node';
10
+ import { convertInjectedMessages } from '@/messages/injected';
9
11
  import { Constants, GraphEvents, Providers } from '@/common';
10
12
  import { AgentContext } from '@/agents/AgentContext';
11
13
  import * as providers from '@/llm/providers';
@@ -945,6 +947,192 @@ describe('recency window — first-turn protection', () => {
945
947
  expect((result.messages![2] as AIMessage).content).toBe('turn 2 reply');
946
948
  });
947
949
 
950
+ describe('summary coverage', () => {
951
+ const runCompaction = async (
952
+ messages: BaseMessage[],
953
+ turns = 1
954
+ ): Promise<t.SummaryContentBlock | undefined> => {
955
+ captureEvents();
956
+ jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
957
+ class {
958
+ constructor() {
959
+ return mockInvokeModel('Summary of older turns');
960
+ }
961
+ } as never
962
+ );
963
+
964
+ let summaryBlock: t.SummaryContentBlock | undefined;
965
+ const graph = mockGraph((_stepId, result) => {
966
+ if (result.type === 'summary') {
967
+ summaryBlock = result.summary;
968
+ }
969
+ });
970
+ const summarizeNode = createSummarizeNode({
971
+ agentContext: createAgentContext({
972
+ summarizationConfig: { retainRecent: { turns } },
973
+ } as never),
974
+ graph: graph as never,
975
+ generateStepId,
976
+ });
977
+
978
+ await summarizeNode(
979
+ {
980
+ messages,
981
+ summarizationRequest: {
982
+ remainingContextTokens: 0,
983
+ agentId: 'agent_0',
984
+ },
985
+ },
986
+ {} as RunnableConfig
987
+ );
988
+
989
+ return summaryBlock;
990
+ };
991
+
992
+ it('records the first retained message as the coverage anchor', async () => {
993
+ const summaryBlock = await runCompaction([
994
+ new HumanMessage({ content: 'turn 1 query', id: 'm1' }),
995
+ new AIMessage({ content: 'turn 1 reply', id: 'm2' }),
996
+ new HumanMessage({ content: 'turn 2 query', id: 'm3' }),
997
+ new AIMessage({ content: 'turn 2 reply', id: 'm4' }),
998
+ ]);
999
+
1000
+ expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm3' });
1001
+ });
1002
+
1003
+ it('skips a retained message that carries no source id', async () => {
1004
+ const summaryBlock = await runCompaction([
1005
+ new HumanMessage({ content: 'turn 1 query', id: 'm1' }),
1006
+ new AIMessage({ content: 'turn 1 reply', id: 'm2' }),
1007
+ new HumanMessage({ content: 'turn 2 query' }),
1008
+ new AIMessage({ content: 'turn 2 reply', id: 'm4' }),
1009
+ ]);
1010
+
1011
+ expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm4' });
1012
+ });
1013
+
1014
+ it('omits coverage when no retained message carries a source id', async () => {
1015
+ const summaryBlock = await runCompaction([
1016
+ new HumanMessage('turn 1 query'),
1017
+ new AIMessage('turn 1 reply'),
1018
+ new HumanMessage('turn 2 query'),
1019
+ new AIMessage('turn 2 reply'),
1020
+ ]);
1021
+
1022
+ expect(summaryBlock?.coverage).toBeUndefined();
1023
+ });
1024
+
1025
+ /** A steer expands one source message into pre-steer, steer, and post-steer
1026
+ * messages sharing its ID, and the recency split lands on the steer. The
1027
+ * straddling message is the anchor, so it survives whole. */
1028
+ it('anchors on a source id that straddles the recency boundary', async () => {
1029
+ const summaryBlock = await runCompaction([
1030
+ new HumanMessage({ content: 'turn 1 query', id: 'm1' }),
1031
+ new AIMessage({ content: 'pre-steer reply', id: 'm2' }),
1032
+ new HumanMessage({
1033
+ content: 'steer',
1034
+ id: 'm2',
1035
+ additional_kwargs: { role: 'user', source: 'steer' },
1036
+ }),
1037
+ new AIMessage({ content: 'post-steer reply', id: 'm2' }),
1038
+ ]);
1039
+
1040
+ expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm2' });
1041
+ });
1042
+
1043
+ /** A steer carries `source: 'steer'` but is replayed from a payload entry
1044
+ * and stamped with its ID, so it is a valid anchor. When compaction lands
1045
+ * before any post-steer message exists it is the *only* retained entry —
1046
+ * treating every marked message as synthetic drops it. */
1047
+ it('anchors on a retained steer with no post-steer message', async () => {
1048
+ const summaryBlock = await runCompaction([
1049
+ new HumanMessage({ content: 'turn 1 query', id: 'm1' }),
1050
+ new AIMessage({ content: 'pre-steer reply', id: 'm2' }),
1051
+ new HumanMessage({
1052
+ content: 'steer',
1053
+ id: 'm2',
1054
+ additional_kwargs: { role: 'user', source: 'steer' },
1055
+ }),
1056
+ ]);
1057
+
1058
+ expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm2' });
1059
+ });
1060
+
1061
+ /** `formatAgentMessages` reconstructs skill bodies inside its payload loop
1062
+ * and keeps processing payload entries after, so this unstamped entry — a
1063
+ * reducer UUID by the time compaction sees it — precedes stamped messages.
1064
+ * Anchoring on it would resolve to nothing on the next run. */
1065
+ it('skips a reconstructed skill body to reach the stamped message behind it', async () => {
1066
+ const summaryBlock = await runCompaction(
1067
+ [
1068
+ new HumanMessage({ content: 'turn 1 query', id: 'm1' }),
1069
+ new AIMessage({ content: 'turn 1 reply', id: 'm2' }),
1070
+ new HumanMessage({
1071
+ content: 'skill body',
1072
+ id: 'reducer-uuid',
1073
+ additional_kwargs: {
1074
+ role: 'user',
1075
+ isMeta: true,
1076
+ source: 'skill',
1077
+ skillName: 'demo',
1078
+ },
1079
+ }),
1080
+ new HumanMessage({ content: 'turn 2 query', id: 'm3' }),
1081
+ new AIMessage({ content: 'turn 2 reply', id: 'm4' }),
1082
+ ],
1083
+ 2
1084
+ );
1085
+
1086
+ expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm3' });
1087
+ });
1088
+
1089
+ /** `InjectedMessage` leaves both `isMeta` and `source` optional, so a bare
1090
+ * injected turn carries no marker of its own — and an injected `steer` is
1091
+ * otherwise indistinguishable from a replayed one. `convertInjectedMessages`
1092
+ * records `injected` on everything it builds, which decides both. */
1093
+ it.each([
1094
+ ['a bare injected turn', { role: 'user' as const, content: 'injected' }],
1095
+ [
1096
+ 'an injected steer',
1097
+ {
1098
+ role: 'user' as const,
1099
+ content: 'injected steer',
1100
+ source: 'steer' as const,
1101
+ },
1102
+ ],
1103
+ ])('skips %s when anchoring', async (_label, injected) => {
1104
+ const [converted] = convertInjectedMessages([injected]);
1105
+ converted.id = 'reducer-uuid';
1106
+
1107
+ const summaryBlock = await runCompaction(
1108
+ [
1109
+ new HumanMessage({ content: 'turn 1 query', id: 'm1' }),
1110
+ new AIMessage({ content: 'turn 1 reply', id: 'm2' }),
1111
+ converted,
1112
+ new HumanMessage({ content: 'turn 2 query', id: 'm3' }),
1113
+ new AIMessage({ content: 'turn 2 reply', id: 'm4' }),
1114
+ ],
1115
+ 2
1116
+ );
1117
+
1118
+ expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm3' });
1119
+ });
1120
+
1121
+ it('anchors on the straddling id when it is the only source', async () => {
1122
+ const summaryBlock = await runCompaction([
1123
+ new AIMessage({ content: 'pre-steer reply', id: 'm1' }),
1124
+ new HumanMessage({
1125
+ content: 'steer',
1126
+ id: 'm1',
1127
+ additional_kwargs: { role: 'user', source: 'steer' },
1128
+ }),
1129
+ new AIMessage({ content: 'post-steer reply', id: 'm1' }),
1130
+ ]);
1131
+
1132
+ expect(summaryBlock?.coverage).toEqual({ retainedFromMessageId: 'm1' });
1133
+ });
1134
+ });
1135
+
948
1136
  it('keeps the masked tail content (does not re-inject restored tool payloads into state)', async () => {
949
1137
  captureEvents();
950
1138
 
@@ -381,10 +381,75 @@ function computeSummaryTokenCount(
381
381
  return 0;
382
382
  }
383
383
 
384
+ /**
385
+ * Names the first retained message so the summary declares its own extent
386
+ * rather than leaving the next run to infer coverage from where the block
387
+ * happens to sit.
388
+ *
389
+ * Anchored to the retained side, not the covered side. One source message can
390
+ * expand into several messages — a steer splits an assistant entry into
391
+ * pre-steer, steer, and post-steer entries sharing its ID — and the recency
392
+ * split lands on any human-type message, including the steer. Naming the last
393
+ * *covered* message would then name a half-covered ID with no correct reading;
394
+ * naming the first *retained* message makes that same message the anchor, so it
395
+ * survives whole and everything before it is unambiguously covered.
396
+ *
397
+ * Synthetic entries are skipped, because they can sit *before* a resolvable
398
+ * one. `formatAgentMessages` reconstructs skill bodies inside its payload loop
399
+ * (see the `pendingSkillNames` block) and keeps processing payload entries
400
+ * afterwards, so an unstamped skill body — which `messagesStateReducer` then
401
+ * gives a UUID no payload entry carries — is followed by stamped messages.
402
+ * Anchoring on the UUID would look resolvable at write time and degrade to
403
+ * positional trimming on read, dropping the retained tail. Skipping it reaches
404
+ * the stamped message behind it.
405
+ *
406
+ * `convertInjectedMessages` records `injected` on everything it builds, which is
407
+ * what makes this decidable: `isMeta` and `source` are both optional on
408
+ * `InjectedMessage`, so a bare entry carries no marker of its own, and an
409
+ * injected `source: 'steer'` is otherwise indistinguishable from a replayed one.
410
+ * The remaining `isMeta`/`source` checks cover the constructors that build
411
+ * synthetic entries directly instead of going through that funnel — hook context
412
+ * in `ToolNode` and `StandardGraph`, handoff cues, reconstructed skill bodies.
413
+ *
414
+ * `steer` is exempt from the `source` check because a replayed steer *is*
415
+ * stamped from its payload entry; rejecting every marked `source` once dropped
416
+ * exactly those retained steers. Injected steers are still caught, by `injected`.
417
+ *
418
+ * Known limitation: a payload entry that omits `messageId` is never stamped, so
419
+ * the reducer's UUID is recorded and cannot resolve on the next run. There is no
420
+ * write-time fix — such an entry has no stable ID to name in the next payload
421
+ * either — and the reader's positional fallback is what `main` already does, so
422
+ * the anchor degrades rather than misleads.
423
+ */
424
+ function isSyntheticContext(message: BaseMessage): boolean {
425
+ const { additional_kwargs: kwargs } = message;
426
+ if (kwargs.injected === true || kwargs.isMeta === true) {
427
+ return true;
428
+ }
429
+ return kwargs.source != null && kwargs.source !== 'steer';
430
+ }
431
+
432
+ function resolveSummaryCoverage(
433
+ messagesToRetain: BaseMessage[]
434
+ ): t.SummaryCoverage | undefined {
435
+ for (let i = 0; i < messagesToRetain.length; i++) {
436
+ const message = messagesToRetain[i];
437
+ if (isSyntheticContext(message)) {
438
+ continue;
439
+ }
440
+ const id = message.id?.trim();
441
+ if (id != null && id !== '') {
442
+ return { retainedFromMessageId: id };
443
+ }
444
+ }
445
+ return undefined;
446
+ }
447
+
384
448
  /** Constructs the SummaryContentBlock persisted in the run step and dispatched to events. */
385
449
  function buildSummaryBlock(params: {
386
450
  summaryText: string;
387
451
  tokenCount: number;
452
+ coverage?: t.SummaryCoverage;
388
453
  stepId: string;
389
454
  stepIndex: number;
390
455
  modelName?: string;
@@ -400,6 +465,7 @@ function buildSummaryBlock(params: {
400
465
  } as t.MessageContentComplex,
401
466
  ],
402
467
  tokenCount: params.tokenCount,
468
+ ...(params.coverage != null ? { coverage: params.coverage } : {}),
403
469
  summaryVersion: params.summaryVersion,
404
470
  boundary: {
405
471
  messageId: params.stepId,
@@ -1025,6 +1091,11 @@ export function createSummarizeNode({
1025
1091
  metadata: {
1026
1092
  ...config.metadata,
1027
1093
  agent_id: request.agentId,
1094
+ /** Canonical agent-identity key, overwritten by every component
1095
+ * that stamps identity (graph model path, ToolNode, here) so the
1096
+ * closest stamper always wins via spread order — Langfuse scope
1097
+ * trust relies on it (see `isForeignScope`). */
1098
+ agentId: request.agentId,
1028
1099
  summarization_provider: clientConfig.provider,
1029
1100
  summarization_model: clientConfig.modelName,
1030
1101
  /**
@@ -1145,6 +1216,7 @@ export function createSummarizeNode({
1145
1216
  const summaryBlock = buildSummaryBlock({
1146
1217
  summaryText,
1147
1218
  tokenCount,
1219
+ coverage: resolveSummaryCoverage(messagesToRetain),
1148
1220
  stepId,
1149
1221
  stepIndex: runStep.index,
1150
1222
  modelName: clientConfig.modelName,