attocode 0.1.8 → 0.2.0

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 (151) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/dist/src/adapters.d.ts.map +1 -1
  3. package/dist/src/adapters.js +1 -0
  4. package/dist/src/adapters.js.map +1 -1
  5. package/dist/src/agent.d.ts +20 -1
  6. package/dist/src/agent.d.ts.map +1 -1
  7. package/dist/src/agent.js +364 -62
  8. package/dist/src/agent.js.map +1 -1
  9. package/dist/src/cli.d.ts +6 -0
  10. package/dist/src/cli.d.ts.map +1 -1
  11. package/dist/src/cli.js +37 -0
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/commands/init-commands.d.ts.map +1 -1
  14. package/dist/src/commands/init-commands.js +57 -0
  15. package/dist/src/commands/init-commands.js.map +1 -1
  16. package/dist/src/core/protocol/types.d.ts +14 -14
  17. package/dist/src/defaults.d.ts +1 -1
  18. package/dist/src/defaults.d.ts.map +1 -1
  19. package/dist/src/defaults.js +1 -0
  20. package/dist/src/defaults.js.map +1 -1
  21. package/dist/src/integrations/budget-pool.d.ts +96 -0
  22. package/dist/src/integrations/budget-pool.d.ts.map +1 -0
  23. package/dist/src/integrations/budget-pool.js +145 -0
  24. package/dist/src/integrations/budget-pool.js.map +1 -0
  25. package/dist/src/integrations/context-engineering.d.ts +16 -1
  26. package/dist/src/integrations/context-engineering.d.ts.map +1 -1
  27. package/dist/src/integrations/context-engineering.js +17 -0
  28. package/dist/src/integrations/context-engineering.js.map +1 -1
  29. package/dist/src/integrations/economics.d.ts +9 -0
  30. package/dist/src/integrations/economics.d.ts.map +1 -1
  31. package/dist/src/integrations/economics.js +25 -0
  32. package/dist/src/integrations/economics.js.map +1 -1
  33. package/dist/src/integrations/file-cache.d.ts +90 -0
  34. package/dist/src/integrations/file-cache.d.ts.map +1 -0
  35. package/dist/src/integrations/file-cache.js +164 -0
  36. package/dist/src/integrations/file-cache.js.map +1 -0
  37. package/dist/src/integrations/index.d.ts +6 -3
  38. package/dist/src/integrations/index.d.ts.map +1 -1
  39. package/dist/src/integrations/index.js +7 -1
  40. package/dist/src/integrations/index.js.map +1 -1
  41. package/dist/src/integrations/learning-store.d.ts.map +1 -1
  42. package/dist/src/integrations/learning-store.js +6 -0
  43. package/dist/src/integrations/learning-store.js.map +1 -1
  44. package/dist/src/integrations/safety.d.ts +25 -0
  45. package/dist/src/integrations/safety.d.ts.map +1 -1
  46. package/dist/src/integrations/safety.js +47 -0
  47. package/dist/src/integrations/safety.js.map +1 -1
  48. package/dist/src/integrations/smart-decomposer.d.ts.map +1 -1
  49. package/dist/src/integrations/smart-decomposer.js +7 -0
  50. package/dist/src/integrations/smart-decomposer.js.map +1 -1
  51. package/dist/src/integrations/swarm/index.d.ts +29 -0
  52. package/dist/src/integrations/swarm/index.d.ts.map +1 -0
  53. package/dist/src/integrations/swarm/index.js +29 -0
  54. package/dist/src/integrations/swarm/index.js.map +1 -0
  55. package/dist/src/integrations/swarm/model-selector.d.ts +55 -0
  56. package/dist/src/integrations/swarm/model-selector.d.ts.map +1 -0
  57. package/dist/src/integrations/swarm/model-selector.js +342 -0
  58. package/dist/src/integrations/swarm/model-selector.js.map +1 -0
  59. package/dist/src/integrations/swarm/request-throttle.d.ts +112 -0
  60. package/dist/src/integrations/swarm/request-throttle.d.ts.map +1 -0
  61. package/dist/src/integrations/swarm/request-throttle.js +263 -0
  62. package/dist/src/integrations/swarm/request-throttle.js.map +1 -0
  63. package/dist/src/integrations/swarm/swarm-budget.d.ts +31 -0
  64. package/dist/src/integrations/swarm/swarm-budget.d.ts.map +1 -0
  65. package/dist/src/integrations/swarm/swarm-budget.js +36 -0
  66. package/dist/src/integrations/swarm/swarm-budget.js.map +1 -0
  67. package/dist/src/integrations/swarm/swarm-config-loader.d.ts +51 -0
  68. package/dist/src/integrations/swarm/swarm-config-loader.d.ts.map +1 -0
  69. package/dist/src/integrations/swarm/swarm-config-loader.js +458 -0
  70. package/dist/src/integrations/swarm/swarm-config-loader.js.map +1 -0
  71. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts +145 -0
  72. package/dist/src/integrations/swarm/swarm-event-bridge.d.ts.map +1 -0
  73. package/dist/src/integrations/swarm/swarm-event-bridge.js +443 -0
  74. package/dist/src/integrations/swarm/swarm-event-bridge.js.map +1 -0
  75. package/dist/src/integrations/swarm/swarm-events.d.ts +157 -0
  76. package/dist/src/integrations/swarm/swarm-events.d.ts.map +1 -0
  77. package/dist/src/integrations/swarm/swarm-events.js +81 -0
  78. package/dist/src/integrations/swarm/swarm-events.js.map +1 -0
  79. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts +166 -0
  80. package/dist/src/integrations/swarm/swarm-orchestrator.d.ts.map +1 -0
  81. package/dist/src/integrations/swarm/swarm-orchestrator.js +1114 -0
  82. package/dist/src/integrations/swarm/swarm-orchestrator.js.map +1 -0
  83. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts +29 -0
  84. package/dist/src/integrations/swarm/swarm-quality-gate.d.ts.map +1 -0
  85. package/dist/src/integrations/swarm/swarm-quality-gate.js +85 -0
  86. package/dist/src/integrations/swarm/swarm-quality-gate.js.map +1 -0
  87. package/dist/src/integrations/swarm/swarm-state-store.d.ts +31 -0
  88. package/dist/src/integrations/swarm/swarm-state-store.d.ts.map +1 -0
  89. package/dist/src/integrations/swarm/swarm-state-store.js +91 -0
  90. package/dist/src/integrations/swarm/swarm-state-store.js.map +1 -0
  91. package/dist/src/integrations/swarm/task-queue.d.ts +128 -0
  92. package/dist/src/integrations/swarm/task-queue.d.ts.map +1 -0
  93. package/dist/src/integrations/swarm/task-queue.js +379 -0
  94. package/dist/src/integrations/swarm/task-queue.js.map +1 -0
  95. package/dist/src/integrations/swarm/types.d.ts +425 -0
  96. package/dist/src/integrations/swarm/types.d.ts.map +1 -0
  97. package/dist/src/integrations/swarm/types.js +96 -0
  98. package/dist/src/integrations/swarm/types.js.map +1 -0
  99. package/dist/src/integrations/swarm/worker-pool.d.ts +96 -0
  100. package/dist/src/integrations/swarm/worker-pool.d.ts.map +1 -0
  101. package/dist/src/integrations/swarm/worker-pool.js +269 -0
  102. package/dist/src/integrations/swarm/worker-pool.js.map +1 -0
  103. package/dist/src/main.js +88 -0
  104. package/dist/src/main.js.map +1 -1
  105. package/dist/src/modes/repl.d.ts +1 -0
  106. package/dist/src/modes/repl.d.ts.map +1 -1
  107. package/dist/src/modes/repl.js +2 -1
  108. package/dist/src/modes/repl.js.map +1 -1
  109. package/dist/src/modes/tui.d.ts +1 -0
  110. package/dist/src/modes/tui.d.ts.map +1 -1
  111. package/dist/src/modes/tui.js +3 -1
  112. package/dist/src/modes/tui.js.map +1 -1
  113. package/dist/src/providers/adapters/anthropic.d.ts +1 -1
  114. package/dist/src/providers/adapters/anthropic.d.ts.map +1 -1
  115. package/dist/src/providers/adapters/anthropic.js +15 -2
  116. package/dist/src/providers/adapters/anthropic.js.map +1 -1
  117. package/dist/src/providers/adapters/mock.d.ts +2 -2
  118. package/dist/src/providers/adapters/mock.d.ts.map +1 -1
  119. package/dist/src/providers/adapters/mock.js +2 -1
  120. package/dist/src/providers/adapters/mock.js.map +1 -1
  121. package/dist/src/providers/adapters/openai.d.ts +1 -1
  122. package/dist/src/providers/adapters/openai.d.ts.map +1 -1
  123. package/dist/src/providers/adapters/openai.js +2 -2
  124. package/dist/src/providers/adapters/openai.js.map +1 -1
  125. package/dist/src/providers/adapters/openrouter.d.ts +15 -1
  126. package/dist/src/providers/adapters/openrouter.d.ts.map +1 -1
  127. package/dist/src/providers/adapters/openrouter.js +60 -5
  128. package/dist/src/providers/adapters/openrouter.js.map +1 -1
  129. package/dist/src/providers/resilient-fetch.d.ts +2 -0
  130. package/dist/src/providers/resilient-fetch.d.ts.map +1 -1
  131. package/dist/src/providers/resilient-fetch.js +27 -3
  132. package/dist/src/providers/resilient-fetch.js.map +1 -1
  133. package/dist/src/providers/types.d.ts +12 -1
  134. package/dist/src/providers/types.d.ts.map +1 -1
  135. package/dist/src/providers/types.js.map +1 -1
  136. package/dist/src/tools/bash.d.ts +2 -2
  137. package/dist/src/tools/file.d.ts +4 -4
  138. package/dist/src/tricks/kv-cache-context.d.ts +24 -0
  139. package/dist/src/tricks/kv-cache-context.d.ts.map +1 -1
  140. package/dist/src/tricks/kv-cache-context.js +68 -0
  141. package/dist/src/tricks/kv-cache-context.js.map +1 -1
  142. package/dist/src/tui/app.d.ts.map +1 -1
  143. package/dist/src/tui/app.js +75 -4
  144. package/dist/src/tui/app.js.map +1 -1
  145. package/dist/src/tui/components/SwarmStatusPanel.d.ts +27 -0
  146. package/dist/src/tui/components/SwarmStatusPanel.d.ts.map +1 -0
  147. package/dist/src/tui/components/SwarmStatusPanel.js +108 -0
  148. package/dist/src/tui/components/SwarmStatusPanel.js.map +1 -0
  149. package/dist/src/types.d.ts +35 -2
  150. package/dist/src/types.d.ts.map +1 -1
  151. package/package.json +1 -1
