attocode 0.1.2 → 0.1.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.
Files changed (175) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +180 -0
  3. package/dist/src/agent.d.ts +78 -1
  4. package/dist/src/agent.d.ts.map +1 -1
  5. package/dist/src/agent.js +639 -36
  6. package/dist/src/agent.js.map +1 -1
  7. package/dist/src/analysis/feedback-loop.d.ts +115 -0
  8. package/dist/src/analysis/feedback-loop.d.ts.map +1 -0
  9. package/dist/src/analysis/feedback-loop.js +226 -0
  10. package/dist/src/analysis/feedback-loop.js.map +1 -0
  11. package/dist/src/analysis/index.d.ts +9 -0
  12. package/dist/src/analysis/index.d.ts.map +1 -0
  13. package/dist/src/analysis/index.js +9 -0
  14. package/dist/src/analysis/index.js.map +1 -0
  15. package/dist/src/analysis/prompt-templates.d.ts +36 -0
  16. package/dist/src/analysis/prompt-templates.d.ts.map +1 -0
  17. package/dist/src/analysis/prompt-templates.js +198 -0
  18. package/dist/src/analysis/prompt-templates.js.map +1 -0
  19. package/dist/src/analysis/trace-summary.d.ts +56 -0
  20. package/dist/src/analysis/trace-summary.d.ts.map +1 -0
  21. package/dist/src/analysis/trace-summary.js +261 -0
  22. package/dist/src/analysis/trace-summary.js.map +1 -0
  23. package/dist/src/commands/agents-commands.d.ts +24 -0
  24. package/dist/src/commands/agents-commands.d.ts.map +1 -0
  25. package/dist/src/commands/agents-commands.js +284 -0
  26. package/dist/src/commands/agents-commands.js.map +1 -0
  27. package/dist/src/commands/handler.d.ts.map +1 -1
  28. package/dist/src/commands/handler.js +329 -21
  29. package/dist/src/commands/handler.js.map +1 -1
  30. package/dist/src/commands/init-commands.d.ts +35 -0
  31. package/dist/src/commands/init-commands.d.ts.map +1 -0
  32. package/dist/src/commands/init-commands.js +187 -0
  33. package/dist/src/commands/init-commands.js.map +1 -0
  34. package/dist/src/commands/skills-commands.d.ts +26 -0
  35. package/dist/src/commands/skills-commands.d.ts.map +1 -0
  36. package/dist/src/commands/skills-commands.js +309 -0
  37. package/dist/src/commands/skills-commands.js.map +1 -0
  38. package/dist/src/commands/types.d.ts +13 -2
  39. package/dist/src/commands/types.d.ts.map +1 -1
  40. package/dist/src/config.d.ts +3 -0
  41. package/dist/src/config.d.ts.map +1 -1
  42. package/dist/src/config.js.map +1 -1
  43. package/dist/src/defaults.d.ts +31 -2
  44. package/dist/src/defaults.d.ts.map +1 -1
  45. package/dist/src/defaults.js +69 -2
  46. package/dist/src/defaults.js.map +1 -1
  47. package/dist/src/errors/index.d.ts +233 -0
  48. package/dist/src/errors/index.d.ts.map +1 -0
  49. package/dist/src/errors/index.js +427 -0
  50. package/dist/src/errors/index.js.map +1 -0
  51. package/dist/src/integrations/agent-registry.d.ts +68 -2
  52. package/dist/src/integrations/agent-registry.d.ts.map +1 -1
  53. package/dist/src/integrations/agent-registry.js +230 -23
  54. package/dist/src/integrations/agent-registry.js.map +1 -1
  55. package/dist/src/integrations/auto-compaction.d.ts +33 -0
  56. package/dist/src/integrations/auto-compaction.d.ts.map +1 -1
  57. package/dist/src/integrations/auto-compaction.js +47 -3
  58. package/dist/src/integrations/auto-compaction.js.map +1 -1
  59. package/dist/src/integrations/cancellation.d.ts +5 -0
  60. package/dist/src/integrations/cancellation.d.ts.map +1 -1
  61. package/dist/src/integrations/cancellation.js +7 -0
  62. package/dist/src/integrations/cancellation.js.map +1 -1
  63. package/dist/src/integrations/capabilities.d.ts +160 -0
  64. package/dist/src/integrations/capabilities.d.ts.map +1 -0
  65. package/dist/src/integrations/capabilities.js +426 -0
  66. package/dist/src/integrations/capabilities.js.map +1 -0
  67. package/dist/src/integrations/context-engineering.d.ts +6 -1
  68. package/dist/src/integrations/context-engineering.d.ts.map +1 -1
  69. package/dist/src/integrations/context-engineering.js +7 -0
  70. package/dist/src/integrations/context-engineering.js.map +1 -1
  71. package/dist/src/integrations/dead-letter-queue.d.ts +208 -0
  72. package/dist/src/integrations/dead-letter-queue.d.ts.map +1 -0
  73. package/dist/src/integrations/dead-letter-queue.js +458 -0
  74. package/dist/src/integrations/dead-letter-queue.js.map +1 -0
  75. package/dist/src/integrations/health-check.d.ts +218 -0
  76. package/dist/src/integrations/health-check.d.ts.map +1 -0
  77. package/dist/src/integrations/health-check.js +400 -0
  78. package/dist/src/integrations/health-check.js.map +1 -0
  79. package/dist/src/integrations/index.d.ts +11 -2
  80. package/dist/src/integrations/index.d.ts.map +1 -1
  81. package/dist/src/integrations/index.js +19 -2
  82. package/dist/src/integrations/index.js.map +1 -1
  83. package/dist/src/integrations/mcp-client.d.ts +9 -0
  84. package/dist/src/integrations/mcp-client.d.ts.map +1 -1
  85. package/dist/src/integrations/mcp-client.js +49 -7
  86. package/dist/src/integrations/mcp-client.js.map +1 -1
  87. package/dist/src/integrations/openrouter-pricing.d.ts +28 -3
  88. package/dist/src/integrations/openrouter-pricing.d.ts.map +1 -1
  89. package/dist/src/integrations/openrouter-pricing.js +57 -16
  90. package/dist/src/integrations/openrouter-pricing.js.map +1 -1
  91. package/dist/src/integrations/retry.d.ts +131 -0
  92. package/dist/src/integrations/retry.d.ts.map +1 -0
  93. package/dist/src/integrations/retry.js +233 -0
  94. package/dist/src/integrations/retry.js.map +1 -0
  95. package/dist/src/integrations/skill-executor.d.ts +113 -0
  96. package/dist/src/integrations/skill-executor.d.ts.map +1 -0
  97. package/dist/src/integrations/skill-executor.js +270 -0
  98. package/dist/src/integrations/skill-executor.js.map +1 -0
  99. package/dist/src/integrations/skills.d.ts +98 -7
  100. package/dist/src/integrations/skills.d.ts.map +1 -1
  101. package/dist/src/integrations/skills.js +210 -11
  102. package/dist/src/integrations/skills.js.map +1 -1
  103. package/dist/src/integrations/sqlite-store.d.ts +42 -0
  104. package/dist/src/integrations/sqlite-store.d.ts.map +1 -1
  105. package/dist/src/integrations/sqlite-store.js +111 -0
  106. package/dist/src/integrations/sqlite-store.js.map +1 -1
  107. package/dist/src/main.js +88 -7
  108. package/dist/src/main.js.map +1 -1
  109. package/dist/src/modes/repl.d.ts.map +1 -1
  110. package/dist/src/modes/repl.js +37 -1
  111. package/dist/src/modes/repl.js.map +1 -1
  112. package/dist/src/modes/tui.d.ts.map +1 -1
  113. package/dist/src/modes/tui.js +46 -5
  114. package/dist/src/modes/tui.js.map +1 -1
  115. package/dist/src/modes.d.ts.map +1 -1
  116. package/dist/src/modes.js +10 -3
  117. package/dist/src/modes.js.map +1 -1
  118. package/dist/src/persistence/schema.d.ts +4 -0
  119. package/dist/src/persistence/schema.d.ts.map +1 -1
  120. package/dist/src/persistence/schema.js +49 -0
  121. package/dist/src/persistence/schema.js.map +1 -1
  122. package/dist/src/providers/adapters/anthropic.d.ts +24 -2
  123. package/dist/src/providers/adapters/anthropic.d.ts.map +1 -1
  124. package/dist/src/providers/adapters/anthropic.js +184 -0
  125. package/dist/src/providers/adapters/anthropic.js.map +1 -1
  126. package/dist/src/tools/bash.d.ts.map +1 -1
  127. package/dist/src/tools/bash.js +7 -4
  128. package/dist/src/tools/bash.js.map +1 -1
  129. package/dist/src/tools/file.d.ts.map +1 -1
  130. package/dist/src/tools/file.js +31 -10
  131. package/dist/src/tools/file.js.map +1 -1
  132. package/dist/src/tools/permission.d.ts +12 -0
  133. package/dist/src/tools/permission.d.ts.map +1 -1
  134. package/dist/src/tools/permission.js +136 -0
  135. package/dist/src/tools/permission.js.map +1 -1
  136. package/dist/src/tools/registry.d.ts +23 -1
  137. package/dist/src/tools/registry.d.ts.map +1 -1
  138. package/dist/src/tools/registry.js +77 -17
  139. package/dist/src/tools/registry.js.map +1 -1
  140. package/dist/src/tools/standard.d.ts.map +1 -1
  141. package/dist/src/tools/standard.js +8 -0
  142. package/dist/src/tools/standard.js.map +1 -1
  143. package/dist/src/tools/types.d.ts +20 -1
  144. package/dist/src/tools/types.d.ts.map +1 -1
  145. package/dist/src/tools/types.js.map +1 -1
  146. package/dist/src/tracing/trace-collector.d.ts +198 -2
  147. package/dist/src/tracing/trace-collector.d.ts.map +1 -1
  148. package/dist/src/tracing/trace-collector.js +315 -3
  149. package/dist/src/tracing/trace-collector.js.map +1 -1
  150. package/dist/src/tracing/types.d.ts +470 -2
  151. package/dist/src/tracing/types.d.ts.map +1 -1
  152. package/dist/src/tracing/types.js +25 -0
  153. package/dist/src/tracing/types.js.map +1 -1
  154. package/dist/src/tui/app.d.ts.map +1 -1
  155. package/dist/src/tui/app.js +292 -18
  156. package/dist/src/tui/app.js.map +1 -1
  157. package/dist/src/tui/index.d.ts +1 -0
  158. package/dist/src/tui/index.d.ts.map +1 -1
  159. package/dist/src/tui/index.js +2 -0
  160. package/dist/src/tui/index.js.map +1 -1
  161. package/dist/src/tui/transparency-aggregator.d.ts +100 -0
  162. package/dist/src/tui/transparency-aggregator.d.ts.map +1 -0
  163. package/dist/src/tui/transparency-aggregator.js +234 -0
  164. package/dist/src/tui/transparency-aggregator.js.map +1 -0
  165. package/dist/src/types.d.ts +129 -0
  166. package/dist/src/types.d.ts.map +1 -1
  167. package/package.json +6 -3
  168. package/dist/src/hello.d.ts +0 -2
  169. package/dist/src/hello.d.ts.map +0 -1
  170. package/dist/src/hello.js +0 -4
  171. package/dist/src/hello.js.map +0 -1
  172. package/dist/src/test-sqlite.d.ts +0 -2
  173. package/dist/src/test-sqlite.d.ts.map +0 -1
  174. package/dist/src/test-sqlite.js +0 -114
  175. package/dist/src/test-sqlite.js.map +0 -1
