aiden-runtime 4.5.0 → 4.6.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 +17 -2
- package/dist/cli/v4/aidenCLI.js +185 -99
- package/dist/cli/v4/chatSession.js +107 -0
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +6 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +22 -2
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/daemonAgentBuilder.js +4 -1
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/core/v4/aidenAgent.js +219 -1
- package/dist/core/v4/daemon/bootstrap.js +47 -0
- package/dist/core/v4/daemon/db/migrations.js +66 -0
- package/dist/core/v4/daemon/runStore.js +33 -3
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/runtimeToggles.js +30 -3
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/toolRegistry.js +19 -3
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/index.js +17 -3
- package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +53 -1
- package/package.json +7 -3
|
@@ -73,7 +73,7 @@ class ChatCompletionsAdapter {
|
|
|
73
73
|
// ── Non-streaming ────────────────────────────────────────────────────
|
|
74
74
|
async call(input) {
|
|
75
75
|
const body = this.buildBody(input, /* streaming */ false);
|
|
76
|
-
const reply = await this.dispatch(body, /* streaming */ false);
|
|
76
|
+
const reply = await this.dispatch(body, /* streaming */ false, input.signal);
|
|
77
77
|
const text = await reply.text();
|
|
78
78
|
let parsed;
|
|
79
79
|
try {
|
|
@@ -91,7 +91,7 @@ class ChatCompletionsAdapter {
|
|
|
91
91
|
// ── Streaming ────────────────────────────────────────────────────────
|
|
92
92
|
async *callStream(input) {
|
|
93
93
|
const body = this.buildBody(input, /* streaming */ true);
|
|
94
|
-
const reply = await this.dispatch(body, /* streaming */ true);
|
|
94
|
+
const reply = await this.dispatch(body, /* streaming */ true, input.signal);
|
|
95
95
|
if (!reply.body) {
|
|
96
96
|
yield {
|
|
97
97
|
type: 'done',
|
|
@@ -150,7 +150,7 @@ class ChatCompletionsAdapter {
|
|
|
150
150
|
headers['Accept'] = 'text/event-stream';
|
|
151
151
|
return { ...headers, ...this.extraHeaders };
|
|
152
152
|
}
|
|
153
|
-
async dispatch(body, streaming) {
|
|
153
|
+
async dispatch(body, streaming, externalSignal) {
|
|
154
154
|
const headers = this.buildHeaders(streaming);
|
|
155
155
|
const serialised = JSON.stringify(body);
|
|
156
156
|
const totalTries = this.maxRetries + 1;
|
|
@@ -158,6 +158,19 @@ class ChatCompletionsAdapter {
|
|
|
158
158
|
for (let attempt = 0; attempt < totalTries; attempt++) {
|
|
159
159
|
const controller = new AbortController();
|
|
160
160
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
161
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
162
|
+
// External aborts surface as raw AbortError so AidenAgent routes
|
|
163
|
+
// them as 'interrupted' rather than retrying as ProviderTimeoutError.
|
|
164
|
+
let externalAbortHandler = null;
|
|
165
|
+
if (externalSignal) {
|
|
166
|
+
if (externalSignal.aborted) {
|
|
167
|
+
controller.abort();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
externalAbortHandler = () => controller.abort();
|
|
171
|
+
externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
161
174
|
let response;
|
|
162
175
|
try {
|
|
163
176
|
response = await fetch(this.endpoint, {
|
|
@@ -169,7 +182,14 @@ class ChatCompletionsAdapter {
|
|
|
169
182
|
}
|
|
170
183
|
catch (err) {
|
|
171
184
|
clearTimeout(timer);
|
|
185
|
+
if (externalAbortHandler && externalSignal) {
|
|
186
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
187
|
+
}
|
|
172
188
|
if (err?.name === 'AbortError') {
|
|
189
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
190
|
+
if (externalSignal?.aborted) {
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
173
193
|
lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
174
194
|
}
|
|
175
195
|
else {
|
|
@@ -182,6 +202,9 @@ class ChatCompletionsAdapter {
|
|
|
182
202
|
throw lastErr;
|
|
183
203
|
}
|
|
184
204
|
clearTimeout(timer);
|
|
205
|
+
if (externalAbortHandler && externalSignal) {
|
|
206
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
207
|
+
}
|
|
185
208
|
if (response.ok)
|
|
186
209
|
return response;
|
|
187
210
|
const status = response.status;
|
|
@@ -104,7 +104,7 @@ class CodexResponsesAdapter {
|
|
|
104
104
|
// ── Public: non-streaming entry ─────────────────────────────────────
|
|
105
105
|
async call(input) {
|
|
106
106
|
const body = this.buildBody(input);
|
|
107
|
-
const reply = await this.dispatch(body);
|
|
107
|
+
const reply = await this.dispatch(body, input.signal);
|
|
108
108
|
// Codex backend always streams; aggregate the SSE frames into the
|
|
109
109
|
// same shape the JSON path returns. Plain api.openai.com path returns
|
|
110
110
|
// JSON directly.
|
|
@@ -175,7 +175,7 @@ class CodexResponsesAdapter {
|
|
|
175
175
|
}
|
|
176
176
|
return { ...headers, ...this.extraHeaders };
|
|
177
177
|
}
|
|
178
|
-
async dispatch(body) {
|
|
178
|
+
async dispatch(body, externalSignal) {
|
|
179
179
|
const headers = this.buildHeaders();
|
|
180
180
|
const serialised = JSON.stringify(body);
|
|
181
181
|
const totalTries = this.maxRetries + 1;
|
|
@@ -183,6 +183,19 @@ class CodexResponsesAdapter {
|
|
|
183
183
|
for (let attempt = 0; attempt < totalTries; attempt++) {
|
|
184
184
|
const controller = new AbortController();
|
|
185
185
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
186
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
187
|
+
// External aborts surface as raw AbortError so AidenAgent routes
|
|
188
|
+
// them as 'interrupted' rather than retrying as ProviderTimeoutError.
|
|
189
|
+
let externalAbortHandler = null;
|
|
190
|
+
if (externalSignal) {
|
|
191
|
+
if (externalSignal.aborted) {
|
|
192
|
+
controller.abort();
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
externalAbortHandler = () => controller.abort();
|
|
196
|
+
externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
186
199
|
let response;
|
|
187
200
|
try {
|
|
188
201
|
response = await fetch(this.endpoint, {
|
|
@@ -194,7 +207,14 @@ class CodexResponsesAdapter {
|
|
|
194
207
|
}
|
|
195
208
|
catch (err) {
|
|
196
209
|
clearTimeout(timer);
|
|
210
|
+
if (externalAbortHandler && externalSignal) {
|
|
211
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
212
|
+
}
|
|
197
213
|
if (err?.name === 'AbortError') {
|
|
214
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
215
|
+
if (externalSignal?.aborted) {
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
198
218
|
lastErr = new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
199
219
|
}
|
|
200
220
|
else {
|
|
@@ -207,6 +227,9 @@ class CodexResponsesAdapter {
|
|
|
207
227
|
throw lastErr;
|
|
208
228
|
}
|
|
209
229
|
clearTimeout(timer);
|
|
230
|
+
if (externalAbortHandler && externalSignal) {
|
|
231
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
232
|
+
}
|
|
210
233
|
if (response.ok)
|
|
211
234
|
return response;
|
|
212
235
|
const status = response.status;
|
|
@@ -63,7 +63,7 @@ class OllamaPromptToolsAdapter {
|
|
|
63
63
|
let lastError = null;
|
|
64
64
|
for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {
|
|
65
65
|
try {
|
|
66
|
-
const response = await this.fetchWithTimeout(url, headers, body);
|
|
66
|
+
const response = await this.fetchWithTimeout(url, headers, body, input.signal);
|
|
67
67
|
if (response.ok) {
|
|
68
68
|
const json = (await response.json());
|
|
69
69
|
return this.parseResponse(json);
|
|
@@ -134,6 +134,17 @@ class OllamaPromptToolsAdapter {
|
|
|
134
134
|
const headers = { 'Content-Type': 'application/json' };
|
|
135
135
|
const controller = new AbortController();
|
|
136
136
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
137
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
138
|
+
let externalAbortHandler = null;
|
|
139
|
+
if (input.signal) {
|
|
140
|
+
if (input.signal.aborted) {
|
|
141
|
+
controller.abort();
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
externalAbortHandler = () => controller.abort();
|
|
145
|
+
input.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
137
148
|
let response;
|
|
138
149
|
try {
|
|
139
150
|
response = await fetch(url, {
|
|
@@ -145,13 +156,23 @@ class OllamaPromptToolsAdapter {
|
|
|
145
156
|
}
|
|
146
157
|
catch (err) {
|
|
147
158
|
clearTimeout(timer);
|
|
159
|
+
if (externalAbortHandler && input.signal) {
|
|
160
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
161
|
+
}
|
|
148
162
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
163
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
164
|
+
if (input.signal?.aborted) {
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
149
167
|
throw new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
150
168
|
}
|
|
151
169
|
throw new errors_1.ProviderError(`Ollama not reachable at ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`, this.providerName, undefined, err, true);
|
|
152
170
|
}
|
|
153
171
|
if (!response.ok) {
|
|
154
172
|
clearTimeout(timer);
|
|
173
|
+
if (externalAbortHandler && input.signal) {
|
|
174
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
175
|
+
}
|
|
155
176
|
const status = response.status;
|
|
156
177
|
const rawText = await this.safeReadText(response);
|
|
157
178
|
// Phase v4.1.1-oauth-fix Phase 5: composeMessage handles body
|
|
@@ -160,8 +181,15 @@ class OllamaPromptToolsAdapter {
|
|
|
160
181
|
}
|
|
161
182
|
if (!response.body) {
|
|
162
183
|
clearTimeout(timer);
|
|
184
|
+
if (externalAbortHandler && input.signal) {
|
|
185
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
186
|
+
}
|
|
163
187
|
throw new errors_1.ProviderError(`Provider ${this.providerName} returned an empty stream body`, this.providerName);
|
|
164
188
|
}
|
|
189
|
+
// Response is good; the stream consumer will run for a while. The
|
|
190
|
+
// controller stays armed (with `externalSignal` still listening) so
|
|
191
|
+
// that mid-stream aborts cancel reader.read() via fetch's signal.
|
|
192
|
+
// Listener cleanup happens in the stream-consumer try/finally below.
|
|
165
193
|
const reader = response.body.getReader();
|
|
166
194
|
const decoder = new TextDecoder('utf-8');
|
|
167
195
|
let lineBuffer = '';
|
|
@@ -223,10 +251,18 @@ class OllamaPromptToolsAdapter {
|
|
|
223
251
|
}
|
|
224
252
|
catch (err) {
|
|
225
253
|
clearTimeout(timer);
|
|
254
|
+
// v4.6 prep — external abort during mid-stream read surfaces as
|
|
255
|
+
// AbortError; re-throw so AidenAgent routes it as 'interrupted'.
|
|
256
|
+
if (err instanceof Error && err.name === 'AbortError' && input.signal?.aborted) {
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
226
259
|
throw new errors_1.ProviderError(`Provider ${this.providerName} stream interrupted: ${err instanceof Error ? err.message : String(err)}`, this.providerName, undefined, err, true);
|
|
227
260
|
}
|
|
228
261
|
finally {
|
|
229
262
|
clearTimeout(timer);
|
|
263
|
+
if (externalAbortHandler && input.signal) {
|
|
264
|
+
input.signal.removeEventListener('abort', externalAbortHandler);
|
|
265
|
+
}
|
|
230
266
|
try {
|
|
231
267
|
reader.releaseLock();
|
|
232
268
|
}
|
|
@@ -422,9 +458,20 @@ class OllamaPromptToolsAdapter {
|
|
|
422
458
|
const textBefore = firstTagIdx >= 0 ? text.slice(0, firstTagIdx).trim() : text;
|
|
423
459
|
return { textBefore, toolCalls };
|
|
424
460
|
}
|
|
425
|
-
async fetchWithTimeout(url, headers, body) {
|
|
461
|
+
async fetchWithTimeout(url, headers, body, externalSignal) {
|
|
426
462
|
const controller = new AbortController();
|
|
427
463
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
464
|
+
// v4.6 prep — forward external abort into the internal controller.
|
|
465
|
+
let externalAbortHandler = null;
|
|
466
|
+
if (externalSignal) {
|
|
467
|
+
if (externalSignal.aborted) {
|
|
468
|
+
controller.abort();
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
externalAbortHandler = () => controller.abort();
|
|
472
|
+
externalSignal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
428
475
|
try {
|
|
429
476
|
return await fetch(url, {
|
|
430
477
|
method: 'POST',
|
|
@@ -435,12 +482,20 @@ class OllamaPromptToolsAdapter {
|
|
|
435
482
|
}
|
|
436
483
|
catch (err) {
|
|
437
484
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
485
|
+
// v4.6 prep — external abort takes priority over internal timeout.
|
|
486
|
+
// Surface the raw AbortError so AidenAgent routes it as 'interrupted'.
|
|
487
|
+
if (externalSignal?.aborted) {
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
438
490
|
throw new errors_1.ProviderTimeoutError(this.providerName, this.timeoutMs);
|
|
439
491
|
}
|
|
440
492
|
throw err;
|
|
441
493
|
}
|
|
442
494
|
finally {
|
|
443
495
|
clearTimeout(timer);
|
|
496
|
+
if (externalAbortHandler && externalSignal) {
|
|
497
|
+
externalSignal.removeEventListener('abort', externalAbortHandler);
|
|
498
|
+
}
|
|
444
499
|
}
|
|
445
500
|
}
|
|
446
501
|
async safeReadText(response) {
|
package/dist/tools/v4/index.js
CHANGED
|
@@ -90,6 +90,9 @@ const memoryReplace_1 = require("./memory/memoryReplace");
|
|
|
90
90
|
const memoryRemove_1 = require("./memory/memoryRemove");
|
|
91
91
|
const sessionSummary_1 = require("./memory/sessionSummary");
|
|
92
92
|
const subagentFanout_1 = require("./subagent/subagentFanout");
|
|
93
|
+
// v4.6 Phase 1 — spawn_sub_agent stub registered alongside the
|
|
94
|
+
// fanout stub so the schema is visible at agent construction.
|
|
95
|
+
const spawnSubAgentTool_1 = require("./subagent/spawnSubAgentTool");
|
|
93
96
|
/**
|
|
94
97
|
* Register every read-only tool into `registry`. The
|
|
95
98
|
* `lookup_tool_schema` tool needs a registry reference, so it's
|
|
@@ -152,11 +155,25 @@ function registerReadOnlyTools(registry) {
|
|
|
152
155
|
// Until then, calling the stub returns a clear "not wired" error
|
|
153
156
|
// rather than crashing.
|
|
154
157
|
register(makeSubagentFanoutStub());
|
|
158
|
+
// v4.6 Phase 1 — register a stub for spawn_sub_agent. Same
|
|
159
|
+
// rationale: agent construction at `cli/v4/aidenCLI.ts` snapshots
|
|
160
|
+
// the tool array, so the schema must be in the registry by then.
|
|
161
|
+
// The REPL wiring at `buildAgentRuntime` calls
|
|
162
|
+
// `register(makeSpawnSubAgentTool({...real deps}))` to replace
|
|
163
|
+
// this stub once `parentAgent`, `runStore`, etc. are available.
|
|
164
|
+
// The stub carries `contexts: ['repl']` so it's excluded from the
|
|
165
|
+
// daemon agent's tool catalog via `getSchemas(_, 'daemon')`.
|
|
166
|
+
register((0, spawnSubAgentTool_1.makeSpawnSubAgentStub)());
|
|
155
167
|
}
|
|
156
168
|
/** Stub used until the runtime wires real provider / adapter / agent
|
|
157
169
|
* dependencies. Returns the SAME schema as the real tool so MCP and
|
|
158
170
|
* /tools see a consistent surface. */
|
|
159
171
|
function makeSubagentFanoutStub() {
|
|
172
|
+
// v4.6 Phase 2R — `runChild` removed from `SubagentFanoutFactoryOptions`.
|
|
173
|
+
// The stub returns a "no providers configured" error envelope on every
|
|
174
|
+
// call via `resolveProviders: () => []`. Production wires real
|
|
175
|
+
// `spawnDeps` post-runtime build (`cli/v4/aidenCLI.ts` for REPL,
|
|
176
|
+
// `cli/v4/commands/mcp.ts` for MCP serve).
|
|
160
177
|
return (0, subagentFanout_1.makeSubagentFanoutTool)({
|
|
161
178
|
resolveProviders: () => [],
|
|
162
179
|
resolveActiveModel: () => ({ providerId: 'unset', modelId: 'unset' }),
|
|
@@ -167,9 +184,6 @@ function makeSubagentFanoutStub() {
|
|
|
167
184
|
'Call register(makeSubagentFanoutTool({...})) after buildAgentRuntime.');
|
|
168
185
|
},
|
|
169
186
|
},
|
|
170
|
-
runChild: async () => {
|
|
171
|
-
throw new Error('subagent_fanout: tool not wired — runtime did not replace the stub.');
|
|
172
|
-
},
|
|
173
187
|
});
|
|
174
188
|
}
|
|
175
189
|
/**
|
|
@@ -64,7 +64,12 @@ function makeLookupToolSchema(registry) {
|
|
|
64
64
|
category: handler.category,
|
|
65
65
|
mutates: handler.mutates,
|
|
66
66
|
toolset: handler.toolset,
|
|
67
|
-
|
|
67
|
+
// v4.6 Phase 1 — read the queried tool's actual risk tier
|
|
68
|
+
// (was previously hardcoded to 'safe' regardless of the tool,
|
|
69
|
+
// which mis-reported caution/dangerous tools as safe in the
|
|
70
|
+
// /tools surface). Falls back to 'safe' for tools that never
|
|
71
|
+
// annotated their tier — matches the registry-level default.
|
|
72
|
+
riskTier: handler.riskTier ?? 'safe',
|
|
68
73
|
};
|
|
69
74
|
},
|
|
70
75
|
};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* tools/v4/subagent/spawnSubAgentTool.ts — v4.6 Phase 1.
|
|
10
|
+
*
|
|
11
|
+
* LLM-callable wrapper for the `spawn_sub_agent` primitive. JSON
|
|
12
|
+
* schema and description text are verbatim from
|
|
13
|
+
* `docs/v4.6/phase-1-design.md` §4. The handler:
|
|
14
|
+
*
|
|
15
|
+
* 1. Reads the parent agent's current AbortSignal via the
|
|
16
|
+
* Flag 1 pattern (`parentAgent.getCurrentSignal()` captured
|
|
17
|
+
* reference, not a widened executor signature).
|
|
18
|
+
* 2. Validates and clamps arguments.
|
|
19
|
+
* 3. Calls `spawnSubAgent` from `core/v4/subagent/spawnSubAgent.ts`.
|
|
20
|
+
* 4. Returns the result envelope as the tool result body.
|
|
21
|
+
*
|
|
22
|
+
* Q9 — additive to the existing `subagent_fanout` tool. Both
|
|
23
|
+
* coexist in Phase 1; Phase 2 will refactor `subagent_fanout` to
|
|
24
|
+
* call this primitive N times.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.SPAWN_SUB_AGENT_SCHEMA = void 0;
|
|
28
|
+
exports.makeSpawnSubAgentStub = makeSpawnSubAgentStub;
|
|
29
|
+
exports.makeSpawnSubAgentTool = makeSpawnSubAgentTool;
|
|
30
|
+
const factory_1 = require("../../../core/v4/logger/factory");
|
|
31
|
+
const spawnSubAgent_1 = require("../../../core/v4/subagent/spawnSubAgent");
|
|
32
|
+
// v4.6 Phase 3A — operator kill-switch. Checked at handler entry
|
|
33
|
+
// BEFORE any work (no run row written, no child built, no provider
|
|
34
|
+
// call). In-flight children continue uninterrupted; only NEW spawns
|
|
35
|
+
// are blocked. Singleton initialised by REPL/daemon/MCP boot wiring.
|
|
36
|
+
const spawnPause_1 = require("../../../core/v4/subagent/spawnPause");
|
|
37
|
+
// ── Pause helper (v4.6 Phase 3A) ──────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Safe pause read for the handler entry guard. Catches the
|
|
40
|
+
* "not initialized" error from `getSpawnPause()` and returns
|
|
41
|
+
* `{paused: false, status: {paused: false}}` — wiring-order bugs
|
|
42
|
+
* (handler firing before `initSpawnPause` ran) must NOT take down
|
|
43
|
+
* the spawn surface. Production boot wiring always inits first,
|
|
44
|
+
* so this only matters for tests that omit the init step.
|
|
45
|
+
*/
|
|
46
|
+
function safeReadPause() {
|
|
47
|
+
try {
|
|
48
|
+
const state = (0, spawnPause_1.getSpawnPause)();
|
|
49
|
+
const status = state.status();
|
|
50
|
+
return { paused: status.paused, status };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { paused: false, status: { paused: false } };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ── Schema description (verbatim from design doc §4) ───────────────────────
|
|
57
|
+
const SCHEMA_DESC = 'Spawn a focused child agent to handle one delegated sub-task synchronously. ' +
|
|
58
|
+
'The child runs with no access to your conversation history, an intersected ' +
|
|
59
|
+
'toolset (cannot exceed your capabilities), and a fresh system prompt built ' +
|
|
60
|
+
'from the goal + optional context. Returns a structured result envelope with ' +
|
|
61
|
+
"the child's summary, metrics, and exit reason. Use this when a sub-task " +
|
|
62
|
+
'benefits from isolated context (e.g. exploring a separate codebase area, ' +
|
|
63
|
+
'running a focused investigation, drafting an artifact without polluting your ' +
|
|
64
|
+
'main turn). Do NOT use for long-running or scheduled work — use daemon ' +
|
|
65
|
+
'triggers for that. Spawning is bounded: max 1 child at a time in Phase 1, ' +
|
|
66
|
+
'no nested spawning, max 200 iterations per child. Each spawn pays full ' +
|
|
67
|
+
'agent-startup cost (system prompt build, tool catalog ship) and roughly ' +
|
|
68
|
+
'doubles token spend for that sub-task. Prefer inline work for anything you ' +
|
|
69
|
+
'can answer in 1-3 of your own iterations. Spawn when isolation, focus, or ' +
|
|
70
|
+
'a restricted toolset actually helps.';
|
|
71
|
+
// ── Schema constant (shared by real factory + boot-time stub) ─────────────
|
|
72
|
+
/**
|
|
73
|
+
* v4.6 Phase 1 — module-level schema constant so the boot-time stub
|
|
74
|
+
* (`makeSpawnSubAgentStub`) advertises the SAME JSON-schema surface
|
|
75
|
+
* the real factory ships. Both register under name `spawn_sub_agent`
|
|
76
|
+
* with `contexts: ['repl']`, so the model sees a consistent surface
|
|
77
|
+
* regardless of whether the stub or the real handler is active.
|
|
78
|
+
*/
|
|
79
|
+
exports.SPAWN_SUB_AGENT_SCHEMA = {
|
|
80
|
+
name: 'spawn_sub_agent',
|
|
81
|
+
description: SCHEMA_DESC,
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
required: ['goal'],
|
|
85
|
+
properties: {
|
|
86
|
+
goal: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'The single concrete task for the child. Phrase as an imperative ' +
|
|
89
|
+
'outcome — what should be done, not how. The child cannot ask ' +
|
|
90
|
+
'follow-up questions; if the goal is ambiguous, refine it before ' +
|
|
91
|
+
'spawning.',
|
|
92
|
+
},
|
|
93
|
+
context: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: "Optional background the child needs but couldn't infer from the " +
|
|
96
|
+
"goal alone (file paths, prior findings, constraints). Plain text. " +
|
|
97
|
+
"The child does NOT see your conversation history; anything it needs " +
|
|
98
|
+
'must be here or discoverable via its toolset.',
|
|
99
|
+
},
|
|
100
|
+
toolsets: {
|
|
101
|
+
type: 'array',
|
|
102
|
+
description: 'OPTIONAL — when present, RESTRICTS the child to specific toolsets. ' +
|
|
103
|
+
'OMIT this field to let the child inherit your full toolset (recommended ' +
|
|
104
|
+
'for most cases — children inherit your capabilities minus the hard ' +
|
|
105
|
+
'blocklist). Each entry MUST be one of the enumerated valid names ' +
|
|
106
|
+
'below; invalid names get stripped, and if every requested name is ' +
|
|
107
|
+
'invalid the child falls back to inheriting your full toolset (with a ' +
|
|
108
|
+
'warning logged). The child can never exceed your capabilities — this ' +
|
|
109
|
+
'parameter only narrows them.',
|
|
110
|
+
items: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
// v4.6 Phase 1 — enum reflects the actual toolset string
|
|
113
|
+
// values registered in tools/v4/. Kept in sync with the
|
|
114
|
+
// registry; new toolsets ship by being added to a tool's
|
|
115
|
+
// `toolset` field AND to this list.
|
|
116
|
+
enum: [
|
|
117
|
+
'browser',
|
|
118
|
+
'execute',
|
|
119
|
+
'files',
|
|
120
|
+
'mcp',
|
|
121
|
+
'memory',
|
|
122
|
+
'process',
|
|
123
|
+
'sessions',
|
|
124
|
+
'skills',
|
|
125
|
+
'subagent',
|
|
126
|
+
'system',
|
|
127
|
+
'terminal',
|
|
128
|
+
'web',
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
maxIterations: {
|
|
133
|
+
type: 'integer',
|
|
134
|
+
description: 'Maximum tool-call iterations the child may run. Clamped to [1, 200]. ' +
|
|
135
|
+
'Choose tight bounds for narrow tasks (5-15) and looser for ' +
|
|
136
|
+
'exploration (50-100). Default 50.',
|
|
137
|
+
},
|
|
138
|
+
timeoutMs: {
|
|
139
|
+
type: 'integer',
|
|
140
|
+
description: 'Hard wall-clock timeout in milliseconds. Default 10 minutes. The ' +
|
|
141
|
+
"child is signalled to interrupt on timeout; if it doesn't yield " +
|
|
142
|
+
'cooperatively, the worker leaks but the parent stays responsive.',
|
|
143
|
+
},
|
|
144
|
+
provider: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: "OPTIONAL — override the child's provider. Pass a provider ID like " +
|
|
147
|
+
"'groq', 'chatgpt-plus', 'anthropic'. Omit to inherit the parent's " +
|
|
148
|
+
'provider (recommended for most callers). Mainly used by ' +
|
|
149
|
+
"`subagent_fanout`'s rotation for provider diversity. Validated " +
|
|
150
|
+
"against the parent's available pool at dispatch — an unknown name " +
|
|
151
|
+
"produces a failed envelope with `exitReason: 'provider_not_found'` " +
|
|
152
|
+
'and lists the valid names in the error message. Single-provider ' +
|
|
153
|
+
'(non-FallbackAdapter) parents reject this field with an error.',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
// ── Boot-time stub (registered before runtime deps are resolved) ──────────
|
|
159
|
+
/**
|
|
160
|
+
* v4.6 Phase 1 — stub handler used until the REPL wiring at
|
|
161
|
+
* `cli/v4/aidenCLI.ts` replaces it with the real factory. Returns
|
|
162
|
+
* the SAME schema surface so `toolRegistry.getSchemas(undefined,
|
|
163
|
+
* 'repl')` at agent construction sees `spawn_sub_agent` and the
|
|
164
|
+
* LLM can address the tool by name. If called before the real
|
|
165
|
+
* wiring lands, returns a clear "not wired" error envelope so the
|
|
166
|
+
* model gets a structured error rather than a crash.
|
|
167
|
+
*
|
|
168
|
+
* Mirrors `makeSubagentFanoutStub` in `tools/v4/index.ts`.
|
|
169
|
+
*/
|
|
170
|
+
function makeSpawnSubAgentStub() {
|
|
171
|
+
return {
|
|
172
|
+
schema: exports.SPAWN_SUB_AGENT_SCHEMA,
|
|
173
|
+
category: 'network',
|
|
174
|
+
mutates: false,
|
|
175
|
+
toolset: 'subagent',
|
|
176
|
+
riskTier: 'caution',
|
|
177
|
+
contexts: ['repl'],
|
|
178
|
+
async execute() {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
status: 'failed',
|
|
182
|
+
summary: null,
|
|
183
|
+
error: 'spawn_sub_agent: tool not wired — runtime did not replace the stub. ' +
|
|
184
|
+
'Call register(makeSpawnSubAgentTool({...})) after buildAgentRuntime.',
|
|
185
|
+
exitReason: 'error',
|
|
186
|
+
metrics: { apiCalls: 0, durationMs: 0, tokensIn: 0, tokensOut: 0 },
|
|
187
|
+
childRunId: '0',
|
|
188
|
+
childSessionId: '',
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// ── Implementation ────────────────────────────────────────────────────────
|
|
194
|
+
function makeSpawnSubAgentTool(factory) {
|
|
195
|
+
return {
|
|
196
|
+
schema: exports.SPAWN_SUB_AGENT_SCHEMA,
|
|
197
|
+
// The tool itself spends tokens. Disk / process side effects, if
|
|
198
|
+
// any, happen INSIDE the child agent whose toolset is intersected
|
|
199
|
+
// with the parent's and stripped of the v4.6 blocklist.
|
|
200
|
+
category: 'network',
|
|
201
|
+
mutates: false,
|
|
202
|
+
toolset: 'subagent',
|
|
203
|
+
riskTier: 'caution',
|
|
204
|
+
// v4.6 Phase 1 — REPL-only execution context per Q6.
|
|
205
|
+
// Daemon-fired agents must not initiate sub-agent spawns in
|
|
206
|
+
// Phase 1: the spawn factory captured the REPL agent reference
|
|
207
|
+
// at construction, so a daemon-fired turn invoking this tool
|
|
208
|
+
// would route its child's signal chain through the REPL agent's
|
|
209
|
+
// state rather than the daemon turn's. Tagging it `['repl']`
|
|
210
|
+
// here causes `toolRegistry.getSchemas(_, 'daemon')` (used by
|
|
211
|
+
// daemonAgentBuilder.ts:130) to exclude `spawn_sub_agent` from
|
|
212
|
+
// the daemon agent's tool catalog, so the model never sees it
|
|
213
|
+
// when running in daemon mode. Phase 3+ may add a daemon-mode
|
|
214
|
+
// spawn factory tied to the daemon agent's own reference.
|
|
215
|
+
contexts: ['repl'],
|
|
216
|
+
async execute(args, _ctx) {
|
|
217
|
+
// ── 0. Operator kill-switch (v4.6 Phase 3A) ─────────────────────────
|
|
218
|
+
// First thing — before arg validation, run-row insertion, or
|
|
219
|
+
// any child build. A paused state must short-circuit cleanly
|
|
220
|
+
// with NO side effects (no `runs` row, no log noise beyond
|
|
221
|
+
// the rejection). Locked decision: paused calls are operator-
|
|
222
|
+
// induced, NOT real failures; they don't pollute `aiden runs
|
|
223
|
+
// list`. Envelope intentionally drops the standard SubAgentResult
|
|
224
|
+
// shape because (a) no childRunId exists (no row was written)
|
|
225
|
+
// and (b) the error class is qualitatively different from a
|
|
226
|
+
// spawn that ran and failed.
|
|
227
|
+
const pauseGate = safeReadPause();
|
|
228
|
+
if (pauseGate.paused) {
|
|
229
|
+
const s = pauseGate.status;
|
|
230
|
+
const reasonSuffix = s.reason ? ` (reason: ${s.reason})` : '';
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
errorCode: 'SUBAGENT_SPAWN_PAUSED',
|
|
234
|
+
message: `spawn_sub_agent: spawning is paused${reasonSuffix}. ` +
|
|
235
|
+
'Run /spawn-pause off to resume.',
|
|
236
|
+
pausedAt: s.pausedAt ?? null,
|
|
237
|
+
reason: s.reason ?? null,
|
|
238
|
+
pausedBy: s.pausedBy ?? null,
|
|
239
|
+
durationMs: s.durationMs ?? null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// ── 1. Validate + coerce ─────────────────────────────────────────────
|
|
243
|
+
const goal = typeof args.goal === 'string' ? args.goal.trim() : '';
|
|
244
|
+
if (!goal) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
status: 'failed',
|
|
248
|
+
summary: null,
|
|
249
|
+
error: "spawn_sub_agent: 'goal' is required and must be a non-empty string",
|
|
250
|
+
exitReason: 'error',
|
|
251
|
+
metrics: { apiCalls: 0, durationMs: 0, tokensIn: 0, tokensOut: 0 },
|
|
252
|
+
childRunId: '0',
|
|
253
|
+
childSessionId: '',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const spec = {
|
|
257
|
+
goal,
|
|
258
|
+
context: typeof args.context === 'string' ? args.context : undefined,
|
|
259
|
+
toolsets: Array.isArray(args.toolsets)
|
|
260
|
+
? args.toolsets.filter((t) => typeof t === 'string')
|
|
261
|
+
: undefined,
|
|
262
|
+
maxIterations: typeof args.maxIterations === 'number' ? args.maxIterations : undefined,
|
|
263
|
+
timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : undefined,
|
|
264
|
+
// v4.6 Phase 2P — per-spawn provider override (per design doc §12.2).
|
|
265
|
+
// Validation happens in childBuilder against the parent's FallbackAdapter
|
|
266
|
+
// provider pool; an unknown name produces a failed envelope.
|
|
267
|
+
provider: typeof args.provider === 'string' ? args.provider : undefined,
|
|
268
|
+
};
|
|
269
|
+
// v4.6 Phase 1 observability — log the parsed spec so the next
|
|
270
|
+
// smoke test can correlate "what the model asked for" with the
|
|
271
|
+
// child's actual behaviour. Goal is truncated to keep the log
|
|
272
|
+
// line readable. The parent's sessionId (read off the agent
|
|
273
|
+
// instance) is included so logs from one user turn cluster.
|
|
274
|
+
const logger = factory.logger ?? (0, factory_1.noopLogger)();
|
|
275
|
+
const goalPreview = spec.goal.length > 200 ? spec.goal.slice(0, 200) + '…' : spec.goal;
|
|
276
|
+
logger.info('spawn_sub_agent invoked', {
|
|
277
|
+
parentSessionId: factory.parentAgent.getCurrentSignal !== undefined
|
|
278
|
+
? factory.parentAgent.sessionId ?? null
|
|
279
|
+
: null,
|
|
280
|
+
goalPreview,
|
|
281
|
+
goalLen: spec.goal.length,
|
|
282
|
+
contextLen: spec.context?.length ?? 0,
|
|
283
|
+
toolsets: spec.toolsets ?? null,
|
|
284
|
+
maxIterations: spec.maxIterations ?? null,
|
|
285
|
+
timeoutMs: spec.timeoutMs ?? null,
|
|
286
|
+
});
|
|
287
|
+
// ── 2. Read the parent's current signal (Flag 1 pattern) ─────────────
|
|
288
|
+
const parentSignal = factory.parentAgent.getCurrentSignal();
|
|
289
|
+
// ── 3. Resolve optional parent run / session identifiers ─────────────
|
|
290
|
+
const parentRunId = factory.resolveParentRunId?.();
|
|
291
|
+
const parentSessionId = factory.resolveParentSessionId?.();
|
|
292
|
+
// ── 4. Invoke the primitive. NEVER throws — always envelope. ─────────
|
|
293
|
+
const result = await (0, spawnSubAgent_1.spawnSubAgent)(spec, {
|
|
294
|
+
// ChildBuilderDeps fields:
|
|
295
|
+
toolRegistry: factory.toolRegistry,
|
|
296
|
+
parentToolContext: factory.parentToolContext,
|
|
297
|
+
parentProvider: factory.parentProvider,
|
|
298
|
+
parentProviderId: factory.parentProviderId,
|
|
299
|
+
parentModelId: factory.parentModelId,
|
|
300
|
+
resolveVerifiedFlag: factory.resolveVerifiedFlag,
|
|
301
|
+
resolveToolset: factory.resolveToolset,
|
|
302
|
+
resolveMutates: factory.resolveMutates,
|
|
303
|
+
// Persistence:
|
|
304
|
+
runStore: factory.runStore,
|
|
305
|
+
instanceId: factory.instanceId,
|
|
306
|
+
// v4.6 Phase 1 observability:
|
|
307
|
+
logger,
|
|
308
|
+
}, {
|
|
309
|
+
signal: parentSignal,
|
|
310
|
+
parentRunId,
|
|
311
|
+
parentSessionId,
|
|
312
|
+
});
|
|
313
|
+
// Completion log — pairs with "spawn_sub_agent invoked" so a
|
|
314
|
+
// grep on parentSessionId surfaces invoke → complete in order.
|
|
315
|
+
logger.info('spawn_sub_agent completed', {
|
|
316
|
+
childRunId: result.childRunId,
|
|
317
|
+
childSessionId: result.childSessionId,
|
|
318
|
+
status: result.status,
|
|
319
|
+
exitReason: result.exitReason,
|
|
320
|
+
ok: result.ok,
|
|
321
|
+
apiCalls: result.metrics.apiCalls,
|
|
322
|
+
durationMs: result.metrics.durationMs,
|
|
323
|
+
tokensIn: result.metrics.tokensIn,
|
|
324
|
+
tokensOut: result.metrics.tokensOut,
|
|
325
|
+
summaryLen: result.summary?.length ?? 0,
|
|
326
|
+
errorPreview: result.error?.slice(0, 200) ?? null,
|
|
327
|
+
});
|
|
328
|
+
// The envelope IS the tool result body. The agent loop's tool-
|
|
329
|
+
// result handling will JSON-stringify this and feed it back to
|
|
330
|
+
// the parent's LLM as the tool message content.
|
|
331
|
+
return result;
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|