@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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// src/specs/preemptSeal.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* End-to-end cooperative seal flow through a real `Run`: fake model streams,
|
|
4
|
+
* the host requests a preempt, the stream seals at a safe chunk, the
|
|
5
|
+
* `PreemptBoundary` drain decides what happens next. Everything below runs
|
|
6
|
+
* the dispatch-synchronous loop in `attemptInvoke` (no registered
|
|
7
|
+
* CHAT_MODEL_STREAM handler), which is the only loop allowed to seal.
|
|
8
|
+
*/
|
|
9
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
10
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
11
|
+
import { RunnableBinding } from '@langchain/core/runnables';
|
|
12
|
+
import type * as t from '@/types';
|
|
13
|
+
import { Providers } from '@/common';
|
|
14
|
+
import { HookRegistry } from '@/hooks/HookRegistry';
|
|
15
|
+
import type { HookCallback } from '@/hooks/types';
|
|
16
|
+
import { FakeChatModel } from '@/llm/fake';
|
|
17
|
+
import { Run } from '@/run';
|
|
18
|
+
|
|
19
|
+
const FULL_RESPONSE = 'Alpha beta gamma delta epsilon zeta';
|
|
20
|
+
const RESUMED_RESPONSE = 'Continuing after the steer.';
|
|
21
|
+
|
|
22
|
+
const streamConfig = {
|
|
23
|
+
configurable: { thread_id: 'preempt-seal-e2e' },
|
|
24
|
+
streamMode: 'values' as const,
|
|
25
|
+
version: 'v2' as const,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function createSealRun(options: {
|
|
29
|
+
runId: string;
|
|
30
|
+
hook: HookCallback<'PreemptBoundary'>;
|
|
31
|
+
responses: string[];
|
|
32
|
+
stopHook?: HookCallback<'Stop'>;
|
|
33
|
+
modelCallbacks?: FakeChatModel['callbacks'];
|
|
34
|
+
}): Promise<Run<t.IState>> {
|
|
35
|
+
const registry = new HookRegistry();
|
|
36
|
+
registry.register('PreemptBoundary', { hooks: [options.hook] });
|
|
37
|
+
if (options.stopHook) {
|
|
38
|
+
registry.register('Stop', { hooks: [options.stopHook] });
|
|
39
|
+
}
|
|
40
|
+
const run = await Run.create<t.IState>({
|
|
41
|
+
runId: options.runId,
|
|
42
|
+
graphConfig: {
|
|
43
|
+
type: 'standard',
|
|
44
|
+
llmConfig: {
|
|
45
|
+
provider: Providers.OPENAI,
|
|
46
|
+
model: 'gpt-4o-mini',
|
|
47
|
+
apiKey: 'test-key',
|
|
48
|
+
},
|
|
49
|
+
instructions: 'Answer plainly.',
|
|
50
|
+
},
|
|
51
|
+
hooks: registry,
|
|
52
|
+
preemption: { shouldPreempt: () => true, maxSeals: 1 },
|
|
53
|
+
returnContent: true,
|
|
54
|
+
skipCleanup: true,
|
|
55
|
+
});
|
|
56
|
+
if (!run.Graph) {
|
|
57
|
+
throw new Error('Expected graph to be initialized');
|
|
58
|
+
}
|
|
59
|
+
const model = new FakeChatModel({
|
|
60
|
+
responses: options.responses,
|
|
61
|
+
});
|
|
62
|
+
if (options.modelCallbacks != null) {
|
|
63
|
+
model.callbacks = options.modelCallbacks;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wrapped, not bare, when the test watches model-level callbacks: with
|
|
67
|
+
* tools bound, production hands `attemptInvoke` a `RunnableBinding` (and a
|
|
68
|
+
* system runnable pipes a sequence on top), while `clientOptions.callbacks`
|
|
69
|
+
* lives on the chat model at the bottom. A bare override would let a
|
|
70
|
+
* naive `model.callbacks` property read pass the detector while missing
|
|
71
|
+
* every real tool-enabled run.
|
|
72
|
+
*/
|
|
73
|
+
run.Graph.overrideModel = (
|
|
74
|
+
options.modelCallbacks != null
|
|
75
|
+
? new RunnableBinding({ bound: model, kwargs: {}, config: {} })
|
|
76
|
+
: model
|
|
77
|
+
) as typeof model;
|
|
78
|
+
return run;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class CountingChatModel extends FakeChatModel {
|
|
82
|
+
invocations = 0;
|
|
83
|
+
|
|
84
|
+
override async *_streamResponseChunks(
|
|
85
|
+
...args: Parameters<FakeChatModel['_streamResponseChunks']>
|
|
86
|
+
): ReturnType<FakeChatModel['_streamResponseChunks']> {
|
|
87
|
+
this.invocations += 1;
|
|
88
|
+
yield* super._streamResponseChunks(...args);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const aiContents = (messages: BaseMessage[]): string[] =>
|
|
93
|
+
messages
|
|
94
|
+
.filter((message) => message.getType() === 'ai')
|
|
95
|
+
.map((message) =>
|
|
96
|
+
typeof message.content === 'string'
|
|
97
|
+
? message.content
|
|
98
|
+
: JSON.stringify(message.content)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
describe('cooperative seal (end-to-end via Run)', () => {
|
|
102
|
+
jest.setTimeout(15000);
|
|
103
|
+
|
|
104
|
+
it('surfaces an empty boundary as preempt_incomplete instead of a natural finish', async () => {
|
|
105
|
+
const run = await createSealRun({
|
|
106
|
+
runId: 'seal-empty-boundary',
|
|
107
|
+
hook: async () => ({}),
|
|
108
|
+
responses: [FULL_RESPONSE],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await run.processStream(
|
|
112
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
113
|
+
streamConfig
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The answer really was cut short: the host asked to preempt, the seal
|
|
118
|
+
* took the budget, and the drain had nothing to resume with. A terminal
|
|
119
|
+
* consumer reading only completion events would persist a truncated
|
|
120
|
+
* answer as finished — `getHaltReason()` is the channel that prevents
|
|
121
|
+
* that (AgentSession emits `run.halted` off it).
|
|
122
|
+
*/
|
|
123
|
+
expect(run.getHaltReason()).toBe('preempt_incomplete');
|
|
124
|
+
expect(run.Graph?.preemptIncomplete).toBe(true);
|
|
125
|
+
expect(run.Graph?.preemptEmptyBoundaries).toBe(1);
|
|
126
|
+
|
|
127
|
+
const contents = aiContents(run.getRunMessages() ?? []);
|
|
128
|
+
expect(contents).toHaveLength(1);
|
|
129
|
+
expect(contents[0].length).toBeGreaterThan(0);
|
|
130
|
+
expect(contents[0].length).toBeLessThan(FULL_RESPONSE.length);
|
|
131
|
+
expect(FULL_RESPONSE.startsWith(contents[0])).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('forwards a halting hook\'s own stopReason to Stop hooks and getHaltReason', async () => {
|
|
135
|
+
let stopReasonSeen: string | undefined;
|
|
136
|
+
const run = await createSealRun({
|
|
137
|
+
runId: 'seal-halt-reason',
|
|
138
|
+
hook: async () => ({
|
|
139
|
+
preventContinuation: true,
|
|
140
|
+
stopReason: 'host_policy_stop',
|
|
141
|
+
}),
|
|
142
|
+
responses: [FULL_RESPONSE],
|
|
143
|
+
stopHook: async (input) => {
|
|
144
|
+
stopReasonSeen = input.stopReason;
|
|
145
|
+
return {};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await run.processStream(
|
|
150
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
151
|
+
streamConfig
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The hook-supplied reason must win end to end: a persistence/audit Stop
|
|
156
|
+
* hook records the actual cause, not the generic preempt_incomplete
|
|
157
|
+
* label, and getHaltReason() reports the same string afterward. A
|
|
158
|
+
* halting boundary that injected nothing also counts as an empty
|
|
159
|
+
* boundary in the truncated-seal telemetry.
|
|
160
|
+
*/
|
|
161
|
+
expect(stopReasonSeen).toBe('host_policy_stop');
|
|
162
|
+
expect(run.getHaltReason()).toBe('host_policy_stop');
|
|
163
|
+
expect(run.Graph?.preemptIncomplete).toBe(true);
|
|
164
|
+
expect(run.Graph?.preemptEmptyBoundaries).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('closes model-level callbacks for the sealed run, not just config-level ones', async () => {
|
|
168
|
+
let starts = 0;
|
|
169
|
+
let ends = 0;
|
|
170
|
+
const run = await createSealRun({
|
|
171
|
+
runId: 'seal-model-callbacks',
|
|
172
|
+
hook: async () => ({
|
|
173
|
+
injectedMessages: [
|
|
174
|
+
{ role: 'user' as const, content: 'Shorter.', source: 'steer' },
|
|
175
|
+
],
|
|
176
|
+
}),
|
|
177
|
+
responses: [FULL_RESPONSE, RESUMED_RESPONSE],
|
|
178
|
+
/**
|
|
179
|
+
* A handler supplied on the MODEL (clientOptions.callbacks) gets
|
|
180
|
+
* handleChatModelStart from the real run, so the sealed turn's
|
|
181
|
+
* synthetic close must reach it too — otherwise its span for the
|
|
182
|
+
* sealed run never closes. Two runs (sealed + resumed): both must
|
|
183
|
+
* balance.
|
|
184
|
+
*/
|
|
185
|
+
modelCallbacks: [
|
|
186
|
+
{
|
|
187
|
+
handleChatModelStart: (): void => {
|
|
188
|
+
starts += 1;
|
|
189
|
+
},
|
|
190
|
+
handleLLMEnd: (): void => {
|
|
191
|
+
ends += 1;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await run.processStream(
|
|
198
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
199
|
+
streamConfig
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(run.Graph?.preemptSealCount).toBe(1);
|
|
203
|
+
expect(starts).toBe(2);
|
|
204
|
+
expect(ends).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('a halting boundary stops multi-agent successors, not just the sealed subgraph', async () => {
|
|
208
|
+
const registry = new HookRegistry();
|
|
209
|
+
registry.register('PreemptBoundary', {
|
|
210
|
+
hooks: [
|
|
211
|
+
async () => ({
|
|
212
|
+
preventContinuation: true,
|
|
213
|
+
stopReason: 'stop_everything',
|
|
214
|
+
}),
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
const run = await Run.create<t.IState>({
|
|
218
|
+
runId: 'seal-halt-multiagent',
|
|
219
|
+
graphConfig: {
|
|
220
|
+
type: 'multi-agent',
|
|
221
|
+
agents: [
|
|
222
|
+
{
|
|
223
|
+
agentId: 'agent_a',
|
|
224
|
+
provider: Providers.OPENAI,
|
|
225
|
+
clientOptions: { model: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
226
|
+
instructions: 'You are agent A.',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
agentId: 'agent_b',
|
|
230
|
+
provider: Providers.OPENAI,
|
|
231
|
+
clientOptions: { model: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
232
|
+
instructions: 'You are agent B.',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
edges: [{ from: 'agent_a', to: 'agent_b', edgeType: 'direct' }],
|
|
236
|
+
},
|
|
237
|
+
hooks: registry,
|
|
238
|
+
preemption: { shouldPreempt: () => true, maxSeals: 1 },
|
|
239
|
+
returnContent: true,
|
|
240
|
+
skipCleanup: true,
|
|
241
|
+
});
|
|
242
|
+
if (!run.Graph) {
|
|
243
|
+
throw new Error('Expected graph to be initialized');
|
|
244
|
+
}
|
|
245
|
+
const model = new CountingChatModel({
|
|
246
|
+
responses: [FULL_RESPONSE, 'agent B should never say this'],
|
|
247
|
+
});
|
|
248
|
+
run.Graph.overrideModel = model;
|
|
249
|
+
|
|
250
|
+
await run.processStream(
|
|
251
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
252
|
+
streamConfig
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The registry halt is cleared to protect the sealed commit, so nothing
|
|
257
|
+
* in processStream's poll stops the outer workflow — the createCallModel
|
|
258
|
+
* entry guard is what keeps the direct-edge successor from taking a
|
|
259
|
+
* model turn after the halting boundary.
|
|
260
|
+
*/
|
|
261
|
+
expect(model.invocations).toBe(1);
|
|
262
|
+
expect(run.getHaltReason()).toBe('stop_everything');
|
|
263
|
+
expect(run.Graph.preemptIncomplete).toBe(true);
|
|
264
|
+
const contents = aiContents(run.getRunMessages() ?? []);
|
|
265
|
+
expect(contents.some((c) => c.includes('agent B should never'))).toBe(
|
|
266
|
+
false
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('resumes after an injecting boundary and completes without a halt reason', async () => {
|
|
271
|
+
const run = await createSealRun({
|
|
272
|
+
runId: 'seal-inject-resume',
|
|
273
|
+
hook: async () => ({
|
|
274
|
+
injectedMessages: [
|
|
275
|
+
{ role: 'user' as const, content: 'Make it shorter.', source: 'steer' },
|
|
276
|
+
],
|
|
277
|
+
}),
|
|
278
|
+
responses: [FULL_RESPONSE, RESUMED_RESPONSE],
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await run.processStream(
|
|
282
|
+
{ messages: [new HumanMessage('hello there')] },
|
|
283
|
+
streamConfig
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(run.getHaltReason()).toBeUndefined();
|
|
287
|
+
expect(run.Graph?.preemptIncomplete).toBe(false);
|
|
288
|
+
expect(run.Graph?.preemptSealCount).toBe(1);
|
|
289
|
+
|
|
290
|
+
const messages = run.getRunMessages() ?? [];
|
|
291
|
+
const steer = messages.find(
|
|
292
|
+
(message) => message.additional_kwargs.source === 'steer'
|
|
293
|
+
);
|
|
294
|
+
expect(steer).toBeDefined();
|
|
295
|
+
expect(steer?.content).toBe('Make it shorter.');
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Two assistant turns: the sealed partial and the post-steer
|
|
299
|
+
* continuation, which must have run to completion — the seal budget was
|
|
300
|
+
* spent, so the second stream cannot seal again even though
|
|
301
|
+
* `shouldPreempt` still answers true.
|
|
302
|
+
*/
|
|
303
|
+
const contents = aiContents(messages);
|
|
304
|
+
expect(contents).toHaveLength(2);
|
|
305
|
+
expect(FULL_RESPONSE.startsWith(contents[0])).toBe(true);
|
|
306
|
+
expect(contents[0].length).toBeLessThan(FULL_RESPONSE.length);
|
|
307
|
+
expect(contents[1]).toBe(RESUMED_RESPONSE);
|
|
308
|
+
});
|
|
309
|
+
});
|