package/dist/src/agent.js CHANGED
@@ -21,7 +21,7 @@
21
21
  import { buildConfig, isFeatureEnabled, getEnabledFeatures, getSubagentTimeout, getSubagentMaxIterations, } from './defaults.js';
22
22
  import { createModeManager, formatModeList, parseMode, calculateTaskSimilarity, SUBAGENT_PLAN_MODE_ADDITION, } 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, SUBAGENT_BUDGET, TIMEOUT_WRAPUP_PROMPT, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createLinkedToken, createGracefulTimeout, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, createSharedBlackboard, createTaskManager, } from './integrations/index.js';
24
+ import { HookManager, MemoryManager, PlanningManager, ObservabilityManager, SafetyManager, RoutingManager, MultiAgentManager, ReActManager, ExecutionPolicyManager, ThreadManager, RulesManager, DEFAULT_RULE_SOURCES, ExecutionEconomicsManager, STANDARD_BUDGET, SUBAGENT_BUDGET, TIMEOUT_WRAPUP_PROMPT, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createLinkedToken, createGracefulTimeout, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createSharedFileCache, createBudgetPool, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, createSharedBlackboard, createTaskManager, createSwarmOrchestrator, createThrottledProvider, FREE_TIER_THROTTLE, PAID_TIER_THROTTLE, } from './integrations/index.js';
25
25
  // Lesson 26: Tracing & Evaluation integration
