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
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
* core/v4/subagent/childBuilder.ts — v4.6 Phase 1.
|
|
10
|
+
*
|
|
11
|
+
* Constructs the child `AidenAgent` for one `spawn_sub_agent` call.
|
|
12
|
+
* Mirrors the closure-capture shape of `cli/v4/daemonAgentBuilder.ts`
|
|
13
|
+
* — shared deps captured at REPL bootstrap, fresh per-spawn state
|
|
14
|
+
* built inline.
|
|
15
|
+
*
|
|
16
|
+
* Per the design doc §5 state-isolation matrix:
|
|
17
|
+
* - Conversation history, system prompt, TCE, file-op cache:
|
|
18
|
+
* ISOLATED (child gets fresh).
|
|
19
|
+
* - Toolset: intersection of (parent's enabled toolsets) ∩
|
|
20
|
+
* (spec.toolsets or parent's full set) MINUS the hard blocklist.
|
|
21
|
+
* - Provider + model + credentials: INHERITED (same adapter).
|
|
22
|
+
* - FallbackAdapter rate-limit state: CLONED per child (when the
|
|
23
|
+
* adapter exposes `clone()`) so a child's 429 doesn't poison
|
|
24
|
+
* the parent's quota tracking.
|
|
25
|
+
* - ApprovalEngine: fresh instance with auto-deny callbacks
|
|
26
|
+
* (child cannot prompt the user).
|
|
27
|
+
* - plannerGuard / honestyEnforcement / skillTeacher / skillMiner:
|
|
28
|
+
* OMITTED (focused worker config, matching daemon agent shape).
|
|
29
|
+
* - Working directory / sandbox / runtimeToggles: shared via the
|
|
30
|
+
* process-level singletons read on each tool dispatch.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.ProviderNotFoundError = exports.SUBAGENT_BLOCKED_TOOL_NAMES = void 0;
|
|
34
|
+
exports.buildChildAgent = buildChildAgent;
|
|
35
|
+
const approvalEngine_1 = require("../../../moat/approvalEngine");
|
|
36
|
+
const aidenAgent_1 = require("../aidenAgent");
|
|
37
|
+
const providerFallback_1 = require("../providerFallback");
|
|
38
|
+
// ── Hard-coded blocklist (Q5 from design doc §2) ────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Tools children must NEVER receive, even if the parent's enabled toolsets
|
|
41
|
+
* cover them and the spec explicitly requests them. Filtered post-intersection.
|
|
42
|
+
*
|
|
43
|
+
* Each entry's rationale, in order:
|
|
44
|
+
* - `spawn_sub_agent` — no recursive spawning (depth cap = 1 in Phase 1)
|
|
45
|
+
* - `clarify` — child cannot prompt the user
|
|
46
|
+
* - `memory` — no writes to shared MEMORY.md / USER.md
|
|
47
|
+
* - `execute_code` — children reason step-by-step, not write scripts
|
|
48
|
+
* - `send_message` — no cross-platform side effects from a child
|
|
49
|
+
*/
|
|
50
|
+
exports.SUBAGENT_BLOCKED_TOOL_NAMES = new Set([
|
|
51
|
+
'spawn_sub_agent',
|
|
52
|
+
'clarify',
|
|
53
|
+
'memory',
|
|
54
|
+
'execute_code',
|
|
55
|
+
'send_message',
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* v4.6 Phase 2P — error thrown by `buildChildAgent` when
|
|
59
|
+
* `input.providerOverride` doesn't match any provider in the
|
|
60
|
+
* parent's pool. Caught by `spawnSubAgent` and converted to a
|
|
61
|
+
* `status: 'failed', exitReason: 'provider_not_found'` envelope
|
|
62
|
+
* with the failing name + the list of valid alternatives.
|
|
63
|
+
*/
|
|
64
|
+
class ProviderNotFoundError extends Error {
|
|
65
|
+
constructor(requested, available, hint) {
|
|
66
|
+
const base = `spawn_sub_agent: provider "${requested}" not in parent's pool.`;
|
|
67
|
+
const list = available.length > 0
|
|
68
|
+
? ` Available: ${available.join(', ')}.`
|
|
69
|
+
: ' Parent has no FallbackAdapter pool (single-provider configuration).';
|
|
70
|
+
super(`${base}${list}${hint ? ' ' + hint : ' Omit the provider field to inherit the parent\'s provider.'}`);
|
|
71
|
+
this.name = 'ProviderNotFoundError';
|
|
72
|
+
this.requested = requested;
|
|
73
|
+
this.available = available;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.ProviderNotFoundError = ProviderNotFoundError;
|
|
77
|
+
// ── Implementation ──────────────────────────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Build the child agent + initial history. Pure factory — no side
|
|
80
|
+
* effects beyond constructing in-memory objects. The caller is
|
|
81
|
+
* responsible for running `agent.runConversation(...)` and writing
|
|
82
|
+
* the `runs` row.
|
|
83
|
+
*/
|
|
84
|
+
function buildChildAgent(deps, input) {
|
|
85
|
+
// ── 1. ApprovalEngine: fresh, auto-deny callbacks ────────────────────────
|
|
86
|
+
// 'smart' mode: safe auto-allows, dangerous auto-denies, caution
|
|
87
|
+
// calls promptUser which we wire to a synchronous deny — children
|
|
88
|
+
// cannot interact with a TUI.
|
|
89
|
+
const autoDenyCallbacks = {
|
|
90
|
+
promptUser: async () => 'deny',
|
|
91
|
+
};
|
|
92
|
+
const childApprovalEngine = new approvalEngine_1.ApprovalEngine('smart', autoDenyCallbacks);
|
|
93
|
+
// ── 2. ToolContext: parent's services + child approval engine + session ──
|
|
94
|
+
const childToolContext = {
|
|
95
|
+
...deps.parentToolContext,
|
|
96
|
+
sessionId: input.sessionId,
|
|
97
|
+
approvalEngine: childApprovalEngine,
|
|
98
|
+
};
|
|
99
|
+
// ── 3. Build the child's toolExecutor from the parent's registry ─────────
|
|
100
|
+
// Same registry, different context. The registry stays read-only.
|
|
101
|
+
const childToolExecutor = deps.toolRegistry.buildExecutor(childToolContext);
|
|
102
|
+
// ── 4. Tool array: intersection + blocklist filter ───────────────────────
|
|
103
|
+
// Step 4a — pick the parent's toolsets we care about.
|
|
104
|
+
// If the spec named toolsets, intersect with the parent's known set.
|
|
105
|
+
// Otherwise the child gets the parent's full enabled set (which on
|
|
106
|
+
// REPL means every toolset the registry knows).
|
|
107
|
+
const allHandlers = deps.toolRegistry.list();
|
|
108
|
+
const parentToolsetNames = new Set();
|
|
109
|
+
for (const name of allHandlers) {
|
|
110
|
+
const handler = deps.toolRegistry.get(name);
|
|
111
|
+
if (handler?.toolset)
|
|
112
|
+
parentToolsetNames.add(handler.toolset);
|
|
113
|
+
}
|
|
114
|
+
let chosenToolsets = input.requestedToolsets && input.requestedToolsets.length > 0
|
|
115
|
+
? input.requestedToolsets.filter((t) => parentToolsetNames.has(t))
|
|
116
|
+
: [...parentToolsetNames];
|
|
117
|
+
// v4.6 Phase 1 (Dispatch 2L) — zero-tools-bug fallback. When the
|
|
118
|
+
// model passes `toolsets: [...]` with values that DON'T match any
|
|
119
|
+
// real registry toolset (e.g. `["functions"]`, a name fabricated
|
|
120
|
+
// from the OpenAI tool-use vocabulary, or `["file_operations"]`, a
|
|
121
|
+
// skill name confused for a toolset), the strict filter strips all
|
|
122
|
+
// entries → `chosenToolsets` is `[]` → child gets ZERO tools → it
|
|
123
|
+
// hallucinates an answer rather than admit it can't do the work.
|
|
124
|
+
//
|
|
125
|
+
// Recover by inheriting the full parent set when the requested
|
|
126
|
+
// names ALL miss. Logs a warning so the operator sees what the
|
|
127
|
+
// model asked for and what real names exist. Partial intersections
|
|
128
|
+
// (some valid, some invalid) keep the valid subset — that's the
|
|
129
|
+
// user's explicit narrowing intent, not a bug.
|
|
130
|
+
if (input.requestedToolsets && input.requestedToolsets.length > 0 &&
|
|
131
|
+
chosenToolsets.length === 0) {
|
|
132
|
+
deps.logger?.warn?.('spawn_sub_agent: requested toolsets stripped to empty, falling back to full parent set', {
|
|
133
|
+
requested: input.requestedToolsets,
|
|
134
|
+
validParentToolsets: [...parentToolsetNames],
|
|
135
|
+
});
|
|
136
|
+
chosenToolsets = [...parentToolsetNames];
|
|
137
|
+
}
|
|
138
|
+
// Step 4b — pull the schemas for those toolsets.
|
|
139
|
+
// v4.6 Phase 1 — pass 'repl' context: in Phase 1 spawn_sub_agent
|
|
140
|
+
// is REPL-only (Q6), so children always spawn from a REPL parent.
|
|
141
|
+
// Phase 3+ may extend the child builder to receive the parent's
|
|
142
|
+
// context dynamically; for now, 'repl' is the only path.
|
|
143
|
+
const candidateSchemas = chosenToolsets.length > 0
|
|
144
|
+
? deps.toolRegistry.getSchemas(chosenToolsets, 'repl')
|
|
145
|
+
: []; // No matching toolsets means an empty child toolset.
|
|
146
|
+
// Step 4c — strip the hard blocklist (with v4.6 Phase 2P legacy
|
|
147
|
+
// env-flag escape hatch per design doc §12.4). Default: full
|
|
148
|
+
// 5-name blocklist. When AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1 is
|
|
149
|
+
// set in the environment, `execute_code` is removed from the
|
|
150
|
+
// blocklist for THIS spawn — backward-compat preservation of any
|
|
151
|
+
// user's existing v4.1.0 .env workflow. The other 4 names
|
|
152
|
+
// (spawn_sub_agent, clarify, memory, send_message) remain
|
|
153
|
+
// blocked regardless of the flag.
|
|
154
|
+
const blocked = resolveBlocklist(deps.logger);
|
|
155
|
+
const childTools = candidateSchemas.filter((t) => !blocked.has(t.name));
|
|
156
|
+
// ── 5. Provider: clone FallbackAdapter rate-limit state if supported ─────
|
|
157
|
+
// Per Q11 (verbatim mirror of providerFallback.ts:578 clone pattern).
|
|
158
|
+
// Best-effort — if the adapter doesn't expose `clone()`, fall back to
|
|
159
|
+
// sharing the parent's adapter. Phase 1 accepts that fallback case
|
|
160
|
+
// means a child's 429 affects the parent's quota tracking; that's
|
|
161
|
+
// explicit in the design-doc §5 row.
|
|
162
|
+
//
|
|
163
|
+
// v4.6 Phase 2P — when `input.providerOverride` is supplied, resolve
|
|
164
|
+
// it against the parent's FallbackAdapter slot pool. Fail-loud on
|
|
165
|
+
// unknown names (`ProviderNotFoundError`) so fanout's diversity
|
|
166
|
+
// invariant is preserved (silent fallback would collapse the
|
|
167
|
+
// rotation). Single-provider parents (non-FallbackAdapter) reject
|
|
168
|
+
// any override, since there's no pool to select from.
|
|
169
|
+
const childProvider = resolveChildProvider(deps.parentProvider, input.providerOverride);
|
|
170
|
+
// ── 6. Observability — onToolCall → run_events + log ─────────────────────
|
|
171
|
+
// When deps.runStore + deps.childRunId are present (the production
|
|
172
|
+
// path from spawnSubAgent.ts), wire an `onToolCall` callback that
|
|
173
|
+
// mirrors the daemon dispatcher's audit shape (see
|
|
174
|
+
// `core/v4/daemon/dispatcher/realAgentRunner.ts:367-382`). Per-call
|
|
175
|
+
// start time is tracked in a Map keyed by tool_call_id so the
|
|
176
|
+
// `durationMs` on completed events is per-tool, not per-turn.
|
|
177
|
+
// Pure no-op when runStore is absent (unit tests of buildChildAgent).
|
|
178
|
+
const onToolCall = buildOnToolCall(deps);
|
|
179
|
+
// ── 7. Build the child agent ─────────────────────────────────────────────
|
|
180
|
+
// Focused worker config: omit plannerGuard, honestyEnforcement,
|
|
181
|
+
// skillTeacher, skillMiner, contextCompressor, promptCaching,
|
|
182
|
+
// promptBuilder. Match the daemon agent's "act on the task, don't
|
|
183
|
+
// self-improve" shape.
|
|
184
|
+
const agent = new aidenAgent_1.AidenAgent({
|
|
185
|
+
provider: childProvider,
|
|
186
|
+
tools: childTools,
|
|
187
|
+
toolExecutor: childToolExecutor,
|
|
188
|
+
sessionId: input.sessionId,
|
|
189
|
+
maxTurns: input.maxIterations,
|
|
190
|
+
providerId: deps.parentProviderId,
|
|
191
|
+
modelId: deps.parentModelId,
|
|
192
|
+
resolveVerifiedFlag: deps.resolveVerifiedFlag,
|
|
193
|
+
resolveToolset: deps.resolveToolset,
|
|
194
|
+
resolveMutates: deps.resolveMutates,
|
|
195
|
+
onToolCall,
|
|
196
|
+
// iterationBudgetInjection inherits the default (true) — child
|
|
197
|
+
// sees its own remaining-budget hint near the end of the run.
|
|
198
|
+
});
|
|
199
|
+
// ── 7. Initial history: fresh system prompt + the user-shaped goal ───────
|
|
200
|
+
const systemContent = buildChildSystemPrompt(input.goal, input.context);
|
|
201
|
+
const userContent = composeUserMessage(input.goal, input.context);
|
|
202
|
+
const history = [
|
|
203
|
+
{ role: 'system', content: systemContent },
|
|
204
|
+
{ role: 'user', content: userContent },
|
|
205
|
+
];
|
|
206
|
+
return { agent, history };
|
|
207
|
+
}
|
|
208
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
209
|
+
/**
|
|
210
|
+
* v4.6 Phase 2P — resolve the child's provider adapter.
|
|
211
|
+
*
|
|
212
|
+
* No override (Phase 1 behavior unchanged):
|
|
213
|
+
* - FallbackAdapter → clone with fresh mutable state, all slots.
|
|
214
|
+
* - Any other adapter shape with `clone()` → clone.
|
|
215
|
+
* - Adapter without `clone()` → reuse the parent's instance.
|
|
216
|
+
*
|
|
217
|
+
* Override supplied (Phase 2P):
|
|
218
|
+
* - Parent must be FallbackAdapter (only adapter type with a
|
|
219
|
+
* multi-provider pool). Otherwise throw `ProviderNotFoundError`.
|
|
220
|
+
* - Override must match one of `parent.getProviderIds()`. Otherwise
|
|
221
|
+
* throw `ProviderNotFoundError` listing the available pool.
|
|
222
|
+
* - On match: clone with slot-subset filter restricted to that
|
|
223
|
+
* provider's slots. Child rotates only within the chosen
|
|
224
|
+
* provider's slots; the diversity invariant fanout depends on
|
|
225
|
+
* is preserved.
|
|
226
|
+
*/
|
|
227
|
+
function resolveChildProvider(parent, override) {
|
|
228
|
+
if (!override) {
|
|
229
|
+
// ── No override path — Phase 1 clone semantics, unchanged. ────────
|
|
230
|
+
const maybeClone = parent.clone;
|
|
231
|
+
if (typeof maybeClone === 'function') {
|
|
232
|
+
try {
|
|
233
|
+
return maybeClone.call(parent);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return parent;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return parent;
|
|
240
|
+
}
|
|
241
|
+
// ── Override path — must be FallbackAdapter. ───────────────────────
|
|
242
|
+
if (!(parent instanceof providerFallback_1.FallbackAdapter)) {
|
|
243
|
+
throw new ProviderNotFoundError(override, [], 'Parent agent uses a single-provider adapter; provider override is only available when parent is configured with FallbackAdapter (multi-provider pool).');
|
|
244
|
+
}
|
|
245
|
+
const available = parent.getProviderIds();
|
|
246
|
+
if (!available.includes(override)) {
|
|
247
|
+
throw new ProviderNotFoundError(override, available);
|
|
248
|
+
}
|
|
249
|
+
return parent.clone({ providerId: override });
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* v4.6 Phase 2P — resolve the effective tool blocklist for THIS
|
|
253
|
+
* child build. Default is the full 5-name set
|
|
254
|
+
* (`SUBAGENT_BLOCKED_TOOL_NAMES`). When
|
|
255
|
+
* `AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1` (or any truthy value) is
|
|
256
|
+
* present in `process.env`, drop `execute_code` — preserves the
|
|
257
|
+
* v4.1.0 fanout escape hatch for users with that flag in `.env`.
|
|
258
|
+
* Other 4 names stay blocked regardless.
|
|
259
|
+
*
|
|
260
|
+
* Logs a one-line warning when the escape hatch is active so the
|
|
261
|
+
* relaxation is visible in observability — silently relaxing a
|
|
262
|
+
* security boundary is exactly the failure mode we want to avoid.
|
|
263
|
+
*/
|
|
264
|
+
function resolveBlocklist(logger) {
|
|
265
|
+
const raw = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE;
|
|
266
|
+
if (!raw)
|
|
267
|
+
return exports.SUBAGENT_BLOCKED_TOOL_NAMES;
|
|
268
|
+
const flag = String(raw).trim().toLowerCase();
|
|
269
|
+
if (flag === '1' || flag === 'true' || flag === 'on' || flag === 'yes') {
|
|
270
|
+
logger?.warn?.('spawn_sub_agent: AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE escape hatch active; ' +
|
|
271
|
+
'execute_code dropped from child blocklist (other 4 names still blocked)', { source: 'env', flag: raw });
|
|
272
|
+
const relaxed = new Set(exports.SUBAGENT_BLOCKED_TOOL_NAMES);
|
|
273
|
+
relaxed.delete('execute_code');
|
|
274
|
+
return relaxed;
|
|
275
|
+
}
|
|
276
|
+
return exports.SUBAGENT_BLOCKED_TOOL_NAMES;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Compose the child's system prompt. Intentionally minimal — no
|
|
280
|
+
* SOUL.md, no parent identity, no MEMORY.md preamble. Goal-focused.
|
|
281
|
+
*/
|
|
282
|
+
function buildChildSystemPrompt(goal, context) {
|
|
283
|
+
const lines = [
|
|
284
|
+
'You are a focused sub-agent dispatched to handle ONE concrete task.',
|
|
285
|
+
'You have no memory of any prior conversation — only the goal and',
|
|
286
|
+
'optional context below. You cannot ask the user follow-up questions',
|
|
287
|
+
'(your `clarify` tool is disabled), you cannot spawn further sub-agents,',
|
|
288
|
+
'and you cannot write to MEMORY.md.',
|
|
289
|
+
'',
|
|
290
|
+
'When the task is done, produce a single final assistant message',
|
|
291
|
+
'summarising what you did and what you found. That summary is the',
|
|
292
|
+
'ONLY output the parent agent will see — no tool traces, no',
|
|
293
|
+
'intermediate reasoning. Make it self-contained, factual, and tight.',
|
|
294
|
+
'',
|
|
295
|
+
`## Goal`,
|
|
296
|
+
goal.trim(),
|
|
297
|
+
];
|
|
298
|
+
if (context && context.trim().length > 0) {
|
|
299
|
+
lines.push('', '## Background context', context.trim());
|
|
300
|
+
}
|
|
301
|
+
return lines.join('\n');
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Compose the initial user message. Currently a stub repeat of the goal
|
|
305
|
+
* — kept distinct from the system prompt so providers that prefer the
|
|
306
|
+
* system role for instructions and the user role for the immediate
|
|
307
|
+
* request both get a sensible payload. Context is included only in
|
|
308
|
+
* the system prompt so the user message stays compact.
|
|
309
|
+
*/
|
|
310
|
+
function composeUserMessage(goal, _context) {
|
|
311
|
+
return goal.trim();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* v4.6 Phase 1 — build the child agent's `onToolCall` callback. Emits
|
|
315
|
+
* `tool_call_started` + `tool_call_completed` events to the child's
|
|
316
|
+
* `runs` row via the supplied runStore, mirroring the daemon
|
|
317
|
+
* dispatcher's audit shape. Also logs each call at info level so
|
|
318
|
+
* users tailing aiden.log see what the child actually did.
|
|
319
|
+
*
|
|
320
|
+
* Returns `undefined` when persistence + logger are both absent —
|
|
321
|
+
* the agent constructor accepts `undefined` for `onToolCall` and
|
|
322
|
+
* dispatches without notifying.
|
|
323
|
+
*/
|
|
324
|
+
function buildOnToolCall(deps) {
|
|
325
|
+
const { runStore, childRunId, logger } = deps;
|
|
326
|
+
if (!runStore && !logger && childRunId === undefined)
|
|
327
|
+
return undefined;
|
|
328
|
+
// Per-call start time keyed by tool_call_id so `durationMs` on the
|
|
329
|
+
// completed event reflects per-tool wall-clock, not per-turn.
|
|
330
|
+
const callStarts = new Map();
|
|
331
|
+
return (call, phase, result) => {
|
|
332
|
+
try {
|
|
333
|
+
if (phase === 'before') {
|
|
334
|
+
const startedAt = Date.now();
|
|
335
|
+
callStarts.set(call.id, startedAt);
|
|
336
|
+
const argsSummary = safeShortJson(call.arguments, 200);
|
|
337
|
+
if (runStore && childRunId !== undefined) {
|
|
338
|
+
runStore.emitEvent(childRunId, 'tool_call_started', {
|
|
339
|
+
toolName: call.name,
|
|
340
|
+
args: argsSummary,
|
|
341
|
+
ts: startedAt,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (logger) {
|
|
345
|
+
logger.info('sub-agent tool call', {
|
|
346
|
+
childRunId: childRunId ?? null,
|
|
347
|
+
toolName: call.name,
|
|
348
|
+
args: argsSummary,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// phase === 'after'
|
|
354
|
+
const startedAt = callStarts.get(call.id) ?? Date.now();
|
|
355
|
+
callStarts.delete(call.id);
|
|
356
|
+
const durationMs = Date.now() - startedAt;
|
|
357
|
+
if (runStore && childRunId !== undefined) {
|
|
358
|
+
runStore.emitEvent(childRunId, 'tool_call_completed', {
|
|
359
|
+
toolName: call.name,
|
|
360
|
+
error: result?.error ?? null,
|
|
361
|
+
hasResult: result?.result !== undefined && result?.result !== null,
|
|
362
|
+
durationMs,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (logger) {
|
|
366
|
+
logger.info('sub-agent tool result', {
|
|
367
|
+
childRunId: childRunId ?? null,
|
|
368
|
+
toolName: call.name,
|
|
369
|
+
ok: !result?.error,
|
|
370
|
+
durationMs,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Observability must never crash the agent loop.
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/** JSON-stringify with a byte cap; returns the truncated string with an
|
|
380
|
+
* ellipsis when the payload exceeds `maxBytes`. */
|
|
381
|
+
function safeShortJson(value, maxBytes) {
|
|
382
|
+
try {
|
|
383
|
+
const s = JSON.stringify(value);
|
|
384
|
+
if (s === undefined)
|
|
385
|
+
return '';
|
|
386
|
+
return s.length > maxBytes ? s.slice(0, maxBytes) + '…' : s;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return String(value).slice(0, maxBytes);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -36,21 +36,22 @@
|
|
|
36
36
|
* - destructive tool exposure: caller filters the schemas array
|
|
37
37
|
* based on `AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE`
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
* AidenAgent
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* v4.6 Phase 2Q-A — the orchestrator was previously decoupled from
|
|
40
|
+
* AidenAgent via a `runChild` callback (one closure per fanout call
|
|
41
|
+
* built the child agent). Post-2Q-A every child flows through the
|
|
42
|
+
* `spawnSubAgent` primitive, so the legacy `runChild` callback is
|
|
43
|
+
* gone (deleted in 2R). Behavioural test fakes inject a stub
|
|
44
|
+
* `SpawnSubAgentDeps` (real `toolRegistry` + mock provider) — see
|
|
45
|
+
* `tests/v4/subagent/fanout.behavioral.test.ts` for the pattern.
|
|
45
46
|
*/
|
|
46
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
48
|
exports.runFanout = runFanout;
|
|
48
|
-
const node_crypto_1 = require("node:crypto");
|
|
49
49
|
const factory_1 = require("../logger/factory");
|
|
50
50
|
const budget_1 = require("./budget");
|
|
51
51
|
const providerRotation_1 = require("./providerRotation");
|
|
52
52
|
const merger_1 = require("./merger");
|
|
53
53
|
const diagnostics_1 = require("./diagnostics");
|
|
54
|
+
const spawnSubAgent_1 = require("./spawnSubAgent");
|
|
54
55
|
// ── Orchestrator ─────────────────────────────────────────────────────────
|
|
55
56
|
async function runFanout(opts) {
|
|
56
57
|
const logger = (opts.logger ?? (0, factory_1.noopLogger)()).child('subagent');
|
|
@@ -103,19 +104,21 @@ async function runFanout(opts) {
|
|
|
103
104
|
const children = [];
|
|
104
105
|
for (let i = 0; i < opts.n; i += 1) {
|
|
105
106
|
const provider = rotation.assignments[i];
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const role = opts.mode === 'partition' ? opts.tasks[i].role : undefined;
|
|
110
|
-
children.push(spawnOne({
|
|
107
|
+
const task = opts.mode === 'partition' ? opts.tasks[i] : null;
|
|
108
|
+
const role = task?.role;
|
|
109
|
+
children.push(spawnViaPrimitive({
|
|
111
110
|
index: i,
|
|
112
|
-
|
|
111
|
+
query: opts.mode === 'ensemble' ? opts.query : task.goal,
|
|
112
|
+
context: task?.context,
|
|
113
113
|
role,
|
|
114
114
|
provider,
|
|
115
|
+
singleProviderWarning: rotation.singleProviderWarning,
|
|
115
116
|
maxIterations: budget.maxIterations,
|
|
116
117
|
perTimeoutMs: budget.perSubagentTimeoutMs,
|
|
117
118
|
wallSignal: wallController.signal,
|
|
118
|
-
|
|
119
|
+
spawnDeps: opts.spawnDeps,
|
|
120
|
+
parentRunId: opts.parentRunId,
|
|
121
|
+
parentSessionId: opts.parentSessionId,
|
|
119
122
|
logger: logger.child(`#${i}:${provider.providerId}`),
|
|
120
123
|
now,
|
|
121
124
|
}));
|
|
@@ -157,60 +160,81 @@ async function runFanout(opts) {
|
|
|
157
160
|
});
|
|
158
161
|
return { results, merged: merge.merged, diagnostics };
|
|
159
162
|
}
|
|
160
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Spawn one child via the `spawnSubAgent` primitive and adapt its
|
|
165
|
+
* envelope to the merger's `SubagentResult` shape. Centralises the
|
|
166
|
+
* fanout-layer → primitive-layer conversion in one place — every
|
|
167
|
+
* call site goes through here so a future envelope-shape change
|
|
168
|
+
* has a single edit point.
|
|
169
|
+
*
|
|
170
|
+
* Envelope → SubagentResult mapping:
|
|
171
|
+
* - `envelope.summary` → `output` (empty string on failure;
|
|
172
|
+
* the merger uses `output.length === 0` for the failed test).
|
|
173
|
+
* - `envelope.error` → `error` (undefined when ok).
|
|
174
|
+
* - `envelope.metrics.durationMs` → ignored; we capture wall-clock
|
|
175
|
+
* at this layer for diagnostics consistency with v4.1's shape.
|
|
176
|
+
*/
|
|
177
|
+
async function spawnViaPrimitive(args) {
|
|
161
178
|
const startedAt = args.now();
|
|
162
|
-
// Per-child controller, aborted on wall-cap OR per-child timeout.
|
|
163
|
-
const childController = new AbortController();
|
|
164
|
-
const timer = setTimeout(() => childController.abort(), args.perTimeoutMs);
|
|
165
|
-
const wallHandler = () => childController.abort();
|
|
166
|
-
if (args.wallSignal.aborted)
|
|
167
|
-
childController.abort();
|
|
168
|
-
else
|
|
169
|
-
args.wallSignal.addEventListener('abort', wallHandler, { once: true });
|
|
170
|
-
const id = (0, node_crypto_1.randomUUID)();
|
|
171
179
|
args.logger.info('child: spawned', {
|
|
172
|
-
id,
|
|
173
180
|
provider: `${args.provider.providerId}:${args.provider.modelId}`,
|
|
174
181
|
role: args.role,
|
|
175
182
|
timeoutMs: args.perTimeoutMs,
|
|
176
183
|
});
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
// v4.6 Phase 2Q-A-FIX — only forward the per-spawn provider
|
|
185
|
+
// override when rotation has real diversity (>= 2 distinct
|
|
186
|
+
// providerIds). For single-provider pools, every child would be
|
|
187
|
+
// assigned the same providerId, so the override path adds nothing
|
|
188
|
+
// — and worse, it trips 2P's `resolveChildProvider` rejection when
|
|
189
|
+
// the parent is a non-FallbackAdapter ("single-provider
|
|
190
|
+
// configuration" branch). Omitting it lets the child inherit the
|
|
191
|
+
// parent's adapter, which is the correct effective behavior.
|
|
192
|
+
const spec = {
|
|
193
|
+
goal: args.role ? `[role: ${args.role}] ${args.query}` : args.query,
|
|
194
|
+
context: args.context,
|
|
195
|
+
maxIterations: args.maxIterations,
|
|
196
|
+
timeoutMs: args.perTimeoutMs,
|
|
197
|
+
provider: args.singleProviderWarning ? undefined : args.provider.providerId,
|
|
198
|
+
};
|
|
199
|
+
let envelope;
|
|
179
200
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
provider: args.provider,
|
|
185
|
-
signal: childController.signal,
|
|
186
|
-
maxIterations: args.maxIterations,
|
|
187
|
-
logger: args.logger,
|
|
201
|
+
envelope = await (0, spawnSubAgent_1.spawnSubAgent)(spec, args.spawnDeps, {
|
|
202
|
+
signal: args.wallSignal,
|
|
203
|
+
parentRunId: args.parentRunId,
|
|
204
|
+
parentSessionId: args.parentSessionId,
|
|
188
205
|
});
|
|
189
206
|
}
|
|
190
207
|
catch (err) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
// spawnSubAgent's contract says it never throws — but defend in
|
|
209
|
+
// depth: a thrown error from the primitive would otherwise sink
|
|
210
|
+
// the whole Promise.all, which would silently kill sibling
|
|
211
|
+
// children. Surface as a failed SubagentResult instead.
|
|
212
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
213
|
+
args.logger.warn('child: primitive threw', { error });
|
|
214
|
+
return {
|
|
215
|
+
index: args.index,
|
|
216
|
+
providerId: args.provider.providerId,
|
|
217
|
+
modelId: args.provider.modelId,
|
|
218
|
+
output: '',
|
|
219
|
+
error,
|
|
220
|
+
elapsedMs: args.now() - startedAt,
|
|
221
|
+
};
|
|
200
222
|
}
|
|
201
223
|
const elapsedMs = args.now() - startedAt;
|
|
202
|
-
|
|
224
|
+
const error = envelope.error ?? undefined;
|
|
225
|
+
args.logger.info('child: done', {
|
|
226
|
+
elapsedMs,
|
|
227
|
+
ok: envelope.ok,
|
|
228
|
+
status: envelope.status,
|
|
229
|
+
exitReason: envelope.exitReason,
|
|
230
|
+
childRunId: envelope.childRunId,
|
|
231
|
+
});
|
|
203
232
|
return {
|
|
204
233
|
index: args.index,
|
|
205
234
|
providerId: args.provider.providerId,
|
|
206
235
|
modelId: args.provider.modelId,
|
|
207
|
-
output:
|
|
236
|
+
output: envelope.ok ? (envelope.summary ?? '') : '',
|
|
208
237
|
error,
|
|
209
238
|
elapsedMs,
|
|
210
239
|
};
|
|
211
240
|
}
|
|
212
|
-
function buildPartitionPrompt(task) {
|
|
213
|
-
const role = task.role ? `Role: ${task.role}\n` : '';
|
|
214
|
-
const context = task.context ? `\nContext:\n${task.context}\n` : '';
|
|
215
|
-
return `${role}Goal: ${task.goal}${context}`;
|
|
216
|
-
}
|