@librechat/agents 3.3.3 → 3.3.5
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 +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/constants.cjs +21 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/events.cjs +10 -1
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +793 -111
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +25 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs +12 -0
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +1 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +8 -0
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +38 -13
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +331 -5
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/preempt.cjs +132 -0
- package/dist/cjs/llm/preempt.cjs.map +1 -0
- package/dist/cjs/main.cjs +36 -5
- package/dist/cjs/messages/alternation.cjs +92 -0
- package/dist/cjs/messages/alternation.cjs.map +1 -0
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +290 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/handoffCue.cjs +63 -0
- package/dist/cjs/messages/handoffCue.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +4 -1
- package/dist/cjs/messages/injected.cjs +60 -0
- package/dist/cjs/messages/injected.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +80 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/handlers.cjs +18 -9
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/stream.cjs +35 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +56 -63
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-scraper.cjs +90 -0
- package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +9 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/constants.mjs +19 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/events.mjs +10 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +798 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +25 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs +12 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +1 -0
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +9 -1
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +38 -13
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +332 -8
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/preempt.mjs +131 -0
- package/dist/esm/llm/preempt.mjs.map +1 -0
- package/dist/esm/main.mjs +14 -11
- package/dist/esm/messages/alternation.mjs +91 -0
- package/dist/esm/messages/alternation.mjs.map +1 -0
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +290 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/handoffCue.mjs +61 -0
- package/dist/esm/messages/handoffCue.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +4 -1
- package/dist/esm/messages/injected.mjs +60 -0
- package/dist/esm/messages/injected.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +80 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/handlers.mjs +19 -10
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/stream.mjs +34 -11
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +56 -63
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-scraper.mjs +88 -0
- package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +9 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/common/constants.d.ts +18 -0
- package/dist/types/graphs/Graph.d.ts +139 -2
- package/dist/types/hooks/HookRegistry.d.ts +15 -0
- package/dist/types/hooks/index.d.ts +12 -1
- package/dist/types/hooks/types.d.ts +45 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +34 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/llm/preempt.d.ts +40 -0
- package/dist/types/messages/alternation.d.ts +23 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/handoffCue.d.ts +40 -0
- package/dist/types/messages/index.d.ts +3 -0
- package/dist/types/messages/injected.d.ts +3 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/run.d.ts +7 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/types/tools/ToolNode.d.ts +0 -7
- package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
- package/dist/types/tools/search/types.d.ts +31 -2
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/run.d.ts +65 -0
- package/dist/types/types/stream.d.ts +1 -26
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- package/src/{splitStream.test.ts → aggregator.test.ts} +59 -666
- package/src/common/constants.ts +21 -0
- package/src/events.ts +15 -1
- package/src/graphs/Graph.ts +1261 -198
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1139 -4
- package/src/graphs/__tests__/Graph.preemptSignal.test.ts +126 -0
- package/src/hooks/HookRegistry.ts +40 -0
- package/src/hooks/__tests__/preemptBoundary.test.ts +152 -0
- package/src/hooks/index.ts +16 -2
- package/src/hooks/types.ts +47 -3
- package/src/index.ts +1 -1
- package/src/langfuse.ts +26 -1
- package/src/llm/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +131 -1
- package/src/llm/bedrock/utils/message_inputs.ts +100 -24
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.alternation.test.ts +87 -0
- package/src/llm/invoke.handoffCue.test.ts +112 -0
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +611 -8
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/llm/preempt.test.ts +323 -0
- package/src/llm/preempt.ts +178 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/alternation.test.ts +212 -0
- package/src/messages/alternation.ts +112 -0
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +454 -91
- package/src/messages/formatAgentMessages.steer.test.ts +267 -0
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/handoffCue.test.ts +96 -0
- package/src/messages/handoffCue.ts +78 -0
- package/src/messages/index.ts +3 -0
- package/src/messages/injected.test.ts +90 -0
- package/src/messages/injected.ts +74 -0
- package/src/messages/prune.ts +996 -183
- package/src/run.ts +91 -6
- package/src/scripts/preempt-probe.ts +330 -0
- package/src/scripts/preempt-scenarios.ts +388 -0
- package/src/session/handlers.ts +32 -12
- package/src/specs/handoffCue.test.ts +165 -0
- package/src/specs/langfuse-callbacks.test.ts +352 -2
- package/src/specs/preemptSeal.test.ts +309 -0
- package/src/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.dispatch.test.ts +63 -0
- package/src/stream.ts +59 -23
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +150 -99
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- package/src/tools/search/keenable-scraper.test.ts +153 -0
- package/src/tools/search/keenable-scraper.ts +137 -0
- package/src/tools/search/tool.ts +13 -2
- package/src/tools/search/types.ts +50 -3
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/graph.ts +12 -1
- package/src/types/run.ts +67 -0
- package/src/types/stream.ts +1 -42
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- package/src/utils/toolContent.ts +2707 -0
- package/dist/cjs/splitStream.cjs +0 -151
- package/dist/cjs/splitStream.cjs.map +0 -1
- package/dist/esm/splitStream.mjs +0 -150
- package/dist/esm/splitStream.mjs.map +0 -1
- package/dist/types/mockStream.d.ts +0 -32
- package/dist/types/splitStream.d.ts +0 -37
- package/src/mockStream.ts +0 -99
- package/src/splitStream.ts +0 -234
package/src/graphs/Graph.ts
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import { nanoid } from 'nanoid';
|
|
3
3
|
import { tool } from '@langchain/core/tools';
|
|
4
4
|
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
|
5
|
+
import { ContextOverflowError } from '@langchain/core/errors';
|
|
5
6
|
import { Runnable, RunnableConfig } from '@langchain/core/runnables';
|
|
6
|
-
import { ToolMessage, AIMessageChunk } from '@langchain/core/messages';
|
|
7
7
|
import { START, END, StateGraph, Annotation } from '@langchain/langgraph';
|
|
8
|
+
import {
|
|
9
|
+
ToolMessage,
|
|
10
|
+
HumanMessage,
|
|
11
|
+
AIMessageChunk,
|
|
12
|
+
} from '@langchain/core/messages';
|
|
8
13
|
import type {
|
|
9
14
|
UsageMetadata,
|
|
10
15
|
BaseMessage,
|
|
@@ -16,55 +21,74 @@ import type { FallbackErrorContext } from '@/llm/invoke';
|
|
|
16
21
|
import type { HookRegistry } from '@/hooks';
|
|
17
22
|
import type * as t from '@/types';
|
|
18
23
|
import {
|
|
19
|
-
|
|
24
|
+
projectAnthropicArtifactContent,
|
|
20
25
|
ensureThinkingBlockInMessages,
|
|
21
26
|
foldToolBlocksForToollessAgent,
|
|
22
27
|
convertMessagesToContent,
|
|
23
28
|
sanitizeOrphanToolBlocks,
|
|
24
29
|
extractToolDiscoveries,
|
|
25
30
|
addBedrockTailCacheControl,
|
|
26
|
-
|
|
31
|
+
projectArtifactPayload,
|
|
27
32
|
formatContentStrings,
|
|
28
|
-
isLegacyConvertible,
|
|
29
33
|
CALIBRATION_RATIO_MAX,
|
|
34
|
+
REPLY_PRIMER_TOKENS,
|
|
30
35
|
createPruneMessages,
|
|
36
|
+
projectToolCallInputs,
|
|
37
|
+
calculateMaxToolCallInputChars,
|
|
38
|
+
projectToolStreamContentForProvider,
|
|
31
39
|
syncBudgetDerivedFields,
|
|
32
40
|
addTailCacheControl,
|
|
33
41
|
resolvePromptCacheTtl,
|
|
34
42
|
resolveBedrockPromptCacheTtl,
|
|
35
43
|
supportsBedrockToolCache,
|
|
44
|
+
isSyntheticProviderContextMessage,
|
|
36
45
|
getMessageId,
|
|
37
46
|
makeIsDeferred,
|
|
38
47
|
partitionAndMarkAnthropicToolCache,
|
|
39
48
|
DEFAULT_RETAIN_RECENT_TURNS,
|
|
40
49
|
splitAtRecencyBoundary,
|
|
50
|
+
convertInjectedMessages,
|
|
51
|
+
coalesceAdjacentUserTurns,
|
|
52
|
+
strictAlternationProviders,
|
|
53
|
+
appendPredecessorHandoffCue,
|
|
54
|
+
removePredecessorHandoffCue,
|
|
41
55
|
} from '@/messages';
|
|
42
|
-
import {
|
|
43
|
-
createLangfuseHandler,
|
|
44
|
-
createLangfuseTraceMetadata,
|
|
45
|
-
disposeLangfuseHandler,
|
|
46
|
-
isLangfuseCallbackHandler,
|
|
47
|
-
} from '@/langfuse';
|
|
48
56
|
import {
|
|
49
57
|
resetIfNotEmpty,
|
|
50
58
|
isAnthropicLike,
|
|
51
59
|
isOpenAILike,
|
|
52
60
|
isGoogleLike,
|
|
53
61
|
apportionTokenCounts,
|
|
62
|
+
calculateMaxToolResultChars,
|
|
54
63
|
joinKeys,
|
|
55
64
|
sleep,
|
|
56
65
|
} from '@/utils';
|
|
57
|
-
import {
|
|
58
|
-
getBlindRecoveryBudget,
|
|
59
|
-
planContextOverflowRecovery,
|
|
60
|
-
translateRecoveryBudget,
|
|
61
|
-
} from '@/llm/contextOverflowRecovery';
|
|
62
66
|
import {
|
|
63
67
|
attemptInvoke,
|
|
64
68
|
tryFallbackProviders,
|
|
65
69
|
getFallbackErrorContext,
|
|
66
70
|
getFallbackOverflowCandidates,
|
|
71
|
+
projectMessagesForProvider,
|
|
72
|
+
resolveServingModelId,
|
|
67
73
|
} from '@/llm/invoke';
|
|
74
|
+
import { v4 } from 'uuid';
|
|
75
|
+
import {
|
|
76
|
+
createLangfuseHandler,
|
|
77
|
+
createLangfuseTraceMetadata,
|
|
78
|
+
disposeLangfuseHandler,
|
|
79
|
+
isLangfuseCallbackHandler,
|
|
80
|
+
} from '@/langfuse';
|
|
81
|
+
import {
|
|
82
|
+
getBlindRecoveryBudget,
|
|
83
|
+
planContextOverflowRecovery,
|
|
84
|
+
translateRecoveryBudget,
|
|
85
|
+
} from '@/llm/contextOverflowRecovery';
|
|
86
|
+
import {
|
|
87
|
+
compactToolContent,
|
|
88
|
+
getToolContentCharLength,
|
|
89
|
+
serializeToolContentBounded,
|
|
90
|
+
} from '@/utils/toolContent';
|
|
91
|
+
import { resolveMaxSeals } from '@/llm/preempt';
|
|
68
92
|
import {
|
|
69
93
|
Constants,
|
|
70
94
|
GraphNodeKeys,
|
|
@@ -72,7 +96,12 @@ import {
|
|
|
72
96
|
GraphEvents,
|
|
73
97
|
Providers,
|
|
74
98
|
StepTypes,
|
|
99
|
+
PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
|
|
75
100
|
} from '@/common';
|
|
101
|
+
import {
|
|
102
|
+
annotateMessagesForLLM,
|
|
103
|
+
ToolOutputReferenceRegistry,
|
|
104
|
+
} from '@/tools/toolOutputReferences';
|
|
76
105
|
import {
|
|
77
106
|
resolveLangfuseRuntimeScope,
|
|
78
107
|
withLangfuseRuntimeScope,
|
|
@@ -87,7 +116,6 @@ import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
|
|
|
87
116
|
import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
|
|
88
117
|
import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
|
|
89
118
|
import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
|
|
90
|
-
import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
91
119
|
import { partitionAndMarkBedrockToolCache } from '@/llm/bedrock/toolCache';
|
|
92
120
|
import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
|
|
93
121
|
import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
|
|
@@ -106,9 +134,41 @@ import { isThinkingEnabled } from '@/llm/request';
|
|
|
106
134
|
import { initializeModel } from '@/llm/init';
|
|
107
135
|
import { HandlerRegistry } from '@/events';
|
|
108
136
|
import { ChatOpenAI } from '@/llm/openai';
|
|
137
|
+
import { executeHooks } from '@/hooks';
|
|
109
138
|
|
|
110
139
|
const { AGENT, TOOLS, SUMMARIZE } = GraphNodeKeys;
|
|
111
140
|
|
|
141
|
+
/** What a `PreemptBoundary` drain resolved to. */
|
|
142
|
+
type PreemptBoundaryResult = {
|
|
143
|
+
messages: BaseMessage[];
|
|
144
|
+
/** A hook asked for no further model turn; the seal must not self-loop. */
|
|
145
|
+
preventContinuation: boolean;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const EMPTY_PREEMPT_BOUNDARY: PreemptBoundaryResult = {
|
|
149
|
+
messages: [],
|
|
150
|
+
preventContinuation: false,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* One signal that fires when either input fires. `AbortSignal.any` is skipped
|
|
155
|
+
* when the inputs collapse to a single signal — the composite is a fresh
|
|
156
|
+
* object per call, and the common cases (one channel, or the host reusing the
|
|
157
|
+
* same controller for both) don't need one.
|
|
158
|
+
*/
|
|
159
|
+
function composeAbortSignals(
|
|
160
|
+
a: AbortSignal | undefined,
|
|
161
|
+
b: AbortSignal | undefined
|
|
162
|
+
): AbortSignal | undefined {
|
|
163
|
+
if (a == null || a === b) {
|
|
164
|
+
return b;
|
|
165
|
+
}
|
|
166
|
+
if (b == null) {
|
|
167
|
+
return a;
|
|
168
|
+
}
|
|
169
|
+
return AbortSignal.any([a, b]);
|
|
170
|
+
}
|
|
171
|
+
|
|
112
172
|
/** Minimum relative variance before calibrated toolSchemaTokens overrides current value. */
|
|
113
173
|
const CALIBRATION_VARIANCE_THRESHOLD = 0.15;
|
|
114
174
|
|
|
@@ -124,26 +184,6 @@ function createToolHandlerRegistry(
|
|
|
124
184
|
return registry;
|
|
125
185
|
}
|
|
126
186
|
|
|
127
|
-
/**
|
|
128
|
-
* Start index of the span post-prune formatters can mutate in place: the
|
|
129
|
-
* trailing tool batch plus its owning AI message (artifact formatting touches
|
|
130
|
-
* every tool result after the last AI tool call; Bedrock rewrites the AI
|
|
131
|
-
* message before a trailing tool result). Capped so the usage-snapshot
|
|
132
|
-
* recount stays constant-cost.
|
|
133
|
-
*/
|
|
134
|
-
function trailingMutationStart(messages: BaseMessage[]): number {
|
|
135
|
-
const MAX_SPAN = 16;
|
|
136
|
-
let index = messages.length - 1;
|
|
137
|
-
while (
|
|
138
|
-
index >= 0 &&
|
|
139
|
-
messages[index]?.getType() === 'tool' &&
|
|
140
|
-
messages.length - index < MAX_SPAN
|
|
141
|
-
) {
|
|
142
|
-
index--;
|
|
143
|
-
}
|
|
144
|
-
return Math.max(0, Math.min(index, messages.length - 2));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
187
|
type ReasoningKey = 'reasoning_content' | 'reasoning';
|
|
148
188
|
type ReasoningSummary = { summary?: Array<{ text?: string }> };
|
|
149
189
|
type ReasoningDetail = { type?: string; text?: string };
|
|
@@ -656,6 +696,20 @@ export abstract class Graph<
|
|
|
656
696
|
reasoningStepHasDeltas: Set<string> = new Set();
|
|
657
697
|
protected handlerDispatchedEventCounts: Map<string, number> = new Map();
|
|
658
698
|
signal?: AbortSignal;
|
|
699
|
+
/**
|
|
700
|
+
* The abort signal the CALLER handed to the current `processStream` call,
|
|
701
|
+
* assigned unconditionally — including back to `undefined` — on every call.
|
|
702
|
+
*
|
|
703
|
+
* Kept separate from {@link signal} on purpose. That field is construction
|
|
704
|
+
* state with its own consumers (model-call config, subagent parentSignal),
|
|
705
|
+
* so adopting a per-call signal into it would leak one call's controller
|
|
706
|
+
* into the next — `clearHeavyState()` is skipped on HITL interrupts, so a
|
|
707
|
+
* host that aborts a finished request's controller would poison the resumed
|
|
708
|
+
* run's model calls and boundary drains with an already-aborted signal.
|
|
709
|
+
* Boundary dispatch composes the two instead; see
|
|
710
|
+
* `StandardGraph.dispatchPreemptBoundary`.
|
|
711
|
+
*/
|
|
712
|
+
callerSignal?: AbortSignal;
|
|
659
713
|
/** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
|
|
660
714
|
invokedToolIds?: Set<string>;
|
|
661
715
|
handlerRegistry: HandlerRegistry | undefined;
|
|
@@ -735,6 +789,7 @@ export abstract class Graph<
|
|
|
735
789
|
clearHeavyState(): void {
|
|
736
790
|
this.config = undefined;
|
|
737
791
|
this.signal = undefined;
|
|
792
|
+
this.callerSignal = undefined;
|
|
738
793
|
this.contentData = [];
|
|
739
794
|
this.contentIndexMap = new Map();
|
|
740
795
|
this.stepKeyIds = new Map();
|
|
@@ -937,6 +992,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
937
992
|
messages: BaseMessage[] = [];
|
|
938
993
|
/** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
|
|
939
994
|
private cachedRunMessages?: BaseMessage[];
|
|
995
|
+
/** Ids of AI turns the agent node returned THIS run; see isRunProducedMessage. */
|
|
996
|
+
protected runProducedAiMessageIds = new Set<string>();
|
|
940
997
|
/** Checkpoint scope whose messages match index-keyed tool snapshots. */
|
|
941
998
|
private originalToolContentCheckpointScope?: string;
|
|
942
999
|
runId: string | undefined;
|
|
@@ -962,6 +1019,53 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
962
1019
|
subagentUsageSink?: t.SubagentUsageSink;
|
|
963
1020
|
/** See {@link t.StandardGraphInput.subagentScope}. */
|
|
964
1021
|
subagentScope: boolean;
|
|
1022
|
+
/** See {@link t.StandardGraphInput.preemption}. */
|
|
1023
|
+
preemption?: t.StreamPreemption;
|
|
1024
|
+
/**
|
|
1025
|
+
* Seals charged against `preemption.maxSeals`. Per-turn: cleared by both
|
|
1026
|
+
* reset paths so a fresh turn gets a fresh budget, while a HITL resume —
|
|
1027
|
+
* which skips `resetValues` — keeps what it had left.
|
|
1028
|
+
*/
|
|
1029
|
+
private preemptSealBudgetUsed = 0;
|
|
1030
|
+
/**
|
|
1031
|
+
* Seals honored over the graph's lifetime. Reported by
|
|
1032
|
+
* {@link getPreemptStats}, so it deliberately SURVIVES `clearHeavyState()`
|
|
1033
|
+
* — a host reads it after `processStream` returns, which is strictly after
|
|
1034
|
+
* cleanup runs.
|
|
1035
|
+
*/
|
|
1036
|
+
preemptSealCount = 0;
|
|
1037
|
+
/** Boundaries that produced nothing to inject, so the turn stopped early. */
|
|
1038
|
+
preemptEmptyBoundaries = 0;
|
|
1039
|
+
/**
|
|
1040
|
+
* Set between claiming a seal and resolving its boundary. `MultiAgentGraph`
|
|
1041
|
+
* fans parallel agents through this one instance against a single host
|
|
1042
|
+
* request, so without a one-at-a-time gate several streams would each seal
|
|
1043
|
+
* for the same queued message and every loser would take the
|
|
1044
|
+
* nothing-to-inject path and cut its answer short.
|
|
1045
|
+
*/
|
|
1046
|
+
private preemptSealInFlight = false;
|
|
1047
|
+
/**
|
|
1048
|
+
* True when a seal ended the turn without a resume. The assistant turn is
|
|
1049
|
+
* real and kept, but it is not the answer the model intended to finish —
|
|
1050
|
+
* hosts persist it as unfinished rather than complete.
|
|
1051
|
+
*/
|
|
1052
|
+
preemptIncomplete = false;
|
|
1053
|
+
/**
|
|
1054
|
+
* `stopReason` from a `PreemptBoundary` hook that halted the turn.
|
|
1055
|
+
*
|
|
1056
|
+
* Clearing the registry halt is what keeps the sealed turn alive, but the
|
|
1057
|
+
* registry held the only copy of the reason — so it is captured here first.
|
|
1058
|
+
* Without it `getHaltReason()` returns undefined and a host records a
|
|
1059
|
+
* hook-halted turn as an ordinary completion.
|
|
1060
|
+
*/
|
|
1061
|
+
preemptHaltReason: string | undefined;
|
|
1062
|
+
/**
|
|
1063
|
+
* Agent IDs whose next superstep must return to the agent node. Keyed by
|
|
1064
|
+
* agent because `MultiAgentGraph` routes every parallel agent through this
|
|
1065
|
+
* same instance, and a single field would let one agent's boundary resume
|
|
1066
|
+
* another's turn.
|
|
1067
|
+
*/
|
|
1068
|
+
pendingPreemptReturn = new Set<string>();
|
|
965
1069
|
|
|
966
1070
|
constructor({
|
|
967
1071
|
runId,
|
|
@@ -973,6 +1077,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
973
1077
|
calibrationRatio,
|
|
974
1078
|
subagentUsageSink,
|
|
975
1079
|
subagentScope,
|
|
1080
|
+
preemption,
|
|
976
1081
|
}: t.StandardGraphInput) {
|
|
977
1082
|
super();
|
|
978
1083
|
this.runId = runId;
|
|
@@ -980,6 +1085,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
980
1085
|
this.langfuse = langfuse;
|
|
981
1086
|
this.subagentUsageSink = subagentUsageSink;
|
|
982
1087
|
this.subagentScope = subagentScope === true;
|
|
1088
|
+
this.preemption = preemption;
|
|
983
1089
|
|
|
984
1090
|
if (agents.length === 0) {
|
|
985
1091
|
throw new Error('At least one agent configuration is required');
|
|
@@ -1019,6 +1125,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1019
1125
|
* a stale reference on 2nd+ processStream calls.
|
|
1020
1126
|
*/
|
|
1021
1127
|
this.toolCallStepIds.clear();
|
|
1128
|
+
this.runProducedAiMessageIds.clear();
|
|
1022
1129
|
this.eagerEventToolExecutions.clear();
|
|
1023
1130
|
this.clearEagerEventToolUsageCounts();
|
|
1024
1131
|
this.eagerEventToolCallChunks.clear();
|
|
@@ -1051,6 +1158,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1051
1158
|
new Map()
|
|
1052
1159
|
);
|
|
1053
1160
|
this.invokedToolIds = resetIfNotEmpty(this.invokedToolIds, undefined);
|
|
1161
|
+
this.resetPreemptTurnState();
|
|
1162
|
+
this.resetPreemptTotals();
|
|
1054
1163
|
const hasScopedCheckpoint =
|
|
1055
1164
|
this.hasCompiledCheckpointer &&
|
|
1056
1165
|
checkpointScope != null &&
|
|
@@ -1071,6 +1180,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1071
1180
|
super.clearHeavyState();
|
|
1072
1181
|
this.messages = [];
|
|
1073
1182
|
this.overrideModel = undefined;
|
|
1183
|
+
/**
|
|
1184
|
+
* Turn state only. The reported totals must outlive cleanup — this runs
|
|
1185
|
+
* in `processStream`'s `finally`, and the host reads `getPreemptStats()`
|
|
1186
|
+
* after that returns.
|
|
1187
|
+
*/
|
|
1188
|
+
this.resetPreemptTurnState();
|
|
1074
1189
|
const preserveOriginalToolContent =
|
|
1075
1190
|
this.hasCompiledCheckpointer &&
|
|
1076
1191
|
this.originalToolContentCheckpointScope != null;
|
|
@@ -1079,6 +1194,120 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1079
1194
|
}
|
|
1080
1195
|
}
|
|
1081
1196
|
|
|
1197
|
+
/**
|
|
1198
|
+
* Per-turn seal budget and routing markers. Cleared by both reset paths so
|
|
1199
|
+
* a new turn starts with a full budget and no stale resume marker.
|
|
1200
|
+
*
|
|
1201
|
+
* The REPORTED counters are deliberately not touched here — see
|
|
1202
|
+
* {@link resetPreemptTotals}.
|
|
1203
|
+
*/
|
|
1204
|
+
private resetPreemptTurnState(): void {
|
|
1205
|
+
this.preemptSealBudgetUsed = 0;
|
|
1206
|
+
this.preemptSealInFlight = false;
|
|
1207
|
+
this.pendingPreemptReturn.clear();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Lifetime seal totals, cleared only when a genuinely new run starts.
|
|
1212
|
+
* `clearHeavyState()` must NOT call this: it runs in `processStream`'s
|
|
1213
|
+
* `finally`, so zeroing here would make {@link getPreemptStats} and
|
|
1214
|
+
* `preemptIncomplete` unreadable for every caller of the method that just
|
|
1215
|
+
* produced them.
|
|
1216
|
+
*/
|
|
1217
|
+
private resetPreemptTotals(): void {
|
|
1218
|
+
this.preemptSealCount = 0;
|
|
1219
|
+
this.preemptEmptyBoundaries = 0;
|
|
1220
|
+
this.preemptIncomplete = false;
|
|
1221
|
+
this.preemptHaltReason = undefined;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* True when the host has requested a cooperative seal AND this graph may
|
|
1226
|
+
* honor it. Read once per streamed chunk, so it stays property reads plus
|
|
1227
|
+
* one host callback — no I/O, no allocation.
|
|
1228
|
+
*
|
|
1229
|
+
* Non-mutating: a true result only means a seal is worth evaluating. The
|
|
1230
|
+
* budget is taken by {@link claimPreemptSeal} once the accumulated chunk is
|
|
1231
|
+
* known to be safe, so a chunk that cannot seal never spends budget.
|
|
1232
|
+
*
|
|
1233
|
+
* Subagent scopes never seal: a steer targets the top-level conversation,
|
|
1234
|
+
* and a child run must finish so its parent sees a complete result.
|
|
1235
|
+
*/
|
|
1236
|
+
/** Internal seal preconditions only — no host callback, no side effects. */
|
|
1237
|
+
private canClaimPreemptSeal(): boolean {
|
|
1238
|
+
/**
|
|
1239
|
+
* Resolved and required here with the same rule `dispatchPreemptBoundary`
|
|
1240
|
+
* uses. Without it a direct `StandardGraph` consumer that supplies no
|
|
1241
|
+
* `runId` could claim a seal on the strength of a global matcher, then hit
|
|
1242
|
+
* the boundary's own null-runId guard and get nothing back — truncating
|
|
1243
|
+
* the answer for a drain that provably could not run.
|
|
1244
|
+
*/
|
|
1245
|
+
const runId =
|
|
1246
|
+
(this.config?.configurable?.run_id as string | undefined) ?? this.runId;
|
|
1247
|
+
return (
|
|
1248
|
+
!this.subagentScope &&
|
|
1249
|
+
this.preemption != null &&
|
|
1250
|
+
!this.preemptSealInFlight &&
|
|
1251
|
+
this.preemptSealBudgetUsed < resolveMaxSeals(this.preemption.maxSeals) &&
|
|
1252
|
+
runId != null &&
|
|
1253
|
+
/**
|
|
1254
|
+
* A seal only buys room for an injection. With no `PreemptBoundary`
|
|
1255
|
+
* matcher live — never registered, or a `once` matcher already
|
|
1256
|
+
* consumed — the boundary provably returns nothing and the answer is
|
|
1257
|
+
* cut short for no gain, so refuse the seal instead. Failing closed
|
|
1258
|
+
* lands on the documented no-preemption behavior: the model finishes
|
|
1259
|
+
* and the message waits for the next tool boundary.
|
|
1260
|
+
*
|
|
1261
|
+
* Same session resolution as `dispatchPreemptBoundary`, or a
|
|
1262
|
+
* session-scoped matcher would be visible at one site and not the other.
|
|
1263
|
+
*/
|
|
1264
|
+
this.hookRegistry?.hasDispatchableHookFor('PreemptBoundary', runId) ===
|
|
1265
|
+
true
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
shouldPreemptStream(): boolean {
|
|
1270
|
+
return (
|
|
1271
|
+
this.canClaimPreemptSeal() && this.preemption?.shouldPreempt() === true
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Takes the seal slot, or returns false if another stream already holds it.
|
|
1277
|
+
*
|
|
1278
|
+
* Assumes the caller already polled `shouldPreemptStream()` for THIS chunk,
|
|
1279
|
+
* and deliberately does not poll the host again — `StreamPreemption`
|
|
1280
|
+
* documents `shouldPreempt` as once per chunk, and a host that consumes a
|
|
1281
|
+
* pending flag on read would lose the request to a second call.
|
|
1282
|
+
*
|
|
1283
|
+
* The guard and both mutations remain one synchronous body, which is what
|
|
1284
|
+
* makes this safe under a parallel `MultiAgentGraph`: several agents share
|
|
1285
|
+
* one graph and can each see the poll as true, but no `await` can split the
|
|
1286
|
+
* claim, so only one takes the slot. The loser keeps streaming normally
|
|
1287
|
+
* rather than sealing for a message it would never receive.
|
|
1288
|
+
*/
|
|
1289
|
+
claimPreemptSeal(): boolean {
|
|
1290
|
+
if (!this.canClaimPreemptSeal()) {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
this.preemptSealInFlight = true;
|
|
1294
|
+
this.preemptSealBudgetUsed += 1;
|
|
1295
|
+
this.preemptSealCount += 1;
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/** Releases the seal slot once its boundary has resolved, win or lose. */
|
|
1300
|
+
releasePreemptSeal(): void {
|
|
1301
|
+
this.preemptSealInFlight = false;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
getPreemptStats(): t.PreemptStats {
|
|
1305
|
+
return {
|
|
1306
|
+
seals: this.preemptSealCount,
|
|
1307
|
+
emptyBoundaries: this.preemptEmptyBoundaries,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1082
1311
|
/* Run Step Processing */
|
|
1083
1312
|
|
|
1084
1313
|
getRunStep(stepId: string): t.RunStep | undefined {
|
|
@@ -1242,6 +1471,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1242
1471
|
return this.messages.slice(this.startIndex);
|
|
1243
1472
|
}
|
|
1244
1473
|
|
|
1474
|
+
/**
|
|
1475
|
+
* True when THIS RUN produced `message` — the provenance the handoff cue
|
|
1476
|
+
* gate needs. Tracked as an id set rather than inferred from `startIndex`
|
|
1477
|
+
* arithmetic: summarization's remove-all compaction rewrites the live
|
|
1478
|
+
* array and leaves `startIndex` stale, so index-based run/host
|
|
1479
|
+
* discrimination silently breaks right after a mid-run summarize. Ids
|
|
1480
|
+
* survive compaction (retained messages keep theirs), host-supplied
|
|
1481
|
+
* prefill messages are never in the set, and membership is O(1) per
|
|
1482
|
+
* model call.
|
|
1483
|
+
*/
|
|
1484
|
+
isRunProducedMessage(message: BaseMessage): boolean {
|
|
1485
|
+
const id = message.id;
|
|
1486
|
+
return (
|
|
1487
|
+
typeof id === 'string' &&
|
|
1488
|
+
id !== '' &&
|
|
1489
|
+
this.runProducedAiMessageIds.has(id)
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1245
1493
|
getContentParts(): t.MessageContentComplex[] | undefined {
|
|
1246
1494
|
// `messages` can be null/undefined on a graph that has been disposed
|
|
1247
1495
|
// (clearHeavyState) but is still reachable via a cache (e.g. RedisJobStore's
|
|
@@ -1622,6 +1870,23 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1622
1870
|
throw new Error('No config provided');
|
|
1623
1871
|
}
|
|
1624
1872
|
|
|
1873
|
+
/**
|
|
1874
|
+
* A `PreemptBoundary` hook halted this run and the sealed commit is
|
|
1875
|
+
* already in state. Enforced at every model node's ENTRY because that
|
|
1876
|
+
* is the only site that covers all of `MultiAgentGraph`'s onward
|
|
1877
|
+
* routing at once — static direct edges, Command fan-out, fan-in
|
|
1878
|
+
* wrappers, and parallel siblings' subsequent inner-loop turns — none
|
|
1879
|
+
* of which consult the halt (the registry signal was deliberately
|
|
1880
|
+
* cleared to keep the stream-cancel from destroying the sealed turn).
|
|
1881
|
+
* Declining the model call turns every routed-to successor into a
|
|
1882
|
+
* no-op, so the outer workflow drains to END without new turns or tool
|
|
1883
|
+
* side effects. Reset per turn in `resetPreemptTotals`, so the next
|
|
1884
|
+
* `processStream` call starts clean.
|
|
1885
|
+
*/
|
|
1886
|
+
if (this.preemptHaltReason != null) {
|
|
1887
|
+
return { messages: [] };
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1625
1890
|
const { messages } = state;
|
|
1626
1891
|
|
|
1627
1892
|
const discoveredNames = extractToolDiscoveries(messages);
|
|
@@ -1747,6 +2012,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1747
2012
|
provider: agentContext.provider,
|
|
1748
2013
|
tokenCounter: agentContext.tokenCounter,
|
|
1749
2014
|
maxTokens: agentContext.maxContextTokens,
|
|
2015
|
+
maxToolResultChars: agentContext.maxToolResultChars,
|
|
1750
2016
|
thinkingEnabled: isThinkingEnabled(
|
|
1751
2017
|
agentContext.provider,
|
|
1752
2018
|
agentContext.clientOptions
|
|
@@ -1923,37 +2189,136 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1923
2189
|
}
|
|
1924
2190
|
|
|
1925
2191
|
let finalMessages = messagesToUse;
|
|
1926
|
-
/**
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
1931
|
-
*
|
|
1932
|
-
|
|
1933
|
-
let
|
|
1934
|
-
|
|
1935
|
-
|
|
2192
|
+
/**
|
|
2193
|
+
* Keep the pruner's provider-grounded aggregate as the authoritative
|
|
2194
|
+
* baseline, then attribute it across retained messages. Provider
|
|
2195
|
+
* transforms can shrink one message while expanding or adding another;
|
|
2196
|
+
* per-origin accounting prevents that unrelated shrink from canceling
|
|
2197
|
+
* the expansion. Raw counts are frozen before in-place formatters run.
|
|
2198
|
+
*/
|
|
2199
|
+
let providerMessageBaseline:
|
|
2200
|
+
| Array<{ rawTokens: number; accountingWeight: number }>
|
|
2201
|
+
| undefined;
|
|
2202
|
+
const providerMessageOrigins = new WeakMap<BaseMessage, number>();
|
|
1936
2203
|
if (contextUsage != null && agentContext.tokenCounter != null) {
|
|
1937
|
-
|
|
1938
|
-
for (
|
|
1939
|
-
|
|
2204
|
+
const sourceIndices = new WeakMap<BaseMessage, number>();
|
|
2205
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2206
|
+
sourceIndices.set(messages[i], i);
|
|
1940
2207
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2208
|
+
providerMessageBaseline = messagesToUse.map((message, index) => {
|
|
2209
|
+
const rawTokens = agentContext.tokenCounter!(message);
|
|
2210
|
+
const sourceIndex = sourceIndices.get(message);
|
|
2211
|
+
const indexedTokens =
|
|
2212
|
+
sourceIndex != null
|
|
2213
|
+
? agentContext.indexTokenCountMap[sourceIndex]
|
|
2214
|
+
: undefined;
|
|
2215
|
+
const accountingWeight =
|
|
2216
|
+
indexedTokens != null &&
|
|
2217
|
+
Number.isFinite(indexedTokens) &&
|
|
2218
|
+
indexedTokens >= 0
|
|
2219
|
+
? indexedTokens
|
|
2220
|
+
: rawTokens;
|
|
2221
|
+
if (!providerMessageOrigins.has(message)) {
|
|
2222
|
+
providerMessageOrigins.set(message, index);
|
|
2223
|
+
}
|
|
2224
|
+
return { rawTokens, accountingWeight };
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
const getProviderMessageOriginKey = (
|
|
2229
|
+
message: BaseMessage
|
|
2230
|
+
): string | undefined => {
|
|
2231
|
+
const type = message.getType();
|
|
2232
|
+
if (
|
|
2233
|
+
message instanceof ToolMessage &&
|
|
2234
|
+
typeof message.tool_call_id === 'string' &&
|
|
2235
|
+
message.tool_call_id.length > 0
|
|
2236
|
+
) {
|
|
2237
|
+
return `tool:call:${message.tool_call_id}`;
|
|
2238
|
+
}
|
|
2239
|
+
if (typeof message.id === 'string' && message.id.length > 0) {
|
|
2240
|
+
return `${type}:id:${message.id}`;
|
|
2241
|
+
}
|
|
2242
|
+
return undefined;
|
|
2243
|
+
};
|
|
2244
|
+
|
|
2245
|
+
/**
|
|
2246
|
+
* Provider projections clone messages. Preserve their baseline origin
|
|
2247
|
+
* without writing tracking metadata onto the wire. Synthetic fold
|
|
2248
|
+
* messages intentionally remain unattributed and are charged in full.
|
|
2249
|
+
*/
|
|
2250
|
+
const trackProviderMessageOrigins = (
|
|
2251
|
+
before: BaseMessage[],
|
|
2252
|
+
after: BaseMessage[]
|
|
2253
|
+
): BaseMessage[] => {
|
|
2254
|
+
if (providerMessageBaseline == null || before === after) {
|
|
2255
|
+
return after;
|
|
2256
|
+
}
|
|
2257
|
+
if (before.length === after.length) {
|
|
2258
|
+
for (let i = 0; i < after.length; i++) {
|
|
2259
|
+
const origin = providerMessageOrigins.get(before[i]);
|
|
2260
|
+
if (
|
|
2261
|
+
origin != null &&
|
|
2262
|
+
!providerMessageOrigins.has(after[i]) &&
|
|
2263
|
+
before[i].getType() === after[i].getType() &&
|
|
2264
|
+
!isSyntheticProviderContextMessage(after[i])
|
|
2265
|
+
) {
|
|
2266
|
+
providerMessageOrigins.set(after[i], origin);
|
|
1949
2267
|
}
|
|
1950
2268
|
}
|
|
2269
|
+
return after;
|
|
1951
2270
|
}
|
|
1952
|
-
|
|
2271
|
+
|
|
2272
|
+
const keyedOrigins = new Map<string, number | null>();
|
|
2273
|
+
for (const message of before) {
|
|
2274
|
+
const origin = providerMessageOrigins.get(message);
|
|
2275
|
+
const key = getProviderMessageOriginKey(message);
|
|
2276
|
+
if (origin == null || key == null) {
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
keyedOrigins.set(key, keyedOrigins.has(key) ? null : origin);
|
|
2280
|
+
}
|
|
2281
|
+
for (const message of after) {
|
|
2282
|
+
if (
|
|
2283
|
+
providerMessageOrigins.has(message) ||
|
|
2284
|
+
isSyntheticProviderContextMessage(message)
|
|
2285
|
+
) {
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
const key = getProviderMessageOriginKey(message);
|
|
2289
|
+
const origin = key != null ? keyedOrigins.get(key) : undefined;
|
|
2290
|
+
if (origin != null) {
|
|
2291
|
+
providerMessageOrigins.set(message, origin);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
return after;
|
|
2295
|
+
};
|
|
2296
|
+
|
|
1953
2297
|
if (agentContext.useLegacyContent) {
|
|
1954
|
-
|
|
2298
|
+
const before = finalMessages;
|
|
2299
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2300
|
+
before,
|
|
2301
|
+
formatContentStrings(before)
|
|
2302
|
+
);
|
|
1955
2303
|
}
|
|
1956
2304
|
|
|
2305
|
+
const maxProviderToolResultChars =
|
|
2306
|
+
agentContext.maxToolResultChars ??
|
|
2307
|
+
calculateMaxToolResultChars(agentContext.maxContextTokens);
|
|
2308
|
+
const beforeToolStreamProjection = finalMessages;
|
|
2309
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2310
|
+
beforeToolStreamProjection,
|
|
2311
|
+
projectToolStreamContentForProvider(beforeToolStreamProjection)
|
|
2312
|
+
);
|
|
2313
|
+
const beforeToolInputProjection = finalMessages;
|
|
2314
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2315
|
+
beforeToolInputProjection,
|
|
2316
|
+
projectToolCallInputs(
|
|
2317
|
+
beforeToolInputProjection,
|
|
2318
|
+
calculateMaxToolCallInputChars(agentContext.maxContextTokens)
|
|
2319
|
+
)
|
|
2320
|
+
);
|
|
2321
|
+
|
|
1957
2322
|
const lastMessageX =
|
|
1958
2323
|
finalMessages.length >= 2
|
|
1959
2324
|
? finalMessages[finalMessages.length - 2]
|
|
@@ -1979,56 +2344,433 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1979
2344
|
trimmed.length > 0 ? [{ type: 'text' as const, text: trimmed }] : '';
|
|
1980
2345
|
}
|
|
1981
2346
|
|
|
2347
|
+
const localProviderOverflowMeasurements = new WeakMap<
|
|
2348
|
+
object,
|
|
2349
|
+
{
|
|
2350
|
+
contextBudget: number;
|
|
2351
|
+
estimatedPromptTokens: number;
|
|
2352
|
+
}
|
|
2353
|
+
>();
|
|
2354
|
+
const measureProviderPayload = (
|
|
2355
|
+
candidate: BaseMessage[],
|
|
2356
|
+
contextBudgetOverride?: number,
|
|
2357
|
+
forceRawRecount = false
|
|
2358
|
+
): {
|
|
2359
|
+
fits: boolean;
|
|
2360
|
+
projectedMessageTokens?: number;
|
|
2361
|
+
availableMessageTokens?: number;
|
|
2362
|
+
contextBudget?: number;
|
|
2363
|
+
effectiveInstructionTokens?: number;
|
|
2364
|
+
} => {
|
|
2365
|
+
const contextBudget =
|
|
2366
|
+
contextBudgetOverride ?? contextUsage?.contextBudget;
|
|
2367
|
+
const effectiveInstructionTokens =
|
|
2368
|
+
contextUsage?.effectiveInstructionTokens ??
|
|
2369
|
+
(forceRawRecount ? agentContext.instructionTokens : undefined);
|
|
2370
|
+
if (
|
|
2371
|
+
agentContext.tokenCounter == null ||
|
|
2372
|
+
contextBudget == null ||
|
|
2373
|
+
effectiveInstructionTokens == null
|
|
2374
|
+
) {
|
|
2375
|
+
return { fits: true };
|
|
2376
|
+
}
|
|
2377
|
+
const availableMessageTokens = Math.max(
|
|
2378
|
+
0,
|
|
2379
|
+
contextBudget - effectiveInstructionTokens
|
|
2380
|
+
);
|
|
2381
|
+
let usageRatio =
|
|
2382
|
+
agentContext.calibrationRatio > 0 ? agentContext.calibrationRatio : 1;
|
|
2383
|
+
if (
|
|
2384
|
+
contextUsage?.calibrationRatio != null &&
|
|
2385
|
+
contextUsage.calibrationRatio > 0
|
|
2386
|
+
) {
|
|
2387
|
+
usageRatio = contextUsage.calibrationRatio;
|
|
2388
|
+
}
|
|
2389
|
+
if (forceRawRecount) {
|
|
2390
|
+
usageRatio = Math.max(1, usageRatio);
|
|
2391
|
+
}
|
|
2392
|
+
const baselineRemaining = contextUsage?.remainingContextTokens;
|
|
2393
|
+
const accountedMessageTokens =
|
|
2394
|
+
!forceRawRecount &&
|
|
2395
|
+
providerMessageBaseline != null &&
|
|
2396
|
+
baselineRemaining != null &&
|
|
2397
|
+
Number.isFinite(baselineRemaining)
|
|
2398
|
+
? availableMessageTokens -
|
|
2399
|
+
Math.min(availableMessageTokens, Math.max(0, baselineRemaining))
|
|
2400
|
+
: undefined;
|
|
2401
|
+
|
|
2402
|
+
let projectedMessageTokens: number;
|
|
2403
|
+
if (accountedMessageTokens != null && providerMessageBaseline != null) {
|
|
2404
|
+
const replyPrimerTokens = Math.round(
|
|
2405
|
+
REPLY_PRIMER_TOKENS * usageRatio
|
|
2406
|
+
);
|
|
2407
|
+
const rawWeights: Record<string, number> = {};
|
|
2408
|
+
let totalWeight = 0;
|
|
2409
|
+
for (let i = 0; i < providerMessageBaseline.length; i++) {
|
|
2410
|
+
const weight = providerMessageBaseline[i].accountingWeight;
|
|
2411
|
+
rawWeights[i] = weight;
|
|
2412
|
+
totalWeight += weight;
|
|
2413
|
+
}
|
|
2414
|
+
const attributableTokens =
|
|
2415
|
+
totalWeight > 0
|
|
2416
|
+
? Math.min(
|
|
2417
|
+
Math.max(0, accountedMessageTokens - replyPrimerTokens),
|
|
2418
|
+
Math.round(totalWeight * usageRatio)
|
|
2419
|
+
)
|
|
2420
|
+
: 0;
|
|
2421
|
+
const apportionedTokens =
|
|
2422
|
+
totalWeight > 0
|
|
2423
|
+
? apportionTokenCounts(
|
|
2424
|
+
rawWeights,
|
|
2425
|
+
attributableTokens / totalWeight,
|
|
2426
|
+
attributableTokens
|
|
2427
|
+
)
|
|
2428
|
+
: {};
|
|
2429
|
+
const attributedByOrigin = providerMessageBaseline.map(
|
|
2430
|
+
(_, origin) => apportionedTokens[origin] || 0
|
|
2431
|
+
);
|
|
2432
|
+
projectedMessageTokens = Math.max(
|
|
2433
|
+
replyPrimerTokens,
|
|
2434
|
+
accountedMessageTokens - attributableTokens
|
|
2435
|
+
);
|
|
2436
|
+
let newRawTokens = 0;
|
|
2437
|
+
const usedOrigins = new Set<number>();
|
|
2438
|
+
for (const message of candidate) {
|
|
2439
|
+
const rawTokens = agentContext.tokenCounter(message);
|
|
2440
|
+
const origin = providerMessageOrigins.get(message);
|
|
2441
|
+
if (origin == null || usedOrigins.has(origin)) {
|
|
2442
|
+
newRawTokens += rawTokens;
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
usedOrigins.add(origin);
|
|
2446
|
+
projectedMessageTokens += Math.max(
|
|
2447
|
+
0,
|
|
2448
|
+
attributedByOrigin[origin] +
|
|
2449
|
+
Math.round(
|
|
2450
|
+
(rawTokens - providerMessageBaseline[origin].rawTokens) *
|
|
2451
|
+
usageRatio
|
|
2452
|
+
)
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
projectedMessageTokens += Math.round(newRawTokens * usageRatio);
|
|
2456
|
+
} else {
|
|
2457
|
+
let rawTokens = REPLY_PRIMER_TOKENS;
|
|
2458
|
+
for (const message of candidate) {
|
|
2459
|
+
rawTokens += agentContext.tokenCounter(message);
|
|
2460
|
+
}
|
|
2461
|
+
projectedMessageTokens = Math.round(rawTokens * usageRatio);
|
|
2462
|
+
}
|
|
2463
|
+
return {
|
|
2464
|
+
fits: projectedMessageTokens <= availableMessageTokens,
|
|
2465
|
+
projectedMessageTokens,
|
|
2466
|
+
availableMessageTokens,
|
|
2467
|
+
contextBudget,
|
|
2468
|
+
effectiveInstructionTokens,
|
|
2469
|
+
};
|
|
2470
|
+
};
|
|
2471
|
+
|
|
2472
|
+
const createProviderPayloadOverflowError = ({
|
|
2473
|
+
projection,
|
|
2474
|
+
provider,
|
|
2475
|
+
info,
|
|
2476
|
+
}: {
|
|
2477
|
+
projection: ReturnType<typeof measureProviderPayload>;
|
|
2478
|
+
provider?: Providers;
|
|
2479
|
+
info: string;
|
|
2480
|
+
}): ContextOverflowError => {
|
|
2481
|
+
const error = new ContextOverflowError(
|
|
2482
|
+
JSON.stringify({
|
|
2483
|
+
type: 'final_context_overflow',
|
|
2484
|
+
info,
|
|
2485
|
+
provider,
|
|
2486
|
+
projectedMessageTokens: projection.projectedMessageTokens,
|
|
2487
|
+
availableMessageTokens: projection.availableMessageTokens,
|
|
2488
|
+
})
|
|
2489
|
+
);
|
|
2490
|
+
if (
|
|
2491
|
+
projection.projectedMessageTokens != null &&
|
|
2492
|
+
projection.contextBudget != null &&
|
|
2493
|
+
projection.effectiveInstructionTokens != null
|
|
2494
|
+
) {
|
|
2495
|
+
localProviderOverflowMeasurements.set(error, {
|
|
2496
|
+
contextBudget: projection.contextBudget,
|
|
2497
|
+
estimatedPromptTokens:
|
|
2498
|
+
projection.projectedMessageTokens +
|
|
2499
|
+
projection.effectiveInstructionTokens,
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
return error;
|
|
2503
|
+
};
|
|
2504
|
+
|
|
2505
|
+
const applyProviderMessageTransforms = (
|
|
2506
|
+
candidate: BaseMessage[]
|
|
2507
|
+
): BaseMessage[] => {
|
|
2508
|
+
let transformed = candidate;
|
|
2509
|
+
if (
|
|
2510
|
+
isThinkingEnabled(agentContext.provider, agentContext.clientOptions)
|
|
2511
|
+
) {
|
|
2512
|
+
/**
|
|
2513
|
+
* Current-run AI messages may validly omit a thinking block. The
|
|
2514
|
+
* boundary prevents them from being mistaken for foreign history.
|
|
2515
|
+
*/
|
|
2516
|
+
const before = transformed;
|
|
2517
|
+
transformed = trackProviderMessageOrigins(
|
|
2518
|
+
before,
|
|
2519
|
+
ensureThinkingBlockInMessages(
|
|
2520
|
+
before,
|
|
2521
|
+
agentContext.provider,
|
|
2522
|
+
config,
|
|
2523
|
+
this.startIndex
|
|
2524
|
+
)
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
/**
|
|
2529
|
+
* Tool-less destinations cannot send inherited tool blocks without a
|
|
2530
|
+
* tool schema, so fold those interactions into provider-valid content.
|
|
2531
|
+
*/
|
|
2532
|
+
if (toolsForBinding == null || toolsForBinding.length === 0) {
|
|
2533
|
+
const before = transformed;
|
|
2534
|
+
transformed = trackProviderMessageOrigins(
|
|
2535
|
+
before,
|
|
2536
|
+
foldToolBlocksForToollessAgent(before, config)
|
|
2537
|
+
);
|
|
2538
|
+
if (agentContext.useLegacyContent) {
|
|
2539
|
+
const beforeLegacyFormat = transformed;
|
|
2540
|
+
transformed = trackProviderMessageOrigins(
|
|
2541
|
+
beforeLegacyFormat,
|
|
2542
|
+
formatContentStrings(beforeLegacyFormat)
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Applied HERE for the primary so the cue is part of the MEASURED
|
|
2548
|
+
* payload — the pre-invoke projection and overflow guard run on this
|
|
2549
|
+
* stage's output, and a post-measure append could push a just-fits
|
|
2550
|
+
* prompt over budget unreported (#346 round 2). The attemptInvoke
|
|
2551
|
+
* funnel re-keys per SERVING provider: it strips this cue for a
|
|
2552
|
+
* tolerant fallback and adds it for a Claude fallback behind a
|
|
2553
|
+
* tolerant primary.
|
|
2554
|
+
*/
|
|
2555
|
+
if (
|
|
2556
|
+
isAnthropicLike(
|
|
2557
|
+
agentContext.provider,
|
|
2558
|
+
agentContext.clientOptions as { model?: string }
|
|
2559
|
+
)
|
|
2560
|
+
) {
|
|
2561
|
+
const before = transformed;
|
|
2562
|
+
transformed = trackProviderMessageOrigins(
|
|
2563
|
+
before,
|
|
2564
|
+
appendPredecessorHandoffCue(before, (message) =>
|
|
2565
|
+
this.isRunProducedMessage(message)
|
|
2566
|
+
)
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
return transformed;
|
|
2570
|
+
};
|
|
2571
|
+
|
|
2572
|
+
const toolOutputRegistry = this.getOrCreateToolOutputRegistry();
|
|
2573
|
+
const providerRunId = config.configurable?.run_id as string | undefined;
|
|
2574
|
+
const projectProviderReferences = (
|
|
2575
|
+
candidate: BaseMessage[]
|
|
2576
|
+
): BaseMessage[] =>
|
|
2577
|
+
trackProviderMessageOrigins(
|
|
2578
|
+
candidate,
|
|
2579
|
+
annotateMessagesForLLM(candidate, toolOutputRegistry, providerRunId)
|
|
2580
|
+
);
|
|
2581
|
+
|
|
2582
|
+
const compactSyntheticProviderContext = (
|
|
2583
|
+
candidate: BaseMessage[]
|
|
2584
|
+
): BaseMessage[] => {
|
|
2585
|
+
const synthetic: Array<{
|
|
2586
|
+
index: number;
|
|
2587
|
+
message: HumanMessage;
|
|
2588
|
+
chars: number;
|
|
2589
|
+
}> = [];
|
|
2590
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
2591
|
+
const message = candidate[i];
|
|
2592
|
+
if (
|
|
2593
|
+
!(message instanceof HumanMessage) ||
|
|
2594
|
+
!isSyntheticProviderContextMessage(message)
|
|
2595
|
+
) {
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
2598
|
+
const content = message.content;
|
|
2599
|
+
synthetic.push({
|
|
2600
|
+
index: i,
|
|
2601
|
+
message,
|
|
2602
|
+
chars: getToolContentCharLength(content),
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
if (synthetic.length === 0) {
|
|
2606
|
+
return candidate;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
const buildCandidate = (scale: number): BaseMessage[] => {
|
|
2610
|
+
const compacted = [...candidate];
|
|
2611
|
+
for (const { index, message, chars } of synthetic) {
|
|
2612
|
+
const content = compactToolContent(
|
|
2613
|
+
message.content,
|
|
2614
|
+
Math.floor(chars * scale)
|
|
2615
|
+
).content;
|
|
2616
|
+
compacted[index] = new HumanMessage({
|
|
2617
|
+
content,
|
|
2618
|
+
id: message.id,
|
|
2619
|
+
name: message.name,
|
|
2620
|
+
additional_kwargs: message.additional_kwargs,
|
|
2621
|
+
response_metadata: message.response_metadata,
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
return compacted;
|
|
2625
|
+
};
|
|
2626
|
+
|
|
2627
|
+
let best = buildCandidate(0);
|
|
2628
|
+
if (!measureProviderPayload(best).fits) {
|
|
2629
|
+
return candidate;
|
|
2630
|
+
}
|
|
2631
|
+
let low = 0;
|
|
2632
|
+
let high = 1;
|
|
2633
|
+
for (let i = 0; i < 12; i++) {
|
|
2634
|
+
const scale = (low + high) / 2;
|
|
2635
|
+
const attempt = buildCandidate(scale);
|
|
2636
|
+
if (measureProviderPayload(attempt).fits) {
|
|
2637
|
+
best = attempt;
|
|
2638
|
+
low = scale;
|
|
2639
|
+
} else {
|
|
2640
|
+
high = scale;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return best;
|
|
2644
|
+
};
|
|
2645
|
+
|
|
2646
|
+
let artifactBaseMessages: BaseMessage[] | undefined;
|
|
1982
2647
|
if (lastMessageY instanceof ToolMessage) {
|
|
2648
|
+
let artifactCandidate = finalMessages;
|
|
1983
2649
|
if (anthropicLike) {
|
|
1984
|
-
|
|
2650
|
+
artifactCandidate = trackProviderMessageOrigins(
|
|
2651
|
+
finalMessages,
|
|
2652
|
+
projectAnthropicArtifactContent(
|
|
2653
|
+
finalMessages,
|
|
2654
|
+
maxProviderToolResultChars
|
|
2655
|
+
)
|
|
2656
|
+
);
|
|
1985
2657
|
} else if (
|
|
1986
2658
|
(isOpenAILike(agentContext.provider) &&
|
|
1987
2659
|
agentContext.provider !== Providers.DEEPSEEK) ||
|
|
1988
2660
|
isGoogleLike(agentContext.provider)
|
|
1989
2661
|
) {
|
|
1990
|
-
|
|
2662
|
+
artifactCandidate = trackProviderMessageOrigins(
|
|
2663
|
+
finalMessages,
|
|
2664
|
+
projectArtifactPayload(finalMessages, maxProviderToolResultChars)
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
if (artifactCandidate !== finalMessages) {
|
|
2669
|
+
const projection = measureProviderPayload(artifactCandidate);
|
|
2670
|
+
if (projection.fits) {
|
|
2671
|
+
artifactBaseMessages = finalMessages;
|
|
2672
|
+
finalMessages = artifactCandidate;
|
|
2673
|
+
} else {
|
|
2674
|
+
emitAgentLog(
|
|
2675
|
+
config,
|
|
2676
|
+
'warn',
|
|
2677
|
+
'graph',
|
|
2678
|
+
'Artifact payload omitted because it exceeds the remaining context budget',
|
|
2679
|
+
{
|
|
2680
|
+
projectedMessageTokens: projection.projectedMessageTokens,
|
|
2681
|
+
availableMessageTokens: projection.availableMessageTokens,
|
|
2682
|
+
},
|
|
2683
|
+
{ runId: this.runId, agentId }
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
1991
2686
|
}
|
|
1992
2687
|
}
|
|
1993
2688
|
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
)
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2689
|
+
finalMessages = projectProviderReferences(
|
|
2690
|
+
applyProviderMessageTransforms(finalMessages)
|
|
2691
|
+
);
|
|
2692
|
+
let finalProjection = measureProviderPayload(finalMessages);
|
|
2693
|
+
if (artifactBaseMessages != null) {
|
|
2694
|
+
if (!finalProjection.fits) {
|
|
2695
|
+
finalMessages = projectProviderReferences(
|
|
2696
|
+
applyProviderMessageTransforms(artifactBaseMessages)
|
|
2697
|
+
);
|
|
2698
|
+
finalProjection = measureProviderPayload(finalMessages);
|
|
2699
|
+
emitAgentLog(
|
|
2700
|
+
config,
|
|
2701
|
+
'warn',
|
|
2702
|
+
'graph',
|
|
2703
|
+
'Artifact payload omitted after final provider formatting exceeded the remaining context budget',
|
|
2704
|
+
{
|
|
2705
|
+
projectedMessageTokens: finalProjection.projectedMessageTokens,
|
|
2706
|
+
availableMessageTokens: finalProjection.availableMessageTokens,
|
|
2707
|
+
},
|
|
2708
|
+
{ runId: this.runId, agentId }
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (!finalProjection.fits) {
|
|
2713
|
+
const compacted = compactSyntheticProviderContext(finalMessages);
|
|
2714
|
+
if (compacted !== finalMessages) {
|
|
2715
|
+
finalMessages = compacted;
|
|
2716
|
+
finalProjection = measureProviderPayload(finalMessages);
|
|
2717
|
+
emitAgentLog(
|
|
2718
|
+
config,
|
|
2719
|
+
finalProjection.fits ? 'warn' : 'error',
|
|
2720
|
+
'graph',
|
|
2721
|
+
finalProjection.fits
|
|
2722
|
+
? 'Synthetic provider context compacted to fit the final payload budget'
|
|
2723
|
+
: 'Final provider payload still exceeds budget after synthetic context compaction',
|
|
2724
|
+
{
|
|
2725
|
+
projectedMessageTokens: finalProjection.projectedMessageTokens,
|
|
2726
|
+
availableMessageTokens: finalProjection.availableMessageTokens,
|
|
2727
|
+
},
|
|
2728
|
+
{ runId: this.runId, agentId }
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2013
2731
|
}
|
|
2014
2732
|
|
|
2015
2733
|
/**
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2734
|
+
* Mistral rejects consecutive user turns outright; Bedrock's Converse
|
|
2735
|
+
* API documents strict user/assistant alternation across its model
|
|
2736
|
+
* families, with enforcement varying by family (Claude on Converse
|
|
2737
|
+
* currently tolerates the shape — verified live — but the payload is
|
|
2738
|
+
* normalized for all of them rather than betting on leniency). Four
|
|
2739
|
+
* sites can emit them — the `PostToolBatch` and `PreemptBoundary` hook
|
|
2740
|
+
* boundaries (a consolidated context message followed by one
|
|
2741
|
+
* `HumanMessage` per injected entry), a queue drain carrying more than
|
|
2742
|
+
* one steer, and `run.ts`'s pre-stream context push onto a payload that
|
|
2743
|
+
* already ends on a user turn.
|
|
2744
|
+
*
|
|
2745
|
+
* Normalized here, at the last provider-facing hop, rather than at any
|
|
2746
|
+
* one boundary: the boundaries must keep per-message identity, because
|
|
2747
|
+
* `additional_kwargs.source`/`skillName` drive steer rendering and the
|
|
2748
|
+
* trailing-steer anchor downstream. Graph state and the host's
|
|
2749
|
+
* persisted messages are untouched — this shapes only what goes on the
|
|
2750
|
+
* wire, for the providers that actually care.
|
|
2751
|
+
*
|
|
2752
|
+
* Runs AFTER synthetic-context compaction: that pass can rewrite or
|
|
2753
|
+
* drop messages, so coalescing has to see its output, and it is the
|
|
2754
|
+
* last shaping step before the cache breakpoint is chosen.
|
|
2023
2755
|
*/
|
|
2024
|
-
if (
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2756
|
+
if (strictAlternationProviders.has(agentContext.provider)) {
|
|
2757
|
+
/**
|
|
2758
|
+
* Wrapped like every other provider transform: the merged message is
|
|
2759
|
+
* a NEW object, and without re-attachment the final pre-invoke
|
|
2760
|
+
* measurement would drop both source turns' calibrated shares and
|
|
2761
|
+
* recharge the merge at full raw estimate — enough to flip a
|
|
2762
|
+
* just-fits payload (the synthetic-context compaction above binary
|
|
2763
|
+
* searches to exactly that) into a spurious pre-invoke overflow. The
|
|
2764
|
+
* merge keeps the first source's id, so the keyed branch re-attaches
|
|
2765
|
+
* that origin; the absorbed turn's tokens are charged as new raw
|
|
2766
|
+
* growth, which only ever under-estimates by less than the old
|
|
2767
|
+
* behavior over-estimated.
|
|
2768
|
+
*/
|
|
2769
|
+
const beforeCoalesce = finalMessages;
|
|
2770
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2771
|
+
beforeCoalesce,
|
|
2772
|
+
coalesceAdjacentUserTurns(beforeCoalesce)
|
|
2773
|
+
);
|
|
2032
2774
|
}
|
|
2033
2775
|
|
|
2034
2776
|
// Determine the prompt-cache strategy up front. Two distinct facts:
|
|
@@ -2083,7 +2825,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2083
2825
|
providerPromptCacheEnabled);
|
|
2084
2826
|
if (needsOrphanSanitize) {
|
|
2085
2827
|
const beforeSanitize = finalMessages.length;
|
|
2086
|
-
|
|
2828
|
+
const beforeSanitizeMessages = finalMessages;
|
|
2829
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2830
|
+
beforeSanitizeMessages,
|
|
2831
|
+
sanitizeOrphanToolBlocks(beforeSanitizeMessages, (source, clone) => {
|
|
2832
|
+
const origin = providerMessageOrigins.get(source);
|
|
2833
|
+
if (origin != null) {
|
|
2834
|
+
providerMessageOrigins.set(clone, origin);
|
|
2835
|
+
}
|
|
2836
|
+
})
|
|
2837
|
+
);
|
|
2087
2838
|
if (finalMessages.length !== beforeSanitize) {
|
|
2088
2839
|
emitAgentLog(
|
|
2089
2840
|
config,
|
|
@@ -2113,20 +2864,24 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2113
2864
|
(anthropicPromptCacheEnabled || openRouterPromptCacheEnabled) &&
|
|
2114
2865
|
!agentContext.systemRunnable
|
|
2115
2866
|
) {
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2867
|
+
const beforeCacheControl = finalMessages;
|
|
2868
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2869
|
+
beforeCacheControl,
|
|
2870
|
+
addTailCacheControl<BaseMessage>(
|
|
2871
|
+
beforeCacheControl,
|
|
2872
|
+
resolvePromptCacheTtl(
|
|
2873
|
+
anthropicPromptCacheEnabled
|
|
2874
|
+
? (
|
|
2875
|
+
agentContext.clientOptions as
|
|
2876
|
+
| t.AnthropicClientOptions
|
|
2877
|
+
| undefined
|
|
2878
|
+
)?.promptCacheTtl
|
|
2879
|
+
: (
|
|
2880
|
+
agentContext.clientOptions as
|
|
2881
|
+
| t.ProviderOptionsMap[Providers.OPENROUTER]
|
|
2882
|
+
| undefined
|
|
2883
|
+
)?.promptCacheTtl
|
|
2884
|
+
)
|
|
2130
2885
|
)
|
|
2131
2886
|
);
|
|
2132
2887
|
} else if (bedrockPromptCacheEnabled) {
|
|
@@ -2135,15 +2890,46 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2135
2890
|
| undefined;
|
|
2136
2891
|
// Non-Claude models (Nova) reject the extended 1h TTL, so resolve it
|
|
2137
2892
|
// against the model — message/system caching stays on, clamped to 5m.
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2893
|
+
const beforeCacheControl = finalMessages;
|
|
2894
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2895
|
+
beforeCacheControl,
|
|
2896
|
+
addBedrockTailCacheControl<BaseMessage>(
|
|
2897
|
+
beforeCacheControl,
|
|
2898
|
+
resolveBedrockPromptCacheTtl(
|
|
2899
|
+
bedrockOptions?.promptCacheTtl,
|
|
2900
|
+
(bedrockOptions as { model?: string } | undefined)?.model
|
|
2901
|
+
)
|
|
2143
2902
|
)
|
|
2144
2903
|
);
|
|
2145
2904
|
}
|
|
2146
2905
|
|
|
2906
|
+
const fallbackBaseMessages = finalMessages;
|
|
2907
|
+
const beforeFinalProviderProjection = fallbackBaseMessages;
|
|
2908
|
+
finalMessages = trackProviderMessageOrigins(
|
|
2909
|
+
beforeFinalProviderProjection,
|
|
2910
|
+
projectMessagesForProvider({
|
|
2911
|
+
model: (this.overrideModel ?? model) as t.ChatModel,
|
|
2912
|
+
messages: beforeFinalProviderProjection,
|
|
2913
|
+
provider: agentContext.provider,
|
|
2914
|
+
maxToolResultChars: maxProviderToolResultChars,
|
|
2915
|
+
callOptions: config,
|
|
2916
|
+
})
|
|
2917
|
+
);
|
|
2918
|
+
|
|
2919
|
+
/**
|
|
2920
|
+
* Prompt-cache placement and orphan sanitization are provider-wire
|
|
2921
|
+
* transforms too. Re-measure after both so no content added after the
|
|
2922
|
+
* earlier artifact/synthetic compaction decision can bypass the guard.
|
|
2923
|
+
*/
|
|
2924
|
+
finalProjection = measureProviderPayload(finalMessages);
|
|
2925
|
+
const preInvokeContextOverflowError = !finalProjection.fits
|
|
2926
|
+
? createProviderPayloadOverflowError({
|
|
2927
|
+
projection: finalProjection,
|
|
2928
|
+
provider: agentContext.provider,
|
|
2929
|
+
info: 'Provider message formatting exceeded the context budget and no safe synthetic-context compaction could make it fit.',
|
|
2930
|
+
})
|
|
2931
|
+
: undefined;
|
|
2932
|
+
|
|
2147
2933
|
if (
|
|
2148
2934
|
agentContext.lastStreamCall != null &&
|
|
2149
2935
|
agentContext.streamBuffer != null
|
|
@@ -2219,66 +3005,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2219
3005
|
|
|
2220
3006
|
/** Past the empty-prompt guard — a model call is now guaranteed */
|
|
2221
3007
|
if (contextUsage != null) {
|
|
2222
|
-
const usageRatio =
|
|
2223
|
-
contextUsage.calibrationRatio != null &&
|
|
2224
|
-
contextUsage.calibrationRatio > 0
|
|
2225
|
-
? contextUsage.calibrationRatio
|
|
2226
|
-
: 1;
|
|
2227
3008
|
if (
|
|
2228
|
-
|
|
2229
|
-
|
|
3009
|
+
finalProjection.projectedMessageTokens != null &&
|
|
3010
|
+
finalProjection.availableMessageTokens != null
|
|
2230
3011
|
) {
|
|
2231
|
-
/** Post-prune formatting restructured the payload (e.g. thinking
|
|
2232
|
-
* placeholder collapse, orphan drops) — recount so the gauge
|
|
2233
|
-
* reflects what is actually sent */
|
|
2234
|
-
let rawTokens = 0;
|
|
2235
|
-
for (const message of finalMessages) {
|
|
2236
|
-
rawTokens += agentContext.tokenCounter(message);
|
|
2237
|
-
}
|
|
2238
3012
|
contextUsage.breakdown.messageCount = finalMessages.length;
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
0,
|
|
2245
|
-
contextUsage.contextBudget -
|
|
2246
|
-
contextUsage.effectiveInstructionTokens -
|
|
2247
|
-
Math.round(rawTokens * usageRatio)
|
|
2248
|
-
);
|
|
2249
|
-
}
|
|
2250
|
-
} else if (
|
|
2251
|
-
preFormatTailTokens != null &&
|
|
2252
|
-
agentContext.tokenCounter != null &&
|
|
2253
|
-
contextUsage.remainingContextTokens != null
|
|
2254
|
-
) {
|
|
2255
|
-
/** Same-length formatting can still mutate in place — the trailing
|
|
2256
|
-
* tool batch (artifacts, Bedrock rewrites) and any legacy-converted
|
|
2257
|
-
* messages before it — adjust remaining by the calibrated delta */
|
|
2258
|
-
let postFormatTailTokens = 0;
|
|
2259
|
-
for (const message of finalMessages.slice(tailStart)) {
|
|
2260
|
-
postFormatTailTokens += agentContext.tokenCounter(message);
|
|
2261
|
-
}
|
|
2262
|
-
let formatDelta = postFormatTailTokens - preFormatTailTokens;
|
|
2263
|
-
if (legacyIndices != null && legacyIndices.length > 0) {
|
|
2264
|
-
let postFormatLegacyTokens = 0;
|
|
2265
|
-
for (const index of legacyIndices) {
|
|
2266
|
-
postFormatLegacyTokens += agentContext.tokenCounter(
|
|
2267
|
-
finalMessages[index]
|
|
2268
|
-
);
|
|
2269
|
-
}
|
|
2270
|
-
formatDelta += postFormatLegacyTokens - preFormatLegacyTokens;
|
|
2271
|
-
}
|
|
2272
|
-
if (formatDelta !== 0) {
|
|
2273
|
-
contextUsage.remainingContextTokens = Math.max(
|
|
2274
|
-
0,
|
|
2275
|
-
Math.min(
|
|
2276
|
-
contextUsage.contextBudget ?? Number.MAX_SAFE_INTEGER,
|
|
2277
|
-
contextUsage.remainingContextTokens -
|
|
2278
|
-
Math.round(formatDelta * usageRatio)
|
|
2279
|
-
)
|
|
2280
|
-
);
|
|
2281
|
-
}
|
|
3013
|
+
contextUsage.remainingContextTokens = Math.max(
|
|
3014
|
+
0,
|
|
3015
|
+
finalProjection.availableMessageTokens -
|
|
3016
|
+
finalProjection.projectedMessageTokens
|
|
3017
|
+
);
|
|
2282
3018
|
}
|
|
2283
3019
|
syncBudgetDerivedFields(contextUsage);
|
|
2284
3020
|
/** Awaited so async host handlers receive the pre-invoke snapshot
|
|
@@ -2346,6 +3082,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2346
3082
|
const metadata = config.metadata as Record<string, unknown>;
|
|
2347
3083
|
|
|
2348
3084
|
try {
|
|
3085
|
+
if (preInvokeContextOverflowError != null) {
|
|
3086
|
+
throw preInvokeContextOverflowError;
|
|
3087
|
+
}
|
|
2349
3088
|
result = await withLangfuseRuntimeScope(
|
|
2350
3089
|
resolveLangfuseRuntimeScope({
|
|
2351
3090
|
runLangfuse: this.langfuse,
|
|
@@ -2383,15 +3122,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2383
3122
|
*/
|
|
2384
3123
|
const estimatedPromptTokens = getEstimatedPromptTokens(contextUsage);
|
|
2385
3124
|
|
|
2386
|
-
/**
|
|
2387
|
-
* A previous correction that left the prompt no smaller proves this
|
|
2388
|
-
* state has nothing left to compact — an emptied message list whose
|
|
2389
|
-
* content rides along in an injected summary, for instance. Measuring
|
|
2390
|
-
* that beats trying to predict every such configuration.
|
|
2391
|
-
*/
|
|
2392
|
-
const recoveryStalled = agentContext.overflowRecoveryStalled(
|
|
2393
|
-
estimatedPromptTokens
|
|
2394
|
-
);
|
|
2395
3125
|
const canSummarizeOverflow =
|
|
2396
3126
|
agentContext.summarizationEnabled === true &&
|
|
2397
3127
|
splitAtRecencyBoundary(messages, {
|
|
@@ -2402,13 +3132,36 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2402
3132
|
tokenCounter: agentContext.tokenCounter,
|
|
2403
3133
|
}).head.length > 0;
|
|
2404
3134
|
|
|
3135
|
+
const getLocalProviderOverflowMeasurement = (
|
|
3136
|
+
error: unknown
|
|
3137
|
+
):
|
|
3138
|
+
| {
|
|
3139
|
+
contextBudget: number;
|
|
3140
|
+
estimatedPromptTokens: number;
|
|
3141
|
+
}
|
|
3142
|
+
| undefined =>
|
|
3143
|
+
typeof error === 'object' && error !== null
|
|
3144
|
+
? localProviderOverflowMeasurements.get(error)
|
|
3145
|
+
: undefined;
|
|
3146
|
+
|
|
3147
|
+
const getRecoveryPromptEstimate = (
|
|
3148
|
+
error: unknown,
|
|
3149
|
+
fallbackContext?: FallbackErrorContext
|
|
3150
|
+
): number | undefined => {
|
|
3151
|
+
const resolvedFallbackContext =
|
|
3152
|
+
fallbackContext ?? getFallbackErrorContext(error);
|
|
3153
|
+
return (
|
|
3154
|
+
getLocalProviderOverflowMeasurement(error)?.estimatedPromptTokens ??
|
|
3155
|
+
(resolvedFallbackContext == null
|
|
3156
|
+
? estimatedPromptTokens
|
|
3157
|
+
: undefined)
|
|
3158
|
+
);
|
|
3159
|
+
};
|
|
3160
|
+
|
|
2405
3161
|
const planRecovery = (
|
|
2406
3162
|
error: unknown,
|
|
2407
3163
|
attributedFallbackContext?: FallbackErrorContext
|
|
2408
3164
|
): OverflowRecoveryPlan | null => {
|
|
2409
|
-
if (recoveryStalled) {
|
|
2410
|
-
return null;
|
|
2411
|
-
}
|
|
2412
3165
|
/**
|
|
2413
3166
|
* When the rejection came from a fallback, plan against *that*
|
|
2414
3167
|
* client: its window and output allowance are why it was configured
|
|
@@ -2416,13 +3169,28 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2416
3169
|
*/
|
|
2417
3170
|
const fallbackContext =
|
|
2418
3171
|
attributedFallbackContext ?? getFallbackErrorContext(error);
|
|
3172
|
+
const localMeasurement = getLocalProviderOverflowMeasurement(error);
|
|
3173
|
+
const recoveryPromptEstimate = getRecoveryPromptEstimate(
|
|
3174
|
+
error,
|
|
3175
|
+
fallbackContext
|
|
3176
|
+
);
|
|
3177
|
+
/**
|
|
3178
|
+
* A previous correction that left the rejected prompt no smaller
|
|
3179
|
+
* proves this state has nothing left to compact. Use the fallback
|
|
3180
|
+
* projection when one exists so unlike provider formats are never
|
|
3181
|
+
* compared through the primary's cheaper pre-projection estimate.
|
|
3182
|
+
*/
|
|
3183
|
+
if (agentContext.overflowRecoveryStalled(recoveryPromptEstimate)) {
|
|
3184
|
+
return null;
|
|
3185
|
+
}
|
|
2419
3186
|
const recovery = planContextOverflowRecovery({
|
|
2420
3187
|
error,
|
|
2421
3188
|
provider: fallbackContext?.provider ?? agentContext.provider,
|
|
2422
3189
|
maxContextTokens:
|
|
3190
|
+
localMeasurement?.contextBudget ??
|
|
2423
3191
|
fallbackContext?.maxContextTokens ??
|
|
2424
3192
|
agentContext.maxContextTokens,
|
|
2425
|
-
estimatedPromptTokens,
|
|
3193
|
+
estimatedPromptTokens: recoveryPromptEstimate,
|
|
2426
3194
|
calibrationRatio: agentContext.calibrationRatio,
|
|
2427
3195
|
instructionTokens: agentContext.instructionTokens,
|
|
2428
3196
|
canSummarize: agentContext.summarizationEnabled === true,
|
|
@@ -2440,12 +3208,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2440
3208
|
...recovery,
|
|
2441
3209
|
budgetTokens: minDefined(
|
|
2442
3210
|
getBlindRecoveryBudget(agentContext.maxContextTokens),
|
|
2443
|
-
|
|
2444
|
-
recovery.budgetTokens
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
3211
|
+
localMeasurement != null
|
|
3212
|
+
? recovery.budgetTokens
|
|
3213
|
+
: translateRecoveryBudget(
|
|
3214
|
+
recovery.budgetTokens,
|
|
3215
|
+
recovery.observedCalibrationRatio ??
|
|
3216
|
+
CALIBRATION_RATIO_MAX,
|
|
3217
|
+
agentContext.calibrationRatio
|
|
3218
|
+
)
|
|
2449
3219
|
),
|
|
2450
3220
|
observedCalibrationRatio: undefined,
|
|
2451
3221
|
}
|
|
@@ -2459,16 +3229,17 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2459
3229
|
|
|
2460
3230
|
const recovery = planRecovery(primaryError);
|
|
2461
3231
|
if (recovery != null) {
|
|
3232
|
+
const recoveryPromptEstimate =
|
|
3233
|
+
getRecoveryPromptEstimate(primaryError);
|
|
2462
3234
|
return this.beginOverflowRecovery({
|
|
2463
3235
|
recovery,
|
|
2464
3236
|
agentContext,
|
|
2465
3237
|
agentId,
|
|
2466
3238
|
config,
|
|
2467
3239
|
originalToolContent: prunedOriginalToolContent,
|
|
2468
|
-
estimatedPromptTokens,
|
|
3240
|
+
estimatedPromptTokens: recoveryPromptEstimate,
|
|
2469
3241
|
});
|
|
2470
3242
|
}
|
|
2471
|
-
|
|
2472
3243
|
/**
|
|
2473
3244
|
* A fallback can reject the same prompt as too large even when the
|
|
2474
3245
|
* primary failed for an unrelated reason — a fallback with a smaller
|
|
@@ -2485,7 +3256,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2485
3256
|
tryFallbackProviders({
|
|
2486
3257
|
fallbacks,
|
|
2487
3258
|
tools: agentContext.tools,
|
|
2488
|
-
messages:
|
|
3259
|
+
messages: fallbackBaseMessages,
|
|
2489
3260
|
config: invokeConfig,
|
|
2490
3261
|
primaryError,
|
|
2491
3262
|
context: this,
|
|
@@ -2499,20 +3270,91 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2499
3270
|
estimatedPromptTokens: getEstimatedPromptTokens(contextUsage),
|
|
2500
3271
|
maxContextTokens: agentContext.maxContextTokens,
|
|
2501
3272
|
},
|
|
3273
|
+
prepareProviderMessages: ({
|
|
3274
|
+
model: fallbackModel,
|
|
3275
|
+
messages: fallbackMessages,
|
|
3276
|
+
provider: fallbackProvider,
|
|
3277
|
+
maxContextTokens: fallbackMaxContextTokens,
|
|
3278
|
+
config: fallbackConfig,
|
|
3279
|
+
}) => {
|
|
3280
|
+
const fallbackToolResultChars =
|
|
3281
|
+
agentContext.maxToolResultChars ??
|
|
3282
|
+
calculateMaxToolResultChars(
|
|
3283
|
+
fallbackMaxContextTokens ?? agentContext.maxContextTokens
|
|
3284
|
+
);
|
|
3285
|
+
/**
|
|
3286
|
+
* Serving-provider cue shaping BEFORE the fallback payload
|
|
3287
|
+
* is measured: a Claude fallback behind a tolerant primary
|
|
3288
|
+
* gains the cue inside the guarded projection (a prompt
|
|
3289
|
+
* within the cue's cost of the fallback budget must take
|
|
3290
|
+
* the recovery path, not ship oversized), and a tolerant
|
|
3291
|
+
* fallback behind an Anthropic primary sheds the baked cue
|
|
3292
|
+
* before it is measured against the tighter budget. The
|
|
3293
|
+
* attemptInvoke funnel pass then finds nothing to change.
|
|
3294
|
+
*/
|
|
3295
|
+
const cueShapedFallbackMessages = trackProviderMessageOrigins(
|
|
3296
|
+
fallbackMessages,
|
|
3297
|
+
isAnthropicLike(fallbackProvider, {
|
|
3298
|
+
model: resolveServingModelId(fallbackModel),
|
|
3299
|
+
})
|
|
3300
|
+
? appendPredecessorHandoffCue(fallbackMessages, (m) =>
|
|
3301
|
+
this.isRunProducedMessage(m)
|
|
3302
|
+
)
|
|
3303
|
+
: removePredecessorHandoffCue(fallbackMessages)
|
|
3304
|
+
);
|
|
3305
|
+
const projectedFallbackMessages = trackProviderMessageOrigins(
|
|
3306
|
+
cueShapedFallbackMessages,
|
|
3307
|
+
projectMessagesForProvider({
|
|
3308
|
+
model: fallbackModel,
|
|
3309
|
+
messages: cueShapedFallbackMessages,
|
|
3310
|
+
provider: fallbackProvider,
|
|
3311
|
+
maxToolResultChars: fallbackToolResultChars,
|
|
3312
|
+
callOptions: fallbackConfig,
|
|
3313
|
+
})
|
|
3314
|
+
);
|
|
3315
|
+
const primaryContextBudget = contextUsage?.contextBudget;
|
|
3316
|
+
const fallbackContextBudget =
|
|
3317
|
+
fallbackMaxContextTokens == null
|
|
3318
|
+
? primaryContextBudget
|
|
3319
|
+
: Math.min(
|
|
3320
|
+
primaryContextBudget ?? fallbackMaxContextTokens,
|
|
3321
|
+
fallbackMaxContextTokens
|
|
3322
|
+
);
|
|
3323
|
+
const projection = measureProviderPayload(
|
|
3324
|
+
projectedFallbackMessages,
|
|
3325
|
+
fallbackContextBudget,
|
|
3326
|
+
true
|
|
3327
|
+
);
|
|
3328
|
+
if (!projection.fits) {
|
|
3329
|
+
throw createProviderPayloadOverflowError({
|
|
3330
|
+
projection,
|
|
3331
|
+
provider: fallbackProvider,
|
|
3332
|
+
info: 'Fallback provider message formatting exceeded the context budget before invocation.',
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
return projectedFallbackMessages;
|
|
3336
|
+
},
|
|
2502
3337
|
})
|
|
2503
3338
|
);
|
|
2504
3339
|
} catch (fallbackError) {
|
|
2505
3340
|
const overflowCandidates =
|
|
2506
3341
|
getFallbackOverflowCandidates(fallbackError);
|
|
2507
3342
|
let fallbackRecovery: OverflowRecoveryPlan | null = null;
|
|
3343
|
+
let fallbackRecoveryPromptEstimate: number | undefined;
|
|
2508
3344
|
for (const candidate of overflowCandidates) {
|
|
2509
3345
|
fallbackRecovery = planRecovery(candidate.error, candidate.context);
|
|
2510
3346
|
if (fallbackRecovery != null) {
|
|
3347
|
+
fallbackRecoveryPromptEstimate = getRecoveryPromptEstimate(
|
|
3348
|
+
candidate.error,
|
|
3349
|
+
candidate.context
|
|
3350
|
+
);
|
|
2511
3351
|
break;
|
|
2512
3352
|
}
|
|
2513
3353
|
}
|
|
2514
3354
|
if (overflowCandidates.length === 0) {
|
|
2515
3355
|
fallbackRecovery = planRecovery(fallbackError);
|
|
3356
|
+
fallbackRecoveryPromptEstimate =
|
|
3357
|
+
getRecoveryPromptEstimate(fallbackError);
|
|
2516
3358
|
}
|
|
2517
3359
|
if (fallbackRecovery == null) {
|
|
2518
3360
|
throw fallbackError;
|
|
@@ -2523,7 +3365,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2523
3365
|
agentId,
|
|
2524
3366
|
config,
|
|
2525
3367
|
originalToolContent: prunedOriginalToolContent,
|
|
2526
|
-
estimatedPromptTokens,
|
|
3368
|
+
estimatedPromptTokens: fallbackRecoveryPromptEstimate,
|
|
2527
3369
|
});
|
|
2528
3370
|
}
|
|
2529
3371
|
} finally {
|
|
@@ -2550,6 +3392,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2550
3392
|
* handled everything — both paths become no-ops.
|
|
2551
3393
|
*/
|
|
2552
3394
|
const responseMessage = result.messages?.[0];
|
|
3395
|
+
/**
|
|
3396
|
+
* Provenance for the handoff-cue gate: recorded at the node, where the
|
|
3397
|
+
* produced turn is unambiguous. The public ChatModel contract does not
|
|
3398
|
+
* require implementations to set message ids — the reducer would
|
|
3399
|
+
* assign one AFTER this node returns, which is too late for the set —
|
|
3400
|
+
* so an id is assigned here first, the same way the reducer does it
|
|
3401
|
+
* (`v4()`, mirrored into `lc_kwargs`), and the reducer's
|
|
3402
|
+
* keep-existing-id rule makes the state message match.
|
|
3403
|
+
*/
|
|
3404
|
+
if (responseMessage?.getType() === 'ai') {
|
|
3405
|
+
if (
|
|
3406
|
+
typeof responseMessage.id !== 'string' ||
|
|
3407
|
+
responseMessage.id === ''
|
|
3408
|
+
) {
|
|
3409
|
+
responseMessage.id = v4();
|
|
3410
|
+
responseMessage.lc_kwargs.id = responseMessage.id;
|
|
3411
|
+
}
|
|
3412
|
+
this.runProducedAiMessageIds.add(responseMessage.id);
|
|
3413
|
+
}
|
|
2553
3414
|
const toolCalls = (responseMessage as AIMessageChunk | undefined)
|
|
2554
3415
|
?.tool_calls;
|
|
2555
3416
|
const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
|
|
@@ -2637,8 +3498,20 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2637
3498
|
|
|
2638
3499
|
const invokeElapsed = ((Date.now() - invokeStart) / 1000).toFixed(2);
|
|
2639
3500
|
agentContext.currentUsage = this.getUsageMetadata(result.messages?.[0]);
|
|
3501
|
+
/**
|
|
3502
|
+
* Synthetic usage from a sealed turn is an estimate derived from the
|
|
3503
|
+
* host's own counter, so feeding it to calibration would teach a ratio
|
|
3504
|
+
* of exactly 1.0 — self-consistent by construction, and wrong for any
|
|
3505
|
+
* provider whose real ratio differs. It still flows to `currentUsage`
|
|
3506
|
+
* for host billing; it just does not get to move the EMA.
|
|
3507
|
+
*/
|
|
3508
|
+
const estimatedUsage =
|
|
3509
|
+
(result.messages?.[0] as AIMessageChunk | undefined)?.response_metadata
|
|
3510
|
+
.estimated_usage === true;
|
|
2640
3511
|
if (agentContext.currentUsage) {
|
|
2641
|
-
|
|
3512
|
+
if (!estimatedUsage) {
|
|
3513
|
+
agentContext.updateLastCallUsage(agentContext.currentUsage);
|
|
3514
|
+
}
|
|
2642
3515
|
emitAgentLog(
|
|
2643
3516
|
config,
|
|
2644
3517
|
'debug',
|
|
@@ -2668,11 +3541,184 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2668
3541
|
{ force: true }
|
|
2669
3542
|
);
|
|
2670
3543
|
}
|
|
3544
|
+
if (
|
|
3545
|
+
(responseMessage as AIMessageChunk | undefined)?.response_metadata
|
|
3546
|
+
.preempted === true
|
|
3547
|
+
) {
|
|
3548
|
+
const { messages: injected, preventContinuation } =
|
|
3549
|
+
await this.dispatchPreemptBoundary(agentId, config);
|
|
3550
|
+
/**
|
|
3551
|
+
* Release before branching: the slot is held only for the duration of
|
|
3552
|
+
* the drain, and an early return below must not strand it.
|
|
3553
|
+
*/
|
|
3554
|
+
this.releasePreemptSeal();
|
|
3555
|
+
if (preventContinuation) {
|
|
3556
|
+
/**
|
|
3557
|
+
* A hook halted at the boundary. Commit the sealed turn and anything
|
|
3558
|
+
* it injected, but do NOT self-loop: `preventContinuation` promises
|
|
3559
|
+
* no further model turn, and the run-loop poll in `processStream`
|
|
3560
|
+
* only sees the halt AFTER the next call would already have started
|
|
3561
|
+
* — direct graph consumers never poll it at all. A trailing injected
|
|
3562
|
+
* HumanMessage carries no tool calls, so `toolsCondition` routes it
|
|
3563
|
+
* to END.
|
|
3564
|
+
*/
|
|
3565
|
+
this.preemptIncomplete = true;
|
|
3566
|
+
/**
|
|
3567
|
+
* A halting boundary that ALSO injected nothing is still an empty
|
|
3568
|
+
* boundary by the `getPreemptStats().emptyBoundaries` contract —
|
|
3569
|
+
* hosts use the counter for truncated-seal telemetry, and both
|
|
3570
|
+
* paths end the turn with nothing to resume from.
|
|
3571
|
+
*/
|
|
3572
|
+
if (injected.length === 0) {
|
|
3573
|
+
this.preemptEmptyBoundaries += 1;
|
|
3574
|
+
}
|
|
3575
|
+
this.cleanupSignalListener();
|
|
3576
|
+
return injected.length > 0
|
|
3577
|
+
? { messages: [...(result.messages ?? []), ...injected] }
|
|
3578
|
+
: result;
|
|
3579
|
+
}
|
|
3580
|
+
if (injected.length > 0) {
|
|
3581
|
+
this.pendingPreemptReturn.add(agentId);
|
|
3582
|
+
this.cleanupSignalListener();
|
|
3583
|
+
return { messages: [...(result.messages ?? []), ...injected] };
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Nothing to inject — the host cancelled or already drained. Do NOT
|
|
3587
|
+
* self-loop: a trailing model turn with no new input is dropped by
|
|
3588
|
+
* some Gemini models and read as prefill by Anthropic. Do NOT pretend
|
|
3589
|
+
* the turn completed either; the answer really was cut short.
|
|
3590
|
+
*/
|
|
3591
|
+
this.preemptEmptyBoundaries += 1;
|
|
3592
|
+
this.preemptIncomplete = true;
|
|
3593
|
+
}
|
|
3594
|
+
|
|
2671
3595
|
this.cleanupSignalListener();
|
|
2672
3596
|
return result;
|
|
2673
3597
|
};
|
|
2674
3598
|
}
|
|
2675
3599
|
|
|
3600
|
+
/**
|
|
3601
|
+
* Fires `PreemptBoundary` after a sealed turn and returns whatever the
|
|
3602
|
+
* hooks asked to inject, converted through the same `convertInjectedMessages`
|
|
3603
|
+
* the tool boundary uses so the two sites cannot emit different shapes.
|
|
3604
|
+
*
|
|
3605
|
+
* Never throws: a drain that fails or times out costs the injection, not the
|
|
3606
|
+
* run. The caller treats an empty result as "nothing to resume with".
|
|
3607
|
+
*
|
|
3608
|
+
* `preventContinuation` is surfaced alongside the messages rather than left
|
|
3609
|
+
* to the registry halt signal, which `processStream` only polls between
|
|
3610
|
+
* stream events — by then the self-loop it was meant to prevent has already
|
|
3611
|
+
* issued another model call, and a direct graph consumer never polls it.
|
|
3612
|
+
*/
|
|
3613
|
+
private async dispatchPreemptBoundary(
|
|
3614
|
+
agentId: string,
|
|
3615
|
+
config: RunnableConfig | undefined
|
|
3616
|
+
): Promise<PreemptBoundaryResult> {
|
|
3617
|
+
if (this.hookRegistry == null) {
|
|
3618
|
+
return EMPTY_PREEMPT_BOUNDARY;
|
|
3619
|
+
}
|
|
3620
|
+
const configurable = config?.configurable;
|
|
3621
|
+
const runId = (configurable?.run_id as string | undefined) ?? this.runId;
|
|
3622
|
+
if (runId == null) {
|
|
3623
|
+
return EMPTY_PREEMPT_BOUNDARY;
|
|
3624
|
+
}
|
|
3625
|
+
const result = await executeHooks({
|
|
3626
|
+
registry: this.hookRegistry,
|
|
3627
|
+
input: {
|
|
3628
|
+
hook_event_name: 'PreemptBoundary',
|
|
3629
|
+
runId,
|
|
3630
|
+
threadId: configurable?.thread_id as string | undefined,
|
|
3631
|
+
agentId: this.subagentScope ? agentId : undefined,
|
|
3632
|
+
executingAgentId: agentId,
|
|
3633
|
+
sealCount: this.preemptSealCount,
|
|
3634
|
+
},
|
|
3635
|
+
sessionId: runId,
|
|
3636
|
+
timeoutMs: PREEMPT_BOUNDARY_HOOK_TIMEOUT_MS,
|
|
3637
|
+
/**
|
|
3638
|
+
* The host's own abort signal(s), deliberately NOT `config.signal` —
|
|
3639
|
+
* inside a node the latter is LangGraph's composed signal, which also
|
|
3640
|
+
* fires when an unrelated sibling in the same superstep throws.
|
|
3641
|
+
* Cancellation already returns control in milliseconds without this;
|
|
3642
|
+
* what it buys is that a drain does not keep running after the run it
|
|
3643
|
+
* belongs to died.
|
|
3644
|
+
*
|
|
3645
|
+
* Composed because the host can cancel through either channel: the
|
|
3646
|
+
* construction signal, or the per-call `callerConfig.signal` — the only
|
|
3647
|
+
* one a multi-agent run has, since `MultiAgentGraphConfig` exposes no
|
|
3648
|
+
* construction signal. When both exist they may be different
|
|
3649
|
+
* controllers, and a drain must stop when EITHER fires.
|
|
3650
|
+
*/
|
|
3651
|
+
signal: composeAbortSignals(this.signal, this.callerSignal),
|
|
3652
|
+
}).catch((): undefined => undefined);
|
|
3653
|
+
if (result == null) {
|
|
3654
|
+
return EMPTY_PREEMPT_BOUNDARY;
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* `executeHooks` raises a registry halt whenever a hook returns
|
|
3658
|
+
* `preventContinuation`. That halt has exactly one consumer — the poll in
|
|
3659
|
+
* `Run.processStream` — and its `break` cancels the stream iterator, which
|
|
3660
|
+
* aborts Pregel. The abort lands BEFORE the outer reducer commits
|
|
3661
|
+
* `StandardGraph.messages`, so honoring the halt here would destroy the
|
|
3662
|
+
* sealed assistant turn: the run returns empty content and the host
|
|
3663
|
+
* persists nothing. Measured deterministically — the commit is several
|
|
3664
|
+
* stream events downstream of the point the halt becomes observable.
|
|
3665
|
+
*
|
|
3666
|
+
* The `preventContinuation` branch in `createCallModel` already enforces
|
|
3667
|
+
* the contract locally by declining to self-loop, and a sealed chunk
|
|
3668
|
+
* provably carries no tool calls, so the turn routes to END after exactly
|
|
3669
|
+
* one model call either way. Clearing the halt therefore costs nothing it
|
|
3670
|
+
* was buying and saves the content the seal exists to preserve.
|
|
3671
|
+
*
|
|
3672
|
+
* Scoped to a halt this event raised, so a halt from an earlier hook in
|
|
3673
|
+
* the same run — `haltRun` is first-write-wins — is left alone.
|
|
3674
|
+
*/
|
|
3675
|
+
const halt = this.hookRegistry.getHaltSignal(runId);
|
|
3676
|
+
if (result.preventContinuation === true && halt?.source === 'PreemptBoundary') {
|
|
3677
|
+
this.preemptHaltReason = halt.reason;
|
|
3678
|
+
this.hookRegistry.clearHaltSignal(runId);
|
|
3679
|
+
}
|
|
3680
|
+
const injected: BaseMessage[] = [];
|
|
3681
|
+
/**
|
|
3682
|
+
* `PreemptBoundaryHookOutput` is `BaseHookOutput`, so `additionalContext`
|
|
3683
|
+
* is part of the contract here just as it is at the tool boundary. It has
|
|
3684
|
+
* to be materialized BEFORE the emptiness test, or a hook that returns
|
|
3685
|
+
* context alone would read as "nothing to resume with" and cut the answer
|
|
3686
|
+
* short. Same system-flavored `HumanMessage` convention `ToolNode` uses —
|
|
3687
|
+
* Anthropic and Google reject a mid-conversation `SystemMessage`.
|
|
3688
|
+
*/
|
|
3689
|
+
/**
|
|
3690
|
+
* Whitespace-only entries are dropped for the same reason empty
|
|
3691
|
+
* `injectedMessages` are: `executeHooks` keeps them because their raw
|
|
3692
|
+
* length is nonzero, but a blank turn is not something to resume from —
|
|
3693
|
+
* it costs a model call and strict providers reject it outright.
|
|
3694
|
+
*/
|
|
3695
|
+
const contexts = result.additionalContexts.filter(
|
|
3696
|
+
(context) => context.trim() !== ''
|
|
3697
|
+
);
|
|
3698
|
+
if (contexts.length > 0) {
|
|
3699
|
+
injected.push(
|
|
3700
|
+
new HumanMessage({
|
|
3701
|
+
content: contexts.join('\n\n'),
|
|
3702
|
+
additional_kwargs: { role: 'system', isMeta: true, source: 'hook' },
|
|
3703
|
+
})
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
if (result.injectedMessages.length > 0) {
|
|
3707
|
+
try {
|
|
3708
|
+
injected.push(...convertInjectedMessages(result.injectedMessages));
|
|
3709
|
+
} catch (e) {
|
|
3710
|
+
console.warn(
|
|
3711
|
+
'[StandardGraph] Failed to convert PreemptBoundary injectedMessages:',
|
|
3712
|
+
e instanceof Error ? e.message : e
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
return {
|
|
3717
|
+
messages: injected,
|
|
3718
|
+
preventContinuation: result.preventContinuation === true,
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
|
|
2676
3722
|
createAgentNode(agentId: string): t.CompiledAgentWorfklow {
|
|
2677
3723
|
const getConfig = (): RunnableConfig | undefined => this.config;
|
|
2678
3724
|
const agentContext = this.agentContexts.get(agentId);
|
|
@@ -2836,6 +3882,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2836
3882
|
config?: RunnableConfig
|
|
2837
3883
|
): string => {
|
|
2838
3884
|
this.config = config;
|
|
3885
|
+
/**
|
|
3886
|
+
* A sealed turn that injected messages resumes in the SAME pregel run:
|
|
3887
|
+
* back to the agent node as a new superstep, so the model continues in
|
|
3888
|
+
* one assistant message instead of restarting the graph.
|
|
3889
|
+
*/
|
|
3890
|
+
if (this.pendingPreemptReturn.delete(agentId)) {
|
|
3891
|
+
return agentNode;
|
|
3892
|
+
}
|
|
2839
3893
|
if (state.summarizationRequest != null) {
|
|
2840
3894
|
return summarizeNode;
|
|
2841
3895
|
}
|
|
@@ -3165,6 +4219,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3165
4219
|
}
|
|
3166
4220
|
|
|
3167
4221
|
const { name, input: args, error } = data;
|
|
4222
|
+
const eventValueLimit = calculateMaxToolResultChars();
|
|
4223
|
+
const errorOutputPrefix = 'Error processing tool';
|
|
4224
|
+
const errorDetail =
|
|
4225
|
+
error?.message != null
|
|
4226
|
+
? `: ${serializeToolContentBounded(
|
|
4227
|
+
error.message,
|
|
4228
|
+
Math.max(0, eventValueLimit - errorOutputPrefix.length - 2)
|
|
4229
|
+
)}`
|
|
4230
|
+
: '';
|
|
3168
4231
|
|
|
3169
4232
|
const runStep = graph.getRunStep(stepId);
|
|
3170
4233
|
if (!runStep) {
|
|
@@ -3174,8 +4237,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
3174
4237
|
const tool_call: t.ProcessedToolCall = {
|
|
3175
4238
|
id: data.id,
|
|
3176
4239
|
name: name || '',
|
|
3177
|
-
args:
|
|
3178
|
-
output:
|
|
4240
|
+
args: serializeToolContentBounded(args, eventValueLimit),
|
|
4241
|
+
output: `${errorOutputPrefix}${errorDetail}`,
|
|
3179
4242
|
progress: 1,
|
|
3180
4243
|
};
|
|
3181
4244
|
|