@librechat/agents 3.2.68 → 3.3.1

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 (159) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +117 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +4 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +243 -49
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +103 -28
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  10. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/index.cjs +11 -2
  12. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  13. package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
  14. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
  15. package/dist/cjs/llm/google/index.cjs +1 -1
  16. package/dist/cjs/llm/invoke.cjs +60 -3
  17. package/dist/cjs/llm/invoke.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/index.cjs +1 -1
  19. package/dist/cjs/main.cjs +21 -7
  20. package/dist/cjs/messages/format.cjs +136 -4
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +16 -5
  23. package/dist/cjs/messages/prune.cjs.map +1 -1
  24. package/dist/cjs/messages/recency.cjs +2 -0
  25. package/dist/cjs/messages/recency.cjs.map +1 -1
  26. package/dist/cjs/prompts/activityLabel.cjs +101 -0
  27. package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
  28. package/dist/cjs/run.cjs +177 -3
  29. package/dist/cjs/run.cjs.map +1 -1
  30. package/dist/cjs/stream.cjs +205 -49
  31. package/dist/cjs/stream.cjs.map +1 -1
  32. package/dist/cjs/summarization/node.cjs +55 -7
  33. package/dist/cjs/summarization/node.cjs.map +1 -1
  34. package/dist/cjs/tools/BashExecutor.cjs +2 -2
  35. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  36. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
  37. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  38. package/dist/cjs/tools/CodeExecutor.cjs +69 -8
  39. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  40. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
  41. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  42. package/dist/cjs/tools/ToolNode.cjs +32 -3
  43. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  45. package/dist/cjs/utils/errors.cjs +317 -52
  46. package/dist/cjs/utils/errors.cjs.map +1 -1
  47. package/dist/esm/agents/AgentContext.mjs +118 -4
  48. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  49. package/dist/esm/common/enum.mjs +4 -0
  50. package/dist/esm/common/enum.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +242 -48
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +103 -28
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
  56. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  57. package/dist/esm/llm/bedrock/index.mjs +11 -2
  58. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  59. package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
  60. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
  61. package/dist/esm/llm/google/index.mjs +1 -1
  62. package/dist/esm/llm/invoke.mjs +59 -4
  63. package/dist/esm/llm/invoke.mjs.map +1 -1
  64. package/dist/esm/llm/openai/index.mjs +1 -1
  65. package/dist/esm/main.mjs +10 -10
  66. package/dist/esm/messages/format.mjs +136 -5
  67. package/dist/esm/messages/format.mjs.map +1 -1
  68. package/dist/esm/messages/prune.mjs +14 -6
  69. package/dist/esm/messages/prune.mjs.map +1 -1
  70. package/dist/esm/messages/recency.mjs +2 -1
  71. package/dist/esm/messages/recency.mjs.map +1 -1
  72. package/dist/esm/prompts/activityLabel.mjs +100 -0
  73. package/dist/esm/prompts/activityLabel.mjs.map +1 -0
  74. package/dist/esm/run.mjs +178 -4
  75. package/dist/esm/run.mjs.map +1 -1
  76. package/dist/esm/stream.mjs +205 -49
  77. package/dist/esm/stream.mjs.map +1 -1
  78. package/dist/esm/summarization/node.mjs +55 -7
  79. package/dist/esm/summarization/node.mjs.map +1 -1
  80. package/dist/esm/tools/BashExecutor.mjs +3 -3
  81. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  82. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  83. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  84. package/dist/esm/tools/CodeExecutor.mjs +62 -9
  85. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  86. package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
  87. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +32 -3
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  91. package/dist/esm/utils/errors.mjs +317 -53
  92. package/dist/esm/utils/errors.mjs.map +1 -1
  93. package/dist/types/agents/AgentContext.d.ts +62 -3
  94. package/dist/types/common/enum.d.ts +5 -1
  95. package/dist/types/graphs/Graph.d.ts +16 -2
  96. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  97. package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
  98. package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
  99. package/dist/types/llm/invoke.d.ts +35 -6
  100. package/dist/types/messages/format.d.ts +22 -0
  101. package/dist/types/messages/prune.d.ts +10 -2
  102. package/dist/types/messages/recency.d.ts +1 -0
  103. package/dist/types/prompts/activityLabel.d.ts +31 -0
  104. package/dist/types/run.d.ts +16 -0
  105. package/dist/types/tools/CodeExecutor.d.ts +14 -1
  106. package/dist/types/types/activityLabel.d.ts +53 -0
  107. package/dist/types/types/index.d.ts +1 -0
  108. package/dist/types/types/llm.d.ts +7 -4
  109. package/dist/types/types/stream.d.ts +7 -4
  110. package/dist/types/types/summarize.d.ts +22 -0
  111. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
  112. package/dist/types/utils/errors.d.ts +65 -16
  113. package/dist/types/utils/redactSecrets.d.ts +3 -0
  114. package/package.json +7 -8
  115. package/src/agents/AgentContext.ts +188 -7
  116. package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
  117. package/src/common/enum.ts +4 -0
  118. package/src/graphs/Graph.ts +409 -58
  119. package/src/graphs/MultiAgentGraph.ts +184 -46
  120. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
  121. package/src/langfuseToolOutputTracing.ts +4 -1
  122. package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
  123. package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
  124. package/src/llm/bedrock/index.ts +25 -12
  125. package/src/llm/contextOverflowRecovery.ts +292 -0
  126. package/src/llm/invoke.ts +119 -4
  127. package/src/messages/foldToollessToolBlocks.test.ts +438 -0
  128. package/src/messages/format.ts +233 -5
  129. package/src/messages/prune.ts +24 -11
  130. package/src/messages/recency.ts +3 -1
  131. package/src/prompts/activityLabel.ts +177 -0
  132. package/src/run.ts +322 -3
  133. package/src/scripts/context-overflow-probe.ts +997 -0
  134. package/src/specs/activity-label-prompt.test.ts +128 -0
  135. package/src/specs/activity-label-trace-seed.test.ts +47 -0
  136. package/src/specs/agent-handoffs.test.ts +903 -1
  137. package/src/specs/bedrock-toolless.live.test.ts +123 -0
  138. package/src/specs/context-overflow-recovery.live.test.ts +213 -0
  139. package/src/splitStream.test.ts +882 -0
  140. package/src/stream.ts +315 -51
  141. package/src/summarization/__tests__/aggregator.test.ts +83 -0
  142. package/src/summarization/__tests__/node.test.ts +139 -0
  143. package/src/summarization/node.ts +99 -14
  144. package/src/tools/BashExecutor.ts +4 -2
  145. package/src/tools/BashProgrammaticToolCalling.ts +4 -7
  146. package/src/tools/CodeExecutor.ts +119 -8
  147. package/src/tools/ProgrammaticToolCalling.ts +29 -27
  148. package/src/tools/ToolNode.ts +50 -8
  149. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
  150. package/src/types/activityLabel.ts +55 -0
  151. package/src/types/index.ts +1 -0
  152. package/src/types/llm.ts +8 -1
  153. package/src/types/stream.ts +7 -4
  154. package/src/types/summarize.ts +22 -0
  155. package/src/utils/__tests__/errors.test.ts +270 -0
  156. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
  157. package/src/utils/__tests__/redactSecrets.test.ts +56 -0
  158. package/src/utils/errors.ts +484 -66
  159. package/src/utils/redactSecrets.ts +61 -0
