@librechat/agents 3.3.4 → 3.3.6
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/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 +456 -7
- package/dist/cjs/graphs/Graph.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/createWorkspacePolicyHook.cjs +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/bedrock/utils/message_inputs.cjs +1 -3
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +268 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +51 -6
- package/dist/cjs/llm/openai/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 +31 -3
- package/dist/cjs/messages/alternation.cjs +92 -0
- package/dist/cjs/messages/alternation.cjs.map +1 -0
- package/dist/cjs/messages/format.cjs +72 -0
- 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 +3 -0
- package/dist/cjs/messages/injected.cjs +60 -0
- package/dist/cjs/messages/injected.cjs.map +1 -0
- package/dist/cjs/run.cjs +80 -7
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
- package/dist/cjs/session/handlers.cjs +18 -9
- package/dist/cjs/session/handlers.cjs.map +1 -1
- package/dist/cjs/stream.cjs +34 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +3 -1
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +19 -16
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +2 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs +8 -4
- package/dist/cjs/tools/ReadFile.cjs.map +1 -1
- package/dist/cjs/tools/SkillTool.cjs +2 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +3 -0
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +91 -50
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +5 -1
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/intentArg.cjs +230 -0
- package/dist/cjs/tools/intentArg.cjs.map +1 -0
- package/dist/cjs/tools/local/CompileCheckTool.cjs +3 -2
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +13 -12
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +2 -2
- package/dist/cjs/tools/ptcTimeout.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/schema.cjs +2 -0
- package/dist/cjs/tools/search/schema.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +40 -2
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +41 -7
- package/dist/cjs/tools/toolOutputReferences.cjs.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 +455 -6
- package/dist/esm/graphs/Graph.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/createWorkspacePolicyHook.mjs +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/bedrock/utils/message_inputs.mjs +1 -3
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +270 -6
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +51 -6
- package/dist/esm/llm/openai/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 +11 -7
- package/dist/esm/messages/alternation.mjs +91 -0
- package/dist/esm/messages/alternation.mjs.map +1 -0
- package/dist/esm/messages/format.mjs +72 -0
- 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 +3 -0
- package/dist/esm/messages/injected.mjs +60 -0
- package/dist/esm/messages/injected.mjs.map +1 -0
- package/dist/esm/run.mjs +80 -7
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +1 -1
- package/dist/esm/session/handlers.mjs +19 -10
- package/dist/esm/session/handlers.mjs.map +1 -1
- package/dist/esm/stream.mjs +33 -9
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -1
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +19 -16
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +2 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs +8 -4
- package/dist/esm/tools/ReadFile.mjs.map +1 -1
- package/dist/esm/tools/SkillTool.mjs +2 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -1
- package/dist/esm/tools/SubagentTool.mjs +3 -0
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +91 -51
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +5 -1
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/intentArg.mjs +220 -0
- package/dist/esm/tools/intentArg.mjs.map +1 -0
- package/dist/esm/tools/local/CompileCheckTool.mjs +3 -2
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +13 -12
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +2 -2
- package/dist/esm/tools/ptcTimeout.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/schema.mjs +2 -0
- package/dist/esm/tools/search/schema.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +40 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +41 -7
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/types/common/constants.d.ts +18 -0
- package/dist/types/graphs/Graph.d.ts +138 -1
- 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 +2 -1
- package/dist/types/llm/invoke.d.ts +7 -0
- package/dist/types/llm/openai/index.d.ts +0 -11
- package/dist/types/llm/preempt.d.ts +40 -0
- package/dist/types/messages/alternation.d.ts +23 -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/run.d.ts +7 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/types/tools/BashExecutor.d.ts +18 -0
- package/dist/types/tools/CodeExecutor.d.ts +18 -0
- package/dist/types/tools/ReadFile.d.ts +18 -0
- package/dist/types/tools/SkillTool.d.ts +18 -0
- package/dist/types/tools/SubagentTool.d.ts +9 -0
- package/dist/types/tools/ToolNode.d.ts +37 -12
- package/dist/types/tools/ToolSearch.d.ts +18 -0
- package/dist/types/tools/intentArg.d.ts +119 -0
- package/dist/types/tools/ptcTimeout.d.ts +2 -0
- package/dist/types/tools/search/keenable-scraper.d.ts +15 -0
- package/dist/types/tools/search/schema.d.ts +18 -0
- package/dist/types/tools/search/tool.d.ts +18 -0
- package/dist/types/tools/search/types.d.ts +31 -2
- package/dist/types/tools/toolOutputReferences.d.ts +12 -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 +12 -25
- package/dist/types/types/tools.d.ts +27 -2
- package/package.json +1 -1
- 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 +568 -3
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +18 -7
- 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 +2 -1
- package/src/langfuse.ts +26 -1
- package/src/llm/bedrock/utils/message_inputs.test.ts +82 -0
- package/src/llm/bedrock/utils/message_inputs.ts +14 -11
- package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +15 -0
- package/src/llm/invoke.alternation.test.ts +87 -0
- package/src/llm/invoke.handoffCue.test.ts +112 -0
- package/src/llm/invoke.ts +417 -8
- package/src/llm/openai/index.ts +74 -10
- package/src/llm/openai/llm.spec.ts +105 -0
- package/src/llm/preempt.test.ts +323 -0
- package/src/llm/preempt.ts +178 -0
- package/src/messages/alternation.test.ts +212 -0
- package/src/messages/alternation.ts +112 -0
- package/src/messages/format.ts +91 -0
- package/src/messages/formatAgentMessages.steer.test.ts +267 -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/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/stream.dispatch.test.ts +63 -0
- package/src/stream.ts +48 -9
- package/src/tools/BashExecutor.ts +6 -0
- package/src/tools/BashProgrammaticToolCalling.ts +2 -0
- package/src/tools/CodeExecutor.ts +6 -0
- package/src/tools/ProgrammaticToolCalling.ts +2 -0
- package/src/tools/ReadFile.ts +2 -0
- package/src/tools/SkillTool.ts +2 -0
- package/src/tools/SubagentTool.ts +3 -0
- package/src/tools/ToolNode.ts +193 -74
- package/src/tools/ToolSearch.ts +3 -0
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +174 -0
- package/src/tools/__tests__/intentArg.test.ts +281 -0
- package/src/tools/__tests__/intentCoverage.test.ts +139 -0
- package/src/tools/__tests__/toolOutputReferences.test.ts +83 -0
- package/src/tools/intentArg.ts +323 -0
- package/src/tools/local/CompileCheckTool.ts +4 -3
- package/src/tools/local/LocalCodingTools.ts +14 -13
- package/src/tools/ptcTimeout.ts +2 -0
- package/src/tools/search/keenable-scraper.test.ts +153 -0
- package/src/tools/search/keenable-scraper.ts +137 -0
- package/src/tools/search/outcome.test.ts +90 -0
- package/src/tools/search/schema.ts +3 -0
- package/src/tools/search/tool.ts +60 -3
- package/src/tools/search/types.ts +50 -3
- package/src/tools/subagent/SubagentExecutor.ts +2 -1
- package/src/tools/toolOutputReferences.ts +77 -9
- package/src/types/graph.ts +12 -1
- package/src/types/run.ts +67 -0
- package/src/types/stream.ts +12 -41
- package/src/types/tools.ts +24 -2
- 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
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type * as t from '@/types';
|
|
2
|
+
import {
|
|
3
|
+
ChatModelStreamHandler,
|
|
4
|
+
dispatchesChatModelStream,
|
|
5
|
+
SDK_STREAM_DISPATCH,
|
|
6
|
+
} from '@/stream';
|
|
7
|
+
import { composeEventHandlers } from '@/events';
|
|
8
|
+
import { GraphEvents } from '@/common';
|
|
9
|
+
|
|
10
|
+
const inert: t.EventHandler = {
|
|
11
|
+
handle: (): void => {
|
|
12
|
+
/* renders nothing */
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('dispatchesChatModelStream', () => {
|
|
17
|
+
it('recognizes the dispatcher itself', () => {
|
|
18
|
+
expect(dispatchesChatModelStream(new ChatModelStreamHandler())).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('is false for a handler that owns no content-part dispatch', () => {
|
|
22
|
+
expect(dispatchesChatModelStream(inert)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('is false for nothing registered', () => {
|
|
26
|
+
expect(dispatchesChatModelStream(undefined)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The reason identity is not a usable test: hosts wrap handlers, and every
|
|
31
|
+
* wrapper fails `instanceof` while still driving the same dispatch. A run
|
|
32
|
+
* that seals on that basis would be sealing against its own contract.
|
|
33
|
+
*/
|
|
34
|
+
it('sees through a composed wrapper in either position', () => {
|
|
35
|
+
const first = composeEventHandlers(
|
|
36
|
+
{ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler() },
|
|
37
|
+
{ [GraphEvents.CHAT_MODEL_STREAM]: inert }
|
|
38
|
+
)[GraphEvents.CHAT_MODEL_STREAM];
|
|
39
|
+
const second = composeEventHandlers(
|
|
40
|
+
{ [GraphEvents.CHAT_MODEL_STREAM]: inert },
|
|
41
|
+
{ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler() }
|
|
42
|
+
)[GraphEvents.CHAT_MODEL_STREAM];
|
|
43
|
+
|
|
44
|
+
expect(first).not.toBeInstanceOf(ChatModelStreamHandler);
|
|
45
|
+
expect(second).not.toBeInstanceOf(ChatModelStreamHandler);
|
|
46
|
+
expect(dispatchesChatModelStream(first)).toBe(true);
|
|
47
|
+
expect(dispatchesChatModelStream(second)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not brand a composition of inert handlers', () => {
|
|
51
|
+
const composed = composeEventHandlers(
|
|
52
|
+
{ [GraphEvents.CHAT_MODEL_STREAM]: inert },
|
|
53
|
+
{ [GraphEvents.CHAT_MODEL_STREAM]: inert }
|
|
54
|
+
)[GraphEvents.CHAT_MODEL_STREAM];
|
|
55
|
+
expect(dispatchesChatModelStream(composed)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('exposes the brand as a cross-realm-safe registered symbol', () => {
|
|
59
|
+
expect(SDK_STREAM_DISPATCH).toBe(
|
|
60
|
+
Symbol.for('@librechat/agents:chatModelStreamDispatch')
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/stream.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
calculateMaxToolResultChars,
|
|
40
40
|
truncateToolResultContent,
|
|
41
41
|
} from '@/utils/truncation';
|
|
42
|
+
import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
|
|
42
43
|
import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
|
|
43
44
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
44
45
|
import { isGoogleLike } from '@/utils/llm';
|
|
@@ -859,6 +860,11 @@ async function dispatchEagerToolCompletions(args: {
|
|
|
859
860
|
maxToolResultChars
|
|
860
861
|
).content;
|
|
861
862
|
}
|
|
863
|
+
const outcome = resolveToolOutcome(
|
|
864
|
+
record.request.args,
|
|
865
|
+
outcomeFieldsFromResult(result),
|
|
866
|
+
{ isError: result.status === 'error' }
|
|
867
|
+
);
|
|
862
868
|
|
|
863
869
|
try {
|
|
864
870
|
const dispatched = await safeDispatchCustomEvent(
|
|
@@ -878,6 +884,7 @@ async function dispatchEagerToolCompletions(args: {
|
|
|
878
884
|
id: result.toolCallId,
|
|
879
885
|
output,
|
|
880
886
|
progress: 1,
|
|
887
|
+
...(outcome != null && { outcome }),
|
|
881
888
|
} as t.ProcessedToolCall,
|
|
882
889
|
},
|
|
883
890
|
},
|
|
@@ -1400,7 +1407,34 @@ function shouldSkipLateOpenRouterReasoningChunk({
|
|
|
1400
1407
|
);
|
|
1401
1408
|
}
|
|
1402
1409
|
|
|
1410
|
+
/**
|
|
1411
|
+
* Brands a handler as one that dispatches content parts for the SDK — either
|
|
1412
|
+
* `ChatModelStreamHandler` itself or a wrapper forwarding to one.
|
|
1413
|
+
*
|
|
1414
|
+
* Identity alone is not a usable contract here. Hosts compose and wrap
|
|
1415
|
+
* handlers (`composeEventHandlers`, `createRunHandlers`), and every wrapper
|
|
1416
|
+
* fails `instanceof` while still driving the same dispatch. A brand survives
|
|
1417
|
+
* wrapping, so "does this handler own content-part dispatch" can be answered
|
|
1418
|
+
* about a value the SDK did not construct.
|
|
1419
|
+
*/
|
|
1420
|
+
export const SDK_STREAM_DISPATCH = Symbol.for(
|
|
1421
|
+
'@librechat/agents:chatModelStreamDispatch'
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
/** True when `handler` is, or forwards to, the SDK's stream dispatcher. */
|
|
1425
|
+
export function dispatchesChatModelStream(handler?: t.EventHandler): boolean {
|
|
1426
|
+
if (handler == null) {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
if (handler instanceof ChatModelStreamHandler) {
|
|
1430
|
+
return true;
|
|
1431
|
+
}
|
|
1432
|
+
return Reflect.get(handler, SDK_STREAM_DISPATCH) === true;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1403
1435
|
export class ChatModelStreamHandler implements t.EventHandler {
|
|
1436
|
+
readonly [SDK_STREAM_DISPATCH] = true;
|
|
1437
|
+
|
|
1404
1438
|
async handle(
|
|
1405
1439
|
event: string,
|
|
1406
1440
|
data: t.StreamEventData,
|
|
@@ -1846,13 +1880,16 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
1846
1880
|
number,
|
|
1847
1881
|
{ agentId?: string; groupId?: number }
|
|
1848
1882
|
>();
|
|
1849
|
-
|
|
1883
|
+
/** A delta's content may carry several parts (e.g. Google server-side tool
|
|
1884
|
+
* chunks emit multiple reasoning entries at once); every entry must reach
|
|
1885
|
+
* the step's slot, in order, or streamed text is silently lost. */
|
|
1886
|
+
const getDeltaContentParts = (
|
|
1850
1887
|
content?: t.MessageDelta['content'] | t.MessageContentComplex
|
|
1851
|
-
): t.MessageContentComplex
|
|
1888
|
+
): t.MessageContentComplex[] => {
|
|
1852
1889
|
if (content == null) {
|
|
1853
|
-
return
|
|
1890
|
+
return [];
|
|
1854
1891
|
}
|
|
1855
|
-
return Array.isArray(content) ? content
|
|
1892
|
+
return Array.isArray(content) ? content : [content];
|
|
1856
1893
|
};
|
|
1857
1894
|
const indexContentPart = (
|
|
1858
1895
|
index: number,
|
|
@@ -2130,7 +2167,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
2130
2167
|
toolCallContentIndexMap.delete(existingToolCallId);
|
|
2131
2168
|
}
|
|
2132
2169
|
|
|
2133
|
-
const newToolCall: ToolCall & t.PartMetadata = {
|
|
2170
|
+
const newToolCall: ToolCall & t.PartMetadata & { outcome?: string } = {
|
|
2134
2171
|
id,
|
|
2135
2172
|
name,
|
|
2136
2173
|
args,
|
|
@@ -2150,6 +2187,10 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
2150
2187
|
if (finalUpdate) {
|
|
2151
2188
|
newToolCall.progress = 1;
|
|
2152
2189
|
newToolCall.output = contentPart.tool_call.output;
|
|
2190
|
+
const outcome = (contentPart.tool_call as t.ToolCallPart).outcome;
|
|
2191
|
+
if (outcome != null) {
|
|
2192
|
+
newToolCall.outcome = outcome;
|
|
2193
|
+
}
|
|
2153
2194
|
}
|
|
2154
2195
|
|
|
2155
2196
|
contentParts[index] = {
|
|
@@ -2284,8 +2325,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
2284
2325
|
return;
|
|
2285
2326
|
}
|
|
2286
2327
|
|
|
2287
|
-
const contentPart
|
|
2288
|
-
if (contentPart != null) {
|
|
2328
|
+
for (const contentPart of getDeltaContentParts(messageDelta.delta.content)) {
|
|
2289
2329
|
updateContent(runStep.index, contentPart);
|
|
2290
2330
|
}
|
|
2291
2331
|
} else if (
|
|
@@ -2314,8 +2354,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
2314
2354
|
return;
|
|
2315
2355
|
}
|
|
2316
2356
|
|
|
2317
|
-
const contentPart
|
|
2318
|
-
if (contentPart != null) {
|
|
2357
|
+
for (const contentPart of getDeltaContentParts(reasoningDelta.delta.content)) {
|
|
2319
2358
|
updateContent(runStep.index, contentPart);
|
|
2320
2359
|
}
|
|
2321
2360
|
} else if (event === GraphEvents.ON_RUN_STEP_DELTA) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
normalizeCodeApiRequestError,
|
|
17
17
|
resolveCodeApiAuthHeaders,
|
|
18
18
|
} from './CodeExecutor';
|
|
19
|
+
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
19
20
|
import { Constants } from '@/common';
|
|
20
21
|
|
|
21
22
|
config();
|
|
@@ -26,6 +27,7 @@ const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
|
|
|
26
27
|
export const BashExecutionToolSchema = {
|
|
27
28
|
type: 'object',
|
|
28
29
|
properties: {
|
|
30
|
+
intent: { ...INTENT_PROPERTY },
|
|
29
31
|
command: {
|
|
30
32
|
type: 'string',
|
|
31
33
|
description: `The bash command or script to execute.
|
|
@@ -187,16 +189,20 @@ function createBashExecutionTool(
|
|
|
187
189
|
/* Drop any model-supplied `runtime_session_hint` from the raw args: the
|
|
188
190
|
* hint must only come from ToolNode's injected `_runtime_session_hint`
|
|
189
191
|
* (below), never from the tool call itself. */
|
|
192
|
+
/* `intent` is a UI display label — never part of the wire body. */
|
|
190
193
|
const {
|
|
191
194
|
command,
|
|
195
|
+
intent: _ignoredIntent,
|
|
192
196
|
runtime_session_hint: _ignoredModelHint,
|
|
193
197
|
...rest
|
|
194
198
|
} = rawInput as {
|
|
195
199
|
command: string;
|
|
200
|
+
intent?: unknown;
|
|
196
201
|
runtime_session_hint?: unknown;
|
|
197
202
|
args?: string[];
|
|
198
203
|
};
|
|
199
204
|
void _ignoredModelHint;
|
|
205
|
+
void _ignoredIntent;
|
|
200
206
|
const { session_id, _injected_files, _runtime_session_hint } =
|
|
201
207
|
(config.toolCall ?? {}) as {
|
|
202
208
|
session_id?: string;
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
executeTools,
|
|
22
22
|
formatCompletedResponse,
|
|
23
23
|
} from './ProgrammaticToolCalling';
|
|
24
|
+
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
24
25
|
import { Constants } from '@/common';
|
|
25
26
|
|
|
26
27
|
config();
|
|
@@ -115,6 +116,7 @@ export function createBashProgrammaticToolCallingSchema(
|
|
|
115
116
|
return {
|
|
116
117
|
type: 'object',
|
|
117
118
|
properties: {
|
|
119
|
+
intent: { ...INTENT_PROPERTY },
|
|
118
120
|
code: {
|
|
119
121
|
type: 'string',
|
|
120
122
|
minLength: 1,
|
|
@@ -5,6 +5,7 @@ import { getEnvironmentVariable } from '@langchain/core/utils/env';
|
|
|
5
5
|
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
6
6
|
import type * as t from '@/types';
|
|
7
7
|
import { appendCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
|
|
8
|
+
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
8
9
|
import { EnvVar, Constants } from '@/common';
|
|
9
10
|
|
|
10
11
|
export {
|
|
@@ -75,6 +76,7 @@ const SUPPORTED_LANGUAGES = [
|
|
|
75
76
|
export const CodeExecutionToolSchema = {
|
|
76
77
|
type: 'object',
|
|
77
78
|
properties: {
|
|
79
|
+
intent: { ...INTENT_PROPERTY },
|
|
78
80
|
lang: {
|
|
79
81
|
type: 'string',
|
|
80
82
|
enum: SUPPORTED_LANGUAGES,
|
|
@@ -346,18 +348,22 @@ function createCodeExecutionTool(
|
|
|
346
348
|
* injected `_runtime_session_hint` (below). Spreading `...rest` into
|
|
347
349
|
* postData would otherwise let a tool call opt itself into / pick a
|
|
348
350
|
* stateful runtime even when statefulSessions is off. */
|
|
351
|
+
/* `intent` is a UI display label — never part of the wire body. */
|
|
349
352
|
const {
|
|
350
353
|
lang,
|
|
351
354
|
code,
|
|
355
|
+
intent: _ignoredIntent,
|
|
352
356
|
runtime_session_hint: _ignoredModelHint,
|
|
353
357
|
...rest
|
|
354
358
|
} = rawInput as {
|
|
355
359
|
lang: SupportedLanguage;
|
|
356
360
|
code: string;
|
|
361
|
+
intent?: unknown;
|
|
357
362
|
runtime_session_hint?: unknown;
|
|
358
363
|
args?: string[];
|
|
359
364
|
};
|
|
360
365
|
void _ignoredModelHint;
|
|
366
|
+
void _ignoredIntent;
|
|
361
367
|
/**
|
|
362
368
|
* Extract session context from config.toolCall (injected by ToolNode).
|
|
363
369
|
* - session_id: associates with the previous run.
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
createCodeApiRunTimeoutSchema,
|
|
25
25
|
resolveCodeApiRunTimeoutMs,
|
|
26
26
|
} from './ptcTimeout';
|
|
27
|
+
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
27
28
|
import { Constants } from '@/common';
|
|
28
29
|
|
|
29
30
|
config();
|
|
@@ -89,6 +90,7 @@ export function createProgrammaticToolCallingSchema(
|
|
|
89
90
|
return {
|
|
90
91
|
type: 'object',
|
|
91
92
|
properties: {
|
|
93
|
+
intent: { ...INTENT_PROPERTY },
|
|
92
94
|
code: {
|
|
93
95
|
type: 'string',
|
|
94
96
|
minLength: 1,
|
package/src/tools/ReadFile.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/tools/ReadFile.ts
|
|
2
|
+
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
2
3
|
import { Constants } from '@/common';
|
|
3
4
|
|
|
4
5
|
export const ReadFileToolName = Constants.READ_FILE;
|
|
@@ -22,6 +23,7 @@ CONSTRAINTS:
|
|
|
22
23
|
export const ReadFileToolSchema = {
|
|
23
24
|
type: 'object',
|
|
24
25
|
properties: {
|
|
26
|
+
intent: { ...INTENT_PROPERTY },
|
|
25
27
|
path: {
|
|
26
28
|
type: 'string',
|
|
27
29
|
description:
|
package/src/tools/SkillTool.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/tools/SkillTool.ts
|
|
2
|
+
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
2
3
|
import { Constants } from '@/common';
|
|
3
4
|
|
|
4
5
|
export const SkillToolName = Constants.SKILL_TOOL;
|
|
@@ -26,6 +27,7 @@ CONSTRAINTS:
|
|
|
26
27
|
export const SkillToolSchema = {
|
|
27
28
|
type: 'object',
|
|
28
29
|
properties: {
|
|
30
|
+
intent: { ...INTENT_PROPERTY },
|
|
29
31
|
skillName: {
|
|
30
32
|
type: 'string',
|
|
31
33
|
description:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JsonSchemaType, LCTool } from '@/types/tools';
|
|
2
2
|
import type { SubagentConfig } from '@/types';
|
|
3
|
+
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
3
4
|
import { Constants } from '@/common';
|
|
4
5
|
|
|
5
6
|
export const SubagentToolName = Constants.SUBAGENT;
|
|
@@ -29,6 +30,7 @@ const SUBAGENT_TYPE_PROP_DESCRIPTION =
|
|
|
29
30
|
export const SubagentToolSchema = {
|
|
30
31
|
type: 'object',
|
|
31
32
|
properties: {
|
|
33
|
+
intent: { ...INTENT_PROPERTY },
|
|
32
34
|
description: {
|
|
33
35
|
type: 'string',
|
|
34
36
|
description: DESCRIPTION_PROP_DESCRIPTION,
|
|
@@ -67,6 +69,7 @@ export function buildSubagentToolParams(configs: SubagentConfig[]): {
|
|
|
67
69
|
schema: {
|
|
68
70
|
type: 'object',
|
|
69
71
|
properties: {
|
|
72
|
+
intent: { ...INTENT_PROPERTY },
|
|
70
73
|
description: {
|
|
71
74
|
type: 'string',
|
|
72
75
|
description: DESCRIPTION_PROP_DESCRIPTION,
|