@librechat/agents 3.1.67-dev.4 → 3.1.68

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 (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -23
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +0 -16
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +0 -91
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -0
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +1 -53
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/messages/format.cjs +12 -74
  12. package/dist/cjs/messages/format.cjs.map +1 -1
  13. package/dist/cjs/run.cjs +0 -111
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/summarization/index.cjs +41 -0
  16. package/dist/cjs/summarization/index.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +121 -63
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +140 -304
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/esm/agents/AgentContext.mjs +3 -23
  22. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  23. package/dist/esm/common/enum.mjs +1 -15
  24. package/dist/esm/common/enum.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +0 -91
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/graphs/MultiAgentGraph.mjs +36 -0
  28. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  29. package/dist/esm/main.mjs +2 -13
  30. package/dist/esm/main.mjs.map +1 -1
  31. package/dist/esm/messages/format.mjs +4 -66
  32. package/dist/esm/messages/format.mjs.map +1 -1
  33. package/dist/esm/run.mjs +0 -111
  34. package/dist/esm/run.mjs.map +1 -1
  35. package/dist/esm/summarization/index.mjs +41 -1
  36. package/dist/esm/summarization/index.mjs.map +1 -1
  37. package/dist/esm/summarization/node.mjs +121 -63
  38. package/dist/esm/summarization/node.mjs.map +1 -1
  39. package/dist/esm/tools/ToolNode.mjs +142 -306
  40. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  41. package/dist/types/agents/AgentContext.d.ts +0 -6
  42. package/dist/types/common/enum.d.ts +1 -10
  43. package/dist/types/graphs/Graph.d.ts +0 -2
  44. package/dist/types/graphs/MultiAgentGraph.d.ts +12 -0
  45. package/dist/types/index.d.ts +0 -8
  46. package/dist/types/messages/format.d.ts +1 -2
  47. package/dist/types/run.d.ts +0 -1
  48. package/dist/types/summarization/index.d.ts +2 -0
  49. package/dist/types/summarization/node.d.ts +0 -2
  50. package/dist/types/tools/ToolNode.d.ts +2 -24
  51. package/dist/types/types/graph.d.ts +2 -61
  52. package/dist/types/types/index.d.ts +0 -1
  53. package/dist/types/types/run.d.ts +0 -20
  54. package/dist/types/types/tools.d.ts +1 -38
  55. package/package.json +1 -5
  56. package/src/agents/AgentContext.ts +2 -26
  57. package/src/common/enum.ts +0 -15
  58. package/src/graphs/Graph.ts +0 -113
  59. package/src/graphs/MultiAgentGraph.ts +39 -0
  60. package/src/graphs/__tests__/MultiAgentGraph.test.ts +91 -0
  61. package/src/index.ts +0 -10
  62. package/src/messages/format.ts +4 -74
  63. package/src/run.ts +0 -126
  64. package/src/summarization/__tests__/node.test.ts +42 -0
  65. package/src/summarization/__tests__/trigger.test.ts +100 -1
  66. package/src/summarization/index.ts +47 -0
  67. package/src/summarization/node.ts +149 -77
  68. package/src/tools/ToolNode.ts +169 -391
  69. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  70. package/src/types/graph.ts +1 -80
  71. package/src/types/index.ts +0 -1
  72. package/src/types/run.ts +0 -20
  73. package/src/types/tools.ts +1 -41
  74. package/dist/cjs/hooks/HookRegistry.cjs +0 -162
  75. package/dist/cjs/hooks/HookRegistry.cjs.map +0 -1
  76. package/dist/cjs/hooks/executeHooks.cjs +0 -276
  77. package/dist/cjs/hooks/executeHooks.cjs.map +0 -1
  78. package/dist/cjs/hooks/matchers.cjs +0 -256
  79. package/dist/cjs/hooks/matchers.cjs.map +0 -1
  80. package/dist/cjs/hooks/types.cjs +0 -27
  81. package/dist/cjs/hooks/types.cjs.map +0 -1
  82. package/dist/cjs/tools/BashExecutor.cjs +0 -175
  83. package/dist/cjs/tools/BashExecutor.cjs.map +0 -1
  84. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -296
  85. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +0 -1
  86. package/dist/cjs/tools/ReadFile.cjs +0 -43
  87. package/dist/cjs/tools/ReadFile.cjs.map +0 -1
  88. package/dist/cjs/tools/SkillTool.cjs +0 -50
  89. package/dist/cjs/tools/SkillTool.cjs.map +0 -1
  90. package/dist/cjs/tools/SubagentTool.cjs +0 -92
  91. package/dist/cjs/tools/SubagentTool.cjs.map +0 -1
  92. package/dist/cjs/tools/skillCatalog.cjs +0 -84
  93. package/dist/cjs/tools/skillCatalog.cjs.map +0 -1
  94. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +0 -511
  95. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +0 -1
  96. package/dist/esm/hooks/HookRegistry.mjs +0 -160
  97. package/dist/esm/hooks/HookRegistry.mjs.map +0 -1
  98. package/dist/esm/hooks/executeHooks.mjs +0 -273
  99. package/dist/esm/hooks/executeHooks.mjs.map +0 -1
  100. package/dist/esm/hooks/matchers.mjs +0 -251
  101. package/dist/esm/hooks/matchers.mjs.map +0 -1
  102. package/dist/esm/hooks/types.mjs +0 -25
  103. package/dist/esm/hooks/types.mjs.map +0 -1
  104. package/dist/esm/tools/BashExecutor.mjs +0 -169
  105. package/dist/esm/tools/BashExecutor.mjs.map +0 -1
  106. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -287
  107. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +0 -1
  108. package/dist/esm/tools/ReadFile.mjs +0 -38
  109. package/dist/esm/tools/ReadFile.mjs.map +0 -1
  110. package/dist/esm/tools/SkillTool.mjs +0 -45
  111. package/dist/esm/tools/SkillTool.mjs.map +0 -1
  112. package/dist/esm/tools/SubagentTool.mjs +0 -85
  113. package/dist/esm/tools/SubagentTool.mjs.map +0 -1
  114. package/dist/esm/tools/skillCatalog.mjs +0 -82
  115. package/dist/esm/tools/skillCatalog.mjs.map +0 -1
  116. package/dist/esm/tools/subagent/SubagentExecutor.mjs +0 -505
  117. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +0 -1
  118. package/dist/types/hooks/HookRegistry.d.ts +0 -56
  119. package/dist/types/hooks/executeHooks.d.ts +0 -79
  120. package/dist/types/hooks/index.d.ts +0 -6
  121. package/dist/types/hooks/matchers.d.ts +0 -95
  122. package/dist/types/hooks/types.d.ts +0 -320
  123. package/dist/types/tools/BashExecutor.d.ts +0 -45
  124. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +0 -72
  125. package/dist/types/tools/ReadFile.d.ts +0 -28
  126. package/dist/types/tools/SkillTool.d.ts +0 -40
  127. package/dist/types/tools/SubagentTool.d.ts +0 -36
  128. package/dist/types/tools/skillCatalog.d.ts +0 -19
  129. package/dist/types/tools/subagent/SubagentExecutor.d.ts +0 -137
  130. package/dist/types/tools/subagent/index.d.ts +0 -2
  131. package/dist/types/types/skill.d.ts +0 -9
  132. package/src/hooks/HookRegistry.ts +0 -208
  133. package/src/hooks/__tests__/HookRegistry.test.ts +0 -190
  134. package/src/hooks/__tests__/compactHooks.test.ts +0 -214
  135. package/src/hooks/__tests__/executeHooks.test.ts +0 -1013
  136. package/src/hooks/__tests__/integration.test.ts +0 -337
  137. package/src/hooks/__tests__/matchers.test.ts +0 -238
  138. package/src/hooks/__tests__/toolHooks.test.ts +0 -669
  139. package/src/hooks/executeHooks.ts +0 -375
  140. package/src/hooks/index.ts +0 -57
  141. package/src/hooks/matchers.ts +0 -280
  142. package/src/hooks/types.ts +0 -404
  143. package/src/messages/formatAgentMessages.skills.test.ts +0 -334
  144. package/src/scripts/multi-agent-subagent.ts +0 -246
  145. package/src/scripts/subagent-event-driven-debug.ts +0 -190
  146. package/src/scripts/subagent-tools-debug.ts +0 -160
  147. package/src/specs/subagent.test.ts +0 -305
  148. package/src/tools/BashExecutor.ts +0 -205
  149. package/src/tools/BashProgrammaticToolCalling.ts +0 -397
  150. package/src/tools/ReadFile.ts +0 -39
  151. package/src/tools/SkillTool.ts +0 -46
  152. package/src/tools/SubagentTool.ts +0 -100
  153. package/src/tools/__tests__/ReadFile.test.ts +0 -44
  154. package/src/tools/__tests__/SkillTool.test.ts +0 -442
  155. package/src/tools/__tests__/SubagentExecutor.test.ts +0 -1148
  156. package/src/tools/__tests__/SubagentTool.test.ts +0 -149
  157. package/src/tools/__tests__/skillCatalog.test.ts +0 -161
  158. package/src/tools/__tests__/subagentHooks.test.ts +0 -215
  159. package/src/tools/skillCatalog.ts +0 -126
  160. package/src/tools/subagent/SubagentExecutor.ts +0 -676
  161. package/src/tools/subagent/index.ts +0 -13
  162. package/src/types/skill.ts +0 -11
@@ -1,100 +0,0 @@
1
- import { Constants } from '@/common';
2
- import type { SubagentConfig } from '@/types';
3
- import type { JsonSchemaType, LCTool } from '@/types/tools';
4
-
5
- export const SubagentToolName = Constants.SUBAGENT;
6
-
7
- export const SubagentToolDescription = `Delegate a task to a specialized subagent that runs in an isolated context window. The subagent executes independently and returns only its final text result — all intermediate tool calls, reasoning, and context stay isolated.
8
-
9
- WHEN TO USE:
10
- - The task is self-contained and can be described in a single prompt.
11
- - You want to offload verbose or exploratory work without bloating your own context.
12
- - A specialized subagent is available for the task domain.
13
-
14
- WHAT HAPPENS:
15
- - A fresh agent is created with the task description as its only input.
16
- - The subagent runs to completion using its own tools and context.
17
- - Only the final text response is returned to you.
18
-
19
- CONSTRAINTS:
20
- - subagent_type must match one of the available types listed below.
21
- - The subagent cannot see your conversation history.`;
22
-
23
- const DESCRIPTION_PROP_DESCRIPTION =
24
- 'Complete task description for the subagent. This is the ONLY information it receives — include all necessary context, requirements, and constraints.';
25
-
26
- const SUBAGENT_TYPE_PROP_DESCRIPTION =
27
- 'Which subagent type to delegate to. Must be one of the available types.';
28
-
29
- export const SubagentToolSchema = {
30
- type: 'object',
31
- properties: {
32
- description: {
33
- type: 'string',
34
- description: DESCRIPTION_PROP_DESCRIPTION,
35
- },
36
- subagent_type: {
37
- type: 'string',
38
- description: SUBAGENT_TYPE_PROP_DESCRIPTION,
39
- },
40
- },
41
- required: ['description', 'subagent_type'] as string[],
42
- } as const;
43
-
44
- export const SubagentToolDefinition: LCTool = {
45
- name: SubagentToolName,
46
- description: SubagentToolDescription,
47
- parameters: SubagentToolSchema,
48
- };
49
-
50
- /**
51
- * Build the name, schema, and description params for `tool()` from available configs.
52
- * Used by `Graph.createAgentNode()` when constructing the runtime tool instance.
53
- * Extends `SubagentToolSchema` by populating `subagent_type.enum` dynamically.
54
- */
55
- export function buildSubagentToolParams(configs: SubagentConfig[]): {
56
- name: string;
57
- schema: JsonSchemaType;
58
- description: string;
59
- } {
60
- const types = configs.map((c) => c.type);
61
- const typeDescriptions = configs
62
- .map((c) => `- "${c.type}" (${c.name}): ${c.description}`)
63
- .join('\n');
64
-
65
- return {
66
- name: SubagentToolName,
67
- schema: {
68
- type: 'object',
69
- properties: {
70
- description: {
71
- type: 'string',
72
- description: DESCRIPTION_PROP_DESCRIPTION,
73
- },
74
- subagent_type: {
75
- type: 'string',
76
- enum: types,
77
- description: `${SUBAGENT_TYPE_PROP_DESCRIPTION} Available: ${types.join(', ')}.`,
78
- },
79
- },
80
- required: ['description', 'subagent_type'],
81
- },
82
- description: `${SubagentToolDescription}\n\nAvailable types:\n${typeDescriptions}`,
83
- };
84
- }
85
-
86
- /**
87
- * Create a SubagentTool LCTool definition with dynamic enum and description
88
- * populated from the available subagent configs.
89
- * Used for the tool registry in event-driven mode.
90
- */
91
- export function createSubagentToolDefinition(
92
- configs: SubagentConfig[]
93
- ): LCTool {
94
- const params = buildSubagentToolParams(configs);
95
- return {
96
- name: params.name,
97
- description: params.description,
98
- parameters: params.schema,
99
- };
100
- }
@@ -1,44 +0,0 @@
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
- });
@@ -1,442 +0,0 @@
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
- });