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
@@ -51,11 +51,23 @@ exports.getSttProviders = getSttProviders;
51
51
  //
52
52
  // If all providers fail: returns { text: '', provider: 'none', error }
53
53
  // — never throws; callers check result.text.
54
+ //
55
+ // Phase v4.1-3 surgical edits:
56
+ // - Cloud providers request `verbose_json` so we receive segment-level
57
+ // `avg_logprob`. The mean is exposed on the result as `confidence`
58
+ // (negative; closer to zero is more confident). Channel adapters
59
+ // (e.g. Telegram voice notes) use this to decide whether to echo
60
+ // a low-confidence transcript back to the user before handing it
61
+ // to the agent.
62
+ // - All `console.*` removed in favour of an injectable `Logger` from
63
+ // `core/v4/logger`. Defaults to a noop logger so callers without a
64
+ // wired logger get silence (REPL-safe). v4.1-1.3a contract.
54
65
  const fs_1 = __importDefault(require("fs"));
55
66
  const path_1 = __importDefault(require("path"));
56
67
  const child_process_1 = require("child_process");
57
68
  const util_1 = require("util");
58
69
  const axios_1 = __importDefault(require("axios"));
70
+ const logger_1 = require("../v4/logger");
59
71
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
60
72
  // ── Internal helpers ──────────────────────────────────────────────────────────
61
73
  const WORKSPACE = path_1.default.join(process.cwd(), 'workspace');
@@ -75,6 +87,29 @@ function resolveAudioPath(opts) {
75
87
  }
76
88
  throw new Error('SttOptions: provide audioFilePath or audioBuffer');
77
89
  }
90
+ /**
91
+ * Compute mean of `avg_logprob` across Whisper segments — Phase v4.1-3.
92
+ * Returns `undefined` when the field is absent (older response shapes,
93
+ * non-verbose_json fallback, or no segments at all). Callers only use
94
+ * this when the value is finite; preserve that invariant here.
95
+ */
96
+ function meanAvgLogprob(payload) {
97
+ const segs = payload?.segments;
98
+ if (!Array.isArray(segs) || segs.length === 0)
99
+ return undefined;
100
+ let sum = 0;
101
+ let count = 0;
102
+ for (const s of segs) {
103
+ const v = s?.avg_logprob;
104
+ if (typeof v === 'number' && Number.isFinite(v)) {
105
+ sum += v;
106
+ count += 1;
107
+ }
108
+ }
109
+ if (count === 0)
110
+ return undefined;
111
+ return sum / count;
112
+ }
78
113
  // ── Provider 1 — Groq Whisper ─────────────────────────────────────────────────
