@librechat/agents 3.1.64 → 3.1.66-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.
Files changed (126) hide show
  1. package/dist/cjs/common/enum.cjs +13 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +3 -0
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  6. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  7. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  8. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  9. package/dist/cjs/hooks/matchers.cjs +256 -0
  10. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  11. package/dist/cjs/hooks/types.cjs +27 -0
  12. package/dist/cjs/hooks/types.cjs.map +1 -0
  13. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  14. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +69 -54
  15. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +40 -0
  17. package/dist/cjs/main.cjs.map +1 -1
  18. package/dist/cjs/messages/core.cjs +8 -1
  19. package/dist/cjs/messages/core.cjs.map +1 -1
  20. package/dist/cjs/messages/format.cjs +74 -12
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/run.cjs +111 -0
  23. package/dist/cjs/run.cjs.map +1 -1
  24. package/dist/cjs/tools/BashExecutor.cjs +175 -0
  25. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  26. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
  27. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  28. package/dist/cjs/tools/ReadFile.cjs +43 -0
  29. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  30. package/dist/cjs/tools/SkillTool.cjs +50 -0
  31. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  32. package/dist/cjs/tools/ToolNode.cjs +304 -140
  33. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  34. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  35. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  36. package/dist/esm/common/enum.mjs +12 -1
  37. package/dist/esm/common/enum.mjs.map +1 -1
  38. package/dist/esm/graphs/Graph.mjs +3 -0
  39. package/dist/esm/graphs/Graph.mjs.map +1 -1
  40. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  41. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  42. package/dist/esm/hooks/executeHooks.mjs +273 -0
  43. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  44. package/dist/esm/hooks/matchers.mjs +251 -0
  45. package/dist/esm/hooks/matchers.mjs.map +1 -0
  46. package/dist/esm/hooks/types.mjs +25 -0
  47. package/dist/esm/hooks/types.mjs.map +1 -0
  48. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  49. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +69 -54
  50. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  51. package/dist/esm/main.mjs +10 -1
  52. package/dist/esm/main.mjs.map +1 -1
  53. package/dist/esm/messages/core.mjs +8 -1
  54. package/dist/esm/messages/core.mjs.map +1 -1
  55. package/dist/esm/messages/format.mjs +66 -4
  56. package/dist/esm/messages/format.mjs.map +1 -1
  57. package/dist/esm/run.mjs +111 -0
  58. package/dist/esm/run.mjs.map +1 -1
  59. package/dist/esm/tools/BashExecutor.mjs +169 -0
  60. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  61. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
  62. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  63. package/dist/esm/tools/ReadFile.mjs +38 -0
  64. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  65. package/dist/esm/tools/SkillTool.mjs +45 -0
  66. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  67. package/dist/esm/tools/ToolNode.mjs +306 -142
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/skillCatalog.mjs +82 -0
  70. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  71. package/dist/types/common/enum.d.ts +7 -1
  72. package/dist/types/graphs/Graph.d.ts +2 -0
  73. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  74. package/dist/types/hooks/executeHooks.d.ts +79 -0
  75. package/dist/types/hooks/index.d.ts +6 -0
  76. package/dist/types/hooks/matchers.d.ts +95 -0
  77. package/dist/types/hooks/types.d.ts +309 -0
  78. package/dist/types/index.d.ts +6 -0
  79. package/dist/types/llm/anthropic/types.d.ts +1 -1
  80. package/dist/types/messages/format.d.ts +2 -1
  81. package/dist/types/run.d.ts +1 -0
  82. package/dist/types/tools/BashExecutor.d.ts +45 -0
  83. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  84. package/dist/types/tools/ReadFile.d.ts +28 -0
  85. package/dist/types/tools/SkillTool.d.ts +40 -0
  86. package/dist/types/tools/ToolNode.d.ts +24 -2
  87. package/dist/types/tools/skillCatalog.d.ts +19 -0
  88. package/dist/types/types/index.d.ts +1 -0
  89. package/dist/types/types/run.d.ts +20 -0
  90. package/dist/types/types/skill.d.ts +9 -0
  91. package/dist/types/types/tools.d.ts +38 -1
  92. package/package.json +2 -2
  93. package/src/common/enum.ts +12 -0
  94. package/src/graphs/Graph.ts +4 -0
  95. package/src/hooks/HookRegistry.ts +208 -0
  96. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  97. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  98. package/src/hooks/__tests__/integration.test.ts +337 -0
  99. package/src/hooks/__tests__/matchers.test.ts +238 -0
  100. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  101. package/src/hooks/executeHooks.ts +375 -0
  102. package/src/hooks/index.ts +55 -0
  103. package/src/hooks/matchers.ts +280 -0
  104. package/src/hooks/types.ts +388 -0
  105. package/src/index.ts +8 -0
  106. package/src/llm/anthropic/types.ts +1 -1
  107. package/src/llm/anthropic/utils/message_inputs.ts +93 -68
  108. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +349 -0
  109. package/src/messages/core.ts +8 -1
  110. package/src/messages/format.ts +74 -4
  111. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  112. package/src/run.ts +126 -0
  113. package/src/tools/BashExecutor.ts +205 -0
  114. package/src/tools/BashProgrammaticToolCalling.ts +397 -0
  115. package/src/tools/ReadFile.ts +39 -0
  116. package/src/tools/SkillTool.ts +46 -0
  117. package/src/tools/ToolNode.ts +391 -169
  118. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  119. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  120. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  121. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  122. package/src/tools/skillCatalog.ts +126 -0
  123. package/src/types/index.ts +1 -0
  124. package/src/types/run.ts +20 -0
  125. package/src/types/skill.ts +11 -0
  126. package/src/types/tools.ts +41 -1
