@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/llm/invoke.ts
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import { concat } from '@langchain/core/utils/stream';
|
|
2
2
|
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import {
|
|
4
|
+
CallbackManager,
|
|
5
|
+
CallbackManagerForLLMRun,
|
|
6
|
+
type Callbacks,
|
|
7
|
+
} from '@langchain/core/callbacks/manager';
|
|
8
|
+
import { getCallbackManagerForConfig } from '@langchain/core/runnables';
|
|
9
|
+
import type { Serialized } from '@langchain/core/load/serializable';
|
|
10
|
+
import type { ChatGeneration } from '@langchain/core/outputs';
|
|
3
11
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
4
12
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
5
13
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
6
14
|
import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
|
|
7
15
|
import type { ContextOverflowContext } from '@/utils/errors';
|
|
8
16
|
import type * as t from '@/types';
|
|
17
|
+
import {
|
|
18
|
+
projectCacheControlledToolOutputsToText,
|
|
19
|
+
projectComputerCallOutputsToText,
|
|
20
|
+
projectOpenAIChatToolMessageContent,
|
|
21
|
+
projectOpenAIResponsesToolMessageContent,
|
|
22
|
+
projectOpenRouterToolMessageContent,
|
|
23
|
+
projectSingleTextToolOutputsToText,
|
|
24
|
+
projectStructuredToolOutputsToText,
|
|
25
|
+
projectToolStreamContentForProvider,
|
|
26
|
+
} from '@/messages/core';
|
|
27
|
+
import {
|
|
28
|
+
stripAnthropicCacheControl,
|
|
29
|
+
stripBedrockCacheControl,
|
|
30
|
+
} from '@/messages/cache';
|
|
9
31
|
import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
|
|
10
32
|
import { assertNotTruncatedToolCall } from '@/llm/truncation';
|
|
11
|
-
import { Constants, GraphEvents, Providers } from '@/common';
|
|
33
|
+
import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
|
|
12
34
|
import { manualToolStreamProviders } from '@/llm/providers';
|
|
35
|
+
import { appendCallbacks } from '@/utils/callbacks';
|
|
36
|
+
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
13
37
|
import { getContextOverflowInfo } from '@/utils/errors';
|
|
14
|
-
import {
|
|
15
|
-
|
|
38
|
+
import {
|
|
39
|
+
modifyDeltaProperties,
|
|
40
|
+
coalesceAdjacentUserTurns,
|
|
41
|
+
strictAlternationProviders,
|
|
42
|
+
appendPredecessorHandoffCue,
|
|
43
|
+
removePredecessorHandoffCue,
|
|
44
|
+
} from '@/messages';
|
|
45
|
+
import { canSealPreempt } from '@/llm/preempt';
|
|
46
|
+
import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
|
|
16
47
|
import { initializeModel } from '@/llm/init';
|
|
48
|
+
import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
|
|
17
49
|
|
|
18
50
|
/**
|
|
19
51
|
* Context passed to `attemptInvoke`. Matches the subset of Graph that
|
|
@@ -54,13 +86,169 @@ export type InvokeContext = NonNullable<
|
|
|
54
86
|
*/
|
|
55
87
|
export type OnChunk = (chunk: AIMessageChunk) => void | Promise<void>;
|
|
56
88
|
|
|
89
|
+
export function usesNativeOpenAIResponses(
|
|
90
|
+
model: t.ChatModel,
|
|
91
|
+
provider: Providers,
|
|
92
|
+
callOptions?: unknown
|
|
93
|
+
): boolean {
|
|
94
|
+
if (!isOpenAILike(provider)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
let candidate: unknown = model;
|
|
98
|
+
let effectiveCallOptions = callOptions;
|
|
99
|
+
const seen = new Set<object>();
|
|
100
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
101
|
+
if (candidate == null || typeof candidate !== 'object') {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (seen.has(candidate)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
seen.add(candidate);
|
|
108
|
+
const runnable = candidate as {
|
|
109
|
+
_useResponsesApi?: (options?: unknown) => boolean;
|
|
110
|
+
bound?: unknown;
|
|
111
|
+
defaultOptions?: unknown;
|
|
112
|
+
last?: unknown;
|
|
113
|
+
constructor?: { name?: unknown };
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
if (
|
|
117
|
+
runnable.defaultOptions != null &&
|
|
118
|
+
typeof runnable.defaultOptions === 'object' &&
|
|
119
|
+
!Array.isArray(runnable.defaultOptions) &&
|
|
120
|
+
effectiveCallOptions != null &&
|
|
121
|
+
typeof effectiveCallOptions === 'object' &&
|
|
122
|
+
!Array.isArray(effectiveCallOptions)
|
|
123
|
+
) {
|
|
124
|
+
effectiveCallOptions = {
|
|
125
|
+
...(runnable.defaultOptions as Record<string, unknown>),
|
|
126
|
+
...(effectiveCallOptions as Record<string, unknown>),
|
|
127
|
+
};
|
|
128
|
+
} else if (effectiveCallOptions == null) {
|
|
129
|
+
effectiveCallOptions = runnable.defaultOptions;
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
runnable._useResponsesApi?.(effectiveCallOptions) === true ||
|
|
133
|
+
runnable._useResponsesApi?.(undefined) === true
|
|
134
|
+
) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Continue through RunnableSequence/RunnableBinding wrappers.
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
typeof runnable.constructor?.name === 'string' &&
|
|
142
|
+
runnable.constructor.name.includes('Responses')
|
|
143
|
+
) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (runnable.last != null && typeof runnable.last === 'object') {
|
|
147
|
+
candidate = runnable.last;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (runnable.bound != null && typeof runnable.bound === 'object') {
|
|
151
|
+
candidate = runnable.bound;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Produces the exact provider-facing message representation before a model
|
|
161
|
+
* adapter serializes it. This is shared by invocation and Graph's final budget
|
|
162
|
+
* guard so structured tool output cannot grow after the payload was measured.
|
|
163
|
+
*/
|
|
164
|
+
export function projectMessagesForProvider({
|
|
165
|
+
model,
|
|
166
|
+
messages,
|
|
167
|
+
provider,
|
|
168
|
+
maxToolResultChars,
|
|
169
|
+
callOptions,
|
|
170
|
+
}: {
|
|
171
|
+
model: t.ChatModel;
|
|
172
|
+
messages: BaseMessage[];
|
|
173
|
+
provider: Providers;
|
|
174
|
+
maxToolResultChars?: number;
|
|
175
|
+
callOptions?: unknown;
|
|
176
|
+
}): BaseMessage[] {
|
|
177
|
+
const providerInputMessages = projectToolStreamContentForProvider(messages);
|
|
178
|
+
if (usesNativeOpenAIResponses(model, provider, callOptions)) {
|
|
179
|
+
return projectOpenAIResponsesToolMessageContent(
|
|
180
|
+
stripAnthropicCacheControl(
|
|
181
|
+
stripBedrockCacheControl(providerInputMessages)
|
|
182
|
+
),
|
|
183
|
+
maxToolResultChars
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (provider === Providers.OPENROUTER) {
|
|
187
|
+
return projectComputerCallOutputsToText(
|
|
188
|
+
projectOpenRouterToolMessageContent(
|
|
189
|
+
stripBedrockCacheControl(providerInputMessages),
|
|
190
|
+
maxToolResultChars
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (isOpenAILike(provider)) {
|
|
195
|
+
return projectComputerCallOutputsToText(
|
|
196
|
+
projectOpenAIChatToolMessageContent(
|
|
197
|
+
stripAnthropicCacheControl(
|
|
198
|
+
stripBedrockCacheControl(providerInputMessages)
|
|
199
|
+
),
|
|
200
|
+
maxToolResultChars
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (provider === Providers.ANTHROPIC) {
|
|
205
|
+
return projectComputerCallOutputsToText(
|
|
206
|
+
projectSingleTextToolOutputsToText(
|
|
207
|
+
stripBedrockCacheControl(providerInputMessages),
|
|
208
|
+
maxToolResultChars
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (provider === Providers.BEDROCK) {
|
|
213
|
+
return stripAnthropicCacheControl(
|
|
214
|
+
projectComputerCallOutputsToText(
|
|
215
|
+
projectCacheControlledToolOutputsToText(
|
|
216
|
+
providerInputMessages,
|
|
217
|
+
maxToolResultChars
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return projectComputerCallOutputsToText(
|
|
223
|
+
projectStructuredToolOutputsToText(
|
|
224
|
+
projectSingleTextToolOutputsToText(
|
|
225
|
+
stripAnthropicCacheControl(
|
|
226
|
+
stripBedrockCacheControl(providerInputMessages)
|
|
227
|
+
),
|
|
228
|
+
maxToolResultChars
|
|
229
|
+
),
|
|
230
|
+
maxToolResultChars
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The registered handler that owns content-part dispatch, if any.
|
|
237
|
+
*
|
|
238
|
+
* Detected by brand rather than by `instanceof`: a host that registers
|
|
239
|
+
* `new ChatModelStreamHandler()` to opt out of sealing gets wrapped by
|
|
240
|
+
* `createRunHandlers` on every `AgentSession` run, and by
|
|
241
|
+
* `composeEventHandlers` on a key collision. Both wrappers forward to the same
|
|
242
|
+
* dispatcher while failing an identity check, so an identity test would
|
|
243
|
+
* silently revoke the opt-out documented on `StreamPreemption`.
|
|
244
|
+
*/
|
|
57
245
|
function getRegisteredDefaultChatStreamHandler(
|
|
58
246
|
context?: InvokeContext
|
|
59
|
-
):
|
|
247
|
+
): t.EventHandler | undefined {
|
|
60
248
|
const handler = context?.handlerRegistry?.getHandler(
|
|
61
249
|
GraphEvents.CHAT_MODEL_STREAM
|
|
62
250
|
);
|
|
63
|
-
return handler
|
|
251
|
+
return dispatchesChatModelStream(handler) ? handler : undefined;
|
|
64
252
|
}
|
|
65
253
|
|
|
66
254
|
function hasReasoningDetails(chunk: AIMessageChunk): boolean {
|
|
@@ -157,6 +345,280 @@ function getStreamHandlingChunk({
|
|
|
157
345
|
);
|
|
158
346
|
}
|
|
159
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Best-effort output-token count for a sealed turn, used only when the
|
|
350
|
+
* provider never got to send its usage chunk.
|
|
351
|
+
*/
|
|
352
|
+
function countSealedTokens(
|
|
353
|
+
context: InvokeContext | undefined,
|
|
354
|
+
metadata: Record<string, unknown> | undefined,
|
|
355
|
+
messages: BaseMessage[]
|
|
356
|
+
): number | undefined {
|
|
357
|
+
try {
|
|
358
|
+
const counter = context?.getAgentContext(metadata).tokenCounter;
|
|
359
|
+
if (counter == null) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
let total = 0;
|
|
363
|
+
for (const message of messages) {
|
|
364
|
+
total += counter(message);
|
|
365
|
+
}
|
|
366
|
+
return total;
|
|
367
|
+
} catch {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Instruction overhead the provider processed but that never appears in the
|
|
374
|
+
* message array: `createCallModel` pipes the model through
|
|
375
|
+
* `agentContext.systemRunnable` and binds tool schemas AFTER `messages` is
|
|
376
|
+
* formed, so the system prompt, dynamic instructions, summary and tool
|
|
377
|
+
* schemas are all billed yet invisible here.
|
|
378
|
+
*
|
|
379
|
+
* Read per-node via `getAgentContext(metadata)` rather than the graph-level
|
|
380
|
+
* accessor, which is hardcoded to `defaultAgentId` and would report the wrong
|
|
381
|
+
* agent's overhead in a `MultiAgentGraph`.
|
|
382
|
+
*/
|
|
383
|
+
function sealedInstructionOverhead(
|
|
384
|
+
context: InvokeContext | undefined,
|
|
385
|
+
metadata: Record<string, unknown> | undefined
|
|
386
|
+
): number {
|
|
387
|
+
try {
|
|
388
|
+
const agentContext = context?.getAgentContext(metadata);
|
|
389
|
+
return (
|
|
390
|
+
agentContext?.resolvedInstructionOverhead ??
|
|
391
|
+
agentContext?.instructionTokens ??
|
|
392
|
+
0
|
|
393
|
+
);
|
|
394
|
+
} catch {
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Best-effort usage for a turn the provider never got to bill us for.
|
|
401
|
+
*
|
|
402
|
+
* The prompt matters as much as the completion: the provider processed the
|
|
403
|
+
* ENTIRE prompt — messages plus instruction overhead — before we sealed, and
|
|
404
|
+
* every resume re-sends it, so under-counting input hides the expensive half
|
|
405
|
+
* of a preempted run.
|
|
406
|
+
*
|
|
407
|
+
* ESTIMATE, NOT MEASUREMENT. Messages are counted with the host's tokenizer
|
|
408
|
+
* rather than the provider's, and `toolSchemaTokens` applies a heuristic
|
|
409
|
+
* multiplier. It is also an over-count on the fallback path, where
|
|
410
|
+
* `tryFallbackProviders` builds a bare model with no `systemRunnable` pipe so
|
|
411
|
+
* the system prompt genuinely is not sent. Accepted rather than threaded
|
|
412
|
+
* through a flag: only the fallback-plus-seal combination is affected, and an
|
|
413
|
+
* over-count is safer than the previous fabricated `input_tokens: 0`.
|
|
414
|
+
*
|
|
415
|
+
* Marked `estimated_usage` so calibration can refuse to learn from it — a
|
|
416
|
+
* ratio derived from the same counter that produced the estimate is
|
|
417
|
+
* self-consistent by construction and would drag a provider's real
|
|
418
|
+
* calibration toward 1.0.
|
|
419
|
+
*/
|
|
420
|
+
function synthesizeSealedUsage(
|
|
421
|
+
context: InvokeContext | undefined,
|
|
422
|
+
chunk: AIMessageChunk,
|
|
423
|
+
prompt: BaseMessage[],
|
|
424
|
+
metadata: Record<string, unknown> | undefined
|
|
425
|
+
): void {
|
|
426
|
+
if (chunk.usage_metadata != null) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const outputTokens = countSealedTokens(context, metadata, [chunk]);
|
|
430
|
+
if (outputTokens == null) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const inputTokens =
|
|
434
|
+
(countSealedTokens(context, metadata, prompt) ?? 0) +
|
|
435
|
+
sealedInstructionOverhead(context, metadata);
|
|
436
|
+
chunk.usage_metadata = {
|
|
437
|
+
input_tokens: inputTokens,
|
|
438
|
+
output_tokens: outputTokens,
|
|
439
|
+
total_tokens: inputTokens + outputTokens,
|
|
440
|
+
};
|
|
441
|
+
chunk.response_metadata = {
|
|
442
|
+
...chunk.response_metadata,
|
|
443
|
+
estimated_usage: true,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function getMessageText(chunk: AIMessageChunk): string {
|
|
448
|
+
if (typeof chunk.content === 'string') {
|
|
449
|
+
return chunk.content;
|
|
450
|
+
}
|
|
451
|
+
let text = '';
|
|
452
|
+
for (const block of chunk.content) {
|
|
453
|
+
if (block.type === ContentTypes.TEXT) {
|
|
454
|
+
const value = block[ContentTypes.TEXT];
|
|
455
|
+
if (typeof value === 'string') {
|
|
456
|
+
text += value;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return text;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Ends the real model run for a turn that was sealed mid-stream.
|
|
465
|
+
*
|
|
466
|
+
* Mandatory, not cosmetic. `@langchain/core`'s `_streamIterator` calls
|
|
467
|
+
* `handleLLMEnd` after its try/catch with no `finally`, so breaking out of the
|
|
468
|
+
* consumer's `for await` produces a *return* completion that fires neither
|
|
469
|
+
* `handleLLMError` nor `handleLLMEnd`. The run would stay open in every
|
|
470
|
+
* callback handler: the host records no usage — and since each seal re-sends
|
|
471
|
+
* the whole prompt, N preemptions cost N unrecorded prompts — while LangSmith
|
|
472
|
+
* and Langfuse hold a span that never closes.
|
|
473
|
+
*
|
|
474
|
+
* `runId` cannot be dictated from here (the bound runnable consumes
|
|
475
|
+
* `config.runId` for its own run and hands the chat model a fresh one), but it
|
|
476
|
+
* can be OBSERVED: the capture handler installed at the `model.stream` call
|
|
477
|
+
* records it from `handleChatModelStart`, which fires before the first chunk.
|
|
478
|
+
* Rebuilding the manager against that id closes the real run, and the host's
|
|
479
|
+
* `on_chat_model_end` then arrives through the ordinary `streamEvents` path.
|
|
480
|
+
*
|
|
481
|
+
* Falls back to a custom-event dispatch if the id was never observed, so the
|
|
482
|
+
* host still records usage even when the native close is unavailable.
|
|
483
|
+
*/
|
|
484
|
+
/**
|
|
485
|
+
* Every callbacks source the real model run would compose beyond the per-call
|
|
486
|
+
* config. `model` here is whatever `createCallModel` produced — with tools
|
|
487
|
+
* that is `bindTools(...)`'s `RunnableBinding`, and a system runnable pipes a
|
|
488
|
+
* `RunnableSequence` on top — while `clientOptions.callbacks` lives on the
|
|
489
|
+
* chat model at the BOTTOM of that stack. Walks `bound` (bindings) and
|
|
490
|
+
* `last`/`steps` (sequences), collecting each wrapper's own `callbacks` and
|
|
491
|
+
* any binding-config callbacks along the way, since the binding merges its
|
|
492
|
+
* config into the call before the chat model composes.
|
|
493
|
+
*/
|
|
494
|
+
function collectModelCallbackSources(model: unknown): Callbacks[] {
|
|
495
|
+
const sources: Callbacks[] = [];
|
|
496
|
+
const seen = new Set<unknown>();
|
|
497
|
+
let current: unknown = model;
|
|
498
|
+
while (current != null && typeof current === 'object' && !seen.has(current)) {
|
|
499
|
+
seen.add(current);
|
|
500
|
+
const wrapper = current as {
|
|
501
|
+
callbacks?: Callbacks;
|
|
502
|
+
config?: { callbacks?: Callbacks };
|
|
503
|
+
bound?: unknown;
|
|
504
|
+
last?: unknown;
|
|
505
|
+
steps?: unknown[];
|
|
506
|
+
};
|
|
507
|
+
if (wrapper.callbacks != null) {
|
|
508
|
+
sources.push(wrapper.callbacks);
|
|
509
|
+
}
|
|
510
|
+
if (wrapper.config?.callbacks != null) {
|
|
511
|
+
sources.push(wrapper.config.callbacks);
|
|
512
|
+
}
|
|
513
|
+
current =
|
|
514
|
+
wrapper.bound ??
|
|
515
|
+
wrapper.last ??
|
|
516
|
+
(Array.isArray(wrapper.steps)
|
|
517
|
+
? wrapper.steps[wrapper.steps.length - 1]
|
|
518
|
+
: undefined);
|
|
519
|
+
}
|
|
520
|
+
return sources;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The serving model's id, read through the same wrapper stack
|
|
525
|
+
* `collectModelCallbackSources` walks — `bindTools` returns a
|
|
526
|
+
* `RunnableBinding` and a system runnable pipes a `RunnableSequence`, and
|
|
527
|
+
* neither exposes the chat model's `model` at the top level.
|
|
528
|
+
*/
|
|
529
|
+
export function resolveServingModelId(model: unknown): string | undefined {
|
|
530
|
+
const seen = new Set<unknown>();
|
|
531
|
+
let current: unknown = model;
|
|
532
|
+
while (current != null && typeof current === 'object' && !seen.has(current)) {
|
|
533
|
+
seen.add(current);
|
|
534
|
+
const wrapper = current as {
|
|
535
|
+
model?: unknown;
|
|
536
|
+
bound?: unknown;
|
|
537
|
+
last?: unknown;
|
|
538
|
+
steps?: unknown[];
|
|
539
|
+
};
|
|
540
|
+
if (typeof wrapper.model === 'string' && wrapper.model !== '') {
|
|
541
|
+
return wrapper.model;
|
|
542
|
+
}
|
|
543
|
+
current =
|
|
544
|
+
wrapper.bound ??
|
|
545
|
+
wrapper.last ??
|
|
546
|
+
(Array.isArray(wrapper.steps)
|
|
547
|
+
? wrapper.steps[wrapper.steps.length - 1]
|
|
548
|
+
: undefined);
|
|
549
|
+
}
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function endSealedModelRun(
|
|
554
|
+
context: InvokeContext | undefined,
|
|
555
|
+
chunk: AIMessageChunk,
|
|
556
|
+
prompt: BaseMessage[],
|
|
557
|
+
llmRunId: string | undefined,
|
|
558
|
+
config?: RunnableConfig,
|
|
559
|
+
model?: t.ChatModel
|
|
560
|
+
): Promise<void> {
|
|
561
|
+
const metadata = config?.metadata as Record<string, unknown> | undefined;
|
|
562
|
+
synthesizeSealedUsage(context, chunk, prompt, metadata);
|
|
563
|
+
if (llmRunId != null) {
|
|
564
|
+
try {
|
|
565
|
+
let callbackManager = await getCallbackManagerForConfig(config);
|
|
566
|
+
/**
|
|
567
|
+
* The real model run composes the per-call config's callbacks WITH the
|
|
568
|
+
* model's own (`CallbackManager.configure(config.callbacks,
|
|
569
|
+
* this.callbacks, …)` in `@langchain/core`'s base chat model), so a
|
|
570
|
+
* handler supplied via `clientOptions.callbacks` received
|
|
571
|
+
* `handleChatModelStart` for this run. Rebuilding from the config alone
|
|
572
|
+
* would close the run for every handler EXCEPT those — leaving their
|
|
573
|
+
* span open forever. Composed the same way the real run composes:
|
|
574
|
+
* model callbacks appended non-inheritable, parent run id preserved by
|
|
575
|
+
* `copy`, tracers deduped by `configure`.
|
|
576
|
+
*/
|
|
577
|
+
for (const source of collectModelCallbackSources(model)) {
|
|
578
|
+
callbackManager =
|
|
579
|
+
CallbackManager.configure(callbackManager ?? undefined, source) ??
|
|
580
|
+
callbackManager;
|
|
581
|
+
}
|
|
582
|
+
if (callbackManager != null) {
|
|
583
|
+
const runManager = new CallbackManagerForLLMRun(
|
|
584
|
+
llmRunId,
|
|
585
|
+
callbackManager.handlers,
|
|
586
|
+
callbackManager.inheritableHandlers,
|
|
587
|
+
callbackManager.tags,
|
|
588
|
+
callbackManager.inheritableTags,
|
|
589
|
+
callbackManager.metadata,
|
|
590
|
+
callbackManager.inheritableMetadata,
|
|
591
|
+
callbackManager.getParentRunId()
|
|
592
|
+
);
|
|
593
|
+
const generation: ChatGeneration = {
|
|
594
|
+
text: getMessageText(chunk),
|
|
595
|
+
message: chunk,
|
|
596
|
+
};
|
|
597
|
+
await runManager.handleLLMEnd({
|
|
598
|
+
generations: [[generation]],
|
|
599
|
+
llmOutput: {},
|
|
600
|
+
});
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
} catch (e) {
|
|
604
|
+
/**
|
|
605
|
+
* A sealed answer that reaches the user is worth more than a tidy
|
|
606
|
+
* trace. Fall through to the custom event rather than failing the run.
|
|
607
|
+
*/
|
|
608
|
+
// eslint-disable-next-line no-console
|
|
609
|
+
console.warn(
|
|
610
|
+
'[attemptInvoke] Native close of the sealed model run failed; falling back to a custom event:',
|
|
611
|
+
e instanceof Error ? e.message : e
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
await safeDispatchCustomEvent(
|
|
616
|
+
GraphEvents.CHAT_MODEL_END,
|
|
617
|
+
{ output: chunk },
|
|
618
|
+
config
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
160
622
|
function appendStreamChunk({
|
|
161
623
|
current,
|
|
162
624
|
next,
|
|
@@ -207,9 +669,66 @@ export async function attemptInvoke(
|
|
|
207
669
|
* untouched so the graph state never sees `[ref: …]` / `_ref`
|
|
208
670
|
* payload.
|
|
209
671
|
*/
|
|
672
|
+
const invocationMessages = projectMessagesForProvider({
|
|
673
|
+
model,
|
|
674
|
+
messages,
|
|
675
|
+
provider,
|
|
676
|
+
callOptions: config,
|
|
677
|
+
});
|
|
210
678
|
const registry = context?.getOrCreateToolOutputRegistry();
|
|
211
679
|
const runId = config?.configurable?.run_id as string | undefined;
|
|
212
|
-
const
|
|
680
|
+
const annotated = annotateMessagesForLLM(
|
|
681
|
+
invocationMessages,
|
|
682
|
+
registry,
|
|
683
|
+
runId
|
|
684
|
+
);
|
|
685
|
+
/**
|
|
686
|
+
* Keyed on the provider ACTUALLY serving this call, not the agent's primary.
|
|
687
|
+
* `createCallModel` normalizes for the primary, but `tryFallbackProviders`
|
|
688
|
+
* re-sends the same array — so an OpenAI primary that fails after a boundary
|
|
689
|
+
* injected two human turns would hand a Bedrock or Mistral fallback the
|
|
690
|
+
* consecutive user turns those APIs reject, and the recovery request would
|
|
691
|
+
* fail for a reason unrelated to the original failure.
|
|
692
|
+
*
|
|
693
|
+
* `attemptInvoke` is the single funnel for primary, fallback and
|
|
694
|
+
* summarization calls, so applying it here covers all three. Idempotent, so
|
|
695
|
+
* the primary simply re-runs a no-op over already-coalesced messages.
|
|
696
|
+
*/
|
|
697
|
+
/**
|
|
698
|
+
* Serving-provider re-keying for the predecessor handoff cue (#345). The
|
|
699
|
+
* PRIMARY's cue is baked in createCallModel's measured transform stage —
|
|
700
|
+
* appending after measurement could push a just-fits prompt over budget —
|
|
701
|
+
* so this funnel only corrects for fallbacks crossing provider families:
|
|
702
|
+
* a tolerant primary falling back to a Claude surface gains the cue here,
|
|
703
|
+
* and an Anthropic primary falling back to OpenAI/Mistral/Nova has the
|
|
704
|
+
* Claude-only synthetic turn stripped. Both helpers are identity on their
|
|
705
|
+
* no-op paths, so the primary's own pass re-runs for free.
|
|
706
|
+
*
|
|
707
|
+
* The serving model id is read through the wrapper stack (`bindTools`'
|
|
708
|
+
* binding, a system runnable's sequence) — a wrapper's top-level `.model`
|
|
709
|
+
* is undefined, and `isAnthropicLike` would otherwise default a wrapped
|
|
710
|
+
* Bedrock-Nova model to Claude. The context cast is widened deliberately:
|
|
711
|
+
* the type says every context is a full Graph, but summarization passes
|
|
712
|
+
* none and long-standing tests pass partial stubs.
|
|
713
|
+
*/
|
|
714
|
+
const isRunProduced = (
|
|
715
|
+
context as
|
|
716
|
+
| { isRunProducedMessage?: (message: BaseMessage) => boolean }
|
|
717
|
+
| undefined
|
|
718
|
+
)?.isRunProducedMessage;
|
|
719
|
+
const cued = isAnthropicLike(provider, {
|
|
720
|
+
model: resolveServingModelId(model),
|
|
721
|
+
})
|
|
722
|
+
? appendPredecessorHandoffCue(
|
|
723
|
+
annotated,
|
|
724
|
+
isRunProduced == null
|
|
725
|
+
? undefined
|
|
726
|
+
: (message): boolean => isRunProduced.call(context, message)
|
|
727
|
+
)
|
|
728
|
+
: removePredecessorHandoffCue(annotated);
|
|
729
|
+
const messagesForProvider = strictAlternationProviders.has(provider)
|
|
730
|
+
? coalesceAdjacentUserTurns(cued)
|
|
731
|
+
: cued;
|
|
213
732
|
|
|
214
733
|
/**
|
|
215
734
|
* Stamp the provider that is ACTUALLY serving this invocation onto the
|
|
@@ -229,8 +748,34 @@ export async function attemptInvoke(
|
|
|
229
748
|
};
|
|
230
749
|
|
|
231
750
|
if (model.stream) {
|
|
232
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Observed, not dictated. `handleChatModelStart` fires with the chat
|
|
753
|
+
* model's real run id before the first chunk, which is the only way to
|
|
754
|
+
* name the run a seal has to close — pinning `config.runId` does not
|
|
755
|
+
* survive the bound runnable. Installed only when preemption is
|
|
756
|
+
* configured, so a run that cannot seal carries no extra handler.
|
|
757
|
+
*/
|
|
758
|
+
let sealedRunId: string | undefined;
|
|
759
|
+
const streamConfig =
|
|
760
|
+
context?.preemption == null
|
|
761
|
+
? config
|
|
762
|
+
: {
|
|
763
|
+
...config,
|
|
764
|
+
callbacks: appendCallbacks(config.callbacks, [
|
|
765
|
+
{
|
|
766
|
+
handleChatModelStart: (
|
|
767
|
+
_llm: Serialized,
|
|
768
|
+
_messages: BaseMessage[][],
|
|
769
|
+
runId: string
|
|
770
|
+
): void => {
|
|
771
|
+
sealedRunId ??= runId;
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
]),
|
|
775
|
+
};
|
|
776
|
+
const stream = await model.stream(messagesForProvider, streamConfig);
|
|
233
777
|
let finalChunk: AIMessageChunk | undefined;
|
|
778
|
+
let preempted = false;
|
|
234
779
|
const registeredStreamHandler =
|
|
235
780
|
getRegisteredDefaultChatStreamHandler(context);
|
|
236
781
|
|
|
@@ -265,6 +810,26 @@ export async function attemptInvoke(
|
|
|
265
810
|
next: chunk,
|
|
266
811
|
provider,
|
|
267
812
|
});
|
|
813
|
+
/**
|
|
814
|
+
* Only this loop may seal. The registered-handler branch below
|
|
815
|
+
* dispatches through `run.ts`'s decoupled `streamEvents` consumer,
|
|
816
|
+
* which can lag the accumulated chunk — sealing there would let the
|
|
817
|
+
* host index a content part the user has not been shown yet.
|
|
818
|
+
*/
|
|
819
|
+
/**
|
|
820
|
+
* Cheap poll first, shape check second, budget claim last. The claim
|
|
821
|
+
* is what makes this safe under a parallel `MultiAgentGraph`: several
|
|
822
|
+
* agents share one graph and can each see the poll as true, but only
|
|
823
|
+
* one can take the slot, and a chunk that cannot seal never spends it.
|
|
824
|
+
*/
|
|
825
|
+
if (
|
|
826
|
+
context?.shouldPreemptStream() === true &&
|
|
827
|
+
canSealPreempt(finalChunk) &&
|
|
828
|
+
context.claimPreemptSeal()
|
|
829
|
+
) {
|
|
830
|
+
preempted = true;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
268
833
|
}
|
|
269
834
|
} else {
|
|
270
835
|
const metadata = config.metadata as Record<string, unknown> | undefined;
|
|
@@ -294,6 +859,21 @@ export async function attemptInvoke(
|
|
|
294
859
|
finalChunk = modifyDeltaProperties(provider, finalChunk);
|
|
295
860
|
}
|
|
296
861
|
|
|
862
|
+
if (preempted && finalChunk != null) {
|
|
863
|
+
finalChunk.response_metadata = {
|
|
864
|
+
...finalChunk.response_metadata,
|
|
865
|
+
preempted: true,
|
|
866
|
+
};
|
|
867
|
+
await endSealedModelRun(
|
|
868
|
+
context,
|
|
869
|
+
finalChunk,
|
|
870
|
+
messagesForProvider,
|
|
871
|
+
sealedRunId,
|
|
872
|
+
config,
|
|
873
|
+
model
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
297
877
|
if ((finalChunk?.tool_calls?.length ?? 0) > 0) {
|
|
298
878
|
finalChunk!.tool_calls = finalChunk!.tool_calls?.filter(
|
|
299
879
|
(tool_call: ToolCall) => !!tool_call.name
|
|
@@ -404,6 +984,7 @@ export async function tryFallbackProviders({
|
|
|
404
984
|
context,
|
|
405
985
|
onChunk,
|
|
406
986
|
overflowContext,
|
|
987
|
+
prepareProviderMessages,
|
|
407
988
|
}: {
|
|
408
989
|
fallbacks: t.FallbackConfig[];
|
|
409
990
|
tools?: t.GraphTools;
|
|
@@ -419,6 +1000,19 @@ export async function tryFallbackProviders({
|
|
|
419
1000
|
* be dropped in favour of whichever failure came last.
|
|
420
1001
|
*/
|
|
421
1002
|
overflowContext?: ContextOverflowContext;
|
|
1003
|
+
/**
|
|
1004
|
+
* Optional final payload guard used by Graph. It receives the initialized,
|
|
1005
|
+
* tool-bound fallback model so Responses-vs-Chat projection is exact before
|
|
1006
|
+
* the fallback request is measured and sent.
|
|
1007
|
+
*/
|
|
1008
|
+
prepareProviderMessages?: (input: {
|
|
1009
|
+
model: t.ChatModel;
|
|
1010
|
+
messages: BaseMessage[];
|
|
1011
|
+
provider: Providers;
|
|
1012
|
+
clientOptions?: t.ClientOptions;
|
|
1013
|
+
maxContextTokens?: number;
|
|
1014
|
+
config?: RunnableConfig;
|
|
1015
|
+
}) => BaseMessage[] | Promise<BaseMessage[]>;
|
|
422
1016
|
}): Promise<Partial<t.BaseGraphState> | undefined> {
|
|
423
1017
|
const isOverflow = (
|
|
424
1018
|
error: unknown,
|
|
@@ -460,10 +1054,19 @@ export async function tryFallbackProviders({
|
|
|
460
1054
|
[Constants.INVOKED_MODEL]: fbModelName,
|
|
461
1055
|
},
|
|
462
1056
|
};
|
|
1057
|
+
const fallbackMessages =
|
|
1058
|
+
(await prepareProviderMessages?.({
|
|
1059
|
+
model: fbModel as t.ChatModel,
|
|
1060
|
+
messages,
|
|
1061
|
+
provider: fb.provider,
|
|
1062
|
+
clientOptions: fb.clientOptions,
|
|
1063
|
+
maxContextTokens: fb.maxContextTokens,
|
|
1064
|
+
config: fbConfig,
|
|
1065
|
+
})) ?? messages;
|
|
463
1066
|
const result = await attemptInvoke(
|
|
464
1067
|
{
|
|
465
1068
|
model: fbModel as t.ChatModel,
|
|
466
|
-
messages,
|
|
1069
|
+
messages: fallbackMessages,
|
|
467
1070
|
provider: fb.provider,
|
|
468
1071
|
context,
|
|
469
1072
|
onChunk,
|