package/dist/src/agent.js CHANGED
@@ -21,9 +21,12 @@
21
21
  import { buildConfig, isFeatureEnabled, getEnabledFeatures, } from './defaults.js';
22
22
  import { createModeManager, formatModeList, parseMode, } from './modes.js';
23
23
  import { createLSPFileTools, } from './agent-tools/index.js';
24
- import { HookManager, MemoryManager, PlanningManager, ObservabilityManager, SafetyManager, RoutingManager, MultiAgentManager, ReActManager, ExecutionPolicyManager, ThreadManager, RulesManager, DEFAULT_RULE_SOURCES, ExecutionEconomicsManager, STANDARD_BUDGET, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, } from './integrations/index.js';
24
+ import { HookManager, MemoryManager, PlanningManager, ObservabilityManager, SafetyManager, RoutingManager, MultiAgentManager, ReActManager, ExecutionPolicyManager, ThreadManager, RulesManager, DEFAULT_RULE_SOURCES, ExecutionEconomicsManager, STANDARD_BUDGET, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createTimeoutToken, createLinkedToken, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, } from './integrations/index.js';
25
25
  // Lesson 26: Tracing & Evaluation integration
26
26
  import { createTraceCollector } from './tracing/trace-collector.js';
27
+ // Model registry for context window limits
28
+ import { modelRegistry } from './costs/index.js';
29
+ import { getModelContextLength } from './integrations/openrouter-pricing.js';
27
30
  // Spawn agent tool for LLM-driven subagent delegation
28
31
  import { createBoundSpawnAgentTool } from './tools/agent.js';
29
32
  // =============================================================================
@@ -62,6 +65,11 @@ export class ProductionAgent {
62
65
  pendingPlanManager;
63
66
  interactivePlanner = null;
64
67
  recursiveContext = null;
68
+ learningStore = null;
69
+ compactor = null;
70
+ autoCompactionManager = null;
71
+ fileChangeTracker = null;
72
+ capabilitiesRegistry = null;
65
73
  toolResolver = null;
66
74
  // Initialization tracking
67
75
  initPromises = [];
@@ -410,6 +418,224 @@ export class ProductionAgent {
410
418
  }
411
419
  });
