aiden-runtime 3.18.0 → 3.19.4

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.
@@ -37,6 +37,7 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  };
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.SEQUENTIAL_ONLY = exports.PARALLEL_SAFE = exports.NO_RETRY_TOOLS = exports.VALID_TOOLS = exports.ALLOWED_TOOLS = void 0;
40
41
  exports.interruptCurrentCall = interruptCurrentCall;
41
42
  exports.setStatusEmitter = setStatusEmitter;
42
43
  exports.getBudgetState = getBudgetState;
@@ -44,6 +45,7 @@ exports.surfaceRelevantMemories = surfaceRelevantMemories;
44
45
  exports.resolveTemplates = resolveTemplates;
45
46
  exports.streamOpenAIResponse = streamOpenAIResponse;
46
47
  exports.streamGeminiResponse = streamGeminiResponse;
48
+ exports.resolveStreamingUrl = resolveStreamingUrl;
47
49
  exports.planWithLLM = planWithLLM;
48
50
  exports.validatePlan = validatePlan;
49
51
  exports.buildDependencyGroups = buildDependencyGroups;
@@ -72,6 +74,8 @@ const knowledgeBase_1 = require("./knowledgeBase");
72
74
  const skillTeacher_1 = require("./skillTeacher");
73
75
  const growthEngine_1 = require("./growthEngine");
74
76
  const aidenPersonality_1 = require("./aidenPersonality");
77
+ const protectedContext_1 = require("./protectedContext");
78
+ const contextHandoff_1 = require("./contextHandoff");
75
79
  const auditTrail_1 = require("./auditTrail");
76
80
  const mcpClient_1 = require("./mcpClient");
77
81
  const memoryRecall_1 = require("./memoryRecall");
@@ -87,14 +91,19 @@ const workflowTracker_1 = require("./workflowTracker");
87
91
  const parallelExecutor_1 = require("./parallelExecutor");
88
92
  const messageValidator_1 = require("./messageValidator");
89
93
  const toolNameRepair_1 = require("./toolNameRepair");
90
- const slashAsTool_1 = require("./slashAsTool");
94
+ // SLASH_MIRROR_TOOL_NAMES import removed in Commit 4 — slash mirrors route
95
+ // through slashAsTool.ts injection path, not the planner's allowed-tool list.
91
96
  const planResponseRepair_1 = require("./planResponseRepair");
97
+ const actionVerbDetector_1 = require("./actionVerbDetector");
98
+ const diagnosticError_1 = require("./diagnosticError");
92
99
  const nodeFs = __importStar(require("fs"));
93
100
  const nodePath = __importStar(require("path"));
94
101
  const nodeOs = __importStar(require("os"));
95
102
  // ── Pre-compact threshold ──────────────────────────────────────
96
103
  // Fire pre_compact hook when history has this many messages
97
104
  const COMPACT_THRESHOLD = 40;
105
+ // Per-session soul hash for Option-B protected-context injection (responder).
106
+ const soulHashBySession = new Map();
98
107
  // ── Interrupt / stop state ─────────────────────────────────────
99
108
  let currentAbortController = null;
100
109
  let executionInterrupted = false;
@@ -397,6 +406,7 @@ const OPENAI_COMPAT_ENDPOINTS = {
397
406
  nvidia: 'https://integrate.api.nvidia.com/v1/chat/completions',
398
407
  github: 'https://models.inference.ai.azure.com/v1/chat/completions',
399
408
  boa: 'https://api.bayofassets.com/v1/chat/completions',
409
+ mistral: 'https://api.mistral.ai/v1/chat/completions',
400
410
  };
