@librechat/agents 3.0.42 → 3.0.43
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 +134 -70
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +1 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +5 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +85 -0
- package/dist/cjs/messages/tools.cjs.map +1 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +55 -32
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +30 -13
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +134 -70
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +8 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +82 -0
- package/dist/esm/messages/tools.mjs.map +1 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +54 -33
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +30 -13
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +37 -17
- package/dist/types/common/enum.d.ts +1 -1
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/tools.d.ts +17 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +15 -23
- package/dist/types/tools/ToolNode.d.ts +9 -7
- package/dist/types/types/tools.d.ts +5 -5
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +157 -85
- package/src/agents/__tests__/AgentContext.test.ts +805 -0
- package/src/common/enum.ts +1 -1
- package/src/graphs/Graph.ts +9 -21
- package/src/messages/__tests__/tools.test.ts +473 -0
- package/src/messages/index.ts +1 -0
- package/src/messages/tools.ts +99 -0
- package/src/scripts/code_exec_ptc.ts +78 -21
- package/src/scripts/programmatic_exec.ts +3 -3
- package/src/scripts/programmatic_exec_agent.ts +4 -4
- package/src/tools/ProgrammaticToolCalling.ts +71 -39
- package/src/tools/ToolNode.ts +33 -14
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +9 -9
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +180 -5
- package/src/types/tools.ts +3 -5
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
// src/agents/__tests__/AgentContext.test.ts
|
|
2
|
+
import { AgentContext } from '../AgentContext';
|
|
3
|
+
import { Providers } from '@/common';
|
|
4
|
+
import type * as t from '@/types';
|
|
5
|
+
|
|
6
|
+
describe('AgentContext', () => {
|
|
7
|
+
type ContextOptions = {
|
|
8
|
+
agentConfig?: Partial<t.AgentInputs>;
|
|
9
|
+
tokenCounter?: t.TokenCounter;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const createBasicContext = (options: ContextOptions = {}): AgentContext => {
|
|
13
|
+
const { agentConfig = {}, tokenCounter } = options;
|
|
14
|
+
return AgentContext.fromConfig(
|
|
15
|
+
{
|
|
16
|
+
agentId: 'test-agent',
|
|
17
|
+
provider: Providers.OPENAI,
|
|
18
|
+
instructions: 'Test instructions',
|
|
19
|
+
...agentConfig,
|
|
20
|
+
},
|
|
21
|
+
tokenCounter
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const createMockTool = (name: string): t.GenericTool =>
|
|
26
|
+
({
|
|
27
|
+
name,
|
|
28
|
+
description: `Mock ${name} tool`,
|
|
29
|
+
invoke: jest.fn(),
|
|
30
|
+
schema: { type: 'object', properties: {} },
|
|
31
|
+
}) as unknown as t.GenericTool;
|
|
32
|
+
|
|
33
|
+
describe('System Runnable - Lazy Creation', () => {
|
|
34
|
+
it('creates system runnable on first access', () => {
|
|
35
|
+
const ctx = createBasicContext({
|
|
36
|
+
agentConfig: { instructions: 'Hello world' },
|
|
37
|
+
});
|
|
38
|
+
expect(ctx.systemRunnable).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns cached system runnable on subsequent access', () => {
|
|
42
|
+
const ctx = createBasicContext({
|
|
43
|
+
agentConfig: { instructions: 'Hello world' },
|
|
44
|
+
});
|
|
45
|
+
const first = ctx.systemRunnable;
|
|
46
|
+
const second = ctx.systemRunnable;
|
|
47
|
+
expect(first).toBe(second);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns undefined when no instructions provided', () => {
|
|
51
|
+
const ctx = createBasicContext({
|
|
52
|
+
agentConfig: {
|
|
53
|
+
instructions: undefined,
|
|
54
|
+
additional_instructions: undefined,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
expect(ctx.systemRunnable).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('includes additional_instructions in system message', () => {
|
|
61
|
+
const ctx = createBasicContext({
|
|
62
|
+
agentConfig: {
|
|
63
|
+
instructions: 'Base instructions',
|
|
64
|
+
additional_instructions: 'Additional instructions',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
expect(ctx.systemRunnable).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('System Runnable - Stale Flag', () => {
|
|
72
|
+
it('rebuilds when marked stale via markToolsAsDiscovered', () => {
|
|
73
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
74
|
+
[
|
|
75
|
+
'deferred_tool',
|
|
76
|
+
{
|
|
77
|
+
name: 'deferred_tool',
|
|
78
|
+
description: 'A deferred code-only tool',
|
|
79
|
+
allowed_callers: ['code_execution'],
|
|
80
|
+
defer_loading: true,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const ctx = createBasicContext({
|
|
86
|
+
agentConfig: { instructions: 'Test', toolRegistry },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const firstRunnable = ctx.systemRunnable;
|
|
90
|
+
const hasNew = ctx.markToolsAsDiscovered(['deferred_tool']);
|
|
91
|
+
expect(hasNew).toBe(true);
|
|
92
|
+
|
|
93
|
+
const secondRunnable = ctx.systemRunnable;
|
|
94
|
+
expect(secondRunnable).not.toBe(firstRunnable);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not rebuild when discovering already-known tools', () => {
|
|
98
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
99
|
+
[
|
|
100
|
+
'tool1',
|
|
101
|
+
{
|
|
102
|
+
name: 'tool1',
|
|
103
|
+
description: 'Tool 1',
|
|
104
|
+
allowed_callers: ['code_execution'],
|
|
105
|
+
defer_loading: true,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const ctx = createBasicContext({
|
|
111
|
+
agentConfig: { instructions: 'Test', toolRegistry },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
ctx.markToolsAsDiscovered(['tool1']);
|
|
115
|
+
const firstRunnable = ctx.systemRunnable;
|
|
116
|
+
|
|
117
|
+
const hasNew = ctx.markToolsAsDiscovered(['tool1']);
|
|
118
|
+
expect(hasNew).toBe(false);
|
|
119
|
+
|
|
120
|
+
const secondRunnable = ctx.systemRunnable;
|
|
121
|
+
expect(secondRunnable).toBe(firstRunnable);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('markToolsAsDiscovered', () => {
|
|
126
|
+
it('returns true when new tools are discovered', () => {
|
|
127
|
+
const ctx = createBasicContext();
|
|
128
|
+
const result = ctx.markToolsAsDiscovered(['tool1', 'tool2']);
|
|
129
|
+
expect(result).toBe(true);
|
|
130
|
+
expect(ctx.discoveredToolNames.has('tool1')).toBe(true);
|
|
131
|
+
expect(ctx.discoveredToolNames.has('tool2')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns false when all tools already discovered', () => {
|
|
135
|
+
const ctx = createBasicContext();
|
|
136
|
+
ctx.markToolsAsDiscovered(['tool1']);
|
|
137
|
+
const result = ctx.markToolsAsDiscovered(['tool1']);
|
|
138
|
+
expect(result).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns true if at least one tool is new', () => {
|
|
142
|
+
const ctx = createBasicContext();
|
|
143
|
+
ctx.markToolsAsDiscovered(['tool1']);
|
|
144
|
+
const result = ctx.markToolsAsDiscovered(['tool1', 'tool2']);
|
|
145
|
+
expect(result).toBe(true);
|
|
146
|
+
expect(ctx.discoveredToolNames.size).toBe(2);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('handles empty array gracefully', () => {
|
|
150
|
+
const ctx = createBasicContext();
|
|
151
|
+
const result = ctx.markToolsAsDiscovered([]);
|
|
152
|
+
expect(result).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('buildProgrammaticOnlyToolsInstructions', () => {
|
|
157
|
+
it('includes code_execution-only tools in system message', () => {
|
|
158
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
159
|
+
[
|
|
160
|
+
'programmatic_tool',
|
|
161
|
+
{
|
|
162
|
+
name: 'programmatic_tool',
|
|
163
|
+
description: 'Only callable via code execution',
|
|
164
|
+
allowed_callers: ['code_execution'],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const ctx = createBasicContext({
|
|
170
|
+
agentConfig: { instructions: 'Base', toolRegistry },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const runnable = ctx.systemRunnable;
|
|
174
|
+
expect(runnable).toBeDefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('excludes direct-callable tools from programmatic section', () => {
|
|
178
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
179
|
+
[
|
|
180
|
+
'direct_tool',
|
|
181
|
+
{
|
|
182
|
+
name: 'direct_tool',
|
|
183
|
+
description: 'Direct callable',
|
|
184
|
+
allowed_callers: ['direct'],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
'both_tool',
|
|
189
|
+
{
|
|
190
|
+
name: 'both_tool',
|
|
191
|
+
description: 'Both direct and code',
|
|
192
|
+
allowed_callers: ['direct', 'code_execution'],
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
const ctx = createBasicContext({
|
|
198
|
+
agentConfig: { instructions: 'Base', toolRegistry },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(ctx.systemRunnable).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('excludes deferred code_execution-only tools until discovered', () => {
|
|
205
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
206
|
+
[
|
|
207
|
+
'deferred_code_tool',
|
|
208
|
+
{
|
|
209
|
+
name: 'deferred_code_tool',
|
|
210
|
+
description: 'Deferred and code-only',
|
|
211
|
+
allowed_callers: ['code_execution'],
|
|
212
|
+
defer_loading: true,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
'immediate_code_tool',
|
|
217
|
+
{
|
|
218
|
+
name: 'immediate_code_tool',
|
|
219
|
+
description: 'Immediate and code-only',
|
|
220
|
+
allowed_callers: ['code_execution'],
|
|
221
|
+
defer_loading: false,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
const ctx = createBasicContext({
|
|
227
|
+
agentConfig: { instructions: 'Base', toolRegistry },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const firstRunnable = ctx.systemRunnable;
|
|
231
|
+
expect(firstRunnable).toBeDefined();
|
|
232
|
+
|
|
233
|
+
ctx.markToolsAsDiscovered(['deferred_code_tool']);
|
|
234
|
+
|
|
235
|
+
const secondRunnable = ctx.systemRunnable;
|
|
236
|
+
expect(secondRunnable).not.toBe(firstRunnable);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('getToolsForBinding', () => {
|
|
241
|
+
it('returns all tools when no toolRegistry', () => {
|
|
242
|
+
const tools = [createMockTool('tool1'), createMockTool('tool2')];
|
|
243
|
+
const ctx = createBasicContext({ agentConfig: { tools } });
|
|
244
|
+
const result = ctx.getToolsForBinding();
|
|
245
|
+
expect(result).toEqual(tools);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('excludes code_execution-only tools', () => {
|
|
249
|
+
const tools = [
|
|
250
|
+
createMockTool('direct_tool'),
|
|
251
|
+
createMockTool('code_only_tool'),
|
|
252
|
+
];
|
|
253
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
254
|
+
['direct_tool', { name: 'direct_tool', allowed_callers: ['direct'] }],
|
|
255
|
+
[
|
|
256
|
+
'code_only_tool',
|
|
257
|
+
{ name: 'code_only_tool', allowed_callers: ['code_execution'] },
|
|
258
|
+
],
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
const ctx = createBasicContext({ agentConfig: { tools, toolRegistry } });
|
|
262
|
+
const result = ctx.getToolsForBinding();
|
|
263
|
+
expect(result?.length).toBe(1);
|
|
264
|
+
expect((result?.[0] as t.GenericTool).name).toBe('direct_tool');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('excludes deferred tools until discovered', () => {
|
|
268
|
+
const tools = [
|
|
269
|
+
createMockTool('immediate_tool'),
|
|
270
|
+
createMockTool('deferred_tool'),
|
|
271
|
+
];
|
|
272
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
273
|
+
[
|
|
274
|
+
'immediate_tool',
|
|
275
|
+
{
|
|
276
|
+
name: 'immediate_tool',
|
|
277
|
+
allowed_callers: ['direct'],
|
|
278
|
+
defer_loading: false,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
[
|
|
282
|
+
'deferred_tool',
|
|
283
|
+
{
|
|
284
|
+
name: 'deferred_tool',
|
|
285
|
+
allowed_callers: ['direct'],
|
|
286
|
+
defer_loading: true,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
const ctx = createBasicContext({ agentConfig: { tools, toolRegistry } });
|
|
292
|
+
|
|
293
|
+
let result = ctx.getToolsForBinding();
|
|
294
|
+
expect(result?.length).toBe(1);
|
|
295
|
+
expect((result?.[0] as t.GenericTool).name).toBe('immediate_tool');
|
|
296
|
+
|
|
297
|
+
ctx.markToolsAsDiscovered(['deferred_tool']);
|
|
298
|
+
result = ctx.getToolsForBinding();
|
|
299
|
+
expect(result?.length).toBe(2);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('includes tools with both direct and code_execution callers', () => {
|
|
303
|
+
const tools = [createMockTool('hybrid_tool')];
|
|
304
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
305
|
+
[
|
|
306
|
+
'hybrid_tool',
|
|
307
|
+
{
|
|
308
|
+
name: 'hybrid_tool',
|
|
309
|
+
allowed_callers: ['direct', 'code_execution'],
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
const ctx = createBasicContext({ agentConfig: { tools, toolRegistry } });
|
|
315
|
+
const result = ctx.getToolsForBinding();
|
|
316
|
+
expect(result?.length).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('defaults to direct when allowed_callers not specified', () => {
|
|
320
|
+
const tools = [createMockTool('default_tool')];
|
|
321
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
322
|
+
['default_tool', { name: 'default_tool' }],
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const ctx = createBasicContext({ agentConfig: { tools, toolRegistry } });
|
|
326
|
+
const result = ctx.getToolsForBinding();
|
|
327
|
+
expect(result?.length).toBe(1);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('Token Accounting', () => {
|
|
332
|
+
const mockTokenCounter = (msg: { content: unknown }): number => {
|
|
333
|
+
const content =
|
|
334
|
+
typeof msg.content === 'string'
|
|
335
|
+
? msg.content
|
|
336
|
+
: JSON.stringify(msg.content);
|
|
337
|
+
return content.length;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
it('counts system message tokens on first access', () => {
|
|
341
|
+
const ctx = createBasicContext({
|
|
342
|
+
agentConfig: { instructions: 'Hello' },
|
|
343
|
+
tokenCounter: mockTokenCounter,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
ctx.initializeSystemRunnable();
|
|
347
|
+
expect(ctx.instructionTokens).toBeGreaterThan(0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('updates token count when system message changes', () => {
|
|
351
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
352
|
+
[
|
|
353
|
+
'code_tool',
|
|
354
|
+
{
|
|
355
|
+
name: 'code_tool',
|
|
356
|
+
description: 'A tool with a long description that adds tokens',
|
|
357
|
+
allowed_callers: ['code_execution'],
|
|
358
|
+
defer_loading: true,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
const ctx = createBasicContext({
|
|
364
|
+
agentConfig: { instructions: 'Short', toolRegistry },
|
|
365
|
+
tokenCounter: mockTokenCounter,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
ctx.initializeSystemRunnable();
|
|
369
|
+
const initialTokens = ctx.instructionTokens;
|
|
370
|
+
|
|
371
|
+
ctx.markToolsAsDiscovered(['code_tool']);
|
|
372
|
+
void ctx.systemRunnable;
|
|
373
|
+
|
|
374
|
+
expect(ctx.instructionTokens).toBeGreaterThan(initialTokens);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('reset()', () => {
|
|
379
|
+
it('clears all cached state', () => {
|
|
380
|
+
const ctx = createBasicContext({ agentConfig: { instructions: 'Test' } });
|
|
381
|
+
|
|
382
|
+
ctx.markToolsAsDiscovered(['tool1']);
|
|
383
|
+
void ctx.systemRunnable;
|
|
384
|
+
ctx.instructionTokens = 100;
|
|
385
|
+
ctx.indexTokenCountMap = { '0': 50 };
|
|
386
|
+
ctx.currentUsage = { input_tokens: 100 };
|
|
387
|
+
|
|
388
|
+
ctx.reset();
|
|
389
|
+
|
|
390
|
+
expect(ctx.discoveredToolNames.size).toBe(0);
|
|
391
|
+
expect(ctx.instructionTokens).toBe(0);
|
|
392
|
+
expect(ctx.indexTokenCountMap).toEqual({});
|
|
393
|
+
expect(ctx.currentUsage).toBeUndefined();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('forces rebuild on next systemRunnable access', () => {
|
|
397
|
+
const ctx = createBasicContext({ agentConfig: { instructions: 'Test' } });
|
|
398
|
+
|
|
399
|
+
const firstRunnable = ctx.systemRunnable;
|
|
400
|
+
ctx.reset();
|
|
401
|
+
|
|
402
|
+
ctx.instructions = 'Test';
|
|
403
|
+
const secondRunnable = ctx.systemRunnable;
|
|
404
|
+
|
|
405
|
+
expect(secondRunnable).not.toBe(firstRunnable);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('initializeSystemRunnable()', () => {
|
|
410
|
+
it('explicitly initializes system runnable', () => {
|
|
411
|
+
const ctx = createBasicContext({ agentConfig: { instructions: 'Test' } });
|
|
412
|
+
|
|
413
|
+
expect(ctx['cachedSystemRunnable']).toBeUndefined();
|
|
414
|
+
ctx.initializeSystemRunnable();
|
|
415
|
+
expect(ctx['cachedSystemRunnable']).toBeDefined();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('is idempotent when not stale', () => {
|
|
419
|
+
const ctx = createBasicContext({ agentConfig: { instructions: 'Test' } });
|
|
420
|
+
|
|
421
|
+
ctx.initializeSystemRunnable();
|
|
422
|
+
const first = ctx['cachedSystemRunnable'];
|
|
423
|
+
|
|
424
|
+
ctx.initializeSystemRunnable();
|
|
425
|
+
const second = ctx['cachedSystemRunnable'];
|
|
426
|
+
|
|
427
|
+
expect(first).toBe(second);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('Edge Cases', () => {
|
|
432
|
+
it('handles empty toolRegistry gracefully', () => {
|
|
433
|
+
const ctx = createBasicContext({
|
|
434
|
+
agentConfig: {
|
|
435
|
+
instructions: 'Test',
|
|
436
|
+
toolRegistry: new Map(),
|
|
437
|
+
tools: [],
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
expect(ctx.systemRunnable).toBeDefined();
|
|
442
|
+
expect(ctx.getToolsForBinding()).toEqual([]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('handles undefined tools array', () => {
|
|
446
|
+
const ctx = createBasicContext({
|
|
447
|
+
agentConfig: { instructions: 'Test', tools: undefined },
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(ctx.getToolsForBinding()).toBeUndefined();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('handles tool not in registry', () => {
|
|
454
|
+
const tools = [createMockTool('unknown_tool')];
|
|
455
|
+
const toolRegistry: t.LCToolRegistry = new Map();
|
|
456
|
+
|
|
457
|
+
const ctx = createBasicContext({ agentConfig: { tools, toolRegistry } });
|
|
458
|
+
const result = ctx.getToolsForBinding();
|
|
459
|
+
|
|
460
|
+
expect(result?.length).toBe(1);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('handles tool without name property', () => {
|
|
464
|
+
const toolWithoutName = { invoke: jest.fn() } as unknown as t.GenericTool;
|
|
465
|
+
const toolRegistry: t.LCToolRegistry = new Map();
|
|
466
|
+
|
|
467
|
+
const ctx = createBasicContext({
|
|
468
|
+
agentConfig: { tools: [toolWithoutName], toolRegistry },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const result = ctx.getToolsForBinding();
|
|
472
|
+
expect(result?.length).toBe(1);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('handles discovery of non-existent tool', () => {
|
|
476
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
477
|
+
[
|
|
478
|
+
'real_tool',
|
|
479
|
+
{ name: 'real_tool', allowed_callers: ['code_execution'] },
|
|
480
|
+
],
|
|
481
|
+
]);
|
|
482
|
+
|
|
483
|
+
const ctx = createBasicContext({
|
|
484
|
+
agentConfig: { instructions: 'Test', toolRegistry },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const result = ctx.markToolsAsDiscovered(['fake_tool']);
|
|
488
|
+
expect(result).toBe(true);
|
|
489
|
+
expect(ctx.discoveredToolNames.has('fake_tool')).toBe(true);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe('Multi-Step Run Flow (emulating createCallModel)', () => {
|
|
494
|
+
/**
|
|
495
|
+
* This test emulates the flow in Graph.createCallModel across multiple turns:
|
|
496
|
+
*
|
|
497
|
+
* Turn 1: User asks a question
|
|
498
|
+
* - No tool search results yet
|
|
499
|
+
* - System runnable built with initial instructions
|
|
500
|
+
* - Token map initialized
|
|
501
|
+
*
|
|
502
|
+
* Turn 2: Tool results come back (including tool search)
|
|
503
|
+
* - extractToolDiscoveries() finds new tools
|
|
504
|
+
* - markToolsAsDiscovered() called → sets stale flag
|
|
505
|
+
* - systemRunnable getter rebuilds with discovered tools
|
|
506
|
+
* - Token counts updated
|
|
507
|
+
*
|
|
508
|
+
* Turn 3: Another turn with same discovered tools
|
|
509
|
+
* - No new discoveries
|
|
510
|
+
* - systemRunnable returns cached (not rebuilt)
|
|
511
|
+
* - Token counts unchanged
|
|
512
|
+
*/
|
|
513
|
+
|
|
514
|
+
const mockTokenCounter = (msg: { content: unknown }): number => {
|
|
515
|
+
const content =
|
|
516
|
+
typeof msg.content === 'string'
|
|
517
|
+
? msg.content
|
|
518
|
+
: JSON.stringify(msg.content);
|
|
519
|
+
return Math.ceil(content.length / 4); // ~4 chars per token (realistic)
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
it('handles complete multi-step run with tool discovery', () => {
|
|
523
|
+
// Setup: Tools with different configurations
|
|
524
|
+
const tools = [
|
|
525
|
+
createMockTool('always_available'),
|
|
526
|
+
createMockTool('deferred_direct_tool'),
|
|
527
|
+
createMockTool('deferred_code_tool'),
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
531
|
+
[
|
|
532
|
+
'always_available',
|
|
533
|
+
{
|
|
534
|
+
name: 'always_available',
|
|
535
|
+
description: 'Always available tool',
|
|
536
|
+
allowed_callers: ['direct'],
|
|
537
|
+
defer_loading: false,
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
[
|
|
541
|
+
'deferred_direct_tool',
|
|
542
|
+
{
|
|
543
|
+
name: 'deferred_direct_tool',
|
|
544
|
+
description: 'Deferred but direct-callable',
|
|
545
|
+
allowed_callers: ['direct'],
|
|
546
|
+
defer_loading: true,
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
[
|
|
550
|
+
'deferred_code_tool',
|
|
551
|
+
{
|
|
552
|
+
name: 'deferred_code_tool',
|
|
553
|
+
description:
|
|
554
|
+
'Deferred and code-execution only - hidden until discovered',
|
|
555
|
+
allowed_callers: ['code_execution'],
|
|
556
|
+
defer_loading: true,
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
]);
|
|
560
|
+
|
|
561
|
+
const ctx = createBasicContext({
|
|
562
|
+
agentConfig: {
|
|
563
|
+
instructions: 'You are a helpful assistant.',
|
|
564
|
+
tools,
|
|
565
|
+
toolRegistry,
|
|
566
|
+
},
|
|
567
|
+
tokenCounter: mockTokenCounter,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ========== TURN 1: Initial call (like first createCallModel) ==========
|
|
571
|
+
|
|
572
|
+
// In createCallModel, we first check for tool discoveries (none yet)
|
|
573
|
+
const turn1Discoveries: string[] = []; // No tool search results
|
|
574
|
+
if (turn1Discoveries.length > 0) {
|
|
575
|
+
ctx.markToolsAsDiscovered(turn1Discoveries);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Get tools for binding
|
|
579
|
+
const turn1Tools = ctx.getToolsForBinding();
|
|
580
|
+
expect(turn1Tools?.length).toBe(1); // Only 'always_available'
|
|
581
|
+
expect(turn1Tools?.map((t) => (t as t.GenericTool).name)).toEqual([
|
|
582
|
+
'always_available',
|
|
583
|
+
]);
|
|
584
|
+
|
|
585
|
+
// Access system runnable (triggers lazy build)
|
|
586
|
+
const turn1Runnable = ctx.systemRunnable;
|
|
587
|
+
expect(turn1Runnable).toBeDefined();
|
|
588
|
+
|
|
589
|
+
// Store initial token count
|
|
590
|
+
const turn1Tokens = ctx.instructionTokens;
|
|
591
|
+
expect(turn1Tokens).toBeGreaterThan(0);
|
|
592
|
+
|
|
593
|
+
// Simulate token map update (as done in fromConfig flow)
|
|
594
|
+
ctx.updateTokenMapWithInstructions({ '0': 10, '1': 20 });
|
|
595
|
+
expect(ctx.indexTokenCountMap['0']).toBe(10 + turn1Tokens);
|
|
596
|
+
expect(ctx.indexTokenCountMap['1']).toBe(20);
|
|
597
|
+
|
|
598
|
+
// ========== TURN 2: Tool search results come back ==========
|
|
599
|
+
|
|
600
|
+
// Simulate tool search discovering tools
|
|
601
|
+
const turn2Discoveries = ['deferred_direct_tool', 'deferred_code_tool'];
|
|
602
|
+
const hasNewDiscoveries = ctx.markToolsAsDiscovered(turn2Discoveries);
|
|
603
|
+
expect(hasNewDiscoveries).toBe(true);
|
|
604
|
+
|
|
605
|
+
// Get tools for binding - now includes discovered direct tool
|
|
606
|
+
const turn2Tools = ctx.getToolsForBinding();
|
|
607
|
+
expect(turn2Tools?.length).toBe(2); // 'always_available' + 'deferred_direct_tool'
|
|
608
|
+
expect(turn2Tools?.map((t) => (t as t.GenericTool).name)).toContain(
|
|
609
|
+
'always_available'
|
|
610
|
+
);
|
|
611
|
+
expect(turn2Tools?.map((t) => (t as t.GenericTool).name)).toContain(
|
|
612
|
+
'deferred_direct_tool'
|
|
613
|
+
);
|
|
614
|
+
// Note: 'deferred_code_tool' is NOT in binding (code_execution only)
|
|
615
|
+
|
|
616
|
+
// Access system runnable - should rebuild due to stale flag
|
|
617
|
+
const turn2Runnable = ctx.systemRunnable;
|
|
618
|
+
expect(turn2Runnable).not.toBe(turn1Runnable); // Different instance = rebuilt
|
|
619
|
+
|
|
620
|
+
// Token count should increase (now includes deferred_code_tool in system message)
|
|
621
|
+
const turn2Tokens = ctx.instructionTokens;
|
|
622
|
+
expect(turn2Tokens).toBeGreaterThan(turn1Tokens);
|
|
623
|
+
|
|
624
|
+
// ========== TURN 3: Another turn, same discoveries ==========
|
|
625
|
+
|
|
626
|
+
// Same discoveries (duplicates)
|
|
627
|
+
const turn3Discoveries = ['deferred_direct_tool'];
|
|
628
|
+
const hasNewDiscoveriesTurn3 =
|
|
629
|
+
ctx.markToolsAsDiscovered(turn3Discoveries);
|
|
630
|
+
expect(hasNewDiscoveriesTurn3).toBe(false); // No NEW discoveries
|
|
631
|
+
|
|
632
|
+
// Tools should be same as turn 2
|
|
633
|
+
const turn3Tools = ctx.getToolsForBinding();
|
|
634
|
+
expect(turn3Tools?.length).toBe(2);
|
|
635
|
+
|
|
636
|
+
// System runnable should be CACHED (same reference)
|
|
637
|
+
const turn3Runnable = ctx.systemRunnable;
|
|
638
|
+
expect(turn3Runnable).toBe(turn2Runnable); // Same instance = cached
|
|
639
|
+
|
|
640
|
+
// Token count unchanged
|
|
641
|
+
expect(ctx.instructionTokens).toBe(turn2Tokens);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('maintains consistent indexTokenCountMap across turns', () => {
|
|
645
|
+
const ctx = createBasicContext({
|
|
646
|
+
agentConfig: { instructions: 'Base instructions' },
|
|
647
|
+
tokenCounter: mockTokenCounter,
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Initial setup (simulating fromConfig flow)
|
|
651
|
+
ctx.initializeSystemRunnable();
|
|
652
|
+
const initialSystemTokens = ctx.instructionTokens;
|
|
653
|
+
|
|
654
|
+
// Simulate message token counts from conversation
|
|
655
|
+
const messageTokenCounts = { '0': 50, '1': 100, '2': 75 };
|
|
656
|
+
ctx.updateTokenMapWithInstructions(messageTokenCounts);
|
|
657
|
+
|
|
658
|
+
// Verify token map: first message gets instruction tokens added
|
|
659
|
+
expect(ctx.indexTokenCountMap['0']).toBe(50 + initialSystemTokens);
|
|
660
|
+
expect(ctx.indexTokenCountMap['1']).toBe(100);
|
|
661
|
+
expect(ctx.indexTokenCountMap['2']).toBe(75);
|
|
662
|
+
|
|
663
|
+
// Simulate turn where system message changes
|
|
664
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
665
|
+
[
|
|
666
|
+
'new_code_tool',
|
|
667
|
+
{
|
|
668
|
+
name: 'new_code_tool',
|
|
669
|
+
description:
|
|
670
|
+
'A newly discovered code-only tool with detailed documentation',
|
|
671
|
+
allowed_callers: ['code_execution'],
|
|
672
|
+
defer_loading: true,
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
]);
|
|
676
|
+
ctx.toolRegistry = toolRegistry;
|
|
677
|
+
|
|
678
|
+
// Discover the tool
|
|
679
|
+
ctx.markToolsAsDiscovered(['new_code_tool']);
|
|
680
|
+
|
|
681
|
+
// Access system runnable to trigger rebuild
|
|
682
|
+
void ctx.systemRunnable;
|
|
683
|
+
|
|
684
|
+
// Token count should have increased
|
|
685
|
+
const newSystemTokens = ctx.instructionTokens;
|
|
686
|
+
expect(newSystemTokens).toBeGreaterThan(initialSystemTokens);
|
|
687
|
+
|
|
688
|
+
// If we update token map again, it should use NEW instruction tokens
|
|
689
|
+
const newMessageTokenCounts = { '0': 60, '1': 110 };
|
|
690
|
+
ctx.updateTokenMapWithInstructions(newMessageTokenCounts);
|
|
691
|
+
|
|
692
|
+
expect(ctx.indexTokenCountMap['0']).toBe(60 + newSystemTokens);
|
|
693
|
+
expect(ctx.indexTokenCountMap['1']).toBe(110);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('correctly tracks token delta when system message content changes', () => {
|
|
697
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
698
|
+
[
|
|
699
|
+
'tool_a',
|
|
700
|
+
{
|
|
701
|
+
name: 'tool_a',
|
|
702
|
+
description: 'Short description',
|
|
703
|
+
allowed_callers: ['code_execution'],
|
|
704
|
+
defer_loading: true,
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
[
|
|
708
|
+
'tool_b',
|
|
709
|
+
{
|
|
710
|
+
name: 'tool_b',
|
|
711
|
+
description: 'Another tool that adds more content',
|
|
712
|
+
allowed_callers: ['code_execution'],
|
|
713
|
+
defer_loading: true,
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
]);
|
|
717
|
+
|
|
718
|
+
const ctx = createBasicContext({
|
|
719
|
+
agentConfig: {
|
|
720
|
+
instructions: 'You are helpful.',
|
|
721
|
+
toolRegistry,
|
|
722
|
+
},
|
|
723
|
+
tokenCounter: mockTokenCounter,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
ctx.initializeSystemRunnable();
|
|
727
|
+
const baseTokens = ctx.instructionTokens;
|
|
728
|
+
|
|
729
|
+
// Discover tool_a
|
|
730
|
+
ctx.markToolsAsDiscovered(['tool_a']);
|
|
731
|
+
void ctx.systemRunnable;
|
|
732
|
+
const tokensAfterA = ctx.instructionTokens;
|
|
733
|
+
expect(tokensAfterA).toBeGreaterThan(baseTokens);
|
|
734
|
+
|
|
735
|
+
// Discover tool_b - adds more content
|
|
736
|
+
ctx.markToolsAsDiscovered(['tool_b']);
|
|
737
|
+
void ctx.systemRunnable;
|
|
738
|
+
const tokensAfterB = ctx.instructionTokens;
|
|
739
|
+
expect(tokensAfterB).toBeGreaterThan(tokensAfterA);
|
|
740
|
+
|
|
741
|
+
// Both deltas should be positive (each discovery adds tokens)
|
|
742
|
+
const deltaBaseToA = tokensAfterA - baseTokens;
|
|
743
|
+
const deltaAToB = tokensAfterB - tokensAfterA;
|
|
744
|
+
expect(deltaBaseToA).toBeGreaterThan(0);
|
|
745
|
+
expect(deltaAToB).toBeGreaterThan(0);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('handles reset between runs correctly', () => {
|
|
749
|
+
const toolRegistry: t.LCToolRegistry = new Map([
|
|
750
|
+
[
|
|
751
|
+
'discovered_tool',
|
|
752
|
+
{
|
|
753
|
+
name: 'discovered_tool',
|
|
754
|
+
description: 'Will be discovered',
|
|
755
|
+
allowed_callers: ['code_execution'],
|
|
756
|
+
defer_loading: true,
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
]);
|
|
760
|
+
|
|
761
|
+
const ctx = createBasicContext({
|
|
762
|
+
agentConfig: {
|
|
763
|
+
instructions: 'Assistant instructions',
|
|
764
|
+
toolRegistry,
|
|
765
|
+
},
|
|
766
|
+
tokenCounter: mockTokenCounter,
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// ========== RUN 1 ==========
|
|
770
|
+
ctx.initializeSystemRunnable();
|
|
771
|
+
ctx.markToolsAsDiscovered(['discovered_tool']);
|
|
772
|
+
void ctx.systemRunnable;
|
|
773
|
+
|
|
774
|
+
expect(ctx.discoveredToolNames.has('discovered_tool')).toBe(true);
|
|
775
|
+
const run1Tokens = ctx.instructionTokens;
|
|
776
|
+
expect(run1Tokens).toBeGreaterThan(0);
|
|
777
|
+
|
|
778
|
+
// ========== RESET (new run) ==========
|
|
779
|
+
ctx.reset();
|
|
780
|
+
|
|
781
|
+
// Verify state is cleared
|
|
782
|
+
expect(ctx.discoveredToolNames.size).toBe(0);
|
|
783
|
+
expect(ctx.instructionTokens).toBe(0);
|
|
784
|
+
expect(ctx.indexTokenCountMap).toEqual({});
|
|
785
|
+
|
|
786
|
+
// ========== RUN 2 ==========
|
|
787
|
+
// Re-initialize (as fromConfig would do)
|
|
788
|
+
ctx.initializeSystemRunnable();
|
|
789
|
+
|
|
790
|
+
// System runnable should NOT include the previously discovered tool
|
|
791
|
+
// (because discoveredToolNames was cleared)
|
|
792
|
+
const run2Tokens = ctx.instructionTokens;
|
|
793
|
+
|
|
794
|
+
// Token count should be lower than run 1 (no discovered tool in system message)
|
|
795
|
+
expect(run2Tokens).toBeLessThan(run1Tokens);
|
|
796
|
+
|
|
797
|
+
// Discover again
|
|
798
|
+
ctx.markToolsAsDiscovered(['discovered_tool']);
|
|
799
|
+
void ctx.systemRunnable;
|
|
800
|
+
|
|
801
|
+
// Now should match run 1
|
|
802
|
+
expect(ctx.instructionTokens).toBe(run1Tokens);
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
});
|