attocode 0.1.2 → 0.1.3

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 (69) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +124 -0
  3. package/dist/src/agent.d.ts +73 -1
  4. package/dist/src/agent.d.ts.map +1 -1
  5. package/dist/src/agent.js +443 -26
  6. package/dist/src/agent.js.map +1 -1
  7. package/dist/src/commands/agents-commands.d.ts +24 -0
  8. package/dist/src/commands/agents-commands.d.ts.map +1 -0
  9. package/dist/src/commands/agents-commands.js +284 -0
  10. package/dist/src/commands/agents-commands.js.map +1 -0
  11. package/dist/src/commands/handler.d.ts.map +1 -1
  12. package/dist/src/commands/handler.js +135 -19
  13. package/dist/src/commands/handler.js.map +1 -1
  14. package/dist/src/commands/init-commands.d.ts +35 -0
  15. package/dist/src/commands/init-commands.d.ts.map +1 -0
  16. package/dist/src/commands/init-commands.js +187 -0
  17. package/dist/src/commands/init-commands.js.map +1 -0
  18. package/dist/src/commands/skills-commands.d.ts +26 -0
  19. package/dist/src/commands/skills-commands.d.ts.map +1 -0
  20. package/dist/src/commands/skills-commands.js +309 -0
  21. package/dist/src/commands/skills-commands.js.map +1 -0
  22. package/dist/src/commands/types.d.ts +13 -2
  23. package/dist/src/commands/types.d.ts.map +1 -1
  24. package/dist/src/defaults.d.ts +21 -1
  25. package/dist/src/defaults.d.ts.map +1 -1
  26. package/dist/src/defaults.js +44 -0
  27. package/dist/src/defaults.js.map +1 -1
  28. package/dist/src/integrations/agent-registry.d.ts +68 -2
  29. package/dist/src/integrations/agent-registry.d.ts.map +1 -1
  30. package/dist/src/integrations/agent-registry.js +230 -23
  31. package/dist/src/integrations/agent-registry.js.map +1 -1
  32. package/dist/src/integrations/cancellation.d.ts +5 -0
  33. package/dist/src/integrations/cancellation.d.ts.map +1 -1
  34. package/dist/src/integrations/cancellation.js +7 -0
  35. package/dist/src/integrations/cancellation.js.map +1 -1
  36. package/dist/src/integrations/capabilities.d.ts +160 -0
  37. package/dist/src/integrations/capabilities.d.ts.map +1 -0
  38. package/dist/src/integrations/capabilities.js +426 -0
  39. package/dist/src/integrations/capabilities.js.map +1 -0
  40. package/dist/src/integrations/context-engineering.d.ts +6 -1
  41. package/dist/src/integrations/context-engineering.d.ts.map +1 -1
  42. package/dist/src/integrations/context-engineering.js +7 -0
  43. package/dist/src/integrations/context-engineering.js.map +1 -1
  44. package/dist/src/integrations/index.d.ts +6 -2
  45. package/dist/src/integrations/index.d.ts.map +1 -1
  46. package/dist/src/integrations/index.js +10 -2
  47. package/dist/src/integrations/index.js.map +1 -1
  48. package/dist/src/integrations/skill-executor.d.ts +113 -0
  49. package/dist/src/integrations/skill-executor.d.ts.map +1 -0
  50. package/dist/src/integrations/skill-executor.js +270 -0
  51. package/dist/src/integrations/skill-executor.js.map +1 -0
  52. package/dist/src/integrations/skills.d.ts +98 -7
  53. package/dist/src/integrations/skills.d.ts.map +1 -1
  54. package/dist/src/integrations/skills.js +210 -11
  55. package/dist/src/integrations/skills.js.map +1 -1
  56. package/dist/src/tui/app.d.ts.map +1 -1
  57. package/dist/src/tui/app.js +131 -14
  58. package/dist/src/tui/app.js.map +1 -1
  59. package/dist/src/tui/index.d.ts +1 -0
  60. package/dist/src/tui/index.d.ts.map +1 -1
  61. package/dist/src/tui/index.js +2 -0
  62. package/dist/src/tui/index.js.map +1 -1
  63. package/dist/src/tui/transparency-aggregator.d.ts +100 -0
  64. package/dist/src/tui/transparency-aggregator.d.ts.map +1 -0
  65. package/dist/src/tui/transparency-aggregator.js +234 -0
  66. package/dist/src/tui/transparency-aggregator.js.map +1 -0
  67. package/dist/src/types.d.ts +94 -0
  68. package/dist/src/types.d.ts.map +1 -1
  69. package/package.json +1 -1