412
420
  }
421
+ // Learning Store (cross-session learning from failures)
422
+ // Connects to the failure tracker in contextEngineering for automatic learning extraction
423
+ if (isFeatureEnabled(this.config.learningStore)) {
424
+ const learningConfig = typeof this.config.learningStore === 'object'
425
+ ? this.config.learningStore
426
+ : {};
427
+ this.learningStore = createLearningStore({
428
+ dbPath: learningConfig.dbPath ?? '.agent/learnings.db',
429
+ requireValidation: learningConfig.requireValidation ?? true,
430
+ autoValidateThreshold: learningConfig.autoValidateThreshold ?? 0.9,
431
+ maxLearnings: learningConfig.maxLearnings ?? 500,
432
+ });
433
+ // Connect to the failure tracker if available
434
+ if (this.contextEngineering) {
435
+ const failureTracker = this.contextEngineering.getFailureTracker();
436
+ if (failureTracker) {
437
+ this.learningStore.connectFailureTracker(failureTracker);
438
+ }
439
+ }
440
+ // Forward learning events to observability
441
+ this.learningStore.on(event => {
442
+ switch (event.type) {
443
+ case 'learning.proposed':
444
+ this.observability?.logger?.info('Learning proposed', {
445
+ learningId: event.learning.id,
446
+ description: event.learning.description,
447
+ });
448
+ this.emit({
449
+ type: 'learning.proposed',
450
+ learningId: event.learning.id,
451
+ description: event.learning.description,
452
+ });
453
+ break;
454
+ case 'learning.validated':
455
+ this.observability?.logger?.info('Learning validated', {
456
+ learningId: event.learningId,
457
+ });
458
+ this.emit({ type: 'learning.validated', learningId: event.learningId });
459
+ break;
460
+ case 'learning.applied':
461
+ this.observability?.logger?.debug('Learning applied', {
462
+ learningId: event.learningId,
463
+ context: event.context,
464
+ });
465
+ this.emit({
466
+ type: 'learning.applied',
467
+ learningId: event.learningId,
468
+ context: event.context,
469
+ });
470
+ break;
471
+ case 'pattern.extracted':
472
+ this.observability?.logger?.info('Pattern extracted as learning', {
473
+ pattern: event.pattern.description,
474
+ learningId: event.learning.id,
475
+ });
476
+ break;
477
+ }
478
+ });
479
+ }
480
+ // Auto-Compaction Manager (sophisticated context compaction)
481
+ // Uses the Compactor for LLM-based summarization with threshold monitoring
482
+ if (isFeatureEnabled(this.config.compaction)) {
483
+ const compactionConfig = typeof this.config.compaction === 'object'
484
+ ? this.config.compaction
485
+ : {};
486
+ // Create the compactor (requires provider for LLM summarization)
487
+ this.compactor = createCompactor(this.provider, {
488
+ enabled: true,
489
+ tokenThreshold: compactionConfig.tokenThreshold ?? 80000,
490
+ preserveRecentCount: compactionConfig.preserveRecentCount ?? 10,
491
+ preserveToolResults: compactionConfig.preserveToolResults ?? true,
492
+ summaryMaxTokens: compactionConfig.summaryMaxTokens ?? 2000,
493
+ summaryModel: compactionConfig.summaryModel,
494
+ });
495
+ // Create the auto-compaction manager with threshold monitoring
496
+ // Wire reversible compaction through contextEngineering when available
497
+ const compactHandler = this.contextEngineering
498
+ ? async (messages) => {
499
+ // Use contextEngineering's reversible compaction to preserve references
500
+ const summarize = async (msgs) => {
501
+ // Use the basic compactor's summarization capability
502
+ const result = await this.compactor.compact(msgs);
503
+ return result.summary;
504
+ };
505
+ const contextMsgs = messages.map(m => ({
506
+ role: m.role,
507
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
508
+ }));
509
+ const result = await this.contextEngineering.compact(contextMsgs, summarize);
510
+ const tokensBefore = this.compactor.estimateTokens(messages);
511
+ const tokensAfter = this.compactor.estimateTokens([{ role: 'assistant', content: result.summary }]);
512
+ return {
513
+ summary: result.summary + (result.reconstructionPrompt ? `\n\n${result.reconstructionPrompt}` : ''),
514
+ tokensBefore,
515
+ tokensAfter,
516
+ preservedMessages: [{ role: 'assistant', content: result.summary }],
517
+ references: result.references,
518
+ };
519
+ }
520
+ : undefined;
521
+ // Get model's actual context window - try OpenRouter first (real API data),
522
+ // then fall back to hardcoded ModelRegistry, then config, then default
523
+ const openRouterContext = getModelContextLength(this.config.model || '');
524
+ const registryInfo = modelRegistry.getModel(this.config.model || '');
525
+ const registryContext = registryInfo?.capabilities?.maxContextTokens;
526
+ const maxContextTokens = this.config.maxContextTokens
527
+ ?? openRouterContext // From OpenRouter API (e.g., GLM-4.7 = 202752)
528
+ ?? registryContext // From hardcoded registry (Claude, GPT-4o, etc.)
529
+ ?? 200000; // Fallback to 200K
530
+ this.autoCompactionManager = createAutoCompactionManager(this.compactor, {
531
+ mode: compactionConfig.mode ?? 'auto',
532
+ warningThreshold: 0.70, // Warn at 70% of model's context
533
+ autoCompactThreshold: 0.80, // Compact at 80% (changed from 0.90)
534
+ hardLimitThreshold: 0.95, // Hard limit at 95%
535
+ preserveRecentUserMessages: Math.ceil((compactionConfig.preserveRecentCount ?? 10) / 2),
536
+ preserveRecentAssistantMessages: Math.ceil((compactionConfig.preserveRecentCount ?? 10) / 2),
537
+ cooldownMs: 60000, // 1 minute cooldown
538
+ maxContextTokens, // Dynamic from model registry or config
539
+ compactHandler, // Use reversible compaction when contextEngineering is available
540
+ });
541
+ // Forward compactor events to observability
542
+ this.compactor.on(event => {
543
+ switch (event.type) {
544
+ case 'compaction.start':
545
+ this.observability?.logger?.info('Compaction started', {
546
+ messageCount: event.messageCount,
547
+ });
548
+ break;
549
+ case 'compaction.complete':
550
+ this.observability?.logger?.info('Compaction complete', {
551
+ tokensBefore: event.result.tokensBefore,
552
+ tokensAfter: event.result.tokensAfter,
553
+ compactedCount: event.result.compactedCount,
554
+ });
555
+ break;
556
+ case 'compaction.error':
557
+ this.observability?.logger?.error('Compaction error', {
558
+ error: event.error,
559
+ });
560
+ break;
561
+ }
562
+ });
563
+ // Forward auto-compaction events
564
+ this.autoCompactionManager.on((event) => {
565
+ switch (event.type) {
566
+ case 'autocompaction.warning':
567
+ this.observability?.logger?.warn('Context approaching limit', {
568
+ currentTokens: event.currentTokens,
569
+ ratio: event.ratio,
570
+ });
571
+ this.emit({
572
+ type: 'compaction.warning',
573
+ currentTokens: event.currentTokens,
574
+ threshold: Math.round(event.ratio * (this.config.maxContextTokens ?? 200000)),
575
+ });
576
+ break;
577
+ case 'autocompaction.triggered':
578
+ this.observability?.logger?.info('Auto-compaction triggered', {
579
+ mode: event.mode,
580
+ currentTokens: event.currentTokens,
581
+ });
582
+ break;
583
+ case 'autocompaction.completed':
584
+ this.observability?.logger?.info('Auto-compaction completed', {
585
+ tokensBefore: event.tokensBefore,
586
+ tokensAfter: event.tokensAfter,
587
+ reduction: event.reduction,
588
+ });
589
+ this.emit({
590
+ type: 'compaction.auto',
591
+ tokensBefore: event.tokensBefore,
592
+ tokensAfter: event.tokensAfter,
593
+ messagesCompacted: event.tokensBefore - event.tokensAfter,
594
+ });
595
+ break;
596
+ case 'autocompaction.hard_limit':
597
+ this.observability?.logger?.error('Context hard limit reached', {
598
+ currentTokens: event.currentTokens,
599
+ ratio: event.ratio,
600
+ });
601
+ break;
602
+ case 'autocompaction.emergency_truncate':
603
+ this.observability?.logger?.warn('Emergency truncation performed', {
604
+ reason: event.reason,
605
+ messagesBefore: event.messagesBefore,
606
+ messagesAfter: event.messagesAfter,
607
+ });
608
+ break;
609
+ }
610
+ });
611
+ }
612
+ // Note: FileChangeTracker requires a database instance which is not
613
+ // available at this point. Use initFileChangeTracker() to enable it
614
+ // after the agent is constructed with a database reference.
615
+ // This allows the feature to be optional and not require SQLite at all times.
616
+ }
617
+ /**
618
+ * Initialize the file change tracker with a database instance.
619
+ * Call this if you want undo capability for file operations.
620
+ *
621
+ * @param db - SQLite database instance from better-sqlite3
622
+ * @param sessionId - Session ID for tracking changes
623
+ */
624
+ initFileChangeTracker(db, sessionId) {
625
+ if (!isFeatureEnabled(this.config.fileChangeTracker)) {
626
+ return;
627
+ }
628
+ const trackerConfig = typeof this.config.fileChangeTracker === 'object'
629
+ ? this.config.fileChangeTracker
630
+ : {};
631
+ this.fileChangeTracker = createFileChangeTracker(db, sessionId, {
632
+ enabled: true,
633
+ maxFullContentBytes: trackerConfig.maxFullContentBytes ?? 50 * 1024,
634
+ });
635
+ this.observability?.logger?.info('File change tracker initialized', {
636
+ sessionId,
637
+ maxFullContentBytes: trackerConfig.maxFullContentBytes ?? 50 * 1024,
638
+ });
413
639
  }
