attocode 0.1.6 → 0.1.7

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 (101) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +47 -0
  3. package/dist/src/adapters.d.ts +21 -1
  4. package/dist/src/adapters.d.ts.map +1 -1
  5. package/dist/src/adapters.js +29 -1
  6. package/dist/src/adapters.js.map +1 -1
  7. package/dist/src/agent.d.ts +29 -2
  8. package/dist/src/agent.d.ts.map +1 -1
  9. package/dist/src/agent.js +266 -34
  10. package/dist/src/agent.js.map +1 -1
  11. package/dist/src/commands/agents-commands.d.ts.map +1 -1
  12. package/dist/src/commands/agents-commands.js +18 -4
  13. package/dist/src/commands/agents-commands.js.map +1 -1
  14. package/dist/src/commands/handler.d.ts.map +1 -1
  15. package/dist/src/commands/handler.js +120 -5
  16. package/dist/src/commands/handler.js.map +1 -1
  17. package/dist/src/defaults.d.ts +30 -0
  18. package/dist/src/defaults.d.ts.map +1 -1
  19. package/dist/src/defaults.js +52 -2
  20. package/dist/src/defaults.js.map +1 -1
  21. package/dist/src/integrations/agent-registry.js +4 -4
  22. package/dist/src/integrations/agent-registry.js.map +1 -1
  23. package/dist/src/integrations/index.d.ts +1 -0
  24. package/dist/src/integrations/index.d.ts.map +1 -1
  25. package/dist/src/integrations/index.js +2 -0
  26. package/dist/src/integrations/index.js.map +1 -1
  27. package/dist/src/integrations/sqlite-store.d.ts.map +1 -1
  28. package/dist/src/integrations/sqlite-store.js +17 -1
  29. package/dist/src/integrations/sqlite-store.js.map +1 -1
  30. package/dist/src/integrations/task-manager.d.ts +132 -0
  31. package/dist/src/integrations/task-manager.d.ts.map +1 -0
  32. package/dist/src/integrations/task-manager.js +309 -0
  33. package/dist/src/integrations/task-manager.js.map +1 -0
  34. package/dist/src/modes/tui.d.ts.map +1 -1
  35. package/dist/src/modes/tui.js +9 -0
  36. package/dist/src/modes/tui.js.map +1 -1
  37. package/dist/src/modes.d.ts +23 -0
  38. package/dist/src/modes.d.ts.map +1 -1
  39. package/dist/src/modes.js +61 -0
  40. package/dist/src/modes.js.map +1 -1
  41. package/dist/src/providers/adapters/openai.d.ts +46 -2
  42. package/dist/src/providers/adapters/openai.d.ts.map +1 -1
  43. package/dist/src/providers/adapters/openai.js +221 -21
  44. package/dist/src/providers/adapters/openai.js.map +1 -1
  45. package/dist/src/tools/agent.d.ts +18 -1
  46. package/dist/src/tools/agent.d.ts.map +1 -1
  47. package/dist/src/tools/agent.js +38 -2
  48. package/dist/src/tools/agent.js.map +1 -1
  49. package/dist/src/tools/tasks.d.ts +32 -0
  50. package/dist/src/tools/tasks.d.ts.map +1 -0
  51. package/dist/src/tools/tasks.js +334 -0
  52. package/dist/src/tools/tasks.js.map +1 -0
  53. package/dist/src/tracing/trace-collector.d.ts +81 -0
  54. package/dist/src/tracing/trace-collector.d.ts.map +1 -1
  55. package/dist/src/tracing/trace-collector.js +216 -4
  56. package/dist/src/tracing/trace-collector.js.map +1 -1
  57. package/dist/src/tracing/types.d.ts +8 -0
  58. package/dist/src/tracing/types.d.ts.map +1 -1
  59. package/dist/src/tracing/types.js.map +1 -1
  60. package/dist/src/tui/app.d.ts.map +1 -1
  61. package/dist/src/tui/app.js +264 -29
  62. package/dist/src/tui/app.js.map +1 -1
  63. package/dist/src/tui/components/ActiveAgentsPanel.d.ts +45 -0
  64. package/dist/src/tui/components/ActiveAgentsPanel.d.ts.map +1 -0
  65. package/dist/src/tui/components/ActiveAgentsPanel.js +121 -0
  66. package/dist/src/tui/components/ActiveAgentsPanel.js.map +1 -0
  67. package/dist/src/tui/components/ErrorBoundary.d.ts +63 -0
  68. package/dist/src/tui/components/ErrorBoundary.d.ts.map +1 -0
  69. package/dist/src/tui/components/ErrorBoundary.js +88 -0
  70. package/dist/src/tui/components/ErrorBoundary.js.map +1 -0
  71. package/dist/src/tui/components/TasksPanel.d.ts +25 -0
  72. package/dist/src/tui/components/TasksPanel.d.ts.map +1 -0
  73. package/dist/src/tui/components/TasksPanel.js +101 -0
  74. package/dist/src/tui/components/TasksPanel.js.map +1 -0
  75. package/dist/src/tui/components/index.d.ts +3 -0
  76. package/dist/src/tui/components/index.d.ts.map +1 -1
  77. package/dist/src/tui/components/index.js +6 -0
  78. package/dist/src/tui/components/index.js.map +1 -1
  79. package/dist/src/tui/hooks/index.d.ts +7 -0
  80. package/dist/src/tui/hooks/index.d.ts.map +1 -0
  81. package/dist/src/tui/hooks/index.js +7 -0
  82. package/dist/src/tui/hooks/index.js.map +1 -0
  83. package/dist/src/tui/hooks/useMessagePruning.d.ts +114 -0
  84. package/dist/src/tui/hooks/useMessagePruning.d.ts.map +1 -0
  85. package/dist/src/tui/hooks/useMessagePruning.js +127 -0
  86. package/dist/src/tui/hooks/useMessagePruning.js.map +1 -0
  87. package/dist/src/tui/index.d.ts +3 -0
  88. package/dist/src/tui/index.d.ts.map +1 -1
  89. package/dist/src/tui/index.js +9 -0
  90. package/dist/src/tui/index.js.map +1 -1
  91. package/dist/src/tui/utils/index.d.ts +7 -0
  92. package/dist/src/tui/utils/index.d.ts.map +1 -0
  93. package/dist/src/tui/utils/index.js +7 -0
  94. package/dist/src/tui/utils/index.js.map +1 -0
  95. package/dist/src/tui/utils/keyboard.d.ts +123 -0
  96. package/dist/src/tui/utils/keyboard.d.ts.map +1 -0
  97. package/dist/src/tui/utils/keyboard.js +185 -0
  98. package/dist/src/tui/utils/keyboard.js.map +1 -0
  99. package/dist/src/types.d.ts +17 -0
  100. package/dist/src/types.d.ts.map +1 -1
  101. package/package.json +1 -1
