@cogitator-ai/self-modifying 19.2.1 → 19.3.1
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/README.md +23 -6
- package/dist/architecture-evolution/capability-analyzer.d.ts +3 -0
- package/dist/architecture-evolution/capability-analyzer.d.ts.map +1 -1
- package/dist/architecture-evolution/capability-analyzer.js +3 -1
- package/dist/architecture-evolution/capability-analyzer.js.map +1 -1
- package/dist/architecture-evolution/parameter-optimizer.d.ts +6 -0
- package/dist/architecture-evolution/parameter-optimizer.d.ts.map +1 -1
- package/dist/architecture-evolution/parameter-optimizer.js +5 -2
- package/dist/architecture-evolution/parameter-optimizer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/self-modifying-agent.d.ts +6 -0
- package/dist/self-modifying-agent.d.ts.map +1 -1
- package/dist/self-modifying-agent.js +12 -2
- package/dist/self-modifying-agent.js.map +1 -1
- package/dist/tool-generation/gap-analyzer.d.ts +13 -4
- package/dist/tool-generation/gap-analyzer.d.ts.map +1 -1
- package/dist/tool-generation/gap-analyzer.js +5 -2
- package/dist/tool-generation/gap-analyzer.js.map +1 -1
- package/dist/tool-generation/index.d.ts +2 -2
- package/dist/tool-generation/index.d.ts.map +1 -1
- package/dist/tool-generation/index.js.map +1 -1
- package/dist/tool-generation/prompts.d.ts +7 -2
- package/dist/tool-generation/prompts.d.ts.map +1 -1
- package/dist/tool-generation/prompts.js +71 -13
- package/dist/tool-generation/prompts.js.map +1 -1
- package/dist/tool-generation/tool-generator.d.ts +6 -0
- package/dist/tool-generation/tool-generator.d.ts.map +1 -1
- package/dist/tool-generation/tool-generator.js +26 -2
- package/dist/tool-generation/tool-generator.js.map +1 -1
- package/dist/tool-generation/tool-sandbox.d.ts.map +1 -1
- package/dist/tool-generation/tool-sandbox.js +14 -2
- package/dist/tool-generation/tool-sandbox.js.map +1 -1
- package/dist/tool-generation/tool-validator.d.ts +16 -0
- package/dist/tool-generation/tool-validator.d.ts.map +1 -1
- package/dist/tool-generation/tool-validator.js +110 -17
- package/dist/tool-generation/tool-validator.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/sandbox-security.test.ts +12 -0
- package/src/__tests__/self-modifying-agent.test.ts +52 -0
- package/src/__tests__/tool-generation.test.ts +22 -0
- package/src/__tests__/tool-review-regressions.test.ts +473 -0
- package/src/architecture-evolution/capability-analyzer.ts +5 -1
- package/src/architecture-evolution/parameter-optimizer.ts +10 -2
- package/src/index.ts +2 -0
- package/src/self-modifying-agent.ts +18 -2
- package/src/tool-generation/gap-analyzer.ts +24 -11
- package/src/tool-generation/index.ts +2 -1
- package/src/tool-generation/prompts.ts +78 -14
- package/src/tool-generation/tool-generator.ts +33 -2
- package/src/tool-generation/tool-sandbox.ts +14 -2
- package/src/tool-generation/tool-validator.ts +121 -20
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
CapabilityGap,
|
|
4
|
+
ChatRequest,
|
|
5
|
+
ChatResponse,
|
|
6
|
+
GeneratedTool,
|
|
7
|
+
LLMBackend,
|
|
8
|
+
ToolSelfGenerationConfig,
|
|
9
|
+
} from '@cogitator-ai/types';
|
|
10
|
+
import {
|
|
11
|
+
GapAnalyzer,
|
|
12
|
+
ToolGenerator,
|
|
13
|
+
ToolValidator,
|
|
14
|
+
buildGapAnalysisPrompt,
|
|
15
|
+
buildToolGenerationPrompt,
|
|
16
|
+
parseToolGenerationResponse,
|
|
17
|
+
} from '../tool-generation';
|
|
18
|
+
import { CapabilityAnalyzer, ParameterOptimizer } from '../architecture-evolution';
|
|
19
|
+
|
|
20
|
+
const config: ToolSelfGenerationConfig = {
|
|
21
|
+
enabled: true,
|
|
22
|
+
autoGenerate: true,
|
|
23
|
+
maxToolsPerSession: 3,
|
|
24
|
+
minConfidenceForGeneration: 0.5,
|
|
25
|
+
maxIterationsPerTool: 2,
|
|
26
|
+
requireLLMValidation: true,
|
|
27
|
+
sandboxConfig: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
maxExecutionTime: 3000,
|
|
30
|
+
maxMemory: 50 * 1024 * 1024,
|
|
31
|
+
allowedModules: [],
|
|
32
|
+
isolationLevel: 'strict',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const LUHN = `async function execute(params) {
|
|
37
|
+
if (typeof params.digits !== 'string' || !/^[0-9]+$/.test(params.digits)) {
|
|
38
|
+
throw new Error('digits must be a non-empty string of 0-9');
|
|
39
|
+
}
|
|
40
|
+
let sum = 0;
|
|
41
|
+
for (let i = 0; i < params.digits.length; i++) {
|
|
42
|
+
let digit = Number(params.digits[params.digits.length - 1 - i]);
|
|
43
|
+
if (i % 2 === 0) { digit *= 2; if (digit > 9) digit -= 9; }
|
|
44
|
+
sum += digit;
|
|
45
|
+
}
|
|
46
|
+
return (10 - (sum % 10)) % 10;
|
|
47
|
+
}`;
|
|
48
|
+
|
|
49
|
+
const DIGITS_SCHEMA = {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { digits: { type: 'string', description: 'Decimal digits only' } },
|
|
52
|
+
required: ['digits'],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function luhnTool(overrides: Partial<GeneratedTool> = {}): GeneratedTool {
|
|
56
|
+
return {
|
|
57
|
+
id: 'gen_luhn',
|
|
58
|
+
name: 'luhn_check_digit',
|
|
59
|
+
description: 'Computes the Luhn check digit of a digit string',
|
|
60
|
+
implementation: LUHN,
|
|
61
|
+
parameters: DIGITS_SCHEMA,
|
|
62
|
+
createdAt: new Date(),
|
|
63
|
+
version: 1,
|
|
64
|
+
status: 'pending_validation',
|
|
65
|
+
...overrides,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function response(content: string): ChatResponse {
|
|
70
|
+
return {
|
|
71
|
+
id: 'resp',
|
|
72
|
+
content,
|
|
73
|
+
finishReason: 'stop',
|
|
74
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function backend(reply: (request: ChatRequest) => string): LLMBackend & {
|
|
79
|
+
chat: ReturnType<typeof vi.fn>;
|
|
80
|
+
} {
|
|
81
|
+
const chat = vi.fn(async (request: ChatRequest) => response(reply(request)));
|
|
82
|
+
return {
|
|
83
|
+
provider: 'stub',
|
|
84
|
+
chat,
|
|
85
|
+
chatStream: async function* () {
|
|
86
|
+
yield { id: 'x', delta: {} };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reviewer(review: Record<string, unknown>) {
|
|
92
|
+
return backend(() => JSON.stringify(review));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const CRC_GAP: CapabilityGap = {
|
|
96
|
+
id: 'gap_crc',
|
|
97
|
+
description: 'No available tool to reliably compute the CRC-32 checksum of a text',
|
|
98
|
+
requiredCapability: 'CRC-32 (IEEE 802.3) checksum of a UTF-8 text as 8 lowercase hex digits',
|
|
99
|
+
suggestedToolName: 'crc32_calculator',
|
|
100
|
+
complexity: 'simple',
|
|
101
|
+
confidence: 0.9,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const CRC_IMPLEMENTATION =
|
|
105
|
+
"async function execute(params) { if (typeof params.text !== 'string') { throw new Error('text must be a string'); } let crc = -1; for (let i = 0; i < params.text.length; i++) { crc ^= params.text.charCodeAt(i); for (let k = 0; k < 8; k++) { crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; } } return ((crc ^ -1) >>> 0).toString(16).padStart(8, '0'); }";
|
|
106
|
+
|
|
107
|
+
function crcToolJson(description: string, extra: Record<string, unknown> = {}): string {
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
name: 'crc32_calculator',
|
|
110
|
+
description,
|
|
111
|
+
implementation: CRC_IMPLEMENTATION,
|
|
112
|
+
parameters: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: { text: { type: 'string' } },
|
|
115
|
+
required: ['text'],
|
|
116
|
+
},
|
|
117
|
+
...extra,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('ToolValidator without a review', () => {
|
|
122
|
+
const validator = new ToolValidator({ config: { ...config, requireLLMValidation: false } });
|
|
123
|
+
|
|
124
|
+
it('accepts a correct tool that rejects malformed input', async () => {
|
|
125
|
+
const result = await validator.validate(luhnTool());
|
|
126
|
+
|
|
127
|
+
expect(result.logicIssues).toEqual([]);
|
|
128
|
+
expect(result.isValid).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('runs the examples the generator gave as cases that must succeed', async () => {
|
|
132
|
+
const broken = luhnTool({
|
|
133
|
+
implementation:
|
|
134
|
+
"async function execute(params) { if (params.digits.length > 3) { throw new Error('unsupported'); } return 0; }",
|
|
135
|
+
metadata: { examples: [{ input: { digits: '7992739871' } }] },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = await validator.validate(broken);
|
|
139
|
+
|
|
140
|
+
expect(result.isValid).toBe(false);
|
|
141
|
+
expect(result.logicIssues.join('\n')).toContain('7992739871');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('ignores examples that do not fit the parameters schema', async () => {
|
|
145
|
+
const result = await validator.validate(
|
|
146
|
+
luhnTool({
|
|
147
|
+
metadata: {
|
|
148
|
+
examples: [{ input: { digits: 42 } }, { input: { number: '123' } }, { input: 'x' }],
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(result.isValid).toBe(true);
|
|
154
|
+
expect(result.testResults.some((test) => JSON.stringify(test.input).includes('number'))).toBe(
|
|
155
|
+
false
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('ToolValidator with a review', () => {
|
|
161
|
+
it('follows an approving review and keeps its notes as suggestions', async () => {
|
|
162
|
+
const validator = new ToolValidator({
|
|
163
|
+
llm: reviewer({
|
|
164
|
+
isValid: true,
|
|
165
|
+
recommendation: 'approve',
|
|
166
|
+
overallScore: 0.95,
|
|
167
|
+
securityIssues: ['No upper bound on the input length'],
|
|
168
|
+
logicIssues: ['Could also accept numbers'],
|
|
169
|
+
edgeCases: [],
|
|
170
|
+
suggestions: ['Consider documenting the return type'],
|
|
171
|
+
}),
|
|
172
|
+
model: 'stub',
|
|
173
|
+
config,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = await validator.validate(luhnTool(), [
|
|
177
|
+
{ input: { digits: '7992739871' }, expectedOutput: 3 },
|
|
178
|
+
{ input: { digits: '12a4' }, shouldThrow: true },
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
expect(result.isValid).toBe(true);
|
|
182
|
+
expect(result.overallScore).toBeGreaterThanOrEqual(0.7);
|
|
183
|
+
expect(result.securityIssues).toEqual([]);
|
|
184
|
+
expect(result.logicIssues).toEqual([]);
|
|
185
|
+
expect(result.suggestions).toEqual(
|
|
186
|
+
expect.arrayContaining([
|
|
187
|
+
expect.stringContaining('No upper bound on the input length'),
|
|
188
|
+
expect.stringContaining('Could also accept numbers'),
|
|
189
|
+
'Consider documenting the return type',
|
|
190
|
+
])
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('rejects the tool when the review rejects it', async () => {
|
|
195
|
+
const validator = new ToolValidator({
|
|
196
|
+
llm: reviewer({
|
|
197
|
+
isValid: false,
|
|
198
|
+
recommendation: 'reject',
|
|
199
|
+
overallScore: 0.1,
|
|
200
|
+
securityIssues: ['Builds a regular expression from user input'],
|
|
201
|
+
logicIssues: [],
|
|
202
|
+
edgeCases: [],
|
|
203
|
+
suggestions: [],
|
|
204
|
+
}),
|
|
205
|
+
model: 'stub',
|
|
206
|
+
config,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const result = await validator.validate(luhnTool());
|
|
210
|
+
|
|
211
|
+
expect(result.isValid).toBe(false);
|
|
212
|
+
expect(result.securityIssues).toContain('Builds a regular expression from user input');
|
|
213
|
+
expect(result.overallScore).toBe(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('asks for a revision when the review recommends one', async () => {
|
|
217
|
+
const validator = new ToolValidator({
|
|
218
|
+
llm: reviewer({
|
|
219
|
+
isValid: true,
|
|
220
|
+
recommendation: 'revise',
|
|
221
|
+
securityIssues: [],
|
|
222
|
+
logicIssues: ['Returns a string instead of a number'],
|
|
223
|
+
}),
|
|
224
|
+
model: 'stub',
|
|
225
|
+
config,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const result = await validator.validate(luhnTool());
|
|
229
|
+
|
|
230
|
+
expect(result.isValid).toBe(false);
|
|
231
|
+
expect(result.logicIssues).toContain('Returns a string instead of a number');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('gives a rejecting review without findings something to fix', async () => {
|
|
235
|
+
const validator = new ToolValidator({
|
|
236
|
+
llm: reviewer({ isValid: false, recommendation: 'reject' }),
|
|
237
|
+
model: 'stub',
|
|
238
|
+
config,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await validator.validate(luhnTool());
|
|
242
|
+
|
|
243
|
+
expect(result.isValid).toBe(false);
|
|
244
|
+
expect(result.logicIssues.length).toBeGreaterThan(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('keeps the static security checks authoritative over an approving review', async () => {
|
|
248
|
+
const llm = reviewer({ isValid: true, recommendation: 'approve', securityIssues: [] });
|
|
249
|
+
const validator = new ToolValidator({ llm, model: 'stub', config });
|
|
250
|
+
|
|
251
|
+
const result = await validator.validate(
|
|
252
|
+
luhnTool({ implementation: "async function execute(p) { return eval('1 + 1'); }" })
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(result.isValid).toBe(false);
|
|
256
|
+
expect(result.overallScore).toBe(0);
|
|
257
|
+
expect(result.securityIssues.some((issue) => issue.includes('no_eval'))).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('keeps failed sandbox tests authoritative over an approving review', async () => {
|
|
261
|
+
const llm = reviewer({ isValid: true, recommendation: 'approve' });
|
|
262
|
+
const validator = new ToolValidator({ llm, model: 'stub', config });
|
|
263
|
+
|
|
264
|
+
const result = await validator.validate(luhnTool(), [
|
|
265
|
+
{ input: { digits: '7992739871' }, expectedOutput: 4 },
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
expect(result.isValid).toBe(false);
|
|
269
|
+
expect(result.logicIssues.join('\n')).toContain('7992739871');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('bounds the review call with maxTokens', async () => {
|
|
273
|
+
const llm = reviewer({ isValid: true, recommendation: 'approve' });
|
|
274
|
+
const validator = new ToolValidator({ llm, model: 'stub', config, maxTokens: 900 });
|
|
275
|
+
|
|
276
|
+
await validator.validate(luhnTool());
|
|
277
|
+
|
|
278
|
+
expect(llm.chat.mock.calls[0]?.[0].maxTokens).toBe(900);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('generated tool description', () => {
|
|
283
|
+
it('asks the model to describe the tool, not the gap', () => {
|
|
284
|
+
const prompt = buildToolGenerationPrompt(CRC_GAP, []);
|
|
285
|
+
|
|
286
|
+
expect(prompt).not.toContain(`"description":"${CRC_GAP.description}"`);
|
|
287
|
+
expect(prompt).toContain('"examples"');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('keeps the description the model wrote for the tool', async () => {
|
|
291
|
+
const llm = backend(() =>
|
|
292
|
+
crcToolJson('Computes the CRC-32 checksum of a text as 8 lowercase hex digits')
|
|
293
|
+
);
|
|
294
|
+
const generator = new ToolGenerator({
|
|
295
|
+
llm,
|
|
296
|
+
model: 'stub',
|
|
297
|
+
config: { ...config, requireLLMValidation: false },
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const result = await generator.generate(CRC_GAP, []);
|
|
301
|
+
|
|
302
|
+
expect(result.success).toBe(true);
|
|
303
|
+
expect(result.tool?.description).toBe(
|
|
304
|
+
'Computes the CRC-32 checksum of a text as 8 lowercase hex digits'
|
|
305
|
+
);
|
|
306
|
+
expect(result.tool?.metadata?.capability).toBe(CRC_GAP.requiredCapability);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('describes the tool by its capability when the model echoes the gap', async () => {
|
|
310
|
+
const llm = backend(() => crcToolJson(CRC_GAP.description));
|
|
311
|
+
const generator = new ToolGenerator({
|
|
312
|
+
llm,
|
|
313
|
+
model: 'stub',
|
|
314
|
+
config: { ...config, requireLLMValidation: false },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const result = await generator.generate(CRC_GAP, []);
|
|
318
|
+
|
|
319
|
+
expect(result.tool?.description).toBe(CRC_GAP.requiredCapability);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('describes the tool by its capability when the model leaves the description out', async () => {
|
|
323
|
+
const llm = backend(() => crcToolJson(''));
|
|
324
|
+
const generator = new ToolGenerator({
|
|
325
|
+
llm,
|
|
326
|
+
model: 'stub',
|
|
327
|
+
config: { ...config, requireLLMValidation: false },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = await generator.generate(CRC_GAP, []);
|
|
331
|
+
|
|
332
|
+
expect(result.tool?.description).toBe(CRC_GAP.requiredCapability);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('reads valid example inputs from the generation response', () => {
|
|
336
|
+
const tool = parseToolGenerationResponse(
|
|
337
|
+
crcToolJson('CRC-32 of a text', {
|
|
338
|
+
examples: [{ input: { text: 'abc' } }, { text: 'bare input' }, 'junk'],
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect(tool?.metadata?.examples).toEqual([
|
|
343
|
+
{ input: { text: 'abc' } },
|
|
344
|
+
{ input: { text: 'bare input' } },
|
|
345
|
+
]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('keeps earlier examples when an improved version brings none', async () => {
|
|
349
|
+
let call = 0;
|
|
350
|
+
const llm = backend(() => {
|
|
351
|
+
call++;
|
|
352
|
+
return call === 1
|
|
353
|
+
? JSON.stringify({
|
|
354
|
+
name: 'crc32_calculator',
|
|
355
|
+
description: 'CRC-32 of a text',
|
|
356
|
+
implementation: "async function execute(params) { throw new Error('todo'); }",
|
|
357
|
+
parameters: { type: 'object', properties: { text: { type: 'string' } } },
|
|
358
|
+
examples: [{ input: { text: 'abc' } }],
|
|
359
|
+
})
|
|
360
|
+
: crcToolJson('CRC-32 of a text');
|
|
361
|
+
});
|
|
362
|
+
const generator = new ToolGenerator({
|
|
363
|
+
llm,
|
|
364
|
+
model: 'stub',
|
|
365
|
+
config: { ...config, requireLLMValidation: false },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const result = await generator.generate(CRC_GAP, []);
|
|
369
|
+
|
|
370
|
+
expect(result.success).toBe(true);
|
|
371
|
+
expect(result.iterations).toBe(2);
|
|
372
|
+
expect(result.tool?.metadata?.examples).toEqual([{ input: { text: 'abc' } }]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('bounds generation calls with maxTokens', async () => {
|
|
376
|
+
const llm = backend(() => crcToolJson('CRC-32 of a text'));
|
|
377
|
+
const generator = new ToolGenerator({ llm, model: 'stub', config, maxTokens: 1200 });
|
|
378
|
+
|
|
379
|
+
await generator.generate(CRC_GAP, []);
|
|
380
|
+
|
|
381
|
+
expect(llm.chat.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
382
|
+
for (const [request] of llm.chat.mock.calls) {
|
|
383
|
+
expect(request.maxTokens).toBe(1200);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('gap analysis context', () => {
|
|
389
|
+
it('shows the agent instructions to the analyzer', async () => {
|
|
390
|
+
const llm = backend(() => '{"hasGap":false,"gaps":[],"canProceed":true}');
|
|
391
|
+
const analyzer = new GapAnalyzer({ llm, model: 'stub', config, maxTokens: 700 });
|
|
392
|
+
|
|
393
|
+
await analyzer.analyze('What is the CRC-32 of "abc"?', [], {
|
|
394
|
+
instructions: 'Never compute checksums yourself: always call a tool.',
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const request = llm.chat.mock.calls[0]?.[0] as ChatRequest;
|
|
398
|
+
expect(String(request.messages[1]?.content)).toContain(
|
|
399
|
+
'Never compute checksums yourself: always call a tool.'
|
|
400
|
+
);
|
|
401
|
+
expect(request.maxTokens).toBe(700);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('does not reuse a cached analysis made under other instructions', async () => {
|
|
405
|
+
const llm = backend(() => '{"hasGap":false,"gaps":[],"canProceed":true}');
|
|
406
|
+
const analyzer = new GapAnalyzer({ llm, model: 'stub', config });
|
|
407
|
+
|
|
408
|
+
await analyzer.analyze('task', [], { instructions: 'one' });
|
|
409
|
+
await analyzer.analyze('task', [], { instructions: 'two' });
|
|
410
|
+
|
|
411
|
+
expect(llm.chat).toHaveBeenCalledTimes(2);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('tells the analyzer that listed tools cover their capability', () => {
|
|
415
|
+
const prompt = buildGapAnalysisPrompt(
|
|
416
|
+
'CRC-32 of "abc"',
|
|
417
|
+
[{ name: 'crc32_calculator', description: 'Computes the CRC-32 checksum of a text' }],
|
|
418
|
+
undefined,
|
|
419
|
+
'Always call a tool.'
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
expect(prompt).toContain('crc32_calculator: Computes the CRC-32 checksum of a text');
|
|
423
|
+
expect(prompt).toContain('AGENT INSTRUCTIONS');
|
|
424
|
+
expect(prompt).toMatch(/already provides/i);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('architecture evolution maxTokens', () => {
|
|
429
|
+
it('bounds the task profile call of the capability analyzer', async () => {
|
|
430
|
+
const llm = backend(() => '{}');
|
|
431
|
+
const analyzer = new CapabilityAnalyzer({
|
|
432
|
+
llm,
|
|
433
|
+
model: 'stub',
|
|
434
|
+
enableLLMAnalysis: true,
|
|
435
|
+
maxTokens: 400,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
await analyzer.analyzeTask('Summarize this text in one line');
|
|
439
|
+
|
|
440
|
+
expect(llm.chat.mock.calls[0]?.[0].maxTokens).toBe(400);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('bounds the calls of the parameter optimizer', async () => {
|
|
444
|
+
const llm = backend(() => '[]');
|
|
445
|
+
const optimizer = new ParameterOptimizer({
|
|
446
|
+
llm,
|
|
447
|
+
model: 'stub',
|
|
448
|
+
maxTokens: 500,
|
|
449
|
+
config: {
|
|
450
|
+
enabled: true,
|
|
451
|
+
strategy: { type: 'ucb', explorationConstant: 2 },
|
|
452
|
+
maxCandidates: 5,
|
|
453
|
+
evaluationWindow: 5,
|
|
454
|
+
minEvaluationsBeforeEvolution: 3,
|
|
455
|
+
adaptationThreshold: 0.1,
|
|
456
|
+
},
|
|
457
|
+
baseConfig: {
|
|
458
|
+
model: 'stub',
|
|
459
|
+
temperature: 0.5,
|
|
460
|
+
maxTokens: 1000,
|
|
461
|
+
toolStrategy: 'sequential',
|
|
462
|
+
reflectionDepth: 0,
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await optimizer.optimize('Summarize this text in one line');
|
|
467
|
+
|
|
468
|
+
expect(llm.chat.mock.calls.length).toBeGreaterThan(0);
|
|
469
|
+
for (const [request] of llm.chat.mock.calls) {
|
|
470
|
+
expect(request.maxTokens).toBe(500);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -21,6 +21,8 @@ export interface CapabilityAnalyzerOptions {
|
|
|
21
21
|
llm?: LLMBackend;
|
|
22
22
|
enableLLMAnalysis?: boolean;
|
|
23
23
|
model?: string;
|
|
24
|
+
/** Upper bound on the output tokens of the task profile call. Unset leaves the backend default. */
|
|
25
|
+
maxTokens?: number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface DomainKeywords {
|
|
@@ -145,11 +147,13 @@ export class CapabilityAnalyzer {
|
|
|
145
147
|
private readonly llm?: LLMBackend;
|
|
146
148
|
private readonly enableLLMAnalysis: boolean;
|
|
147
149
|
private readonly model: string;
|
|
150
|
+
private readonly maxTokens?: number;
|
|
148
151
|
private readonly profileCache = new Map<string, { profile: TaskProfile; timestamp: number }>();
|
|
149
152
|
private readonly cacheTTL = 60000;
|
|
150
153
|
|
|
151
154
|
constructor(options: CapabilityAnalyzerOptions = {}) {
|
|
152
155
|
this.llm = options.llm;
|
|
156
|
+
this.maxTokens = options.maxTokens;
|
|
153
157
|
this.enableLLMAnalysis = options.enableLLMAnalysis ?? false;
|
|
154
158
|
this.model = this.enableLLMAnalysis
|
|
155
159
|
? requireModelForLLM(options.llm, options.model, 'CapabilityAnalyzer')
|
|
@@ -218,7 +222,7 @@ export class CapabilityAnalyzer {
|
|
|
218
222
|
},
|
|
219
223
|
{ role: 'user', content: prompt },
|
|
220
224
|
],
|
|
221
|
-
{ model: this.model, temperature: 0.2 }
|
|
225
|
+
{ model: this.model, temperature: 0.2, maxTokens: this.maxTokens }
|
|
222
226
|
);
|
|
223
227
|
|
|
224
228
|
const parsed = parseTaskProfileResponse(content);
|
|
@@ -26,6 +26,11 @@ export interface ParameterOptimizerOptions {
|
|
|
26
26
|
/** Model the LLM calls use, without the provider prefix of the backend */
|
|
27
27
|
model: string;
|
|
28
28
|
availableModels?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Upper bound on the output tokens of each task profile, candidate and analysis call.
|
|
31
|
+
* Unset leaves the backend default.
|
|
32
|
+
*/
|
|
33
|
+
maxTokens?: number;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export interface OptimizationResult {
|
|
@@ -50,6 +55,7 @@ export class ParameterOptimizer {
|
|
|
50
55
|
private readonly llm: LLMBackend;
|
|
51
56
|
private readonly config: ArchitectureEvolutionConfig;
|
|
52
57
|
private readonly model: string;
|
|
58
|
+
private readonly maxTokens?: number;
|
|
53
59
|
private readonly baseConfig: ArchitectureConfig;
|
|
54
60
|
private readonly availableModels: string[];
|
|
55
61
|
private readonly capabilityAnalyzer: CapabilityAnalyzer;
|
|
@@ -65,6 +71,7 @@ export class ParameterOptimizer {
|
|
|
65
71
|
this.llm = options.llm;
|
|
66
72
|
this.config = options.config;
|
|
67
73
|
this.model = options.model;
|
|
74
|
+
this.maxTokens = options.maxTokens;
|
|
68
75
|
this.baseConfig = options.baseConfig;
|
|
69
76
|
this.availableModels = options.availableModels ?? [];
|
|
70
77
|
|
|
@@ -72,6 +79,7 @@ export class ParameterOptimizer {
|
|
|
72
79
|
llm: options.llm,
|
|
73
80
|
enableLLMAnalysis: true,
|
|
74
81
|
model: this.model,
|
|
82
|
+
maxTokens: options.maxTokens,
|
|
75
83
|
});
|
|
76
84
|
|
|
77
85
|
this.evolutionStrategy = new EvolutionStrategy({
|
|
@@ -170,7 +178,7 @@ export class ParameterOptimizer {
|
|
|
170
178
|
},
|
|
171
179
|
{ role: 'user', content: prompt },
|
|
172
180
|
],
|
|
173
|
-
{ model: this.model, temperature: 0.5 }
|
|
181
|
+
{ model: this.model, temperature: 0.5, maxTokens: this.maxTokens }
|
|
174
182
|
);
|
|
175
183
|
|
|
176
184
|
const generated = parseCandidateGenerationResponse(content, {
|
|
@@ -529,7 +537,7 @@ export class ParameterOptimizer {
|
|
|
529
537
|
{ role: 'system', content: 'You are an AI performance analyst.' },
|
|
530
538
|
{ role: 'user', content: prompt },
|
|
531
539
|
],
|
|
532
|
-
{ model: this.model, temperature: 0.2 }
|
|
540
|
+
{ model: this.model, temperature: 0.2, maxTokens: this.maxTokens }
|
|
533
541
|
);
|
|
534
542
|
|
|
535
543
|
return parsePerformanceAnalysisResponse(content);
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,9 @@ export {
|
|
|
22
22
|
parseGapAnalysisResponse,
|
|
23
23
|
parseToolGenerationResponse,
|
|
24
24
|
parseValidationResponse,
|
|
25
|
+
type ToolReview,
|
|
25
26
|
type GapAnalyzerOptions,
|
|
27
|
+
type GapAnalysisContext,
|
|
26
28
|
type ToolGeneratorOptions,
|
|
27
29
|
type ToolValidatorOptions,
|
|
28
30
|
type ValidationRule,
|
|
@@ -53,6 +53,12 @@ export interface SelfModifyingAgentConfig {
|
|
|
53
53
|
metaReasoning?: MetaReasoningOverrides;
|
|
54
54
|
architectureEvolution?: Partial<ArchitectureEvolutionConfig>;
|
|
55
55
|
constraints?: Partial<ModificationConstraintsConfig>;
|
|
56
|
+
/**
|
|
57
|
+
* Upper bound on the output tokens of each internal LLM call: gap analysis, tool
|
|
58
|
+
* generation and review, and architecture evolution. Unset leaves the backend default.
|
|
59
|
+
* Meta-reasoning has its own bound, `metaReasoning.maxMetaTokens`.
|
|
60
|
+
*/
|
|
61
|
+
maxInternalTokens?: number;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export interface SelfModifyingAgentOptions {
|
|
@@ -184,11 +190,18 @@ export class SelfModifyingAgent {
|
|
|
184
190
|
|
|
185
191
|
const toolGenConfig = this.config.toolGeneration;
|
|
186
192
|
const agentModel = this.modelName;
|
|
187
|
-
|
|
193
|
+
const maxTokens = options.config?.maxInternalTokens;
|
|
194
|
+
this.gapAnalyzer = new GapAnalyzer({
|
|
195
|
+
llm: this.llm,
|
|
196
|
+
config: toolGenConfig,
|
|
197
|
+
model: agentModel,
|
|
198
|
+
maxTokens,
|
|
199
|
+
});
|
|
188
200
|
this.toolGenerator = new ToolGenerator({
|
|
189
201
|
llm: this.llm,
|
|
190
202
|
config: toolGenConfig,
|
|
191
203
|
model: agentModel,
|
|
204
|
+
maxTokens,
|
|
192
205
|
});
|
|
193
206
|
this.toolStore = new InMemoryGeneratedToolStore();
|
|
194
207
|
|
|
@@ -204,6 +217,7 @@ export class SelfModifyingAgent {
|
|
|
204
217
|
baseConfig: this.createBaseArchitectureConfig(),
|
|
205
218
|
model: agentModel,
|
|
206
219
|
availableModels: options.availableModels,
|
|
220
|
+
maxTokens,
|
|
207
221
|
});
|
|
208
222
|
|
|
209
223
|
this.modificationValidator = new ModificationValidator({
|
|
@@ -621,7 +635,9 @@ export class SelfModifyingAgent {
|
|
|
621
635
|
|
|
622
636
|
private async analyzeAndGenerateTools(ctx: RunContext): Promise<void> {
|
|
623
637
|
try {
|
|
624
|
-
const analysis = await this.gapAnalyzer.analyze(ctx.input, ctx.tools
|
|
638
|
+
const analysis = await this.gapAnalyzer.analyze(ctx.input, ctx.tools, {
|
|
639
|
+
instructions: this.agent.instructions,
|
|
640
|
+
});
|
|
625
641
|
|
|
626
642
|
for (const gap of analysis.gaps) {
|
|
627
643
|
if (ctx.generatedTools.length >= this.config.toolGeneration.maxToolsPerSession) break;
|