26
26
  import { createTraceCollector } from './tracing/trace-collector.js';
27
27
  // Model registry for context window limits
@@ -74,8 +74,11 @@ export class ProductionAgent {
74
74
  capabilitiesRegistry = null;
75
75
  toolResolver = null;
76
76
  blackboard = null;
77
+ fileCache = null;
78
+ budgetPool = null;
77
79
  taskManager = null;
78
80
  store = null;
81
+ swarmOrchestrator = null;
79
82
  // Duplicate spawn prevention - tracks recently spawned tasks to prevent doom loops
80
83
  // Map<taskKey, { timestamp: number; result: string; queuedChanges: number }>
81
84
  spawnedTasks = new Map();
@@ -88,6 +91,9 @@ export class ProductionAgent {
88
91
  // Graceful wrapup support (for subagent timeout wrapup phase)
89
92
  wrapupRequested = false;
90
93
  wrapupReason = null;
94
+ // Cacheable system prompt blocks for prompt caching (Improvement P1)
95
+ // When set, callLLM() will inject these as structured content with cache_control markers
96
+ cacheableSystemBlocks = null;
91
97
  // Initialization tracking
92
98
  initPromises = [];
93
99
  initComplete = false;
@@ -141,6 +147,26 @@ export class ProductionAgent {
141
147
  deduplicateFindings: true,
142
148
  });
143
149
  }
150
+ // Shared File Cache - eliminates redundant file reads across parent and subagents
151
+ // Subagents inherit parent's cache; parent agents create their own
152
+ if (userConfig.fileCache) {
153
+ this.fileCache = userConfig.fileCache;
154
+ }
155
+ else if (this.config.subagent !== false) {
156
+ this.fileCache = createSharedFileCache({
157
+ maxCacheBytes: 5 * 1024 * 1024, // 5MB
158
+ ttlMs: 5 * 60 * 1000, // 5 minutes
159
+ });
160
+ }
161
+ // Shared Budget Pool - pools token budget across parent and subagents
162
+ // Only parent agents create the pool; subagents don't need their own
163
+ // The pool is used in spawnAgent() to allocate budgets from the parent's total
164
+ if (this.config.subagent !== false) {
165
+ // Use actual configured budget (custom or default), not always STANDARD_BUDGET
166
+ const baseBudget = this.config.budget ?? STANDARD_BUDGET;
167
+ const parentBudgetTokens = baseBudget.maxTokens ?? STANDARD_BUDGET.maxTokens ?? 200000;
168
+ this.budgetPool = createBudgetPool(parentBudgetTokens, 0.25, 100000);
169
+ }
144
170
  // Initialize enabled features
145
171
  this.initializeFeatures();
146
172
  }
