attocode 0.1.0 → 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 (103) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +138 -10
  3. package/dist/src/agent.d.ts +75 -1
  4. package/dist/src/agent.d.ts.map +1 -1
  5. package/dist/src/agent.js +700 -25
  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 +29 -1
  25. package/dist/src/defaults.d.ts.map +1 -1
  26. package/dist/src/defaults.js +66 -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 +12 -2
  45. package/dist/src/integrations/index.d.ts.map +1 -1
  46. package/dist/src/integrations/index.js +22 -2
  47. package/dist/src/integrations/index.js.map +1 -1
  48. package/dist/src/integrations/interactive-planning.d.ts +322 -0
  49. package/dist/src/integrations/interactive-planning.d.ts.map +1 -0
  50. package/dist/src/integrations/interactive-planning.js +655 -0
  51. package/dist/src/integrations/interactive-planning.js.map +1 -0
  52. package/dist/src/integrations/learning-store.d.ts +291 -0
  53. package/dist/src/integrations/learning-store.d.ts.map +1 -0
  54. package/dist/src/integrations/learning-store.js +640 -0
  55. package/dist/src/integrations/learning-store.js.map +1 -0
  56. package/dist/src/integrations/pending-plan.d.ts.map +1 -1
  57. package/dist/src/integrations/pending-plan.js +69 -10
  58. package/dist/src/integrations/pending-plan.js.map +1 -1
  59. package/dist/src/integrations/skill-executor.d.ts +113 -0
  60. package/dist/src/integrations/skill-executor.d.ts.map +1 -0
  61. package/dist/src/integrations/skill-executor.js +270 -0
  62. package/dist/src/integrations/skill-executor.js.map +1 -0
  63. package/dist/src/integrations/skills.d.ts +98 -7
  64. package/dist/src/integrations/skills.d.ts.map +1 -1
  65. package/dist/src/integrations/skills.js +210 -11
  66. package/dist/src/integrations/skills.js.map +1 -1
  67. package/dist/src/providers/circuit-breaker.d.ts +180 -0
  68. package/dist/src/providers/circuit-breaker.d.ts.map +1 -0
  69. package/dist/src/providers/circuit-breaker.js +349 -0
  70. package/dist/src/providers/circuit-breaker.js.map +1 -0
  71. package/dist/src/providers/fallback-chain.d.ts +194 -0
  72. package/dist/src/providers/fallback-chain.d.ts.map +1 -0
  73. package/dist/src/providers/fallback-chain.js +363 -0
  74. package/dist/src/providers/fallback-chain.js.map +1 -0
  75. package/dist/src/providers/llm-resilience.d.ts +126 -0
  76. package/dist/src/providers/llm-resilience.d.ts.map +1 -0
  77. package/dist/src/providers/llm-resilience.js +261 -0
  78. package/dist/src/providers/llm-resilience.js.map +1 -0
  79. package/dist/src/providers/resilient-provider.d.ts +124 -0
  80. package/dist/src/providers/resilient-provider.d.ts.map +1 -0
  81. package/dist/src/providers/resilient-provider.js +242 -0
  82. package/dist/src/providers/resilient-provider.js.map +1 -0
  83. package/dist/src/tricks/recursive-context.d.ts +296 -0
  84. package/dist/src/tricks/recursive-context.d.ts.map +1 -0
  85. package/dist/src/tricks/recursive-context.js +518 -0
  86. package/dist/src/tricks/recursive-context.js.map +1 -0
  87. package/dist/src/tui/app.d.ts.map +1 -1
  88. package/dist/src/tui/app.js +226 -29
  89. package/dist/src/tui/app.js.map +1 -1
  90. package/dist/src/tui/components/ApprovalDialog.d.ts.map +1 -1
  91. package/dist/src/tui/components/ApprovalDialog.js +1 -1
  92. package/dist/src/tui/components/ApprovalDialog.js.map +1 -1
  93. package/dist/src/tui/index.d.ts +1 -0
  94. package/dist/src/tui/index.d.ts.map +1 -1
  95. package/dist/src/tui/index.js +2 -0
  96. package/dist/src/tui/index.js.map +1 -1
  97. package/dist/src/tui/transparency-aggregator.d.ts +100 -0
  98. package/dist/src/tui/transparency-aggregator.d.ts.map +1 -0
  99. package/dist/src/tui/transparency-aggregator.js +234 -0
  100. package/dist/src/tui/transparency-aggregator.js.map +1 -0
  101. package/dist/src/types.d.ts +155 -0
  102. package/dist/src/types.d.ts.map +1 -1
  103. 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, } 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