package/src/stream.ts CHANGED
@@ -1813,9 +1813,22 @@ hasToolCallChunks: ${hasToolCallChunks}
1813
1813
  }
1814
1814
 
1815
1815
  export function createContentAggregator(): t.ContentAggregatorResult {
1816
+ type ToolStepContentState = {
1817
+ indices: Set<number>;
1818
+ chunkIndices: Map<number, number>;
1819
+ unclaimedIndices: Set<number>;
1820
+ unboundIndices: Set<number>;
1821
+ callIdsByIndex: Map<number, string>;
1822
+ };
1823
+
1816
1824
  const contentParts: Array<t.MessageContentComplex | undefined> = [];
1817
1825
  const stepMap = new Map<string, t.RunStep>();
1818
- const toolCallIdMap = new Map<string, string>();
1826
+ const toolCallContentIndexMap = new Map<string, number>();
1827
+ const sourceContentIndexMap = new Map<number, number>();
1828
+ const toolStepContentMap = new Map<string, ToolStepContentState>();
1829
+ let indexedContentLength = 0;
1830
+ /** Physical append cursor; event/chunk indices are correlation keys, not array offsets. */
1831
+ let nextContentIndex = 0;
1819
1832
  // Track agentId and groupId for each content index (applied to content parts)
1820
1833
  const contentMetaMap = new Map<
1821
1834
  number,
@@ -1829,6 +1842,137 @@ export function createContentAggregator(): t.ContentAggregatorResult {
1829
1842
  }
1830
1843
  return Array.isArray(content) ? content[0] : content;
1831
1844
  };