79
114
  async function transcribeGroq(audioPath, opts) {
80
115
  const apiKey = process.env.GROQ_API_KEY;
@@ -88,15 +123,20 @@ async function transcribeGroq(audioPath, opts) {
88
123
  form.append('model', 'whisper-large-v3');
89
124
  if (opts.language)
90
125
  form.append('language', opts.language);
91
- form.append('response_format', 'json');
126
+ // Phase v4.1-3 — verbose_json gives us segment-level `avg_logprob`
127
+ // for confidence scoring on the channel side. Groq mirrors OpenAI's
128
+ // Whisper response shape here.
129
+ form.append('response_format', 'verbose_json');
92
130
  const res = await axios_1.default.post('https://api.groq.com/openai/v1/audio/transcriptions', form, {
93
131
  headers: { ...form.getHeaders(), Authorization: `Bearer ${apiKey}` },
94
132
  timeout,
95
133
  });
134
+ const confidence = meanAvgLogprob(res.data);
96
135
  return {
97
136
  text: (res.data.text ?? '').trim(),
98
137
  provider: 'groq',
99
138
  durationMs: Date.now() - t0,
139
+ ...(typeof confidence === 'number' ? { confidence } : {}),
100
140
  };
101
141
  }
102
142
  // ── Provider 2 — OpenAI Whisper ───────────────────────────────────────────────
@@ -112,15 +152,18 @@ async function transcribeOpenAI(audioPath, opts) {
112
152
  form.append('model', 'whisper-1');
113
153
  if (opts.language)
114
154
  form.append('language', opts.language);
115
- form.append('response_format', 'json');
155
+ // Phase v4.1-3 — same verbose_json switch as Groq for parity.
156
+ form.append('response_format', 'verbose_json');
116
157
  const res = await axios_1.default.post('https://api.openai.com/v1/audio/transcriptions', form, {
117
158
  headers: { ...form.getHeaders(), Authorization: `Bearer ${apiKey}` },
118
159
  timeout,
119
160
  });
161
+ const confidence = meanAvgLogprob(res.data);
120
162
  return {
121
163
  text: (res.data.text ?? '').trim(),
122
164
  provider: 'openai',
123
165
  durationMs: Date.now() - t0,
166
+ ...(typeof confidence === 'number' ? { confidence } : {}),
124
167
  };
125
168
  }
126
169
  // ── Provider 3 — Local Whisper.cpp ────────────────────────────────────────────
@@ -168,6 +211,7 @@ async function transcribe(options) {
168
211
  const t0 = Date.now();
169
212
  let tmpFile = '';
170
213
  const errors = [];
214
+ const log = options.logger ?? (0, logger_1.noopLogger)();
171
215
  try {
172
216
  const audioPath = resolveAudioPath(options);
173
217
  if (!options.audioFilePath && options.audioBuffer)
@@ -175,7 +219,11 @@ async function transcribe(options) {
175
219
  // Provider 1 — Groq
176
220
  try {
177
221
  const r = await transcribeGroq(audioPath, options);
178
- console.log(`[STT] Groq Whisper: "${r.text.slice(0, 60)}" (${r.durationMs}ms)`);
222
+ log.info(`groq whisper transcribed`, {
223
+ snippet: r.text.slice(0, 60),
224
+ durationMs: r.durationMs,
225
+ confidence: r.confidence,
226
+ });
179
227
  return r;
180
228
  }
181
229
  catch (e) {
@@ -184,7 +232,11 @@ async function transcribe(options) {
184
232
  // Provider 2 — OpenAI
185
233
  try {
186
234
  const r = await transcribeOpenAI(audioPath, options);
187
- console.log(`[STT] OpenAI Whisper: "${r.text.slice(0, 60)}" (${r.durationMs}ms)`);
235
+ log.info(`openai whisper transcribed`, {
236
+ snippet: r.text.slice(0, 60),
237
+ durationMs: r.durationMs,
238
+ confidence: r.confidence,
239
+ });
188
240
  return r;
189
241
  }
190
242
  catch (e) {
@@ -193,7 +245,10 @@ async function transcribe(options) {
193
245
  // Provider 3 — Local Whisper.cpp
194
246
  try {
195
247
  const r = await transcribeLocal(audioPath, options);
196
- console.log(`[STT] Local Whisper.cpp: "${r.text.slice(0, 60)}" (${r.durationMs}ms)`);
248
+ log.info(`local whisper transcribed`, {
249
+ snippet: r.text.slice(0, 60),
250
+ durationMs: r.durationMs,
251
+ });
197
252
  return r;
198
253
  }
199
254
  catch (e) {
@@ -201,7 +256,7 @@ async function transcribe(options) {
201
256
  }
202
257
  // All failed
203
258
  const errorMsg = errors.join(' | ');
204
- console.warn(`[STT] All providers failed: ${errorMsg}`);
259
+ log.warn(`all providers failed`, { errors: errorMsg });
205
260
  return { text: '', provider: 'none', durationMs: Date.now() - t0, error: errorMsg };
206
261
  }
207
262
  catch (outer) {
@@ -136,15 +136,27 @@ async function synthesizeEdge(text, opts) {
136
136
  const voice = opts.voice ?? DEFAULT_VOICE;
137
137
  const audioPath = path_1.default.join(WORKSPACE, `tts_edge_${Date.now()}.mp3`);
138
138
  const audioFwd = audioPath.replace(/\\/g, '/');
139
- const escaped = text.replace(/"/g, '\\"').replace(/'/g, "\\'");
140
139
  const timeout = opts.timeoutMs ?? 20000;
140
+ // Phase v4.1-voice-cli (Piece 0) — write the user text to a UTF-8
141
+ // file and have Python read it from there. The old inline-escape
142
+ // path (`text.replace(/"/g,'\\"').replace(/'/g,"\\'")`) was
143
+ // brittle for any text containing both quote styles plus
144
+ // backticks / `${...}` (which break the JS template literal that
145
+ // generates the Python script). Reading from a file removes ALL
146
+ // escaping concerns. JSON.stringify on the file paths + voice
147
+ // produces valid Python string literals (JSON ⊂ Python string syntax).
148
+ const textFile = path_1.default.join(WORKSPACE, `tts_edge_text_${Date.now()}.txt`);
149
+ fs_1.default.writeFileSync(textFile, text, 'utf-8');
150
+ const textFileFwd = textFile.replace(/\\/g, '/');
141
151
  const script = `
142
152
  import asyncio, sys
143
153
  sys.stderr = open('nul', 'w')
144
154
  import edge_tts
145
155
  async def main():
146
- communicate = edge_tts.Communicate("${escaped}", "${voice}")
147
- await communicate.save("${audioFwd}")
156
+ with open(${JSON.stringify(textFileFwd)}, 'r', encoding='utf-8') as f:
157
+ txt = f.read()
158
+ communicate = edge_tts.Communicate(txt, ${JSON.stringify(voice)})
159
+ await communicate.save(${JSON.stringify(audioFwd)})
148
160
  asyncio.run(main())
149
161
  `.trim();
150
162
  const tmpPy = path_1.default.join(WORKSPACE, `tts_edge_gen_${Date.now()}.py`);
@@ -170,6 +182,10 @@ asyncio.run(main())
170
182
  fs_1.default.unlinkSync(tmpPy);
171
183
  }
172
184
  catch { /* ignore */ }
185
+ try {
186
+ fs_1.default.unlinkSync(textFile);
187
+ }
188
+ catch { /* ignore */ }
173
189
  }
174
190
  }
175
191
  // ── Provider 2 — ElevenLabs ───────────────────────────────────────────────────
@@ -52,7 +52,7 @@ exports.DANGEROUS_PATTERNS = [
52
52
  { name: 'kill_all', regex: /\bkill\s+-9\s+-1\b/, tier: 'dangerous', description: 'kill all processes' },
53
53
  { name: 'pkill_force', regex: /\bpkill\s+-9\b/, tier: 'caution', description: 'force kill processes' },
54
54
  { name: 'systemctl_disable', regex: /\bsystemctl\s+(-[^\s]+\s+)*(stop|restart|disable|mask)\b/i, tier: 'caution', description: 'stop/restart system service' },
55
- { name: 'pkill_aiden', regex: /\b(pkill|killall)\b.*\b(aiden|gateway|hermes)\b/i, tier: 'dangerous', description: 'kill aiden/gateway process (self-termination)' },
55
+ { name: 'pkill_aiden', regex: /\b(pkill|killall)\b.*\b(aiden|gateway)\b/i, tier: 'dangerous', description: 'kill aiden/gateway process (self-termination)' },
56
56
  // ── Sensitive write targets ───────────────────────────────────
57
57
  { name: 'write_etc', regex: />\s*\/etc\//, tier: 'dangerous', description: 'overwrite system config' },
58
58
  { name: 'tee_etc', regex: /\btee\b.*\/etc\//, tier: 'dangerous', description: 'overwrite system file via tee' },
@@ -129,14 +129,19 @@ class CodexResponsesAdapter {
129
129
  const body = {
130
130
  model: this.model,
131
131
  input: items,
132
- tool_choice: 'auto',
133
- parallel_tool_calls: true,
134
132
  store: false,
135
133
  };
136
134
  if (instructions)
137
135
  body.instructions = instructions;
136
+ // Phase v4.1.1-oauth-fix Phase 5: `tool_choice` and
137
+ // `parallel_tool_calls` are only meaningful when tools are present.
138
+ // OpenAI Codex returns HTTP 400 (empty body) for `tool_choice: 'auto'`
139
+ // without a `tools` field — surfaced by `aiden doctor --providers`'s
140
+ // no-tools liveness probe.
138
141
  if (input.tools && input.tools.length > 0) {
139
142
  body.tools = input.tools.map(toWireTool);
143
+ body.tool_choice = 'auto';
144
+ body.parallel_tool_calls = true;
140
145
  }
141
146
  if (typeof input.temperature === 'number') {
142
147
  body.temperature = input.temperature;
@@ -16,9 +16,59 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.ProviderRateLimitError = exports.ProviderTimeoutError = exports.ProviderError = void 0;
19
+ exports.formatRawForMessage = formatRawForMessage;
20
+ /**
21
+ * Format a raw response body for inclusion in the user-facing error
22
+ * message. Recognises the OpenAI / Anthropic JSON envelope shape
23
+ * (`{ error: { message: "..." } }`) and falls back to the raw string
24
+ * for plain-text bodies. Returns null when nothing useful is available
25
+ * so callers can omit the ": <detail>" tail entirely.
26
+ *
27
+ * Truncates to 300 chars to keep multi-line responses from blowing
28
+ * up the user's terminal — full body remains on `error.raw` for
29
+ * programmatic consumers / `aiden doctor --providers` deep mode.
30
+ */
31
+ function formatRawForMessage(raw) {
32
+ if (raw === undefined || raw === null)
33
+ return null;
34
+ // OpenAI / Anthropic JSON envelope: { error: { message: "..." } }
35
+ if (typeof raw === 'object') {
36
+ const err = raw.error;
37
+ if (err && typeof err === 'object') {
38
+ const msg = err.message;
39
+ if (typeof msg === 'string' && msg.length > 0) {
40
+ return msg.length > 300 ? `${msg.slice(0, 300)}…` : msg;
41
+ }
42
+ }
43
+ // Some providers put the message at the top level.
44
+ const topMsg = raw.message;
45
+ if (typeof topMsg === 'string' && topMsg.length > 0) {
46
+ return topMsg.length > 300 ? `${topMsg.slice(0, 300)}…` : topMsg;
47
+ }
48
+ return null;
49
+ }
50
+ // Plain string body.
51
+ if (typeof raw === 'string') {
52
+ const trimmed = raw.trim();
53
+ if (trimmed.length === 0)
54
+ return null;
55
+ return trimmed.length > 300 ? `${trimmed.slice(0, 300)}…` : trimmed;
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Compose the final `Error.message` from the short summary and (when
61
+ * available) the parsed/truncated raw response body. The body remains
62
+ * stashed on `ProviderError.raw` either way — this only enriches what
63
+ * users see when the error is rendered.
64
+ */
65
+ function composeMessage(message, raw) {
66
+ const tail = formatRawForMessage(raw);
67
+ return tail ? `${message}: ${tail}` : message;
68
+ }
19
69
  class ProviderError extends Error {
20
70
  constructor(message, providerName, statusCode, raw, retryable = false) {
21
- super(message);
71
+ super(composeMessage(message, raw));
22
72
  this.providerName = providerName;
23
73
  this.statusCode = statusCode;
24
74
  this.raw = raw;
@@ -71,7 +71,12 @@ class OllamaPromptToolsAdapter {
71
71
  const status = response.status;
72
72
  const rawText = await this.safeReadText(response);
73
73
  const retryable = status >= 500 || status === 429;
74
- const err = new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}: ${rawText.slice(0, 500)}`, this.providerName, status, rawText, retryable);
74
+ // Phase v4.1.1-oauth-fix Phase 5: short message only. The raw
75
+ // body flows via the .raw arg and composeMessage in errors.ts
76
+ // appends a truncated summary into the final .message — this
77
+ // file used to inline it, producing duplicated output in
78
+ // `aiden doctor --providers` and anywhere else err.message is logged.
79
+ const err = new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}`, this.providerName, status, rawText, retryable);
75
80
  if (!retryable || attempt >= totalAttempts)
76
81
  throw err;
77
82
  lastError = err;
@@ -149,7 +154,9 @@ class OllamaPromptToolsAdapter {
149
154
  clearTimeout(timer);
150
155
  const status = response.status;
151
156
  const rawText = await this.safeReadText(response);
152
- throw new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}: ${rawText.slice(0, 500)}`, this.providerName, status, rawText, status >= 500);
157
+ // Phase v4.1.1-oauth-fix Phase 5: composeMessage handles body
158
+ // rendering centrally; inlining it here would duplicate.
159
+ throw new errors_1.ProviderError(`Provider ${this.providerName} returned ${status}`, this.providerName, status, rawText, status >= 500);
153
160
  }
154
161
  if (!response.body) {
155
162
  clearTimeout(timer);
@@ -21,7 +21,7 @@
21
21
  * Status: PHASE 8.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.memoryRemoveTool = exports.memoryReplaceTool = exports.memoryAddTool = exports.processWaitTool = exports.processKillTool = exports.processLogReadTool = exports.processListTool = exports.processSpawnTool = exports.executeCodeTool = exports.shellExecTool = exports.naturalEventsTool = exports.nowPlayingTool = exports.systemInfoTool = exports.makeLookupToolSchema = exports.skillManageTool = exports.skillViewTool = exports.skillsListTool = exports.sessionListTool = exports.sessionSearchTool = exports.browserCloseTool = exports.browserScrollTool = exports.browserFillTool = exports.browserTypeTool = exports.browserClickTool = exports.browserNavigateTool = exports.browserGetUrlTool = exports.browserExtractTool = exports.browserScreenshotTool = exports.fileCopyTool = exports.fileMoveTool = exports.fileDeleteTool = exports.filePatchTool = exports.fileWriteTool = exports.fileListTool = exports.fileReadTool = exports.deepResearchTool = exports.webPageTool = exports.webFetchTool = exports.webSearchTool = void 0;
24
+ exports.memoryRemoveTool = exports.memoryReplaceTool = exports.memoryAddTool = exports.processWaitTool = exports.processKillTool = exports.processLogReadTool = exports.processListTool = exports.processSpawnTool = exports.executeCodeTool = exports.shellExecTool = exports.naturalEventsTool = exports.nowPlayingTool = exports.systemInfoTool = exports.makeLookupToolSchema = exports.skillManageTool = exports.skillViewTool = exports.skillsListTool = exports.sessionListTool = exports.sessionSearchTool = exports.browserCloseTool = exports.browserScrollTool = exports.browserFillTool = exports.browserTypeTool = exports.browserClickTool = exports.browserNavigateTool = exports.browserGetUrlTool = exports.browserExtractTool = exports.browserScreenshotTool = exports.fileCopyTool = exports.fileMoveTool = exports.fileDeleteTool = exports.filePatchTool = exports.fileWriteTool = exports.fileListTool = exports.fileReadTool = exports.deepResearchTool = exports.webPageTool = exports.webFetchTool = exports.webSearchTool = exports.makeSubagentFanoutTool = void 0;
25
25
  exports.registerReadOnlyTools = registerReadOnlyTools;
26
26
  exports.registerWriteTools = registerWriteTools;
27
27
  exports.registerAllTools = registerAllTools;
@@ -66,6 +66,7 @@ const processWait_1 = require("./process/processWait");
66
66
  const memoryAdd_1 = require("./memory/memoryAdd");
67
67
  const memoryReplace_1 = require("./memory/memoryReplace");
68
68
  const memoryRemove_1 = require("./memory/memoryRemove");
69
+ const subagentFanout_1 = require("./subagent/subagentFanout");
69
70
  /**
70
71
  * Register every read-only tool into `registry`. The
71
72
  * `lookup_tool_schema` tool needs a registry reference, so it's
@@ -99,6 +100,34 @@ function registerReadOnlyTools(registry) {
99
100
  registry.register(nowPlaying_1.nowPlayingTool);
100
101
  registry.register(naturalEvents_1.naturalEventsTool);
101
102
  registry.register((0, lookupToolSchema_1.makeLookupToolSchema)(registry));
103
+ // Phase v4.1-subagent — register a stub for subagent_fanout so its
104
+ // schema is visible to the agent loop, the MCP server, and the
105
+ // /tools slash command BEFORE the runtime resolves provider /
106
+ // adapter / agent dependencies. The full runtime calls
107
+ // `registry.register(makeSubagentFanoutTool({...real opts}))` to
108
+ // replace this stub once `buildAgentRuntime` has those handles.
109
+ // Until then, calling the stub returns a clear "not wired" error
110
+ // rather than crashing.
111
+ registry.register(makeSubagentFanoutStub());
112
+ }
113
+ /** Stub used until the runtime wires real provider / adapter / agent
114
+ * dependencies. Returns the SAME schema as the real tool so MCP and
115
+ * /tools see a consistent surface. */
116
+ function makeSubagentFanoutStub() {
117
+ return (0, subagentFanout_1.makeSubagentFanoutTool)({
118
+ resolveProviders: () => [],
119
+ resolveActiveModel: () => ({ providerId: 'unset', modelId: 'unset' }),
120
+ aggregatorAdapter: {
121
+ apiMode: 'chat_completions',
122
+ async call() {
123
+ throw new Error('subagent_fanout: tool not wired — runtime did not replace the stub. ' +
124
+ 'Call registry.register(makeSubagentFanoutTool({...})) after buildAgentRuntime.');
125
+ },
126
+ },
127
+ runChild: async () => {
128
+ throw new Error('subagent_fanout: tool not wired — runtime did not replace the stub.');
129
+ },
130
+ });
102
131
  }
103
132
  /**
104
133
  * Register every Phase 8 write/execute tool. Phase 9 will gate
@@ -138,6 +167,8 @@ function registerAllTools(registry) {
138
167
  registerReadOnlyTools(registry);
139
168
  registerWriteTools(registry);
140
169
  }
170
+ var subagentFanout_2 = require("./subagent/subagentFanout");
171
+ Object.defineProperty(exports, "makeSubagentFanoutTool", { enumerable: true, get: function () { return subagentFanout_2.makeSubagentFanoutTool; } });
141
172
  var webSearch_2 = require("./web/webSearch");
142
173
  Object.defineProperty(exports, "webSearchTool", { enumerable: true, get: function () { return webSearch_2.webSearchTool; } });
143
174
  var webFetch_2 = require("./web/webFetch");
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * tools/v4/subagent/subagentFanout.ts — `subagent_fanout` wrapper.
10
+ *
11
+ * Phase v4.1-subagent. Spawns N parallel agent instances against the
12
+ * same problem (or a partition of it), then merges results via the
13
+ * chosen strategy. The orchestrator lives at
14
+ * `core/v4/subagent/fanout.ts`; this file is the agent-callable
15
+ * adapter that:
16
+ *
17
+ * 1. Validates the LLM's call args against the schema.
18
+ * 2. Builds a per-child runner (closure over the parent runtime)
19
+ * that wraps an AidenAgent run.
20
+ * 3. Filters mutating tools out of each child's schema array
21
+ * unless `AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1`.
22
+ * 4. Returns the merged output + raw N results + diagnostics.
23
+ *
24
+ * The factory pattern (`makeSubagentFanoutTool`) mirrors
25
+ * `lookup_tool_schema` — the runtime constructs it with a closure
26
+ * over registry / providers / paths that the schema can't carry.
27
+ *
28
+ * Tool category is `network` not `write` — the tool itself doesn't
29
+ * touch disk; it only spends LLM tokens. That keeps it default-
30
+ * exposed in MCP under the read-only env (mutates: false).
31
+ *
32
+ * The description bakes a hard-learned lesson from prior multi-agent
33
+ * systems: "Self-reports are not verified facts" — the parent must
34
+ * verify any side-effects children report rather than trust the
35
+ * summary. Children's tool calls are executed in isolated contexts;
36
+ * a child claiming "wrote file X" or "ran command Y" must be
37
+ * verified by the parent before the parent acts on that claim.
38
+ */
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.makeSubagentFanoutTool = makeSubagentFanoutTool;
41
+ const factory_1 = require("../../../core/v4/logger/factory");
42
+ const fanout_1 = require("../../../core/v4/subagent/fanout");
43
+ const merger_1 = require("../../../core/v4/subagent/merger");
44
+ const SCHEMA_DESC = 'Spawn N parallel agent children against the same problem (ensemble) or a partitioned task list, ' +
45
+ 'then merge results via the chosen strategy. Use this for multi-perspective research, ' +
46
+ 'provider-diverse fact-checking, or analyzing N independent inputs in parallel. ' +
47
+ 'IMPORTANT: self-reports from children are not verified facts — if a child claims it ' +
48
+ 'wrote a file, ran a command, or completed a side-effect, you (the parent) MUST verify ' +
49
+ 'independently before trusting that claim.';
50
+ function makeSubagentFanoutTool(factory) {
51
+ return {
52
+ schema: {
53
+ name: 'subagent_fanout',
54
+ description: SCHEMA_DESC,
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ mode: {
59
+ type: 'string',
60
+ description: "'partition' = each child gets a different goal from `tasks`. " +
61
+ "'ensemble' = every child gets the same `query`.",
62
+ enum: ['partition', 'ensemble'],
63
+ },
64
+ n: {
65
+ type: 'number',
66
+ description: 'Number of children to spawn. Default 3, hard cap 5. ' +
67
+ 'Higher N hits provider RPM limits and inflates tail latency.',
68
+ },
69
+ query: {
70
+ type: 'string',
71
+ description: 'Same input given to every child (ensemble mode only).',
72
+ },
73
+ tasks: {
74
+ type: 'array',
75
+ description: 'Per-child task list (partition mode only). Length must equal n.',
76
+ // Schema mirrors PartitionTask interface in
77
+ // core/v4/subagent/fanout.ts:70-75. If you change one, change
78
+ // the other. OpenAI Codex backend strictly validates schemas
79
+ // and rejects `type: "array"` declarations missing `items`,
80
+ // so the inner shape must be explicit here.
81
+ items: {
82
+ type: 'object',
83
+ description: 'One unit of work for a partition-mode child.',
84
+ properties: {
85
+ goal: {
86
+ type: 'string',
87
+ description: 'The task this child should accomplish.',
88
+ },
89
+ context: {
90
+ type: 'string',
91
+ description: 'Optional shared context for the child.',
92
+ },
93
+ role: {
94
+ type: 'string',
95
+ description: 'Optional role tag, diagnostic only.',
96
+ },
97
+ },
98
+ required: ['goal'],
99
+ },
100
+ },
101
+ merge: {
102
+ type: 'string',
103
+ description: "'all' = return raw N results, no aggregator (FREE). " +
104
+ "'vote' = LLM judge picks one verbatim (+1 call). " +
105
+ "'pick-best' = LLM judge picks one with reasoning (+1 call). " +
106
+ "'combine' = LLM synthesizes one unified answer (+1 call).",
107
+ enum: ['all', 'vote', 'pick-best', 'combine'],
108
+ },
109
+ timeoutMs: {
110
+ type: 'number',
111
+ description: 'Per-child wall-clock timeout (ms). Default 90000. ' +
112
+ 'Outer wall-clock cap is 5x this value.',
113
+ },
114
+ },
115
+ required: ['mode'],
116
+ },
117
+ },
118
+ // mutates: false because the tool itself only spends tokens — disk /
119
+ // process side-effects, if any, happen INSIDE child agents whose
120
+ // toolsets are filtered to read-only by default. This keeps the
121
+ // tool default-exposed in MCP under the read-only env.
122
+ category: 'network',
123
+ mutates: false,
124
+ toolset: 'subagent',
125
+ async execute(args, _ctx) {
126
+ const logger = factory.logger ?? (0, factory_1.noopLogger)();
127
+ // ── Coerce args ────────────────────────────────────────────
128
+ const mode = (args.mode === 'partition' || args.mode === 'ensemble')
129
+ ? args.mode
130
+ : null;
131
+ if (!mode) {
132
+ return {
133
+ success: false,
134
+ error: "subagent_fanout: 'mode' must be 'partition' or 'ensemble'",
135
+ };
136
+ }
137
+ const n = typeof args.n === 'number' && Number.isInteger(args.n)
138
+ ? args.n
139
+ : 3;
140
+ const merge = (args.merge === 'all' || args.merge === 'vote'
141
+ || args.merge === 'pick-best' || args.merge === 'combine')
142
+ ? args.merge
143
+ : 'combine';
144
+ const query = typeof args.query === 'string' ? args.query : undefined;
145
+ const tasks = Array.isArray(args.tasks)
146
+ ? args.tasks
147
+ : undefined;
148
+ const timeoutMs = typeof args.timeoutMs === 'number'
149
+ && args.timeoutMs > 0
150
+ ? args.timeoutMs
151
+ : undefined;
152
+ // ── Resolve providers + aggregator at call time ───────────
153
+ const providers = factory.resolveProviders();
154
+ if (providers.length === 0) {
155
+ return {
156
+ success: false,
157
+ error: 'subagent_fanout: no providers configured — run `aiden setup` first',
158
+ };
159
+ }
160
+ const aggOverride = (0, merger_1.resolveAggregatorOverride)();
161
+ const aggregatorModel = aggOverride ?? factory.resolveActiveModel();
162
+ const fanoutOpts = {
163
+ mode,
164
+ query,
165
+ tasks,
166
+ n,
167
+ merge,
168
+ providers,
169
+ runChild: factory.runChild,
170
+ aggregatorAdapter: factory.aggregatorAdapter,
171
+ aggregatorModel,
172
+ timeoutMs,
173
+ logger,
174
+ };
175
+ try {
176
+ const result = await (0, fanout_1.runFanout)(fanoutOpts);
177
+ return {
178
+ success: true,
179
+ merged: result.merged,
180
+ results: result.results,
181
+ diagnostics: result.diagnostics,
182
+ };
183
+ }
184
+ catch (err) {
185
+ const message = err instanceof Error ? err.message : String(err);
186
+ return { success: false, error: message };
187
+ }
188
+ },
189
+ };
190
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-runtime",
3
- "version": "4.0.2",
3
+ "version": "4.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -224,7 +224,7 @@
224
224
  "dependencies": {
225
225
  "@inquirer/core": "^11.1.9",
226
226
  "@inquirer/prompts": "^8.4.2",
227
- "@modelcontextprotocol/sdk": "^1.27.1",
227
+ "@modelcontextprotocol/sdk": "^1.29.0",
228
228
  "@slack/bolt": "^4.6.0",
229
229
  "@slack/web-api": "^7.15.0",
230
230
  "@types/archiver": "^7.0.0",
@@ -232,6 +232,7 @@
232
232
  "@types/sql.js": "^1.4.9",
233
233
  "@types/twilio": "^3.19.2",
234
234
  "@types/ws": "^8.18.1",
235
+ "ansi-escapes": "^4.3.2",
235
236
  "archiver": "^7.0.1",
236
237
  "axios": "^1.15.2",
237
238
  "bcrypt": "^6.0.0",
@@ -256,15 +257,18 @@
256
257
  "marked": "^15.0.12",
257
258
  "marked-terminal": "^7.3.0",
258
259
  "multer": "^2.1.1",
260
+ "node-telegram-bot-api": "0.67.0",
259
261
  "nodemailer": "^8.0.3",
260
262
  "open": "^11.0.0",
261
263
  "ora": "^9.3.0",
262
264
  "pdf-parse": "^1.1.1",
263
265
  "playwright": "^1.58.2",
266
+ "proper-lockfile": "^4.1.2",
264
267
  "puppeteer": "^24.39.1",
265
268
  "qrcode-terminal": "^0.12.0",
266
269
  "screenshot-desktop": "^1.15.3",
267
270
  "sql.js": "^1.14.1",
271
+ "string-width": "^4.2.3",
268
272
  "stripe": "^20.4.1",
269
273
  "tar-stream": "^3.1.8",
270
274
  "twilio": "^5.13.1",
@@ -272,6 +276,10 @@
272
276
  "whatsapp-web.js": "^1.26.0",
273
277
  "ws": "^8.20.0"
274
278
  },
279
+ "optionalDependencies": {
280
+ "decibri": "*",
281
+ "node-record-lpcm16": "*"
282
+ },
275
283
  "overrides": {
276
284
  "basic-ftp": "^5.3.1",
277
285
  "ip-address": "^10.1.1",
@@ -287,6 +295,7 @@
287
295
  "@types/js-yaml": "^4.0.9",
288
296
  "@types/multer": "^2.0.0",
289
297
  "@types/node": "^25.3.0",
298
+ "@types/node-telegram-bot-api": "0.64.14",
290
299
  "@types/nodemailer": "^7.0.11",
291
300
  "@types/pdf-parse": "^1.1.4",
292
301
  "@types/qrcode-terminal": "^0.12.2",