package/dist/src/agent.js CHANGED
@@ -18,10 +18,10 @@
18
18
  * - Execution Policies (Lesson 23)
19
19
  * - Thread Management (Lesson 24)
20
20
  */
21
- import { buildConfig, isFeatureEnabled, getEnabledFeatures, } from './defaults.js';
22
- import { createModeManager, formatModeList, parseMode, } from './modes.js';
21
+ import { buildConfig, isFeatureEnabled, getEnabledFeatures, getSubagentTimeout, getSubagentMaxIterations, } from './defaults.js';
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, 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, createSharedBlackboard, } 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, createSharedBlackboard, createTaskManager, } 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
@@ -29,6 +29,8 @@ import { modelRegistry } from './costs/index.js';
29
29
  import { getModelContextLength } from './integrations/openrouter-pricing.js';
30
30
  // Spawn agent tools for LLM-driven subagent delegation
31
31
  import { createBoundSpawnAgentTool, createBoundSpawnAgentsParallelTool, } from './tools/agent.js';
32
+ // Task tools for Claude Code-style task management
33
+ import { createTaskTools, } from './tools/tasks.js';
32
34
  // =============================================================================
33
35
  // PRODUCTION AGENT
34
36
  // =============================================================================
@@ -72,12 +74,16 @@ export class ProductionAgent {
72
74
  capabilitiesRegistry = null;
73
75
  toolResolver = null;
74
76
  blackboard = null;
77
+ taskManager = null;
75
78
  // Duplicate spawn prevention - tracks recently spawned tasks to prevent doom loops
76
79
  // Map<taskKey, { timestamp: number; result: string; queuedChanges: number }>
77
80
  spawnedTasks = new Map();
78
81
  static SPAWN_DEDUP_WINDOW_MS = 60000; // 60 seconds
79
82
  // Parent iteration tracking for total budget calculation
80
83
  parentIterations = 0;
84
+ // External cancellation token (for subagent timeout propagation)
85
+ // When set, the agent will check this token in addition to its own cancellation manager
86
+ externalCancellationToken = null;
81
87
  // Initialization tracking
82
88
  initPromises = [];
83
89
  initComplete = false;
@@ -234,11 +240,25 @@ export class ProductionAgent {
234
240
  console.warn('[ProductionAgent] Failed to load user agents:', err);
235
241
  }));