401
411
  function buildHeaders(providerName, apiKey) {
402
412
  const headers = {
@@ -409,6 +419,56 @@ function buildHeaders(providerName, apiKey) {
409
419
  }
410
420
  return headers;
411
421
  }
422
+ function extractChatMessageContent(content) {
423
+ if (typeof content === 'string')
424
+ return content;
425
+ if (!Array.isArray(content))
426
+ return '';
427
+ return content
428
+ .map((part) => {
429
+ if (typeof part === 'string')
430
+ return part;
431
+ if (part && typeof part === 'object' && 'text' in part) {
432
+ const text = part.text;
433
+ return typeof text === 'string' ? text : '';
434
+ }
435
+ return '';
436
+ })
437
+ .join('');
438
+ }
439
+ /**
440
+ * C9b: Resolve streaming URL for any provider — custom or known.
441
+ *
442
+ * Custom providers look up baseUrl from config; known providers
443
+ * use OPENAI_COMPAT_ENDPOINTS; unknown falls back to groq.
444
+ *
445
+ * Note: when multiple custom providers share the same API key
446
+ * (e.g. together-1 and together-deepseek both using
447
+ * TOGETHER_API_KEY), the first matching enabled entry wins.
448
+ * Consumers should not rely on which specific entry resolves
449
+ * if keys overlap.
450
+ */
451
+ function resolveStreamingUrl(providerName, apiKey) {
452
+ if (OPENAI_COMPAT_ENDPOINTS[providerName])
453
+ return OPENAI_COMPAT_ENDPOINTS[providerName];
454
+ if (providerName === 'custom') {
455
+ const cfg = (0, index_1.loadConfig)();
456
+ const fromCustom = cfg.customProviders?.find((c) => c.enabled && c.apiKey === apiKey)?.baseUrl;
457
+ if (fromCustom)
458
+ return fromCustom;
459
+ const apiEntry = (cfg.providers?.apis ?? []).find((a) => {
460
+ if (a.provider !== 'custom' || !a.enabled || !a.baseUrl)
461
+ return false;
462
+ const resolved = a.key?.startsWith('env:')
463
+ ? (process.env[a.key.replace('env:', '')] || '')
464
+ : a.key;
465
+ return resolved === apiKey;
466
+ });
467
+ if (apiEntry?.baseUrl)
468
+ return apiEntry.baseUrl;
469
+ }
470
+ return OPENAI_COMPAT_ENDPOINTS.groq; // last resort
471
+ }
412
472
  // ── Phase inference from tool steps ───────────────────────────
413
473
  // Groups consecutive steps of the same capability type into phases.
414
474
  function inferPhasesFromSteps(steps) {
@@ -570,7 +630,16 @@ async function racePlannerAPIs(promptText, topN = 2) {
570
630
  if (!a.enabled || a.rateLimited)
571
631
  continue;
572
632
  const k = a.key.startsWith('env:') ? (process.env[a.key.replace('env:', '')] || '') : a.key;
573
- if (!k || !OPENAI_COMPAT_ENDPOINTS[a.provider])
633
+ if (!k)
634
+ continue;
635
+ if (a.provider === 'custom') {
636
+ // providers.apis entries with provider:'custom' supply their own baseUrl
637
+ if (!a.baseUrl)
638
+ continue;
639
+ candidates.push({ provider: 'custom', model: a.model, key: k, url: a.baseUrl, tier: a.tier ?? 50 });
640
+ continue;
641
+ }
642
+ if (!OPENAI_COMPAT_ENDPOINTS[a.provider])
574
643
  continue;
575
644
  candidates.push({ provider: a.provider, model: a.model, key: k, url: OPENAI_COMPAT_ENDPOINTS[a.provider], tier: a.tier ?? 50 });
576
645
  }
@@ -593,7 +662,7 @@ async function racePlannerAPIs(promptText, topN = 2) {
593
662
  if (!r.ok)
594
663
  throw new Error(`${entry.provider} ${r.status}`);
595
664
  const d = await r.json();
596
- const text = d?.choices?.[0]?.message?.content || '';
665
+ const text = extractChatMessageContent(d?.choices?.[0]?.message?.content);
597
666
  if (!text.trim() || !text.includes('{'))
598
667
  throw new Error('no JSON');
599
668
  return text;
@@ -626,14 +695,24 @@ const COMPACTION_PROTECTED = [
626
695
  async function rebuildContextAfterCompaction(contextHistory) {
627
696
  const workspaceDir = nodePath.join(process.cwd(), 'workspace');
628
697
  const protectedContent = [];
629
- // Read all protected files
698
+ // Use hash-cached manager — no previousHash so SOUL always injects in full.
699
+ const _pctx = protectedContext_1.protectedContextManager.getProtectedContext();
700
+ const _pctxBlock = (0, contextHandoff_1.buildProtectedContextBlock)(_pctx, undefined, 'compaction');
701
+ if (_pctxBlock)
702
+ protectedContent.push(_pctxBlock);
703
+ // Legacy per-file entries for any COMPACTION_PROTECTED files not covered above.
704
+ // (instincts.json is not in protectedContextManager — still read directly.)
630
705
  for (const filename of COMPACTION_PROTECTED) {
631
706
  try {
632
707
  const filepath = nodePath.join(workspaceDir, filename);
633
708
  if (nodeFs.existsSync(filepath)) {
634
709
  const content = nodeFs.readFileSync(filepath, 'utf-8');
635
710
  if (content.trim()) {
636
- protectedContent.push(`## ${filename}\n${content.trim()}`);
711
+ // Skip the 5 files already in the protected block to avoid duplication.
712
+ const skip = ['SOUL.md', 'USER.md', 'GOALS.md', 'STANDING_ORDERS.md', 'LESSONS.md'];
713
+ if (!skip.includes(filename)) {
714
+ protectedContent.push(`## ${filename}\n${content.trim()}`);
715
+ }
637
716
  }
638
717
  }
639
718
  }
@@ -669,6 +748,10 @@ async function rebuildContextAfterCompaction(contextHistory) {
669
748
  };
670
749
  return [protectedMessage, ...contextHistory];
671
750
  }
751
+ // ── v3.19 Phase 1 Commit 4: derived from TOOL_REGISTRY — literal deleted ──────
752
+ // Slash-mirror tools (status, analytics, etc.) are intentionally excluded here;
753
+ // they route through the slashAsTool.ts injection path, not the planner.
754
+ exports.ALLOWED_TOOLS = (0, toolRegistry_1.registryAllowedTools)();
672
755
  // ── STEP 1: planWithLLM ────────────────────────────────────────
673
756
  async function planWithLLM(message, history, apiKey, model, provider, memoryContext) {
674
757
  // ── Pre-compact hook — fire at multiples of COMPACT_THRESHOLD ─
@@ -709,33 +792,19 @@ async function planWithLLM(message, history, apiKey, model, provider, memoryCont
709
792
  console.warn(`[Recipe] Execution failed for ${recipeMatch.recipe.name}: ${err} — falling through to LLM planner`);
710
793
  }
711
794
  }
712
- const ALLOWED_TOOLS = [
713
- 'web_search', 'fetch_page', 'open_browser', 'browser_extract',
714
- 'browser_click', 'browser_type', 'browser_screenshot', 'browser_scroll', 'browser_get_url',
715
- 'file_write', 'file_read',
716
- 'file_list', 'shell_exec', 'run_python', 'run_node',
717
- 'system_info', 'notify', 'deep_research', 'get_stocks',
718
- 'get_market_data', 'get_company_info', 'social_research',
719
- 'mouse_move', 'mouse_click', 'keyboard_type', 'keyboard_press',
720
- 'screenshot', 'screen_read', 'vision_loop', 'wait',
721
- 'code_interpreter_python', 'code_interpreter_node',
722
- 'clipboard_read', 'clipboard_write', 'window_list', 'window_focus',
723
- 'app_launch', 'app_close', 'system_volume',
724
- 'watch_folder', 'watch_folder_list',
725
- 'send_file_local', 'receive_file_local',
726
- 'get_briefing',
727
- 'respond',
728
- 'clarify', 'todo', 'cronjob', 'vision_analyze',
729
- 'voice_speak', 'voice_transcribe', 'voice_clone', 'voice_design',
730
- 'lookup_skill', 'lookup_tool_schema',
731
- 'spawn', 'spawn_subagent', 'swarm',
732
- ...slashAsTool_1.SLASH_MIRROR_TOOL_NAMES,
733
- ];
734
795
  // Sprint 13: append discovered MCP tools
735
796
  const mcpToolNames = mcpClient_1.mcpClient.getAllCachedTools().map(t => t.name);
736
797
  const allTools = mcpToolNames.length > 0
737
- ? [...ALLOWED_TOOLS, ...mcpToolNames]
738
- : ALLOWED_TOOLS;
798
+ ? [...exports.ALLOWED_TOOLS, ...mcpToolNames]
799
+ : exports.ALLOWED_TOOLS;
800
+ // Instant dispatch: deterministic single-tool plans that don't need the LLM planner
801
+ // TODO(v3.20): TEMPORARY — llama-3.3-70b ignores prompt rules and picks run_powershell for media
802
+ // queries even when now_playing is listed and flagged. Proper fix: redesign planner prompt so
803
+ // real-time state tools are reliably preferred. See docs/v3.20-candidates.md.
804
+ if (/\b(what|which).*(music|song|track|artist|playing)|now.?playing|currently playing|what('?s| is) (on|playing)/i.test(message)) {
805
+ console.log('[Planner] instant-dispatch → now_playing');
806
+ return { goal: message, requires_execution: true, plan: [{ step: 1, tool: 'now_playing', input: {}, description: 'Get currently playing media' }], phases: [] };
807
+ }
739
808
  // Dynamic tool loading — filter to relevant tools per task category
740
809
  // Reduces planner prompt from ~15K to ~3-5K tokens without losing capability.
741
810
  // Validation (line ~898) still uses full allTools — filtering is prompt-only.
@@ -823,12 +892,16 @@ SYSTEM CONTEXT — use these exact values for all file paths:
823
892
  IMPORTANT: NEVER use "C:\\Users\\Aiden" — "Aiden" is the AI assistant's name, NOT the Windows username. Always use "${_sysUsername}" as the username in any path.
824
893
 
825
894
  CRITICAL RULES:
895
+ 0. LIVE STATE OVERRIDE (takes priority over all other rules): queries about current music/media/song/track → requires_execution: true, tool: now_playing (no params). You CANNOT know this from training data. Never answer "I'll respond directly" for these.
896
+ 0b. MEMORY OPERATIONS (highest priority after rule 0): When the user says "remember X", "track X", "note X", "store X", "keep track of X", or any variant → requires_execution: true, tool: memory_store({ fact: "<the thing to remember>" }). When the user says "forget X", "remove X from memory", "delete X from memory" → requires_execution: true, tool: memory_forget({ fact: "<keyword to match>" }). NEVER use file_write or file_read for memory intents. memory_store/memory_forget write to Aiden's internal persistent memory (workspace/memory/records.jsonl). file_write is for user-visible files only.
826
897
  1. If the answer is in your training data (capitals, definitions, facts, opinions, advice) → requires_execution: false
827
898
  2. ONLY use tools when you need: live data, file operations, running code, or computer control
899
+ Live data includes: current music, system state, time, weather, stock prices — these are NEVER in training data
828
900
  3. AVAILABLE TOOLS (use ONLY these — name: one-liner):
829
901
  ${plannerTools.map(t => ` ${t}: ${toolRegistry_1.TOOL_NAMES_ONLY[t] ?? ''}`).join('\n')}
830
902
  For full parameter schema: call lookup_tool_schema({ toolName: "name" })
831
- Tier-0 (no lookup needed): web_search, notify, lookup_skill, lookup_tool_schema, schedule_reminder, file_read, file_write, respond
903
+ Tier-0 (no lookup needed): web_search, notify, lookup_skill, lookup_tool_schema, schedule_reminder, file_read, file_write, respond, now_playing
904
+ Media rule: what is playing / current song / music → now_playing (zero params). NEVER use run_powershell for media state.
832
905
  4. DO NOT invent tools like "identify_top_3", "generate_report", "analyze" — these don't exist
833
906
  5. Processing/analysis happens in your response — NOT as a tool step
834
907
  6. NEVER use placeholders like "{{result}}" or "{output}" — steps must have real concrete inputs
@@ -1290,16 +1363,18 @@ Output ONLY valid JSON, nothing else:`;
1290
1363
  }
1291
1364
  }
1292
1365
  if (!parsed) {
1293
- console.warn('[Planner] All LLM attempts failed respond fallback');
1294
- return {
1295
- goal: message,
1366
+ // Don't return early let FORCE_RESPOND_TEST hook and PlannerGuard process the fallback plan
1367
+ console.warn('[Planner] All LLM attempts failed — respond fallback (going through guard)');
1368
+ parsed = {
1369
+ plan: [{ step: 1, tool: 'respond', input: { message: (0, diagnosticError_1.buildDiagnostic)({ tool: 'planner', error: 'All LLM attempts failed', retries: 3, suggestion: 'Provider chain may be rate-limited. Try again in 1–2 minutes or rephrase your request.' }) }, description: 'Fallback response' }],
1296
1370
  requires_execution: true,
1297
- plan: [{ step: 1, tool: 'respond', input: { message: "I'm not sure how to help with that right now. Could you rephrase your request?" }, description: 'Fallback response' }],
1298
- phases: [],
1371
+ goal: message,
1299
1372
  };
1300
1373
  }
1301
- // Guard against null/empty plan object
1302
- if (!parsed.plan && !parsed.steps) {
1374
+ // Guard against null/empty plan object — direct_response path bypasses guard (no action tools involved)
1375
+ // C10: But NOT for action intents — "read X", "delete X", etc. must flow through
1376
+ // PlannerGuard and respondWithResults so C6 CRITICAL RULES can fire.
1377
+ if (!parsed.plan && !parsed.steps && !(0, actionVerbDetector_1.isActionIntent)(message)) {
1303
1378
  return {
1304
1379
  goal: message,
1305
1380
  requires_execution: false,
@@ -1393,30 +1468,97 @@ Output ONLY valid JSON, nothing else:`;
1393
1468
  console.warn(`[Planner] Retry failed: ${e.message}`);
1394
1469
  }
1395
1470
  }
1471
+ // ── PlannerGuard: reject respond-only plans for action intents ──────────
1472
+ const isRespondOnly = candidatePlan.plan.length === 1 && candidatePlan.plan[0].tool === 'respond';
1473
+ if (isRespondOnly && (0, actionVerbDetector_1.isActionIntent)(message)) {
1474
+ const verb = (0, actionVerbDetector_1.detectActionVerb)(message);
1475
+ process.stderr.write(`[PlannerGuard] rejected respond-only plan for action intent: verb='${verb}' message='${message.slice(0, 60)}'\n`);
1476
+ const guardRetryMessages = [
1477
+ ...messages,
1478
+ { role: 'assistant', content: JSON.stringify({ plan: candidatePlan.plan }).slice(0, 300) },
1479
+ {
1480
+ role: 'user',
1481
+ content: `PLAN REJECTED: User intent is action (${verb}). You returned respond-only. Generate a plan with concrete tool calls.`,
1482
+ },
1483
+ ];
1484
+ try {
1485
+ const guardRetryRaw = await callLLM(guardRetryMessages.map(m => `${m.role}: ${m.content}`).join('\n'), curApiKey, curModel, curProvider);
1486
+ const guardMatch = guardRetryRaw.replace(/```json\s*/g, '').replace(/```\s*/g, '').match(/\{[\s\S]*\}/);
1487
+ if (!guardMatch) {
1488
+ process.stderr.write(`[PlannerGuard] retry returned no JSON (providers exhausted) for verb='${verb}'\n`);
1489
+ candidatePlan.plan = [];
1490
+ candidatePlan.requires_execution = false;
1491
+ candidatePlan.direct_response = (0, diagnosticError_1.buildDiagnostic)({
1492
+ tool: 'planner',
1493
+ error: 'Could not generate tool plan for action intent',
1494
+ retries: 1,
1495
+ suggestion: 'Provider chain may be rate-limited. Try again in 1–2 minutes or use a more specific instruction.',
1496
+ });
1497
+ }
1498
+ if (guardMatch) {
1499
+ const guardParsed = JSON.parse(guardMatch[0]);
1500
+ const guardRawPlan = (guardParsed.plan || guardParsed.steps || []);
1501
+ const guardValid = guardRawPlan.filter((s) => allTools.includes(s.tool));
1502
+ const guardNorm = guardValid.map((s, idx) => ({
1503
+ step: s.step ?? (idx + 1),
1504
+ tool: s.tool || '',
1505
+ input: s.input || s.args || {},
1506
+ description: s.description || '',
1507
+ }));
1508
+ const guardOrdered = fixStepOrdering(guardNorm);
1509
+ const stillRespondOnly = guardOrdered.length === 1 && guardOrdered[0].tool === 'respond';
1510
+ if (guardOrdered.length > 0 && !stillRespondOnly) {
1511
+ candidatePlan.plan = guardOrdered;
1512
+ candidatePlan.requires_execution = true;
1513
+ process.stderr.write(`[PlannerGuard] retry succeeded: ${guardOrdered.length} tool step(s) for verb='${verb}'\n`);
1514
+ }
1515
+ else {
1516
+ process.stderr.write(`[PlannerGuard] retry still respond-only — emitting diagnostic for verb='${verb}'\n`);
1517
+ candidatePlan.plan = [];
1518
+ candidatePlan.requires_execution = false;
1519
+ candidatePlan.direct_response =
1520
+ `Planner failed to emit tool call for action intent after retry. User asked: '${message}'`;
1521
+ }
1522
+ }
1523
+ }
1524
+ catch (e) {
1525
+ process.stderr.write(`[PlannerGuard] retry threw: ${e.message}\n`);
1526
+ }
1527
+ }
1528
+ // ── MemoryGuard: override wrong-tool plans for memory intents ──────────────
1529
+ // If the user said "remember/track/note/store X" but the planner chose a tool
1530
+ // other than memory_store (e.g. file_write), force a memory_store plan.
1531
+ // C11: Also handles forget intents → force memory_forget.
1532
+ if ((0, actionVerbDetector_1.isMemoryIntent)(message)) {
1533
+ if ((0, actionVerbDetector_1.isForgetIntent)(message)) {
1534
+ // C11: Forget branch — force memory_forget
1535
+ const usesMemoryForget = candidatePlan.plan.some(s => s.tool === 'memory_forget');
1536
+ if (!usesMemoryForget) {
1537
+ const verb = (0, actionVerbDetector_1.detectActionVerb)(message);
1538
+ const fact = (0, actionVerbDetector_1.extractMemoryFact)(message);
1539
+ process.stderr.write(`[MemoryGuard] overriding plan [${candidatePlan.plan.map(s => s.tool).join(',')}] → memory_forget for verb='${verb}'\n`);
1540
+ candidatePlan.plan = [{ step: 1, tool: 'memory_forget', input: { fact }, description: 'Remove from permanent memory' }];
1541
+ candidatePlan.requires_execution = true;
1542
+ }
1543
+ }
1544
+ else {
1545
+ // Store branch — force memory_store (original C5 logic)
1546
+ const usesMemoryStore = candidatePlan.plan.some(s => s.tool === 'memory_store');
1547
+ if (!usesMemoryStore) {
1548
+ const verb = (0, actionVerbDetector_1.detectActionVerb)(message);
1549
+ const fact = (0, actionVerbDetector_1.extractMemoryFact)(message);
1550
+ process.stderr.write(`[MemoryGuard] overriding plan [${candidatePlan.plan.map(s => s.tool).join(',')}] → memory_store for verb='${verb}'\n`);
1551
+ candidatePlan.plan = [{ step: 1, tool: 'memory_store', input: { fact }, description: 'Store to permanent memory' }];
1552
+ candidatePlan.requires_execution = true;
1553
+ }
1554
+ }
1555
+ }
1396
1556
  return candidatePlan;
1397
1557
  }
1398
1558
  // ── Plan validation ────────────────────────────────────────────
1399
1559
  // Called after planWithLLM — rejects structurally bad plans before execution.
1400
- const VALID_TOOLS = [
1401
- 'web_search', 'fetch_page', 'fetch_url', 'open_browser', 'browser_extract',
1402
- 'browser_click', 'browser_type', 'browser_screenshot', 'browser_scroll', 'browser_get_url',
1403
- 'file_write', 'file_read',
1404
- 'file_list', 'shell_exec', 'run_python', 'run_node', 'run_powershell',
1405
- 'system_info', 'notify', 'deep_research', 'get_stocks', 'run_agent', 'git_commit',
1406
- 'git_push', 'get_market_data', 'get_company_info',
1407
- 'mouse_move', 'mouse_click', 'keyboard_type', 'keyboard_press',
1408
- 'screenshot', 'screen_read', 'vision_loop', 'wait',
1409
- 'code_interpreter_python', 'code_interpreter_node',
1410
- 'clipboard_read', 'clipboard_write', 'window_list', 'window_focus',
1411
- 'app_launch', 'app_close', 'system_volume',
1412
- 'watch_folder', 'watch_folder_list',
1413
- 'send_file_local', 'receive_file_local',
1414
- 'clarify', 'todo', 'cronjob', 'vision_analyze',
1415
- 'voice_speak', 'voice_transcribe', 'voice_clone', 'voice_design',
1416
- 'lookup_skill', 'lookup_tool_schema',
1417
- 'spawn', 'spawn_subagent', 'swarm',
1418
- ...slashAsTool_1.SLASH_MIRROR_TOOL_NAMES,
1419
- ];
1560
+ // ── v3.19 Phase 1 Commit 4: derived from TOOL_REGISTRY — literal deleted ──────
1561
+ exports.VALID_TOOLS = (0, toolRegistry_1.registryValidTools)();
1420
1562
  function validatePlan(plan) {
1421
1563
  const errors = [];
1422
1564
  const warnings = [];
@@ -1425,8 +1567,8 @@ function validatePlan(plan) {
1425
1567
  }
1426
1568
  for (const step of plan.plan) {
1427
1569
  // Check tool name — attempt fuzzy repair before flagging as error
1428
- if (!VALID_TOOLS.includes(step.tool)) {
1429
- const repair = (0, toolNameRepair_1.repairToolName)(step.tool, VALID_TOOLS);
1570
+ if (!exports.VALID_TOOLS.includes(step.tool)) {
1571
+ const repair = (0, toolNameRepair_1.repairToolName)(step.tool, exports.VALID_TOOLS);
1430
1572
  if (repair) {
1431
1573
  warnings.push(`Step ${step.step}: auto-repaired tool "${repair.original}" → "${repair.repaired}" (edit distance ${repair.distance})`);
1432
1574
  console.log(`[ToolRepair] ↺ "${repair.original}" → "${repair.repaired}" (distance ${repair.distance})`);
@@ -1700,14 +1842,10 @@ function appendLesson(lesson) {
1700
1842
  }
1701
1843
  // ── executeToolWithRetry — step-level retry with exponential backoff ──
1702
1844
  // Tools that mutate state are excluded from retry to prevent double-execution.
1703
- const NO_RETRY_TOOLS = new Set([
1704
- 'shell_exec', 'run_python', 'run_node', 'notify',
1705
- 'mouse_click', 'keyboard_type', 'keyboard_press',
1706
- 'app_launch', 'app_close',
1707
- 'open_browser', 'browser_extract', 'browser_screenshot', 'browser_click', 'browser_type', 'browser_scroll', 'browser_get_url',
1708
- ]);
1845
+ // ── v3.19 Phase 1 Commit 5: derived from TOOL_REGISTRY[retry=false] literal deleted ──
1846
+ exports.NO_RETRY_TOOLS = (0, toolRegistry_1.registryNoRetrySet)();
1709
1847
  async function executeToolWithRetry(tool, input, maxRetries = 2) {
1710
- const retryable = !NO_RETRY_TOOLS.has(tool);
1848
+ const retryable = !exports.NO_RETRY_TOOLS.has(tool);
1711
1849
  const effectiveMax = retryable ? maxRetries : 0;
1712
1850
  // ── Plugin preTool hooks ──────────────────────────────────────
1713
1851
  let effectiveInput = input;
@@ -1776,28 +1914,17 @@ async function executeToolWithRetry(tool, input, maxRetries = 2) {
1776
1914
  // —— Sprint 8: dependency-group builder ——————————————
1777
1915
  // Groups consecutive tool steps into batches: parallel-safe tools are
1778
1916
  // batched together; sequential tools break the batch.
1779
- const PARALLEL_SAFE = new Set([
1780
- 'web_search', 'system_info', 'get_stocks', 'get_market_data',
1781
- 'social_research', 'fetch_url', 'fetch_page', 'get_company_info',
1782
- 'deep_research', 'code_interpreter_python', 'code_interpreter_node',
1783
- 'clipboard_read', 'window_list', 'watch_folder_list',
1784
- 'get_calendar', 'read_email', 'get_natural_events', 'ingest_youtube',
1785
- ]);
1786
- const SEQUENTIAL_ONLY = new Set([
1787
- 'file_write', 'run_python', 'run_node', 'shell_exec',
1788
- 'open_browser', 'browser_click', 'browser_type', 'browser_extract',
1789
- 'mouse_move', 'mouse_click', 'keyboard_type', 'keyboard_press',
1790
- 'screenshot', 'screen_read', 'vision_loop', 'notify', 'wait',
1791
- 'clipboard_write', 'window_focus', 'app_launch', 'app_close', 'system_volume',
1792
- 'watch_folder',
1793
- ]);
1917
+ // ── v3.19 Phase 1 Commit 5: derived from TOOL_REGISTRY[parallel=safe] literal deleted ──
1918
+ exports.PARALLEL_SAFE = (0, toolRegistry_1.registryParallelSafeSet)();
1919
+ // ── v3.19 Phase 1 Commit 5: derived from TOOL_REGISTRY[parallel=sequential] — literal deleted ──
1920
+ exports.SEQUENTIAL_ONLY = (0, toolRegistry_1.registrySequentialOnlySet)();
1794
1921
  function buildDependencyGroups(steps) {
1795
1922
  const groups = [];
1796
1923
  let currentGroup = [];
1797
1924
  for (const step of steps) {
1798
1925
  const inputStr = JSON.stringify(step.input || {});
1799
- const dependsOnPrevious = inputStr.includes('PREVIOUS_OUTPUT') || SEQUENTIAL_ONLY.has(step.tool);
1800
- if (PARALLEL_SAFE.has(step.tool) && !dependsOnPrevious) {
1926
+ const dependsOnPrevious = inputStr.includes('PREVIOUS_OUTPUT') || exports.SEQUENTIAL_ONLY.has(step.tool);
1927
+ if (exports.PARALLEL_SAFE.has(step.tool) && !dependsOnPrevious) {
1801
1928
  currentGroup.push(step);
1802
1929
  }
1803
1930
  else {
@@ -1880,8 +2007,11 @@ async function executePlan(plan, onStep, onPhaseChange, existingState, replanApi
1880
2007
  console.log(`[Exec] Step ${step.step}/${totalSteps}: ${step.tool} — RUNNING`);
1881
2008
  console.log(`[ExecutePlan] Step ${step.step}: ${step.tool} — input: ${JSON.stringify(step.input).slice(0, 100)}`);
1882
2009
  livePulse_1.livePulse.tool('Aiden', step.tool, JSON.stringify(step.input).slice(0, 80));
1883
- // Validate tool exists
1884
- if (!toolRegistry_1.TOOLS[step.tool]) {
2010
+ // Validate tool exists — use isKnownTool() which checks both static TOOLS and
2011
+ // runtime-registered externalTools (e.g. memory_store from registerSlashMirrorTools).
2012
+ // ALLOWED_TOOLS is frozen at module-load time before mirror tools are registered,
2013
+ // so it cannot be used here.
2014
+ if (!(0, toolRegistry_1.isKnownTool)(step.tool)) {
1885
2015
  const stepResult = {
1886
2016
  step: step.step, tool: step.tool, input: step.input,
1887
2017
  success: false, output: '',
@@ -1893,7 +2023,7 @@ async function executePlan(plan, onStep, onPhaseChange, existingState, replanApi
1893
2023
  return stepResult;
1894
2024
  }
1895
2025
  // Tools that legitimately take zero input
1896
- const NO_INPUT_TOOLS = ['system_info', 'screenshot', 'get_hardware', 'screen_read', 'vision_loop', 'health_check', 'respond'];
2026
+ const NO_INPUT_TOOLS = ['system_info', 'screenshot', 'get_hardware', 'screen_read', 'vision_loop', 'health_check', 'respond', 'now_playing'];
1897
2027
  if (!NO_INPUT_TOOLS.includes(step.tool)) {
1898
2028
  if (!step.input || Object.keys(step.input).length === 0) {
1899
2029
  console.log(`[ExecutePlan] Skipping step ${step.step} (${step.tool}) — empty input`);
@@ -2263,7 +2393,19 @@ function resolvePreviousOutput(input, stepOutputs, currentStep) {
2263
2393
  return resolved;
2264
2394
  }
2265
2395
  // ── STEP 3: respondWithResults ────────────────────────────────
2266
- function responderSystem(userName, date) {
2396
+ function responderSystem(userName, date, sessionId) {
2397
+ // Option-B: SOUL.md in full on first turn or when content changed on disk;
2398
+ // reference line only on unchanged turns. AIDEN_RESPONDER_SYSTEM already
2399
+ // calls getLiveSoul() — hash tracking here is additional cost guard.
2400
+ const _ctx = protectedContext_1.protectedContextManager.getProtectedContext();
2401
+ const _prevHash = sessionId ? soulHashBySession.get(sessionId) : undefined;
2402
+ if (sessionId)
2403
+ soulHashBySession.set(sessionId, _ctx.hash);
2404
+ // When soul is unchanged, prepend a compact block then the responder body.
2405
+ if (_prevHash !== undefined && _ctx.hash === _prevHash) {
2406
+ const refBlock = (0, contextHandoff_1.buildProtectedContextBlock)(_ctx, _prevHash, sessionId);
2407
+ return refBlock ? refBlock + '\n\n' + (0, aidenPersonality_1.AIDEN_RESPONDER_SYSTEM)(userName, date) : (0, aidenPersonality_1.AIDEN_RESPONDER_SYSTEM)(userName, date);
2408
+ }
2267
2409
  return (0, aidenPersonality_1.AIDEN_RESPONDER_SYSTEM)(userName, date);
2268
2410
  }
2269
2411
  async function respondWithResults(originalMessage, plan, results, history, userName, apiKey, model, providerName, onToken, sessionId, goals) {
@@ -2337,7 +2479,7 @@ async function respondWithResults(originalMessage, plan, results, history, userN
2337
2479
  ? results.map(r => `[${r.tool} result]: ${r.success ? r.output.slice(0, 1000) : 'FAILED: ' + r.error}`).join('\n')
2338
2480
  : '';
2339
2481
  const systemWithResults = toolResultsContext
2340
- ? `${capabilitiesSection}${entitySummary}${responderSystem(userName, date)}${responseSkillContext}${knowledgeResponderSection}${multiGoalInstruction}
2482
+ ? `${capabilitiesSection}${entitySummary}${responderSystem(userName, date, sessionId)}${responseSkillContext}${knowledgeResponderSection}${multiGoalInstruction}
2341
2483
 
2342
2484
  YOU JUST RAN THESE TOOLS AND GOT THESE RESULTS:
2343
2485
  ${toolResultsContext}
@@ -2346,11 +2488,17 @@ CRITICAL RULES FOR YOUR RESPONSE:
2346
2488
  - Include the ACTUAL output from the tools above in your response
2347
2489
  - Do NOT say "I ran the tool" — show the RESULT
2348
2490
  - If run_python returned a number, say that number
2349
- - If file_read returned text, show that text
2491
+ - If file_read SUCCEEDED, show the actual text returned
2492
+ - If file_read FAILED (ENOENT or any error), state the file does not exist or could not be read — NEVER invent or fabricate file contents
2493
+ - If file_list SUCCEEDED, show the actual listing
2494
+ - If file_list FAILED, say the directory could not be listed — NEVER invent filenames
2495
+ - If web_fetch SUCCEEDED, show the actual fetched content
2496
+ - If web_fetch FAILED, say the page could not be fetched — NEVER invent page content
2497
+ - If a search tool returned no results, say no results were found — NEVER invent search results
2350
2498
  - If system_info returned hardware data, show the data
2351
2499
  - Be direct: show the actual output, then provide context if needed
2352
- - If a tool failed, say it failed and why`
2353
- : `${capabilitiesSection}${entitySummary}${responderSystem(userName, date)}${responseSkillContext}${knowledgeResponderSection}${multiGoalInstruction}`;
2500
+ - If a tool result starts with "FAILED:", tell the user it failed and why — NEVER fabricate a successful result`
2501
+ : `${capabilitiesSection}${entitySummary}${responderSystem(userName, date, sessionId)}${responseSkillContext}${knowledgeResponderSection}${multiGoalInstruction}`;
2354
2502
  const userContent = executionSummary
2355
2503
  ? `User asked: "${originalMessage}"\n\nReal execution results:\n${executionSummary}\n\nRespond naturally based on these real results only. Show the actual output, not a description of it.${depthInstruction}${memSection}`
2356
2504
  : `${originalMessage}${memSection}`;
@@ -2427,8 +2575,9 @@ CRITICAL RULES FOR YOUR RESPONSE:
2427
2575
  throw new Error('Ollama: empty response — no tokens emitted');
2428
2576
  }
2429
2577
  else {
2430
- // OpenAI-compatible
2431
- const url = OPENAI_COMPAT_ENDPOINTS[providerName] || OPENAI_COMPAT_ENDPOINTS.groq;
2578
+ // C9b: Unified path for all OpenAI-compatible providers (known + custom).
2579
+ // resolveStreamingUrl handles custom→config lookup and known→endpoint map.
2580
+ const url = resolveStreamingUrl(providerName, apiKey);
2432
2581
  const r = await fetch(url, {
2433
2582
  method: 'POST',
2434
2583
  headers: buildHeaders(providerName, apiKey),
@@ -2471,7 +2620,8 @@ CRITICAL RULES FOR YOUR RESPONSE:
2471
2620
  if (nextCloud.providerName !== 'ollama' && nextCloud.apiName !== providerName && nextCloud.apiKey) {
2472
2621
  console.log(`[Responder] ${providerName} at capacity — trying ${nextCloud.providerName} (${nextCloud.model})`);
2473
2622
  try {
2474
- const url = OPENAI_COMPAT_ENDPOINTS[nextCloud.providerName] || OPENAI_COMPAT_ENDPOINTS.groq;
2623
+ // C9b: use resolveStreamingUrl for correct custom-provider routing
2624
+ const url = resolveStreamingUrl(nextCloud.providerName, nextCloud.apiKey);
2475
2625
  const headers = buildHeaders(nextCloud.providerName, nextCloud.apiKey);
2476
2626
  const r = await fetch(url, {
2477
2627
  method: 'POST',
@@ -2501,7 +2651,8 @@ CRITICAL RULES FOR YOUR RESPONSE:
2501
2651
  if (cloudFallback.providerName !== 'ollama' && cloudFallback.apiKey) {
2502
2652
  console.log(`[Router] Ollama timeout/error — falling back to ${cloudFallback.providerName} (${cloudFallback.model})`);
2503
2653
  try {
2504
- const url = OPENAI_COMPAT_ENDPOINTS[cloudFallback.providerName] || OPENAI_COMPAT_ENDPOINTS.groq;
2654
+ // C9b: use resolveStreamingUrl for correct custom-provider routing
2655
+ const url = resolveStreamingUrl(cloudFallback.providerName, cloudFallback.apiKey);
2505
2656
  const headers = buildHeaders(cloudFallback.providerName, cloudFallback.apiKey);
2506
2657
  const r = await fetch(url, {
2507
2658
  method: 'POST',
@@ -2572,20 +2723,23 @@ CRITICAL RULES FOR YOUR RESPONSE:
2572
2723
  }
2573
2724
  if (ollamaResponded)
2574
2725
  return;
2575
- // Last resort: return raw tool output if tools ran successfully
2576
- if (results && results.length > 0 && results.some(r => r.success)) {
2577
- const successResults = results.filter(r => r.success);
2578
- const lastResult = successResults[successResults.length - 1];
2579
- onToken(lastResult.output || 'Here are the results.');
2580
- return;
2581
- }
2582
- // Include error info from failed tools if any
2726
+ // Last resort: synthesize honest summary (all LLM providers down)
2583
2727
  if (results && results.length > 0) {
2584
- const failedResult = results[results.length - 1];
2585
- if (failedResult.error) {
2586
- onToken(`Error: ${failedResult.error}`);
2728
+ const successes = results.filter(r => r.success);
2729
+ const failures = results.filter(r => !r.success);
2730
+ if (failures.length === 0) {
2731
+ // All steps succeeded — return last output as before
2732
+ onToken(successes[successes.length - 1].output || 'Done.');
2587
2733
  return;
2588
2734
  }
2735
+ // Mixed or all-failed — surface both sides honestly
2736
+ const parts = [];
2737
+ if (successes.length > 0)
2738
+ parts.push(`Completed: ${successes.map(r => r.tool).join(', ')}.`);
2739
+ parts.push(`Failed: ${failures.map(r => `${r.tool} — ${r.error || 'unknown error'}`).join('; ')}.`);
2740
+ parts.push('(All language providers are currently unavailable — full response cannot be generated.)');
2741
+ onToken(parts.join(' '));
2742
+ return;
2589
2743
  }
2590
2744
  const degraded = (0, router_1.enterDegradedMode)(e.message || 'unknown error');
2591
2745
  onToken(degraded.message);
@@ -2676,12 +2830,25 @@ async function callLLM(prompt, apiKey, model, providerName, opts) {
2676
2830
  return d?.result?.response || '';
2677
2831
  }
2678
2832
  else if (providerName === 'custom') {
2679
- // Custom provider — look up baseUrl from config by matching apiKey
2833
+ // Custom provider — look up baseUrl from config.
2834
+ // Checks customProviders first (direct apiKey match), then providers.apis
2835
+ // entries with provider:'custom' (key resolved from env).
2680
2836
  const cfgCustom = (0, index_1.loadConfig)();
2681
- const cp = cfgCustom.customProviders?.find((c) => c.enabled && c.apiKey === apiKey);
2682
- if (!cp?.baseUrl)
2837
+ let customBaseUrl = cfgCustom.customProviders?.find((c) => c.enabled && c.apiKey === apiKey)?.baseUrl;
2838
+ if (!customBaseUrl) {
2839
+ const apiEntry = (cfgCustom.providers?.apis ?? []).find((a) => {
2840
+ if (a.provider !== 'custom' || !a.enabled || !a.baseUrl)
2841
+ return false;
2842
+ const resolved = a.key?.startsWith('env:')
2843
+ ? (process.env[a.key.replace('env:', '')] || '')
2844
+ : a.key;
2845
+ return resolved === apiKey;
2846
+ });
2847
+ customBaseUrl = apiEntry?.baseUrl;
2848
+ }
2849
+ if (!customBaseUrl)
2683
2850
  throw new Error(`callLLM: no baseUrl for custom provider (model=${model})`);
2684
- const r = await fetch(cp.baseUrl, {
2851
+ const r = await fetch(customBaseUrl, {
2685
2852
  method: 'POST',
2686
2853
  headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
2687
2854
  body: JSON.stringify({
@@ -2709,7 +2876,7 @@ async function callLLM(prompt, apiKey, model, providerName, opts) {
2709
2876
  costTracker_1.costTracker.trackUsage(providerName, model, d?.usage?.prompt_tokens ?? 0, d?.usage?.completion_tokens ?? 0, opts?.traceId, opts?.isSystem ?? false);
2710
2877
  }
2711
2878
  catch { }
2712
- return d?.choices?.[0]?.message?.content || '';
2879
+ return extractChatMessageContent(d?.choices?.[0]?.message?.content);
2713
2880
  }
2714
2881
  else {
2715
2882
  // OpenAI-compatible: groq, openrouter, cerebras, nvidia, github
@@ -2736,7 +2903,7 @@ async function callLLM(prompt, apiKey, model, providerName, opts) {
2736
2903
  costTracker_1.costTracker.trackUsage(providerName, model, d?.usage?.prompt_tokens ?? 0, d?.usage?.completion_tokens ?? 0, opts?.traceId, opts?.isSystem ?? false);
2737
2904
  }
2738
2905
  catch { }
2739
- return d?.choices?.[0]?.message?.content || '';
2906
+ return extractChatMessageContent(d?.choices?.[0]?.message?.content);
2740
2907
  }
2741
2908
  }
2742
2909
  catch (e) {