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.
Files changed (38) hide show
  1. package/README.md +17 -2
  2. package/dist/cli/v4/aidenCLI.js +185 -99
  3. package/dist/cli/v4/chatSession.js +107 -0
  4. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
  5. package/dist/cli/v4/commands/fanout.js +42 -59
  6. package/dist/cli/v4/commands/help.js +6 -0
  7. package/dist/cli/v4/commands/index.js +16 -1
  8. package/dist/cli/v4/commands/mcp.js +80 -54
  9. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  10. package/dist/cli/v4/commands/recovery.js +122 -0
  11. package/dist/cli/v4/commands/runs.js +22 -2
  12. package/dist/cli/v4/commands/spawnPause.js +93 -0
  13. package/dist/cli/v4/daemonAgentBuilder.js +4 -1
  14. package/dist/cli/v4/defaultSoul.js +1 -1
  15. package/dist/core/v4/aidenAgent.js +219 -1
  16. package/dist/core/v4/daemon/bootstrap.js +47 -0
  17. package/dist/core/v4/daemon/db/migrations.js +66 -0
  18. package/dist/core/v4/daemon/runStore.js +33 -3
  19. package/dist/core/v4/providerFallback.js +35 -2
  20. package/dist/core/v4/runtimeToggles.js +30 -3
  21. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  22. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  23. package/dist/core/v4/subagent/childBuilder.js +391 -0
  24. package/dist/core/v4/subagent/fanout.js +75 -51
  25. package/dist/core/v4/subagent/spawnPause.js +191 -0
  26. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  27. package/dist/core/v4/toolRegistry.js +19 -3
  28. package/dist/core/version.js +1 -1
  29. package/dist/moat/plannerGuard.js +29 -0
  30. package/dist/providers/v4/anthropicAdapter.js +31 -3
  31. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  32. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  33. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  34. package/dist/tools/v4/index.js +17 -3
  35. package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
  36. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  37. package/dist/tools/v4/subagent/subagentFanout.js +53 -1
  38. 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) {
@@ -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
- riskTier: 'safe', // v4.4 Phase 1
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
+ }