package/dist/src/agent.js CHANGED
@@ -21,7 +21,7 @@
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
27
  // Spawn agent tool for LLM-driven subagent delegation
@@ -62,6 +62,11 @@ export class ProductionAgent {
62
62
  pendingPlanManager;
63
63
  interactivePlanner = null;
64
64
  recursiveContext = null;
65
+ learningStore = null;
66
+ compactor = null;
67
+ autoCompactionManager = null;
68
+ fileChangeTracker = null;
69
+ capabilitiesRegistry = null;
65
70
  toolResolver = null;
66
71
  // Initialization tracking
67
72
  initPromises = [];
@@ -410,6 +415,189 @@ export class ProductionAgent {
410
415
  }
411
416
  });
412
417
  }
418
+ // Learning Store (cross-session learning from failures)
419
+ // Connects to the failure tracker in contextEngineering for automatic learning extraction
420
+ if (isFeatureEnabled(this.config.learningStore)) {
421
+ const learningConfig = typeof this.config.learningStore === 'object'
422
+ ? this.config.learningStore
423
+ : {};
424
+ this.learningStore = createLearningStore({
425
+ dbPath: learningConfig.dbPath ?? '.agent/learnings.db',
426
+ requireValidation: learningConfig.requireValidation ?? true,
427
+ autoValidateThreshold: learningConfig.autoValidateThreshold ?? 0.9,
428
+ maxLearnings: learningConfig.maxLearnings ?? 500,
429
+ });
430
+ // Connect to the failure tracker if available
431
+ if (this.contextEngineering) {
432
+ const failureTracker = this.contextEngineering.getFailureTracker();
433
+ if (failureTracker) {
434
+ this.learningStore.connectFailureTracker(failureTracker);
435
+ }
436
+ }
437
+ // Forward learning events to observability
438
+ this.learningStore.on(event => {
439
+ switch (event.type) {
440
+ case 'learning.proposed':
441
+ this.observability?.logger?.info('Learning proposed', {
442
+ learningId: event.learning.id,
443
+ description: event.learning.description,
444
+ });
445
+ this.emit({
446
+ type: 'learning.proposed',
447
+ learningId: event.learning.id,
448
+ description: event.learning.description,
449
+ });
450
+ break;
451
+ case 'learning.validated':
452
+ this.observability?.logger?.info('Learning validated', {
453
+ learningId: event.learningId,
454
+ });
455
+ this.emit({ type: 'learning.validated', learningId: event.learningId });
456
+ break;
457
+ case 'learning.applied':
458
+ this.observability?.logger?.debug('Learning applied', {
459
+ learningId: event.learningId,
460
+ context: event.context,
461
+ });
462
+ this.emit({
463
+ type: 'learning.applied',
464
+ learningId: event.learningId,
465
+ context: event.context,
466
+ });
467
+ break;
468
+ case 'pattern.extracted':
469
+ this.observability?.logger?.info('Pattern extracted as learning', {
470
+ pattern: event.pattern.description,
471
+ learningId: event.learning.id,
472
+ });
473
+ break;
474
+ }
475
+ });
476
+ }
477
+ // Auto-Compaction Manager (sophisticated context compaction)
478
+ // Uses the Compactor for LLM-based summarization with threshold monitoring
479
+ if (isFeatureEnabled(this.config.compaction)) {
480
+ const compactionConfig = typeof this.config.compaction === 'object'
481
+ ? this.config.compaction
482
+ : {};
483
+ // Create the compactor (requires provider for LLM summarization)
484
+ this.compactor = createCompactor(this.provider, {
485
+ enabled: true,
486
+ tokenThreshold: compactionConfig.tokenThreshold ?? 80000,
487
+ preserveRecentCount: compactionConfig.preserveRecentCount ?? 10,
488
+ preserveToolResults: compactionConfig.preserveToolResults ?? true,
489
+ summaryMaxTokens: compactionConfig.summaryMaxTokens ?? 2000,
490
+ summaryModel: compactionConfig.summaryModel,
491
+ });
492
+ // Create the auto-compaction manager with threshold monitoring
493
+ this.autoCompactionManager = createAutoCompactionManager(this.compactor, {
494
+ mode: compactionConfig.mode ?? 'auto',
495
+ warningThreshold: 0.80,
496
+ autoCompactThreshold: 0.90,
497
+ hardLimitThreshold: 0.98,
498
+ preserveRecentUserMessages: Math.ceil((compactionConfig.preserveRecentCount ?? 10) / 2),
499
+ preserveRecentAssistantMessages: Math.ceil((compactionConfig.preserveRecentCount ?? 10) / 2),
500
+ cooldownMs: 60000, // 1 minute cooldown
501
+ maxContextTokens: this.config.maxContextTokens ?? 200000,
502
+ });
503
+ // Forward compactor events to observability
504
+ this.compactor.on(event => {
505
+ switch (event.type) {
506
+ case 'compaction.start':
507
+ this.observability?.logger?.info('Compaction started', {
508
+ messageCount: event.messageCount,
509
+ });
510
+ break;
511
+ case 'compaction.complete':
512
+ this.observability?.logger?.info('Compaction complete', {
513
+ tokensBefore: event.result.tokensBefore,
514
+ tokensAfter: event.result.tokensAfter,
515
+ compactedCount: event.result.compactedCount,
516
+ });
517
+ break;
518
+ case 'compaction.error':
519
+ this.observability?.logger?.error('Compaction error', {
520
+ error: event.error,
521
+ });
522
+ break;
523
+ }
524
+ });
525
+ // Forward auto-compaction events
526
+ this.autoCompactionManager.on((event) => {
527
+ switch (event.type) {
528
+ case 'autocompaction.warning':
529
+ this.observability?.logger?.warn('Context approaching limit', {
530
+ currentTokens: event.currentTokens,
531
+ ratio: event.ratio,
532
+ });
533
+ this.emit({
534
+ type: 'compaction.warning',
535
+ currentTokens: event.currentTokens,
536
+ threshold: Math.round(event.ratio * (this.config.maxContextTokens ?? 200000)),
537
+ });
538
+ break;
539
+ case 'autocompaction.triggered':
540
+ this.observability?.logger?.info('Auto-compaction triggered', {
541
+ mode: event.mode,
542
+ currentTokens: event.currentTokens,
543
+ });
544
+ break;
545
+ case 'autocompaction.completed':
546
+ this.observability?.logger?.info('Auto-compaction completed', {
547
+ tokensBefore: event.tokensBefore,
548
+ tokensAfter: event.tokensAfter,
549
+ reduction: event.reduction,
550
+ });
551
+ this.emit({
552
+ type: 'compaction.auto',
553
+ tokensBefore: event.tokensBefore,
554
+ tokensAfter: event.tokensAfter,
555
+ messagesCompacted: event.tokensBefore - event.tokensAfter,
556
+ });
557
+ break;
558
+ case 'autocompaction.hard_limit':
559
+ this.observability?.logger?.error('Context hard limit reached', {
560
+ currentTokens: event.currentTokens,
561
+ ratio: event.ratio,
562
+ });
563
+ break;
564
+ case 'autocompaction.emergency_truncate':
565
+ this.observability?.logger?.warn('Emergency truncation performed', {
566
+ reason: event.reason,
567
+ messagesBefore: event.messagesBefore,
568
+ messagesAfter: event.messagesAfter,
569
+ });
570
+ break;
571
+ }
572
+ });
573
+ }
574
+ // Note: FileChangeTracker requires a database instance which is not
575
+ // available at this point. Use initFileChangeTracker() to enable it
576
+ // after the agent is constructed with a database reference.
577
+ // This allows the feature to be optional and not require SQLite at all times.
578
+ }
579
+ /**
580
+ * Initialize the file change tracker with a database instance.
581
+ * Call this if you want undo capability for file operations.
582
+ *
583
+ * @param db - SQLite database instance from better-sqlite3
584
+ * @param sessionId - Session ID for tracking changes
585
+ */
586
+ initFileChangeTracker(db, sessionId) {
587
+ if (!isFeatureEnabled(this.config.fileChangeTracker)) {
588
+ return;
589
+ }
590
+ const trackerConfig = typeof this.config.fileChangeTracker === 'object'
591
+ ? this.config.fileChangeTracker
592
+ : {};
593
+ this.fileChangeTracker = createFileChangeTracker(db, sessionId, {
594
+ enabled: true,
595
+ maxFullContentBytes: trackerConfig.maxFullContentBytes ?? 50 * 1024,
596
+ });
597
+ this.observability?.logger?.info('File change tracker initialized', {
598
+ sessionId,
599
+ maxFullContentBytes: trackerConfig.maxFullContentBytes ?? 50 * 1024,
600
+ });
413
601
  }
