aiden-runtime 4.0.2 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +19 -11
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +424 -7
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +256 -55
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +102 -1
  28. package/dist/cli/v4/doctorLiveness.js +329 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/shellInterpolation.js +139 -0
  37. package/dist/cli/v4/skinEngine.js +21 -1
  38. package/dist/cli/v4/streamingPrefix.js +121 -0
  39. package/dist/cli/v4/syntaxHighlight.js +345 -0
  40. package/dist/cli/v4/table.js +216 -0
  41. package/dist/cli/v4/themeDetect.js +81 -0
  42. package/dist/cli/v4/uiBuild.js +74 -0
  43. package/dist/cli/v4/voiceCli.js +113 -0
  44. package/dist/cli/v4/voicePromptApi.js +196 -0
  45. package/dist/core/channels/discord.js +16 -10
  46. package/dist/core/channels/email.js +13 -9
  47. package/dist/core/channels/imessage.js +13 -9
  48. package/dist/core/channels/manager.js +25 -7
  49. package/dist/core/channels/pdf-extract.js +180 -0
  50. package/dist/core/channels/photo-vision.js +157 -0
  51. package/dist/core/channels/signal.js +11 -7
  52. package/dist/core/channels/slack.js +13 -10
  53. package/dist/core/channels/telegram-commands.js +154 -0
  54. package/dist/core/channels/telegram-groups.js +198 -0
  55. package/dist/core/channels/telegram-rate-limit.js +124 -0
  56. package/dist/core/channels/telegram.js +1980 -0
  57. package/dist/core/channels/twilio.js +11 -7
  58. package/dist/core/channels/webhook.js +9 -5
  59. package/dist/core/channels/whatsapp.js +15 -11
  60. package/dist/core/channels/whisper-transcribe.js +163 -0
  61. package/dist/core/cronManager.js +33 -294
  62. package/dist/core/gateway.js +29 -8
  63. package/dist/core/playwrightBridge.js +90 -0
  64. package/dist/core/v4/aidenAgent.js +35 -0
  65. package/dist/core/v4/auxiliaryClient.js +2 -2
  66. package/dist/core/v4/cron/atomicWrite.js +18 -4
  67. package/dist/core/v4/cron/cronExecute.js +300 -0
  68. package/dist/core/v4/cron/cronManager.js +502 -0
  69. package/dist/core/v4/cron/cronState.js +314 -0
  70. package/dist/core/v4/cron/cronTick.js +90 -0
  71. package/dist/core/v4/cron/diagnostics.js +104 -0
  72. package/dist/core/v4/cron/graceWindow.js +79 -0
  73. package/dist/core/v4/logger/factory.js +110 -0
  74. package/dist/core/v4/logger/index.js +22 -0
  75. package/dist/core/v4/logger/logger.js +101 -0
  76. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  77. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  78. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  79. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  80. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  81. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  82. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  83. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  84. package/dist/core/v4/platformPaths.js +105 -0
  85. package/dist/core/v4/providerFallback.js +25 -0
  86. package/dist/core/v4/skillLoader.js +21 -5
  87. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  88. package/dist/core/v4/skillMining/extractorPrompt.js +118 -0
  89. package/dist/core/v4/skillMining/proposalBuilder.js +140 -0
  90. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  91. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  92. package/dist/core/v4/subagent/budget.js +76 -0
  93. package/dist/core/v4/subagent/diagnostics.js +22 -0
  94. package/dist/core/v4/subagent/fanout.js +216 -0
  95. package/dist/core/v4/subagent/merger.js +148 -0
  96. package/dist/core/v4/subagent/providerRotation.js +54 -0
  97. package/dist/core/v4/voice/audioStream.js +373 -0
  98. package/dist/core/v4/voice/cliVoice.js +393 -0
  99. package/dist/core/v4/voice/diagnostics.js +66 -0
  100. package/dist/core/v4/voice/ttsStream.js +193 -0
  101. package/dist/core/version.js +1 -1
  102. package/dist/core/visionAnalyze.js +291 -90
  103. package/dist/core/voice/audio.js +61 -5
  104. package/dist/core/voice/audioBackend.js +134 -0
  105. package/dist/core/voice/stt.js +61 -6
  106. package/dist/core/voice/tts.js +19 -3
  107. package/dist/moat/dangerousPatterns.js +1 -1
  108. package/dist/providers/v4/codexResponsesAdapter.js +7 -2
  109. package/dist/providers/v4/errors.js +51 -1
  110. package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
  111. package/dist/tools/v4/index.js +32 -1
  112. package/dist/tools/v4/subagent/subagentFanout.js +190 -0
  113. package/package.json +11 -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
+ }