414
640
  /**
415
641
  * Ensure all async initialization is complete before running.
@@ -438,9 +664,19 @@ export class ProductionAgent {
438
664
  const traceId = this.observability?.tracer?.startTrace('agent.run') || `trace-${Date.now()}`;
439
665
  this.emit({ type: 'start', task, traceId });
440
666
  this.observability?.logger?.info('Agent started', { task });
441
- // Lesson 26: Start trace capture session
442
- const traceSessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
443
- await this.traceCollector?.startSession(traceSessionId, task, this.config.model || 'default', {});
667
+ // Lesson 26: Start trace capture
668
+ // If session is already active (managed by REPL), start a task within it.
669
+ // Otherwise, start a new session for backward compatibility (single-task mode).
670
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
671
+ if (this.traceCollector?.isSessionActive()) {
672
+ // Session managed by REPL - just start a task
673
+ await this.traceCollector.startTask(taskId, task);
674
+ }
675
+ else {
676
+ // Single-task mode (backward compatibility) - start session with task
677
+ const traceSessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
678
+ await this.traceCollector?.startSession(traceSessionId, task, this.config.model || 'default', {});
679
+ }
444
680
  try {
445
681
  // Check for cancellation before starting
446
682
  cancellationToken?.throwIfCancellationRequested();
@@ -471,8 +707,14 @@ export class ProductionAgent {
471
707
  };
472
708
  this.emit({ type: 'complete', result });
473
709
  this.observability?.logger?.info('Agent completed', { duration, success: true });
474
- // Lesson 26: End trace capture session
475
- await this.traceCollector?.endSession({ success: true, output: response });
710
+ // Lesson 26: End trace capture
711
+ // If task is active (REPL mode), end the task. Otherwise end the session (single-task mode).
712
+ if (this.traceCollector?.isTaskActive()) {
713
+ await this.traceCollector.endTask({ success: true, output: response });
714
+ }
715
+ else if (this.traceCollector?.isSessionActive()) {
716
+ await this.traceCollector.endSession({ success: true, output: response });
717
+ }
476
718
  return result;
477
719
  }
478
720
  catch (err) {
@@ -484,8 +726,13 @@ export class ProductionAgent {
484
726
  const cleanupDuration = Date.now() - cleanupStart;
485
727
  this.emit({ type: 'cancellation.completed', cleanupDuration });
486
728
  this.observability?.logger?.info('Agent cancelled', { reason: error.message, cleanupDuration });
487
- // Lesson 26: End trace capture session on cancellation
488
- await this.traceCollector?.endSession({ success: false, failureReason: `Cancelled: ${error.message}` });
729
+ // Lesson 26: End trace capture on cancellation
730
+ if (this.traceCollector?.isTaskActive()) {
731
+ await this.traceCollector.endTask({ success: false, failureReason: `Cancelled: ${error.message}` });
732
+ }
733
+ else if (this.traceCollector?.isSessionActive()) {
734
+ await this.traceCollector.endSession({ success: false, failureReason: `Cancelled: ${error.message}` });
735
+ }
489
736
  return {
490
737
  success: false,
491
738
  response: '',
@@ -499,8 +746,13 @@ export class ProductionAgent {
499
746
  await this.observability?.tracer?.endTrace();
500
747
  this.emit({ type: 'error', error: error.message });
501
748
  this.observability?.logger?.error('Agent failed', { error: error.message });
502
- // Lesson 26: End trace capture session on error
503
- await this.traceCollector?.endSession({ success: false, failureReason: error.message });
749
+ // Lesson 26: End trace capture on error
750
+ if (this.traceCollector?.isTaskActive()) {
751
+ await this.traceCollector.endTask({ success: false, failureReason: error.message });
752
+ }
753
+ else if (this.traceCollector?.isSessionActive()) {
754
+ await this.traceCollector.endSession({ success: false, failureReason: error.message });
755
+ }
504
756
  return {
505
757
  success: false,
506
758
  response: '',
@@ -573,6 +825,11 @@ export class ProductionAgent {
573
825
  // Agent loop - now uses economics-based budget checking
574
826
  while (true) {
575
827
  this.state.iteration++;
828
+ // Record iteration start for tracing
829
+ this.traceCollector?.record({
830
+ type: 'iteration.start',
831
+ data: { iterationNumber: this.state.iteration },
832
+ });
576
833
  // =======================================================================
577
834
  // CANCELLATION CHECK
578
835
  // =======================================================================
@@ -774,14 +1031,21 @@ export class ProductionAgent {
774
1031
  // =====================================================================
775
1032
  // RESILIENT LLM CALL: Empty response retries + max_tokens continuation
776
1033
  // =====================================================================
777
- const MAX_EMPTY_RETRIES = 2;
778
- const MAX_CONTINUATIONS = 3;
1034
+ // Get resilience config
1035
+ const resilienceConfig = typeof this.config.resilience === 'object'
1036
+ ? this.config.resilience
1037
+ : {};
1038
+ const resilienceEnabled = isFeatureEnabled(this.config.resilience);
1039
+ const MAX_EMPTY_RETRIES = resilienceConfig.maxEmptyRetries ?? 2;
1040
+ const MAX_CONTINUATIONS = resilienceConfig.maxContinuations ?? 3;
1041
+ const AUTO_CONTINUE = resilienceConfig.autoContinue ?? true;
1042
+ const MIN_CONTENT_LENGTH = resilienceConfig.minContentLength ?? 1;
779
1043
  let response = await this.callLLM(messages);
780
1044
  let emptyRetries = 0;
781
1045
  let continuations = 0;
782
- // Phase 1: Handle empty responses with retry
783
- while (emptyRetries < MAX_EMPTY_RETRIES) {
784
- const hasContent = response.content && response.content.length > 0;
1046
+ // Phase 1: Handle empty responses with retry (if resilience enabled)
1047
+ while (resilienceEnabled && emptyRetries < MAX_EMPTY_RETRIES) {
1048
+ const hasContent = response.content && response.content.length >= MIN_CONTENT_LENGTH;
785
1049
  const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;
786
1050
  if (hasContent || hasToolCalls) {
787
1051
  // Valid response received
@@ -818,8 +1082,8 @@ export class ProductionAgent {
818
1082
  this.state.messages.push(nudgeMessage);
819
1083
  response = await this.callLLM(messages);
820
1084
  }
821
- // Phase 2: Handle max_tokens truncation with continuation
822
- if (response.stopReason === 'max_tokens' && !response.toolCalls?.length) {
1085
+ // Phase 2: Handle max_tokens truncation with continuation (if enabled)
1086
+ if (resilienceEnabled && AUTO_CONTINUE && response.stopReason === 'max_tokens' && !response.toolCalls?.length) {
823
1087
  let accumulatedContent = response.content || '';
824
1088
  while (continuations < MAX_CONTINUATIONS && response.stopReason === 'max_tokens') {
825
1089
  continuations++;
@@ -896,6 +1160,11 @@ export class ProductionAgent {
896
1160
  continuations,
897
1161
  });
898
1162
  }
1163
+ // Record iteration end for tracing (no tool calls case)
1164
+ this.traceCollector?.record({
1165
+ type: 'iteration.end',
1166
+ data: { iterationNumber: this.state.iteration },
1167
+ });
899
1168
  break;
900
1169
  }
901
1170
  // Execute tool calls
@@ -910,8 +1179,34 @@ export class ProductionAgent {
910
1179
  const MAX_TOOL_OUTPUT_CHARS = 8000; // ~2000 tokens max per tool output
911
1180
  // =======================================================================
912
1181
  // PROACTIVE BUDGET CHECK - compact BEFORE we overflow, not after
1182
+ // Uses AutoCompactionManager if available for sophisticated compaction
913
1183
  // =======================================================================
914
- if (this.economics) {
1184
+ const currentContextTokens = this.estimateContextTokens(messages);
1185
+ if (this.autoCompactionManager) {
1186
+ // Use the AutoCompactionManager for threshold-based compaction
1187
+ const compactionResult = await this.autoCompactionManager.checkAndMaybeCompact({
1188
+ currentTokens: currentContextTokens,
1189
+ messages: messages,
1190
+ });
1191
+ // Handle compaction result
1192
+ if (compactionResult.status === 'compacted' && compactionResult.compactedMessages) {
1193
+ // Replace messages with compacted version
1194
+ messages.length = 0;
1195
+ messages.push(...compactionResult.compactedMessages);
1196
+ this.state.messages.length = 0;
1197
+ this.state.messages.push(...compactionResult.compactedMessages);
1198
+ }
1199
+ else if (compactionResult.status === 'hard_limit') {
1200
+ // Hard limit reached - this is serious, emit error
1201
+ this.emit({
1202
+ type: 'error',
1203
+ error: `Context hard limit reached (${Math.round(compactionResult.ratio * 100)}% of max tokens)`,
1204
+ });
1205
+ break;
1206
+ }
1207
+ }
1208
+ else if (this.economics) {
1209
+ // Fallback to simple compaction
915
1210
  const currentUsage = this.economics.getUsage();
916
1211
  const budget = this.economics.getBudget();
917
1212
  const percentUsed = (currentUsage.tokens / budget.maxTokens) * 100;
@@ -965,6 +1260,25 @@ export class ProductionAgent {
965
1260
  messages.push(toolMessage);
966
1261
  this.state.messages.push(toolMessage);
967
1262
  }
1263
+ // Emit context health after adding tool results
1264
+ const currentTokenEstimate = this.estimateContextTokens(messages);
1265
+ const contextLimit = this.getMaxContextTokens();
1266
+ const percentUsed = Math.round((currentTokenEstimate / contextLimit) * 100);
1267
+ const avgTokensPerExchange = currentTokenEstimate / Math.max(1, this.state.iteration);
1268
+ const remainingTokens = contextLimit - currentTokenEstimate;
1269
+ const estimatedExchanges = Math.floor(remainingTokens / Math.max(1, avgTokensPerExchange));
1270
+ this.emit({
1271
+ type: 'context.health',
1272
+ currentTokens: currentTokenEstimate,
1273
+ maxTokens: contextLimit,
1274
+ estimatedExchanges,
1275
+ percentUsed,
1276
+ });
1277
+ // Record iteration end for tracing (after tool execution)
1278
+ this.traceCollector?.record({
1279
+ type: 'iteration.end',
1280
+ data: { iterationNumber: this.state.iteration },
1281
+ });
968
1282
  }
969
1283
  // =======================================================================
970
1284
  // REFLECTION (Lesson 16)
@@ -1012,6 +1326,11 @@ export class ProductionAgent {
1012
1326
  const rulesContent = this.rules?.getRulesContent() ?? '';
1013
1327
  const skillsPrompt = this.skillManager?.getActiveSkillsPrompt() ?? '';
1014
1328
  const memoryContext = this.memory?.getContextStrings(task) ?? [];
1329
+ // Get relevant learnings from past sessions
1330
+ const learningsContext = this.learningStore?.getLearningContext({
1331
+ query: task,
1332
+ maxLearnings: 5,
1333
+ }) ?? '';
1015
1334
  // Budget-aware codebase context selection
1016
1335
  let codebaseContextStr = '';
1017
1336
  if (this.codebaseContext) {
@@ -1059,9 +1378,10 @@ export class ProductionAgent {
1059
1378
  }
1060
1379
  // Build system prompt using cache-aware builder if available (Trick P)
1061
1380
  let systemPrompt;
1062
- // Combine memory and codebase context
1381
+ // Combine memory, learnings, and codebase context
1063
1382
  const combinedContext = [
1064
1383
  ...(memoryContext.length > 0 ? memoryContext : []),
1384
+ ...(learningsContext ? [learningsContext] : []),
1065
1385
  ...(codebaseContextStr ? [`\n## Relevant Code\n${codebaseContextStr}`] : []),
1066
1386
  ].join('\n');
1067
1387
  if (this.contextEngineering) {
@@ -1118,7 +1438,7 @@ export class ProductionAgent {
1118
1438
  return sum + Math.ceil(content.length / 3.5); // ~3.5 chars per token estimate
1119
1439
  }, 0);
1120
1440
  // Use context window size, not output token limit
1121
- const contextLimit = this.config.maxContextTokens || 100000;
1441
+ const contextLimit = this.getMaxContextTokens();
1122
1442
  this.emit({
1123
1443
  type: 'insight.context',
1124
1444
  currentTokens: estimatedTokens,
@@ -1193,6 +1513,38 @@ export class ProductionAgent {
1193
1513
  reason: actualModel !== model ? 'Routed based on complexity' : 'Default model',
1194
1514
  complexity: complexity <= 0.3 ? 'low' : complexity <= 0.7 ? 'medium' : 'high',
1195
1515
  });
1516
+ // Emit decision transparency event
1517
+ this.emit({
1518
+ type: 'decision.routing',
1519
+ model: actualModel,
1520
+ reason: actualModel !== model
1521
+ ? `Complexity ${(complexity * 100).toFixed(0)}% - using ${actualModel}`
1522
+ : 'Default model for current task',
1523
+ alternatives: actualModel !== model
1524
+ ? [{ model, rejected: 'complexity threshold exceeded' }]
1525
+ : undefined,
1526
+ });
1527
+ // Enhanced tracing: Record routing decision
1528
+ this.traceCollector?.record({
1529
+ type: 'decision',
1530
+ data: {
1531
+ type: 'routing',
1532
+ decision: `Selected model: ${actualModel}`,
1533
+ outcome: 'allowed',
1534
+ reasoning: actualModel !== model
1535
+ ? `Task complexity ${(complexity * 100).toFixed(0)}% exceeded threshold - routed to ${actualModel}`
1536
+ : `Default model ${model} suitable for task complexity ${(complexity * 100).toFixed(0)}%`,
1537
+ factors: [
1538
+ { name: 'complexity', value: complexity, weight: 0.8 },
1539
+ { name: 'hasTools', value: context.hasTools, weight: 0.1 },
1540
+ { name: 'taskType', value: context.taskType, weight: 0.1 },
1541
+ ],
1542
+ alternatives: actualModel !== model
1543
+ ? [{ option: model, reason: 'complexity threshold exceeded', rejected: true }]
1544
+ : undefined,
1545
+ confidence: 0.9,
1546
+ },
1547
+ });
1196
1548
  }
1197
1549
  else {
1198
1550
  response = await this.provider.chat(messages, {
@@ -1226,6 +1578,19 @@ export class ProductionAgent {
1226
1578
  durationMs: duration,
1227
1579
  },
1228
1580
  });
1581
+ // Enhanced tracing: Record thinking/reasoning blocks if present
1582
+ if (response.thinking) {
1583
+ this.traceCollector?.record({
1584
+ type: 'llm.thinking',
1585
+ data: {
1586
+ requestId,
1587
+ content: response.thinking,
1588
+ summarized: response.thinking.length > 10000, // Summarize if very long
1589
+ originalLength: response.thinking.length,
1590
+ durationMs: duration,
1591
+ },
1592
+ });
1593
+ }
1229
1594
  // Record metrics
1230
1595
  this.observability?.metrics?.recordLLMCall(response.usage?.inputTokens || 0, response.usage?.outputTokens || 0, duration, actualModel, response.usage?.cost // Actual cost from provider (e.g., OpenRouter)
1231
1596
  );
@@ -1299,6 +1664,7 @@ export class ProductionAgent {
1299
1664
  type: 'plan.change.queued',
1300
1665
  tool: toolCall.name,
1301
1666
  changeId: change?.id,
1667
+ summary: this.formatToolArgsForPlan(toolCall.name, toolCall.arguments),
1302
1668
  });
1303
1669
  // Return a message indicating the change was queued
1304
1670
  const queueMessage = `[PLAN MODE] Change queued for approval:\n` +
@@ -1329,6 +1695,32 @@ export class ProductionAgent {
1329
1695
  policy: evaluation.policy,
1330
1696
  reason: evaluation.reason,
1331
1697
  });
1698
+ // Emit decision transparency event
1699
+ this.emit({
1700
+ type: 'decision.tool',
1701
+ tool: toolCall.name,
1702
+ decision: evaluation.policy === 'forbidden' ? 'blocked'
1703
+ : evaluation.policy === 'prompt' ? 'prompted'
1704
+ : 'allowed',
1705
+ policyMatch: evaluation.reason,
1706
+ });
1707
+ // Enhanced tracing: Record policy decision
1708
+ this.traceCollector?.record({
1709
+ type: 'decision',
1710
+ data: {
1711
+ type: 'policy',
1712
+ decision: `Tool ${toolCall.name}: ${evaluation.policy}`,
1713
+ outcome: evaluation.policy === 'forbidden' ? 'blocked'
1714
+ : evaluation.policy === 'prompt' ? 'deferred'
1715
+ : 'allowed',
1716
+ reasoning: evaluation.reason,
1717
+ factors: [
1718
+ { name: 'policy', value: evaluation.policy },
1719
+ { name: 'requiresApproval', value: evaluation.requiresApproval ?? false },
1720
+ ],
1721
+ confidence: evaluation.intent?.confidence ?? 0.8,
1722
+ },
1723
+ });
1332
1724
  // Handle forbidden policy - always block
1333
1725
  if (evaluation.policy === 'forbidden') {
1334
1726
  throw new Error(`Forbidden by policy: ${evaluation.reason}`);
@@ -1594,6 +1986,12 @@ export class ProductionAgent {
1594
1986
  if (toolName === 'delete_file') {
1595
1987
  return `Delete: ${args.path || args.file_path}`;
1596
1988
  }
1989
+ if (toolName === 'spawn_agent' || toolName === 'researcher') {
1990
+ const task = String(args.task || args.prompt || args.goal || '');
1991
+ const model = args.model ? ` (${args.model})` : '';
1992
+ const firstLine = task.split('\n')[0].slice(0, 100);
1993
+ return `${firstLine}${task.length > 100 ? '...' : ''}${model}`;
1994
+ }
1597
1995
  // Generic
1598
1996
  return `Args: ${JSON.stringify(args).slice(0, 100)}...`;
1599
1997
  }
@@ -1621,6 +2019,27 @@ export class ProductionAgent {
1621
2019
  getState() {
1622
2020
  return { ...this.state };
1623
2021
  }
2022
+ /**
2023
+ * Get the maximum context tokens for this agent's model.
2024
+ * Priority: user config > OpenRouter API > hardcoded ModelRegistry > 200K default
2025
+ */
2026
+ getMaxContextTokens() {
2027
+ if (this.config.maxContextTokens) {
2028
+ return this.config.maxContextTokens;
2029
+ }
2030
+ // Try OpenRouter API cache (has real data for GLM-4.7, etc.)
2031
+ const openRouterContext = getModelContextLength(this.config.model || '');
2032
+ if (openRouterContext) {
2033
+ return openRouterContext;
2034
+ }
2035
+ // Fall back to hardcoded registry
2036
+ const registryInfo = modelRegistry.getModel(this.config.model || '');
2037
+ if (registryInfo?.capabilities?.maxContextTokens) {
2038
+ return registryInfo.capabilities.maxContextTokens;
2039
+ }
2040
+ // Default
2041
+ return 200000;
2042
+ }
1624
2043
  /**
1625
2044
  * Get the trace collector (Lesson 26).
1626
2045
  * Returns null if trace capture is not enabled.
@@ -1628,6 +2047,67 @@ export class ProductionAgent {
1628
2047
  getTraceCollector() {
1629
2048
  return this.traceCollector;
1630
2049
  }
2050
+ /**
2051
+ * Get the learning store for cross-session learning.
2052
+ * Returns null if learning store is not enabled.
2053
+ */
2054
+ getLearningStore() {
2055
+ return this.learningStore;
2056
+ }
2057
+ /**
2058
+ * Get the auto-compaction manager.
2059
+ * Returns null if compaction is not enabled.
2060
+ */
2061
+ getAutoCompactionManager() {
2062
+ return this.autoCompactionManager;
2063
+ }
2064
+ /**
2065
+ * Get the file change tracker for undo capability.
2066
+ * Returns null if file change tracking is not enabled.
2067
+ */
2068
+ getFileChangeTracker() {
2069
+ return this.fileChangeTracker;
2070
+ }
2071
+ /**
2072
+ * Record a file change for potential undo.
2073
+ * No-op if file change tracking is not enabled.
2074
+ *
2075
+ * @param params - Change details
2076
+ * @returns Change ID if tracked, -1 otherwise
2077
+ */
2078
+ async trackFileChange(params) {
2079
+ if (!this.fileChangeTracker) {
2080
+ return -1;
2081
+ }
2082
+ return this.fileChangeTracker.recordChange({
2083
+ filePath: params.filePath,
2084
+ operation: params.operation,
2085
+ contentBefore: params.contentBefore,
2086
+ contentAfter: params.contentAfter,
2087
+ turnNumber: this.state.iteration,
2088
+ toolCallId: params.toolCallId,
2089
+ });
2090
+ }
2091
+ /**
2092
+ * Undo the last change to a specific file.
2093
+ * Returns null if file change tracking is not enabled.
2094
+ */
2095
+ async undoLastFileChange(filePath) {
2096
+ if (!this.fileChangeTracker) {
2097
+ return null;
2098
+ }
2099
+ return this.fileChangeTracker.undoLastChange(filePath);
2100
+ }
2101
+ /**
2102
+ * Undo all changes in the current turn.
2103
+ * Returns null if file change tracking is not enabled.
2104
+ */
2105
+ async undoCurrentTurn() {
2106
+ if (!this.fileChangeTracker) {
2107
+ return null;
2108
+ }
2109
+ return this.fileChangeTracker.undoTurn(this.state.iteration);
2110
+ }
1631
2111
  /**
1632
2112
  * Subscribe to events.
1633
2113
  */
