@cogitator-ai/self-modifying 19.2.1 → 19.3.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 (47) hide show
  1. package/README.md +23 -6
  2. package/dist/architecture-evolution/capability-analyzer.d.ts +3 -0
  3. package/dist/architecture-evolution/capability-analyzer.d.ts.map +1 -1
  4. package/dist/architecture-evolution/capability-analyzer.js +3 -1
  5. package/dist/architecture-evolution/capability-analyzer.js.map +1 -1
  6. package/dist/architecture-evolution/parameter-optimizer.d.ts +6 -0
  7. package/dist/architecture-evolution/parameter-optimizer.d.ts.map +1 -1
  8. package/dist/architecture-evolution/parameter-optimizer.js +5 -2
  9. package/dist/architecture-evolution/parameter-optimizer.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/self-modifying-agent.d.ts +6 -0
  14. package/dist/self-modifying-agent.d.ts.map +1 -1
  15. package/dist/self-modifying-agent.js +12 -2
  16. package/dist/self-modifying-agent.js.map +1 -1
  17. package/dist/tool-generation/gap-analyzer.d.ts +13 -4
  18. package/dist/tool-generation/gap-analyzer.d.ts.map +1 -1
  19. package/dist/tool-generation/gap-analyzer.js +5 -2
  20. package/dist/tool-generation/gap-analyzer.js.map +1 -1
  21. package/dist/tool-generation/index.d.ts +2 -2
  22. package/dist/tool-generation/index.d.ts.map +1 -1
  23. package/dist/tool-generation/index.js.map +1 -1
  24. package/dist/tool-generation/prompts.d.ts +7 -2
  25. package/dist/tool-generation/prompts.d.ts.map +1 -1
  26. package/dist/tool-generation/prompts.js +44 -12
  27. package/dist/tool-generation/prompts.js.map +1 -1
  28. package/dist/tool-generation/tool-generator.d.ts +6 -0
  29. package/dist/tool-generation/tool-generator.d.ts.map +1 -1
  30. package/dist/tool-generation/tool-generator.js +26 -2
  31. package/dist/tool-generation/tool-generator.js.map +1 -1
  32. package/dist/tool-generation/tool-validator.d.ts +16 -0
  33. package/dist/tool-generation/tool-validator.d.ts.map +1 -1
  34. package/dist/tool-generation/tool-validator.js +110 -17
  35. package/dist/tool-generation/tool-validator.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/__tests__/self-modifying-agent.test.ts +52 -0
  38. package/src/__tests__/tool-review-regressions.test.ts +473 -0
  39. package/src/architecture-evolution/capability-analyzer.ts +5 -1
  40. package/src/architecture-evolution/parameter-optimizer.ts +10 -2
  41. package/src/index.ts +2 -0
  42. package/src/self-modifying-agent.ts +18 -2
  43. package/src/tool-generation/gap-analyzer.ts +24 -11
  44. package/src/tool-generation/index.ts +2 -1
  45. package/src/tool-generation/prompts.ts +52 -13
  46. package/src/tool-generation/tool-generator.ts +33 -2
  47. 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
- this.gapAnalyzer = new GapAnalyzer({ llm: this.llm, config: toolGenConfig, model: agentModel });
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;