@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
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
2
|
+
import type { TPayload } from '@/types';
|
|
3
|
+
import { formatAgentMessages } from './format';
|
|
4
|
+
import { ContentTypes, Constants } from '@/common';
|
|
5
|
+
|
|
6
|
+
/** Helper to build a skill tool_call content part */
|
|
7
|
+
function skillToolCall(
|
|
8
|
+
id: string,
|
|
9
|
+
skillName: string,
|
|
10
|
+
output = 'Skill loaded.',
|
|
11
|
+
): Record<string, unknown> {
|
|
12
|
+
return {
|
|
13
|
+
type: ContentTypes.TOOL_CALL,
|
|
14
|
+
tool_call: {
|
|
15
|
+
id,
|
|
16
|
+
name: Constants.SKILL_TOOL,
|
|
17
|
+
args: JSON.stringify({ skillName }),
|
|
18
|
+
output,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('formatAgentMessages skill body reconstruction', () => {
|
|
24
|
+
const skillBodies = new Map([
|
|
25
|
+
['pdf-analyzer', '# PDF Analyzer\nAnalyze PDF files step by step.'],
|
|
26
|
+
['code-review', '# Code Review\nReview the code for issues.'],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
describe('with discoveredTools (tools filtering active)', () => {
|
|
30
|
+
const tools = new Set([Constants.SKILL_TOOL, 'web_search']);
|
|
31
|
+
|
|
32
|
+
it('reconstructs HumanMessage after skill ToolMessage', () => {
|
|
33
|
+
const payload: TPayload = [
|
|
34
|
+
{ role: 'user', content: 'Analyze this PDF' },
|
|
35
|
+
{
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: ContentTypes.TEXT,
|
|
40
|
+
[ContentTypes.TEXT]: 'I\'ll invoke the skill.',
|
|
41
|
+
tool_call_ids: ['call_1'],
|
|
42
|
+
},
|
|
43
|
+
skillToolCall('call_1', 'pdf-analyzer'),
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const { messages } = formatAgentMessages(payload, undefined, tools, skillBodies);
|
|
49
|
+
|
|
50
|
+
// user, AI, ToolMessage, injected HumanMessage
|
|
51
|
+
expect(messages.length).toBeGreaterThanOrEqual(4);
|
|
52
|
+
const last = messages[messages.length - 1];
|
|
53
|
+
expect(last).toBeInstanceOf(HumanMessage);
|
|
54
|
+
expect(last.content).toBe('# PDF Analyzer\nAnalyze PDF files step by step.');
|
|
55
|
+
expect((last as HumanMessage).additional_kwargs.source).toBe('skill');
|
|
56
|
+
expect((last as HumanMessage).additional_kwargs.skillName).toBe('pdf-analyzer');
|
|
57
|
+
expect((last as HumanMessage).additional_kwargs.isMeta).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does NOT inject body when skill tool is not in discoveredTools', () => {
|
|
61
|
+
const restrictedTools = new Set(['web_search']); // skill NOT allowed
|
|
62
|
+
const payload: TPayload = [
|
|
63
|
+
{ role: 'user', content: 'Analyze this' },
|
|
64
|
+
{
|
|
65
|
+
role: 'assistant',
|
|
66
|
+
content: [skillToolCall('call_1', 'pdf-analyzer')],
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const { messages } = formatAgentMessages(payload, undefined, restrictedTools, skillBodies);
|
|
71
|
+
|
|
72
|
+
const humanMessages = messages.filter((m) => m instanceof HumanMessage);
|
|
73
|
+
// Only the user message, no injected skill body
|
|
74
|
+
expect(humanMessages).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not inject when skill name is not in skills Map', () => {
|
|
78
|
+
const payload: TPayload = [
|
|
79
|
+
{ role: 'user', content: 'Hello' },
|
|
80
|
+
{
|
|
81
|
+
role: 'assistant',
|
|
82
|
+
content: [skillToolCall('call_1', 'unknown-skill')],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const { messages } = formatAgentMessages(payload, undefined, tools, skillBodies);
|
|
87
|
+
|
|
88
|
+
const humanMessages = messages.filter((m) => m instanceof HumanMessage);
|
|
89
|
+
expect(humanMessages).toHaveLength(1); // only the user message
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('without discoveredTools (no tools filtering)', () => {
|
|
94
|
+
it('reconstructs HumanMessage when skills Map provided', () => {
|
|
95
|
+
const payload: TPayload = [
|
|
96
|
+
{ role: 'user', content: 'Review my code' },
|
|
97
|
+
{
|
|
98
|
+
role: 'assistant',
|
|
99
|
+
content: [skillToolCall('call_1', 'code-review')],
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const { messages } = formatAgentMessages(payload, undefined, undefined, skillBodies);
|
|
104
|
+
|
|
105
|
+
const injected = messages.filter(
|
|
106
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
107
|
+
);
|
|
108
|
+
expect(injected).toHaveLength(1);
|
|
109
|
+
expect(injected[0].content).toBe('# Code Review\nReview the code for issues.');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('no injection when skills Map is undefined', () => {
|
|
113
|
+
const payload: TPayload = [
|
|
114
|
+
{ role: 'user', content: 'Hello' },
|
|
115
|
+
{
|
|
116
|
+
role: 'assistant',
|
|
117
|
+
content: [skillToolCall('call_1', 'pdf-analyzer')],
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const { messages } = formatAgentMessages(payload, undefined, undefined, undefined);
|
|
122
|
+
|
|
123
|
+
const injected = messages.filter(
|
|
124
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
125
|
+
);
|
|
126
|
+
expect(injected).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('no injection when skills Map is empty', () => {
|
|
130
|
+
const payload: TPayload = [
|
|
131
|
+
{ role: 'user', content: 'Hello' },
|
|
132
|
+
{
|
|
133
|
+
role: 'assistant',
|
|
134
|
+
content: [skillToolCall('call_1', 'pdf-analyzer')],
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const { messages } = formatAgentMessages(payload, undefined, undefined, new Map());
|
|
139
|
+
|
|
140
|
+
const injected = messages.filter(
|
|
141
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
142
|
+
);
|
|
143
|
+
expect(injected).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('extractSkillName edge cases', () => {
|
|
148
|
+
const tools = new Set([Constants.SKILL_TOOL]);
|
|
149
|
+
|
|
150
|
+
it('handles object args (not stringified)', () => {
|
|
151
|
+
const payload: TPayload = [
|
|
152
|
+
{ role: 'user', content: 'Go' },
|
|
153
|
+
{
|
|
154
|
+
role: 'assistant',
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: ContentTypes.TOOL_CALL,
|
|
158
|
+
tool_call: {
|
|
159
|
+
id: 'call_1',
|
|
160
|
+
name: Constants.SKILL_TOOL,
|
|
161
|
+
args: { skillName: 'pdf-analyzer' }, // object, not string
|
|
162
|
+
output: 'Loaded.',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const { messages } = formatAgentMessages(payload, undefined, tools, skillBodies);
|
|
170
|
+
|
|
171
|
+
const injected = messages.filter(
|
|
172
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
173
|
+
);
|
|
174
|
+
expect(injected).toHaveLength(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('gracefully skips malformed JSON args', () => {
|
|
178
|
+
const payload: TPayload = [
|
|
179
|
+
{ role: 'user', content: 'Go' },
|
|
180
|
+
{
|
|
181
|
+
role: 'assistant',
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: ContentTypes.TOOL_CALL,
|
|
185
|
+
tool_call: {
|
|
186
|
+
id: 'call_1',
|
|
187
|
+
name: Constants.SKILL_TOOL,
|
|
188
|
+
args: '{bad json',
|
|
189
|
+
output: 'Loaded.',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const { messages } = formatAgentMessages(payload, undefined, tools, skillBodies);
|
|
197
|
+
|
|
198
|
+
const injected = messages.filter(
|
|
199
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
200
|
+
);
|
|
201
|
+
expect(injected).toHaveLength(0); // gracefully skipped
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('skips empty skillName', () => {
|
|
205
|
+
const payload: TPayload = [
|
|
206
|
+
{ role: 'user', content: 'Go' },
|
|
207
|
+
{
|
|
208
|
+
role: 'assistant',
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: ContentTypes.TOOL_CALL,
|
|
212
|
+
tool_call: {
|
|
213
|
+
id: 'call_1',
|
|
214
|
+
name: Constants.SKILL_TOOL,
|
|
215
|
+
args: JSON.stringify({ skillName: '' }),
|
|
216
|
+
output: 'Loaded.',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const { messages } = formatAgentMessages(payload, undefined, tools, skillBodies);
|
|
224
|
+
|
|
225
|
+
const injected = messages.filter(
|
|
226
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
227
|
+
);
|
|
228
|
+
expect(injected).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('deduplication', () => {
|
|
233
|
+
const tools = new Set([Constants.SKILL_TOOL]);
|
|
234
|
+
|
|
235
|
+
it('injects body only once when same skill invoked twice in one message', () => {
|
|
236
|
+
const payload: TPayload = [
|
|
237
|
+
{ role: 'user', content: 'Go' },
|
|
238
|
+
{
|
|
239
|
+
role: 'assistant',
|
|
240
|
+
content: [
|
|
241
|
+
skillToolCall('call_1', 'pdf-analyzer'),
|
|
242
|
+
skillToolCall('call_2', 'pdf-analyzer'),
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const { messages } = formatAgentMessages(payload, undefined, tools, skillBodies);
|
|
248
|
+
|
|
249
|
+
const injected = messages.filter(
|
|
250
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
251
|
+
);
|
|
252
|
+
expect(injected).toHaveLength(1);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('injects body for each distinct skill invoked', () => {
|
|
256
|
+
const payload: TPayload = [
|
|
257
|
+
{ role: 'user', content: 'Go' },
|
|
258
|
+
{
|
|
259
|
+
role: 'assistant',
|
|
260
|
+
content: [
|
|
261
|
+
skillToolCall('call_1', 'pdf-analyzer'),
|
|
262
|
+
skillToolCall('call_2', 'code-review'),
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const { messages } = formatAgentMessages(payload, undefined, tools, skillBodies);
|
|
268
|
+
|
|
269
|
+
const injected = messages.filter(
|
|
270
|
+
(m) => m instanceof HumanMessage && (m as HumanMessage).additional_kwargs?.source === 'skill',
|
|
271
|
+
);
|
|
272
|
+
expect(injected).toHaveLength(2);
|
|
273
|
+
const names = injected.map((m) => (m as HumanMessage).additional_kwargs.skillName);
|
|
274
|
+
expect(names).toContain('pdf-analyzer');
|
|
275
|
+
expect(names).toContain('code-review');
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('indexTokenCountMap distribution', () => {
|
|
280
|
+
const tools = new Set([Constants.SKILL_TOOL]);
|
|
281
|
+
|
|
282
|
+
it('excludes injected HumanMessages from assistant token distribution', () => {
|
|
283
|
+
const payload: TPayload = [
|
|
284
|
+
{ role: 'user', content: 'Analyze this' },
|
|
285
|
+
{
|
|
286
|
+
role: 'assistant',
|
|
287
|
+
content: [
|
|
288
|
+
{
|
|
289
|
+
type: ContentTypes.TEXT,
|
|
290
|
+
[ContentTypes.TEXT]: 'Invoking skill.',
|
|
291
|
+
tool_call_ids: ['call_1'],
|
|
292
|
+
},
|
|
293
|
+
skillToolCall('call_1', 'pdf-analyzer'),
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
const inputTokenMap: Record<number, number | undefined> = {
|
|
299
|
+
0: 100, // user message
|
|
300
|
+
1: 500, // assistant message
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const { messages, indexTokenCountMap } = formatAgentMessages(
|
|
304
|
+
payload,
|
|
305
|
+
inputTokenMap,
|
|
306
|
+
tools,
|
|
307
|
+
skillBodies,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// There should be messages: user, AI, ToolMessage, injected HumanMessage
|
|
311
|
+
expect(messages.length).toBeGreaterThanOrEqual(4);
|
|
312
|
+
const lastMsg = messages[messages.length - 1];
|
|
313
|
+
expect(lastMsg).toBeInstanceOf(HumanMessage);
|
|
314
|
+
expect((lastMsg as HumanMessage).additional_kwargs.source).toBe('skill');
|
|
315
|
+
|
|
316
|
+
// Token map must be defined when input was provided
|
|
317
|
+
expect(indexTokenCountMap).toBeDefined();
|
|
318
|
+
|
|
319
|
+
// The injected HumanMessage's index should NOT be in the token map
|
|
320
|
+
const injectedIndex = messages.length - 1;
|
|
321
|
+
expect(indexTokenCountMap![injectedIndex]).toBeUndefined();
|
|
322
|
+
|
|
323
|
+
// The assistant's 500 tokens should be distributed only across
|
|
324
|
+
// the AI + ToolMessage, NOT the injected HumanMessage
|
|
325
|
+
let assistantTotal = 0;
|
|
326
|
+
for (const [idx, count] of Object.entries(indexTokenCountMap!)) {
|
|
327
|
+
if (Number(idx) > 0 && Number(idx) < injectedIndex) {
|
|
328
|
+
assistantTotal += count ?? 0;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
expect(assistantTotal).toBe(500);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
package/src/messages/prune.ts
CHANGED
|
@@ -683,10 +683,17 @@ export function getMessagesWithinTokenLimit({
|
|
|
683
683
|
) as ThinkingContentText | undefined;
|
|
684
684
|
thinkingStartIndex = thinkingBlock != null ? currentIndex : -1;
|
|
685
685
|
}
|
|
686
|
-
/**
|
|
686
|
+
/**
|
|
687
|
+
* Exited the trailing assistant/tool sequence without finding a
|
|
688
|
+
* thinking block. Anthropic does not require Claude to emit a
|
|
689
|
+
* thinking block before every tool call, so the absence of one is
|
|
690
|
+
* a valid sequence — clear thinkingEndIndex so the pruner does not
|
|
691
|
+
* treat it as malformed.
|
|
692
|
+
*/
|
|
687
693
|
if (
|
|
688
694
|
thinkingEndIndex > -1 &&
|
|
689
|
-
|
|
695
|
+
thinkingStartIndex < 0 &&
|
|
696
|
+
!thinkingBlock &&
|
|
690
697
|
messageType !== 'ai' &&
|
|
691
698
|
messageType !== 'tool'
|
|
692
699
|
) {
|
package/src/run.ts
CHANGED
|
@@ -22,8 +22,10 @@ import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
|
|
|
22
22
|
import { StandardGraph } from '@/graphs/Graph';
|
|
23
23
|
import { initializeModel } from '@/llm/init';
|
|
24
24
|
import { HandlerRegistry } from '@/events';
|
|
25
|
+
import { executeHooks } from '@/hooks';
|
|
25
26
|
import { isOpenAILike } from '@/utils/llm';
|
|
26
27
|
import { isPresent } from '@/utils/misc';
|
|
28
|
+
import type { HookRegistry } from '@/hooks';
|
|
27
29
|
|
|
28
30
|
export const defaultOmitOptions = new Set([
|
|
29
31
|
'stream',
|
|
@@ -42,6 +44,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
42
44
|
id: string;
|
|
43
45
|
private tokenCounter?: t.TokenCounter;
|
|
44
46
|
private handlerRegistry?: HandlerRegistry;
|
|
47
|
+
private hookRegistry?: HookRegistry;
|
|
48
|
+
private toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
45
49
|
private indexTokenCountMap?: Record<string, number>;
|
|
46
50
|
calibrationRatio: number = 1;
|
|
47
51
|
graphRunnable?: t.CompiledStateWorkflow;
|
|
@@ -74,6 +78,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
this.handlerRegistry = handlerRegistry;
|
|
81
|
+
this.hookRegistry = config.hooks;
|
|
82
|
+
this.toolOutputReferences = config.toolOutputReferences;
|
|
77
83
|
|
|
78
84
|
if (!config.graphConfig) {
|
|
79
85
|
throw new Error('Graph config not provided');
|
|
@@ -95,6 +101,12 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
if (config.initialSessions && this.Graph) {
|
|
105
|
+
for (const [key, value] of config.initialSessions) {
|
|
106
|
+
this.Graph.sessions.set(key, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
this.returnContent = config.returnContent ?? false;
|
|
99
111
|
this.skipCleanup = config.skipCleanup ?? false;
|
|
100
112
|
}
|
|
@@ -143,6 +155,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
143
155
|
});
|
|
144
156
|
/** Propagate compile options from graph config */
|
|
145
157
|
standardGraph.compileOptions = config.compileOptions;
|
|
158
|
+
standardGraph.hookRegistry = this.hookRegistry;
|
|
159
|
+
standardGraph.toolOutputReferences = this.toolOutputReferences;
|
|
146
160
|
this.Graph = standardGraph;
|
|
147
161
|
return standardGraph.createWorkflow();
|
|
148
162
|
}
|
|
@@ -165,6 +179,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
165
179
|
multiAgentGraph.compileOptions = compileOptions;
|
|
166
180
|
}
|
|
167
181
|
|
|
182
|
+
multiAgentGraph.hookRegistry = this.hookRegistry;
|
|
183
|
+
multiAgentGraph.toolOutputReferences = this.toolOutputReferences;
|
|
168
184
|
this.Graph = multiAgentGraph;
|
|
169
185
|
return multiAgentGraph.createWorkflow();
|
|
170
186
|
}
|
|
@@ -332,6 +348,47 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
332
348
|
run_id: this.id,
|
|
333
349
|
});
|
|
334
350
|
|
|
351
|
+
const threadId = config.configurable.thread_id as string | undefined;
|
|
352
|
+
|
|
353
|
+
if (this.hookRegistry != null) {
|
|
354
|
+
await executeHooks({
|
|
355
|
+
registry: this.hookRegistry,
|
|
356
|
+
input: {
|
|
357
|
+
hook_event_name: 'RunStart',
|
|
358
|
+
runId: this.id,
|
|
359
|
+
threadId,
|
|
360
|
+
agentId: this.Graph.defaultAgentId,
|
|
361
|
+
messages: inputs.messages,
|
|
362
|
+
},
|
|
363
|
+
sessionId: this.id,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const lastHuman = findLastMessageOfType(inputs.messages, 'human');
|
|
367
|
+
if (lastHuman != null) {
|
|
368
|
+
const promptResult = await executeHooks({
|
|
369
|
+
registry: this.hookRegistry,
|
|
370
|
+
input: {
|
|
371
|
+
hook_event_name: 'UserPromptSubmit',
|
|
372
|
+
runId: this.id,
|
|
373
|
+
threadId,
|
|
374
|
+
agentId: this.Graph.defaultAgentId,
|
|
375
|
+
prompt: extractPromptText(lastHuman),
|
|
376
|
+
// attachments: not yet wired — Phase 2 will extract
|
|
377
|
+
// non-text content blocks (images, files) from messages
|
|
378
|
+
},
|
|
379
|
+
sessionId: this.id,
|
|
380
|
+
});
|
|
381
|
+
if (
|
|
382
|
+
promptResult.decision === 'deny' ||
|
|
383
|
+
promptResult.decision === 'ask'
|
|
384
|
+
) {
|
|
385
|
+
this.hookRegistry.clearSession(this.id);
|
|
386
|
+
config.callbacks = undefined;
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
335
392
|
const stream = this.graphRunnable.streamEvents(inputs, config, {
|
|
336
393
|
raiseError: true,
|
|
337
394
|
/**
|
|
@@ -361,7 +418,45 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
361
418
|
await handler.handle(eventName, data, metadata, this.Graph);
|
|
362
419
|
}
|
|
363
420
|
}
|
|
421
|
+
|
|
422
|
+
if (this.hookRegistry?.hasHookFor('Stop', this.id) === true) {
|
|
423
|
+
await executeHooks({
|
|
424
|
+
registry: this.hookRegistry,
|
|
425
|
+
input: {
|
|
426
|
+
hook_event_name: 'Stop',
|
|
427
|
+
runId: this.id,
|
|
428
|
+
threadId,
|
|
429
|
+
agentId: this.Graph.defaultAgentId,
|
|
430
|
+
messages: this.Graph.getRunMessages() ?? inputs.messages,
|
|
431
|
+
stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
|
|
432
|
+
},
|
|
433
|
+
sessionId: this.id,
|
|
434
|
+
}).catch(() => {
|
|
435
|
+
/* Stop hook errors must not masquerade as stream failures */
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (this.hookRegistry?.hasHookFor('StopFailure', this.id) === true) {
|
|
440
|
+
const runMessages = this.Graph.getRunMessages() ?? [];
|
|
441
|
+
await executeHooks({
|
|
442
|
+
registry: this.hookRegistry,
|
|
443
|
+
input: {
|
|
444
|
+
hook_event_name: 'StopFailure',
|
|
445
|
+
runId: this.id,
|
|
446
|
+
threadId,
|
|
447
|
+
agentId: this.Graph.defaultAgentId,
|
|
448
|
+
error: err instanceof Error ? err.message : String(err),
|
|
449
|
+
lastAssistantMessage: findLastMessageOfType(runMessages, 'ai'),
|
|
450
|
+
},
|
|
451
|
+
sessionId: this.id,
|
|
452
|
+
}).catch(() => {
|
|
453
|
+
/* swallow hook errors — the original error must propagate */
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
throw err;
|
|
364
457
|
} finally {
|
|
458
|
+
this.hookRegistry?.clearSession(this.id);
|
|
459
|
+
|
|
365
460
|
/**
|
|
366
461
|
* Break the reference chain that keeps heavy data alive via
|
|
367
462
|
* LangGraph's internal `__pregel_scratchpad.currentTaskInput` →
|
|
@@ -557,3 +652,38 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
557
652
|
}
|
|
558
653
|
}
|
|
559
654
|
}
|
|
655
|
+
|
|
656
|
+
function findLastMessageOfType(
|
|
657
|
+
messages: BaseMessage[],
|
|
658
|
+
type: string
|
|
659
|
+
): BaseMessage | undefined {
|
|
660
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
661
|
+
if (messages[i].getType() === type) {
|
|
662
|
+
return messages[i];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function extractPromptText(message: BaseMessage): string {
|
|
669
|
+
const content = message.content;
|
|
670
|
+
if (typeof content === 'string') {
|
|
671
|
+
return content;
|
|
672
|
+
}
|
|
673
|
+
if (!Array.isArray(content)) {
|
|
674
|
+
return String(content);
|
|
675
|
+
}
|
|
676
|
+
const parts: string[] = [];
|
|
677
|
+
for (const block of content) {
|
|
678
|
+
if (
|
|
679
|
+
typeof block === 'object' &&
|
|
680
|
+
'type' in block &&
|
|
681
|
+
block.type === 'text' &&
|
|
682
|
+
'text' in block &&
|
|
683
|
+
typeof block.text === 'string'
|
|
684
|
+
) {
|
|
685
|
+
parts.push(block.text);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return parts.join('\n');
|
|
689
|
+
}
|