@@ -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
+ });
@@ -216,7 +216,7 @@ describe('ToolNode code execution session management', () => {
216
216
  toolNode as unknown as {
217
217
  storeCodeSessionFromResults: (
218
218
  results: t.ToolExecuteResult[],
219
- requests: t.ToolCallRequest[]
219
+ requestMap: Map<string, t.ToolCallRequest>
220
220
  ) => void;
221
221
  }
222
222
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -233,7 +233,7 @@ describe('ToolNode code execution session management', () => {
233
233
  status: 'success',
234
234
  },
235
235
  ],
236
- [{ id: 'tc1', name: Constants.EXECUTE_CODE, args: {} }]
236
+ new Map([['tc1', { id: 'tc1', name: Constants.EXECUTE_CODE, args: {} }]])
237
237
  );
238
238
 
239
239
  const stored = sessions.get(
@@ -265,7 +265,7 @@ describe('ToolNode code execution session management', () => {
265
265
  toolNode as unknown as {
266
266
  storeCodeSessionFromResults: (
267
267
  results: t.ToolExecuteResult[],
268
- requests: t.ToolCallRequest[]
268
+ requestMap: Map<string, t.ToolCallRequest>
269
269
  ) => void;
270
270
  }
271
271
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -279,7 +279,7 @@ describe('ToolNode code execution session management', () => {
279
279
  status: 'success',
280
280
  },
281
281
  ],
282
- [{ id: 'tc2', name: Constants.EXECUTE_CODE, args: {} }]
282
+ new Map([['tc2', { id: 'tc2', name: Constants.EXECUTE_CODE, args: {} }]])
283
283
  );
284
284
 
285
285
  const stored = sessions.get(
@@ -312,7 +312,7 @@ describe('ToolNode code execution session management', () => {
312
312
  toolNode as unknown as {
313
313
  storeCodeSessionFromResults: (
314
314
  results: t.ToolExecuteResult[],
315
- requests: t.ToolCallRequest[]
315
+ requestMap: Map<string, t.ToolCallRequest>
316
316
  ) => void;
317
317
  }
318
318
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -329,7 +329,7 @@ describe('ToolNode code execution session management', () => {
329
329
  status: 'success',
330
330
  },
331
331
  ],
332
- [{ id: 'tc3', name: Constants.EXECUTE_CODE, args: {} }]
332
+ new Map([['tc3', { id: 'tc3', name: Constants.EXECUTE_CODE, args: {} }]])
333
333
  );
334
334
 
335
335
  const stored = sessions.get(
@@ -365,7 +365,7 @@ describe('ToolNode code execution session management', () => {
365
365
  toolNode as unknown as {
366
366
  storeCodeSessionFromResults: (
367
367
  results: t.ToolExecuteResult[],
368
- requests: t.ToolCallRequest[]
368
+ requestMap: Map<string, t.ToolCallRequest>
369
369
  ) => void;
370
370
  }
371
371
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -379,7 +379,7 @@ describe('ToolNode code execution session management', () => {
379
379
  status: 'success',
380
380
  },
381
381
  ],
382
- [{ id: 'tc4', name: Constants.EXECUTE_CODE, args: {} }]
382
+ new Map([['tc4', { id: 'tc4', name: Constants.EXECUTE_CODE, args: {} }]])
383
383
  );
384
384
 
385
385
  const stored = sessions.get(
@@ -404,7 +404,7 @@ describe('ToolNode code execution session management', () => {
404
404
  toolNode as unknown as {
405
405
  storeCodeSessionFromResults: (
406
406
  results: t.ToolExecuteResult[],
407
- requests: t.ToolCallRequest[]
407
+ requestMap: Map<string, t.ToolCallRequest>
408
408
  ) => void;
409
409
  }
410
410
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -418,7 +418,7 @@ describe('ToolNode code execution session management', () => {
418
418
  status: 'success',
419
419
  },
420
420
  ],
421
- [{ id: 'tc5', name: 'web_search', args: {} }]
421
+ new Map([['tc5', { id: 'tc5', name: 'web_search', args: {} }]])
422
422
  );
423
423
 
424
424
  expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
@@ -438,7 +438,7 @@ describe('ToolNode code execution session management', () => {
438
438
  toolNode as unknown as {
439
439
  storeCodeSessionFromResults: (
440
440
  results: t.ToolExecuteResult[],
441
- requests: t.ToolCallRequest[]
441
+ requestMap: Map<string, t.ToolCallRequest>
442
442
  ) => void;
443
443
  }
444
444
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -456,7 +456,7 @@ describe('ToolNode code execution session management', () => {
456
456
  errorMessage: 'execution failed',
457
457
  },
458
458
  ],
459
- [{ id: 'tc6', name: Constants.EXECUTE_CODE, args: {} }]
459
+ new Map([['tc6', { id: 'tc6', name: Constants.EXECUTE_CODE, args: {} }]])
460
460
  );
461
461
 
462
462
  expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);