@librechat/agents 3.1.68 → 3.1.71-dev.0
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 +23 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +16 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +136 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +162 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
- package/dist/cjs/hooks/executeHooks.cjs +276 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
- package/dist/cjs/hooks/matchers.cjs +256 -0
- package/dist/cjs/hooks/matchers.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs +27 -0
- package/dist/cjs/hooks/types.cjs.map +1 -0
- package/dist/cjs/main.cjs +57 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +74 -12
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +9 -2
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/run.cjs +115 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +44 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +208 -0
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +287 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/CodeExecutor.cjs +0 -9
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -23
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs +43 -0
- package/dist/cjs/tools/ReadFile.cjs.map +1 -0
- package/dist/cjs/tools/SkillTool.cjs +50 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -0
- package/dist/cjs/tools/SubagentTool.cjs +92 -0
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +746 -174
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +2 -13
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/skillCatalog.cjs +84 -0
- package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +475 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -0
- package/dist/cjs/utils/truncation.cjs +28 -0
- package/dist/cjs/utils/truncation.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +23 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +15 -2
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +136 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +160 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
- package/dist/esm/hooks/executeHooks.mjs +273 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -0
- package/dist/esm/hooks/matchers.mjs +251 -0
- package/dist/esm/hooks/matchers.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs +25 -0
- package/dist/esm/hooks/types.mjs.map +1 -0
- package/dist/esm/main.mjs +13 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +66 -4
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +9 -2
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/run.mjs +115 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +44 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +200 -0
- package/dist/esm/tools/BashExecutor.mjs.map +1 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +278 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/CodeExecutor.mjs +0 -9
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -24
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs +38 -0
- package/dist/esm/tools/ReadFile.mjs.map +1 -0
- package/dist/esm/tools/SkillTool.mjs +45 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -0
- package/dist/esm/tools/SubagentTool.mjs +85 -0
- package/dist/esm/tools/SubagentTool.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +748 -176
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +3 -14
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/skillCatalog.mjs +82 -0
- package/dist/esm/tools/skillCatalog.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +505 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +468 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -0
- package/dist/esm/utils/truncation.mjs +27 -1
- package/dist/esm/utils/truncation.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +6 -0
- package/dist/types/common/enum.d.ts +10 -2
- package/dist/types/graphs/Graph.d.ts +23 -0
- package/dist/types/hooks/HookRegistry.d.ts +56 -0
- package/dist/types/hooks/executeHooks.d.ts +79 -0
- package/dist/types/hooks/index.d.ts +6 -0
- package/dist/types/hooks/matchers.d.ts +95 -0
- package/dist/types/hooks/types.d.ts +320 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/messages/format.d.ts +2 -1
- package/dist/types/run.d.ts +2 -0
- package/dist/types/summarization/node.d.ts +2 -0
- package/dist/types/tools/BashExecutor.d.ts +76 -0
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -9
- package/dist/types/tools/ReadFile.d.ts +28 -0
- package/dist/types/tools/SkillTool.d.ts +40 -0
- package/dist/types/tools/SubagentTool.d.ts +36 -0
- package/dist/types/tools/ToolNode.d.ts +109 -4
- package/dist/types/tools/ToolSearch.d.ts +2 -2
- package/dist/types/tools/skillCatalog.d.ts +19 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
- package/dist/types/tools/subagent/index.d.ts +2 -0
- package/dist/types/tools/toolOutputReferences.d.ts +205 -0
- package/dist/types/types/graph.d.ts +61 -2
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/run.d.ts +28 -0
- package/dist/types/types/skill.d.ts +9 -0
- package/dist/types/types/tools.d.ts +108 -10
- package/dist/types/utils/truncation.d.ts +21 -0
- package/package.json +5 -1
- package/src/agents/AgentContext.ts +26 -2
- package/src/common/enum.ts +15 -1
- package/src/graphs/Graph.ts +161 -0
- package/src/hooks/HookRegistry.ts +208 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
- package/src/hooks/__tests__/compactHooks.test.ts +214 -0
- package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
- package/src/hooks/__tests__/integration.test.ts +337 -0
- package/src/hooks/__tests__/matchers.test.ts +238 -0
- package/src/hooks/__tests__/toolHooks.test.ts +669 -0
- package/src/hooks/executeHooks.ts +375 -0
- package/src/hooks/index.ts +57 -0
- package/src/hooks/matchers.ts +280 -0
- package/src/hooks/types.ts +404 -0
- package/src/index.ts +10 -0
- package/src/messages/format.ts +74 -4
- package/src/messages/formatAgentMessages.skills.test.ts +334 -0
- package/src/messages/prune.ts +9 -2
- package/src/run.ts +130 -0
- package/src/scripts/multi-agent-subagent.ts +246 -0
- package/src/scripts/programmatic_exec.ts +1 -10
- package/src/scripts/subagent-event-driven-debug.ts +190 -0
- package/src/scripts/subagent-tools-debug.ts +160 -0
- package/src/scripts/test_code_api.ts +0 -7
- package/src/scripts/tool_search.ts +1 -10
- package/src/specs/prune.test.ts +413 -0
- package/src/specs/subagent.test.ts +305 -0
- package/src/summarization/node.ts +53 -0
- package/src/tools/BashExecutor.ts +238 -0
- package/src/tools/BashProgrammaticToolCalling.ts +381 -0
- package/src/tools/CodeExecutor.ts +0 -11
- package/src/tools/ProgrammaticToolCalling.ts +4 -29
- package/src/tools/ReadFile.ts +39 -0
- package/src/tools/SkillTool.ts +46 -0
- package/src/tools/SubagentTool.ts +100 -0
- package/src/tools/ToolNode.ts +999 -214
- package/src/tools/ToolSearch.ts +3 -19
- package/src/tools/__tests__/BashExecutor.test.ts +36 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +7 -8
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1
- package/src/tools/__tests__/ReadFile.test.ts +44 -0
- package/src/tools/__tests__/SkillTool.test.ts +442 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
- package/src/tools/__tests__/SubagentTool.test.ts +149 -0
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +1395 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/tools/__tests__/ToolSearch.integration.test.ts +7 -8
- package/src/tools/__tests__/skillCatalog.test.ts +161 -0
- package/src/tools/__tests__/subagentHooks.test.ts +215 -0
- package/src/tools/__tests__/toolOutputReferences.test.ts +415 -0
- package/src/tools/skillCatalog.ts +126 -0
- package/src/tools/subagent/SubagentExecutor.ts +676 -0
- package/src/tools/subagent/index.ts +13 -0
- package/src/tools/toolOutputReferences.ts +590 -0
- package/src/types/graph.ts +80 -1
- package/src/types/index.ts +1 -0
- package/src/types/run.ts +28 -0
- package/src/types/skill.ts +11 -0
- package/src/types/tools.ts +112 -10
- package/src/utils/__tests__/truncation.test.ts +66 -0
- package/src/utils/truncation.ts +30 -0
package/src/tools/ToolSearch.ts
CHANGED
|
@@ -22,11 +22,10 @@ function getBM25Function(): BM25Fn {
|
|
|
22
22
|
const BM25 = getBM25Function();
|
|
23
23
|
import fetch, { RequestInit } from 'node-fetch';
|
|
24
24
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
25
|
-
import { getEnvironmentVariable } from '@langchain/core/utils/env';
|
|
26
25
|
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
27
26
|
import type * as t from '@/types';
|
|
28
27
|
import { getCodeBaseURL } from './CodeExecutor';
|
|
29
|
-
import {
|
|
28
|
+
import { Constants } from '@/common';
|
|
30
29
|
|
|
31
30
|
config();
|
|
32
31
|
|
|
@@ -837,11 +836,11 @@ function formatServerListing(
|
|
|
837
836
|
*
|
|
838
837
|
* @example
|
|
839
838
|
* // Option 1: Code interpreter mode (regex via sandbox)
|
|
840
|
-
* const tool = createToolSearch({
|
|
839
|
+
* const tool = createToolSearch({ toolRegistry });
|
|
841
840
|
* await tool.invoke({ query: 'expense.*report' });
|
|
842
841
|
*
|
|
843
842
|
* @example
|
|
844
|
-
* // Option 2: Local mode (safe substring search
|
|
843
|
+
* // Option 2: Local mode (safe substring search)
|
|
845
844
|
* const tool = createToolSearch({ mode: 'local', toolRegistry });
|
|
846
845
|
* await tool.invoke({ query: 'expense' });
|
|
847
846
|
*/
|
|
@@ -853,20 +852,6 @@ function createToolSearch(
|
|
|
853
852
|
const mcpNameFormat: t.McpNameFormat = initParams.mcpNameFormat ?? 'full';
|
|
854
853
|
const schema = createToolSearchSchema(mode);
|
|
855
854
|
|
|
856
|
-
const apiKey: string =
|
|
857
|
-
mode === 'code_interpreter'
|
|
858
|
-
? ((initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
|
|
859
|
-
initParams.apiKey ??
|
|
860
|
-
getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
|
|
861
|
-
'')
|
|
862
|
-
: '';
|
|
863
|
-
|
|
864
|
-
if (mode === 'code_interpreter' && !apiKey) {
|
|
865
|
-
throw new Error(
|
|
866
|
-
'No API key provided for tool search in code_interpreter mode. Use mode: "local" to search without an API key.'
|
|
867
|
-
);
|
|
868
|
-
}
|
|
869
|
-
|
|
870
855
|
const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
|
|
871
856
|
const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
|
|
872
857
|
|
|
@@ -1052,7 +1037,6 @@ ${mcpNote}${toolsListSection}
|
|
|
1052
1037
|
headers: {
|
|
1053
1038
|
'Content-Type': 'application/json',
|
|
1054
1039
|
'User-Agent': 'LibreChat/1.0',
|
|
1055
|
-
'X-API-Key': apiKey,
|
|
1056
1040
|
},
|
|
1057
1041
|
body: JSON.stringify(postData),
|
|
1058
1042
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
BashExecutionToolDescription,
|
|
4
|
+
BashToolOutputReferencesGuide,
|
|
5
|
+
buildBashExecutionToolDescription,
|
|
6
|
+
} from '../BashExecutor';
|
|
7
|
+
|
|
8
|
+
describe('buildBashExecutionToolDescription', () => {
|
|
9
|
+
it('returns the base description by default', () => {
|
|
10
|
+
expect(buildBashExecutionToolDescription()).toBe(
|
|
11
|
+
BashExecutionToolDescription
|
|
12
|
+
);
|
|
13
|
+
expect(buildBashExecutionToolDescription({})).toBe(
|
|
14
|
+
BashExecutionToolDescription
|
|
15
|
+
);
|
|
16
|
+
expect(
|
|
17
|
+
buildBashExecutionToolDescription({ enableToolOutputReferences: false })
|
|
18
|
+
).toBe(BashExecutionToolDescription);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('appends the tool-output references guide when enabled', () => {
|
|
22
|
+
const composed = buildBashExecutionToolDescription({
|
|
23
|
+
enableToolOutputReferences: true,
|
|
24
|
+
});
|
|
25
|
+
expect(composed.startsWith(BashExecutionToolDescription)).toBe(true);
|
|
26
|
+
expect(composed).toContain(BashToolOutputReferencesGuide);
|
|
27
|
+
expect(composed).toContain('{{tool<idx>turn<turn>}}');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('separates base and guide with a blank line', () => {
|
|
31
|
+
const composed = buildBashExecutionToolDescription({
|
|
32
|
+
enableToolOutputReferences: true,
|
|
33
|
+
});
|
|
34
|
+
expect(composed.includes(`${BashExecutionToolDescription}\n\n`)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Integration tests for Programmatic Tool Calling.
|
|
4
4
|
* These tests hit the LIVE Code API and verify end-to-end functionality.
|
|
5
5
|
*
|
|
6
|
-
* Run with: npm test -- ProgrammaticToolCalling.integration.test.ts
|
|
6
|
+
* Run with: RUN_CODE_INTEGRATION_TESTS=1 npm test -- ProgrammaticToolCalling.integration.test.ts
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Opt-in via the `RUN_CODE_INTEGRATION_TESTS` environment variable —
|
|
9
|
+
* these tests hit a real sandbox so they don't run in CI by default.
|
|
10
10
|
*/
|
|
11
11
|
import { config as dotenvConfig } from 'dotenv';
|
|
12
12
|
dotenvConfig();
|
|
@@ -22,12 +22,11 @@ import {
|
|
|
22
22
|
createProgrammaticToolRegistry,
|
|
23
23
|
} from '@/test/mockTools';
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
const shouldSkip = apiKey == null || apiKey === '';
|
|
25
|
+
const shouldSkip = process.env.RUN_CODE_INTEGRATION_TESTS !== '1';
|
|
27
26
|
|
|
28
|
-
const
|
|
27
|
+
const describeIfLive = shouldSkip ? describe.skip : describe;
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
describeIfLive('ProgrammaticToolCalling - Live API Integration', () => {
|
|
31
30
|
let ptcTool: ReturnType<typeof createProgrammaticToolCallingTool>;
|
|
32
31
|
let toolMap: t.ToolMap;
|
|
33
32
|
let toolDefinitions: t.LCTool[];
|
|
@@ -43,7 +42,7 @@ describeIfApiKey('ProgrammaticToolCalling - Live API Integration', () => {
|
|
|
43
42
|
toolMap = new Map(tools.map((t) => [t.name, t]));
|
|
44
43
|
toolDefinitions = Array.from(createProgrammaticToolRegistry().values());
|
|
45
44
|
|
|
46
|
-
ptcTool = createProgrammaticToolCallingTool(
|
|
45
|
+
ptcTool = createProgrammaticToolCallingTool();
|
|
47
46
|
});
|
|
48
47
|
|
|
49
48
|
it('executes simple single tool call', async () => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { Constants } from '@/common';
|
|
3
|
+
import {
|
|
4
|
+
ReadFileToolName,
|
|
5
|
+
ReadFileToolSchema,
|
|
6
|
+
ReadFileToolDescription,
|
|
7
|
+
ReadFileToolDefinition,
|
|
8
|
+
} from '../ReadFile';
|
|
9
|
+
|
|
10
|
+
describe('ReadFile', () => {
|
|
11
|
+
describe('schema structure', () => {
|
|
12
|
+
it('has file_path as required string property', () => {
|
|
13
|
+
expect(ReadFileToolSchema.properties.file_path.type).toBe('string');
|
|
14
|
+
expect(ReadFileToolSchema.required).toContain('file_path');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('is an object type schema', () => {
|
|
18
|
+
expect(ReadFileToolSchema.type).toBe('object');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('ReadFileToolDefinition', () => {
|
|
23
|
+
it('has correct name', () => {
|
|
24
|
+
expect(ReadFileToolDefinition.name).toBe(Constants.READ_FILE);
|
|
25
|
+
expect(ReadFileToolDefinition.name).toBe('read_file');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('references the same ReadFileToolSchema object', () => {
|
|
29
|
+
expect(ReadFileToolDefinition.parameters).toBe(ReadFileToolSchema);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('has a non-empty description', () => {
|
|
33
|
+
expect(ReadFileToolDefinition.description).toBe(ReadFileToolDescription);
|
|
34
|
+
expect(ReadFileToolDefinition.description.length).toBeGreaterThan(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('ReadFileToolName', () => {
|
|
39
|
+
it('equals Constants.READ_FILE', () => {
|
|
40
|
+
expect(ReadFileToolName).toBe('read_file');
|
|
41
|
+
expect(ReadFileToolName).toBe(Constants.READ_FILE);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
|
+
import { describe, it, expect } from '@jest/globals';
|
|
4
|
+
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
5
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
6
|
+
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
7
|
+
import type * as t from '@/types';
|
|
8
|
+
import * as events from '@/utils/events';
|
|
9
|
+
import { ToolNode } from '../ToolNode';
|
|
10
|
+
import { Constants } from '@/common';
|
|
11
|
+
import {
|
|
12
|
+
SkillToolDescription,
|
|
13
|
+
SkillToolDefinition,
|
|
14
|
+
SkillToolSchema,
|
|
15
|
+
SkillToolName,
|
|
16
|
+
} from '../SkillTool';
|
|
17
|
+
|
|
18
|
+
describe('SkillTool', () => {
|
|
19
|
+
describe('schema structure', () => {
|
|
20
|
+
it('has skillName as required string property', () => {
|
|
21
|
+
expect(SkillToolSchema.properties.skillName.type).toBe('string');
|
|
22
|
+
expect(SkillToolSchema.required).toContain('skillName');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('has args as optional string property', () => {
|
|
26
|
+
expect(SkillToolSchema.properties.args.type).toBe('string');
|
|
27
|
+
expect(SkillToolSchema.required).not.toContain('args');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is an object type schema', () => {
|
|
31
|
+
expect(SkillToolSchema.type).toBe('object');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('SkillToolDefinition', () => {
|
|
36
|
+
it('has correct name', () => {
|
|
37
|
+
expect(SkillToolDefinition.name).toBe(Constants.SKILL_TOOL);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('references the same SkillToolSchema object (no duplication)', () => {
|
|
41
|
+
expect(SkillToolDefinition.parameters).toBe(SkillToolSchema);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('has a non-empty description', () => {
|
|
45
|
+
expect(SkillToolDefinition.description).toBe(SkillToolDescription);
|
|
46
|
+
expect(SkillToolDefinition.description.length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('SkillToolName', () => {
|
|
51
|
+
it('equals Constants.SKILL_TOOL', () => {
|
|
52
|
+
expect(SkillToolName).toBe('skill');
|
|
53
|
+
expect(SkillToolName).toBe(Constants.SKILL_TOOL);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('InjectedMessage type-check', () => {
|
|
58
|
+
it('constructs a valid ToolExecuteResult with injectedMessages', () => {
|
|
59
|
+
const result: t.ToolExecuteResult = {
|
|
60
|
+
toolCallId: 'call_1',
|
|
61
|
+
content: 'Skill loaded successfully.',
|
|
62
|
+
status: 'success',
|
|
63
|
+
injectedMessages: [
|
|
64
|
+
{
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: '# PDF Processor Instructions\n\nFollow these steps...',
|
|
67
|
+
isMeta: true,
|
|
68
|
+
source: 'skill',
|
|
69
|
+
skillName: 'pdf-processor',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
role: 'system',
|
|
73
|
+
content: 'Skill files are available at /skills/pdf-processor/',
|
|
74
|
+
source: 'skill',
|
|
75
|
+
skillName: 'pdf-processor',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
expect(result.injectedMessages).toHaveLength(2);
|
|
81
|
+
expect(result.injectedMessages![0].role).toBe('user');
|
|
82
|
+
expect(result.injectedMessages![1].role).toBe('system');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('accepts MessageContentComplex[] content', () => {
|
|
86
|
+
const result: t.ToolExecuteResult = {
|
|
87
|
+
toolCallId: 'call_1',
|
|
88
|
+
content: '',
|
|
89
|
+
status: 'success',
|
|
90
|
+
injectedMessages: [
|
|
91
|
+
{
|
|
92
|
+
role: 'user',
|
|
93
|
+
content: [
|
|
94
|
+
{ type: 'text', text: 'Skill instructions here' },
|
|
95
|
+
{ type: 'image_url', image_url: { url: 'data:image/png;...' } },
|
|
96
|
+
],
|
|
97
|
+
isMeta: true,
|
|
98
|
+
source: 'skill',
|
|
99
|
+
skillName: 'visual-skill',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
expect(Array.isArray(result.injectedMessages![0].content)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('ToolNode injectedMessages plumbing (event-driven)', () => {
|
|
109
|
+
const createDummyTool = (name = 'dummy'): StructuredToolInterface =>
|
|
110
|
+
tool(async () => 'dummy', {
|
|
111
|
+
name,
|
|
112
|
+
description: 'dummy',
|
|
113
|
+
schema: z.object({ x: z.string() }),
|
|
114
|
+
}) as unknown as StructuredToolInterface;
|
|
115
|
+
|
|
116
|
+
function mockEventDispatch(
|
|
117
|
+
mockResults: t.ToolExecuteResult[]
|
|
118
|
+
): jest.SpyInstance {
|
|
119
|
+
return jest
|
|
120
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
121
|
+
.mockImplementation(async (_event, data) => {
|
|
122
|
+
const request = data as Record<string, unknown>;
|
|
123
|
+
if (typeof request.resolve === 'function') {
|
|
124
|
+
(request.resolve as (r: t.ToolExecuteResult[]) => void)(
|
|
125
|
+
mockResults
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
jest.restoreAllMocks();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('appends injected messages AFTER ToolMessages in output', async () => {
|
|
136
|
+
const toolNode = new ToolNode({
|
|
137
|
+
tools: [createDummyTool()],
|
|
138
|
+
eventDrivenMode: true,
|
|
139
|
+
agentId: 'test-agent',
|
|
140
|
+
toolCallStepIds: new Map([['call_1', 'step_1']]),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const aiMsg = new AIMessage({
|
|
144
|
+
content: '',
|
|
145
|
+
tool_calls: [{ id: 'call_1', name: 'dummy', args: { x: 'hello' } }],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
mockEventDispatch([
|
|
149
|
+
{
|
|
150
|
+
toolCallId: 'call_1',
|
|
151
|
+
content: 'Tool result text',
|
|
152
|
+
status: 'success',
|
|
153
|
+
injectedMessages: [
|
|
154
|
+
{
|
|
155
|
+
role: 'user',
|
|
156
|
+
content: 'Injected skill body content',
|
|
157
|
+
isMeta: true,
|
|
158
|
+
source: 'skill',
|
|
159
|
+
skillName: 'test-skill',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
role: 'system',
|
|
163
|
+
content: 'System context hint',
|
|
164
|
+
source: 'system',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const result = await toolNode.invoke({ messages: [aiMsg] });
|
|
171
|
+
const messages = (result as { messages: BaseMessage[] }).messages;
|
|
172
|
+
|
|
173
|
+
expect(messages).toHaveLength(3);
|
|
174
|
+
|
|
175
|
+
// ToolMessage comes FIRST (preserves AIMessage -> ToolMessage adjacency)
|
|
176
|
+
expect(messages[0]._getType()).toBe('tool');
|
|
177
|
+
|
|
178
|
+
// Injected messages come AFTER
|
|
179
|
+
const second = messages[1] as HumanMessage;
|
|
180
|
+
expect(second).toBeInstanceOf(HumanMessage);
|
|
181
|
+
expect(second.content).toBe('Injected skill body content');
|
|
182
|
+
expect(second.additional_kwargs.role).toBe('user');
|
|
183
|
+
expect(second.additional_kwargs.isMeta).toBe(true);
|
|
184
|
+
expect(second.additional_kwargs.source).toBe('skill');
|
|
185
|
+
expect(second.additional_kwargs.skillName).toBe('test-skill');
|
|
186
|
+
|
|
187
|
+
// role: 'system' also becomes HumanMessage (avoids provider rejections)
|
|
188
|
+
const third = messages[2] as HumanMessage;
|
|
189
|
+
expect(third).toBeInstanceOf(HumanMessage);
|
|
190
|
+
expect(third.content).toBe('System context hint');
|
|
191
|
+
expect(third.additional_kwargs.role).toBe('system');
|
|
192
|
+
expect(third.additional_kwargs.source).toBe('system');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns only ToolMessages when no injectedMessages present', async () => {
|
|
196
|
+
const toolNode = new ToolNode({
|
|
197
|
+
tools: [createDummyTool()],
|
|
198
|
+
eventDrivenMode: true,
|
|
199
|
+
agentId: 'test-agent',
|
|
200
|
+
toolCallStepIds: new Map([['call_2', 'step_2']]),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const aiMsg = new AIMessage({
|
|
204
|
+
content: '',
|
|
205
|
+
tool_calls: [{ id: 'call_2', name: 'dummy', args: { x: 'test' } }],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
mockEventDispatch([
|
|
209
|
+
{ toolCallId: 'call_2', content: 'Normal result', status: 'success' },
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
const result = await toolNode.invoke({ messages: [aiMsg] });
|
|
213
|
+
const messages = (result as { messages: BaseMessage[] }).messages;
|
|
214
|
+
|
|
215
|
+
expect(messages).toHaveLength(1);
|
|
216
|
+
expect(messages[0]._getType()).toBe('tool');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('passes MessageContentComplex[] content through without stringifying', async () => {
|
|
220
|
+
const toolNode = new ToolNode({
|
|
221
|
+
tools: [createDummyTool()],
|
|
222
|
+
eventDrivenMode: true,
|
|
223
|
+
agentId: 'test-agent',
|
|
224
|
+
toolCallStepIds: new Map([['call_3', 'step_3']]),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const aiMsg = new AIMessage({
|
|
228
|
+
content: '',
|
|
229
|
+
tool_calls: [{ id: 'call_3', name: 'dummy', args: { x: 'test' } }],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const complexContent = [
|
|
233
|
+
{ type: 'text', text: 'Multi-part skill instructions' },
|
|
234
|
+
{ type: 'text', text: 'Second part of instructions' },
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
mockEventDispatch([
|
|
238
|
+
{
|
|
239
|
+
toolCallId: 'call_3',
|
|
240
|
+
content: '',
|
|
241
|
+
status: 'success',
|
|
242
|
+
injectedMessages: [
|
|
243
|
+
{
|
|
244
|
+
role: 'user' as const,
|
|
245
|
+
content: complexContent,
|
|
246
|
+
isMeta: true,
|
|
247
|
+
source: 'skill' as const,
|
|
248
|
+
skillName: 'complex-skill',
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
const result = await toolNode.invoke({ messages: [aiMsg] });
|
|
255
|
+
const messages = (result as { messages: BaseMessage[] }).messages;
|
|
256
|
+
|
|
257
|
+
expect(messages).toHaveLength(2);
|
|
258
|
+
// ToolMessage first
|
|
259
|
+
expect(messages[0]._getType()).toBe('tool');
|
|
260
|
+
// Injected message second with array content preserved (not stringified)
|
|
261
|
+
const injected = messages[1] as HumanMessage;
|
|
262
|
+
expect(injected).toBeInstanceOf(HumanMessage);
|
|
263
|
+
expect(Array.isArray(injected.content)).toBe(true);
|
|
264
|
+
expect(injected.content).toEqual(complexContent);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('aggregates injected messages from multiple tool calls', async () => {
|
|
268
|
+
const toolNode = new ToolNode({
|
|
269
|
+
tools: [createDummyTool('tool_a'), createDummyTool('tool_b')],
|
|
270
|
+
eventDrivenMode: true,
|
|
271
|
+
agentId: 'test-agent',
|
|
272
|
+
toolCallStepIds: new Map([
|
|
273
|
+
['call_a', 'step_a'],
|
|
274
|
+
['call_b', 'step_b'],
|
|
275
|
+
]),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const aiMsg = new AIMessage({
|
|
279
|
+
content: '',
|
|
280
|
+
tool_calls: [
|
|
281
|
+
{ id: 'call_a', name: 'tool_a', args: { x: 'a' } },
|
|
282
|
+
{ id: 'call_b', name: 'tool_b', args: { x: 'b' } },
|
|
283
|
+
],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
mockEventDispatch([
|
|
287
|
+
{
|
|
288
|
+
toolCallId: 'call_a',
|
|
289
|
+
content: 'Result A',
|
|
290
|
+
status: 'success',
|
|
291
|
+
injectedMessages: [
|
|
292
|
+
{
|
|
293
|
+
role: 'user',
|
|
294
|
+
content: 'Injected from A',
|
|
295
|
+
isMeta: true,
|
|
296
|
+
source: 'skill',
|
|
297
|
+
skillName: 'skill-a',
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
toolCallId: 'call_b',
|
|
303
|
+
content: 'Result B',
|
|
304
|
+
status: 'success',
|
|
305
|
+
injectedMessages: [
|
|
306
|
+
{
|
|
307
|
+
role: 'user',
|
|
308
|
+
content: 'Injected from B',
|
|
309
|
+
isMeta: true,
|
|
310
|
+
source: 'skill',
|
|
311
|
+
skillName: 'skill-b',
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const result = await toolNode.invoke({ messages: [aiMsg] });
|
|
318
|
+
const messages = (result as { messages: BaseMessage[] }).messages;
|
|
319
|
+
|
|
320
|
+
// 2 ToolMessages + 2 injected messages
|
|
321
|
+
expect(messages).toHaveLength(4);
|
|
322
|
+
// ToolMessages come first
|
|
323
|
+
expect(messages[0]._getType()).toBe('tool');
|
|
324
|
+
expect(messages[1]._getType()).toBe('tool');
|
|
325
|
+
// Injected messages come after all ToolMessages
|
|
326
|
+
expect(messages[2]).toBeInstanceOf(HumanMessage);
|
|
327
|
+
expect((messages[2] as HumanMessage).content).toBe('Injected from A');
|
|
328
|
+
expect(messages[3]).toBeInstanceOf(HumanMessage);
|
|
329
|
+
expect((messages[3] as HumanMessage).content).toBe('Injected from B');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('handles mixed mode: direct tools + event-driven with injected messages', async () => {
|
|
333
|
+
const directTool = tool(async () => 'direct result', {
|
|
334
|
+
name: 'handoff_tool',
|
|
335
|
+
description: 'A direct tool',
|
|
336
|
+
schema: z.object({ target: z.string() }),
|
|
337
|
+
}) as unknown as StructuredToolInterface;
|
|
338
|
+
|
|
339
|
+
const eventTool = createDummyTool('event_tool');
|
|
340
|
+
|
|
341
|
+
const toolNode = new ToolNode({
|
|
342
|
+
tools: [directTool, eventTool],
|
|
343
|
+
eventDrivenMode: true,
|
|
344
|
+
agentId: 'test-agent',
|
|
345
|
+
directToolNames: new Set(['handoff_tool']),
|
|
346
|
+
toolCallStepIds: new Map([
|
|
347
|
+
['call_direct', 'step_direct'],
|
|
348
|
+
['call_event', 'step_event'],
|
|
349
|
+
]),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const aiMsg = new AIMessage({
|
|
353
|
+
content: '',
|
|
354
|
+
tool_calls: [
|
|
355
|
+
{
|
|
356
|
+
id: 'call_direct',
|
|
357
|
+
name: 'handoff_tool',
|
|
358
|
+
args: { target: 'agent-2' },
|
|
359
|
+
},
|
|
360
|
+
{ id: 'call_event', name: 'event_tool', args: { x: 'hello' } },
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
mockEventDispatch([
|
|
365
|
+
{
|
|
366
|
+
toolCallId: 'call_event',
|
|
367
|
+
content: 'Event result',
|
|
368
|
+
status: 'success',
|
|
369
|
+
injectedMessages: [
|
|
370
|
+
{
|
|
371
|
+
role: 'user',
|
|
372
|
+
content: 'Skill body from event tool',
|
|
373
|
+
isMeta: true,
|
|
374
|
+
source: 'skill',
|
|
375
|
+
skillName: 'my-skill',
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
const result = await toolNode.invoke({ messages: [aiMsg] });
|
|
382
|
+
const messages = (result as { messages: BaseMessage[] }).messages;
|
|
383
|
+
|
|
384
|
+
// directOutputs first, then eventResult.toolMessages, then eventResult.injected
|
|
385
|
+
expect(messages.length).toBeGreaterThanOrEqual(3);
|
|
386
|
+
// Direct tool result (ToolMessage from runTool)
|
|
387
|
+
expect(messages[0]._getType()).toBe('tool');
|
|
388
|
+
// Event tool result (ToolMessage from dispatchToolEvents)
|
|
389
|
+
expect(messages[1]._getType()).toBe('tool');
|
|
390
|
+
// Injected message last
|
|
391
|
+
const last = messages[messages.length - 1] as HumanMessage;
|
|
392
|
+
expect(last).toBeInstanceOf(HumanMessage);
|
|
393
|
+
expect(last.content).toBe('Skill body from event tool');
|
|
394
|
+
expect(last.additional_kwargs.skillName).toBe('my-skill');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('includes injected messages even when tool result has error status', async () => {
|
|
398
|
+
const toolNode = new ToolNode({
|
|
399
|
+
tools: [createDummyTool()],
|
|
400
|
+
eventDrivenMode: true,
|
|
401
|
+
agentId: 'test-agent',
|
|
402
|
+
toolCallStepIds: new Map([['call_err', 'step_err']]),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const aiMsg = new AIMessage({
|
|
406
|
+
content: '',
|
|
407
|
+
tool_calls: [{ id: 'call_err', name: 'dummy', args: { x: 'fail' } }],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
mockEventDispatch([
|
|
411
|
+
{
|
|
412
|
+
toolCallId: 'call_err',
|
|
413
|
+
content: '',
|
|
414
|
+
status: 'error',
|
|
415
|
+
errorMessage: 'Skill not found',
|
|
416
|
+
injectedMessages: [
|
|
417
|
+
{
|
|
418
|
+
role: 'user',
|
|
419
|
+
content: 'Partial context before failure',
|
|
420
|
+
isMeta: true,
|
|
421
|
+
source: 'skill',
|
|
422
|
+
skillName: 'broken-skill',
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
const result = await toolNode.invoke({ messages: [aiMsg] });
|
|
429
|
+
const messages = (result as { messages: BaseMessage[] }).messages;
|
|
430
|
+
|
|
431
|
+
expect(messages).toHaveLength(2);
|
|
432
|
+
// Error ToolMessage first
|
|
433
|
+
expect(messages[0]._getType()).toBe('tool');
|
|
434
|
+
expect(String(messages[0].content)).toContain('Skill not found');
|
|
435
|
+
// Injected message still included
|
|
436
|
+
const injected = messages[1] as HumanMessage;
|
|
437
|
+
expect(injected).toBeInstanceOf(HumanMessage);
|
|
438
|
+
expect(injected.content).toBe('Partial context before failure');
|
|
439
|
+
expect(injected.additional_kwargs.skillName).toBe('broken-skill');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|