236
242
  // Register spawn_agent tool so LLM can delegate to subagents
237
- const boundSpawnTool = createBoundSpawnAgentTool((name, task) => this.spawnAgent(name, task));
243
+ const boundSpawnTool = createBoundSpawnAgentTool((name, task, constraints) => this.spawnAgent(name, task, constraints));
238
244
  this.tools.set(boundSpawnTool.name, boundSpawnTool);
239
245
  // Register spawn_agents_parallel tool for parallel subagent execution
240
246
  const boundParallelSpawnTool = createBoundSpawnAgentsParallelTool((tasks) => this.spawnAgentsParallel(tasks));
241
247
  this.tools.set(boundParallelSpawnTool.name, boundParallelSpawnTool);
248
+ // Task Manager - Claude Code-style task system for coordination
249
+ this.taskManager = createTaskManager();
250
+ // Forward task events
251
+ this.taskManager.on('task.created', (data) => {
252
+ this.emit({ type: 'task.created', task: data.task });
253
+ });
254
+ this.taskManager.on('task.updated', (data) => {
255
+ this.emit({ type: 'task.updated', task: data.task });
256
+ });
257
+ // Register task tools
258
+ const taskTools = createTaskTools(this.taskManager);
259
+ for (const tool of taskTools) {
260
+ this.tools.set(tool.name, tool);
261
+ }
242
262
  // Cancellation Support
243
263
  if (isFeatureEnabled(this.config.cancellation)) {
244
264
  this.cancellation = createCancellationManager();
@@ -863,10 +883,16 @@ export class ProductionAgent {
863
883
  });
864
884
  // =======================================================================
865
885
  // CANCELLATION CHECK
886
+ // Checks both internal cancellation (ESC key) and external cancellation
887
+ // (parent timeout when this agent is a subagent)
866
888
  // =======================================================================
867
889
  if (this.cancellation?.isCancelled) {
868
890
  this.cancellation.token.throwIfCancellationRequested();
869
891
  }
892
+ // Also check external cancellation token (from parent when spawned as subagent)
893
+ if (this.externalCancellationToken?.isCancellationRequested) {
894
+ this.externalCancellationToken.throwIfCancellationRequested();
895
+ }
870
896
  // =======================================================================
871
897
  // RESOURCE CHECK - system resource limits
872
898
  // =======================================================================