414
602
  /**
415
603
  * Ensure all async initialization is complete before running.
@@ -774,14 +962,21 @@ export class ProductionAgent {
774
962
  // =====================================================================
775
963
  // RESILIENT LLM CALL: Empty response retries + max_tokens continuation
776
964
  // =====================================================================
777
- const MAX_EMPTY_RETRIES = 2;
778
- const MAX_CONTINUATIONS = 3;
965
+ // Get resilience config
966
+ const resilienceConfig = typeof this.config.resilience === 'object'
967
+ ? this.config.resilience
968
+ : {};
969
+ const resilienceEnabled = isFeatureEnabled(this.config.resilience);
970
+ const MAX_EMPTY_RETRIES = resilienceConfig.maxEmptyRetries ?? 2;
971
+ const MAX_CONTINUATIONS = resilienceConfig.maxContinuations ?? 3;
972
+ const AUTO_CONTINUE = resilienceConfig.autoContinue ?? true;
973
+ const MIN_CONTENT_LENGTH = resilienceConfig.minContentLength ?? 1;
779
974
  let response = await this.callLLM(messages);
780
975
  let emptyRetries = 0;
781
976
  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;
977
+ // Phase 1: Handle empty responses with retry (if resilience enabled)
978
+ while (resilienceEnabled && emptyRetries < MAX_EMPTY_RETRIES) {
979
+ const hasContent = response.content && response.content.length >= MIN_CONTENT_LENGTH;
785
980
  const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;
786
981
  if (hasContent || hasToolCalls) {
787
982
  // Valid response received
@@ -818,8 +1013,8 @@ export class ProductionAgent {
818
1013
  this.state.messages.push(nudgeMessage);
819
1014
  response = await this.callLLM(messages);
820
1015
  }
821
- // Phase 2: Handle max_tokens truncation with continuation
822
- if (response.stopReason === 'max_tokens' && !response.toolCalls?.length) {
1016
+ // Phase 2: Handle max_tokens truncation with continuation (if enabled)
1017
+ if (resilienceEnabled && AUTO_CONTINUE && response.stopReason === 'max_tokens' && !response.toolCalls?.length) {
823
1018
  let accumulatedContent = response.content || '';
824
1019
  while (continuations < MAX_CONTINUATIONS && response.stopReason === 'max_tokens') {
825
1020
  continuations++;
@@ -910,8 +1105,34 @@ export class ProductionAgent {
910
1105
  const MAX_TOOL_OUTPUT_CHARS = 8000; // ~2000 tokens max per tool output
911
1106
  // =======================================================================
912
1107
  // PROACTIVE BUDGET CHECK - compact BEFORE we overflow, not after
1108
+ // Uses AutoCompactionManager if available for sophisticated compaction
913
1109
  // =======================================================================
914
- if (this.economics) {
1110
+ const currentContextTokens = this.estimateContextTokens(messages);
1111
+ if (this.autoCompactionManager) {
1112
+ // Use the AutoCompactionManager for threshold-based compaction
1113
+ const compactionResult = await this.autoCompactionManager.checkAndMaybeCompact({
1114
+ currentTokens: currentContextTokens,
1115
+ messages: messages,
1116
+ });
1117
+ // Handle compaction result
1118
+ if (compactionResult.status === 'compacted' && compactionResult.compactedMessages) {
1119
+ // Replace messages with compacted version
1120
+ messages.length = 0;
1121
+ messages.push(...compactionResult.compactedMessages);
1122
+ this.state.messages.length = 0;
1123
+ this.state.messages.push(...compactionResult.compactedMessages);
1124
+ }
1125
+ else if (compactionResult.status === 'hard_limit') {
1126
+ // Hard limit reached - this is serious, emit error
1127
+ this.emit({
1128
+ type: 'error',
1129
+ error: `Context hard limit reached (${Math.round(compactionResult.ratio * 100)}% of max tokens)`,
1130
+ });
1131
+ break;
1132
+ }
1133
+ }
1134
+ else if (this.economics) {
1135
+ // Fallback to simple compaction
915
1136
  const currentUsage = this.economics.getUsage();
916
1137
  const budget = this.economics.getBudget();
917
1138
  const percentUsed = (currentUsage.tokens / budget.maxTokens) * 100;
@@ -965,6 +1186,20 @@ export class ProductionAgent {
965
1186
  messages.push(toolMessage);
966
1187
  this.state.messages.push(toolMessage);
967
1188
  }
1189
+ // Emit context health after adding tool results
1190
+ const currentTokenEstimate = this.estimateContextTokens(messages);
1191
+ const contextLimit = this.config.maxContextTokens || 100000;
1192
+ const percentUsed = Math.round((currentTokenEstimate / contextLimit) * 100);
1193
+ const avgTokensPerExchange = currentTokenEstimate / Math.max(1, this.state.iteration);
1194
+ const remainingTokens = contextLimit - currentTokenEstimate;
1195
+ const estimatedExchanges = Math.floor(remainingTokens / Math.max(1, avgTokensPerExchange));
1196
+ this.emit({
1197
+ type: 'context.health',
1198
+ currentTokens: currentTokenEstimate,
1199
+ maxTokens: contextLimit,
1200
+ estimatedExchanges,
1201
+ percentUsed,
1202
+ });
968
1203
  }
969
1204
  // =======================================================================
970
1205
  // REFLECTION (Lesson 16)
@@ -1012,6 +1247,11 @@ export class ProductionAgent {
1012
1247
  const rulesContent = this.rules?.getRulesContent() ?? '';
1013
1248
  const skillsPrompt = this.skillManager?.getActiveSkillsPrompt() ?? '';
1014
1249
  const memoryContext = this.memory?.getContextStrings(task) ?? [];
1250
+ // Get relevant learnings from past sessions
1251
+ const learningsContext = this.learningStore?.getLearningContext({
1252
+ query: task,
1253
+ maxLearnings: 5,
1254
+ }) ?? '';
1015
1255
  // Budget-aware codebase context selection
1016
1256
  let codebaseContextStr = '';
1017
1257
  if (this.codebaseContext) {
@@ -1059,9 +1299,10 @@ export class ProductionAgent {
1059
1299
  }
1060
1300
  // Build system prompt using cache-aware builder if available (Trick P)
1061
1301
  let systemPrompt;
1062
- // Combine memory and codebase context
1302
+ // Combine memory, learnings, and codebase context
1063
1303
  const combinedContext = [
1064
1304
  ...(memoryContext.length > 0 ? memoryContext : []),
1305
+ ...(learningsContext ? [learningsContext] : []),
1065
1306
  ...(codebaseContextStr ? [`\n## Relevant Code\n${codebaseContextStr}`] : []),
1066
1307
  ].join('\n');
1067
1308
  if (this.contextEngineering) {
@@ -1193,6 +1434,17 @@ export class ProductionAgent {
1193
1434
  reason: actualModel !== model ? 'Routed based on complexity' : 'Default model',
1194
1435
  complexity: complexity <= 0.3 ? 'low' : complexity <= 0.7 ? 'medium' : 'high',
1195
1436
  });
1437
+ // Emit decision transparency event
1438
+ this.emit({
1439
+ type: 'decision.routing',
1440
+ model: actualModel,
1441
+ reason: actualModel !== model
1442
+ ? `Complexity ${(complexity * 100).toFixed(0)}% - using ${actualModel}`
1443
+ : 'Default model for current task',
1444
+ alternatives: actualModel !== model
1445
+ ? [{ model, rejected: 'complexity threshold exceeded' }]
1446
+ : undefined,
1447
+ });
1196
1448
  }
1197
1449
  else {
1198
1450
  response = await this.provider.chat(messages, {
@@ -1329,6 +1581,15 @@ export class ProductionAgent {
1329
1581
  policy: evaluation.policy,
1330
1582
  reason: evaluation.reason,
1331
1583
  });
1584
+ // Emit decision transparency event
1585
+ this.emit({
1586
+ type: 'decision.tool',
1587
+ tool: toolCall.name,
1588
+ decision: evaluation.policy === 'forbidden' ? 'blocked'
1589
+ : evaluation.policy === 'prompt' ? 'prompted'
1590
+ : 'allowed',
1591
+ policyMatch: evaluation.reason,
1592
+ });
1332
1593
  // Handle forbidden policy - always block
1333
1594
  if (evaluation.policy === 'forbidden') {
1334
1595
  throw new Error(`Forbidden by policy: ${evaluation.reason}`);
@@ -1628,6 +1889,67 @@ export class ProductionAgent {
1628
1889
  getTraceCollector() {
1629
1890
  return this.traceCollector;
1630
1891
  }
1892
+ /**
1893
+ * Get the learning store for cross-session learning.
1894
+ * Returns null if learning store is not enabled.
1895
+ */
1896
+ getLearningStore() {
1897
+ return this.learningStore;
1898
+ }
1899
+ /**
1900
+ * Get the auto-compaction manager.
1901
+ * Returns null if compaction is not enabled.
1902
+ */
1903
+ getAutoCompactionManager() {
1904
+ return this.autoCompactionManager;
1905
+ }
1906
+ /**
1907
+ * Get the file change tracker for undo capability.
1908
+ * Returns null if file change tracking is not enabled.
1909
+ */
1910
+ getFileChangeTracker() {
1911
+ return this.fileChangeTracker;
1912
+ }
1913
+ /**
1914
+ * Record a file change for potential undo.
1915
+ * No-op if file change tracking is not enabled.
1916
+ *
1917
+ * @param params - Change details
1918
+ * @returns Change ID if tracked, -1 otherwise
1919
+ */
1920
+ async trackFileChange(params) {
1921
+ if (!this.fileChangeTracker) {
1922
+ return -1;
1923
+ }
1924
+ return this.fileChangeTracker.recordChange({
1925
+ filePath: params.filePath,
1926
+ operation: params.operation,
1927
+ contentBefore: params.contentBefore,
1928
+ contentAfter: params.contentAfter,
1929
+ turnNumber: this.state.iteration,
1930
+ toolCallId: params.toolCallId,
1931
+ });
1932
+ }
1933
+ /**
1934
+ * Undo the last change to a specific file.
1935
+ * Returns null if file change tracking is not enabled.
1936
+ */
1937
+ async undoLastFileChange(filePath) {
1938
+ if (!this.fileChangeTracker) {
1939
+ return null;
1940
+ }
1941
+ return this.fileChangeTracker.undoLastChange(filePath);
1942
+ }
1943
+ /**
1944
+ * Undo all changes in the current turn.
1945
+ * Returns null if file change tracking is not enabled.
1946
+ */
1947
+ async undoCurrentTurn() {
1948
+ if (!this.fileChangeTracker) {
1949
+ return null;
1950
+ }
1951
+ return this.fileChangeTracker.undoTurn(this.state.iteration);
1952
+ }
1631
1953
  /**
1632
1954
  * Subscribe to events.
1633
1955
  */
