@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.
Files changed (114) hide show
  1. package/README.md +112 -48
  2. package/dist/architecture-evolution/capability-analyzer.d.ts.map +1 -1
  3. package/dist/architecture-evolution/capability-analyzer.js +44 -40
  4. package/dist/architecture-evolution/capability-analyzer.js.map +1 -1
  5. package/dist/architecture-evolution/evolution-strategy.d.ts.map +1 -1
  6. package/dist/architecture-evolution/evolution-strategy.js +4 -2
  7. package/dist/architecture-evolution/evolution-strategy.js.map +1 -1
  8. package/dist/architecture-evolution/index.d.ts +2 -2
  9. package/dist/architecture-evolution/index.d.ts.map +1 -1
  10. package/dist/architecture-evolution/index.js +1 -1
  11. package/dist/architecture-evolution/index.js.map +1 -1
  12. package/dist/architecture-evolution/parameter-optimizer.d.ts +5 -4
  13. package/dist/architecture-evolution/parameter-optimizer.d.ts.map +1 -1
  14. package/dist/architecture-evolution/parameter-optimizer.js +62 -34
  15. package/dist/architecture-evolution/parameter-optimizer.js.map +1 -1
  16. package/dist/architecture-evolution/prompts.d.ts +21 -2
  17. package/dist/architecture-evolution/prompts.d.ts.map +1 -1
  18. package/dist/architecture-evolution/prompts.js +189 -37
  19. package/dist/architecture-evolution/prompts.js.map +1 -1
  20. package/dist/constraints/modification-validator.d.ts +1 -0
  21. package/dist/constraints/modification-validator.d.ts.map +1 -1
  22. package/dist/constraints/modification-validator.js +45 -18
  23. package/dist/constraints/modification-validator.js.map +1 -1
  24. package/dist/constraints/rollback-manager.d.ts +1 -0
  25. package/dist/constraints/rollback-manager.d.ts.map +1 -1
  26. package/dist/constraints/rollback-manager.js +25 -1
  27. package/dist/constraints/rollback-manager.js.map +1 -1
  28. package/dist/events/event-emitter.d.ts +0 -1
  29. package/dist/events/event-emitter.d.ts.map +1 -1
  30. package/dist/events/event-emitter.js +6 -10
  31. package/dist/events/event-emitter.js.map +1 -1
  32. package/dist/index.d.ts +4 -4
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +3 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/meta-reasoning/meta-reasoner.d.ts +4 -1
  37. package/dist/meta-reasoning/meta-reasoner.d.ts.map +1 -1
  38. package/dist/meta-reasoning/meta-reasoner.js +120 -43
  39. package/dist/meta-reasoning/meta-reasoner.js.map +1 -1
  40. package/dist/meta-reasoning/observation-collector.d.ts.map +1 -1
  41. package/dist/meta-reasoning/observation-collector.js +6 -2
  42. package/dist/meta-reasoning/observation-collector.js.map +1 -1
  43. package/dist/meta-reasoning/prompts.d.ts +10 -14
  44. package/dist/meta-reasoning/prompts.d.ts.map +1 -1
  45. package/dist/meta-reasoning/prompts.js +108 -33
  46. package/dist/meta-reasoning/prompts.js.map +1 -1
  47. package/dist/meta-reasoning/strategy-selector.d.ts.map +1 -1
  48. package/dist/meta-reasoning/strategy-selector.js +6 -0
  49. package/dist/meta-reasoning/strategy-selector.js.map +1 -1
  50. package/dist/self-modifying-agent.d.ts +88 -15
  51. package/dist/self-modifying-agent.d.ts.map +1 -1
  52. package/dist/self-modifying-agent.js +583 -376
  53. package/dist/self-modifying-agent.js.map +1 -1
  54. package/dist/tool-generation/gap-analyzer.d.ts +0 -1
  55. package/dist/tool-generation/gap-analyzer.d.ts.map +1 -1
  56. package/dist/tool-generation/gap-analyzer.js +16 -15
  57. package/dist/tool-generation/gap-analyzer.js.map +1 -1
  58. package/dist/tool-generation/generated-tool-store.d.ts.map +1 -1
  59. package/dist/tool-generation/generated-tool-store.js +90 -10
  60. package/dist/tool-generation/generated-tool-store.js.map +1 -1
  61. package/dist/tool-generation/index.d.ts +3 -3
  62. package/dist/tool-generation/index.d.ts.map +1 -1
  63. package/dist/tool-generation/index.js +2 -2
  64. package/dist/tool-generation/index.js.map +1 -1
  65. package/dist/tool-generation/prompts.d.ts +4 -3
  66. package/dist/tool-generation/prompts.d.ts.map +1 -1
  67. package/dist/tool-generation/prompts.js +96 -18
  68. package/dist/tool-generation/prompts.js.map +1 -1
  69. package/dist/tool-generation/tool-generator.d.ts +6 -6
  70. package/dist/tool-generation/tool-generator.d.ts.map +1 -1
  71. package/dist/tool-generation/tool-generator.js +88 -23
  72. package/dist/tool-generation/tool-generator.js.map +1 -1
  73. package/dist/tool-generation/tool-sandbox.d.ts +10 -11
  74. package/dist/tool-generation/tool-sandbox.d.ts.map +1 -1
  75. package/dist/tool-generation/tool-sandbox.js +346 -156
  76. package/dist/tool-generation/tool-sandbox.js.map +1 -1
  77. package/dist/tool-generation/tool-validator.d.ts +4 -8
  78. package/dist/tool-generation/tool-validator.d.ts.map +1 -1
  79. package/dist/tool-generation/tool-validator.js +74 -28
  80. package/dist/tool-generation/tool-validator.js.map +1 -1
  81. package/dist/utils/index.d.ts.map +1 -1
  82. package/dist/utils/index.js +30 -5
  83. package/dist/utils/index.js.map +1 -1
  84. package/dist/utils/llm-helper.d.ts.map +1 -1
  85. package/dist/utils/llm-helper.js +1 -0
  86. package/dist/utils/llm-helper.js.map +1 -1
  87. package/package.json +3 -12
  88. package/src/__tests__/component-regressions.test.ts +599 -0
  89. package/src/__tests__/meta-reasoning.test.ts +5 -1
  90. package/src/__tests__/sandbox-security.test.ts +186 -0
  91. package/src/__tests__/self-modifying-agent.test.ts +405 -11
  92. package/src/architecture-evolution/capability-analyzer.ts +63 -44
  93. package/src/architecture-evolution/evolution-strategy.ts +5 -2
  94. package/src/architecture-evolution/index.ts +2 -0
  95. package/src/architecture-evolution/parameter-optimizer.ts +82 -40
  96. package/src/architecture-evolution/prompts.ts +224 -42
  97. package/src/constraints/modification-validator.ts +48 -21
  98. package/src/constraints/rollback-manager.ts +20 -1
  99. package/src/events/event-emitter.ts +6 -15
  100. package/src/index.ts +14 -0
  101. package/src/meta-reasoning/meta-reasoner.ts +124 -52
  102. package/src/meta-reasoning/observation-collector.ts +6 -2
  103. package/src/meta-reasoning/prompts.ts +131 -56
  104. package/src/meta-reasoning/strategy-selector.ts +6 -0
  105. package/src/self-modifying-agent.ts +797 -413
  106. package/src/tool-generation/gap-analyzer.ts +21 -19
  107. package/src/tool-generation/generated-tool-store.ts +97 -10
  108. package/src/tool-generation/index.ts +13 -3
  109. package/src/tool-generation/prompts.ts +120 -18
  110. package/src/tool-generation/tool-generator.ts +120 -26
  111. package/src/tool-generation/tool-sandbox.ts +408 -206
  112. package/src/tool-generation/tool-validator.ts +90 -37
  113. package/src/utils/index.ts +29 -5
  114. 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 { Agent, LLMBackend, Tool, ChatResponse, ToolCall } from '@cogitator-ai/types';
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(chatResponse('The answer is 42.')),
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('The answer is 42.');
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(chatResponse('Here is your formatted result.')),
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('Here is your formatted result.');
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(chatResponse('The tool failed but I can help anyway.')),
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('The tool failed but I can help anyway.');
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(chatResponse('I could not find that tool.')),
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('I could not find that tool.');
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.fn().mockResolvedValueOnce(chatResponse('Hello world')),
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('Hello world');
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
+ });