@cogitator-ai/self-modifying 17.0.18 → 18.0.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.
- package/README.md +112 -48
- package/dist/architecture-evolution/capability-analyzer.d.ts.map +1 -1
- package/dist/architecture-evolution/capability-analyzer.js +38 -37
- package/dist/architecture-evolution/capability-analyzer.js.map +1 -1
- package/dist/architecture-evolution/index.d.ts +2 -2
- package/dist/architecture-evolution/index.d.ts.map +1 -1
- package/dist/architecture-evolution/index.js +1 -1
- package/dist/architecture-evolution/index.js.map +1 -1
- package/dist/architecture-evolution/parameter-optimizer.d.ts +5 -4
- package/dist/architecture-evolution/parameter-optimizer.d.ts.map +1 -1
- package/dist/architecture-evolution/parameter-optimizer.js +36 -31
- package/dist/architecture-evolution/parameter-optimizer.js.map +1 -1
- package/dist/architecture-evolution/prompts.d.ts +21 -2
- package/dist/architecture-evolution/prompts.d.ts.map +1 -1
- package/dist/architecture-evolution/prompts.js +150 -36
- package/dist/architecture-evolution/prompts.js.map +1 -1
- package/dist/constraints/modification-validator.d.ts +1 -0
- package/dist/constraints/modification-validator.d.ts.map +1 -1
- package/dist/constraints/modification-validator.js +30 -14
- package/dist/constraints/modification-validator.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/meta-reasoning/meta-reasoner.d.ts +3 -0
- package/dist/meta-reasoning/meta-reasoner.d.ts.map +1 -1
- package/dist/meta-reasoning/meta-reasoner.js +101 -33
- package/dist/meta-reasoning/meta-reasoner.js.map +1 -1
- package/dist/meta-reasoning/prompts.d.ts +10 -14
- package/dist/meta-reasoning/prompts.d.ts.map +1 -1
- package/dist/meta-reasoning/prompts.js +109 -1
- package/dist/meta-reasoning/prompts.js.map +1 -1
- package/dist/self-modifying-agent.d.ts +83 -17
- package/dist/self-modifying-agent.d.ts.map +1 -1
- package/dist/self-modifying-agent.js +557 -444
- package/dist/self-modifying-agent.js.map +1 -1
- package/dist/tool-generation/gap-analyzer.d.ts +0 -1
- package/dist/tool-generation/gap-analyzer.d.ts.map +1 -1
- package/dist/tool-generation/gap-analyzer.js +14 -14
- package/dist/tool-generation/gap-analyzer.js.map +1 -1
- package/dist/tool-generation/index.d.ts +3 -3
- package/dist/tool-generation/index.d.ts.map +1 -1
- package/dist/tool-generation/index.js +2 -2
- package/dist/tool-generation/index.js.map +1 -1
- package/dist/tool-generation/prompts.d.ts +1 -0
- package/dist/tool-generation/prompts.d.ts.map +1 -1
- package/dist/tool-generation/prompts.js +33 -8
- package/dist/tool-generation/prompts.js.map +1 -1
- package/dist/tool-generation/tool-generator.d.ts +5 -5
- package/dist/tool-generation/tool-generator.d.ts.map +1 -1
- package/dist/tool-generation/tool-generator.js +41 -28
- package/dist/tool-generation/tool-generator.js.map +1 -1
- package/dist/tool-generation/tool-sandbox.d.ts +9 -6
- package/dist/tool-generation/tool-sandbox.d.ts.map +1 -1
- package/dist/tool-generation/tool-sandbox.js +257 -157
- package/dist/tool-generation/tool-sandbox.js.map +1 -1
- package/dist/tool-generation/tool-validator.d.ts +3 -8
- package/dist/tool-generation/tool-validator.d.ts.map +1 -1
- package/dist/tool-generation/tool-validator.js +53 -54
- package/dist/tool-generation/tool-validator.js.map +1 -1
- package/dist/utils/llm-helper.d.ts.map +1 -1
- package/dist/utils/llm-helper.js +1 -0
- package/dist/utils/llm-helper.js.map +1 -1
- package/package.json +3 -12
- package/src/__tests__/component-regressions.test.ts +599 -0
- package/src/__tests__/meta-reasoning.test.ts +5 -1
- package/src/__tests__/sandbox-security.test.ts +186 -0
- package/src/__tests__/self-modifying-agent.test.ts +363 -1
- package/src/architecture-evolution/capability-analyzer.ts +56 -40
- package/src/architecture-evolution/index.ts +2 -0
- package/src/architecture-evolution/parameter-optimizer.ts +52 -34
- package/src/architecture-evolution/prompts.ts +179 -41
- package/src/constraints/modification-validator.ts +30 -14
- package/src/index.ts +14 -0
- package/src/meta-reasoning/meta-reasoner.ts +105 -40
- package/src/meta-reasoning/prompts.ts +129 -15
- package/src/self-modifying-agent.ts +764 -489
- package/src/tool-generation/gap-analyzer.ts +19 -18
- package/src/tool-generation/index.ts +13 -3
- package/src/tool-generation/prompts.ts +41 -8
- package/src/tool-generation/tool-generator.ts +59 -33
- package/src/tool-generation/tool-sandbox.ts +306 -175
- package/src/tool-generation/tool-validator.ts +66 -63
- package/src/utils/llm-helper.ts +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import type { GeneratedTool } from '@cogitator-ai/types';
|
|
3
|
+
import { ToolSandbox, deepEqual } from '../tool-generation';
|
|
4
|
+
|
|
5
|
+
function makeTool(implementation: string): GeneratedTool {
|
|
6
|
+
return {
|
|
7
|
+
id: 'sandbox_test',
|
|
8
|
+
name: 'sandbox_test',
|
|
9
|
+
description: 'sandbox test tool',
|
|
10
|
+
implementation,
|
|
11
|
+
parameters: { type: 'object', properties: {} },
|
|
12
|
+
createdAt: new Date(),
|
|
13
|
+
version: 1,
|
|
14
|
+
status: 'validated',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('ToolSandbox isolation', () => {
|
|
19
|
+
const sandbox = new ToolSandbox({ enabled: true, maxExecutionTime: 3000 });
|
|
20
|
+
|
|
21
|
+
it('blocks constructor-chain escapes to the host realm', async () => {
|
|
22
|
+
const result = await sandbox.execute(
|
|
23
|
+
makeTool(`async function execute() {
|
|
24
|
+
const F = Array["constr" + "uctor"];
|
|
25
|
+
return F("return pro" + "cess")();
|
|
26
|
+
}`),
|
|
27
|
+
{}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(result.success).toBe(false);
|
|
31
|
+
expect(result.error).toMatch(/code generation/i);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('does not expose host globals inside the sandbox', async () => {
|
|
35
|
+
const result = await sandbox.execute(
|
|
36
|
+
makeTool(`async function execute() {
|
|
37
|
+
return [typeof process, typeof require, typeof setTimeout, typeof Buffer];
|
|
38
|
+
}`),
|
|
39
|
+
{}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result.success).toBe(true);
|
|
43
|
+
expect(result.result).toEqual(['undefined', 'undefined', 'undefined', 'undefined']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('passes params as sandbox-realm data that cannot reach host constructors', async () => {
|
|
47
|
+
const result = await sandbox.execute(
|
|
48
|
+
makeTool(`async function execute(params) {
|
|
49
|
+
const ctor = params["constr" + "uctor"]["constr" + "uctor"];
|
|
50
|
+
return ctor("return 1")();
|
|
51
|
+
}`),
|
|
52
|
+
{ a: 1 }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(result.success).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('captures console output with JSON-formatted objects', async () => {
|
|
59
|
+
const result = await sandbox.execute(
|
|
60
|
+
makeTool(`async function execute(params) {
|
|
61
|
+
console.log("value", { a: params.a });
|
|
62
|
+
console.warn("careful");
|
|
63
|
+
return params.a;
|
|
64
|
+
}`),
|
|
65
|
+
{ a: 5 }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result.success).toBe(true);
|
|
69
|
+
expect(result.logs).toEqual(['[LOG] value {"a":5}', '[WARN] careful']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('reports promises that can never settle without waiting for the timeout', async () => {
|
|
73
|
+
const started = Date.now();
|
|
74
|
+
const result = await sandbox.execute(
|
|
75
|
+
makeTool('async function execute() { return new Promise(() => {}); }'),
|
|
76
|
+
{}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result.success).toBe(false);
|
|
80
|
+
expect(result.error).toMatch(/never settled/);
|
|
81
|
+
expect(Date.now() - started).toBeLessThan(2500);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects non-serializable parameters', async () => {
|
|
85
|
+
const circular: Record<string, unknown> = {};
|
|
86
|
+
circular.self = circular;
|
|
87
|
+
|
|
88
|
+
const result = await sandbox.execute(
|
|
89
|
+
makeTool('async function execute(p) { return 1; }'),
|
|
90
|
+
circular
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(result.success).toBe(false);
|
|
94
|
+
expect(result.error).toMatch(/JSON-serializable/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('reports non-serializable results instead of crashing the worker', async () => {
|
|
98
|
+
const result = await sandbox.execute(
|
|
99
|
+
makeTool('async function execute() { const a = {}; a.self = a; return a; }'),
|
|
100
|
+
{}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(false);
|
|
104
|
+
expect(result.error).toMatch(/non-serializable/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('allows RegExp.prototype.exec in generated code', async () => {
|
|
108
|
+
const result = await sandbox.execute(
|
|
109
|
+
makeTool('async function execute(p) { return /(\\d+)/.exec(p.text)[1]; }'),
|
|
110
|
+
{ text: 'order 42' }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(result).toMatchObject({ success: true, result: '42' });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('ToolSandbox.testWithCases', () => {
|
|
118
|
+
const sandbox = new ToolSandbox({ enabled: true, maxExecutionTime: 500 });
|
|
119
|
+
|
|
120
|
+
it('distinguishes thrown errors from timeouts for shouldThrow and allowThrow cases', async () => {
|
|
121
|
+
const tool = makeTool(`async function execute(params) {
|
|
122
|
+
if (params.mode === 'throw') throw new Error('invalid input');
|
|
123
|
+
if (params.mode === 'hang') { while (true) {} }
|
|
124
|
+
return 'ok';
|
|
125
|
+
}`);
|
|
126
|
+
|
|
127
|
+
const { results } = await sandbox.testWithCases(tool, [
|
|
128
|
+
{ input: { mode: 'throw' }, shouldThrow: true },
|
|
129
|
+
{ input: { mode: 'throw' }, allowThrow: true },
|
|
130
|
+
{ input: { mode: 'ok' }, allowThrow: true },
|
|
131
|
+
{ input: { mode: 'hang' }, shouldThrow: true },
|
|
132
|
+
{ input: { mode: 'hang' }, allowThrow: true },
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
expect(results.map((r) => r.passed)).toEqual([true, true, true, false, false]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('ToolSandbox without isolation', () => {
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
vi.useRealTimers();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('clears its timeout timer after execution completes', async () => {
|
|
145
|
+
vi.useFakeTimers();
|
|
146
|
+
const sandbox = new ToolSandbox({ enabled: false, maxExecutionTime: 60_000 });
|
|
147
|
+
|
|
148
|
+
const result = await sandbox.execute(
|
|
149
|
+
makeTool('async function execute(p) { return p.a * 2; }'),
|
|
150
|
+
{ a: 21 }
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(result).toMatchObject({ success: true, result: 42 });
|
|
154
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('deepEqual', () => {
|
|
159
|
+
it('treats shared references and distinct copies as equal', () => {
|
|
160
|
+
const shared = { v: 1 };
|
|
161
|
+
expect(deepEqual({ a: shared, b: shared }, { a: { v: 1 }, b: { v: 1 } })).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('handles circular arrays and objects without overflowing the stack', () => {
|
|
165
|
+
const a: unknown[] = [1];
|
|
166
|
+
a.push(a);
|
|
167
|
+
const b: unknown[] = [1];
|
|
168
|
+
b.push(b);
|
|
169
|
+
expect(deepEqual(a, b)).toBe(true);
|
|
170
|
+
|
|
171
|
+
const x: Record<string, unknown> = { n: 1 };
|
|
172
|
+
x.self = x;
|
|
173
|
+
const y: Record<string, unknown> = { n: 2 };
|
|
174
|
+
y.self = y;
|
|
175
|
+
expect(deepEqual(x, y)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('compares sets of objects structurally', () => {
|
|
179
|
+
expect(deepEqual(new Set([{ a: 1 }]), new Set([{ a: 1 }]))).toBe(true);
|
|
180
|
+
expect(deepEqual(new Set([{ a: 1 }]), new Set([{ a: 2 }]))).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('distinguishes arrays from plain objects', () => {
|
|
184
|
+
expect(deepEqual([1], { 0: 1 })).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { SelfModifyingAgent } from '../self-modifying-agent';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Agent,
|
|
5
|
+
LLMBackend,
|
|
6
|
+
Tool,
|
|
7
|
+
ChatRequest,
|
|
8
|
+
ChatResponse,
|
|
9
|
+
ToolCall,
|
|
10
|
+
} from '@cogitator-ai/types';
|
|
4
11
|
import { z } from 'zod';
|
|
5
12
|
|
|
6
13
|
function createMockAgent(tools: Tool[] = []): Agent {
|
|
@@ -231,3 +238,358 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
231
238
|
expect(chatCall.tools).toBeUndefined();
|
|
232
239
|
});
|
|
233
240
|
});
|
|
241
|
+
|
|
242
|
+
const ADD_TOOL_JSON = JSON.stringify({
|
|
243
|
+
name: 'add_numbers',
|
|
244
|
+
description: 'Add two numbers',
|
|
245
|
+
implementation:
|
|
246
|
+
"async function execute(params) { if (typeof params.a !== 'number' || typeof params.b !== 'number') { throw new Error('a and b must be numbers'); } return params.a + params.b; }",
|
|
247
|
+
parameters: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: { a: { type: 'number' }, b: { type: 'number' } },
|
|
250
|
+
required: ['a', 'b'],
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const ADD_GAP_JSON = JSON.stringify({
|
|
255
|
+
hasGap: true,
|
|
256
|
+
gaps: [
|
|
257
|
+
{
|
|
258
|
+
id: 'gap_add',
|
|
259
|
+
description: 'Add two numbers',
|
|
260
|
+
requiredCapability: 'a + b',
|
|
261
|
+
suggestedToolName: 'add_numbers',
|
|
262
|
+
complexity: 'simple',
|
|
263
|
+
confidence: 0.95,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
canProceed: false,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
interface RouterReplies {
|
|
270
|
+
gap?: string;
|
|
271
|
+
tool?: string;
|
|
272
|
+
profile?: string;
|
|
273
|
+
candidates?: string;
|
|
274
|
+
meta?: string;
|
|
275
|
+
agent: (request: ChatRequest) => ChatResponse;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function routedLLM(replies: RouterReplies): LLMBackend & { chat: ReturnType<typeof vi.fn> } {
|
|
279
|
+
const chat = vi.fn(async (request: ChatRequest): Promise<ChatResponse> => {
|
|
280
|
+
const system = String(request.messages[0]?.content ?? '');
|
|
281
|
+
if (system.includes('capability analyzer')) return chatResponse(replies.gap ?? '{"gaps":[]}');
|
|
282
|
+
if (system.includes('You generate JavaScript tools')) return chatResponse(replies.tool ?? '');
|
|
283
|
+
if (system.includes('task analysis expert')) return chatResponse(replies.profile ?? '{}');
|
|
284
|
+
if (system.includes('architecture optimizer')) return chatResponse(replies.candidates ?? '[]');
|
|
285
|
+
if (system.includes('meta-reasoning system')) return chatResponse(replies.meta ?? '{}');
|
|
286
|
+
return replies.agent(request);
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
provider: 'ollama',
|
|
290
|
+
chat,
|
|
291
|
+
chatStream: async function* () {
|
|
292
|
+
yield { id: 'x', delta: {} };
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function agentCalls(llm: { chat: ReturnType<typeof vi.fn> }): ChatRequest[] {
|
|
298
|
+
return llm.chat.mock.calls
|
|
299
|
+
.map((call) => call[0] as ChatRequest)
|
|
300
|
+
.filter((req) => String(req.messages[0]?.content) === 'You are a test assistant.');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const quietConfig = {
|
|
304
|
+
toolGeneration: { enabled: false },
|
|
305
|
+
metaReasoning: { enabled: false },
|
|
306
|
+
architectureEvolution: { enabled: false },
|
|
307
|
+
constraints: { enabled: false },
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
describe('SelfModifyingAgent run semantics', () => {
|
|
311
|
+
it('completes a short answer with a single LLM call even with meta-reasoning enabled', async () => {
|
|
312
|
+
const llm = routedLLM({ agent: () => chatResponse('4') });
|
|
313
|
+
const selfMod = new SelfModifyingAgent({
|
|
314
|
+
agent: createMockAgent(),
|
|
315
|
+
llm,
|
|
316
|
+
config: {
|
|
317
|
+
toolGeneration: { enabled: false },
|
|
318
|
+
architectureEvolution: { enabled: false },
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const result = await selfMod.run('What is 2 + 2?');
|
|
323
|
+
|
|
324
|
+
expect(result.output).toBe('4');
|
|
325
|
+
expect(llm.chat).toHaveBeenCalledTimes(1);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('skips every self-modification step when disabled globally', async () => {
|
|
329
|
+
const llm = routedLLM({
|
|
330
|
+
gap: ADD_GAP_JSON,
|
|
331
|
+
tool: ADD_TOOL_JSON,
|
|
332
|
+
agent: () => chatResponse('ok'),
|
|
333
|
+
});
|
|
334
|
+
const selfMod = new SelfModifyingAgent({
|
|
335
|
+
agent: createMockAgent(),
|
|
336
|
+
llm,
|
|
337
|
+
config: { enabled: false },
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const result = await selfMod.run('add 1 and 2');
|
|
341
|
+
|
|
342
|
+
expect(result.output).toBe('ok');
|
|
343
|
+
expect(result.toolsGenerated).toEqual([]);
|
|
344
|
+
expect(llm.chat).toHaveBeenCalledTimes(1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('uses the agent temperature and max tokens as the baseline config', async () => {
|
|
348
|
+
const llm = routedLLM({ agent: () => chatResponse('done') });
|
|
349
|
+
const agent = { ...createMockAgent(), config: { temperature: 0.15, maxTokens: 321 } } as Agent;
|
|
350
|
+
const selfMod = new SelfModifyingAgent({ agent, llm, config: quietConfig });
|
|
351
|
+
|
|
352
|
+
const result = await selfMod.run('hi');
|
|
353
|
+
|
|
354
|
+
expect(llm.chat.mock.calls[0][0]).toMatchObject({ temperature: 0.15, maxTokens: 321 });
|
|
355
|
+
expect(result.finalConfig).toMatchObject({ temperature: 0.15, maxTokens: 321 });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('validates tool arguments against the tool schema before executing', async () => {
|
|
359
|
+
const calculator = createMockTool('calculator', 42);
|
|
360
|
+
const llm = routedLLM({
|
|
361
|
+
agent: vi
|
|
362
|
+
.fn()
|
|
363
|
+
.mockReturnValueOnce(
|
|
364
|
+
chatResponse('', [{ id: 'c1', name: 'calculator', arguments: { input: 7 } }])
|
|
365
|
+
)
|
|
366
|
+
.mockReturnValueOnce(chatResponse('could not compute')),
|
|
367
|
+
});
|
|
368
|
+
const selfMod = new SelfModifyingAgent({
|
|
369
|
+
agent: createMockAgent([calculator]),
|
|
370
|
+
llm,
|
|
371
|
+
config: quietConfig,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await selfMod.run('compute');
|
|
375
|
+
|
|
376
|
+
expect(calculator.execute).not.toHaveBeenCalled();
|
|
377
|
+
const toolMessage = llm.chat.mock.calls[1][0].messages.find(
|
|
378
|
+
(m: { role: string }) => m.role === 'tool'
|
|
379
|
+
);
|
|
380
|
+
expect(toolMessage.content).toContain('Invalid arguments');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('serializes concurrent runs so contexts never mix', async () => {
|
|
384
|
+
let resolveFirst: ((value: ChatResponse) => void) | undefined;
|
|
385
|
+
const llm = routedLLM({
|
|
386
|
+
agent: (request) => {
|
|
387
|
+
const input = String(request.messages[request.messages.length - 1].content);
|
|
388
|
+
if (input === 'first') {
|
|
389
|
+
return new Promise<ChatResponse>((resolve) => {
|
|
390
|
+
resolveFirst = resolve;
|
|
391
|
+
}) as unknown as ChatResponse;
|
|
392
|
+
}
|
|
393
|
+
return chatResponse(`answer:${input}`);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
const selfMod = new SelfModifyingAgent({ agent: createMockAgent(), llm, config: quietConfig });
|
|
397
|
+
|
|
398
|
+
const first = selfMod.run('first');
|
|
399
|
+
const second = selfMod.run('second');
|
|
400
|
+
await vi.waitFor(() => expect(resolveFirst).toBeDefined());
|
|
401
|
+
expect(llm.chat).toHaveBeenCalledTimes(1);
|
|
402
|
+
|
|
403
|
+
resolveFirst!(chatResponse('answer:first'));
|
|
404
|
+
|
|
405
|
+
await expect(first).resolves.toMatchObject({ output: 'answer:first' });
|
|
406
|
+
await expect(second).resolves.toMatchObject({ output: 'answer:second' });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('provides typed events and an unsubscribe function', async () => {
|
|
410
|
+
const llm = routedLLM({ agent: () => chatResponse('done') });
|
|
411
|
+
const selfMod = new SelfModifyingAgent({ agent: createMockAgent(), llm, config: quietConfig });
|
|
412
|
+
const inputs: string[] = [];
|
|
413
|
+
|
|
414
|
+
const unsubscribe = selfMod.on('run_started', (event) => {
|
|
415
|
+
inputs.push(event.data.input);
|
|
416
|
+
});
|
|
417
|
+
await selfMod.run('one');
|
|
418
|
+
unsubscribe();
|
|
419
|
+
await selfMod.run('two');
|
|
420
|
+
|
|
421
|
+
expect(inputs).toEqual(['one']);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('SelfModifyingAgent tool generation', () => {
|
|
426
|
+
it('activates accepted tools, exposes them to the LLM and keeps them for later runs', async () => {
|
|
427
|
+
const llm = routedLLM({
|
|
428
|
+
gap: ADD_GAP_JSON,
|
|
429
|
+
tool: ADD_TOOL_JSON,
|
|
430
|
+
agent: (request) => {
|
|
431
|
+
const hasToolResult = request.messages.some((m) => m.role === 'tool');
|
|
432
|
+
if (!hasToolResult && request.tools?.some((t) => t.name === 'add_numbers')) {
|
|
433
|
+
return chatResponse('', [{ id: 'c1', name: 'add_numbers', arguments: { a: 2, b: 3 } }]);
|
|
434
|
+
}
|
|
435
|
+
return chatResponse('The sum is 5');
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
const selfMod = new SelfModifyingAgent({
|
|
439
|
+
agent: createMockAgent(),
|
|
440
|
+
llm,
|
|
441
|
+
config: {
|
|
442
|
+
toolGeneration: { requireLLMValidation: false },
|
|
443
|
+
metaReasoning: { enabled: false },
|
|
444
|
+
architectureEvolution: { enabled: false },
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const first = await selfMod.run('Add 2 and 3');
|
|
449
|
+
|
|
450
|
+
expect(first.output).toBe('The sum is 5');
|
|
451
|
+
expect(first.toolsGenerated.map((t) => t.name)).toEqual(['add_numbers']);
|
|
452
|
+
const active = await selfMod.getGeneratedTools();
|
|
453
|
+
expect(active.map((t) => t.status)).toEqual(['active']);
|
|
454
|
+
|
|
455
|
+
const toolMessage = agentCalls(llm)
|
|
456
|
+
.flatMap((r) => r.messages)
|
|
457
|
+
.find((m) => m.role === 'tool');
|
|
458
|
+
expect(toolMessage?.content).toBe('5');
|
|
459
|
+
|
|
460
|
+
const firstSchema = agentCalls(llm)[0].tools?.find((t) => t.name === 'add_numbers');
|
|
461
|
+
expect(firstSchema?.parameters).toEqual({
|
|
462
|
+
type: 'object',
|
|
463
|
+
properties: { a: { type: 'number' }, b: { type: 'number' } },
|
|
464
|
+
required: ['a', 'b'],
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
llm.chat.mockClear();
|
|
468
|
+
await selfMod.run('Add 2 and 3 again');
|
|
469
|
+
expect(agentCalls(llm)[0].tools?.map((t) => t.name)).toContain('add_numbers');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('does not store tools rejected by modification constraints', async () => {
|
|
473
|
+
const llm = routedLLM({
|
|
474
|
+
gap: ADD_GAP_JSON,
|
|
475
|
+
tool: ADD_TOOL_JSON,
|
|
476
|
+
agent: () => chatResponse('ok'),
|
|
477
|
+
});
|
|
478
|
+
const selfMod = new SelfModifyingAgent({
|
|
479
|
+
agent: createMockAgent(),
|
|
480
|
+
llm,
|
|
481
|
+
config: {
|
|
482
|
+
toolGeneration: { requireLLMValidation: false, sandboxConfig: { enabled: false } },
|
|
483
|
+
metaReasoning: { enabled: false },
|
|
484
|
+
architectureEvolution: { enabled: false },
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
const completed: Array<{ success: boolean; error?: string }> = [];
|
|
488
|
+
selfMod.on('tool_generation_completed', (e) => completed.push(e.data));
|
|
489
|
+
|
|
490
|
+
const result = await selfMod.run('Add 2 and 3');
|
|
491
|
+
|
|
492
|
+
expect(result.toolsGenerated).toEqual([]);
|
|
493
|
+
expect(await selfMod.getGeneratedTools()).toEqual([]);
|
|
494
|
+
expect(completed).toEqual([
|
|
495
|
+
expect.objectContaining({ success: false, error: 'Rejected by modification constraints' }),
|
|
496
|
+
]);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('SelfModifyingAgent meta-reasoning', () => {
|
|
501
|
+
it('retries failed steps with the adapted reasoning mode', async () => {
|
|
502
|
+
const llm = routedLLM({
|
|
503
|
+
meta: JSON.stringify({
|
|
504
|
+
onTrack: false,
|
|
505
|
+
confidence: 0.3,
|
|
506
|
+
recommendation: {
|
|
507
|
+
action: 'switch_mode',
|
|
508
|
+
newMode: 'creative',
|
|
509
|
+
confidence: 0.9,
|
|
510
|
+
reasoning: 'try a different approach',
|
|
511
|
+
},
|
|
512
|
+
}),
|
|
513
|
+
agent: vi.fn().mockReturnValueOnce(chatResponse('')).mockReturnValueOnce(chatResponse('42')),
|
|
514
|
+
});
|
|
515
|
+
const selfMod = new SelfModifyingAgent({
|
|
516
|
+
agent: createMockAgent(),
|
|
517
|
+
llm,
|
|
518
|
+
config: {
|
|
519
|
+
toolGeneration: { enabled: false },
|
|
520
|
+
architectureEvolution: { enabled: false },
|
|
521
|
+
metaReasoning: { metaAssessmentCooldown: 0, adaptationCooldown: 0 },
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
const modes: string[] = [];
|
|
525
|
+
selfMod.on('strategy_changed', (e) => modes.push(`${e.data.previousMode}->${e.data.newMode}`));
|
|
526
|
+
|
|
527
|
+
const result = await selfMod.run('answer');
|
|
528
|
+
const calls = agentCalls(llm);
|
|
529
|
+
|
|
530
|
+
expect(result.output).toBe('42');
|
|
531
|
+
expect(calls.map((c) => c.temperature)).toEqual([0.5, 0.9]);
|
|
532
|
+
expect(result.adaptationsMade).toHaveLength(1);
|
|
533
|
+
expect(modes).toEqual(['analytical->analytical', 'analytical->creative']);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe('SelfModifyingAgent architecture evolution', () => {
|
|
538
|
+
it('applies config changes and records outcomes so candidates get evaluated', async () => {
|
|
539
|
+
const llm = routedLLM({
|
|
540
|
+
profile: JSON.stringify({ complexity: 'simple', domain: 'general' }),
|
|
541
|
+
candidates: JSON.stringify([
|
|
542
|
+
{ id: 'cool', config: { temperature: 0.2, reflectionDepth: 1 }, risk: 'low' },
|
|
543
|
+
]),
|
|
544
|
+
agent: () => chatResponse('answer'),
|
|
545
|
+
});
|
|
546
|
+
const selfMod = new SelfModifyingAgent({
|
|
547
|
+
agent: createMockAgent(),
|
|
548
|
+
llm,
|
|
549
|
+
config: {
|
|
550
|
+
toolGeneration: { enabled: false },
|
|
551
|
+
metaReasoning: { enabled: false },
|
|
552
|
+
architectureEvolution: { strategy: { type: 'ucb' } },
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
const evolved: Array<string | undefined> = [];
|
|
556
|
+
selfMod.on('architecture_evolved', (e) => evolved.push(e.data.candidateId));
|
|
557
|
+
|
|
558
|
+
const first = await selfMod.run('task');
|
|
559
|
+
expect(first.finalConfig.temperature).toBe(0.5);
|
|
560
|
+
|
|
561
|
+
llm.chat.mockClear();
|
|
562
|
+
const second = await selfMod.run('task');
|
|
563
|
+
|
|
564
|
+
expect(second.finalConfig).toMatchObject({ temperature: 0.2, reflectionDepth: 1 });
|
|
565
|
+
expect(evolved).toEqual(['cool']);
|
|
566
|
+
const calls = agentCalls(llm);
|
|
567
|
+
expect(calls).toHaveLength(2);
|
|
568
|
+
expect(String(calls[1].messages[calls[1].messages.length - 1].content)).toContain(
|
|
569
|
+
'Review your previous answer'
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
describe('SelfModifyingAgent model resolution', () => {
|
|
575
|
+
it('strips the backend provider prefix from the agent model', async () => {
|
|
576
|
+
const llm = routedLLM({ agent: () => chatResponse('done') });
|
|
577
|
+
const agent = { ...createMockAgent(), model: 'ollama/llama3.2' } as Agent;
|
|
578
|
+
const selfMod = new SelfModifyingAgent({ agent, llm, config: quietConfig });
|
|
579
|
+
|
|
580
|
+
const result = await selfMod.run('hi');
|
|
581
|
+
|
|
582
|
+
expect(llm.chat.mock.calls[0][0].model).toBe('llama3.2');
|
|
583
|
+
expect(result.finalConfig.model).toBe('llama3.2');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('keeps model names whose prefix belongs to another provider', async () => {
|
|
587
|
+
const llm = routedLLM({ agent: () => chatResponse('done') });
|
|
588
|
+
const agent = { ...createMockAgent(), model: 'openai/gpt-4o' } as Agent;
|
|
589
|
+
const selfMod = new SelfModifyingAgent({ agent, llm, config: quietConfig });
|
|
590
|
+
|
|
591
|
+
await selfMod.run('hi');
|
|
592
|
+
|
|
593
|
+
expect(llm.chat.mock.calls[0][0].model).toBe('openai/gpt-4o');
|
|
594
|
+
});
|
|
595
|
+
});
|