@librechat/agents 3.3.0 → 3.3.2
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.
- package/dist/cjs/agents/AgentContext.cjs +116 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +229 -48
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +102 -27
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +11 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
- package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
- package/dist/cjs/llm/google/index.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +60 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/main.cjs +20 -7
- package/dist/cjs/messages/prune.cjs +16 -5
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +2 -0
- package/dist/cjs/messages/recency.cjs.map +1 -1
- package/dist/cjs/run.cjs +15 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +205 -49
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -7
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +2 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +69 -8
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +32 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/utils/errors.cjs +317 -52
- package/dist/cjs/utils/errors.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +117 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +227 -46
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +102 -27
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +11 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
- package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
- package/dist/esm/llm/google/index.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +59 -4
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/main.mjs +9 -9
- package/dist/esm/messages/prune.mjs +14 -6
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +2 -1
- package/dist/esm/messages/recency.mjs.map +1 -1
- package/dist/esm/run.mjs +15 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +205 -49
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -7
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -3
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +62 -9
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +32 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/utils/errors.mjs +317 -53
- package/dist/esm/utils/errors.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +62 -3
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +16 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
- package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
- package/dist/types/llm/invoke.d.ts +35 -6
- package/dist/types/messages/prune.d.ts +10 -2
- package/dist/types/messages/recency.d.ts +1 -0
- package/dist/types/run.d.ts +2 -0
- package/dist/types/tools/CodeExecutor.d.ts +14 -1
- package/dist/types/types/llm.d.ts +7 -4
- package/dist/types/types/stream.d.ts +5 -4
- package/dist/types/types/summarize.d.ts +22 -0
- package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
- package/dist/types/utils/errors.d.ts +65 -16
- package/dist/types/utils/redactSecrets.d.ts +3 -0
- package/package.json +10 -11
- package/src/agents/AgentContext.ts +188 -7
- package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
- package/src/common/enum.ts +2 -0
- package/src/graphs/Graph.ts +389 -58
- package/src/graphs/MultiAgentGraph.ts +184 -46
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
- package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
- package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
- package/src/llm/anthropic/llm.spec.ts +8 -0
- package/src/llm/bedrock/index.ts +25 -12
- package/src/llm/contextOverflowRecovery.ts +292 -0
- package/src/llm/invoke.ts +119 -4
- package/src/messages/prune.ts +24 -11
- package/src/messages/recency.ts +3 -1
- package/src/run.ts +24 -1
- package/src/scripts/context-overflow-probe.ts +997 -0
- package/src/specs/agent-handoffs.test.ts +903 -1
- package/src/specs/context-overflow-recovery.live.test.ts +213 -0
- package/src/splitStream.test.ts +882 -0
- package/src/stream.ts +315 -51
- package/src/summarization/__tests__/aggregator.test.ts +83 -0
- package/src/summarization/__tests__/node.test.ts +139 -0
- package/src/summarization/node.ts +99 -14
- package/src/tools/BashExecutor.ts +4 -2
- package/src/tools/BashProgrammaticToolCalling.ts +4 -7
- package/src/tools/CodeExecutor.ts +119 -8
- package/src/tools/ProgrammaticToolCalling.ts +29 -27
- package/src/tools/ToolNode.ts +50 -8
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
- package/src/types/llm.ts +8 -1
- package/src/types/stream.ts +5 -4
- package/src/types/summarize.ts +22 -0
- package/src/utils/__tests__/errors.test.ts +270 -0
- package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
- package/src/utils/__tests__/redactSecrets.test.ts +56 -0
- package/src/utils/errors.ts +484 -66
- 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
|
|
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
|
-
|
|
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
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
const
|
|
2061
|
-
if (
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
(runStep.
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
+
});
|