@@ -2288,13 +2610,27 @@ export class ProductionAgent {
2288
2610
  const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
2289
2611
  ? agentDef.model
2290
2612
  : this.config.model;
2613
+ // Get subagent config with defaults
2614
+ // Note: subagent config is SubagentConfig | false from buildConfig
2615
+ const subagentConfig = this.config.subagent;
2616
+ const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
2617
+ const defaultMaxIterations = hasSubagentConfig
2618
+ ? subagentConfig.defaultMaxIterations ?? 10
2619
+ : 10;
2620
+ const subagentTimeout = hasSubagentConfig
2621
+ ? subagentConfig.defaultTimeout ?? 120000
2622
+ : 120000;
2291
2623
  // Create a sub-agent with the agent's config
2292
2624
  const subAgent = new ProductionAgent({
2293
2625
  provider: this.provider,
2294
2626
  tools: agentTools,
2627
+ // Pass toolResolver so subagent can lazy-load MCP tools
2628
+ toolResolver: this.toolResolver || undefined,
2629
+ // Pass MCP tool summaries so subagent knows what tools are available
2630
+ mcpToolSummaries: this.config.mcpToolSummaries,
2295
2631
  systemPrompt: agentDef.systemPrompt,
2296
2632
  model: resolvedModel,
2297
- maxIterations: agentDef.maxIterations || 30,
2633
+ maxIterations: agentDef.maxIterations || defaultMaxIterations,
2298
2634
  // Inherit some features but keep subagent simpler
2299
2635
  memory: false,
2300
2636
  planning: false,
@@ -2317,21 +2653,62 @@ export class ProductionAgent {
2317
2653
  const taggedEvent = { ...event, subagent: agentName };
2318
2654
  this.emit(taggedEvent);
2319
2655
  });
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;
2656
+ // Create timeout token for subagent execution
2657
+ const timeoutSource = createTimeoutToken(subagentTimeout);
2658
+ // Link parent's cancellation with subagent timeout so ESC propagates to subagents
2659
+ const parentSource = this.cancellation?.getSource();
2660
+ const effectiveSource = parentSource
2661
+ ? createLinkedToken(parentSource, timeoutSource)
2662
+ : timeoutSource;
2663
+ try {
2664
+ // Run the task with cancellation propagation from parent
2665
+ const result = await race(subAgent.run(task), effectiveSource.token);
2666
+ const duration = Date.now() - startTime;
2667
+ const spawnResult = {
2668
+ success: result.success,
2669
+ output: result.response || result.error || '',
2670
+ metrics: {
2671
+ tokens: result.metrics.totalTokens,
2672
+ duration,
2673
+ toolCalls: result.metrics.toolCalls,
2674
+ },
2675
+ };
2676
+ this.emit({ type: 'agent.complete', agentId: agentName, success: result.success });
2677
+ await subAgent.cleanup();
2678
+ return spawnResult;
2679
+ }
2680
+ catch (err) {
2681
+ // Handle cancellation (user ESC or timeout) for cleaner error messages
2682
+ if (isCancellationError(err)) {
2683
+ const duration = Date.now() - startTime;
2684
+ const isUserCancellation = parentSource?.isCancellationRequested;
2685
+ const reason = isUserCancellation
2686
+ ? 'User cancelled'
2687
+ : `Timed out after ${subagentTimeout}ms`;
2688
+ this.emit({ type: 'agent.error', agentId: agentName, error: reason });
2689
+ // Try to cleanup the subagent gracefully
2690
+ try {
2691
+ await subAgent.cleanup();
2692
+ }
2693
+ catch {
2694
+ // Ignore cleanup errors on cancellation
2695
+ }
2696
+ const output = isUserCancellation
2697
+ ? `Subagent '${agentName}' was cancelled by user.`
2698
+ : `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s. The task may be too complex or the model may be slow.`;
2699
+ return {
2700
+ success: false,
2701
+ output,
2702
+ metrics: { tokens: 0, duration, toolCalls: 0 },
2703
+ };
2704
+ }
2705
+ throw err; // Re-throw non-cancellation errors
2706
+ }
2707
+ finally {
2708
+ // Dispose both sources (linked source disposes its internal state, timeout source handles its timer)
2709
+ effectiveSource.dispose();
2710
+ timeoutSource.dispose();
2711
+ }
2335
2712
  }
2336
2713
  catch (err) {
2337
2714
  const error = err instanceof Error ? err.message : String(err);
@@ -2814,6 +3191,18 @@ If the task is a simple question or doesn't need specialized handling, set bestA
2814
3191
  // =========================================================================
2815
3192
  // SKILLS METHODS
2816
3193
  // =========================================================================
3194
+ /**
3195
+ * Get the skill manager instance for advanced operations.
3196
+ */
3197
+ getSkillManager() {
3198
+ return this.skillManager;
3199
+ }
3200
+ /**
3201
+ * Get the agent registry instance for advanced operations.
3202
+ */
3203
+ getAgentRegistry() {
3204
+ return this.agentRegistry;
3205
+ }
2817
3206
  /**
2818
3207
  * Get all loaded skills.
2819
3208
  */
@@ -2860,6 +3249,34 @@ If the task is a simple question or doesn't need specialized handling, set bestA
2860
3249
  findMatchingSkills(query) {
2861
3250
  return this.skillManager?.findMatchingSkills(query) || [];
2862
3251
  }
3252
+ /**
3253
+ * Get the capabilities registry for unified discovery.
3254
+ * Lazily creates and populates the registry on first access.
3255
+ */
3256
+ getCapabilitiesRegistry() {
3257
+ if (!this.capabilitiesRegistry) {
3258
+ this.capabilitiesRegistry = createCapabilitiesRegistry();
3259
+ // Register sources
3260
+ this.capabilitiesRegistry.registerToolRegistry({
3261
+ getTools: () => this.getTools(),
3262
+ });
3263
+ if (this.skillManager) {
3264
+ this.capabilitiesRegistry.registerSkillManager(this.skillManager);
3265
+ }
3266
+ if (this.agentRegistry) {
3267
+ this.capabilitiesRegistry.registerAgentRegistry(this.agentRegistry);
3268
+ }
3269
+ // MCP client is registered externally if available
3270
+ }
3271
+ return this.capabilitiesRegistry;
3272
+ }
3273
+ /**
3274
+ * Register an MCP client with the capabilities registry.
3275
+ */
3276
+ registerMCPClient(client) {
3277
+ const registry = this.getCapabilitiesRegistry();
3278
+ registry.registerMCPClient(client);
3279
+ }
2863
3280
  /**
2864
3281
  * Get formatted list of available skills.
2865
3282
  */