@librechat/agents 3.3.11 → 3.3.12
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/graphs/Graph.cjs +246 -25
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +13 -2
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +146 -36
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +2 -2
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/streamLimits.cjs +723 -0
- package/dist/cjs/llm/streamLimits.cjs.map +1 -0
- package/dist/cjs/main.cjs +8 -0
- package/dist/cjs/run.cjs +6 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +4 -1
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/stream.cjs +103 -12
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +157 -37
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +3 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +4 -3
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -3
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +76 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +3 -2
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +7 -1
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-search.cjs +3 -1
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +7 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +7 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +3 -1
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +26 -8
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +30 -10
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +7 -1
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs +7 -1
- package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
- package/dist/cjs/tools/search/tavily-search.cjs +3 -1
- package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +16 -2
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +54 -3
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -0
- package/dist/cjs/utils/misc.cjs +12 -0
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/proxy.cjs +63 -0
- package/dist/cjs/utils/proxy.cjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +245 -24
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +13 -2
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +146 -36
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +2 -2
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/streamLimits.mjs +704 -0
- package/dist/esm/llm/streamLimits.mjs.map +1 -0
- package/dist/esm/main.mjs +4 -2
- package/dist/esm/run.mjs +6 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +4 -1
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/stream.mjs +103 -12
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +157 -37
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -2
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +4 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -3
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +76 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +3 -2
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +7 -1
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/crw-search.mjs +3 -1
- package/dist/esm/tools/search/crw-search.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +7 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +7 -1
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +3 -1
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +26 -8
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +30 -10
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +7 -1
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-scraper.mjs +7 -1
- package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
- package/dist/esm/tools/search/tavily-search.mjs +3 -1
- package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +16 -2
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +54 -3
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -0
- package/dist/esm/utils/misc.mjs +12 -1
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/proxy.mjs +62 -0
- package/dist/esm/utils/proxy.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +53 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/llm/invoke.d.ts +25 -4
- package/dist/types/llm/openai/index.d.ts +3 -0
- package/dist/types/llm/streamLimits.d.ts +314 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/summarization/node.d.ts +27 -2
- package/dist/types/tools/BashExecutor.d.ts +2 -2
- package/dist/types/tools/CodeExecutor.d.ts +3 -3
- package/dist/types/tools/ToolNode.d.ts +11 -1
- package/dist/types/tools/search/crw-scraper.d.ts +2 -0
- package/dist/types/tools/search/firecrawl.d.ts +2 -0
- package/dist/types/tools/search/keenable-scraper.d.ts +2 -0
- package/dist/types/tools/search/rerankers.d.ts +9 -5
- package/dist/types/tools/search/serper-scraper.d.ts +2 -0
- package/dist/types/tools/search/tavily-scraper.d.ts +2 -0
- package/dist/types/tools/search/types.d.ts +28 -33
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +44 -0
- package/dist/types/types/graph.d.ts +7 -1
- package/dist/types/types/run.d.ts +46 -1
- package/dist/types/types/tools.d.ts +21 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/misc.d.ts +7 -0
- package/dist/types/utils/proxy.d.ts +31 -0
- package/package.json +2 -1
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +158 -0
- package/src/__tests__/stream.eagerEventExecution.test.ts +1 -0
- package/src/__tests__/stream.streamLimits.test.ts +1982 -0
- package/src/graphs/Graph.ts +302 -27
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +234 -0
- package/src/index.ts +11 -0
- package/src/llm/bedrock/index.ts +22 -2
- package/src/llm/invoke.streamLimits.test.ts +142 -0
- package/src/llm/invoke.test.ts +89 -1
- package/src/llm/invoke.ts +197 -20
- package/src/llm/openai/cacheWriteTokens.test.ts +112 -0
- package/src/llm/openai/index.ts +14 -6
- package/src/llm/streamLimits.test.ts +450 -0
- package/src/llm/streamLimits.ts +1158 -0
- package/src/run.ts +4 -0
- package/src/session/AgentSession.ts +5 -0
- package/src/stream.ts +141 -6
- package/src/summarization/__tests__/node.test.ts +269 -0
- package/src/summarization/chunkHandler.test.ts +196 -0
- package/src/summarization/node.ts +203 -6
- package/src/tools/BashExecutor.ts +4 -3
- package/src/tools/CodeExecutor.ts +5 -4
- package/src/tools/ProgrammaticToolCalling.ts +7 -5
- package/src/tools/ToolNode.ts +109 -6
- package/src/tools/ToolSearch.ts +4 -3
- package/src/tools/__tests__/BashExecutor.test.ts +2 -2
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +2 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +126 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +389 -0
- package/src/tools/__tests__/ToolNode.streamLimits.test.ts +69 -0
- package/src/tools/search/crw-scraper.ts +6 -0
- package/src/tools/search/crw-search.ts +6 -1
- package/src/tools/search/firecrawl.ts +6 -0
- package/src/tools/search/http-agent.test.ts +133 -0
- package/src/tools/search/keenable-scraper.ts +6 -0
- package/src/tools/search/keenable-search.ts +6 -1
- package/src/tools/search/rerankers.ts +36 -10
- package/src/tools/search/search.ts +29 -11
- package/src/tools/search/serper-scraper.ts +6 -0
- package/src/tools/search/tavily-scraper.ts +6 -0
- package/src/tools/search/tavily-search.ts +2 -0
- package/src/tools/search/tool.ts +16 -0
- package/src/tools/search/types.ts +31 -33
- package/src/tools/subagent/SubagentExecutor.ts +96 -3
- package/src/types/graph.ts +7 -0
- package/src/types/run.ts +49 -1
- package/src/types/tools.ts +21 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +19 -0
- package/src/utils/proxy.test.ts +176 -0
- package/src/utils/proxy.ts +93 -0
package/src/run.ts
CHANGED
|
@@ -148,6 +148,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
148
148
|
private toolExecution?: t.ToolExecutionConfig;
|
|
149
149
|
private subagentUsageSink?: t.SubagentUsageSink;
|
|
150
150
|
private preemption?: t.StreamPreemption;
|
|
151
|
+
private streamLimits?: t.StreamLimits;
|
|
151
152
|
private indexTokenCountMap?: Record<string, number>;
|
|
152
153
|
calibrationRatio: number = 1;
|
|
153
154
|
graphRunnable?: t.CompiledStateWorkflow;
|
|
@@ -210,6 +211,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
210
211
|
this.toolExecution = config.toolExecution;
|
|
211
212
|
this.subagentUsageSink = config.subagentUsageSink;
|
|
212
213
|
this.preemption = config.preemption;
|
|
214
|
+
this.streamLimits = config.streamLimits;
|
|
213
215
|
|
|
214
216
|
if (!config.graphConfig) {
|
|
215
217
|
throw new Error('Graph config not provided');
|
|
@@ -285,6 +287,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
285
287
|
calibrationRatio: this.calibrationRatio,
|
|
286
288
|
subagentUsageSink: this.subagentUsageSink,
|
|
287
289
|
preemption: this.preemption,
|
|
290
|
+
streamLimits: this.streamLimits,
|
|
288
291
|
});
|
|
289
292
|
/** Propagate compile options from graph config */
|
|
290
293
|
standardGraph.compileOptions = this.applyHITLCheckpointerFallback(
|
|
@@ -317,6 +320,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
317
320
|
calibrationRatio: this.calibrationRatio,
|
|
318
321
|
subagentUsageSink: this.subagentUsageSink,
|
|
319
322
|
preemption: this.preemption,
|
|
323
|
+
streamLimits: this.streamLimits,
|
|
320
324
|
});
|
|
321
325
|
|
|
322
326
|
multiAgentGraph.compileOptions =
|
|
@@ -25,6 +25,7 @@ import type { HookRegistry } from '@/hooks';
|
|
|
25
25
|
import type * as t from '@/types';
|
|
26
26
|
import { deserializeMessage } from './messageSerialization';
|
|
27
27
|
import { createSummarizeNode } from '@/summarization/node';
|
|
28
|
+
import { resolveStreamLimits } from '@/llm/streamLimits';
|
|
28
29
|
import { JsonlSessionStore } from './JsonlSessionStore';
|
|
29
30
|
import { AgentContext } from '@/agents/AgentContext';
|
|
30
31
|
import { ContentTypes, GraphEvents } from '@/common';
|
|
@@ -610,6 +611,8 @@ function createManualCompactGraph(params: {
|
|
|
610
611
|
runId: string;
|
|
611
612
|
customHandlers?: Record<string, t.EventHandler>;
|
|
612
613
|
hooks?: HookRegistry;
|
|
614
|
+
/** Session run-level stream limits, so manual compaction honors them. */
|
|
615
|
+
streamLimits?: t.StreamLimits;
|
|
613
616
|
}): {
|
|
614
617
|
graph: Parameters<typeof createSummarizeNode>[0]['graph'];
|
|
615
618
|
completedSummary?: t.SummaryContentBlock;
|
|
@@ -626,6 +629,7 @@ function createManualCompactGraph(params: {
|
|
|
626
629
|
runId: params.runId,
|
|
627
630
|
isMultiAgent: false,
|
|
628
631
|
hookRegistry: params.hooks,
|
|
632
|
+
streamLimits: resolveStreamLimits(params.streamLimits),
|
|
629
633
|
dispatchRunStep: async (runStep): Promise<void> => {
|
|
630
634
|
contentData.push(runStep);
|
|
631
635
|
contentIndexMap.set(runStep.id, runStep.index);
|
|
@@ -1251,6 +1255,7 @@ export class AgentSession {
|
|
|
1251
1255
|
runId: compactRunId,
|
|
1252
1256
|
customHandlers: this.runConfig.customHandlers,
|
|
1253
1257
|
hooks: this.runConfig.hooks,
|
|
1258
|
+
streamLimits: this.runConfig.streamLimits,
|
|
1254
1259
|
});
|
|
1255
1260
|
const summarizeNode = createSummarizeNode({
|
|
1256
1261
|
agentContext,
|
package/src/stream.ts
CHANGED
|
@@ -39,9 +39,21 @@ import {
|
|
|
39
39
|
calculateMaxToolResultChars,
|
|
40
40
|
truncateToolResultContent,
|
|
41
41
|
} from '@/utils/truncation';
|
|
42
|
+
import type { RunBreakerScope } from '@/llm/streamLimits';
|
|
43
|
+
import {
|
|
44
|
+
claimStreamLimitCharge,
|
|
45
|
+
combineCompleteToolCalls,
|
|
46
|
+
enforceCompleteToolCallArgLimit,
|
|
47
|
+
enforceStreamedToolCallArgLimit,
|
|
48
|
+
enforceStreamDeltaEventLimit,
|
|
49
|
+
requiresStreamLimitAccounting,
|
|
50
|
+
StreamLimitExceededError,
|
|
51
|
+
STREAM_LIMIT_EPOCH_KEY,
|
|
52
|
+
} from '@/llm/streamLimits';
|
|
42
53
|
import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
|
|
43
54
|
import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
|
|
44
55
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
56
|
+
import { composeAbortSignals } from '@/utils/misc';
|
|
45
57
|
import { isGoogleLike } from '@/utils/llm';
|
|
46
58
|
import { getMessageId } from '@/messages';
|
|
47
59
|
|
|
@@ -166,7 +178,10 @@ function isEagerExecutionExcludedTool(
|
|
|
166
178
|
// the final request ("changed after eager execution started"), stop
|
|
167
179
|
// prestarting it so the model's retry executes normally instead of
|
|
168
180
|
// re-diverging in a loop (LibreChat#14371).
|
|
169
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
(graph.eagerEventToolSuppressions as Set<string> | undefined)?.has(name) ===
|
|
183
|
+
true
|
|
184
|
+
) {
|
|
170
185
|
return true;
|
|
171
186
|
}
|
|
172
187
|
// A code-session participant writes to the shared sandbox, so it is
|
|
@@ -778,6 +793,10 @@ function startEagerToolExecutions(args: {
|
|
|
778
793
|
| Record<string, unknown>
|
|
779
794
|
| undefined,
|
|
780
795
|
metadata,
|
|
796
|
+
signal: composeAbortSignals(
|
|
797
|
+
graph.config?.signal,
|
|
798
|
+
graph.breakerAbort.signal
|
|
799
|
+
),
|
|
781
800
|
resolve: (results): void => {
|
|
782
801
|
resultSettled = true;
|
|
783
802
|
settledResults = results;
|
|
@@ -1069,9 +1088,8 @@ function recordEagerToolCallChunks(args: {
|
|
|
1069
1088
|
const sealCoversChunk =
|
|
1070
1089
|
seal != null &&
|
|
1071
1090
|
(seal.kind === 'all' ||
|
|
1072
|
-
(seal.
|
|
1073
|
-
|
|
1074
|
-
(seal.index != null && seal.index === index))));
|
|
1091
|
+
(seal.id != null && seal.id === id) ||
|
|
1092
|
+
(seal.index != null && seal.index === index));
|
|
1075
1093
|
const next = {
|
|
1076
1094
|
id,
|
|
1077
1095
|
name,
|
|
@@ -1512,10 +1530,119 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1512
1530
|
return;
|
|
1513
1531
|
}
|
|
1514
1532
|
|
|
1515
|
-
const agentContext = graph.getAgentContext(metadata);
|
|
1516
|
-
|
|
1517
1533
|
const chunk = data.chunk as Partial<AIMessageChunk>;
|
|
1518
1534
|
|
|
1535
|
+
/** Attempts stamp their breaker epoch into event metadata; a mismatch
|
|
1536
|
+
* marks a straggling chunk from a failed run that outlived
|
|
1537
|
+
* `resetValues()`. Dropped OUTRIGHT: content handling and the eager
|
|
1538
|
+
* paths below compose the LIVE controller, so acting on a dead run's
|
|
1539
|
+
* chunk could dispatch host tools into the run now using it. Events
|
|
1540
|
+
* without a stamp (direct handler callers, partial stubs) keep the
|
|
1541
|
+
* live-controller behavior. */
|
|
1542
|
+
const eventEpoch = metadata?.[STREAM_LIMIT_EPOCH_KEY];
|
|
1543
|
+
/** Runtime-honest widening: partial handler stubs carry neither an
|
|
1544
|
+
* epoch nor a run scope despite the field types. */
|
|
1545
|
+
const liveEpoch = graph.breakerEpoch as number | undefined;
|
|
1546
|
+
if (eventEpoch != null && liveEpoch != null && eventEpoch !== liveEpoch) {
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
const eventBreaker =
|
|
1550
|
+
graph.breakerAbort instanceof AbortController
|
|
1551
|
+
? graph.breakerAbort
|
|
1552
|
+
: undefined;
|
|
1553
|
+
/** Immutable scope captured at handler entry. A reset while this
|
|
1554
|
+
* handler is suspended in an await replaces the object, so ONE
|
|
1555
|
+
* reference comparison proves the event still belongs to the live run
|
|
1556
|
+
* before anything composes `graph.breakerAbort` or `graph.config`. */
|
|
1557
|
+
const entryRunScope = graph.runScope as RunBreakerScope | undefined;
|
|
1558
|
+
const runScopeInvalidated = (): boolean =>
|
|
1559
|
+
entryRunScope != null && graph.runScope !== entryRunScope;
|
|
1560
|
+
const throwIfRunBreakerTripped = (): void => {
|
|
1561
|
+
if (
|
|
1562
|
+
eventBreaker != null &&
|
|
1563
|
+
eventBreaker.signal.aborted &&
|
|
1564
|
+
eventBreaker.signal.reason instanceof StreamLimitExceededError
|
|
1565
|
+
) {
|
|
1566
|
+
throw eventBreaker.signal.reason;
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Enforced before every content-specific early return below
|
|
1572
|
+
* (server-tool results, deferred mixed reasoning, late OpenRouter
|
|
1573
|
+
* reasoning): a looping provider can flood through any of those paths,
|
|
1574
|
+
* a coalesced event can carry client `tool_call_chunks` alongside a
|
|
1575
|
+
* server-tool result, and the complete-call dispatch branch further
|
|
1576
|
+
* down can prestart a side-effecting tool from an arrival-sealed
|
|
1577
|
+
* oversized call. Charging is claim-based: the producer loop and this
|
|
1578
|
+
* decoupled echo can observe the same chunk object in either order, and
|
|
1579
|
+
* only the first claimer charges it. The argument guard is
|
|
1580
|
+
* deliberately NOT gated on numeric chunk indices, so id-only or
|
|
1581
|
+
* index-less runaway streams stay bounded, and complete parsed
|
|
1582
|
+
* `tool_calls` without a raw chunk representation are judged standalone.
|
|
1583
|
+
*/
|
|
1584
|
+
if (
|
|
1585
|
+
requiresStreamLimitAccounting(graph, chunk) &&
|
|
1586
|
+
claimStreamLimitCharge(graph, data.chunk, 'consumer', metadata)
|
|
1587
|
+
) {
|
|
1588
|
+
try {
|
|
1589
|
+
enforceStreamDeltaEventLimit({ graph, metadata });
|
|
1590
|
+
/** Combined first so raw-chunk name correlation sees invalid calls
|
|
1591
|
+
* too; an unnamed raw chunk twinned with a named invalid call must
|
|
1592
|
+
* select that tool's override, not the global cap. */
|
|
1593
|
+
const completeCalls = combineCompleteToolCalls(chunk);
|
|
1594
|
+
if (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) {
|
|
1595
|
+
enforceStreamedToolCallArgLimit({
|
|
1596
|
+
graph,
|
|
1597
|
+
metadata,
|
|
1598
|
+
toolCallChunks: chunk.tool_call_chunks,
|
|
1599
|
+
responseMetadata: chunk.response_metadata as
|
|
1600
|
+
| Record<string, unknown>
|
|
1601
|
+
| undefined,
|
|
1602
|
+
parsedToolCalls: completeCalls,
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
/** Judged whenever parsed calls are present, not only when raw
|
|
1606
|
+
* chunks are absent — an adapter can pair an empty or partial raw
|
|
1607
|
+
* chunk with a complete parsed call; the standalone check is
|
|
1608
|
+
* stateless, so the common both-present case is not double-tallied.
|
|
1609
|
+
* Invalid calls are included because ToolNode processes and
|
|
1610
|
+
* promotes them. */
|
|
1611
|
+
if (completeCalls != null) {
|
|
1612
|
+
enforceCompleteToolCallArgLimit({
|
|
1613
|
+
graph,
|
|
1614
|
+
metadata,
|
|
1615
|
+
toolCalls: completeCalls,
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
/** A breach detected on this consumer path must still stop parallel
|
|
1620
|
+
* fan-out work: the producer skips its own enforcement once this
|
|
1621
|
+
* side has claimed the emission, so createCallModel's breaker-abort
|
|
1622
|
+
* never fires for it. Trip the EVENT's run-bound breaker before the
|
|
1623
|
+
* throw rejects the run — never the live controller of a newer run. */
|
|
1624
|
+
if (error instanceof StreamLimitExceededError && eventBreaker != null) {
|
|
1625
|
+
eventBreaker.abort(error);
|
|
1626
|
+
}
|
|
1627
|
+
throw error;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/** A parallel producer can trip the shared breaker while this event was
|
|
1632
|
+
* already queued in `streamEvents`. Stop before content handling or the
|
|
1633
|
+
* eager-tool paths below — those can dispatch a side-effecting host
|
|
1634
|
+
* tool with an already-aborted signal the handler never inspects.
|
|
1635
|
+
* Rechecked again immediately before each eager dispatch: the awaits in
|
|
1636
|
+
* between (server-tool results, tool-call handling, content dispatch)
|
|
1637
|
+
* are windows for a sibling's trip — or for a full reset, after which
|
|
1638
|
+
* this event belongs to a dead run and is dropped. */
|
|
1639
|
+
if (runScopeInvalidated()) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
throwIfRunBreakerTripped();
|
|
1643
|
+
|
|
1644
|
+
const agentContext = graph.getAgentContext(metadata);
|
|
1645
|
+
|
|
1519
1646
|
const content = getChunkContent({
|
|
1520
1647
|
chunk,
|
|
1521
1648
|
reasoningKey: agentContext.reasoningKey,
|
|
@@ -1576,6 +1703,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1576
1703
|
) {
|
|
1577
1704
|
hasToolCalls = true;
|
|
1578
1705
|
await handleToolCalls(chunk.tool_calls, metadata, graph);
|
|
1706
|
+
if (runScopeInvalidated()) {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
throwIfRunBreakerTripped();
|
|
1579
1710
|
if (hasFinalToolCallSignal(chunk)) {
|
|
1580
1711
|
startEagerToolExecutions({
|
|
1581
1712
|
graph,
|
|
@@ -1655,6 +1786,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
|
|
|
1655
1786
|
metadata,
|
|
1656
1787
|
});
|
|
1657
1788
|
if (canStreamEager) {
|
|
1789
|
+
if (runScopeInvalidated()) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
throwIfRunBreakerTripped();
|
|
1658
1793
|
startReadyStreamedEagerToolExecutions({
|
|
1659
1794
|
graph,
|
|
1660
1795
|
metadata,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
DEFAULT_SUMMARIZATION_PROMPT,
|
|
8
8
|
DEFAULT_UPDATE_SUMMARIZATION_PROMPT,
|
|
9
9
|
} from '@/summarization/node';
|
|
10
|
+
import { StreamLimitExceededError } from '@/llm/streamLimits';
|
|
10
11
|
import { convertInjectedMessages } from '@/messages/injected';
|
|
11
12
|
import { Constants, GraphEvents, Providers } from '@/common';
|
|
12
13
|
import { AgentContext } from '@/agents/AgentContext';
|
|
@@ -1707,3 +1708,271 @@ describe('createSummarizeNode — overflow recovery', () => {
|
|
|
1707
1708
|
expect(agentContext.hasSummary()).toBe(true);
|
|
1708
1709
|
});
|
|
1709
1710
|
});
|
|
1711
|
+
|
|
1712
|
+
describe('summarize node breaker capture', () => {
|
|
1713
|
+
it('stamps the entry-captured breaker epoch into summary attempt metadata', async () => {
|
|
1714
|
+
captureEvents();
|
|
1715
|
+
const capturedConfigs: Array<{ metadata?: Record<string, unknown> }> = [];
|
|
1716
|
+
jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
|
|
1717
|
+
class {
|
|
1718
|
+
constructor() {
|
|
1719
|
+
return {
|
|
1720
|
+
invoke: jest
|
|
1721
|
+
.fn()
|
|
1722
|
+
.mockImplementation(
|
|
1723
|
+
async (_messages: unknown, config?: unknown) => {
|
|
1724
|
+
capturedConfigs.push(
|
|
1725
|
+
config as { metadata?: Record<string, unknown> }
|
|
1726
|
+
);
|
|
1727
|
+
return { content: 'Summary text' };
|
|
1728
|
+
}
|
|
1729
|
+
),
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
} as never
|
|
1733
|
+
);
|
|
1734
|
+
|
|
1735
|
+
const agentContext = createAgentContext();
|
|
1736
|
+
const graph = {
|
|
1737
|
+
...mockGraph(),
|
|
1738
|
+
getBreakerEpoch: (): number => 7,
|
|
1739
|
+
};
|
|
1740
|
+
const node = createSummarizeNode({
|
|
1741
|
+
agentContext,
|
|
1742
|
+
graph,
|
|
1743
|
+
generateStepId,
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
await node(
|
|
1747
|
+
{
|
|
1748
|
+
messages: [new HumanMessage('Hello'), new HumanMessage('World')],
|
|
1749
|
+
summarizationRequest: {
|
|
1750
|
+
remainingContextTokens: 1000,
|
|
1751
|
+
agentId: 'agent_0',
|
|
1752
|
+
},
|
|
1753
|
+
},
|
|
1754
|
+
{} as RunnableConfig
|
|
1755
|
+
);
|
|
1756
|
+
|
|
1757
|
+
expect(capturedConfigs.length).toBeGreaterThan(0);
|
|
1758
|
+
for (const config of capturedConfigs) {
|
|
1759
|
+
expect(config.metadata?.lc_stream_limit_epoch).toBe(7);
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
it('rejects before the model call when the breaker trips during pre-call awaits', async () => {
|
|
1764
|
+
const trip = new StreamLimitExceededError({
|
|
1765
|
+
kind: 'tool_call_args',
|
|
1766
|
+
limit: 10,
|
|
1767
|
+
observed: 11,
|
|
1768
|
+
toolName: 'db_query',
|
|
1769
|
+
});
|
|
1770
|
+
const entryBreaker = new AbortController();
|
|
1771
|
+
/** The trip lands while ON_SUMMARIZE_START is awaited — after the
|
|
1772
|
+
* entry check already passed. */
|
|
1773
|
+
jest.spyOn(eventUtils, 'safeDispatchCustomEvent').mockImplementation((async (
|
|
1774
|
+
...args: unknown[]
|
|
1775
|
+
) => {
|
|
1776
|
+
if (args[0] === GraphEvents.ON_SUMMARIZE_START) {
|
|
1777
|
+
entryBreaker.abort(trip);
|
|
1778
|
+
}
|
|
1779
|
+
}) as never);
|
|
1780
|
+
const modelClassSpy = jest
|
|
1781
|
+
.spyOn(providers, 'getChatModelClass')
|
|
1782
|
+
.mockReturnValue(
|
|
1783
|
+
class {
|
|
1784
|
+
constructor() {
|
|
1785
|
+
return mockInvokeModel('should never run');
|
|
1786
|
+
}
|
|
1787
|
+
} as never
|
|
1788
|
+
);
|
|
1789
|
+
|
|
1790
|
+
const agentContext = createAgentContext();
|
|
1791
|
+
const graph = {
|
|
1792
|
+
...mockGraph(),
|
|
1793
|
+
getBreakerSignal: (): AbortSignal => entryBreaker.signal,
|
|
1794
|
+
};
|
|
1795
|
+
const node = createSummarizeNode({
|
|
1796
|
+
agentContext,
|
|
1797
|
+
graph,
|
|
1798
|
+
generateStepId,
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
await expect(
|
|
1802
|
+
node(
|
|
1803
|
+
{
|
|
1804
|
+
messages: [new HumanMessage('Hello'), new HumanMessage('World')],
|
|
1805
|
+
summarizationRequest: {
|
|
1806
|
+
remainingContextTokens: 1000,
|
|
1807
|
+
agentId: 'agent_0',
|
|
1808
|
+
},
|
|
1809
|
+
},
|
|
1810
|
+
{} as RunnableConfig
|
|
1811
|
+
)
|
|
1812
|
+
).rejects.toBe(trip);
|
|
1813
|
+
expect(modelClassSpy).not.toHaveBeenCalled();
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
it('rethrows a parent trip on the config signal instead of degrading to the stub', async () => {
|
|
1817
|
+
const trip = new StreamLimitExceededError({
|
|
1818
|
+
kind: 'tool_call_args',
|
|
1819
|
+
limit: 10,
|
|
1820
|
+
observed: 11,
|
|
1821
|
+
toolName: 'db_query',
|
|
1822
|
+
});
|
|
1823
|
+
/** Child graph's own breaker stays live: in a subagent, a ROOT
|
|
1824
|
+
* sibling's trip arrives only through the composed invocation signal. */
|
|
1825
|
+
const childBreaker = new AbortController();
|
|
1826
|
+
const configAbort = new AbortController();
|
|
1827
|
+
captureEvents();
|
|
1828
|
+
jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
|
|
1829
|
+
class {
|
|
1830
|
+
constructor() {
|
|
1831
|
+
return {
|
|
1832
|
+
invoke: jest.fn().mockImplementation(async () => {
|
|
1833
|
+
configAbort.abort(trip);
|
|
1834
|
+
throw new Error('The operation was aborted');
|
|
1835
|
+
}),
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
} as never
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
const agentContext = createAgentContext();
|
|
1842
|
+
const graph = {
|
|
1843
|
+
...mockGraph(),
|
|
1844
|
+
getBreakerSignal: (): AbortSignal => childBreaker.signal,
|
|
1845
|
+
};
|
|
1846
|
+
const node = createSummarizeNode({
|
|
1847
|
+
agentContext,
|
|
1848
|
+
graph,
|
|
1849
|
+
generateStepId,
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
await expect(
|
|
1853
|
+
node(
|
|
1854
|
+
{
|
|
1855
|
+
messages: [new HumanMessage('Hello'), new HumanMessage('World')],
|
|
1856
|
+
summarizationRequest: {
|
|
1857
|
+
remainingContextTokens: 1000,
|
|
1858
|
+
agentId: 'agent_0',
|
|
1859
|
+
},
|
|
1860
|
+
},
|
|
1861
|
+
{ signal: configAbort.signal } as RunnableConfig
|
|
1862
|
+
)
|
|
1863
|
+
).rejects.toBe(trip);
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
it('rejects at entry when the breaker has already tripped', async () => {
|
|
1867
|
+
const trip = new StreamLimitExceededError({
|
|
1868
|
+
kind: 'tool_call_args',
|
|
1869
|
+
limit: 10,
|
|
1870
|
+
observed: 11,
|
|
1871
|
+
toolName: 'db_query',
|
|
1872
|
+
});
|
|
1873
|
+
const entryBreaker = new AbortController();
|
|
1874
|
+
entryBreaker.abort(trip);
|
|
1875
|
+
|
|
1876
|
+
const modelClassSpy = jest
|
|
1877
|
+
.spyOn(providers, 'getChatModelClass')
|
|
1878
|
+
.mockReturnValue(
|
|
1879
|
+
class {
|
|
1880
|
+
constructor() {
|
|
1881
|
+
return mockInvokeModel('should never run');
|
|
1882
|
+
}
|
|
1883
|
+
} as never
|
|
1884
|
+
);
|
|
1885
|
+
|
|
1886
|
+
const agentContext = createAgentContext();
|
|
1887
|
+
const graph = {
|
|
1888
|
+
...mockGraph(),
|
|
1889
|
+
getBreakerSignal: (): AbortSignal => entryBreaker.signal,
|
|
1890
|
+
};
|
|
1891
|
+
const node = createSummarizeNode({
|
|
1892
|
+
agentContext,
|
|
1893
|
+
graph,
|
|
1894
|
+
generateStepId,
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
await expect(
|
|
1898
|
+
node(
|
|
1899
|
+
{
|
|
1900
|
+
messages: [new HumanMessage('Hello'), new HumanMessage('World')],
|
|
1901
|
+
summarizationRequest: {
|
|
1902
|
+
remainingContextTokens: 1000,
|
|
1903
|
+
agentId: 'agent_0',
|
|
1904
|
+
},
|
|
1905
|
+
},
|
|
1906
|
+
{} as RunnableConfig
|
|
1907
|
+
)
|
|
1908
|
+
).rejects.toBe(trip);
|
|
1909
|
+
|
|
1910
|
+
expect(graph.contentData).toHaveLength(0);
|
|
1911
|
+
expect(modelClassSpy).not.toHaveBeenCalled();
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
it('binds the model call to the breaker signal read at node entry', async () => {
|
|
1915
|
+
const entryBreaker = new AbortController();
|
|
1916
|
+
const lateBreaker = new AbortController();
|
|
1917
|
+
let started = false;
|
|
1918
|
+
jest.spyOn(eventUtils, 'safeDispatchCustomEvent').mockImplementation((async (
|
|
1919
|
+
...args: unknown[]
|
|
1920
|
+
) => {
|
|
1921
|
+
if (args[0] === GraphEvents.ON_SUMMARIZE_START) {
|
|
1922
|
+
started = true;
|
|
1923
|
+
}
|
|
1924
|
+
}) as never);
|
|
1925
|
+
|
|
1926
|
+
const capturedSignals: Array<AbortSignal | undefined> = [];
|
|
1927
|
+
jest.spyOn(providers, 'getChatModelClass').mockReturnValue(
|
|
1928
|
+
class {
|
|
1929
|
+
constructor() {
|
|
1930
|
+
return {
|
|
1931
|
+
invoke: jest
|
|
1932
|
+
.fn()
|
|
1933
|
+
.mockImplementation(
|
|
1934
|
+
async (_messages: unknown, config?: unknown) => {
|
|
1935
|
+
capturedSignals.push(
|
|
1936
|
+
(config as RunnableConfig | undefined)?.signal
|
|
1937
|
+
);
|
|
1938
|
+
return { content: 'Summary text' };
|
|
1939
|
+
}
|
|
1940
|
+
),
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
} as never
|
|
1944
|
+
);
|
|
1945
|
+
|
|
1946
|
+
const agentContext = createAgentContext();
|
|
1947
|
+
/** Simulates a graph reset between node entry and the model call: once
|
|
1948
|
+
* ON_SUMMARIZE_START has been awaited, the live accessor hands out a
|
|
1949
|
+
* fresh controller's signal. The model call must still see the signal
|
|
1950
|
+
* captured at entry. */
|
|
1951
|
+
const graph = {
|
|
1952
|
+
...mockGraph(),
|
|
1953
|
+
getBreakerSignal: (): AbortSignal =>
|
|
1954
|
+
started ? lateBreaker.signal : entryBreaker.signal,
|
|
1955
|
+
};
|
|
1956
|
+
const node = createSummarizeNode({
|
|
1957
|
+
agentContext,
|
|
1958
|
+
graph,
|
|
1959
|
+
generateStepId,
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1962
|
+
await node(
|
|
1963
|
+
{
|
|
1964
|
+
messages: [new HumanMessage('Hello'), new HumanMessage('World')],
|
|
1965
|
+
summarizationRequest: {
|
|
1966
|
+
remainingContextTokens: 1000,
|
|
1967
|
+
agentId: 'agent_0',
|
|
1968
|
+
},
|
|
1969
|
+
},
|
|
1970
|
+
{} as RunnableConfig
|
|
1971
|
+
);
|
|
1972
|
+
|
|
1973
|
+
expect(capturedSignals.length).toBeGreaterThan(0);
|
|
1974
|
+
for (const signal of capturedSignals) {
|
|
1975
|
+
expect(signal).toBe(entryBreaker.signal);
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
});
|