1845
+ const indexContentPart = (
1846
+ index: number,
1847
+ contentPart?: t.MessageContentComplex
1848
+ ): void => {
1849
+ if (contentPart == null) {
1850
+ return;
1851
+ }
1852
+ if (contentPart.type === ContentTypes.TOOL_CALL) {
1853
+ const toolCallId = contentPart.tool_call.id;
1854
+ if (toolCallId != null && toolCallId !== '') {
1855
+ toolCallContentIndexMap.set(toolCallId, index);
1856
+ }
1857
+ }
1858
+ const hasAgentId =
1859
+ contentPart.agentId != null && contentPart.agentId !== '';
1860
+ const hasGroupId = contentPart.groupId != null;
1861
+ if (hasAgentId || hasGroupId) {
1862
+ const existingMeta = contentMetaMap.get(index) ?? {};
1863
+ if (hasAgentId) {
1864
+ existingMeta.agentId = contentPart.agentId;
1865
+ }
1866
+ if (hasGroupId) {
1867
+ existingMeta.groupId = contentPart.groupId;
1868
+ }
1869
+ contentMetaMap.set(index, existingMeta);
1870
+ }
1871
+ };
1872
+ const syncSeededContent = (): void => {
1873
+ /** Hosts can seed pre-pause content after creating the aggregator. */
1874
+ for (
1875
+ let index = indexedContentLength;
1876
+ index < contentParts.length;
1877
+ index++
1878
+ ) {
1879
+ indexContentPart(index, contentParts[index]);
1880
+ }
1881
+ indexedContentLength = contentParts.length;
1882
+ nextContentIndex = Math.max(nextContentIndex, contentParts.length);
1883
+ };
1884
+ const getToolCallContentIndex = (
1885
+ toolCallId: string | undefined
1886
+ ): number | undefined => {
1887
+ if (toolCallId == null || toolCallId === '') {
1888
+ return undefined;
1889
+ }
1890
+ syncSeededContent();
1891
+ return toolCallContentIndexMap.get(toolCallId);
1892
+ };
1893
+ const allocateContentIndex = (minimumIndex = 0): number => {
1894
+ syncSeededContent();
1895
+ const contentIndex = Math.max(nextContentIndex, minimumIndex);
1896
+ nextContentIndex = contentIndex + 1;
1897
+ return contentIndex;
1898
+ };
1899
+ const resolveSourceContentIndex = (sourceIndex: number): number => {
1900
+ const existingIndex = sourceContentIndexMap.get(sourceIndex);
1901
+ if (existingIndex != null) {
1902
+ return existingIndex;
1903
+ }
1904
+ const contentIndex = allocateContentIndex(sourceIndex);
1905
+ sourceContentIndexMap.set(sourceIndex, contentIndex);
1906
+ return contentIndex;
1907
+ };
1908
+ const createToolStepContentState = (
1909
+ contentIndex: number
1910
+ ): ToolStepContentState => ({
1911
+ indices: new Set<number>([contentIndex]),
1912
+ chunkIndices: new Map<number, number>(),
1913
+ unclaimedIndices: new Set<number>([contentIndex]),
1914
+ unboundIndices: new Set<number>([contentIndex]),
1915
+ callIdsByIndex: new Map<number, string>(),
1916
+ });
1917
+ const registerToolContentIndex = (
1918
+ state: ToolStepContentState,
1919
+ contentIndex: number,
1920
+ toolCallId?: string
1921
+ ): void => {
1922
+ if (!state.indices.has(contentIndex)) {
1923
+ state.indices.add(contentIndex);
1924
+ state.unclaimedIndices.add(contentIndex);
1925
+ state.unboundIndices.add(contentIndex);
1926
+ }
1927
+ if (toolCallId != null && toolCallId !== '') {
1928
+ const existingToolCallId = state.callIdsByIndex.get(contentIndex);
1929
+ if (
1930
+ existingToolCallId != null &&
1931
+ existingToolCallId !== toolCallId &&
1932
+ toolCallContentIndexMap.get(existingToolCallId) === contentIndex
1933
+ ) {
1934
+ toolCallContentIndexMap.delete(existingToolCallId);
1935
+ }
1936
+ toolCallContentIndexMap.set(toolCallId, contentIndex);
1937
+ state.callIdsByIndex.set(contentIndex, toolCallId);
1938
+ state.unboundIndices.delete(contentIndex);
1939
+ }
1940
+ };
1941
+ const takeFirstIndex = (indices: Set<number>): number | undefined => {
1942
+ const contentIndex = indices.values().next().value;
1943
+ if (contentIndex != null) {
1944
+ indices.delete(contentIndex);
1945
+ }
1946
+ return contentIndex;
1947
+ };
1948
+ const setContentMeta = (index: number, runStep: t.RunStep): void => {
1949
+ const hasAgentId = runStep.agentId != null && runStep.agentId !== '';
1950
+ const hasGroupId = runStep.groupId != null;
1951
+ if (!hasAgentId && !hasGroupId) {
1952
+ return;
1953
+ }
1954
+ const existingMeta = contentMetaMap.get(index) ?? {};
1955
+ if (hasAgentId) {
1956
+ existingMeta.agentId = runStep.agentId;
1957
+ }
1958
+ if (hasGroupId) {
1959
+ existingMeta.groupId = runStep.groupId;
1960
+ }
1961
+ contentMetaMap.set(index, existingMeta);
1962
+ };
1963
+ const applyContentMetadata = (index: number): void => {
1964
+ const contentPart = contentParts[index];
1965
+ if (contentPart == null) {
1966
+ return;
1967
+ }
1968
+ const meta = contentMetaMap.get(index);
1969
+ if (meta?.agentId != null) {
1970
+ contentPart.agentId = meta.agentId;
1971
+ }
1972
+ if (meta?.groupId != null) {
1973
+ contentPart.groupId = meta.groupId;
1974
+ }
1975
+ };
1832
1976
 