@@ -275,6 +301,26 @@ export class ProductionAgent {
275
301
  for (const tool of taskTools) {
276
302
  this.tools.set(tool.name, tool);
277
303
  }
304
+ // Swarm Mode (experimental)
305
+ if (this.config.swarm) {
306
+ const swarmConfig = this.config.swarm;
307
+ // Wrap provider with request throttle to prevent 429 rate limiting.
308
+ // All subagents share this.provider by reference (line 4398),
309
+ // so wrapping here throttles ALL downstream LLM calls.
310
+ if (swarmConfig.throttle !== false) {
311
+ const throttleConfig = swarmConfig.throttle === 'paid'
312
+ ? PAID_TIER_THROTTLE
313
+ : swarmConfig.throttle === 'free' || swarmConfig.throttle === undefined
314
+ ? FREE_TIER_THROTTLE
315
+ : swarmConfig.throttle;
316
+ this.provider = createThrottledProvider(this.provider, throttleConfig);
317
+ }
318
+ this.swarmOrchestrator = createSwarmOrchestrator(swarmConfig, this.provider, this.agentRegistry, (name, task) => this.spawnAgent(name, task), this.blackboard ?? undefined);
319
+ // Override parent budget pool with swarm's much larger pool so spawnAgent()
320
+ // allocates from the swarm budget (e.g. 10M tokens) instead of the parent's
321
+ // generic pool (200K tokens). Without this, workers get 5K emergency budget.
322
+ this.budgetPool = this.swarmOrchestrator.getBudgetPool().pool;
323
+ }
278
324
  // Cancellation Support
279
325
  if (isFeatureEnabled(this.config.cancellation)) {
280
326
  this.cancellation = createCancellationManager();
@@ -751,8 +797,14 @@ export class ProductionAgent {
751
797
  try {
752
798
  // Check for cancellation before starting
753
799
  cancellationToken?.throwIfCancellationRequested();
754
- // Check if planning is needed
755
- if (this.planning?.shouldPlan(task)) {
800
+ // Check if swarm mode should handle this task
801
+ if (this.swarmOrchestrator) {
802
+ const swarmResult = await this.runSwarm(task);
803
+ // Store swarm summary as an assistant message for the response
804
+ this.state.messages.push({ role: 'assistant', content: swarmResult.summary });
805
+ }
806
+ else if (this.planning?.shouldPlan(task)) {
807
+ // Check if planning is needed
756
808
  await this.createAndExecutePlan(task);
757
809
  }
758
810
  else {
@@ -877,6 +929,43 @@ export class ProductionAgent {
877
929
  }
878
930
  }
879
931
  }
932
+ /**
933
+ * Run a task in swarm mode using the SwarmOrchestrator.
934
+ */
935
+ async runSwarm(task) {
936
+ if (!this.swarmOrchestrator) {
937
+ throw new Error('Swarm orchestrator not initialized');
938
+ }
939
+ this.observability?.logger?.info('Starting swarm execution', { task: task.slice(0, 100) });
940
+ this.observability?.logger?.info('Starting swarm mode — decomposing task into subtasks...');
941
+ // Forward swarm events to the main agent event system
942
+ const unsubSwarm = this.swarmOrchestrator.subscribe(event => {
943
+ // Forward as a generic agent event for TUI display
944
+ this.emit(event);
945
+ });
946
+ // Bridge events to filesystem for live dashboard
947
+ const { SwarmEventBridge } = await import('./integrations/swarm/swarm-event-bridge.js');
948
+ const bridge = new SwarmEventBridge({ outputDir: '.agent/swarm-live' });
949
+ const unsubBridge = bridge.attach(this.swarmOrchestrator);
950
+ try {
951
+ const result = await this.swarmOrchestrator.execute(task);
952
+ // Populate task DAG for dashboard after execution
953
+ bridge.setTasks(result.tasks);
954
+ this.observability?.logger?.info('Swarm execution complete', {
955
+ success: result.success,
956
+ tasks: result.stats.totalTasks,
957
+ completed: result.stats.completedTasks,
958
+ tokens: result.stats.totalTokens,
959
+ cost: result.stats.totalCost,
960
+ });
961
+ return result;
962
+ }
963
+ finally {
964
+ unsubBridge();
965
+ bridge.close();
966
+ unsubSwarm();
967
+ }
968
+ }
880
969
  /**
881
970
  * Execute a task directly without planning.
882
971
  */
@@ -1222,8 +1311,9 @@ export class ProductionAgent {
1222
1311
  while (resilienceEnabled && emptyRetries < MAX_EMPTY_RETRIES) {
1223
1312
  const hasContent = response.content && response.content.length >= MIN_CONTENT_LENGTH;
1224
1313
  const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;
1314
+ const hasThinking = response.thinking && response.thinking.length > 0;
1225
1315
  if (hasContent || hasToolCalls) {
1226
- // Valid response received
1316
+ // Valid visible response
1227
1317
  if (emptyRetries > 0) {
1228
1318
  this.emit({
1229
1319
  type: 'resilience.recovered',
@@ -1236,7 +1326,38 @@ export class ProductionAgent {
1236
1326
  }
1237
1327
  break;
1238
1328
  }
1239
- // Empty response - retry with nudge
1329
+ if (hasThinking && !hasContent && !hasToolCalls) {
1330
+ // Model produced reasoning but no visible output (e.g., DeepSeek-R1, GLM-4, QwQ).
1331
+ // Give ONE targeted nudge, then accept thinking as content.
1332
+ if (emptyRetries === 0) {
1333
+ emptyRetries++;
1334
+ this.emit({
1335
+ type: 'resilience.retry',
1336
+ reason: 'thinking_only_response',
1337
+ attempt: emptyRetries,
1338
+ maxAttempts: MAX_EMPTY_RETRIES,
1339
+ });
1340
+ this.state.metrics.retryCount = (this.state.metrics.retryCount ?? 0) + 1;
1341
+ this.observability?.logger?.warn('Thinking-only response (no visible content), nudging', {
1342
+ thinkingLength: response.thinking.length,
1343
+ });
1344
+ const thinkingNudge = {
1345
+ role: 'user',
1346
+ content: '[System: You produced reasoning but no visible response. Please provide your answer based on your analysis.]',
1347
+ };
1348
+ messages.push(thinkingNudge);
1349
+ this.state.messages.push(thinkingNudge);
1350
+ response = await this.callLLM(messages);
1351
+ continue;
1352
+ }
1353
+ // Second attempt also thinking-only → accept thinking as content
1354
+ this.observability?.logger?.info('Accepting thinking as content after nudge failed', {
1355
+ thinkingLength: response.thinking.length,
1356
+ });
1357
+ response = { ...response, content: response.thinking };
1358
+ break;
1359
+ }
1360
+ // Truly empty (no content, no tools, no thinking) — existing retry logic
1240
1361
  emptyRetries++;
1241
1362
  this.emit({
1242
1363
  type: 'resilience.retry',
@@ -1353,10 +1474,11 @@ export class ProductionAgent {
1353
1474
  role: 'assistant',
1354
1475
  content: response.content,
1355
1476
  toolCalls: response.toolCalls,
1477
+ ...(response.thinking ? { metadata: { thinking: response.thinking } } : {}),
1356
1478
  };
1357
1479
  messages.push(assistantMessage);
1358
1480
  this.state.messages.push(assistantMessage);
1359
- lastResponse = response.content;
1481
+ lastResponse = response.content || (response.thinking ? response.thinking : '');
1360
1482
  // In plan mode: capture exploration findings as we go (not just at the end)
1361
1483
  // This ensures we collect context from exploration iterations before writes are queued
1362
1484
  if (this.modeManager.getMode() === 'plan' && response.content && response.content.length > 50) {
@@ -1689,28 +1811,42 @@ export class ProductionAgent {
1689
1811
  }
1690
1812
  }
1691
1813
  // Build system prompt using cache-aware builder if available (Trick P)
1692
- let systemPrompt;
1693
1814
  // Combine memory, learnings, and codebase context
1694
1815
  const combinedContext = [
1695
1816
  ...(memoryContext.length > 0 ? memoryContext : []),
1696
1817
  ...(learningsContext ? [learningsContext] : []),
1697
1818
  ...(codebaseContextStr ? [`\n## Relevant Code\n${codebaseContextStr}`] : []),
1698
1819
  ].join('\n');
1820
+ const promptOptions = {
1821
+ rules: rulesContent + (skillsPrompt ? '\n\n' + skillsPrompt : ''),
1822
+ tools: toolDescriptions,
1823
+ memory: combinedContext.length > 0 ? combinedContext : undefined,
1824
+ dynamic: {
1825
+ mode: this.modeManager?.getMode() ?? 'default',
1826
+ },
1827
+ };
1699
1828
  if (this.contextEngineering) {
1700
- // Use cache-optimized prompt builder - orders sections for KV-cache reuse:
1701
- // static prefix -> rules -> tools -> memory/codebase -> dynamic
1702
- systemPrompt = this.contextEngineering.buildSystemPrompt({
1703
- rules: rulesContent + (skillsPrompt ? '\n\n' + skillsPrompt : ''),
1704
- tools: toolDescriptions,
1705
- memory: combinedContext.length > 0 ? combinedContext : undefined,
1706
- dynamic: {
1707
- mode: this.modeManager?.getMode() ?? 'default',
1708
- },
1709
- });
1829
+ // Build cache-aware system prompt with cache_control markers (Improvement P1).
1830
+ // Store structured blocks for callLLM() to inject as MessageWithContent.
1831
+ // The string version is still used for token estimation and debugging.
1832
+ const cacheableBlocks = this.contextEngineering.buildCacheableSystemPrompt(promptOptions);
1833
+ // Safety check: ensure we have content (empty array = no cache context configured)
1834
+ if (cacheableBlocks.length === 0 || cacheableBlocks.every(b => b.text.trim().length === 0)) {
1835
+ this.cacheableSystemBlocks = null;
1836
+ messages.push({ role: 'system', content: this.config.systemPrompt || 'You are a helpful AI assistant.' });
1837
+ }
1838
+ else {
1839
+ // Store cacheable blocks for provider injection
1840
+ this.cacheableSystemBlocks = cacheableBlocks;
1841
+ // Push a regular string Message for backward compatibility (token estimation, etc.)
1842
+ const flatPrompt = cacheableBlocks.map(b => b.text).join('');
1843
+ messages.push({ role: 'system', content: flatPrompt });
1844
+ }
1710
1845
  }
1711
1846
  else {
1712
- // Fallback: manual concatenation (original behavior)
1713
- systemPrompt = this.config.systemPrompt;
1847
+ // Fallback: manual concatenation (original behavior) — no cache markers
1848
+ this.cacheableSystemBlocks = null;
1849
+ let systemPrompt = this.config.systemPrompt;
1714
1850
  if (rulesContent)
1715
1851
  systemPrompt += '\n\n' + rulesContent;
1716
1852
  if (skillsPrompt)
@@ -1721,13 +1857,13 @@ export class ProductionAgent {
1721
1857
  if (toolDescriptions) {
1722
1858
  systemPrompt += '\n\nAvailable tools:\n' + toolDescriptions;
1723
1859
  }
1860
+ // Safety check: ensure system prompt is not empty
1861
+ if (!systemPrompt || systemPrompt.trim().length === 0) {
1862
+ console.warn('[buildMessages] Warning: Empty system prompt detected, using fallback');
1863
+ systemPrompt = this.config.systemPrompt || 'You are a helpful AI assistant.';
1864
+ }
1865
+ messages.push({ role: 'system', content: systemPrompt });
1724
1866
  }
1725
- // Safety check: ensure system prompt is not empty
1726
- if (!systemPrompt || systemPrompt.trim().length === 0) {
1727
- console.warn('[buildMessages] Warning: Empty system prompt detected, using fallback');
1728
- systemPrompt = this.config.systemPrompt || 'You are a helpful AI assistant.';
1729
- }
1730
- messages.push({ role: 'system', content: systemPrompt });
1731
1867
  // Add existing conversation
1732
1868
  for (const msg of this.state.messages) {
1733
1869
  if (msg.role !== 'system') {
@@ -1744,6 +1880,22 @@ export class ProductionAgent {
1744
1880
  async callLLM(messages) {
1745
1881
  const spanId = this.observability?.tracer?.startSpan('llm.call');
1746
1882
  this.emit({ type: 'llm.start', model: this.config.model || 'default' });
1883
+ // Prompt caching (Improvement P1): Replace the system message with structured content
1884
+ // that includes cache_control markers, enabling 60-70% cache hit rates.
1885
+ // The original Message[] is kept for token estimation; the provider gets MessageWithContent[].
1886
+ let providerMessages = messages;
1887
+ if (this.cacheableSystemBlocks && this.cacheableSystemBlocks.length > 0) {
1888
+ providerMessages = messages.map((m, i) => {
1889
+ if (i === 0 && m.role === 'system') {
1890
+ // Replace system message with structured cacheable content
1891
+ return {
1892
+ role: 'system',
1893
+ content: this.cacheableSystemBlocks,
1894
+ };
1895
+ }
1896
+ return m;
1897
+ });
1898
+ }
1747
1899
  // Emit context insight for verbose feedback
1748
1900
  const estimatedTokens = messages.reduce((sum, m) => {
1749
1901
  const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
@@ -1859,7 +2011,7 @@ export class ProductionAgent {
1859
2011
  });
1860
2012
  }
1861
2013
  else {
1862
- response = await this.provider.chat(messages, {
2014
+ response = await this.provider.chat(providerMessages, {
1863
2015
  model: this.config.model,
1864
2016
  tools: Array.from(this.tools.values()),
1865
2017
  });
@@ -2132,6 +2284,29 @@ export class ProductionAgent {
2132
2284
  }
2133
2285
  }
2134
2286
  }
2287
+ // FILE CACHE: Check cache for read_file operations before executing
2288
+ if (this.fileCache && toolCall.name === 'read_file') {
2289
+ const args = toolCall.arguments;
2290
+ const readPath = String(args.path || '');
2291
+ if (readPath) {
2292
+ const cached = this.fileCache.get(readPath);
2293
+ if (cached !== undefined) {
2294
+ const lines = cached.split('\n').length;
2295
+ const cacheResult = { success: true, output: cached, metadata: { lines, bytes: cached.length, cached: true } };
2296
+ const duration = Date.now() - startTime;
2297
+ this.traceCollector?.record({ type: 'tool.end', data: { executionId, status: 'success', result: cacheResult, durationMs: duration } });
2298
+ this.observability?.metrics?.recordToolCall(toolCall.name, duration, true);
2299
+ this.state.metrics.toolCalls++;
2300
+ this.emit({ type: 'tool.complete', tool: toolCall.name, result: cacheResult });
2301
+ results.push({
2302
+ callId: toolCall.id,
2303
+ result: typeof cacheResult === 'string' ? cacheResult : JSON.stringify(cacheResult),
2304
+ });
2305
+ this.observability?.tracer?.endSpan(spanId);
2306
+ continue; // Skip actual file I/O
2307
+ }
2308
+ }
2309
+ }
2135
2310
  // Execute tool (with sandbox if available)
2136
2311
  let result;
2137
2312
  if (this.safety?.sandbox) {
@@ -2170,6 +2345,22 @@ export class ProductionAgent {
2170
2345
  this.observability?.metrics?.recordToolCall(toolCall.name, duration, true);
2171
2346
  this.state.metrics.toolCalls++;
2172
2347
  this.emit({ type: 'tool.complete', tool: toolCall.name, result });
2348
+ // FILE CACHE: Store read results and invalidate on writes
2349
+ if (this.fileCache) {
2350
+ const args = toolCall.arguments;
2351
+ const filePath = String(args.path || args.file_path || '');
2352
+ if (toolCall.name === 'read_file' && filePath) {
2353
+ // Cache successful read results
2354
+ const resultObj = result;
2355
+ if (resultObj?.success && typeof resultObj.output === 'string') {
2356
+ this.fileCache.set(filePath, resultObj.output);
2357
+ }
2358
+ }
2359
+ else if ((toolCall.name === 'write_file' || toolCall.name === 'edit_file' || toolCall.name === 'undo_file_change') && filePath) {
2360
+ // Invalidate cache when files are modified (including undo operations)
2361
+ this.fileCache.invalidate(filePath);
2362
+ }
2363
+ }
2173
2364
  // Emit tool insight with result summary
2174
2365
  const summary = this.summarizeToolResult(toolCall.name, result);
2175
2366
  this.emit({
@@ -3331,7 +3522,9 @@ export class ProductionAgent {
3331
3522
  };
3332
3523
  }
3333
3524
  // DUPLICATE SPAWN PREVENTION with SEMANTIC SIMILARITY
3334
- // First try exact string match, then check semantic similarity for similar tasks
3525
+ // Skip for swarm workers the orchestrator handles retry logic and deduplication
3526
+ // at the task level. Without this bypass, retried swarm tasks return stale results.
3527
+ const isSwarmWorker = agentName.startsWith('swarm-');
3335
3528
  const SEMANTIC_SIMILARITY_THRESHOLD = 0.75; // 75% similarity = duplicate
3336
3529
  const taskKey = `${agentName}:${task.slice(0, 150).toLowerCase().replace(/\s+/g, ' ').trim()}`;
3337
3530
  const now = Date.now();
@@ -3341,30 +3534,33 @@ export class ProductionAgent {
3341
3534
  this.spawnedTasks.delete(key);
3342
3535
  }
3343
3536
  }
3344
- // Check for exact match first
3345
- let existingMatch = this.spawnedTasks.get(taskKey);
3537
+ let existingMatch;
3346
3538
  let matchType = 'exact';
3347
- // If no exact match, check for semantic similarity among same agent's tasks
3348
- if (!existingMatch) {
3349
- for (const [key, entry] of this.spawnedTasks.entries()) {
3350
- // Only compare tasks from the same agent type
3351
- if (!key.startsWith(`${agentName}:`))
3352
- continue;
3353
- if (now - entry.timestamp >= ProductionAgent.SPAWN_DEDUP_WINDOW_MS)
3354
- continue;
3355
- // Extract the task portion from the key
3356
- const existingTask = key.slice(agentName.length + 1);
3357
- const similarity = calculateTaskSimilarity(task, existingTask);
3358
- if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
3359
- existingMatch = entry;
3360
- matchType = 'semantic';
3361
- this.observability?.logger?.debug('Semantic duplicate detected', {
3362
- agent: agentName,
3363
- newTask: task.slice(0, 80),
3364
- existingTask: existingTask.slice(0, 80),
3365
- similarity: (similarity * 100).toFixed(1) + '%',
3366
- });
3367
- break;
3539
+ if (!isSwarmWorker) {
3540
+ // Check for exact match first
3541
+ existingMatch = this.spawnedTasks.get(taskKey);
3542
+ // If no exact match, check for semantic similarity among same agent's tasks
3543
+ if (!existingMatch) {
3544
+ for (const [key, entry] of this.spawnedTasks.entries()) {
3545
+ // Only compare tasks from the same agent type
3546
+ if (!key.startsWith(`${agentName}:`))
3547
+ continue;
3548
+ if (now - entry.timestamp >= ProductionAgent.SPAWN_DEDUP_WINDOW_MS)
3549
+ continue;
3550
+ // Extract the task portion from the key
3551
+ const existingTask = key.slice(agentName.length + 1);
3552
+ const similarity = calculateTaskSimilarity(task, existingTask);
3553
+ if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
3554
+ existingMatch = entry;
3555
+ matchType = 'semantic';
3556
+ this.observability?.logger?.debug('Semantic duplicate detected', {
3557
+ agent: agentName,
3558
+ newTask: task.slice(0, 80),
3559
+ existingTask: existingTask.slice(0, 80),
3560
+ similarity: (similarity * 100).toFixed(1) + '%',
3561
+ });
3562
+ break;
3563
+ }
3368
3564
  }
3369
3565
  }
3370
3566
  }
@@ -3420,20 +3616,34 @@ export class ProductionAgent {
3420
3616
  }
3421
3617
  // Get subagent config with agent-type-specific timeouts and iteration limits
3422
3618
  // Uses dynamic configuration based on agent type (researcher needs more time than reviewer)
3619
+ // Precedence: per-type config > per-type default > global config > hardcoded fallback
3423
3620
  const subagentConfig = this.config.subagent;
3424
3621
  const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
3425
- // Agent-type-specific timeout: researchers get 5min, reviewers get 2min, etc.
3622
+ // Timeout precedence: per-type config override > agent-type default > global config default
3426
3623
  const agentTypeTimeout = getSubagentTimeout(agentName);
3427
- const configTimeout = hasSubagentConfig
3624
+ const rawPerTypeTimeout = hasSubagentConfig
3625
+ ? subagentConfig.timeouts?.[agentName]
3626
+ : undefined;
3627
+ const rawGlobalTimeout = hasSubagentConfig
3428
3628
  ? subagentConfig.defaultTimeout
3429
3629
  : undefined;
3430
- const subagentTimeout = configTimeout ?? agentTypeTimeout;
3431
- // Agent-type-specific iteration limit: researchers get 25, documenters get 10, etc.
3630
+ // Validate: reject negative, NaN, or non-finite timeout values
3631
+ const isValidTimeout = (v) => v !== undefined && Number.isFinite(v) && v > 0;
3632
+ const perTypeConfigTimeout = isValidTimeout(rawPerTypeTimeout) ? rawPerTypeTimeout : undefined;
3633
+ const globalConfigTimeout = isValidTimeout(rawGlobalTimeout) ? rawGlobalTimeout : undefined;
3634
+ const subagentTimeout = perTypeConfigTimeout ?? agentTypeTimeout ?? globalConfigTimeout ?? 300000;
3635
+ // Iteration precedence: per-type config override > agent-type default > global config default
3432
3636
  const agentTypeMaxIter = getSubagentMaxIterations(agentName);
3433
- const configMaxIter = hasSubagentConfig
3637
+ const rawPerTypeMaxIter = hasSubagentConfig
3638
+ ? subagentConfig.maxIterations?.[agentName]
3639
+ : undefined;
3640
+ const rawGlobalMaxIter = hasSubagentConfig
3434
3641
  ? subagentConfig.defaultMaxIterations
3435
3642
  : undefined;
3436
- const defaultMaxIterations = agentDef.maxIterations ?? configMaxIter ?? agentTypeMaxIter;
3643
+ const isValidIter = (v) => v !== undefined && Number.isFinite(v) && v > 0 && Number.isInteger(v);
3644
+ const perTypeConfigMaxIter = isValidIter(rawPerTypeMaxIter) ? rawPerTypeMaxIter : undefined;
3645
+ const globalConfigMaxIter = isValidIter(rawGlobalMaxIter) ? rawGlobalMaxIter : undefined;
3646
+ const defaultMaxIterations = agentDef.maxIterations ?? perTypeConfigMaxIter ?? agentTypeMaxIter ?? globalConfigMaxIter ?? 15;
3437
3647
  // BLACKBOARD CONTEXT INJECTION
3438
3648
  // Gather relevant context from the blackboard for the subagent
3439
3649
  let blackboardContext = '';
@@ -3506,6 +3716,9 @@ export class ProductionAgent {
3506
3716
  const subagentSystemPrompt = parentMode === 'plan'
3507
3717
  ? `${agentDef.systemPrompt}\n\n${SUBAGENT_PLAN_MODE_ADDITION}${blackboardContext}${constraintContext}`
3508
3718
  : `${agentDef.systemPrompt}${blackboardContext}${constraintContext}`;
3719
+ // Allocate budget from pool (or use default) — track allocation ID for release later
3720
+ const pooledBudget = this.getSubagentBudget(agentName, constraints);
3721
+ const poolAllocationId = pooledBudget.allocationId;
3509
3722
  // Create a sub-agent with the agent's config
3510
3723
  // Use SUBAGENT_BUDGET to constrain resource usage (prevents runaway token consumption)
3511
3724
  const subAgent = new ProductionAgent({
@@ -3522,10 +3735,29 @@ export class ProductionAgent {
3522
3735
  memory: false,
3523
3736
  planning: false,
3524
3737
  reflection: false,
3738
+ // Enable lightweight compaction for subagents (Improvement P5)
3739
+ // tokenThreshold configures the Compactor's per-pass size limit
3740
+ // maxContextTokens constrains AutoCompactionManager's percentage thresholds
3741
+ // With maxContextTokens=80000 and default 80% threshold, compaction triggers at ~64K
3742
+ compaction: {
3743
+ enabled: true,
3744
+ mode: 'auto',
3745
+ tokenThreshold: 40000, // Compactor summarization size limit per pass
3746
+ preserveRecentCount: 4, // Preserve fewer messages (splits to 2 user + 2 assistant)
3747
+ preserveToolResults: false, // More aggressive — subagents can re-read files
3748
+ summaryMaxTokens: 500,
3749
+ },
3750
+ // Lower context window for subagents so percentage-based compaction triggers earlier
3751
+ maxContextTokens: 80000,
3525
3752
  observability: this.config.observability,
3526
3753
  sandbox: this.config.sandbox,
3527
3754
  humanInLoop: this.config.humanInLoop,
3528
- executionPolicy: this.config.executionPolicy,
3755
+ // Subagents get 'allow' as default policy since they're already
3756
+ // constrained to their registered tool set. The parent's 'prompt'
3757
+ // policy can't work without humanInLoop.
3758
+ executionPolicy: this.config.executionPolicy
3759
+ ? { ...this.config.executionPolicy, defaultPolicy: 'allow' }
3760
+ : this.config.executionPolicy,
3529
3761
  threads: false,
3530
3762
  // Disable hooks console output in subagents - parent handles event display
3531
3763
  hooks: this.config.hooks === false ? false : {
@@ -3535,11 +3767,11 @@ export class ProductionAgent {
3535
3767
  },
3536
3768
  // Share parent's blackboard for coordination between parallel subagents
3537
3769
  blackboard: this.blackboard || undefined,
3538
- // CONSTRAINED BUDGET: Subagents get smaller budget to prevent runaway consumption
3539
- // Uses SUBAGENT_BUDGET (100k tokens, 4 min) vs STANDARD_BUDGET (200k, 5 min)
3540
- budget: constraints?.maxTokens
3541
- ? { ...SUBAGENT_BUDGET, maxTokens: constraints.maxTokens }
3542
- : SUBAGENT_BUDGET,
3770
+ // Share parent's file cache to eliminate redundant reads across agents
3771
+ fileCache: this.fileCache || undefined,
3772
+ // CONSTRAINED BUDGET: Use pooled budget when available, falling back to SUBAGENT_BUDGET
3773
+ // Pooled budget ensures total tree cost stays bounded by parent's budget
3774
+ budget: pooledBudget.budget,
3543
3775
  });
3544
3776
  // CRITICAL: Subagent inherits parent's mode
3545
3777
  // This ensures that if parent is in plan mode:
@@ -3549,6 +3781,17 @@ export class ProductionAgent {
3549
3781
  if (parentMode !== 'build') {
3550
3782
  subAgent.setMode(parentMode);
3551
3783
  }
3784
+ // APPROVAL BATCHING (Improvement P6): Set approval scope for subagents
3785
+ // Read-only tools are auto-approved; write tools get scoped approval
3786
+ // This reduces interruptions from ~8 per session to ~1-2
3787
+ subAgent.setApprovalScope({
3788
+ autoApprove: ['read_file', 'list_files', 'glob', 'grep', 'show_file_history', 'show_session_changes'],
3789
+ scopedApprove: {
3790
+ write_file: { paths: ['src/', 'tests/', 'tools/'] },
3791
+ edit_file: { paths: ['src/', 'tests/', 'tools/'] },
3792
+ },
3793
+ requireApproval: ['bash', 'delete_file'],
3794
+ });
3552
3795
  // Pass parent's iteration count to subagent for accurate budget tracking
3553
3796
  // This prevents subagents from consuming excessive iterations when parent already used many
3554
3797
  subAgent.setParentIterations(this.getTotalIterations());
@@ -3899,6 +4142,13 @@ export class ProductionAgent {
3899
4142
  // Dispose both sources (linked source disposes its internal state, timeout source handles its timer)
3900
4143
  effectiveSource.dispose();
3901
4144
  progressAwareTimeout.dispose();
4145
+ // BUDGET POOL: Record actual usage and release the allocation
4146
+ // This must happen in finally to ensure cleanup on both success and error paths
4147
+ if (this.budgetPool && poolAllocationId) {
4148
+ const subMetrics = subAgent.getMetrics();
4149
+ this.budgetPool.recordUsage(poolAllocationId, subMetrics.totalTokens, subMetrics.estimatedCost);
4150
+ this.budgetPool.release(poolAllocationId);
4151
+ }
3902
4152
  }
3903
4153
  }
3904
4154
  catch (err) {
@@ -3926,6 +4176,49 @@ export class ProductionAgent {
3926
4176
  * Spawn multiple agents in parallel to work on independent tasks.
3927
4177
  * Uses the shared blackboard for coordination and conflict prevention.
3928
4178
  *
4179
+ * Get budget for a subagent, using the pooled budget when available.
4180
+ * Falls back to the static SUBAGENT_BUDGET if no pool is configured.
4181
+ * Returns both the budget and the pool allocation ID (if any) for tracking.
4182
+ */
4183
+ getSubagentBudget(agentName, constraints) {
4184
+ // If explicit maxTokens constraint, use that
4185
+ if (constraints?.maxTokens) {
4186
+ return {
4187
+ budget: { ...SUBAGENT_BUDGET, maxTokens: constraints.maxTokens },
4188
+ allocationId: null,
4189
+ };
4190
+ }
4191
+ // Try to allocate from the shared budget pool
4192
+ if (this.budgetPool) {
4193
+ const allocationId = `${agentName}-${Date.now()}`;
4194
+ const allocation = this.budgetPool.reserve(allocationId);
4195
+ if (allocation) {
4196
+ return {
4197
+ budget: {
4198
+ ...SUBAGENT_BUDGET,
4199
+ maxTokens: allocation.tokenBudget,
4200
+ softTokenLimit: Math.floor(allocation.tokenBudget * 0.7),
4201
+ maxCost: allocation.costBudget,
4202
+ },
4203
+ allocationId,
4204
+ };
4205
+ }
4206
+ // Pool exhausted — give a tiny emergency budget (just enough to report failure)
4207
+ // This does NOT bypass the pool — it's a fixed small cost for error messaging
4208
+ return {
4209
+ budget: {
4210
+ ...SUBAGENT_BUDGET,
4211
+ maxTokens: 5000,
4212
+ softTokenLimit: 3000,
4213
+ maxCost: 0.01,
4214
+ },
4215
+ allocationId: null,
4216
+ };
4217
+ }
4218
+ // No pool — use default subagent budget
4219
+ return { budget: SUBAGENT_BUDGET, allocationId: null };
4220
+ }
4221
+ /**
3929
4222
  * Uses Promise.allSettled to handle partial failures gracefully - if one
3930
4223
  * agent fails or times out, others can still complete successfully.
3931
4224
  */
@@ -4321,6 +4614,15 @@ If the task is a simple question or doesn't need specialized handling, set bestA
4321
4614
  setParentIterations(count) {
4322
4615
  this.parentIterations = count;
4323
4616
  }
4617
+ /**
4618
+ * Set an approval scope for this agent (used by parent when spawning subagents).
4619
+ * Enables pre-approved operations within a defined scope, reducing approval prompts.
4620
+ */
4621
+ setApprovalScope(scope) {
4622
+ if (this.safety?.humanInLoop) {
4623
+ this.safety.humanInLoop.setApprovalScope(scope);
4624
+ }
4625
+ }
4324
4626
  /**
4325
4627
  * Set an external cancellation token for this agent.
4326
4628
  * Used when spawning subagents to propagate parent timeout/cancellation.