@@ -1900,17 +1926,21 @@ export class ProductionAgent {
1900
1926
  // Execute tool (with sandbox if available)
1901
1927
  let result;
1902
1928
  if (this.safety?.sandbox) {
1903
- // CRITICAL: spawn_agent needs a MUCH longer timeout than regular tools
1929
+ // CRITICAL: spawn_agent and spawn_agents_parallel need MUCH longer timeouts
1904
1930
  // The default 60s sandbox timeout would kill subagents prematurely
1905
1931
  // Subagents may run for minutes (per their own timeout config)
1906
1932
  const isSpawnAgent = toolCall.name === 'spawn_agent';
1933
+ const isSpawnParallel = toolCall.name === 'spawn_agents_parallel';
1934
+ const isSubagentTool = isSpawnAgent || isSpawnParallel;
1907
1935
  const subagentConfig = this.config.subagent;
1908
1936
  const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
1909
1937
  const subagentTimeout = hasSubagentConfig
1910
1938
  ? subagentConfig.defaultTimeout ?? 600000 // 10 min default
1911
1939
  : 600000;
1912
- // Use subagent timeout + buffer for spawn_agent, default for others
1913
- const toolTimeout = isSpawnAgent ? subagentTimeout + 30000 : undefined;
1940
+ // Use subagent timeout + buffer for spawn tools, default for others
1941
+ // For spawn_agents_parallel, multiply by number of agents (they run in parallel,
1942
+ // but the total wall-clock time should still allow the slowest agent to complete)
1943
+ const toolTimeout = isSubagentTool ? subagentTimeout + 30000 : undefined;
1914
1944
  result = await this.safety.sandbox.executeWithLimits(() => tool.execute(toolCall.arguments), toolTimeout);
1915
1945
  }
1916
1946
  else {
@@ -2294,6 +2324,13 @@ export class ProductionAgent {
2294
2324
  getTraceCollector() {
2295
2325
  return this.traceCollector;
2296
2326
  }
2327
+ /**
2328
+ * Set a trace collector for this agent.
2329
+ * Used for subagents to share the parent's trace collector (with subagent context).
2330
+ */
2331
+ setTraceCollector(collector) {
2332
+ this.traceCollector = collector;
2333
+ }
2297
2334
  /**
2298
2335
  * Get the learning store for cross-session learning.
2299
2336
  * Returns null if learning store is not enabled.
@@ -2813,6 +2850,11 @@ export class ProductionAgent {
2813
2850
  if (!this.threadManager) {
2814
2851
  throw new Error('Thread management not enabled. Enable it in config to use createCheckpoint()');
2815
2852
  }
2853
+ // CRITICAL: Sync current state.messages to threadManager before checkpoint
2854
+ // The run() method adds messages directly to this.state.messages but doesn't sync
2855
+ // to threadManager, so thread.messages would be empty without this sync
2856
+ const thread = this.threadManager.getActiveThread();
2857
+ thread.messages = [...this.state.messages];
2816
2858
  const checkpoint = this.threadManager.createCheckpoint({
2817
2859
  label,
2818
2860
  agentState: this.state,
@@ -2987,8 +3029,12 @@ export class ProductionAgent {
2987
3029
  /**
2988
3030
  * Spawn an agent to execute a task.
2989
3031
  * Returns the result when the agent completes.
3032
+ *
3033
+ * @param agentName - Name of the agent to spawn (researcher, coder, etc.)
3034
+ * @param task - The task description for the agent
3035
+ * @param constraints - Optional constraints to keep the subagent focused
2990
3036
  */
2991
- async spawnAgent(agentName, task) {
3037
+ async spawnAgent(agentName, task, constraints) {
2992
3038
  if (!this.agentRegistry) {
2993
3039
  return {
2994
3040
  success: false,
@@ -3004,10 +3050,10 @@ export class ProductionAgent {
3004
3050
  metrics: { tokens: 0, duration: 0, toolCalls: 0 },
3005
3051
  };
3006
3052
  }
3007
- // DUPLICATE SPAWN PREVENTION
3008
- // Create a key from agent name and normalized task (first 150 chars to catch same intent)
3053
+ // DUPLICATE SPAWN PREVENTION with SEMANTIC SIMILARITY
3054
+ // First try exact string match, then check semantic similarity for similar tasks
3055
+ const SEMANTIC_SIMILARITY_THRESHOLD = 0.75; // 75% similarity = duplicate
3009
3056
  const taskKey = `${agentName}:${task.slice(0, 150).toLowerCase().replace(/\s+/g, ' ').trim()}`;
3010
- const existing = this.spawnedTasks.get(taskKey);
3011
3057
  const now = Date.now();
3012
3058
  // Clean up old entries (older than dedup window)
3013
3059
  for (const [key, entry] of this.spawnedTasks.entries()) {
@@ -3015,21 +3061,48 @@ export class ProductionAgent {
3015
3061
  this.spawnedTasks.delete(key);
3016
3062
  }
3017
3063
  }
3018
- if (existing && now - existing.timestamp < ProductionAgent.SPAWN_DEDUP_WINDOW_MS) {
3019
- // Same task spawned within the dedup window - return cached result
3020
- // Log this as a warning for observability (no special event type needed)
3064
+ // Check for exact match first
3065
+ let existingMatch = this.spawnedTasks.get(taskKey);
3066
+ let matchType = 'exact';
3067
+ // If no exact match, check for semantic similarity among same agent's tasks
3068
+ if (!existingMatch) {
3069
+ for (const [key, entry] of this.spawnedTasks.entries()) {
3070
+ // Only compare tasks from the same agent type
3071
+ if (!key.startsWith(`${agentName}:`))
3072
+ continue;
3073
+ if (now - entry.timestamp >= ProductionAgent.SPAWN_DEDUP_WINDOW_MS)
3074
+ continue;
3075
+ // Extract the task portion from the key
3076
+ const existingTask = key.slice(agentName.length + 1);
3077
+ const similarity = calculateTaskSimilarity(task, existingTask);
3078
+ if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
3079
+ existingMatch = entry;
3080
+ matchType = 'semantic';
3081
+ this.observability?.logger?.debug('Semantic duplicate detected', {
3082
+ agent: agentName,
3083
+ newTask: task.slice(0, 80),
3084
+ existingTask: existingTask.slice(0, 80),
3085
+ similarity: (similarity * 100).toFixed(1) + '%',
3086
+ });
3087
+ break;
3088
+ }
3089
+ }
3090
+ }
3091
+ if (existingMatch && now - existingMatch.timestamp < ProductionAgent.SPAWN_DEDUP_WINDOW_MS) {
3092
+ // Same or semantically similar task spawned within the dedup window
3021
3093
  this.observability?.logger?.warn('Duplicate spawn prevented', {
3022
3094
  agent: agentName,
3023
3095
  task: task.slice(0, 100),
3024
- originalTimestamp: existing.timestamp,
3025
- elapsedMs: now - existing.timestamp,
3096
+ matchType,
3097
+ originalTimestamp: existingMatch.timestamp,
3098
+ elapsedMs: now - existingMatch.timestamp,
3026
3099
  });
3027
- const duplicateMessage = `[DUPLICATE SPAWN PREVENTED]\n` +
3028
- `This task was already spawned ${Math.round((now - existing.timestamp) / 1000)}s ago.\n` +
3029
- `${existing.queuedChanges > 0
3030
- ? `The previous spawn queued ${existing.queuedChanges} change(s) to the pending plan.\n` +
3100
+ const duplicateMessage = `[DUPLICATE SPAWN PREVENTED${matchType === 'semantic' ? ' - SEMANTIC MATCH' : ''}]\n` +
3101
+ `This task was already spawned ${Math.round((now - existingMatch.timestamp) / 1000)}s ago.\n` +
3102
+ `${existingMatch.queuedChanges > 0
3103
+ ? `The previous spawn queued ${existingMatch.queuedChanges} change(s) to the pending plan.\n` +
3031
3104
  `These changes are already in your plan - do NOT spawn again.\n`
3032
- : ''}Previous result summary:\n${existing.result.slice(0, 500)}`;
3105
+ : ''}Previous result summary:\n${existingMatch.result.slice(0, 500)}`;
3033
3106
  return {
3034
3107
  success: true, // Mark as success since original task completed
3035
3108
  output: duplicateMessage,
@@ -3049,16 +3122,88 @@ export class ProductionAgent {
3049
3122
  const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
3050
3123
  ? agentDef.model
3051
3124
  : this.config.model;
3052
- // Get subagent config with defaults
3053
- // Note: subagent config is SubagentConfig | false from buildConfig
3125
+ // Get subagent config with agent-type-specific timeouts and iteration limits
3126
+ // Uses dynamic configuration based on agent type (researcher needs more time than reviewer)
3054
3127
  const subagentConfig = this.config.subagent;
3055
3128
  const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
3056
- const defaultMaxIterations = hasSubagentConfig
3057
- ? subagentConfig.defaultMaxIterations ?? 25
3058
- : 25; // Increased from 10 to allow more thorough exploration
3059
- const subagentTimeout = hasSubagentConfig
3060
- ? subagentConfig.defaultTimeout ?? 600000
3061
- : 600000; // Increased from 120s to 600s (10 minutes) for deeper analysis
3129
+ // Agent-type-specific timeout: researchers get 5min, reviewers get 2min, etc.
3130
+ const agentTypeTimeout = getSubagentTimeout(agentName);
3131
+ const configTimeout = hasSubagentConfig
3132
+ ? subagentConfig.defaultTimeout
3133
+ : undefined;
3134
+ const subagentTimeout = configTimeout ?? agentTypeTimeout;
3135
+ // Agent-type-specific iteration limit: researchers get 25, documenters get 10, etc.
3136
+ const agentTypeMaxIter = getSubagentMaxIterations(agentName);
3137
+ const configMaxIter = hasSubagentConfig
3138
+ ? subagentConfig.defaultMaxIterations
3139
+ : undefined;
3140
+ const defaultMaxIterations = agentDef.maxIterations ?? configMaxIter ?? agentTypeMaxIter;
3141
+ // BLACKBOARD CONTEXT INJECTION
3142
+ // Gather relevant context from the blackboard for the subagent
3143
+ let blackboardContext = '';
3144
+ const parentAgentId = `parent-${Date.now()}`;
3145
+ if (this.blackboard) {
3146
+ // Post parent's exploration context before spawning
3147
+ this.blackboard.post(parentAgentId, {
3148
+ topic: 'spawn.parent_context',
3149
+ content: `Parent spawning ${agentName} for task: ${task.slice(0, 200)}`,
3150
+ type: 'progress',
3151
+ confidence: 1,
3152
+ metadata: { agentName, taskPreview: task.slice(0, 100) },
3153
+ });
3154
+ // Gather recent findings that might help the subagent
3155
+ const recentFindings = this.blackboard.query({
3156
+ limit: 5,
3157
+ types: ['discovery', 'analysis', 'progress'],
3158
+ minConfidence: 0.7,
3159
+ });
3160
+ if (recentFindings.length > 0) {
3161
+ const findingsSummary = recentFindings
3162
+ .map(f => `- [${f.agentId}] ${f.topic}: ${f.content.slice(0, 150)}${f.content.length > 150 ? '...' : ''}`)
3163
+ .join('\n');
3164
+ blackboardContext = `\n\n**BLACKBOARD CONTEXT (from parent/sibling agents):**\n${findingsSummary}\n`;
3165
+ }
3166
+ }
3167
+ // Check for files already being modified in parent's pending plan
3168
+ const currentPlan = this.pendingPlanManager.getPendingPlan();
3169
+ if (currentPlan && currentPlan.proposedChanges.length > 0) {
3170
+ const pendingFiles = currentPlan.proposedChanges
3171
+ .filter((c) => c.tool === 'write_file' || c.tool === 'edit_file')
3172
+ .map((c) => c.args.path || c.args.file_path)
3173
+ .filter(Boolean);
3174
+ if (pendingFiles.length > 0) {
3175
+ blackboardContext += `\n**FILES ALREADY IN PENDING PLAN (do not duplicate):**\n${pendingFiles.slice(0, 10).join('\n')}\n`;
3176
+ }
3177
+ }
3178
+ // CONSTRAINT INJECTION
3179
+ // Add constraints to the subagent's context if provided
3180
+ let constraintContext = '';
3181
+ if (constraints) {
3182
+ const constraintParts = [];
3183
+ if (constraints.focusAreas && constraints.focusAreas.length > 0) {
3184
+ constraintParts.push(`**FOCUS AREAS (limit exploration to these paths):**\n${constraints.focusAreas.map(a => ` - ${a}`).join('\n')}`);
3185
+ }
3186
+ if (constraints.excludeAreas && constraints.excludeAreas.length > 0) {
3187
+ constraintParts.push(`**EXCLUDED AREAS (do NOT explore these):**\n${constraints.excludeAreas.map(a => ` - ${a}`).join('\n')}`);
3188
+ }
3189
+ if (constraints.requiredDeliverables && constraints.requiredDeliverables.length > 0) {
3190
+ constraintParts.push(`**REQUIRED DELIVERABLES (you must produce these):**\n${constraints.requiredDeliverables.map(d => ` - ${d}`).join('\n')}`);
3191
+ }
3192
+ if (constraints.maxTokens) {
3193
+ constraintParts.push(`**TOKEN BUDGET:** ${constraints.maxTokens} tokens maximum`);
3194
+ }
3195
+ if (constraints.timeboxMinutes) {
3196
+ constraintParts.push(`**TIME LIMIT:** ${constraints.timeboxMinutes} minutes (soft limit - wrap up if approaching)`);
3197
+ }
3198
+ if (constraintParts.length > 0) {
3199
+ constraintContext = `\n\n**EXECUTION CONSTRAINTS:**\n${constraintParts.join('\n\n')}\n`;
3200
+ }
3201
+ }
3202
+ // Build subagent system prompt with subagent-specific plan mode addition
3203
+ const parentMode = this.getMode();
3204
+ const subagentSystemPrompt = parentMode === 'plan'
3205
+ ? `${agentDef.systemPrompt}\n\n${SUBAGENT_PLAN_MODE_ADDITION}${blackboardContext}${constraintContext}`
3206
+ : `${agentDef.systemPrompt}${blackboardContext}${constraintContext}`;
3062
3207
  // Create a sub-agent with the agent's config
3063
3208
  const subAgent = new ProductionAgent({
3064
3209
  provider: this.provider,
@@ -3067,7 +3212,7 @@ export class ProductionAgent {
3067
3212
  toolResolver: this.toolResolver || undefined,
3068
3213
  // Pass MCP tool summaries so subagent knows what tools are available
3069
3214
  mcpToolSummaries: this.config.mcpToolSummaries,
3070
- systemPrompt: agentDef.systemPrompt,
3215
+ systemPrompt: subagentSystemPrompt,
3071
3216
  model: resolvedModel,
3072
3217
  maxIterations: agentDef.maxIterations || defaultMaxIterations,
3073
3218
  // Inherit some features but keep subagent simpler
@@ -3093,13 +3238,23 @@ export class ProductionAgent {
3093
3238
  // - Subagent's read operations execute immediately (visible exploration)
3094
3239
  // - Subagent's write operations get queued in the subagent's pending plan
3095
3240
  // - User maintains control over what actually gets written
3096
- const parentMode = this.getMode();
3097
3241
  if (parentMode !== 'build') {
3098
3242
  subAgent.setMode(parentMode);
3099
3243
  }
3100
3244
  // Pass parent's iteration count to subagent for accurate budget tracking
3101
3245
  // This prevents subagents from consuming excessive iterations when parent already used many
3102
3246
  subAgent.setParentIterations(this.getTotalIterations());
3247
+ // UNIFIED TRACING: Share parent's trace collector with subagent context
3248
+ // This ensures all subagent events are written to the same trace file as the parent,
3249
+ // tagged with subagent context for proper aggregation in /trace output
3250
+ if (this.traceCollector) {
3251
+ const subagentTraceView = this.traceCollector.createSubagentView({
3252
+ parentSessionId: this.traceCollector.getSessionId() || 'unknown',
3253
+ agentType: agentName,
3254
+ spawnedAtIteration: this.state.iteration,
3255
+ });
3256
+ subAgent.setTraceCollector(subagentTraceView);
3257
+ }
3103
3258
  // Forward events from subagent with context
3104
3259
  subAgent.subscribe(event => {
3105
3260
  // Tag event with subagent source so TUI can display it properly
@@ -3113,6 +3268,10 @@ export class ProductionAgent {
3113
3268
  const effectiveSource = parentSource
3114
3269
  ? createLinkedToken(parentSource, timeoutSource)
3115
3270
  : timeoutSource;
3271
+ // CRITICAL: Pass the cancellation token to the subagent so it can check and stop
3272
+ // gracefully when timeout fires. Without this, the subagent continues running as
3273
+ // a "zombie" even after race() returns with a timeout error.
3274
+ subAgent.setExternalCancellation(effectiveSource.token);
3116
3275
  try {
3117
3276
  // Run the task with cancellation propagation from parent
3118
3277
  const result = await race(subAgent.run(task), effectiveSource.token);
@@ -3225,6 +3384,20 @@ export class ProductionAgent {
3225
3384
  ? 'User cancelled'
3226
3385
  : `Timed out after ${subagentTimeout}ms`;
3227
3386
  this.emit({ type: 'agent.error', agentId: agentName, error: reason });
3387
+ // =======================================================================
3388
+ // PRESERVE PARTIAL RESULTS
3389
+ // Instead of discarding all work, capture whatever the subagent produced
3390
+ // before timeout. This prevents the "zombie agent" problem where tokens
3391
+ // are consumed but results are lost.
3392
+ // =======================================================================
3393
+ const subagentState = subAgent.getState();
3394
+ const subagentMetrics = subAgent.getMetrics();
3395
+ // Extract partial response from the last assistant message
3396
+ const assistantMessages = subagentState.messages.filter(m => m.role === 'assistant');
3397
+ const lastAssistantMsg = assistantMessages[assistantMessages.length - 1];
3398
+ const partialResponse = typeof lastAssistantMsg?.content === 'string'
3399
+ ? lastAssistantMsg.content
3400
+ : '';
3228
3401
  // Extract pending plan before cleanup (even on cancellation, preserve any queued work)
3229
3402
  let cancelledQueuedSummary = '';
3230
3403
  if (subAgent.hasPendingPlan()) {
@@ -3267,13 +3440,50 @@ export class ProductionAgent {
3267
3440
  catch {
3268
3441
  // Ignore cleanup errors on cancellation
3269
3442
  }
3443
+ // Build output message with partial results
3270
3444
  const baseOutput = isUserCancellation
3271
3445
  ? `Subagent '${agentName}' was cancelled by user.`
3272
- : `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s. The task may be too complex or the model may be slow.`;
3446
+ : `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s.`;
3447
+ // Include partial response if we have one
3448
+ const partialResultSection = partialResponse
3449
+ ? `\n\n[PARTIAL RESULTS BEFORE TIMEOUT]\n${partialResponse.slice(0, 2000)}${partialResponse.length > 2000 ? '...(truncated)' : ''}`
3450
+ : '';
3451
+ // Enhanced tracing: Record subagent timeout with partial results
3452
+ this.traceCollector?.record({
3453
+ type: 'subagent.link',
3454
+ data: {
3455
+ parentSessionId: this.traceCollector.getSessionId() || 'unknown',
3456
+ childSessionId,
3457
+ childTraceId,
3458
+ childConfig: {
3459
+ agentType: agentName,
3460
+ model: resolvedModel || 'default',
3461
+ task,
3462
+ tools: agentTools.map(t => t.name),
3463
+ },
3464
+ spawnContext: {
3465
+ reason: `Delegated task: ${task.slice(0, 100)}`,
3466
+ expectedOutcome: agentDef.description,
3467
+ parentIteration: this.state.iteration,
3468
+ },
3469
+ result: {
3470
+ success: false,
3471
+ summary: `[TIMEOUT] ${baseOutput}\n${partialResponse.slice(0, 200)}`,
3472
+ tokensUsed: subagentMetrics.totalTokens,
3473
+ durationMs: duration,
3474
+ },
3475
+ },
3476
+ });
3273
3477
  return {
3274
3478
  success: false,
3275
- output: baseOutput + cancelledQueuedSummary,
3276
- metrics: { tokens: 0, duration, toolCalls: 0 },
3479
+ output: baseOutput + partialResultSection + cancelledQueuedSummary,
3480
+ // IMPORTANT: Use actual metrics instead of zeros
3481
+ // This ensures accurate token tracking in /trace output
3482
+ metrics: {
3483
+ tokens: subagentMetrics.totalTokens,
3484
+ duration,
3485
+ toolCalls: subagentMetrics.toolCalls,
3486
+ },
3277
3487
  };
3278
3488
  }
3279
3489
  throw err; // Re-throw non-cancellation errors
@@ -3663,6 +3873,22 @@ If the task is a simple question or doesn't need specialized handling, set bestA
3663
3873
  setParentIterations(count) {
3664
3874
  this.parentIterations = count;
3665
3875
  }
3876
+ /**
3877
+ * Set an external cancellation token for this agent.
3878
+ * Used when spawning subagents to propagate parent timeout/cancellation.
3879
+ * The agent will check this token in its main loop and stop gracefully
3880
+ * when cancellation is requested, preserving partial results.
3881
+ */
3882
+ setExternalCancellation(token) {
3883
+ this.externalCancellationToken = token;
3884
+ }
3885
+ /**
3886
+ * Check if external cancellation has been requested.
3887
+ * Returns true if the external token signals cancellation.
3888
+ */
3889
+ isExternallyCancelled() {
3890
+ return this.externalCancellationToken?.isCancellationRequested ?? false;
3891
+ }
3666
3892
  /**
3667
3893
  * Get total iterations (this agent + parent).
3668
3894
  * Used for accurate budget tracking across subagent hierarchies.
@@ -3844,6 +4070,12 @@ If the task is a simple question or doesn't need specialized handling, set bestA
3844
4070
  getAgentRegistry() {
3845
4071
  return this.agentRegistry;
3846
4072
  }
4073
+ /**
4074
+ * Get the task manager instance for task tracking.
4075
+ */
4076
+ getTaskManager() {
4077
+ return this.taskManager;
4078
+ }
3847
4079
  /**
3848
4080
  * Get all loaded skills.
3849
4081
  */