@@ -60,6 +60,13 @@ export class ProductionAgent {
60
60
  traceCollector = null;
61
61
  modeManager;
62
62
  pendingPlanManager;
63
+ interactivePlanner = null;
64
+ recursiveContext = null;
65
+ learningStore = null;
66
+ compactor = null;
67
+ autoCompactionManager = null;
68
+ fileChangeTracker = null;
69
+ capabilitiesRegistry = null;
63
70
  toolResolver = null;
64
71
  // Initialization tracking
65
72
  initPromises = [];
@@ -327,6 +334,270 @@ export class ProductionAgent {
327
334
  break;
328
335
  }
329
336
  });
337
+ // Interactive Planning (conversational + editable planning)
338
+ if (isFeatureEnabled(this.config.interactivePlanning)) {
339
+ const interactiveConfig = typeof this.config.interactivePlanning === 'object'
340
+ ? this.config.interactivePlanning
341
+ : {};
342
+ this.interactivePlanner = createInteractivePlanner({
343
+ autoCheckpoint: interactiveConfig.enableCheckpoints ?? true,
344
+ confirmBeforeExecute: interactiveConfig.requireApproval ?? true,
345
+ maxCheckpoints: 20,
346
+ autoPauseAtDecisions: true,
347
+ });
348
+ // Forward planner events to observability
349
+ this.interactivePlanner.on(event => {
350
+ switch (event.type) {
351
+ case 'plan.created':
352
+ this.observability?.logger?.info('Interactive plan created', {
353
+ planId: event.plan.id,
354
+ stepCount: event.plan.steps.length,
355
+ });
356
+ break;
357
+ case 'step.completed':
358
+ this.observability?.logger?.debug('Plan step completed', {
359
+ stepId: event.step.id,
360
+ status: event.step.status,
361
+ });
362
+ break;
363
+ case 'plan.cancelled':
364
+ this.observability?.logger?.info('Plan cancelled', { reason: event.reason });
365
+ break;
366
+ case 'checkpoint.created':
367
+ this.observability?.logger?.debug('Plan checkpoint created', {
368
+ checkpointId: event.checkpoint.id,
369
+ });
370
+ break;
371
+ }
372
+ });
373
+ }
374
+ // Recursive Context (RLM - Recursive Language Models)
375
+ // Enables on-demand context exploration for large codebases
376
+ if (isFeatureEnabled(this.config.recursiveContext)) {
377
+ const recursiveConfig = typeof this.config.recursiveContext === 'object'
378
+ ? this.config.recursiveContext
379
+ : {};
380
+ this.recursiveContext = createRecursiveContext({
381
+ maxDepth: recursiveConfig.maxRecursionDepth ?? 5,
382
+ snippetTokens: recursiveConfig.maxSnippetTokens ?? 2000,
383
+ synthesisTokens: 1000,
384
+ totalBudget: 50000,
385
+ cacheResults: recursiveConfig.cacheNavigationResults ?? true,
386
+ });
387
+ // Note: File system source should be registered when needed with proper glob/readFile functions
388
+ // This is deferred to allow flexible configuration
389
+ // Forward RLM events
390
+ this.recursiveContext.on(event => {
391
+ switch (event.type) {
392
+ case 'process.started':
393
+ this.observability?.logger?.debug('RLM process started', {
394
+ query: event.query,
395
+ depth: event.depth,
396
+ });
397
+ break;
398
+ case 'navigation.command':
399
+ this.observability?.logger?.debug('RLM navigation command', {
400
+ command: event.command,
401
+ depth: event.depth,
402
+ });
403
+ break;
404
+ case 'process.completed':
405
+ this.observability?.logger?.debug('RLM process completed', {
406
+ stats: event.stats,
407
+ });
408
+ break;
409
+ case 'budget.warning':
410
+ this.observability?.logger?.warn('RLM budget warning', {
411
+ remaining: event.remaining,
412
+ total: event.total,
413
+ });
414
+ break;
415
+ }
416
+ });
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
+ });
330
601
  }
