@cogitator-ai/self-modifying 17.0.17 → 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 +44 -40
- package/dist/architecture-evolution/capability-analyzer.js.map +1 -1
- package/dist/architecture-evolution/evolution-strategy.d.ts.map +1 -1
- package/dist/architecture-evolution/evolution-strategy.js +4 -2
- package/dist/architecture-evolution/evolution-strategy.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 +62 -34
- 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 +189 -37
- 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 +45 -18
- package/dist/constraints/modification-validator.js.map +1 -1
- package/dist/constraints/rollback-manager.d.ts +1 -0
- package/dist/constraints/rollback-manager.d.ts.map +1 -1
- package/dist/constraints/rollback-manager.js +25 -1
- package/dist/constraints/rollback-manager.js.map +1 -1
- package/dist/events/event-emitter.d.ts +0 -1
- package/dist/events/event-emitter.d.ts.map +1 -1
- package/dist/events/event-emitter.js +6 -10
- package/dist/events/event-emitter.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 +4 -1
- package/dist/meta-reasoning/meta-reasoner.d.ts.map +1 -1
- package/dist/meta-reasoning/meta-reasoner.js +120 -43
- package/dist/meta-reasoning/meta-reasoner.js.map +1 -1
- package/dist/meta-reasoning/observation-collector.d.ts.map +1 -1
- package/dist/meta-reasoning/observation-collector.js +6 -2
- package/dist/meta-reasoning/observation-collector.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 +108 -33
- package/dist/meta-reasoning/prompts.js.map +1 -1
- package/dist/meta-reasoning/strategy-selector.d.ts.map +1 -1
- package/dist/meta-reasoning/strategy-selector.js +6 -0
- package/dist/meta-reasoning/strategy-selector.js.map +1 -1
- package/dist/self-modifying-agent.d.ts +88 -15
- package/dist/self-modifying-agent.d.ts.map +1 -1
- package/dist/self-modifying-agent.js +583 -376
- 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 +16 -15
- package/dist/tool-generation/gap-analyzer.js.map +1 -1
- package/dist/tool-generation/generated-tool-store.d.ts.map +1 -1
- package/dist/tool-generation/generated-tool-store.js +90 -10
- package/dist/tool-generation/generated-tool-store.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 +4 -3
- package/dist/tool-generation/prompts.d.ts.map +1 -1
- package/dist/tool-generation/prompts.js +96 -18
- package/dist/tool-generation/prompts.js.map +1 -1
- package/dist/tool-generation/tool-generator.d.ts +6 -6
- package/dist/tool-generation/tool-generator.d.ts.map +1 -1
- package/dist/tool-generation/tool-generator.js +88 -23
- package/dist/tool-generation/tool-generator.js.map +1 -1
- package/dist/tool-generation/tool-sandbox.d.ts +10 -11
- package/dist/tool-generation/tool-sandbox.d.ts.map +1 -1
- package/dist/tool-generation/tool-sandbox.js +346 -156
- package/dist/tool-generation/tool-sandbox.js.map +1 -1
- package/dist/tool-generation/tool-validator.d.ts +4 -8
- package/dist/tool-generation/tool-validator.d.ts.map +1 -1
- package/dist/tool-generation/tool-validator.js +74 -28
- package/dist/tool-generation/tool-validator.js.map +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +30 -5
- package/dist/utils/index.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 +405 -11
- package/src/architecture-evolution/capability-analyzer.ts +63 -44
- package/src/architecture-evolution/evolution-strategy.ts +5 -2
- package/src/architecture-evolution/index.ts +2 -0
- package/src/architecture-evolution/parameter-optimizer.ts +82 -40
- package/src/architecture-evolution/prompts.ts +224 -42
- package/src/constraints/modification-validator.ts +48 -21
- package/src/constraints/rollback-manager.ts +20 -1
- package/src/events/event-emitter.ts +6 -15
- package/src/index.ts +14 -0
- package/src/meta-reasoning/meta-reasoner.ts +124 -52
- package/src/meta-reasoning/observation-collector.ts +6 -2
- package/src/meta-reasoning/prompts.ts +131 -56
- package/src/meta-reasoning/strategy-selector.ts +6 -0
- package/src/self-modifying-agent.ts +797 -413
- package/src/tool-generation/gap-analyzer.ts +21 -19
- package/src/tool-generation/generated-tool-store.ts +97 -10
- package/src/tool-generation/index.ts +13 -3
- package/src/tool-generation/prompts.ts +120 -18
- package/src/tool-generation/tool-generator.ts +120 -26
- package/src/tool-generation/tool-sandbox.ts +408 -206
- package/src/tool-generation/tool-validator.ts +90 -37
- package/src/utils/index.ts +29 -5
- 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 {
|
|
@@ -46,7 +53,11 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
46
53
|
.mockResolvedValueOnce(
|
|
47
54
|
chatResponse('', [{ id: 'call_1', name: 'calculator', arguments: { input: '6*7' } }])
|
|
48
55
|
)
|
|
49
|
-
.mockResolvedValueOnce(
|
|
56
|
+
.mockResolvedValueOnce(
|
|
57
|
+
chatResponse(
|
|
58
|
+
'The answer to your calculation is 42. I used the calculator tool to compute 6 times 7.'
|
|
59
|
+
)
|
|
60
|
+
),
|
|
50
61
|
};
|
|
51
62
|
|
|
52
63
|
const agent = createMockAgent([calculator]);
|
|
@@ -63,7 +74,9 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
63
74
|
|
|
64
75
|
const result = await selfMod.run('What is 6 times 7?');
|
|
65
76
|
|
|
66
|
-
expect(result.output).toBe(
|
|
77
|
+
expect(result.output).toBe(
|
|
78
|
+
'The answer to your calculation is 42. I used the calculator tool to compute 6 times 7.'
|
|
79
|
+
);
|
|
67
80
|
expect(calculator.execute).toHaveBeenCalledOnce();
|
|
68
81
|
expect(calculator.execute).toHaveBeenCalledWith(
|
|
69
82
|
{ input: '6*7' },
|
|
@@ -89,7 +102,11 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
89
102
|
.mockResolvedValueOnce(
|
|
90
103
|
chatResponse('', [{ id: 'call_2', name: 'format', arguments: { input: 'found it' } }])
|
|
91
104
|
)
|
|
92
|
-
.mockResolvedValueOnce(
|
|
105
|
+
.mockResolvedValueOnce(
|
|
106
|
+
chatResponse(
|
|
107
|
+
'Here is your formatted result. I searched for the information and then formatted it for you.'
|
|
108
|
+
)
|
|
109
|
+
),
|
|
93
110
|
};
|
|
94
111
|
|
|
95
112
|
const agent = createMockAgent([search, format]);
|
|
@@ -106,7 +123,9 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
106
123
|
|
|
107
124
|
const result = await selfMod.run('Search and format');
|
|
108
125
|
|
|
109
|
-
expect(result.output).toBe(
|
|
126
|
+
expect(result.output).toBe(
|
|
127
|
+
'Here is your formatted result. I searched for the information and then formatted it for you.'
|
|
128
|
+
);
|
|
110
129
|
expect(search.execute).toHaveBeenCalledOnce();
|
|
111
130
|
expect(format.execute).toHaveBeenCalledOnce();
|
|
112
131
|
expect(vi.mocked(mockLLM.chat!)).toHaveBeenCalledTimes(3);
|
|
@@ -122,7 +141,11 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
122
141
|
.mockResolvedValueOnce(
|
|
123
142
|
chatResponse('', [{ id: 'call_1', name: 'broken', arguments: { input: 'test' } }])
|
|
124
143
|
)
|
|
125
|
-
.mockResolvedValueOnce(
|
|
144
|
+
.mockResolvedValueOnce(
|
|
145
|
+
chatResponse(
|
|
146
|
+
'The tool failed but I can still help you with your request. Let me try a different approach.'
|
|
147
|
+
)
|
|
148
|
+
),
|
|
126
149
|
};
|
|
127
150
|
|
|
128
151
|
const agent = createMockAgent([failTool]);
|
|
@@ -139,7 +162,9 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
139
162
|
|
|
140
163
|
const result = await selfMod.run('Use the broken tool');
|
|
141
164
|
|
|
142
|
-
expect(result.output).toBe(
|
|
165
|
+
expect(result.output).toBe(
|
|
166
|
+
'The tool failed but I can still help you with your request. Let me try a different approach.'
|
|
167
|
+
);
|
|
143
168
|
const secondCall = vi.mocked(mockLLM.chat!).mock.calls[1][0];
|
|
144
169
|
const toolMsg = secondCall.messages.find((m: { role: string }) => m.role === 'tool');
|
|
145
170
|
expect(toolMsg?.content).toContain('tool crashed');
|
|
@@ -152,7 +177,11 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
152
177
|
.mockResolvedValueOnce(
|
|
153
178
|
chatResponse('', [{ id: 'call_1', name: 'nonexistent', arguments: { input: 'x' } }])
|
|
154
179
|
)
|
|
155
|
-
.mockResolvedValueOnce(
|
|
180
|
+
.mockResolvedValueOnce(
|
|
181
|
+
chatResponse(
|
|
182
|
+
'I could not find that tool in my available tools. Let me try to help you without it.'
|
|
183
|
+
)
|
|
184
|
+
),
|
|
156
185
|
};
|
|
157
186
|
|
|
158
187
|
const agent = createMockAgent([]);
|
|
@@ -169,7 +198,9 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
169
198
|
|
|
170
199
|
const result = await selfMod.run('Call a tool');
|
|
171
200
|
|
|
172
|
-
expect(result.output).toBe(
|
|
201
|
+
expect(result.output).toBe(
|
|
202
|
+
'I could not find that tool in my available tools. Let me try to help you without it.'
|
|
203
|
+
);
|
|
173
204
|
const secondCall = vi.mocked(mockLLM.chat!).mock.calls[1][0];
|
|
174
205
|
const toolMsg = secondCall.messages.find((m: { role: string }) => m.role === 'tool');
|
|
175
206
|
expect(toolMsg?.content).toContain('not found');
|
|
@@ -177,7 +208,13 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
177
208
|
|
|
178
209
|
it('works without any tools (no tools passed to LLM)', async () => {
|
|
179
210
|
const mockLLM: Partial<LLMBackend> = {
|
|
180
|
-
chat: vi
|
|
211
|
+
chat: vi
|
|
212
|
+
.fn()
|
|
213
|
+
.mockResolvedValueOnce(
|
|
214
|
+
chatResponse(
|
|
215
|
+
'Hello world! I am a test assistant and I am here to help you with your questions.'
|
|
216
|
+
)
|
|
217
|
+
),
|
|
181
218
|
};
|
|
182
219
|
|
|
183
220
|
const agent = createMockAgent([]);
|
|
@@ -194,8 +231,365 @@ describe('SelfModifyingAgent tool execution', () => {
|
|
|
194
231
|
|
|
195
232
|
const result = await selfMod.run('Just say hi');
|
|
196
233
|
|
|
197
|
-
expect(result.output).toBe(
|
|
234
|
+
expect(result.output).toBe(
|
|
235
|
+
'Hello world! I am a test assistant and I am here to help you with your questions.'
|
|
236
|
+
);
|
|
198
237
|
const chatCall = vi.mocked(mockLLM.chat!).mock.calls[0][0];
|
|
199
238
|
expect(chatCall.tools).toBeUndefined();
|
|
200
239
|
});
|
|
201
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
|
+
});
|