@@ -2280,6 +2760,8 @@ export class ProductionAgent {
2280
2760
  this.emit({ type: 'agent.spawn', agentId: `spawn-${Date.now()}`, name: agentName, task });
2281
2761
  this.observability?.logger?.info('Spawning agent', { name: agentName, task });
2282
2762
  const startTime = Date.now();
2763
+ const childSessionId = `subagent-${agentName}-${Date.now()}`;
2764
+ const childTraceId = `trace-${childSessionId}`;
2283
2765
  try {
2284
2766
  // Filter tools for this agent
2285
2767
  const agentTools = filterToolsForAgent(agentDef, Array.from(this.tools.values()));
@@ -2288,13 +2770,27 @@ export class ProductionAgent {
2288
2770
  const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
2289
2771
  ? agentDef.model
2290
2772
  : this.config.model;
2773
+ // Get subagent config with defaults
2774
+ // Note: subagent config is SubagentConfig | false from buildConfig
2775
+ const subagentConfig = this.config.subagent;
2776
+ const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
2777
+ const defaultMaxIterations = hasSubagentConfig
2778
+ ? subagentConfig.defaultMaxIterations ?? 10
2779
+ : 10;
2780
+ const subagentTimeout = hasSubagentConfig
2781
+ ? subagentConfig.defaultTimeout ?? 120000
2782
+ : 120000;
2291
2783
  // Create a sub-agent with the agent's config
2292
2784
  const subAgent = new ProductionAgent({
2293
2785
  provider: this.provider,
2294
2786
  tools: agentTools,
2787
+ // Pass toolResolver so subagent can lazy-load MCP tools
2788
+ toolResolver: this.toolResolver || undefined,
2789
+ // Pass MCP tool summaries so subagent knows what tools are available
2790
+ mcpToolSummaries: this.config.mcpToolSummaries,
2295
2791
  systemPrompt: agentDef.systemPrompt,
2296
2792
  model: resolvedModel,
2297
- maxIterations: agentDef.maxIterations || 30,
2793
+ maxIterations: agentDef.maxIterations || defaultMaxIterations,
2298
2794
  // Inherit some features but keep subagent simpler
2299
2795
  memory: false,
2300
2796
  planning: false,
@@ -2317,21 +2813,88 @@ export class ProductionAgent {
2317
2813
  const taggedEvent = { ...event, subagent: agentName };
2318
2814
  this.emit(taggedEvent);
2319
2815
  });
2320
- // Run the task
2321
- const result = await subAgent.run(task);
2322
- const duration = Date.now() - startTime;
2323
- const spawnResult = {
2324
- success: result.success,
2325
- output: result.response || result.error || '',
2326
- metrics: {
2327
- tokens: result.metrics.totalTokens,
2328
- duration,
2329
- toolCalls: result.metrics.toolCalls,
2330
- },
2331
- };
2332
- this.emit({ type: 'agent.complete', agentId: agentName, success: result.success });
2333
- await subAgent.cleanup();
2334
- return spawnResult;
2816
+ // Create timeout token for subagent execution
2817
+ const timeoutSource = createTimeoutToken(subagentTimeout);
2818
+ // Link parent's cancellation with subagent timeout so ESC propagates to subagents
2819
+ const parentSource = this.cancellation?.getSource();
2820
+ const effectiveSource = parentSource
2821
+ ? createLinkedToken(parentSource, timeoutSource)
2822
+ : timeoutSource;
2823
+ try {
2824
+ // Run the task with cancellation propagation from parent
2825
+ const result = await race(subAgent.run(task), effectiveSource.token);
2826
+ const duration = Date.now() - startTime;
2827
+ const spawnResult = {
2828
+ success: result.success,
2829
+ output: result.response || result.error || '',
2830
+ metrics: {
2831
+ tokens: result.metrics.totalTokens,
2832
+ duration,
2833
+ toolCalls: result.metrics.toolCalls,
2834
+ },
2835
+ };
2836
+ this.emit({ type: 'agent.complete', agentId: agentName, success: result.success });
2837
+ // Enhanced tracing: Record subagent completion
2838
+ this.traceCollector?.record({
2839
+ type: 'subagent.link',
2840
+ data: {
2841
+ parentSessionId: this.traceCollector.getSessionId() || 'unknown',
2842
+ childSessionId,
2843
+ childTraceId,
2844
+ childConfig: {
2845
+ agentType: agentName,
2846
+ model: resolvedModel || 'default',
2847
+ task,
2848
+ tools: agentTools.map(t => t.name),
2849
+ },
2850
+ spawnContext: {
2851
+ reason: `Delegated task: ${task.slice(0, 100)}`,
2852
+ expectedOutcome: agentDef.description,
2853
+ parentIteration: this.state.iteration,
2854
+ },
2855
+ result: {
2856
+ success: result.success,
2857
+ summary: (result.response || result.error || '').slice(0, 500),
2858
+ tokensUsed: result.metrics.totalTokens,
2859
+ durationMs: duration,
2860
+ },
2861
+ },
2862
+ });
2863
+ await subAgent.cleanup();
2864
+ return spawnResult;
2865
+ }
2866
+ catch (err) {
2867
+ // Handle cancellation (user ESC or timeout) for cleaner error messages
2868
+ if (isCancellationError(err)) {
2869
+ const duration = Date.now() - startTime;
2870
+ const isUserCancellation = parentSource?.isCancellationRequested;
2871
+ const reason = isUserCancellation
2872
+ ? 'User cancelled'
2873
+ : `Timed out after ${subagentTimeout}ms`;
2874
+ this.emit({ type: 'agent.error', agentId: agentName, error: reason });
2875
+ // Try to cleanup the subagent gracefully
2876
+ try {
2877
+ await subAgent.cleanup();
2878
+ }
2879
+ catch {
2880
+ // Ignore cleanup errors on cancellation
2881
+ }
2882
+ const output = isUserCancellation
2883
+ ? `Subagent '${agentName}' was cancelled by user.`
2884
+ : `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s. The task may be too complex or the model may be slow.`;
2885
+ return {
2886
+ success: false,
2887
+ output,
2888
+ metrics: { tokens: 0, duration, toolCalls: 0 },
2889
+ };
2890
+ }
2891
+ throw err; // Re-throw non-cancellation errors
2892
+ }
2893
+ finally {
2894
+ // Dispose both sources (linked source disposes its internal state, timeout source handles its timer)
2895
+ effectiveSource.dispose();
2896
+ timeoutSource.dispose();
2897
+ }
2335
2898
  }
2336
2899
  catch (err) {
2337
2900
  const error = err instanceof Error ? err.message : String(err);
@@ -2814,6 +3377,18 @@ If the task is a simple question or doesn't need specialized handling, set bestA
2814
3377
  // =========================================================================
2815
3378
  // SKILLS METHODS
2816
3379
  // =========================================================================
3380
+ /**
3381
+ * Get the skill manager instance for advanced operations.
3382
+ */
3383
+ getSkillManager() {
3384
+ return this.skillManager;
3385
+ }
3386
+ /**
3387
+ * Get the agent registry instance for advanced operations.
3388
+ */
3389
+ getAgentRegistry() {
3390
+ return this.agentRegistry;
3391
+ }
2817
3392
  /**
2818
3393
  * Get all loaded skills.
2819
3394
  */
@@ -2860,6 +3435,34 @@ If the task is a simple question or doesn't need specialized handling, set bestA
2860
3435
  findMatchingSkills(query) {
2861
3436
  return this.skillManager?.findMatchingSkills(query) || [];
2862
3437
  }
3438
+ /**
3439
+ * Get the capabilities registry for unified discovery.
3440
+ * Lazily creates and populates the registry on first access.
3441
+ */
3442
+ getCapabilitiesRegistry() {
3443
+ if (!this.capabilitiesRegistry) {
3444
+ this.capabilitiesRegistry = createCapabilitiesRegistry();
3445
+ // Register sources
3446
+ this.capabilitiesRegistry.registerToolRegistry({
3447
+ getTools: () => this.getTools(),
3448
+ });
3449
+ if (this.skillManager) {
3450
+ this.capabilitiesRegistry.registerSkillManager(this.skillManager);
3451
+ }
3452
+ if (this.agentRegistry) {
3453
+ this.capabilitiesRegistry.registerAgentRegistry(this.agentRegistry);
3454
+ }
3455
+ // MCP client is registered externally if available
3456
+ }
3457
+ return this.capabilitiesRegistry;
3458
+ }
3459
+ /**
3460
+ * Register an MCP client with the capabilities registry.
3461
+ */
3462
+ registerMCPClient(client) {
3463
+ const registry = this.getCapabilitiesRegistry();
3464
+ registry.registerMCPClient(client);
3465
+ }
2863
3466
  /**
2864
3467
  * Get formatted list of available skills.
2865
3468
  */