331
602
  /**
332
603
  * Ensure all async initialization is complete before running.
@@ -518,11 +789,82 @@ export class ProductionAgent {
518
789
  }
519
790
  // =======================================================================
520
791
  // ECONOMICS CHECK (Token Budget) - replaces hard iteration limit
792
+ // With recovery: try compaction before giving up on token limits
521
793
  // =======================================================================
522
794
  if (this.economics) {
523
795
  const budgetCheck = this.economics.checkBudget();
524
796
  if (!budgetCheck.canContinue) {
525
- // Hard limit reached
797
+ // ===================================================================
798
+ // RECOVERY ATTEMPT: Try emergency context reduction before giving up
799
+ // Only for token-based limits, not iteration limits
800
+ // ===================================================================
801
+ const isTokenLimit = budgetCheck.budgetType === 'tokens' || budgetCheck.budgetType === 'cost';
802
+ const alreadyTriedRecovery = this.state._recoveryAttempted === true;
803
+ if (isTokenLimit && !alreadyTriedRecovery) {
804
+ this.observability?.logger?.info('Budget limit reached, attempting recovery via context reduction', {
805
+ reason: budgetCheck.reason,
806
+ percentUsed: budgetCheck.percentUsed,
807
+ });
808
+ this.emit({
809
+ type: 'resilience.retry',
810
+ reason: 'budget_limit_compaction',
811
+ attempt: 1,
812
+ maxAttempts: 1,
813
+ });
814
+ // Mark that we've attempted recovery to prevent infinite loops
815
+ this.state._recoveryAttempted = true;
816
+ const tokensBefore = this.estimateContextTokens(messages);
817
+ // Step 1: Compact tool outputs aggressively
818
+ this.compactToolOutputs();
819
+ // Step 2: Emergency truncation - keep system + last N messages
820
+ const PRESERVE_RECENT = 10;
821
+ if (messages.length > PRESERVE_RECENT + 2) {
822
+ const systemMessage = messages.find(m => m.role === 'system');
823
+ const recentMessages = messages.slice(-(PRESERVE_RECENT));
824
+ // Rebuild message array
825
+ messages.length = 0;
826
+ if (systemMessage) {
827
+ messages.push(systemMessage);
828
+ }
829
+ messages.push({
830
+ role: 'system',
831
+ content: `[CONTEXT REDUCED: Earlier messages were removed to stay within budget. Conversation continues from recent context.]`,
832
+ });
833
+ messages.push(...recentMessages);
834
+ // Update state messages too
835
+ this.state.messages.length = 0;
836
+ this.state.messages.push(...messages);
837
+ }
838
+ const tokensAfter = this.estimateContextTokens(messages);
839
+ const reduction = Math.round((1 - tokensAfter / tokensBefore) * 100);
840
+ if (tokensAfter < tokensBefore * 0.8) {
841
+ // Significant reduction achieved
842
+ this.observability?.logger?.info('Context reduction successful, continuing execution', {
843
+ tokensBefore,
844
+ tokensAfter,
845
+ reduction,
846
+ });
847
+ this.emit({
848
+ type: 'resilience.recovered',
849
+ reason: 'budget_limit_compaction',
850
+ attempts: 1,
851
+ });
852
+ this.emit({
853
+ type: 'compaction.auto',
854
+ tokensBefore,
855
+ tokensAfter,
856
+ messagesCompacted: tokensBefore - tokensAfter,
857
+ });
858
+ // Continue execution instead of breaking
859
+ continue;
860
+ }
861
+ this.observability?.logger?.warn('Context reduction insufficient', {
862
+ tokensBefore,
863
+ tokensAfter,
864
+ reduction,
865
+ });
866
+ }
867
+ // Hard limit reached and recovery failed (or not applicable)
526
868
  this.observability?.logger?.warn('Budget limit reached', {
527
869
  reason: budgetCheck.reason,
528
870
  budgetType: budgetCheck.budgetType,
@@ -617,8 +959,104 @@ export class ProductionAgent {
617
959
  }
618
960
  }
619
961
  }
620
- // Make LLM call
621
- const response = await this.callLLM(messages);
962
+ // =====================================================================
963
+ // RESILIENT LLM CALL: Empty response retries + max_tokens continuation
964
+ // =====================================================================
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;
974
+ let response = await this.callLLM(messages);
975
+ let emptyRetries = 0;
976
+ let continuations = 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;
980
+ const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;
981
+ if (hasContent || hasToolCalls) {
982
+ // Valid response received
983
+ if (emptyRetries > 0) {
984
+ this.emit({
985
+ type: 'resilience.recovered',
986
+ reason: 'empty_response',
987
+ attempts: emptyRetries,
988
+ });
989
+ this.observability?.logger?.info('Recovered from empty response', {
990
+ retries: emptyRetries,
991
+ });
992
+ }
993
+ break;
994
+ }
995
+ // Empty response - retry with nudge
996
+ emptyRetries++;
997
+ this.emit({
998
+ type: 'resilience.retry',
999
+ reason: 'empty_response',
1000
+ attempt: emptyRetries,
1001
+ maxAttempts: MAX_EMPTY_RETRIES,
1002
+ });
1003
+ this.observability?.logger?.warn('Empty LLM response, retrying', {
1004
+ attempt: emptyRetries,
1005
+ maxAttempts: MAX_EMPTY_RETRIES,
1006
+ });
1007
+ // Add gentle nudge and retry
1008
+ const nudgeMessage = {
1009
+ role: 'user',
1010
+ content: '[System: Your previous response was empty. Please provide a response or use a tool.]',
1011
+ };
1012
+ messages.push(nudgeMessage);
1013
+ this.state.messages.push(nudgeMessage);
1014
+ response = await this.callLLM(messages);
1015
+ }
1016
+ // Phase 2: Handle max_tokens truncation with continuation (if enabled)
1017
+ if (resilienceEnabled && AUTO_CONTINUE && response.stopReason === 'max_tokens' && !response.toolCalls?.length) {
1018
+ let accumulatedContent = response.content || '';
1019
+ while (continuations < MAX_CONTINUATIONS && response.stopReason === 'max_tokens') {
1020
+ continuations++;
1021
+ this.emit({
1022
+ type: 'resilience.continue',
1023
+ reason: 'max_tokens',
1024
+ continuation: continuations,
1025
+ maxContinuations: MAX_CONTINUATIONS,
1026
+ accumulatedLength: accumulatedContent.length,
1027
+ });
1028
+ this.observability?.logger?.info('Response truncated at max_tokens, continuing', {
1029
+ continuation: continuations,
1030
+ accumulatedLength: accumulatedContent.length,
1031
+ });
1032
+ // Add continuation request
1033
+ const continuationMessage = {
1034
+ role: 'assistant',
1035
+ content: accumulatedContent,
1036
+ };
1037
+ const continueRequest = {
1038
+ role: 'user',
1039
+ content: '[System: Please continue from where you left off. Do not repeat what you already said.]',
1040
+ };
1041
+ messages.push(continuationMessage, continueRequest);
1042
+ this.state.messages.push(continuationMessage, continueRequest);
1043
+ response = await this.callLLM(messages);
1044
+ // Accumulate content
1045
+ if (response.content) {
1046
+ accumulatedContent += response.content;
1047
+ }
1048
+ }
1049
+ // Update response with accumulated content
1050
+ if (continuations > 0) {
1051
+ response = { ...response, content: accumulatedContent };
1052
+ this.emit({
1053
+ type: 'resilience.completed',
1054
+ reason: 'max_tokens_continuation',
1055
+ continuations,
1056
+ finalLength: accumulatedContent.length,
1057
+ });
1058
+ }
1059
+ }
622
1060
  // Record LLM usage for economics
623
1061
  if (this.economics && response.usage) {
624
1062
  this.economics.recordLLMUsage(response.usage.inputTokens, response.usage.outputTokens, this.config.model, response.usage.cost // Use actual cost from provider when available
@@ -639,6 +1077,20 @@ export class ProductionAgent {
639
1077
  // The model has "consumed" the tool outputs and produced a response,
640
1078
  // so we can replace verbose outputs with compact summaries
641
1079
  this.compactToolOutputs();
1080
+ // Final validation: warn if response is still empty after all retries
1081
+ if (!response.content || response.content.length === 0) {
1082
+ this.observability?.logger?.error('Agent finished with empty response after all retries', {
1083
+ emptyRetries,
1084
+ continuations,
1085
+ iteration: this.state.iteration,
1086
+ });
1087
+ this.emit({
1088
+ type: 'resilience.failed',
1089
+ reason: 'empty_final_response',
1090
+ emptyRetries,
1091
+ continuations,
1092
+ });
1093
+ }
642
1094
  break;
643
1095
  }
644
1096
  // Execute tool calls
@@ -653,8 +1105,34 @@ export class ProductionAgent {
653
1105
  const MAX_TOOL_OUTPUT_CHARS = 8000; // ~2000 tokens max per tool output
654
1106
  // =======================================================================
655
1107
  // PROACTIVE BUDGET CHECK - compact BEFORE we overflow, not after
1108
+ // Uses AutoCompactionManager if available for sophisticated compaction
656
1109
  // =======================================================================
657
- 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
658
1136
  const currentUsage = this.economics.getUsage();
659
1137
  const budget = this.economics.getBudget();
660
1138
  const percentUsed = (currentUsage.tokens / budget.maxTokens) * 100;
@@ -708,6 +1186,20 @@ export class ProductionAgent {
708
1186
  messages.push(toolMessage);
709
1187
  this.state.messages.push(toolMessage);
710
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
+ });
711
1203
  }
712
1204
  // =======================================================================
713
1205
  // REFLECTION (Lesson 16)
@@ -755,6 +1247,11 @@ export class ProductionAgent {
755
1247
  const rulesContent = this.rules?.getRulesContent() ?? '';
756
1248
  const skillsPrompt = this.skillManager?.getActiveSkillsPrompt() ?? '';
757
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
+ }) ?? '';
758
1255
  // Budget-aware codebase context selection
759
1256
  let codebaseContextStr = '';
760
1257
  if (this.codebaseContext) {
@@ -802,9 +1299,10 @@ export class ProductionAgent {
802
1299
  }
803
1300
  // Build system prompt using cache-aware builder if available (Trick P)
804
1301
  let systemPrompt;
805
- // Combine memory and codebase context
1302
+ // Combine memory, learnings, and codebase context
806
1303
  const combinedContext = [
807
1304
  ...(memoryContext.length > 0 ? memoryContext : []),
1305
+ ...(learningsContext ? [learningsContext] : []),
808
1306
  ...(codebaseContextStr ? [`\n## Relevant Code\n${codebaseContextStr}`] : []),
809
1307
  ].join('\n');
810
1308
  if (this.contextEngineering) {
@@ -936,6 +1434,17 @@ export class ProductionAgent {
936
1434
  reason: actualModel !== model ? 'Routed based on complexity' : 'Default model',
937
1435
  complexity: complexity <= 0.3 ? 'low' : complexity <= 0.7 ? 'medium' : 'high',
938
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
+ });
939
1448
  }
940
1449
  else {
941
1450
  response = await this.provider.chat(messages, {
@@ -1072,6 +1581,15 @@ export class ProductionAgent {
1072
1581
  policy: evaluation.policy,
1073
1582
  reason: evaluation.reason,
1074
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
+ });
1075
1593
  // Handle forbidden policy - always block
1076
1594
  if (evaluation.policy === 'forbidden') {
1077
1595
  throw new Error(`Forbidden by policy: ${evaluation.reason}`);
@@ -1371,6 +1889,67 @@ export class ProductionAgent {
1371
1889
  getTraceCollector() {
1372
1890
  return this.traceCollector;
1373
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
+ }
1374
1953
  /**
1375
1954
  * Subscribe to events.
1376
1955
  */
