aiden-runtime 4.0.1 → 4.1.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 +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +513 -14
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +269 -52
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +19 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/setup.js +34 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +300 -14
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/setupWizard.js +466 -232
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/firstRun/providerDetection.js +287 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/providers/v4/nullAdapter.js +58 -0
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- package/package.json +11 -2
|
@@ -0,0 +1,51 @@
|
|
|
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/skillMining/traceFingerprint.ts — Phase v4.1-skill-mining
|
|
10
|
+
*
|
|
11
|
+
* Deterministic content-addressing for tool-call traces.
|
|
12
|
+
*
|
|
13
|
+
* The fingerprint is the sha256 hex of the normalized
|
|
14
|
+
* (toolName, sorted-arg-keys) sequence joined by `|`.
|
|
15
|
+
*
|
|
16
|
+
* Properties (verified by smoke):
|
|
17
|
+
* - identical traces produce identical hashes
|
|
18
|
+
* - traces that differ only in arg *values* (same arg keys)
|
|
19
|
+
* produce identical hashes — this is the desired behavior:
|
|
20
|
+
* "search github for X" and "search github for Y" should
|
|
21
|
+
* dedup to one candidate
|
|
22
|
+
* - traces with different tool sequences or arg keys produce
|
|
23
|
+
* different hashes
|
|
24
|
+
* - deterministic across runs (no salt, no time)
|
|
25
|
+
*
|
|
26
|
+
* The candidateStore + skillMiner use this to dedup proposals;
|
|
27
|
+
* the rejected list also tracks fingerprints so a user-rejected
|
|
28
|
+
* workflow doesn't get re-proposed on the next run.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.traceFingerprint = traceFingerprint;
|
|
32
|
+
const node_crypto_1 = require("node:crypto");
|
|
33
|
+
/** Normalize a single trace entry to its fingerprint contribution. */
|
|
34
|
+
function normalizeEntry(entry) {
|
|
35
|
+
const name = String(entry.name ?? '').trim().toLowerCase();
|
|
36
|
+
let argKeys = [];
|
|
37
|
+
if (entry.args && typeof entry.args === 'object' && !Array.isArray(entry.args)) {
|
|
38
|
+
argKeys = Object.keys(entry.args).sort();
|
|
39
|
+
}
|
|
40
|
+
return `${name}(${argKeys.join(',')})`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fingerprint a trace. Returns a 64-char lowercase sha256 hex.
|
|
44
|
+
* Empty trace is a valid input (returns the hash of the empty
|
|
45
|
+
* normalized string) — callers should reject empty traces before
|
|
46
|
+
* fingerprinting to keep the pending queue free of useless entries.
|
|
47
|
+
*/
|
|
48
|
+
function traceFingerprint(trace) {
|
|
49
|
+
const normalized = trace.map(normalizeEntry).join('|');
|
|
50
|
+
return (0, node_crypto_1.createHash)('sha256').update(normalized, 'utf8').digest('hex');
|
|
51
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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/budget.ts — Phase v4.1-subagent
|
|
10
|
+
*
|
|
11
|
+
* Per-subagent timeouts and iteration caps. Two budgets layered:
|
|
12
|
+
*
|
|
13
|
+
* - `perSubagentTimeoutMs` — hard wall-clock cap on a single
|
|
14
|
+
* subagent. Fired via AbortController; AidenAgent's provider
|
|
15
|
+
* adapter receives the abort and the in-flight HTTP call is
|
|
16
|
+
* cancelled (the v3 lesson — flag-only cancellation leaks
|
|
17
|
+
* tokens, AbortController plumbed through is the v4 fix).
|
|
18
|
+
*
|
|
19
|
+
* - `wallClockCapMs` — outer cap on the whole fanout. Defaults
|
|
20
|
+
* to 5× the per-subagent timeout because parallel subagents
|
|
21
|
+
* should finish faster than 5× one-at-a-time, but variance
|
|
22
|
+
* (provider rate limits, retry backoff) can extend the tail.
|
|
23
|
+
*
|
|
24
|
+
* - `maxIterations` — fresh per subagent. v3 starved nested
|
|
25
|
+
* spawns by dividing a global budget; v4 hands each subagent a
|
|
26
|
+
* full fresh budget and relies on the wall-clock cap for the
|
|
27
|
+
* outer bound.
|
|
28
|
+
*
|
|
29
|
+
* Read at fanout start. Each subagent gets its own AbortSignal
|
|
30
|
+
* derived from the timeout; abort propagates from parent down via
|
|
31
|
+
* `parentAbort.aborted`.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.DEFAULT_FANOUT_N = exports.MAX_FANOUT_N = exports.WALL_CLOCK_CAP_MULT = exports.DEFAULT_SUBAGENT_MAX_ITERATIONS = exports.DEFAULT_SUBAGENT_TIMEOUT_MS = void 0;
|
|
35
|
+
exports.resolveBudget = resolveBudget;
|
|
36
|
+
exports.validateN = validateN;
|
|
37
|
+
/** Default per-subagent timeout (ms). Override via env
|
|
38
|
+
* `AIDEN_SUBAGENT_TIMEOUT_MS` or `timeoutMs` argument on the tool. */
|
|
39
|
+
exports.DEFAULT_SUBAGENT_TIMEOUT_MS = 90000;
|
|
40
|
+
/** Default per-subagent iteration cap. Fresh per subagent. */
|
|
41
|
+
exports.DEFAULT_SUBAGENT_MAX_ITERATIONS = 20;
|
|
42
|
+
/** Wall-clock cap multiplier — outer cap = `perSubagentTimeoutMs * MULT`. */
|
|
43
|
+
exports.WALL_CLOCK_CAP_MULT = 5;
|
|
44
|
+
/** Max N — hard refuse beyond. */
|
|
45
|
+
exports.MAX_FANOUT_N = 5;
|
|
46
|
+
/** Default N when the caller doesn't specify. */
|
|
47
|
+
exports.DEFAULT_FANOUT_N = 3;
|
|
48
|
+
/** Resolve the live budget. Tool-call argument > env > module default. */
|
|
49
|
+
function resolveBudget(opts = {}) {
|
|
50
|
+
const env = opts.env ?? process.env;
|
|
51
|
+
const envTimeoutRaw = env.AIDEN_SUBAGENT_TIMEOUT_MS;
|
|
52
|
+
const envTimeout = envTimeoutRaw && /^\d+$/.test(envTimeoutRaw)
|
|
53
|
+
? Number.parseInt(envTimeoutRaw, 10)
|
|
54
|
+
: null;
|
|
55
|
+
const perSubagentTimeoutMs = opts.timeoutMs ?? envTimeout ?? exports.DEFAULT_SUBAGENT_TIMEOUT_MS;
|
|
56
|
+
return {
|
|
57
|
+
perSubagentTimeoutMs,
|
|
58
|
+
wallClockCapMs: perSubagentTimeoutMs * exports.WALL_CLOCK_CAP_MULT,
|
|
59
|
+
maxIterations: exports.DEFAULT_SUBAGENT_MAX_ITERATIONS,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Validate the requested N — throws with a clear message when out of
|
|
63
|
+
* bounds. Caller surfaces the error as a tool-result error string. */
|
|
64
|
+
function validateN(n) {
|
|
65
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
66
|
+
throw new Error(`subagent_fanout: n must be an integer, got ${n}`);
|
|
67
|
+
}
|
|
68
|
+
if (n < 1) {
|
|
69
|
+
throw new Error(`subagent_fanout: n must be >= 1, got ${n}`);
|
|
70
|
+
}
|
|
71
|
+
if (n > exports.MAX_FANOUT_N) {
|
|
72
|
+
throw new Error(`subagent_fanout: n=${n} exceeds hard cap ${exports.MAX_FANOUT_N}. ` +
|
|
73
|
+
`Higher concurrency hits provider RPM limits and increases tail latency variance.`);
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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/diagnostics.ts — Phase v4.1-subagent
|
|
10
|
+
*
|
|
11
|
+
* Build fingerprint + counts surfaced by `aiden subagent status` and
|
|
12
|
+
* the per-fanout result envelope. The fingerprint follows the same
|
|
13
|
+
* convention every Aiden phase since v4.1-3.2 has used: a constant
|
|
14
|
+
* string the user can grep for to verify the running build matches
|
|
15
|
+
* the phase they expected. Bump on every shipped phase. Format:
|
|
16
|
+
* `v4.1-subagent[+suffix]`.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.AIDEN_SUBAGENT_BUILD = void 0;
|
|
20
|
+
/** Build fingerprint — bump per phase. Surfaced in `aiden subagent
|
|
21
|
+
* status` and the post-fanout summary line. */
|
|
22
|
+
exports.AIDEN_SUBAGENT_BUILD = 'v4.1-subagent.2';
|
|
@@ -0,0 +1,216 @@
|
|
|
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/fanout.ts — Phase v4.1-subagent
|
|
10
|
+
*
|
|
11
|
+
* Parallel-agent orchestrator. Spawn N children against the same
|
|
12
|
+
* problem (or a partition), enforce per-child timeouts and an outer
|
|
13
|
+
* wall-clock cap, then merge results via the chosen strategy.
|
|
14
|
+
*
|
|
15
|
+
* Design constraints (locked from recon):
|
|
16
|
+
*
|
|
17
|
+
* - In-process `Promise.all` over N children. No child processes,
|
|
18
|
+
* no MCP-spawn (Aiden's MCP server is for external clients).
|
|
19
|
+
* - Per-child AbortSignal derived from a parent signal + timeout.
|
|
20
|
+
* Aborts cascade — parent abort kills every child mid-flight via
|
|
21
|
+
* the provider's own HTTP AbortController.
|
|
22
|
+
* - Each child gets:
|
|
23
|
+
* * own session ID (UUID) — sessions never collide
|
|
24
|
+
* * own provider rotation slot
|
|
25
|
+
* * own cloned FallbackAdapter when applicable (mutable rate-
|
|
26
|
+
* limit state isolated per child)
|
|
27
|
+
* * fresh max_iterations (no v3-style budget halving)
|
|
28
|
+
* - Shared (read-only) across children:
|
|
29
|
+
* * tool registry, skill loader, paths, memoryManager
|
|
30
|
+
*
|
|
31
|
+
* Hot blockers from the recon are addressed by the caller:
|
|
32
|
+
* - browser bridge: caller wraps browser tool dispatch in
|
|
33
|
+
* `withPwLock` (see core/playwrightBridge.ts)
|
|
34
|
+
* - approval engine: caller passes a ToolContext with
|
|
35
|
+
* `approvalEngine` undefined (no prompts in subagents)
|
|
36
|
+
* - destructive tool exposure: caller filters the schemas array
|
|
37
|
+
* based on `AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE`
|
|
38
|
+
*
|
|
39
|
+
* The orchestrator itself is INTENTIONALLY decoupled from
|
|
40
|
+
* AidenAgent — it takes a `runChild` callback that knows how to run
|
|
41
|
+
* one subagent. The tool wrapper at tools/v4/subagent/subagentFanout
|
|
42
|
+
* supplies the production callback (which constructs an AidenAgent);
|
|
43
|
+
* tests inject a stub that returns canned strings without any
|
|
44
|
+
* provider plumbing. This is what made the offline smoke tractable.
|
|
45
|
+
*/
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.runFanout = runFanout;
|
|
48
|
+
const node_crypto_1 = require("node:crypto");
|
|
49
|
+
const factory_1 = require("../logger/factory");
|
|
50
|
+
const budget_1 = require("./budget");
|
|
51
|
+
const providerRotation_1 = require("./providerRotation");
|
|
52
|
+
const merger_1 = require("./merger");
|
|
53
|
+
const diagnostics_1 = require("./diagnostics");
|
|
54
|
+
// ── Orchestrator ─────────────────────────────────────────────────────────
|
|
55
|
+
async function runFanout(opts) {
|
|
56
|
+
const logger = (opts.logger ?? (0, factory_1.noopLogger)()).child('subagent');
|
|
57
|
+
const now = opts.now ?? Date.now;
|
|
58
|
+
// ── Pre-flight validation ─────────────────────────────────────
|
|
59
|
+
(0, budget_1.validateN)(opts.n);
|
|
60
|
+
if (opts.mode === 'ensemble' && !opts.query) {
|
|
61
|
+
throw new Error('subagent_fanout: ensemble mode requires a `query`');
|
|
62
|
+
}
|
|
63
|
+
if (opts.mode === 'partition') {
|
|
64
|
+
if (!opts.tasks || opts.tasks.length === 0) {
|
|
65
|
+
throw new Error('subagent_fanout: partition mode requires `tasks[]`');
|
|
66
|
+
}
|
|
67
|
+
if (opts.tasks.length !== opts.n) {
|
|
68
|
+
throw new Error(`subagent_fanout: partition tasks.length (${opts.tasks.length}) ` +
|
|
69
|
+
`must equal n (${opts.n})`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (opts.providers.length === 0) {
|
|
73
|
+
throw new Error('subagent_fanout: no providers available — cannot fan out');
|
|
74
|
+
}
|
|
75
|
+
const budget = (0, budget_1.resolveBudget)({ timeoutMs: opts.timeoutMs });
|
|
76
|
+
const rotation = (0, providerRotation_1.rotateProviders)(opts.n, opts.providers);
|
|
77
|
+
if (rotation.singleProviderWarning) {
|
|
78
|
+
logger.warn('subagent_fanout: single-provider fanout — diversity ≈ temperature variation', {
|
|
79
|
+
providers: opts.providers.length,
|
|
80
|
+
n: opts.n,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
logger.info('subagent_fanout: launching', {
|
|
84
|
+
build: diagnostics_1.AIDEN_SUBAGENT_BUILD,
|
|
85
|
+
mode: opts.mode,
|
|
86
|
+
n: opts.n,
|
|
87
|
+
merge: opts.merge,
|
|
88
|
+
perSubagentTimeoutMs: budget.perSubagentTimeoutMs,
|
|
89
|
+
wallClockCapMs: budget.wallClockCapMs,
|
|
90
|
+
});
|
|
91
|
+
// ── Spawn ─────────────────────────────────────────────────────
|
|
92
|
+
const startedAt = now();
|
|
93
|
+
const wallController = new AbortController();
|
|
94
|
+
const wallTimer = setTimeout(() => wallController.abort(), budget.wallClockCapMs);
|
|
95
|
+
// Forward parent abort to the wall controller so it cascades.
|
|
96
|
+
const parentAbortHandler = () => wallController.abort();
|
|
97
|
+
if (opts.parentAbort) {
|
|
98
|
+
if (opts.parentAbort.aborted)
|
|
99
|
+
wallController.abort();
|
|
100
|
+
else
|
|
101
|
+
opts.parentAbort.addEventListener('abort', parentAbortHandler, { once: true });
|
|
102
|
+
}
|
|
103
|
+
const children = [];
|
|
104
|
+
for (let i = 0; i < opts.n; i += 1) {
|
|
105
|
+
const provider = rotation.assignments[i];
|
|
106
|
+
const prompt = opts.mode === 'ensemble'
|
|
107
|
+
? opts.query
|
|
108
|
+
: buildPartitionPrompt(opts.tasks[i]);
|
|
109
|
+
const role = opts.mode === 'partition' ? opts.tasks[i].role : undefined;
|
|
110
|
+
children.push(spawnOne({
|
|
111
|
+
index: i,
|
|
112
|
+
prompt,
|
|
113
|
+
role,
|
|
114
|
+
provider,
|
|
115
|
+
maxIterations: budget.maxIterations,
|
|
116
|
+
perTimeoutMs: budget.perSubagentTimeoutMs,
|
|
117
|
+
wallSignal: wallController.signal,
|
|
118
|
+
runChild: opts.runChild,
|
|
119
|
+
logger: logger.child(`#${i}:${provider.providerId}`),
|
|
120
|
+
now,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
const results = await Promise.all(children);
|
|
124
|
+
clearTimeout(wallTimer);
|
|
125
|
+
if (opts.parentAbort) {
|
|
126
|
+
opts.parentAbort.removeEventListener('abort', parentAbortHandler);
|
|
127
|
+
}
|
|
128
|
+
const totalMs = now() - startedAt;
|
|
129
|
+
// ── Merge ─────────────────────────────────────────────────────
|
|
130
|
+
const merge = await (0, merger_1.mergeResults)(results, {
|
|
131
|
+
strategy: opts.merge,
|
|
132
|
+
aggregatorAdapter: opts.aggregatorAdapter,
|
|
133
|
+
aggregatorModel: opts.aggregatorModel,
|
|
134
|
+
userQuery: opts.mode === 'ensemble'
|
|
135
|
+
? opts.query
|
|
136
|
+
: opts.tasks.map((t, i) => `(${i + 1}) ${t.goal}`).join('\n'),
|
|
137
|
+
logger,
|
|
138
|
+
signal: wallController.signal,
|
|
139
|
+
});
|
|
140
|
+
// ── Diagnostics ───────────────────────────────────────────────
|
|
141
|
+
const diagnostics = {
|
|
142
|
+
build: diagnostics_1.AIDEN_SUBAGENT_BUILD,
|
|
143
|
+
launched: opts.n,
|
|
144
|
+
succeeded: results.filter((r) => !r.error && r.output.length > 0).length,
|
|
145
|
+
failed: results.filter((r) => !!r.error || r.output.length === 0).length,
|
|
146
|
+
totalMs,
|
|
147
|
+
perSubagentMs: results.map((r) => r.elapsedMs),
|
|
148
|
+
providerDistribution: results.map((r) => r.providerId),
|
|
149
|
+
singleProviderWarning: rotation.singleProviderWarning,
|
|
150
|
+
aggregator: merge.aggregator,
|
|
151
|
+
};
|
|
152
|
+
logger.info('subagent_fanout: complete', {
|
|
153
|
+
succeeded: diagnostics.succeeded,
|
|
154
|
+
failed: diagnostics.failed,
|
|
155
|
+
totalMs,
|
|
156
|
+
aggregator: merge.aggregator || '(none)',
|
|
157
|
+
});
|
|
158
|
+
return { results, merged: merge.merged, diagnostics };
|
|
159
|
+
}
|
|
160
|
+
async function spawnOne(args) {
|
|
161
|
+
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
|
+
args.logger.info('child: spawned', {
|
|
172
|
+
id,
|
|
173
|
+
provider: `${args.provider.providerId}:${args.provider.modelId}`,
|
|
174
|
+
role: args.role,
|
|
175
|
+
timeoutMs: args.perTimeoutMs,
|
|
176
|
+
});
|
|
177
|
+
let output = '';
|
|
178
|
+
let error;
|
|
179
|
+
try {
|
|
180
|
+
output = await args.runChild({
|
|
181
|
+
index: args.index,
|
|
182
|
+
prompt: args.prompt,
|
|
183
|
+
role: args.role,
|
|
184
|
+
provider: args.provider,
|
|
185
|
+
signal: childController.signal,
|
|
186
|
+
maxIterations: args.maxIterations,
|
|
187
|
+
logger: args.logger,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
error = err instanceof Error ? err.message : String(err);
|
|
192
|
+
if (childController.signal.aborted) {
|
|
193
|
+
error = `aborted (timeout=${args.perTimeoutMs}ms or parent abort): ${error}`;
|
|
194
|
+
}
|
|
195
|
+
args.logger.warn('child: errored', { error });
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
args.wallSignal.removeEventListener('abort', wallHandler);
|
|
200
|
+
}
|
|
201
|
+
const elapsedMs = args.now() - startedAt;
|
|
202
|
+
args.logger.info('child: done', { elapsedMs, ok: !error && output.length > 0 });
|
|
203
|
+
return {
|
|
204
|
+
index: args.index,
|
|
205
|
+
providerId: args.provider.providerId,
|
|
206
|
+
modelId: args.provider.modelId,
|
|
207
|
+
output: error ? '' : output,
|
|
208
|
+
error,
|
|
209
|
+
elapsedMs,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
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/merger.ts — Phase v4.1-subagent
|
|
10
|
+
*
|
|
11
|
+
* Combine N subagent outputs into one (or zero) aggregator response.
|
|
12
|
+
* Four strategies, each with a different cost shape — the tool's
|
|
13
|
+
* description surfaces this so the calling LLM picks knowingly:
|
|
14
|
+
*
|
|
15
|
+
* - 'all' — return raw N results, no aggregator call. FREE.
|
|
16
|
+
* Caller's parent agent reads them in its own
|
|
17
|
+
* next turn (the partition pattern).
|
|
18
|
+
* - 'vote' — LLM judge picks ONE result verbatim. +1 call.
|
|
19
|
+
* - 'pick-best' — LLM judge picks one with reasoning. +1 call.
|
|
20
|
+
* Same wire shape as 'vote', different prompt.
|
|
21
|
+
* - 'combine' — LLM synthesizes N results into one answer.
|
|
22
|
+
* +1 call (the ensemble pattern).
|
|
23
|
+
*
|
|
24
|
+
* The aggregator uses the parent's active provider+model by default,
|
|
25
|
+
* env override `AIDEN_SUBAGENT_AGGREGATOR_MODEL` (provider:model
|
|
26
|
+
* format, e.g. `groq:llama-3.3-70b-versatile`) when the user wants
|
|
27
|
+
* to control aggregator cost without affecting subagent fanout.
|
|
28
|
+
*
|
|
29
|
+
* The aggregator call is intentionally THIN — single-shot, no tools,
|
|
30
|
+
* no agent loop. It's a text-in / text-out pass over the N results
|
|
31
|
+
* with a strategy-specific system prompt.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.resolveAggregatorOverride = resolveAggregatorOverride;
|
|
35
|
+
exports.mergeResults = mergeResults;
|
|
36
|
+
const factory_1 = require("../logger/factory");
|
|
37
|
+
/** Resolve env override for aggregator model. Returns null when unset
|
|
38
|
+
* or malformed; caller falls back to parent's active model. */
|
|
39
|
+
function resolveAggregatorOverride(env = process.env) {
|
|
40
|
+
const raw = env.AIDEN_SUBAGENT_AGGREGATOR_MODEL?.trim();
|
|
41
|
+
if (!raw)
|
|
42
|
+
return null;
|
|
43
|
+
const colon = raw.indexOf(':');
|
|
44
|
+
if (colon < 1 || colon === raw.length - 1)
|
|
45
|
+
return null;
|
|
46
|
+
const providerId = raw.slice(0, colon).trim();
|
|
47
|
+
const modelId = raw.slice(colon + 1).trim();
|
|
48
|
+
if (!providerId || !modelId)
|
|
49
|
+
return null;
|
|
50
|
+
return { providerId, modelId };
|
|
51
|
+
}
|
|
52
|
+
/** Apply the strategy. Logs every aggregator call for observability. */
|
|
53
|
+
async function mergeResults(results, opts) {
|
|
54
|
+
const logger = opts.logger ?? (0, factory_1.noopLogger)();
|
|
55
|
+
if (opts.strategy === 'all') {
|
|
56
|
+
return { merged: null, aggregator: '' };
|
|
57
|
+
}
|
|
58
|
+
// Filter out failures — aggregator only sees usable outputs.
|
|
59
|
+
const usable = results.filter((r) => !r.error && r.output.length > 0);
|
|
60
|
+
if (usable.length === 0) {
|
|
61
|
+
return {
|
|
62
|
+
merged: '[Aggregator: every subagent failed — no output to merge]',
|
|
63
|
+
aggregator: '',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const aggregatorLabel = `${opts.aggregatorModel.providerId}:${opts.aggregatorModel.modelId}`;
|
|
67
|
+
const systemPrompt = buildSystemPrompt(opts.strategy);
|
|
68
|
+
const userPrompt = buildUserPrompt(opts.strategy, usable, opts.userQuery);
|
|
69
|
+
const messages = [
|
|
70
|
+
{ role: 'system', content: systemPrompt },
|
|
71
|
+
{ role: 'user', content: userPrompt },
|
|
72
|
+
];
|
|
73
|
+
logger.info('subagent merge: aggregator dispatching', {
|
|
74
|
+
scope: 'subagent',
|
|
75
|
+
strategy: opts.strategy,
|
|
76
|
+
aggregator: aggregatorLabel,
|
|
77
|
+
sources: usable.length,
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
const out = await opts.aggregatorAdapter.call({
|
|
81
|
+
messages,
|
|
82
|
+
tools: [],
|
|
83
|
+
stream: false,
|
|
84
|
+
});
|
|
85
|
+
const text = extractFinalText(out);
|
|
86
|
+
return { merged: text, aggregator: aggregatorLabel };
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
logger.warn('subagent merge: aggregator failed', {
|
|
91
|
+
scope: 'subagent',
|
|
92
|
+
strategy: opts.strategy,
|
|
93
|
+
error: message,
|
|
94
|
+
});
|
|
95
|
+
// Graceful degrade: return the first usable subagent output rather
|
|
96
|
+
// than crash the whole fanout. Caller sees `aggregator === ''`
|
|
97
|
+
// and a synthetic merged note.
|
|
98
|
+
return {
|
|
99
|
+
merged: `[Aggregator failed: ${message}]\n\n${usable[0].output}`,
|
|
100
|
+
aggregator: '',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function buildSystemPrompt(strategy) {
|
|
105
|
+
switch (strategy) {
|
|
106
|
+
case 'vote':
|
|
107
|
+
return [
|
|
108
|
+
'You are an answer-selection judge. You will be shown a user query and N candidate answers from independent agents.',
|
|
109
|
+
'Pick exactly ONE candidate that best answers the query and return ITS TEXT VERBATIM with no preamble, no commentary, no formatting changes.',
|
|
110
|
+
'Choose the answer that is most factually accurate, complete, and directly addresses the query.',
|
|
111
|
+
].join(' ');
|
|
112
|
+
case 'pick-best':
|
|
113
|
+
return [
|
|
114
|
+
'You are an answer-selection judge. You will be shown a user query and N candidate answers from independent agents.',
|
|
115
|
+
'Pick the BEST candidate. Output a one-sentence reason on the first line, then a blank line, then the chosen candidate text verbatim.',
|
|
116
|
+
'Format:\nReason: <one sentence>\n\n<chosen candidate verbatim>',
|
|
117
|
+
].join(' ');
|
|
118
|
+
case 'combine':
|
|
119
|
+
return [
|
|
120
|
+
'You are a synthesis aggregator. You will be shown a user query and N candidate answers from independent agents.',
|
|
121
|
+
'Produce ONE unified answer that integrates the strongest points from each candidate.',
|
|
122
|
+
'Resolve disagreements by stating both positions when sources diverge factually; collapse redundancy where they agree.',
|
|
123
|
+
'Do not name the candidates. Speak directly. No meta-commentary about being an aggregator.',
|
|
124
|
+
].join(' ');
|
|
125
|
+
default:
|
|
126
|
+
return 'You are a helpful assistant.';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function buildUserPrompt(strategy, results, query) {
|
|
130
|
+
const blocks = results.map((r, i) => `--- CANDIDATE ${i + 1} (${r.providerId}:${r.modelId}) ---\n${r.output.trim()}`).join('\n\n');
|
|
131
|
+
const action = strategy === 'combine'
|
|
132
|
+
? 'Synthesize these into one unified answer.'
|
|
133
|
+
: strategy === 'pick-best'
|
|
134
|
+
? 'Pick the best candidate.'
|
|
135
|
+
: 'Pick the best candidate verbatim.';
|
|
136
|
+
return `USER QUERY:\n${query}\n\n${blocks}\n\n${action}`;
|
|
137
|
+
}
|
|
138
|
+
function extractFinalText(out) {
|
|
139
|
+
// ProviderCallOutput.content is `string | null` per providers/v4/types.ts.
|
|
140
|
+
// For a single-shot non-streaming aggregator call we expect the model
|
|
141
|
+
// to return text directly with `finishReason: 'stop'`.
|
|
142
|
+
if (out && typeof out === 'object') {
|
|
143
|
+
const o = out;
|
|
144
|
+
if (typeof o.content === 'string' && o.content.length > 0)
|
|
145
|
+
return o.content;
|
|
146
|
+
}
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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/providerRotation.ts — Phase v4.1-subagent
|
|
10
|
+
*
|
|
11
|
+
* Round-robin provider selection across N subagents. v3's lesson —
|
|
12
|
+
* "N samples from one provider is `temperature` with extra steps" —
|
|
13
|
+
* makes provider diversity load-bearing for fanout. This module
|
|
14
|
+
* decides which provider each subagent uses.
|
|
15
|
+
*
|
|
16
|
+
* Two layers:
|
|
17
|
+
*
|
|
18
|
+
* 1. If multiple providers are configured (i.e. multiple keys
|
|
19
|
+
* across distinct providerIds), round-robin across them.
|
|
20
|
+
* 2. If only one provider is configured, fall back to round-robin
|
|
21
|
+
* across the slots WITHIN that provider (Groq slot 1/2/3/4,
|
|
22
|
+
* Together primary/fallback). Diversity reduces to temperature
|
|
23
|
+
* variation; the diagnostics flag this with
|
|
24
|
+
* `singleProviderWarning: true`.
|
|
25
|
+
*
|
|
26
|
+
* The module does NOT build adapters — it picks PROVIDER IDS and
|
|
27
|
+
* leaves adapter construction to the caller (which knows how to
|
|
28
|
+
* resolve a credential / clone a FallbackAdapter / etc).
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.rotateProviders = rotateProviders;
|
|
32
|
+
/**
|
|
33
|
+
* Pick a provider for each of `n` subagents. `available` is the list
|
|
34
|
+
* of configured options the caller has resolved; ordering matters
|
|
35
|
+
* (the first becomes the primary fallback when round-robin wraps).
|
|
36
|
+
*
|
|
37
|
+
* The function is deterministic given the same inputs — useful for
|
|
38
|
+
* tests and for users debugging "why did subagent 3 hit Together?".
|
|
39
|
+
*/
|
|
40
|
+
function rotateProviders(n, available) {
|
|
41
|
+
if (available.length === 0) {
|
|
42
|
+
throw new Error('subagent_fanout: no providers available for rotation');
|
|
43
|
+
}
|
|
44
|
+
if (n < 1) {
|
|
45
|
+
throw new Error(`subagent_fanout: n must be >= 1, got ${n}`);
|
|
46
|
+
}
|
|
47
|
+
const distinct = new Set(available.map((o) => o.providerId));
|
|
48
|
+
const singleProviderWarning = distinct.size < 2;
|
|
49
|
+
const assignments = [];
|
|
50
|
+
for (let i = 0; i < n; i += 1) {
|
|
51
|
+
assignments.push(available[i % available.length]);
|
|
52
|
+
}
|
|
53
|
+
return { assignments, singleProviderWarning };
|
|
54
|
+
}
|