aiden-runtime 4.1.5 → 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 +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -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 +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +145 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +362 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +144 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- 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/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- 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/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +88 -61
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +13 -3
|
@@ -0,0 +1,158 @@
|
|
|
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/selfimprovement/signatureBuilder.ts — v4.6 Phase 3b.
|
|
10
|
+
*
|
|
11
|
+
* Builds a stable, deterministic signature string for a failed tool
|
|
12
|
+
* call so equivalent failures collapse into one `failure_signatures`
|
|
13
|
+
* row. The shape is:
|
|
14
|
+
*
|
|
15
|
+
* <tool_name>:<failure_category>[:<args_hash_prefix>]
|
|
16
|
+
*
|
|
17
|
+
* The `args_hash_prefix` field is OPTIONAL. When the caller supplies
|
|
18
|
+
* `args`, this module normalises them (strips volatile fields like
|
|
19
|
+
* timestamps, run IDs, UUIDs, monotonic counters), serialises the
|
|
20
|
+
* result deterministically, and takes the first 6 hex chars of a
|
|
21
|
+
* SHA-256 digest. When `args` is omitted, the signature collapses to
|
|
22
|
+
* `<tool>:<category>` only — same logical failure, broader grouping.
|
|
23
|
+
*
|
|
24
|
+
* Granularity trade-offs:
|
|
25
|
+
*
|
|
26
|
+
* * Too granular ("every failure unique") → no aggregation; the
|
|
27
|
+
* `occurrences` column never increments past 1; operators can't
|
|
28
|
+
* see "this tool fails the same way over and over."
|
|
29
|
+
* * Too coarse ("only tool+category") → "file_read failed with
|
|
30
|
+
* `not_found`" groups EVERY missing file together; the operator
|
|
31
|
+
* can't tell which paths are sore points.
|
|
32
|
+
*
|
|
33
|
+
* The args-hash compromise: same tool + same category + same
|
|
34
|
+
* normalized args → same signature (good); same tool + same category
|
|
35
|
+
* + meaningfully different args → different signatures (also good).
|
|
36
|
+
* Volatile fields are stripped BEFORE hashing so re-hashing on a
|
|
37
|
+
* later turn produces the same signature even when only the
|
|
38
|
+
* timestamp / call id changes.
|
|
39
|
+
*
|
|
40
|
+
* Volatile field list (`VOLATILE_KEYS`) — defensive; covers the
|
|
41
|
+
* fields Aiden's tool layer tends to thread through args. Plugin
|
|
42
|
+
* authors who emit custom volatile keys should pre-normalise before
|
|
43
|
+
* calling this module.
|
|
44
|
+
*/
|
|
45
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
46
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.buildFailureSignature = buildFailureSignature;
|
|
50
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
51
|
+
// ── Implementation ───────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Keys whose values are stripped from the args object before hashing.
|
|
54
|
+
* These are fields that DO change between otherwise-identical
|
|
55
|
+
* failures (turn timestamps, run row ids, etc.) so leaving them in
|
|
56
|
+
* would prevent any signature from ever grouping.
|
|
57
|
+
*
|
|
58
|
+
* The list is intentionally narrow — only fields Aiden's tool layer
|
|
59
|
+
* is known to inject. Plugins emitting custom volatile keys must
|
|
60
|
+
* pre-normalise their args before calling this module.
|
|
61
|
+
*/
|
|
62
|
+
const VOLATILE_KEYS = new Set([
|
|
63
|
+
'timestamp',
|
|
64
|
+
'ts',
|
|
65
|
+
'requestId',
|
|
66
|
+
'request_id',
|
|
67
|
+
'runId',
|
|
68
|
+
'run_id',
|
|
69
|
+
'callId',
|
|
70
|
+
'call_id',
|
|
71
|
+
'sessionId',
|
|
72
|
+
'session_id',
|
|
73
|
+
'turnId',
|
|
74
|
+
'turn_id',
|
|
75
|
+
'eventId',
|
|
76
|
+
'event_id',
|
|
77
|
+
'createdAt',
|
|
78
|
+
'created_at',
|
|
79
|
+
'updatedAt',
|
|
80
|
+
'updated_at',
|
|
81
|
+
// Common UUID/idempotency-key names.
|
|
82
|
+
'uuid',
|
|
83
|
+
'idempotencyKey',
|
|
84
|
+
'idempotency_key',
|
|
85
|
+
]);
|
|
86
|
+
/**
|
|
87
|
+
* Deterministically stringify a value. Sorts object keys so
|
|
88
|
+
* `{a:1, b:2}` and `{b:2, a:1}` produce identical bytes. Strips
|
|
89
|
+
* volatile keys from any nested object before stringifying.
|
|
90
|
+
*
|
|
91
|
+
* Non-JSON-serialisable values (functions, symbols, circular refs)
|
|
92
|
+
* collapse to the literal string `'[unserializable]'` so the hash
|
|
93
|
+
* remains stable. Better-than-throwing is the right trade-off for
|
|
94
|
+
* a write-through hot path.
|
|
95
|
+
*/
|
|
96
|
+
function deterministicStringify(value) {
|
|
97
|
+
const seen = new WeakSet();
|
|
98
|
+
const visit = (v) => {
|
|
99
|
+
if (v === null || v === undefined)
|
|
100
|
+
return null;
|
|
101
|
+
const t = typeof v;
|
|
102
|
+
if (t === 'string' || t === 'number' || t === 'boolean')
|
|
103
|
+
return v;
|
|
104
|
+
if (t === 'function' || t === 'symbol')
|
|
105
|
+
return '[unserializable]';
|
|
106
|
+
if (typeof v === 'bigint')
|
|
107
|
+
return v.toString();
|
|
108
|
+
if (Array.isArray(v)) {
|
|
109
|
+
if (seen.has(v))
|
|
110
|
+
return '[circular]';
|
|
111
|
+
seen.add(v);
|
|
112
|
+
return v.map(visit);
|
|
113
|
+
}
|
|
114
|
+
if (t === 'object') {
|
|
115
|
+
const obj = v;
|
|
116
|
+
if (seen.has(obj))
|
|
117
|
+
return '[circular]';
|
|
118
|
+
seen.add(obj);
|
|
119
|
+
const out = {};
|
|
120
|
+
const keys = Object.keys(obj).filter((k) => !VOLATILE_KEYS.has(k));
|
|
121
|
+
keys.sort();
|
|
122
|
+
for (const k of keys)
|
|
123
|
+
out[k] = visit(obj[k]);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
return '[unserializable]';
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(visit(value));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return '[unserializable]';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build a failure signature. Pure function — no I/O, no side
|
|
137
|
+
* effects. Safe to call on the hot path of every classified
|
|
138
|
+
* failure; SHA-256 of a small JSON string is cheap (microseconds).
|
|
139
|
+
*/
|
|
140
|
+
function buildFailureSignature(input) {
|
|
141
|
+
const base = `${input.toolName}:${input.category}`;
|
|
142
|
+
if (input.args === undefined) {
|
|
143
|
+
return { signature: base };
|
|
144
|
+
}
|
|
145
|
+
const normalized = deterministicStringify(input.args);
|
|
146
|
+
// Empty / trivially-null args don't deserve a hash suffix —
|
|
147
|
+
// collapse to the base signature so "args: {}" and "no args"
|
|
148
|
+
// group together.
|
|
149
|
+
if (normalized === 'null' || normalized === '{}' || normalized === '[]') {
|
|
150
|
+
return { signature: base };
|
|
151
|
+
}
|
|
152
|
+
const digest = node_crypto_1.default.createHash('sha256').update(normalized).digest('hex');
|
|
153
|
+
const argsHash = digest.slice(0, 6);
|
|
154
|
+
return {
|
|
155
|
+
signature: `${base}:${argsHash}`,
|
|
156
|
+
argsHash,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -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
|
+
}
|