@@ -2031,13 +2610,27 @@ export class ProductionAgent {
2031
2610
  const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
2032
2611
  ? agentDef.model
2033
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;
2034
2623
  // Create a sub-agent with the agent's config
2035
2624
  const subAgent = new ProductionAgent({
2036
2625
  provider: this.provider,
2037
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,
2038
2631
  systemPrompt: agentDef.systemPrompt,
2039
2632
  model: resolvedModel,
2040
- maxIterations: agentDef.maxIterations || 30,
2633
+ maxIterations: agentDef.maxIterations || defaultMaxIterations,
2041
2634
  // Inherit some features but keep subagent simpler
2042
2635
  memory: false,
2043
2636
  planning: false,
@@ -2054,26 +2647,68 @@ export class ProductionAgent {
2054
2647
  custom: [],
2055
2648
  },
2056
2649
  });
2057
- // Forward events from subagent
2650
+ // Forward events from subagent with context
2058
2651
  subAgent.subscribe(event => {
2059
- // Just forward the event as-is - the agent.spawn event already logged the agent name
2060
- this.emit(event);
2652
+ // Tag event with subagent source so TUI can display it properly
2653
+ const taggedEvent = { ...event, subagent: agentName };
2654
+ this.emit(taggedEvent);
2061
2655
  });
2062
- // Run the task
2063
- const result = await subAgent.run(task);
2064
- const duration = Date.now() - startTime;
2065
- const spawnResult = {
2066
- success: result.success,
2067
- output: result.response || result.error || '',
2068
- metrics: {
2069
- tokens: result.metrics.totalTokens,
2070
- duration,
2071
- toolCalls: result.metrics.toolCalls,
2072
- },
2073
- };
2074
- this.emit({ type: 'agent.complete', agentId: agentName, success: result.success });
2075
- await subAgent.cleanup();
2076
- 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
+ }
2077
2712
  }
2078
2713
  catch (err) {
2079
2714
  const error = err instanceof Error ? err.message : String(err);
@@ -2556,6 +3191,18 @@ If the task is a simple question or doesn't need specialized handling, set bestA
2556
3191
  // =========================================================================
2557
3192
  // SKILLS METHODS
2558
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
+ }
2559
3206
  /**
2560
3207
  * Get all loaded skills.
2561
3208
  */
@@ -2602,6 +3249,34 @@ If the task is a simple question or doesn't need specialized handling, set bestA
2602
3249
  findMatchingSkills(query) {
2603
3250
  return this.skillManager?.findMatchingSkills(query) || [];
2604
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
+ }
2605
3280
  /**
2606
3281
  * Get formatted list of available skills.
2607
3282
  */