1833
1977
  const updateContent = (
1834
1978
  index: number,
@@ -1964,6 +2108,15 @@ export function createContentAggregator(): t.ContentAggregatorResult {
1964
2108
  const name =
1965
2109
  getNonEmptyValue([incomingName, existingContent?.tool_call?.name]) ??
1966
2110
  '';
2111
+ const existingToolCallId = existingContent?.tool_call?.id;
2112
+ if (
2113
+ existingToolCallId != null &&
2114
+ existingToolCallId !== '' &&
2115
+ existingToolCallId !== id &&
2116
+ toolCallContentIndexMap.get(existingToolCallId) === index
2117
+ ) {
2118
+ toolCallContentIndexMap.delete(existingToolCallId);
2119
+ }
1967
2120
 
1968
2121
  const newToolCall: ToolCall & t.PartMetadata = {
1969
2122
  id,
@@ -1991,18 +2144,14 @@ export function createContentAggregator(): t.ContentAggregatorResult {
1991
2144
  type: ContentTypes.TOOL_CALL,
1992
2145
  tool_call: newToolCall,
1993
2146
  };
2147
+ indexContentPart(index, contentParts[index]);
2148
+ indexedContentLength = Math.max(indexedContentLength, index + 1);
1994
2149
  }
1995
2150
 
1996
2151
  // Apply agentId (for MultiAgentGraph) and groupId (for parallel execution) to content parts
1997
2152
  // - agentId present → MultiAgentGraph (show agent labels)
1998
2153
  // - groupId present → parallel execution (render columns)
1999
- const meta = contentMetaMap.get(index);
2000
- if (meta?.agentId != null) {
2001
- (contentParts[index] as t.MessageContentComplex).agentId = meta.agentId;
2002
- }
2003
- if (meta?.groupId != null) {
2004
- (contentParts[index] as t.MessageContentComplex).groupId = meta.groupId;
2005
- }
2154
+ applyContentMetadata(index);
2006
2155
  };
2007
2156
 
2008
2157
  const aggregateContent = ({
@@ -2046,53 +2195,78 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2046
2195
  // concatenate in updateContent. This event carries only the correct
2047
2196
  // final text from the last stage.
2048
2197
  contentParts[runStep.index] = summary;
2198
+ applyContentMetadata(runStep.index);
2049
2199
  return;
2050
2200
  }
2051
2201
 
2052
2202
  if (event === GraphEvents.ON_RUN_STEP) {
2053
- const runStep = data as t.RunStep;
2054
- stepMap.set(runStep.id, runStep);
2055
-
2056
- // Track agentId (MultiAgentGraph) and groupId (parallel execution) separately
2057
- // - agentId: present for all MultiAgentGraph runs (enables agent labels in UI)
2058
- // - groupId: present only for parallel execution (enables column rendering)
2059
- const hasAgentId = runStep.agentId != null && runStep.agentId !== '';
2060
- const hasGroupId = runStep.groupId != null;
2061
- if (hasAgentId || hasGroupId) {
2062
- const existingMeta = contentMetaMap.get(runStep.index) ?? {};
2063
- if (hasAgentId) {
2064
- existingMeta.agentId = runStep.agentId;
2065
- }
2066
- if (hasGroupId) {
2067
- existingMeta.groupId = runStep.groupId;
2203
+ const incomingRunStep = data as t.RunStep;
2204
+ const toolCalls =
2205
+ incomingRunStep.stepDetails.type === StepTypes.TOOL_CALLS
2206
+ ? (incomingRunStep.stepDetails.tool_calls as ToolCall[] | undefined)
2207
+ : undefined;
2208
+ syncSeededContent();
2209
+ let runStepIndex: number;
2210
+ const toolCallIndices: number[] = [];
2211
+ if (toolCalls && toolCalls.length > 0) {
2212
+ for (let index = 0; index < toolCalls.length; index++) {
2213
+ const toolCallId = toolCalls[index].id;
2214
+ let contentIndex = getToolCallContentIndex(toolCallId);
2215
+ if (contentIndex == null) {
2216
+ contentIndex =
2217
+ index === 0
2218
+ ? resolveSourceContentIndex(incomingRunStep.index)
2219
+ : allocateContentIndex();
2220
+ } else if (index === 0) {
2221
+ sourceContentIndexMap.set(incomingRunStep.index, contentIndex);
2222
+ }
2223
+ toolCallIndices.push(contentIndex);
2068
2224
  }
2069
- contentMetaMap.set(runStep.index, existingMeta);
2225
+ runStepIndex = toolCallIndices[0];
2226
+ } else {
2227
+ runStepIndex = resolveSourceContentIndex(incomingRunStep.index);
2070
2228
  }
2229
+ const runStep =
2230
+ runStepIndex !== incomingRunStep.index
2231
+ ? { ...incomingRunStep, index: runStepIndex }
2232
+ : incomingRunStep;
2233
+ stepMap.set(runStep.id, runStep);
2234
+
2235
+ setContentMeta(runStep.index, runStep);
2071
2236
 
2072
2237
  if (runStep.summary != null) {
2073
2238
  updateContent(runStep.index, runStep.summary);
2074
2239
  }
2075
2240
 
2076
- if (
2077
- runStep.stepDetails.type === StepTypes.TOOL_CALLS &&
2078
- runStep.stepDetails.tool_calls
2079
- ) {
2080
- (runStep.stepDetails.tool_calls as ToolCall[]).forEach((toolCall) => {
2081
- const toolCallId = toolCall.id ?? '';
2082
- if ('id' in toolCall && toolCallId) {
2083
- toolCallIdMap.set(runStep.id, toolCallId);
2084
- }
2085
- const contentPart: t.MessageContentComplex = {
2086
- type: ContentTypes.TOOL_CALL,
2087
- tool_call: {
2088
- args: toolCall.args,
2089
- name: toolCall.name,
2090
- id: toolCallId,
2091
- },
2092
- };
2241
+ if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) {
2242
+ const toolStepContent =
2243
+ toolStepContentMap.get(runStep.id) ??
2244
+ createToolStepContentState(runStep.index);
2245
+ registerToolContentIndex(toolStepContent, runStep.index);
2246
+ (runStep.stepDetails.tool_calls as ToolCall[] | undefined)?.forEach(
2247
+ (toolCall, toolCallIndex) => {
2248
+ const contentIndex =
2249
+ toolCallIndices[toolCallIndex] ?? runStep.index;
2250
+ const toolCallId = toolCall.id ?? '';
2251
+ registerToolContentIndex(
2252
+ toolStepContent,
2253
+ contentIndex,
2254
+ toolCallId
2255
+ );
2256
+ const contentPart: t.MessageContentComplex = {
2257
+ type: ContentTypes.TOOL_CALL,
2258
+ tool_call: {
2259
+ args: toolCall.args,
2260
+ name: toolCall.name,
2261
+ id: toolCallId,
2262
+ },
2263
+ };
2093
2264
 
2094
- updateContent(runStep.index, contentPart);
2095
- });
2265
+ setContentMeta(contentIndex, runStep);
2266
+ updateContent(contentIndex, contentPart);
2267
+ }
2268
+ );
2269
+ toolStepContentMap.set(runStep.id, toolStepContent);
2096
2270
  }
2097
2271
  } else if (event === GraphEvents.ON_MESSAGE_DELTA) {
2098
2272
  const messageDelta = data as t.MessageDeltaEvent;
@@ -2114,7 +2288,16 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2114
2288
  if (!contentPart) {
2115
2289
  return;
2116
2290
  }
2117
- updateContent(contentPart.agent_update.index, contentPart);
2291
+ const contentIndex = resolveSourceContentIndex(
2292
+ contentPart.agent_update.index
2293
+ );
2294
+ updateContent(contentIndex, {
2295
+ ...contentPart,
2296
+ agent_update: {
2297
+ ...contentPart.agent_update,
2298
+ index: contentIndex,
2299
+ },
2300
+ });
2118
2301
  } else if (event === GraphEvents.ON_REASONING_DELTA) {
2119
2302
  const reasoningDelta = data as t.ReasoningDeltaEvent;
2120
2303
  const runStep = stepMap.get(reasoningDelta.id);
@@ -2139,8 +2322,62 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2139
2322
  runStepDelta.delta.type === StepTypes.TOOL_CALLS &&
2140
2323
  runStepDelta.delta.tool_calls
2141
2324
  ) {
2325
+ const toolStepContent =
2326
+ toolStepContentMap.get(runStepDelta.id) ??
2327
+ createToolStepContentState(runStep.index);
2142
2328
  runStepDelta.delta.tool_calls.forEach((toolCallDelta) => {
2143
- const toolCallId = toolCallIdMap.get(runStepDelta.id);
2329
+ const chunkIndex =
2330
+ typeof toolCallDelta.index === 'number'
2331
+ ? toolCallDelta.index
2332
+ : undefined;
2333
+ const explicitToolCallId =
2334
+ toolCallDelta.id != null && toolCallDelta.id !== ''
2335
+ ? toolCallDelta.id
2336
+ : undefined;
2337
+ let contentIndex = getToolCallContentIndex(explicitToolCallId);
2338
+ if (contentIndex == null && chunkIndex != null) {
2339
+ contentIndex = toolStepContent.chunkIndices.get(chunkIndex);
2340
+ }
2341
+ const soleContentIndex =
2342
+ toolStepContent.indices.size === 1
2343
+ ? toolStepContent.indices.values().next().value
2344
+ : undefined;
2345
+ if (
2346
+ contentIndex == null &&
2347
+ explicitToolCallId == null &&
2348
+ soleContentIndex != null &&
2349
+ (chunkIndex == null ||
2350
+ toolStepContent.callIdsByIndex.has(soleContentIndex) ||
2351
+ toolStepContent.unclaimedIndices.has(soleContentIndex))
2352
+ ) {
2353
+ contentIndex = soleContentIndex;
2354
+ }
2355
+ if (contentIndex == null) {
2356
+ if (chunkIndex == null && explicitToolCallId == null) {
2357
+ console.warn(
2358
+ 'No tool call id or chunk index found for run step delta event'
2359
+ );
2360
+ return;
2361
+ }
2362
+ contentIndex =
2363
+ explicitToolCallId == null
2364
+ ? takeFirstIndex(toolStepContent.unclaimedIndices)
2365
+ : takeFirstIndex(toolStepContent.unboundIndices);
2366
+ contentIndex ??= allocateContentIndex();
2367
+ }
2368
+ registerToolContentIndex(
2369
+ toolStepContent,
2370
+ contentIndex,
2371
+ explicitToolCallId
2372
+ );
2373
+ if (chunkIndex != null) {
2374
+ toolStepContent.chunkIndices.set(chunkIndex, contentIndex);
2375
+ toolStepContent.unclaimedIndices.delete(contentIndex);
2376
+ toolStepContent.unboundIndices.delete(contentIndex);
2377
+ }
2378
+ const toolCallId =
2379
+ explicitToolCallId ??
2380
+ toolStepContent.callIdsByIndex.get(contentIndex);
2144
2381
 
2145
2382
  const contentPart: t.MessageContentComplex = {
2146
2383
  type: ContentTypes.TOOL_CALL,
@@ -2153,8 +2390,10 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2153
2390
  },
2154
2391
  };
2155
2392
 
2156
- updateContent(runStep.index, contentPart);
2393
+ setContentMeta(contentIndex, runStep);
2394
+ updateContent(contentIndex, contentPart);
2157
2395
  });
2396
+ toolStepContentMap.set(runStepDelta.id, toolStepContent);
2158
2397
  }
2159
2398
  } else if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
2160
2399
  const { result } = data as unknown as {
@@ -2166,19 +2405,44 @@ export function createContentAggregator(): t.ContentAggregatorResult {
2166
2405
  const { id: stepId } = result;
2167
2406
 
2168
2407
  const runStep = stepMap.get(stepId);
2169
- if (!runStep) {
2170
- console.warn('No run step or runId found for completed step event');
2171
- return;
2172
- }
2173
2408
 
2174
2409
  if (result.type === ContentTypes.SUMMARY && 'summary' in result) {
2410
+ if (!runStep) {
2411
+ console.warn('No run step or runId found for completed step event');
2412
+ return;
2413
+ }
2175
2414
  contentParts[runStep.index] = result.summary as t.MessageContentComplex;
2415
+ applyContentMetadata(runStep.index);
2176
2416
  } else if ('tool_call' in result) {
2417
+ let contentIndex = getToolCallContentIndex(result.tool_call.id);
2418
+ if (contentIndex == null && runStep != null) {
2419
+ const toolStepContent = toolStepContentMap.get(runStep.id);
2420
+ if (toolStepContent?.indices.size === 1) {
2421
+ contentIndex = toolStepContent.indices.values().next().value;
2422
+ } else if (toolStepContent == null) {
2423
+ const declaredToolCalls =
2424
+ runStep.stepDetails.type === StepTypes.TOOL_CALLS
2425
+ ? runStep.stepDetails.tool_calls
2426
+ : undefined;
2427
+ if ((declaredToolCalls?.length ?? 0) <= 1) {
2428
+ contentIndex = runStep.index;
2429
+ }
2430
+ }
2431
+ }
2432
+ if (contentIndex == null) {
2433
+ console.warn(
2434
+ 'No run step or tool call found for completed step event'
2435
+ );
2436
+ return;
2437
+ }
2438
+ if (runStep != null) {
2439
+ setContentMeta(contentIndex, runStep);
2440
+ }
2177
2441
  const contentPart: t.MessageContentComplex = {
2178
2442
  type: ContentTypes.TOOL_CALL,
2179
2443
  tool_call: (result as t.ToolEndEvent).tool_call,
2180
2444
  };
2181
- updateContent(runStep.index, contentPart, true);
2445
+ updateContent(contentIndex, contentPart, true);
2182
2446
  }
2183
2447
  }
2184
2448
  };
@@ -125,6 +125,89 @@ describe('createContentAggregator – SUMMARY accumulation', () => {
125
125
  expect(part.model).toBe('claude-sonnet-4-5');
126
126
  });
127
127
 
128
+ it('preserves agent and parallel-group metadata on summary completion', () => {
129
+ const { aggregateContent, contentParts } = createContentAggregator();
130
+
131
+ const runStep: t.RunStep = {
132
+ stepIndex: 0,
133
+ id: 'step_sum_3',
134
+ type: StepTypes.MESSAGE_CREATION,
135
+ index: 0,
136
+ agentId: 'researcher',
137
+ groupId: 7,
138
+ stepDetails: {
139
+ type: StepTypes.MESSAGE_CREATION,
140
+ message_creation: { message_id: 'step_sum_3' },
141
+ },
142
+ summary: {
143
+ type: ContentTypes.SUMMARY,
144
+ content: [],
145
+ tokenCount: 0,
146
+ provider: 'openai',
147
+ },
148
+ usage: null,
149
+ };
150
+
151
+ aggregateContent({
152
+ event: GraphEvents.ON_RUN_STEP,
153
+ data: runStep,
154
+ });
155
+
156
+ aggregateContent({
157
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
158
+ data: {
159
+ result: {
160
+ id: 'step_sum_3',
161
+ index: 0,
162
+ type: ContentTypes.SUMMARY,
163
+ summary: {
164
+ type: ContentTypes.SUMMARY,
165
+ content: [{ type: ContentTypes.TEXT, text: 'Step summary' }],
166
+ tokenCount: 4,
167
+ provider: 'openai',
168
+ },
169
+ },
170
+ } as unknown as { result: t.ToolEndEvent },
171
+ });
172
+
173
+ expect(contentParts[0]).toEqual(
174
+ expect.objectContaining({
175
+ type: ContentTypes.SUMMARY,
176
+ content: [{ type: ContentTypes.TEXT, text: 'Step summary' }],
177
+ agentId: 'researcher',
178
+ groupId: 7,
179
+ })
180
+ );
181
+
182
+ aggregateContent({
183
+ event: GraphEvents.ON_SUMMARIZE_COMPLETE,
184
+ data: {
185
+ id: 'step_sum_3',
186
+ agentId: 'researcher',
187
+ summary: {
188
+ type: ContentTypes.SUMMARY,
189
+ content: [{ type: ContentTypes.TEXT, text: 'Final summary' }],
190
+ tokenCount: 5,
191
+ provider: 'openai',
192
+ boundary: {
193
+ messageId: 'step_sum_3',
194
+ contentIndex: 0,
195
+ },
196
+ },
197
+ } as t.SummarizeCompleteEvent,
198
+ });
199
+
200
+ expect(contentParts[0]).toEqual(
201
+ expect.objectContaining({
202
+ type: ContentTypes.SUMMARY,
203
+ content: [{ type: ContentTypes.TEXT, text: 'Final summary' }],
204
+ tokenCount: 5,
205
+ agentId: 'researcher',
206
+ groupId: 7,
207
+ })
208
+ );
209
+ });
210
+
128
211
  it('handles delta when no run step exists', () => {
129
212
  const { aggregateContent, contentParts } = createContentAggregator();
130
213
 
@@ -1303,3 +1303,142 @@ describe('emoji-heavy content does not break summarization', () => {
1303
1303
  expect(result.messages!.length).toBeGreaterThan(0);
1304
1304
  });
1305
1305
  });
1306
+
1307
+ describe('createSummarizeNode — overflow recovery', () => {
1308
+ /**
1309
+ * The first recovery compacts deterministically: re-pruning under the
1310
+ * corrected budget drives the pruner's tool-output compression. Spending a
1311
+ * summarization call there would cost money and risk replacing message
1312
+ * content that compression alone can shrink.
1313
+ */
1314
+ it('skips the model call when summarization is not yet escalated', async () => {
1315
+ const modelFactory = jest.spyOn(providers, 'getChatModelClass');
1316
+ const agentContext = createAgentContext();
1317
+ const graph = mockGraph(() => {});
1318
+ const node = createSummarizeNode({ agentContext, graph, generateStepId });
1319
+
1320
+ const result = await node(
1321
+ {
1322
+ messages: [new HumanMessage('Hello'), new HumanMessage('World')],
1323
+ summarizationRequest: {
1324
+ remainingContextTokens: 0,
1325
+ agentId: 'agent_0',
1326
+ reason: 'overflow',
1327
+ allowSummarization: false,
1328
+ },
1329
+ },
1330
+ {} as RunnableConfig
1331
+ );
1332
+
1333
+ expect(modelFactory).not.toHaveBeenCalled();
1334
+ expect(result.summarizationRequest).toBeUndefined();
1335
+ /** State untouched, so the agent node re-prunes and retries. */
1336
+ expect(result.messages).toBeUndefined();
1337
+ });
1338
+
1339
+ it('runs the model call once the recovery escalates', async () => {
1340
+ jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
1341
+ class {
1342
+ constructor() {
1343
+ return mockInvokeModel('Escalated summary');
1344
+ }
1345
+ } as never
1346
+ );
1347
+ const agentContext = createAgentContext();
1348
+ const graph = mockGraph(() => {});
1349
+ const node = createSummarizeNode({ agentContext, graph, generateStepId });
1350
+
1351
+ await node(
1352
+ {
1353
+ messages: [new HumanMessage('Hello'), new HumanMessage('World')],
1354
+ summarizationRequest: {
1355
+ remainingContextTokens: 0,
1356
+ agentId: 'agent_0',
1357
+ reason: 'overflow',
1358
+ allowSummarization: true,
1359
+ },
1360
+ },
1361
+ {} as RunnableConfig
1362
+ );
1363
+
1364
+ expect(agentContext.getSummaryText()).toContain('Escalated summary');
1365
+ });
1366
+
1367
+ /**
1368
+ * The metadata stub describes the history instead of summarizing it, so
1369
+ * committing it removes the head and keeps nothing of what it said. That is
1370
+ * never an acceptable way to paper over an overflow.
1371
+ */
1372
+ it('keeps history when the escalated summarizer falls back to a stub', async () => {
1373
+ jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
1374
+ class {
1375
+ constructor() {
1376
+ return {
1377
+ invoke: (): never => {
1378
+ throw new Error(
1379
+ '400 prompt is too long: 900 tokens > 500 maximum'
1380
+ );
1381
+ },
1382
+ stream: (): never => {
1383
+ throw new Error(
1384
+ '400 prompt is too long: 900 tokens > 500 maximum'
1385
+ );
1386
+ },
1387
+ };
1388
+ }
1389
+ } as never
1390
+ );
1391
+ const agentContext = createAgentContext();
1392
+ const graph = mockGraph(() => {});
1393
+ const node = createSummarizeNode({ agentContext, graph, generateStepId });
1394
+
1395
+ const result = await node(
1396
+ {
1397
+ messages: [new HumanMessage('Hello'), new HumanMessage('World')],
1398
+ summarizationRequest: {
1399
+ remainingContextTokens: 0,
1400
+ agentId: 'agent_0',
1401
+ reason: 'overflow',
1402
+ allowSummarization: true,
1403
+ },
1404
+ },
1405
+ {} as RunnableConfig
1406
+ );
1407
+
1408
+ expect(agentContext.hasSummary()).toBe(false);
1409
+ expect(result.messages).toBeUndefined();
1410
+ });
1411
+
1412
+ it('still commits a stub for a configured trigger, preserving prior behavior', async () => {
1413
+ jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
1414
+ class {
1415
+ constructor() {
1416
+ return {
1417
+ invoke: (): never => {
1418
+ throw new Error('upstream unavailable');
1419
+ },
1420
+ stream: (): never => {
1421
+ throw new Error('upstream unavailable');
1422
+ },
1423
+ };
1424
+ }
1425
+ } as never
1426
+ );
1427
+ const agentContext = createAgentContext();
1428
+ const graph = mockGraph(() => {});
1429
+ const node = createSummarizeNode({ agentContext, graph, generateStepId });
1430
+
1431
+ await node(
1432
+ {
1433
+ messages: [new HumanMessage('Hello'), new HumanMessage('World')],
1434
+ summarizationRequest: {
1435
+ remainingContextTokens: 0,
1436
+ agentId: 'agent_0',
1437
+ },
1438
+ },
1439
+ {} as RunnableConfig
1440
+ );
1441
+
1442
+ expect(agentContext.hasSummary()).toBe(true);
1443
+ });
1444
+ });