attocode 0.1.5 → 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.
- package/CHANGELOG.md +27 -2
- package/README.md +47 -0
- package/dist/src/adapters.d.ts +21 -1
- package/dist/src/adapters.d.ts.map +1 -1
- package/dist/src/adapters.js +29 -1
- package/dist/src/adapters.js.map +1 -1
- package/dist/src/agent.d.ts +69 -3
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +692 -42
- package/dist/src/agent.js.map +1 -1
- package/dist/src/commands/agents-commands.d.ts.map +1 -1
- package/dist/src/commands/agents-commands.js +18 -4
- package/dist/src/commands/agents-commands.js.map +1 -1
- package/dist/src/commands/handler.d.ts.map +1 -1
- package/dist/src/commands/handler.js +120 -5
- package/dist/src/commands/handler.js.map +1 -1
- package/dist/src/core/index.d.ts +1 -6
- package/dist/src/core/index.d.ts.map +1 -1
- package/dist/src/core/index.js +4 -7
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/defaults.d.ts +31 -1
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +52 -2
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/integrations/agent-registry.d.ts.map +1 -1
- package/dist/src/integrations/agent-registry.js +12 -5
- package/dist/src/integrations/agent-registry.js.map +1 -1
- package/dist/src/integrations/codebase-context.d.ts +163 -0
- package/dist/src/integrations/codebase-context.d.ts.map +1 -1
- package/dist/src/integrations/codebase-context.js +462 -0
- package/dist/src/integrations/codebase-context.js.map +1 -1
- package/dist/src/integrations/economics.js +2 -2
- package/dist/src/integrations/economics.js.map +1 -1
- package/dist/src/integrations/file-change-tracker.d.ts +1 -0
- package/dist/src/integrations/file-change-tracker.d.ts.map +1 -1
- package/dist/src/integrations/file-change-tracker.js +30 -12
- package/dist/src/integrations/file-change-tracker.js.map +1 -1
- package/dist/src/integrations/graph-visualization.d.ts +72 -0
- package/dist/src/integrations/graph-visualization.d.ts.map +1 -0
- package/dist/src/integrations/graph-visualization.js +383 -0
- package/dist/src/integrations/graph-visualization.js.map +1 -0
- package/dist/src/integrations/index.d.ts +3 -1
- package/dist/src/integrations/index.d.ts.map +1 -1
- package/dist/src/integrations/index.js +4 -0
- package/dist/src/integrations/index.js.map +1 -1
- package/dist/src/integrations/memory.d.ts +8 -0
- package/dist/src/integrations/memory.d.ts.map +1 -1
- package/dist/src/integrations/memory.js +23 -0
- package/dist/src/integrations/memory.js.map +1 -1
- package/dist/src/integrations/pending-plan.d.ts +26 -1
- package/dist/src/integrations/pending-plan.d.ts.map +1 -1
- package/dist/src/integrations/pending-plan.js +131 -3
- package/dist/src/integrations/pending-plan.js.map +1 -1
- package/dist/src/integrations/planning.d.ts +19 -0
- package/dist/src/integrations/planning.d.ts.map +1 -1
- package/dist/src/integrations/planning.js +78 -4
- package/dist/src/integrations/planning.js.map +1 -1
- package/dist/src/integrations/safety.d.ts +4 -0
- package/dist/src/integrations/safety.d.ts.map +1 -1
- package/dist/src/integrations/safety.js +46 -7
- package/dist/src/integrations/safety.js.map +1 -1
- package/dist/src/integrations/sqlite-store.d.ts.map +1 -1
- package/dist/src/integrations/sqlite-store.js +24 -2
- package/dist/src/integrations/sqlite-store.js.map +1 -1
- package/dist/src/integrations/task-manager.d.ts +132 -0
- package/dist/src/integrations/task-manager.d.ts.map +1 -0
- package/dist/src/integrations/task-manager.js +309 -0
- package/dist/src/integrations/task-manager.js.map +1 -0
- package/dist/src/modes/tui.d.ts.map +1 -1
- package/dist/src/modes/tui.js +9 -0
- package/dist/src/modes/tui.js.map +1 -1
- package/dist/src/modes.d.ts +30 -0
- package/dist/src/modes.d.ts.map +1 -1
- package/dist/src/modes.js +130 -2
- package/dist/src/modes.js.map +1 -1
- package/dist/src/providers/adapters/openai.d.ts +46 -2
- package/dist/src/providers/adapters/openai.d.ts.map +1 -1
- package/dist/src/providers/adapters/openai.js +221 -21
- package/dist/src/providers/adapters/openai.js.map +1 -1
- package/dist/src/providers/llm-resilience.d.ts +8 -0
- package/dist/src/providers/llm-resilience.d.ts.map +1 -1
- package/dist/src/providers/llm-resilience.js +36 -0
- package/dist/src/providers/llm-resilience.js.map +1 -1
- package/dist/src/tools/agent.d.ts +38 -1
- package/dist/src/tools/agent.d.ts.map +1 -1
- package/dist/src/tools/agent.js +152 -2
- package/dist/src/tools/agent.js.map +1 -1
- package/dist/src/tools/file.d.ts.map +1 -1
- package/dist/src/tools/file.js +17 -3
- package/dist/src/tools/file.js.map +1 -1
- package/dist/src/tools/tasks.d.ts +32 -0
- package/dist/src/tools/tasks.d.ts.map +1 -0
- package/dist/src/tools/tasks.js +334 -0
- package/dist/src/tools/tasks.js.map +1 -0
- package/dist/src/tracing/trace-collector.d.ts +81 -0
- package/dist/src/tracing/trace-collector.d.ts.map +1 -1
- package/dist/src/tracing/trace-collector.js +216 -4
- package/dist/src/tracing/trace-collector.js.map +1 -1
- package/dist/src/tracing/types.d.ts +8 -0
- package/dist/src/tracing/types.d.ts.map +1 -1
- package/dist/src/tracing/types.js.map +1 -1
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +503 -90
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/components/ActiveAgentsPanel.d.ts +45 -0
- package/dist/src/tui/components/ActiveAgentsPanel.d.ts.map +1 -0
- package/dist/src/tui/components/ActiveAgentsPanel.js +121 -0
- package/dist/src/tui/components/ActiveAgentsPanel.js.map +1 -0
- package/dist/src/tui/components/CollapsibleDiffView.d.ts +49 -0
- package/dist/src/tui/components/CollapsibleDiffView.d.ts.map +1 -0
- package/dist/src/tui/components/CollapsibleDiffView.js +302 -0
- package/dist/src/tui/components/CollapsibleDiffView.js.map +1 -0
- package/dist/src/tui/components/DiffView.d.ts +55 -0
- package/dist/src/tui/components/DiffView.d.ts.map +1 -0
- package/dist/src/tui/components/DiffView.js +356 -0
- package/dist/src/tui/components/DiffView.js.map +1 -0
- package/dist/src/tui/components/ErrorBoundary.d.ts +63 -0
- package/dist/src/tui/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/src/tui/components/ErrorBoundary.js +88 -0
- package/dist/src/tui/components/ErrorBoundary.js.map +1 -0
- package/dist/src/tui/components/FileChangeSummary.d.ts +48 -0
- package/dist/src/tui/components/FileChangeSummary.d.ts.map +1 -0
- package/dist/src/tui/components/FileChangeSummary.js +152 -0
- package/dist/src/tui/components/FileChangeSummary.js.map +1 -0
- package/dist/src/tui/components/SideBySideDiff.d.ts +62 -0
- package/dist/src/tui/components/SideBySideDiff.d.ts.map +1 -0
- package/dist/src/tui/components/SideBySideDiff.js +320 -0
- package/dist/src/tui/components/SideBySideDiff.js.map +1 -0
- package/dist/src/tui/components/SyntaxText.d.ts +47 -0
- package/dist/src/tui/components/SyntaxText.d.ts.map +1 -0
- package/dist/src/tui/components/SyntaxText.js +43 -0
- package/dist/src/tui/components/SyntaxText.js.map +1 -0
- package/dist/src/tui/components/TasksPanel.d.ts +25 -0
- package/dist/src/tui/components/TasksPanel.d.ts.map +1 -0
- package/dist/src/tui/components/TasksPanel.js +101 -0
- package/dist/src/tui/components/TasksPanel.js.map +1 -0
- package/dist/src/tui/components/index.d.ts +8 -0
- package/dist/src/tui/components/index.d.ts.map +1 -1
- package/dist/src/tui/components/index.js +13 -0
- package/dist/src/tui/components/index.js.map +1 -1
- package/dist/src/tui/hooks/index.d.ts +7 -0
- package/dist/src/tui/hooks/index.d.ts.map +1 -0
- package/dist/src/tui/hooks/index.js +7 -0
- package/dist/src/tui/hooks/index.js.map +1 -0
- package/dist/src/tui/hooks/useMessagePruning.d.ts +114 -0
- package/dist/src/tui/hooks/useMessagePruning.d.ts.map +1 -0
- package/dist/src/tui/hooks/useMessagePruning.js +127 -0
- package/dist/src/tui/hooks/useMessagePruning.js.map +1 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.d.ts.map +1 -1
- package/dist/src/tui/index.js +9 -0
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/syntax/index.d.ts +12 -0
- package/dist/src/tui/syntax/index.d.ts.map +1 -0
- package/dist/src/tui/syntax/index.js +14 -0
- package/dist/src/tui/syntax/index.js.map +1 -0
- package/dist/src/tui/syntax/languages/bash.d.ts +8 -0
- package/dist/src/tui/syntax/languages/bash.d.ts.map +1 -0
- package/dist/src/tui/syntax/languages/bash.js +296 -0
- package/dist/src/tui/syntax/languages/bash.js.map +1 -0
- package/dist/src/tui/syntax/languages/javascript.d.ts +8 -0
- package/dist/src/tui/syntax/languages/javascript.d.ts.map +1 -0
- package/dist/src/tui/syntax/languages/javascript.js +253 -0
- package/dist/src/tui/syntax/languages/javascript.js.map +1 -0
- package/dist/src/tui/syntax/languages/json.d.ts +8 -0
- package/dist/src/tui/syntax/languages/json.d.ts.map +1 -0
- package/dist/src/tui/syntax/languages/json.js +112 -0
- package/dist/src/tui/syntax/languages/json.js.map +1 -0
- package/dist/src/tui/syntax/languages/python.d.ts +8 -0
- package/dist/src/tui/syntax/languages/python.d.ts.map +1 -0
- package/dist/src/tui/syntax/languages/python.js +232 -0
- package/dist/src/tui/syntax/languages/python.js.map +1 -0
- package/dist/src/tui/syntax/lexer.d.ts +51 -0
- package/dist/src/tui/syntax/lexer.d.ts.map +1 -0
- package/dist/src/tui/syntax/lexer.js +131 -0
- package/dist/src/tui/syntax/lexer.js.map +1 -0
- package/dist/src/tui/utils/index.d.ts +7 -0
- package/dist/src/tui/utils/index.d.ts.map +1 -0
- package/dist/src/tui/utils/index.js +7 -0
- package/dist/src/tui/utils/index.js.map +1 -0
- package/dist/src/tui/utils/keyboard.d.ts +123 -0
- package/dist/src/tui/utils/keyboard.d.ts.map +1 -0
- package/dist/src/tui/utils/keyboard.js +185 -0
- package/dist/src/tui/utils/keyboard.js.map +1 -0
- package/dist/src/types.d.ts +73 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +4 -1
package/dist/src/agent.js
CHANGED
|
@@ -18,17 +18,19 @@
|
|
|
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, } 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
|
|
28
28
|
import { modelRegistry } from './costs/index.js';
|
|
29
29
|
import { getModelContextLength } from './integrations/openrouter-pricing.js';
|
|
30
|
-
// Spawn agent
|
|
31
|
-
import { createBoundSpawnAgentTool } from './tools/agent.js';
|
|
30
|
+
// Spawn agent tools for LLM-driven subagent delegation
|
|
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
|
// =============================================================================
|
|
@@ -71,6 +73,17 @@ export class ProductionAgent {
|
|
|
71
73
|
fileChangeTracker = null;
|
|
72
74
|
capabilitiesRegistry = null;
|
|
73
75
|
toolResolver = null;
|
|
76
|
+
blackboard = null;
|
|
77
|
+
taskManager = null;
|
|
78
|
+
// Duplicate spawn prevention - tracks recently spawned tasks to prevent doom loops
|
|
79
|
+
// Map<taskKey, { timestamp: number; result: string; queuedChanges: number }>
|
|
80
|
+
spawnedTasks = new Map();
|
|
81
|
+
static SPAWN_DEDUP_WINDOW_MS = 60000; // 60 seconds
|
|
82
|
+
// Parent iteration tracking for total budget calculation
|
|
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;
|
|
74
87
|
// Initialization tracking
|
|
75
88
|
initPromises = [];
|
|
76
89
|
initComplete = false;
|
|
@@ -106,6 +119,18 @@ export class ProductionAgent {
|
|
|
106
119
|
this.modeManager = createModeManager(this.config.tools);
|
|
107
120
|
// Initialize pending plan manager for plan mode write interception
|
|
108
121
|
this.pendingPlanManager = createPendingPlanManager();
|
|
122
|
+
// Shared Blackboard - enables coordination between parallel subagents
|
|
123
|
+
// Subagents inherit parent's blackboard; parent agents create their own
|
|
124
|
+
if (userConfig.blackboard) {
|
|
125
|
+
this.blackboard = userConfig.blackboard;
|
|
126
|
+
}
|
|
127
|
+
else if (this.config.subagent !== false) {
|
|
128
|
+
this.blackboard = createSharedBlackboard({
|
|
129
|
+
maxFindings: 500,
|
|
130
|
+
defaultClaimTTL: 120000, // 2 minutes for file claims
|
|
131
|
+
deduplicateFindings: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
109
134
|
// Initialize enabled features
|
|
110
135
|
this.initializeFeatures();
|
|
111
136
|
}
|
|
@@ -215,8 +240,25 @@ export class ProductionAgent {
|
|
|
215
240
|
console.warn('[ProductionAgent] Failed to load user agents:', err);
|
|
216
241
|
}));
|
|
217
242
|
// Register spawn_agent tool so LLM can delegate to subagents
|
|
218
|
-
const boundSpawnTool = createBoundSpawnAgentTool((name, task) => this.spawnAgent(name, task));
|
|
243
|
+
const boundSpawnTool = createBoundSpawnAgentTool((name, task, constraints) => this.spawnAgent(name, task, constraints));
|
|
219
244
|
this.tools.set(boundSpawnTool.name, boundSpawnTool);
|
|
245
|
+
// Register spawn_agents_parallel tool for parallel subagent execution
|
|
246
|
+
const boundParallelSpawnTool = createBoundSpawnAgentsParallelTool((tasks) => this.spawnAgentsParallel(tasks));
|
|
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
|
+
}
|
|
220
262
|
// Cancellation Support
|
|
221
263
|
if (isFeatureEnabled(this.config.cancellation)) {
|
|
222
264
|
this.cancellation = createCancellationManager();
|
|
@@ -313,6 +355,11 @@ export class ProductionAgent {
|
|
|
313
355
|
cacheResults: true,
|
|
314
356
|
cacheTTL: 5 * 60 * 1000, // 5 minutes
|
|
315
357
|
});
|
|
358
|
+
// Connect LSP manager to codebase context for enhanced code selection
|
|
359
|
+
// This enables LSP-based relevance boosting (Phase 4.1)
|
|
360
|
+
if (this.lspManager) {
|
|
361
|
+
this.codebaseContext.setLSPManager(this.lspManager);
|
|
362
|
+
}
|
|
316
363
|
}
|
|
317
364
|
// Forward context engineering events
|
|
318
365
|
this.contextEngineering.on(event => {
|
|
@@ -792,9 +839,13 @@ export class ProductionAgent {
|
|
|
792
839
|
this.observability?.logger?.warn('Plan task failed', { taskId: currentTask.id });
|
|
793
840
|
// Continue with other tasks if possible
|
|
794
841
|
}
|
|
795
|
-
// Check iteration limit
|
|
796
|
-
if (this.
|
|
797
|
-
this.observability?.logger?.warn('Max iterations reached'
|
|
842
|
+
// Check iteration limit (using total iterations to account for parent)
|
|
843
|
+
if (this.getTotalIterations() >= this.config.maxIterations) {
|
|
844
|
+
this.observability?.logger?.warn('Max iterations reached', {
|
|
845
|
+
iteration: this.state.iteration,
|
|
846
|
+
parentIterations: this.parentIterations,
|
|
847
|
+
total: this.getTotalIterations(),
|
|
848
|
+
});
|
|
798
849
|
break;
|
|
799
850
|
}
|
|
800
851
|
}
|
|
@@ -832,10 +883,16 @@ export class ProductionAgent {
|
|
|
832
883
|
});
|
|
833
884
|
// =======================================================================
|
|
834
885
|
// CANCELLATION CHECK
|
|
886
|
+
// Checks both internal cancellation (ESC key) and external cancellation
|
|
887
|
+
// (parent timeout when this agent is a subagent)
|
|
835
888
|
// =======================================================================
|
|
836
889
|
if (this.cancellation?.isCancelled) {
|
|
837
890
|
this.cancellation.token.throwIfCancellationRequested();
|
|
838
891
|
}
|
|
892
|
+
// Also check external cancellation token (from parent when spawned as subagent)
|
|
893
|
+
if (this.externalCancellationToken?.isCancellationRequested) {
|
|
894
|
+
this.externalCancellationToken.throwIfCancellationRequested();
|
|
895
|
+
}
|
|
839
896
|
// =======================================================================
|
|
840
897
|
// RESOURCE CHECK - system resource limits
|
|
841
898
|
// =======================================================================
|
|
@@ -945,7 +1002,11 @@ export class ProductionAgent {
|
|
|
945
1002
|
});
|
|
946
1003
|
// Emit appropriate event
|
|
947
1004
|
if (budgetCheck.budgetType === 'iterations') {
|
|
948
|
-
|
|
1005
|
+
const totalIter = this.getTotalIterations();
|
|
1006
|
+
const iterMsg = this.parentIterations > 0
|
|
1007
|
+
? `${this.state.iteration} + ${this.parentIterations} parent = ${totalIter}`
|
|
1008
|
+
: `${this.state.iteration}`;
|
|
1009
|
+
this.emit({ type: 'error', error: `Max iterations reached (${iterMsg})` });
|
|
949
1010
|
}
|
|
950
1011
|
else {
|
|
951
1012
|
this.emit({ type: 'error', error: budgetCheck.reason || 'Budget exceeded' });
|
|
@@ -963,8 +1024,13 @@ export class ProductionAgent {
|
|
|
963
1024
|
}
|
|
964
1025
|
else {
|
|
965
1026
|
// Fallback to simple iteration check if economics not available
|
|
966
|
-
|
|
967
|
-
|
|
1027
|
+
// Use getTotalIterations() to account for parent iterations (subagent hierarchy)
|
|
1028
|
+
if (this.getTotalIterations() >= this.config.maxIterations) {
|
|
1029
|
+
this.observability?.logger?.warn('Max iterations reached', {
|
|
1030
|
+
iteration: this.state.iteration,
|
|
1031
|
+
parentIterations: this.parentIterations,
|
|
1032
|
+
total: this.getTotalIterations(),
|
|
1033
|
+
});
|
|
968
1034
|
break;
|
|
969
1035
|
}
|
|
970
1036
|
}
|
|
@@ -1167,6 +1233,15 @@ export class ProductionAgent {
|
|
|
1167
1233
|
messages.push(assistantMessage);
|
|
1168
1234
|
this.state.messages.push(assistantMessage);
|
|
1169
1235
|
lastResponse = response.content;
|
|
1236
|
+
// In plan mode: capture exploration findings as we go (not just at the end)
|
|
1237
|
+
// This ensures we collect context from exploration iterations before writes are queued
|
|
1238
|
+
if (this.modeManager.getMode() === 'plan' && response.content && response.content.length > 50) {
|
|
1239
|
+
const hasReadOnlyTools = response.toolCalls?.every(tc => ['read_file', 'list_files', 'glob', 'grep', 'search', 'mcp_'].some(prefix => tc.name.startsWith(prefix) || tc.name === prefix));
|
|
1240
|
+
// Capture substantive exploration content (not just "let me read..." responses)
|
|
1241
|
+
if (hasReadOnlyTools && !response.content.match(/^(Let me|I'll|I will|I need to|First,)/i)) {
|
|
1242
|
+
this.pendingPlanManager.appendExplorationFinding(response.content.slice(0, 1000));
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1170
1245
|
// Check for tool calls
|
|
1171
1246
|
// When forceTextOnly is set (max iterations reached), ignore any tool calls
|
|
1172
1247
|
const hasToolCalls = response.toolCalls && response.toolCalls.length > 0;
|
|
@@ -1182,6 +1257,14 @@ export class ProductionAgent {
|
|
|
1182
1257
|
// The model has "consumed" the tool outputs and produced a response,
|
|
1183
1258
|
// so we can replace verbose outputs with compact summaries
|
|
1184
1259
|
this.compactToolOutputs();
|
|
1260
|
+
// In plan mode: capture exploration summary from the final response
|
|
1261
|
+
// This provides context for what was learned during exploration before proposing changes
|
|
1262
|
+
if (this.modeManager.getMode() === 'plan' && this.pendingPlanManager.hasPendingPlan()) {
|
|
1263
|
+
const explorationContent = response.content || '';
|
|
1264
|
+
if (explorationContent.length > 0) {
|
|
1265
|
+
this.pendingPlanManager.setExplorationSummary(explorationContent);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1185
1268
|
// Final validation: warn if response is still empty after all retries
|
|
1186
1269
|
if (!response.content || response.content.length === 0) {
|
|
1187
1270
|
this.observability?.logger?.error('Agent finished with empty response after all retries', {
|
|
@@ -1683,11 +1766,8 @@ export class ProductionAgent {
|
|
|
1683
1766
|
// =====================================================================
|
|
1684
1767
|
// In plan mode, intercept write operations and queue them as proposed changes
|
|
1685
1768
|
if (this.modeManager.shouldInterceptTool(toolCall.name, toolCall.arguments)) {
|
|
1686
|
-
// Extract
|
|
1687
|
-
const
|
|
1688
|
-
const reason = typeof lastAssistantMsg?.content === 'string'
|
|
1689
|
-
? lastAssistantMsg.content.slice(0, 200)
|
|
1690
|
-
: `Proposed change: ${toolCall.name}`;
|
|
1769
|
+
// Extract contextual reasoning instead of simple truncation
|
|
1770
|
+
const reason = this.extractChangeReasoning(toolCall, this.state.messages);
|
|
1691
1771
|
// Start a new plan if needed
|
|
1692
1772
|
if (!this.pendingPlanManager.hasPendingPlan()) {
|
|
1693
1773
|
const lastUserMsg = [...this.state.messages].reverse().find(m => m.role === 'user');
|
|
@@ -1823,10 +1903,45 @@ export class ProductionAgent {
|
|
|
1823
1903
|
if (process.env.DEBUG && toolCall.name.startsWith('mcp_') && wasPreloaded) {
|
|
1824
1904
|
console.log(` ✓ Using pre-loaded MCP tool: ${toolCall.name}`);
|
|
1825
1905
|
}
|
|
1906
|
+
// =====================================================================
|
|
1907
|
+
// BLACKBOARD FILE COORDINATION (Parallel Subagent Support)
|
|
1908
|
+
// =====================================================================
|
|
1909
|
+
// Claim file resources before write operations to prevent conflicts
|
|
1910
|
+
if (this.blackboard && (toolCall.name === 'write_file' || toolCall.name === 'edit_file')) {
|
|
1911
|
+
const args = toolCall.arguments;
|
|
1912
|
+
const filePath = String(args.path || args.file_path || '');
|
|
1913
|
+
if (filePath) {
|
|
1914
|
+
const agentId = this.config.systemPrompt?.slice(0, 50) || 'agent';
|
|
1915
|
+
const claimed = this.blackboard.claim(filePath, agentId, 'write', {
|
|
1916
|
+
ttl: 60000, // 1 minute claim
|
|
1917
|
+
intent: `${toolCall.name}: ${filePath}`,
|
|
1918
|
+
});
|
|
1919
|
+
if (!claimed) {
|
|
1920
|
+
const existingClaim = this.blackboard.getClaim(filePath);
|
|
1921
|
+
throw new Error(`File "${filePath}" is being edited by another agent (${existingClaim?.agentId || 'unknown'}). ` +
|
|
1922
|
+
`Wait for the other agent to complete or choose a different file.`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1826
1926
|
// Execute tool (with sandbox if available)
|
|
1827
1927
|
let result;
|
|
1828
1928
|
if (this.safety?.sandbox) {
|
|
1829
|
-
|
|
1929
|
+
// CRITICAL: spawn_agent and spawn_agents_parallel need MUCH longer timeouts
|
|
1930
|
+
// The default 60s sandbox timeout would kill subagents prematurely
|
|
1931
|
+
// Subagents may run for minutes (per their own timeout config)
|
|
1932
|
+
const isSpawnAgent = toolCall.name === 'spawn_agent';
|
|
1933
|
+
const isSpawnParallel = toolCall.name === 'spawn_agents_parallel';
|
|
1934
|
+
const isSubagentTool = isSpawnAgent || isSpawnParallel;
|
|
1935
|
+
const subagentConfig = this.config.subagent;
|
|
1936
|
+
const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
|
|
1937
|
+
const subagentTimeout = hasSubagentConfig
|
|
1938
|
+
? subagentConfig.defaultTimeout ?? 600000 // 10 min default
|
|
1939
|
+
: 600000;
|
|
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;
|
|
1944
|
+
result = await this.safety.sandbox.executeWithLimits(() => tool.execute(toolCall.arguments), toolTimeout);
|
|
1830
1945
|
}
|
|
1831
1946
|
else {
|
|
1832
1947
|
result = await tool.execute(toolCall.arguments);
|
|
@@ -1859,6 +1974,15 @@ export class ProductionAgent {
|
|
|
1859
1974
|
callId: toolCall.id,
|
|
1860
1975
|
result,
|
|
1861
1976
|
});
|
|
1977
|
+
// Release blackboard claim after successful file write
|
|
1978
|
+
if (this.blackboard && (toolCall.name === 'write_file' || toolCall.name === 'edit_file')) {
|
|
1979
|
+
const args = toolCall.arguments;
|
|
1980
|
+
const filePath = String(args.path || args.file_path || '');
|
|
1981
|
+
if (filePath) {
|
|
1982
|
+
const agentId = this.config.systemPrompt?.slice(0, 50) || 'agent';
|
|
1983
|
+
this.blackboard.release(filePath, agentId);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1862
1986
|
this.observability?.tracer?.endSpan(spanId);
|
|
1863
1987
|
}
|
|
1864
1988
|
catch (err) {
|
|
@@ -1895,8 +2019,32 @@ export class ProductionAgent {
|
|
|
1895
2019
|
}
|
|
1896
2020
|
return results;
|
|
1897
2021
|
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Get recently modified file paths from the file change tracker.
|
|
2024
|
+
* Returns paths of files modified in this session (not undone).
|
|
2025
|
+
*/
|
|
2026
|
+
getRecentlyModifiedFiles(limit = 5) {
|
|
2027
|
+
if (!this.fileChangeTracker)
|
|
2028
|
+
return [];
|
|
2029
|
+
try {
|
|
2030
|
+
const changes = this.fileChangeTracker.getChanges();
|
|
2031
|
+
const recentFiles = new Set();
|
|
2032
|
+
// Iterate in reverse to get most recent first
|
|
2033
|
+
for (let i = changes.length - 1; i >= 0 && recentFiles.size < limit; i--) {
|
|
2034
|
+
const change = changes[i];
|
|
2035
|
+
if (!change.isUndone) {
|
|
2036
|
+
recentFiles.add(change.filePath);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return Array.from(recentFiles);
|
|
2040
|
+
}
|
|
2041
|
+
catch {
|
|
2042
|
+
return [];
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
1898
2045
|
/**
|
|
1899
2046
|
* Select relevant code synchronously using cached repo analysis.
|
|
2047
|
+
* Uses LSP-enhanced selection when available to boost related files.
|
|
1900
2048
|
* Returns empty result if analysis hasn't been run yet.
|
|
1901
2049
|
*/
|
|
1902
2050
|
selectRelevantCodeSync(task, maxTokens) {
|
|
@@ -1907,6 +2055,30 @@ export class ProductionAgent {
|
|
|
1907
2055
|
if (!repoMap) {
|
|
1908
2056
|
return { chunks: [], totalTokens: 0 };
|
|
1909
2057
|
}
|
|
2058
|
+
// Get recently modified files for LSP-enhanced selection
|
|
2059
|
+
const recentFiles = this.getRecentlyModifiedFiles();
|
|
2060
|
+
const priorityFileSet = new Set(recentFiles);
|
|
2061
|
+
// LSP-related files (files that reference or are referenced by recent files)
|
|
2062
|
+
const lspRelatedFiles = new Set();
|
|
2063
|
+
if (this.codebaseContext.hasActiveLSP() && recentFiles.length > 0) {
|
|
2064
|
+
// Use dependency graph as a synchronous proxy for LSP relationships
|
|
2065
|
+
for (const file of recentFiles) {
|
|
2066
|
+
// Files that this file depends on
|
|
2067
|
+
const deps = repoMap.dependencyGraph.get(file);
|
|
2068
|
+
if (deps) {
|
|
2069
|
+
for (const dep of deps) {
|
|
2070
|
+
lspRelatedFiles.add(dep);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
// Files that depend on this file
|
|
2074
|
+
const reverseDeps = repoMap.reverseDependencyGraph.get(file);
|
|
2075
|
+
if (reverseDeps) {
|
|
2076
|
+
for (const dep of reverseDeps) {
|
|
2077
|
+
lspRelatedFiles.add(dep);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
1910
2082
|
// Get all chunks and score by relevance
|
|
1911
2083
|
const allChunks = Array.from(repoMap.chunks.values());
|
|
1912
2084
|
const taskLower = task.toLowerCase();
|
|
@@ -1930,13 +2102,22 @@ export class ProductionAgent {
|
|
|
1930
2102
|
}
|
|
1931
2103
|
}
|
|
1932
2104
|
// Combine with base importance
|
|
1933
|
-
|
|
2105
|
+
let combinedScore = chunk.importance * 0.4 + Math.min(relevance, 1) * 0.6;
|
|
2106
|
+
// Boost recently modified files (highest priority)
|
|
2107
|
+
if (priorityFileSet.has(chunk.filePath)) {
|
|
2108
|
+
combinedScore = Math.min(1.0, combinedScore + 0.4);
|
|
2109
|
+
}
|
|
2110
|
+
// Boost LSP-related files (files connected to recent edits)
|
|
2111
|
+
else if (lspRelatedFiles.has(chunk.filePath)) {
|
|
2112
|
+
combinedScore = Math.min(1.0, combinedScore + 0.25);
|
|
2113
|
+
}
|
|
1934
2114
|
return { chunk, score: combinedScore };
|
|
1935
2115
|
});
|
|
1936
2116
|
// Sort by score and select within budget
|
|
1937
2117
|
scored.sort((a, b) => b.score - a.score);
|
|
1938
2118
|
const selected = [];
|
|
1939
2119
|
let totalTokens = 0;
|
|
2120
|
+
const boostedFiles = [];
|
|
1940
2121
|
for (const { chunk, score } of scored) {
|
|
1941
2122
|
if (score < 0.1)
|
|
1942
2123
|
continue; // Skip very low relevance
|
|
@@ -1949,8 +2130,16 @@ export class ProductionAgent {
|
|
|
1949
2130
|
importance: score,
|
|
1950
2131
|
});
|
|
1951
2132
|
totalTokens += chunk.tokenCount;
|
|
2133
|
+
// Track which files were boosted by LSP/dependency relationships
|
|
2134
|
+
if (lspRelatedFiles.has(chunk.filePath)) {
|
|
2135
|
+
boostedFiles.push(chunk.filePath);
|
|
2136
|
+
}
|
|
1952
2137
|
}
|
|
1953
|
-
return {
|
|
2138
|
+
return {
|
|
2139
|
+
chunks: selected,
|
|
2140
|
+
totalTokens,
|
|
2141
|
+
lspBoostedFiles: boostedFiles.length > 0 ? boostedFiles : undefined,
|
|
2142
|
+
};
|
|
1954
2143
|
}
|
|
1955
2144
|
/**
|
|
1956
2145
|
* Analyze the codebase (async). Call this once at startup for optimal performance.
|
|
@@ -2032,6 +2221,57 @@ export class ProductionAgent {
|
|
|
2032
2221
|
// Generic
|
|
2033
2222
|
return `Args: ${JSON.stringify(args).slice(0, 100)}...`;
|
|
2034
2223
|
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Extract contextual reasoning for a proposed change in plan mode.
|
|
2226
|
+
* Looks at recent assistant messages to find relevant explanation.
|
|
2227
|
+
* Returns a more complete reason than simple truncation.
|
|
2228
|
+
*/
|
|
2229
|
+
extractChangeReasoning(toolCall, messages) {
|
|
2230
|
+
// Get last few assistant messages (most recent first)
|
|
2231
|
+
const assistantMsgs = messages
|
|
2232
|
+
.filter(m => m.role === 'assistant' && typeof m.content === 'string')
|
|
2233
|
+
.slice(-3)
|
|
2234
|
+
.reverse();
|
|
2235
|
+
if (assistantMsgs.length === 0) {
|
|
2236
|
+
return `Proposed change: ${toolCall.name}`;
|
|
2237
|
+
}
|
|
2238
|
+
// Use the most recent assistant message
|
|
2239
|
+
const lastMsg = assistantMsgs[0];
|
|
2240
|
+
const content = lastMsg.content;
|
|
2241
|
+
// For spawn_agent, the task itself is usually the reason
|
|
2242
|
+
if (toolCall.name === 'spawn_agent') {
|
|
2243
|
+
const args = toolCall.arguments;
|
|
2244
|
+
const task = String(args.task || args.prompt || args.goal || '');
|
|
2245
|
+
if (task.length > 0) {
|
|
2246
|
+
// Use first paragraph or 500 chars of task as reason
|
|
2247
|
+
const firstPara = task.split(/\n\n/)[0];
|
|
2248
|
+
return firstPara.length > 500 ? firstPara.slice(0, 500) + '...' : firstPara;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
// For file operations, look for context about the file
|
|
2252
|
+
if (['write_file', 'edit_file'].includes(toolCall.name)) {
|
|
2253
|
+
const args = toolCall.arguments;
|
|
2254
|
+
const path = String(args.path || args.file_path || '');
|
|
2255
|
+
// Look for mentions of this file in the assistant's explanation
|
|
2256
|
+
if (path && content.toLowerCase().includes(path.toLowerCase().split('/').pop() || '')) {
|
|
2257
|
+
// Extract the sentence(s) mentioning this file
|
|
2258
|
+
const sentences = content.split(/[.!?\n]+/).filter(s => s.toLowerCase().includes(path.toLowerCase().split('/').pop() || ''));
|
|
2259
|
+
if (sentences.length > 0) {
|
|
2260
|
+
const relevant = sentences.slice(0, 2).join('. ').trim();
|
|
2261
|
+
return relevant.length > 500 ? relevant.slice(0, 500) + '...' : relevant;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
// Fallback: use first 500 chars instead of 200
|
|
2266
|
+
// Look for the first meaningful paragraph/section
|
|
2267
|
+
const paragraphs = content.split(/\n\n+/).filter(p => p.trim().length > 20);
|
|
2268
|
+
if (paragraphs.length > 0) {
|
|
2269
|
+
const firstPara = paragraphs[0].trim();
|
|
2270
|
+
return firstPara.length > 500 ? firstPara.slice(0, 500) + '...' : firstPara;
|
|
2271
|
+
}
|
|
2272
|
+
// Ultimate fallback
|
|
2273
|
+
return content.length > 500 ? content.slice(0, 500) + '...' : content;
|
|
2274
|
+
}
|
|
2035
2275
|
/**
|
|
2036
2276
|
* Update memory statistics.
|
|
2037
2277
|
* Memory stats are retrieved via memory manager, not stored in state.
|
|
@@ -2084,6 +2324,13 @@ export class ProductionAgent {
|
|
|
2084
2324
|
getTraceCollector() {
|
|
2085
2325
|
return this.traceCollector;
|
|
2086
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
|
+
}
|
|
2087
2334
|
/**
|
|
2088
2335
|
* Get the learning store for cross-session learning.
|
|
2089
2336
|
* Returns null if learning store is not enabled.
|
|
@@ -2603,6 +2850,11 @@ export class ProductionAgent {
|
|
|
2603
2850
|
if (!this.threadManager) {
|
|
2604
2851
|
throw new Error('Thread management not enabled. Enable it in config to use createCheckpoint()');
|
|
2605
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];
|
|
2606
2858
|
const checkpoint = this.threadManager.createCheckpoint({
|
|
2607
2859
|
label,
|
|
2608
2860
|
agentState: this.state,
|
|
@@ -2777,8 +3029,12 @@ export class ProductionAgent {
|
|
|
2777
3029
|
/**
|
|
2778
3030
|
* Spawn an agent to execute a task.
|
|
2779
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
|
|
2780
3036
|
*/
|
|
2781
|
-
async spawnAgent(agentName, task) {
|
|
3037
|
+
async spawnAgent(agentName, task, constraints) {
|
|
2782
3038
|
if (!this.agentRegistry) {
|
|
2783
3039
|
return {
|
|
2784
3040
|
success: false,
|
|
@@ -2794,6 +3050,65 @@ export class ProductionAgent {
|
|
|
2794
3050
|
metrics: { tokens: 0, duration: 0, toolCalls: 0 },
|
|
2795
3051
|
};
|
|
2796
3052
|
}
|
|
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
|
|
3056
|
+
const taskKey = `${agentName}:${task.slice(0, 150).toLowerCase().replace(/\s+/g, ' ').trim()}`;
|
|
3057
|
+
const now = Date.now();
|
|
3058
|
+
// Clean up old entries (older than dedup window)
|
|
3059
|
+
for (const [key, entry] of this.spawnedTasks.entries()) {
|
|
3060
|
+
if (now - entry.timestamp > ProductionAgent.SPAWN_DEDUP_WINDOW_MS) {
|
|
3061
|
+
this.spawnedTasks.delete(key);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
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
|
|
3093
|
+
this.observability?.logger?.warn('Duplicate spawn prevented', {
|
|
3094
|
+
agent: agentName,
|
|
3095
|
+
task: task.slice(0, 100),
|
|
3096
|
+
matchType,
|
|
3097
|
+
originalTimestamp: existingMatch.timestamp,
|
|
3098
|
+
elapsedMs: now - existingMatch.timestamp,
|
|
3099
|
+
});
|
|
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` +
|
|
3104
|
+
`These changes are already in your plan - do NOT spawn again.\n`
|
|
3105
|
+
: ''}Previous result summary:\n${existingMatch.result.slice(0, 500)}`;
|
|
3106
|
+
return {
|
|
3107
|
+
success: true, // Mark as success since original task completed
|
|
3108
|
+
output: duplicateMessage,
|
|
3109
|
+
metrics: { tokens: 0, duration: 0, toolCalls: 0 },
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
2797
3112
|
this.emit({ type: 'agent.spawn', agentId: `spawn-${Date.now()}`, name: agentName, task });
|
|
2798
3113
|
this.observability?.logger?.info('Spawning agent', { name: agentName, task });
|
|
2799
3114
|
const startTime = Date.now();
|
|
@@ -2807,16 +3122,88 @@ export class ProductionAgent {
|
|
|
2807
3122
|
const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
|
|
2808
3123
|
? agentDef.model
|
|
2809
3124
|
: this.config.model;
|
|
2810
|
-
// Get subagent config with
|
|
2811
|
-
//
|
|
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)
|
|
2812
3127
|
const subagentConfig = this.config.subagent;
|
|
2813
3128
|
const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
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}`;
|
|
2820
3207
|
// Create a sub-agent with the agent's config
|
|
2821
3208
|
const subAgent = new ProductionAgent({
|
|
2822
3209
|
provider: this.provider,
|
|
@@ -2825,7 +3212,7 @@ export class ProductionAgent {
|
|
|
2825
3212
|
toolResolver: this.toolResolver || undefined,
|
|
2826
3213
|
// Pass MCP tool summaries so subagent knows what tools are available
|
|
2827
3214
|
mcpToolSummaries: this.config.mcpToolSummaries,
|
|
2828
|
-
systemPrompt:
|
|
3215
|
+
systemPrompt: subagentSystemPrompt,
|
|
2829
3216
|
model: resolvedModel,
|
|
2830
3217
|
maxIterations: agentDef.maxIterations || defaultMaxIterations,
|
|
2831
3218
|
// Inherit some features but keep subagent simpler
|
|
@@ -2843,7 +3230,31 @@ export class ProductionAgent {
|
|
|
2843
3230
|
builtIn: { logging: false, timing: false, metrics: false },
|
|
2844
3231
|
custom: [],
|
|
2845
3232
|
},
|
|
3233
|
+
// Share parent's blackboard for coordination between parallel subagents
|
|
3234
|
+
blackboard: this.blackboard || undefined,
|
|
2846
3235
|
});
|
|
3236
|
+
// CRITICAL: Subagent inherits parent's mode
|
|
3237
|
+
// This ensures that if parent is in plan mode:
|
|
3238
|
+
// - Subagent's read operations execute immediately (visible exploration)
|
|
3239
|
+
// - Subagent's write operations get queued in the subagent's pending plan
|
|
3240
|
+
// - User maintains control over what actually gets written
|
|
3241
|
+
if (parentMode !== 'build') {
|
|
3242
|
+
subAgent.setMode(parentMode);
|
|
3243
|
+
}
|
|
3244
|
+
// Pass parent's iteration count to subagent for accurate budget tracking
|
|
3245
|
+
// This prevents subagents from consuming excessive iterations when parent already used many
|
|
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
|
+
}
|
|
2847
3258
|
// Forward events from subagent with context
|
|
2848
3259
|
subAgent.subscribe(event => {
|
|
2849
3260
|
// Tag event with subagent source so TUI can display it properly
|
|
@@ -2857,20 +3268,77 @@ export class ProductionAgent {
|
|
|
2857
3268
|
const effectiveSource = parentSource
|
|
2858
3269
|
? createLinkedToken(parentSource, timeoutSource)
|
|
2859
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);
|
|
2860
3275
|
try {
|
|
2861
3276
|
// Run the task with cancellation propagation from parent
|
|
2862
3277
|
const result = await race(subAgent.run(task), effectiveSource.token);
|
|
2863
3278
|
const duration = Date.now() - startTime;
|
|
2864
|
-
|
|
3279
|
+
// BEFORE cleanup - extract subagent's pending plan and merge into parent's plan
|
|
3280
|
+
// This ensures that when a subagent in plan mode queues writes, they bubble up to the parent
|
|
3281
|
+
let queuedChangeSummary = '';
|
|
3282
|
+
let queuedChangesCount = 0;
|
|
3283
|
+
if (subAgent.hasPendingPlan()) {
|
|
3284
|
+
const subPlan = subAgent.getPendingPlan();
|
|
3285
|
+
if (subPlan && subPlan.proposedChanges.length > 0) {
|
|
3286
|
+
queuedChangesCount = subPlan.proposedChanges.length;
|
|
3287
|
+
// Emit event for TUI to display
|
|
3288
|
+
this.emit({
|
|
3289
|
+
type: 'agent.pending_plan',
|
|
3290
|
+
agentId: agentName,
|
|
3291
|
+
changes: subPlan.proposedChanges,
|
|
3292
|
+
});
|
|
3293
|
+
// Build detailed summary of what was queued for the return message
|
|
3294
|
+
// This prevents the "doom loop" where parent doesn't know what subagent did
|
|
3295
|
+
const changeSummaries = subPlan.proposedChanges.map(c => {
|
|
3296
|
+
if (c.tool === 'write_file' || c.tool === 'edit_file') {
|
|
3297
|
+
const path = c.args.path || c.args.file_path || '(unknown file)';
|
|
3298
|
+
return ` - [${c.tool}] ${path}: ${c.reason}`;
|
|
3299
|
+
}
|
|
3300
|
+
else if (c.tool === 'bash') {
|
|
3301
|
+
const cmd = String(c.args.command || '').slice(0, 60);
|
|
3302
|
+
return ` - [bash] ${cmd}${String(c.args.command || '').length > 60 ? '...' : ''}: ${c.reason}`;
|
|
3303
|
+
}
|
|
3304
|
+
return ` - [${c.tool}]: ${c.reason}`;
|
|
3305
|
+
});
|
|
3306
|
+
queuedChangeSummary = `\n\n[PLAN MODE - CHANGES QUEUED TO PARENT]\n` +
|
|
3307
|
+
`The following ${subPlan.proposedChanges.length} change(s) have been queued in the parent's pending plan:\n` +
|
|
3308
|
+
changeSummaries.join('\n') + '\n' +
|
|
3309
|
+
`\nThese changes are now in YOUR pending plan. The task for this subagent is COMPLETE.\n` +
|
|
3310
|
+
`Do NOT spawn another agent for the same task - the changes are already queued.\n` +
|
|
3311
|
+
`Use /show-plan to see all pending changes, /approve to execute them.`;
|
|
3312
|
+
// Merge into parent's pending plan with subagent context
|
|
3313
|
+
for (const change of subPlan.proposedChanges) {
|
|
3314
|
+
this.pendingPlanManager.addProposedChange(change.tool, { ...change.args, _fromSubagent: agentName }, `[${agentName}] ${change.reason}`, change.toolCallId);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
// Also merge exploration summary if available
|
|
3318
|
+
if (subPlan?.explorationSummary) {
|
|
3319
|
+
this.pendingPlanManager.appendExplorationFinding(`[${agentName}] ${subPlan.explorationSummary}`);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
// If subagent queued changes, override output with informative message
|
|
3323
|
+
// This is critical to prevent doom loops where parent doesn't understand what happened
|
|
3324
|
+
const finalOutput = queuedChangeSummary
|
|
3325
|
+
? (result.response || '') + queuedChangeSummary
|
|
3326
|
+
: (result.response || result.error || '');
|
|
3327
|
+
const spawnResultFinal = {
|
|
2865
3328
|
success: result.success,
|
|
2866
|
-
output:
|
|
3329
|
+
output: finalOutput,
|
|
2867
3330
|
metrics: {
|
|
2868
3331
|
tokens: result.metrics.totalTokens,
|
|
2869
3332
|
duration,
|
|
2870
3333
|
toolCalls: result.metrics.toolCalls,
|
|
2871
3334
|
},
|
|
2872
3335
|
};
|
|
2873
|
-
this.emit({
|
|
3336
|
+
this.emit({
|
|
3337
|
+
type: 'agent.complete',
|
|
3338
|
+
agentId: agentName,
|
|
3339
|
+
success: result.success,
|
|
3340
|
+
output: finalOutput.slice(0, 500), // Include output preview
|
|
3341
|
+
});
|
|
2874
3342
|
// Enhanced tracing: Record subagent completion
|
|
2875
3343
|
this.traceCollector?.record({
|
|
2876
3344
|
type: 'subagent.link',
|
|
@@ -2898,7 +3366,14 @@ export class ProductionAgent {
|
|
|
2898
3366
|
},
|
|
2899
3367
|
});
|
|
2900
3368
|
await subAgent.cleanup();
|
|
2901
|
-
|
|
3369
|
+
// Cache result for duplicate spawn prevention
|
|
3370
|
+
// Use the same taskKey from the dedup check above
|
|
3371
|
+
this.spawnedTasks.set(taskKey, {
|
|
3372
|
+
timestamp: Date.now(),
|
|
3373
|
+
result: finalOutput,
|
|
3374
|
+
queuedChanges: queuedChangesCount,
|
|
3375
|
+
});
|
|
3376
|
+
return spawnResultFinal;
|
|
2902
3377
|
}
|
|
2903
3378
|
catch (err) {
|
|
2904
3379
|
// Handle cancellation (user ESC or timeout) for cleaner error messages
|
|
@@ -2909,6 +3384,55 @@ export class ProductionAgent {
|
|
|
2909
3384
|
? 'User cancelled'
|
|
2910
3385
|
: `Timed out after ${subagentTimeout}ms`;
|
|
2911
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
|
+
: '';
|
|
3401
|
+
// Extract pending plan before cleanup (even on cancellation, preserve any queued work)
|
|
3402
|
+
let cancelledQueuedSummary = '';
|
|
3403
|
+
if (subAgent.hasPendingPlan()) {
|
|
3404
|
+
const subPlan = subAgent.getPendingPlan();
|
|
3405
|
+
if (subPlan && subPlan.proposedChanges.length > 0) {
|
|
3406
|
+
this.emit({
|
|
3407
|
+
type: 'agent.pending_plan',
|
|
3408
|
+
agentId: agentName,
|
|
3409
|
+
changes: subPlan.proposedChanges,
|
|
3410
|
+
});
|
|
3411
|
+
// Build summary of changes that were queued before cancellation
|
|
3412
|
+
const changeSummaries = subPlan.proposedChanges.map(c => {
|
|
3413
|
+
if (c.tool === 'write_file' || c.tool === 'edit_file') {
|
|
3414
|
+
const path = c.args.path || c.args.file_path || '(unknown file)';
|
|
3415
|
+
return ` - [${c.tool}] ${path}: ${c.reason}`;
|
|
3416
|
+
}
|
|
3417
|
+
else if (c.tool === 'bash') {
|
|
3418
|
+
const cmd = String(c.args.command || '').slice(0, 60);
|
|
3419
|
+
return ` - [bash] ${cmd}...: ${c.reason}`;
|
|
3420
|
+
}
|
|
3421
|
+
return ` - [${c.tool}]: ${c.reason}`;
|
|
3422
|
+
});
|
|
3423
|
+
cancelledQueuedSummary = `\n\n[PLAN MODE - CHANGES QUEUED BEFORE CANCELLATION]\n` +
|
|
3424
|
+
`${subPlan.proposedChanges.length} change(s) were queued to the parent plan:\n` +
|
|
3425
|
+
changeSummaries.join('\n') + '\n' +
|
|
3426
|
+
`These changes are preserved in your pending plan.`;
|
|
3427
|
+
for (const change of subPlan.proposedChanges) {
|
|
3428
|
+
this.pendingPlanManager.addProposedChange(change.tool, { ...change.args, _fromSubagent: agentName }, `[${agentName}] ${change.reason}`, change.toolCallId);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
// Also preserve exploration summary
|
|
3432
|
+
if (subPlan?.explorationSummary) {
|
|
3433
|
+
this.pendingPlanManager.appendExplorationFinding(`[${agentName}] ${subPlan.explorationSummary}`);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
2912
3436
|
// Try to cleanup the subagent gracefully
|
|
2913
3437
|
try {
|
|
2914
3438
|
await subAgent.cleanup();
|
|
@@ -2916,13 +3440,50 @@ export class ProductionAgent {
|
|
|
2916
3440
|
catch {
|
|
2917
3441
|
// Ignore cleanup errors on cancellation
|
|
2918
3442
|
}
|
|
2919
|
-
|
|
3443
|
+
// Build output message with partial results
|
|
3444
|
+
const baseOutput = isUserCancellation
|
|
2920
3445
|
? `Subagent '${agentName}' was cancelled by user.`
|
|
2921
|
-
: `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s
|
|
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
|
+
});
|
|
2922
3477
|
return {
|
|
2923
3478
|
success: false,
|
|
2924
|
-
output,
|
|
2925
|
-
|
|
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
|
+
},
|
|
2926
3487
|
};
|
|
2927
3488
|
}
|
|
2928
3489
|
throw err; // Re-throw non-cancellation errors
|
|
@@ -2943,6 +3504,33 @@ export class ProductionAgent {
|
|
|
2943
3504
|
};
|
|
2944
3505
|
}
|
|
2945
3506
|
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Spawn multiple agents in parallel to work on independent tasks.
|
|
3509
|
+
* Uses the shared blackboard for coordination and conflict prevention.
|
|
3510
|
+
*/
|
|
3511
|
+
async spawnAgentsParallel(tasks) {
|
|
3512
|
+
// Emit start event for TUI visibility
|
|
3513
|
+
this.emit({
|
|
3514
|
+
type: 'parallel.spawn.start',
|
|
3515
|
+
count: tasks.length,
|
|
3516
|
+
agents: tasks.map(t => t.agent),
|
|
3517
|
+
});
|
|
3518
|
+
// Execute all tasks in parallel
|
|
3519
|
+
const promises = tasks.map(({ agent, task }) => this.spawnAgent(agent, task));
|
|
3520
|
+
const results = await Promise.all(promises);
|
|
3521
|
+
// Emit completion event
|
|
3522
|
+
this.emit({
|
|
3523
|
+
type: 'parallel.spawn.complete',
|
|
3524
|
+
count: tasks.length,
|
|
3525
|
+
successCount: results.filter(r => r.success).length,
|
|
3526
|
+
results: results.map((r, i) => ({
|
|
3527
|
+
agent: tasks[i].agent,
|
|
3528
|
+
success: r.success,
|
|
3529
|
+
tokens: r.metrics?.tokens || 0,
|
|
3530
|
+
})),
|
|
3531
|
+
});
|
|
3532
|
+
return results;
|
|
3533
|
+
}
|
|
2946
3534
|
/**
|
|
2947
3535
|
* Get a formatted list of available agents.
|
|
2948
3536
|
*/
|
|
@@ -3277,6 +3865,37 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3277
3865
|
this.emit({ type: 'mode.changed', from: this.getMode(), to: parsed });
|
|
3278
3866
|
}
|
|
3279
3867
|
}
|
|
3868
|
+
/**
|
|
3869
|
+
* Set the parent's iteration count for total budget tracking.
|
|
3870
|
+
* When this agent is a subagent, the parent passes its iteration count
|
|
3871
|
+
* so the subagent can account for total iterations across the hierarchy.
|
|
3872
|
+
*/
|
|
3873
|
+
setParentIterations(count) {
|
|
3874
|
+
this.parentIterations = count;
|
|
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
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Get total iterations (this agent + parent).
|
|
3894
|
+
* Used for accurate budget tracking across subagent hierarchies.
|
|
3895
|
+
*/
|
|
3896
|
+
getTotalIterations() {
|
|
3897
|
+
return this.state.iteration + this.parentIterations;
|
|
3898
|
+
}
|
|
3280
3899
|
/**
|
|
3281
3900
|
* Cycle to the next mode (for Tab key).
|
|
3282
3901
|
*/
|
|
@@ -3351,20 +3970,21 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3351
3970
|
/**
|
|
3352
3971
|
* Approve the pending plan and execute the changes.
|
|
3353
3972
|
* @param count - If provided, only approve first N changes
|
|
3354
|
-
* @returns Result of executing the approved changes
|
|
3973
|
+
* @returns Result of executing the approved changes, including tool outputs
|
|
3355
3974
|
*/
|
|
3356
3975
|
async approvePlan(count) {
|
|
3357
3976
|
const result = this.pendingPlanManager.approve(count);
|
|
3358
3977
|
if (result.changes.length === 0) {
|
|
3359
|
-
return { success: true, executed: 0, errors: [] };
|
|
3978
|
+
return { success: true, executed: 0, errors: [], results: [] };
|
|
3360
3979
|
}
|
|
3361
3980
|
// Switch to build mode for execution
|
|
3362
3981
|
const previousMode = this.getMode();
|
|
3363
3982
|
this.setMode('build');
|
|
3364
3983
|
this.emit({ type: 'plan.approved', changeCount: result.changes.length });
|
|
3365
3984
|
const errors = [];
|
|
3985
|
+
const results = [];
|
|
3366
3986
|
let executed = 0;
|
|
3367
|
-
// Execute each change
|
|
3987
|
+
// Execute each change and CAPTURE results
|
|
3368
3988
|
for (let i = 0; i < result.changes.length; i++) {
|
|
3369
3989
|
const change = result.changes[i];
|
|
3370
3990
|
this.emit({ type: 'plan.executing', changeIndex: i, totalChanges: result.changes.length });
|
|
@@ -3372,14 +3992,37 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3372
3992
|
const tool = this.tools.get(change.tool);
|
|
3373
3993
|
if (!tool) {
|
|
3374
3994
|
errors.push(`Unknown tool: ${change.tool}`);
|
|
3995
|
+
this.emit({
|
|
3996
|
+
type: 'plan.change.complete',
|
|
3997
|
+
changeIndex: i,
|
|
3998
|
+
tool: change.tool,
|
|
3999
|
+
result: null,
|
|
4000
|
+
error: `Unknown tool: ${change.tool}`,
|
|
4001
|
+
});
|
|
3375
4002
|
continue;
|
|
3376
4003
|
}
|
|
3377
|
-
|
|
4004
|
+
// CRITICAL: Capture tool result instead of discarding it
|
|
4005
|
+
const toolResult = await tool.execute(change.args);
|
|
4006
|
+
results.push({ tool: change.tool, output: toolResult });
|
|
3378
4007
|
executed++;
|
|
4008
|
+
// Emit result for TUI display
|
|
4009
|
+
this.emit({
|
|
4010
|
+
type: 'plan.change.complete',
|
|
4011
|
+
changeIndex: i,
|
|
4012
|
+
tool: change.tool,
|
|
4013
|
+
result: toolResult,
|
|
4014
|
+
});
|
|
3379
4015
|
}
|
|
3380
4016
|
catch (err) {
|
|
3381
4017
|
const error = err instanceof Error ? err.message : String(err);
|
|
3382
4018
|
errors.push(`${change.tool}: ${error}`);
|
|
4019
|
+
this.emit({
|
|
4020
|
+
type: 'plan.change.complete',
|
|
4021
|
+
changeIndex: i,
|
|
4022
|
+
tool: change.tool,
|
|
4023
|
+
result: null,
|
|
4024
|
+
error,
|
|
4025
|
+
});
|
|
3383
4026
|
}
|
|
3384
4027
|
}
|
|
3385
4028
|
// Restore previous mode if it wasn't build
|
|
@@ -3390,6 +4033,7 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3390
4033
|
success: errors.length === 0,
|
|
3391
4034
|
executed,
|
|
3392
4035
|
errors,
|
|
4036
|
+
results,
|
|
3393
4037
|
};
|
|
3394
4038
|
}
|
|
3395
4039
|
/**
|
|
@@ -3426,6 +4070,12 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3426
4070
|
getAgentRegistry() {
|
|
3427
4071
|
return this.agentRegistry;
|
|
3428
4072
|
}
|
|
4073
|
+
/**
|
|
4074
|
+
* Get the task manager instance for task tracking.
|
|
4075
|
+
*/
|
|
4076
|
+
getTaskManager() {
|
|
4077
|
+
return this.taskManager;
|
|
4078
|
+
}
|
|
3429
4079
|
/**
|
|
3430
4080
|
* Get all loaded skills.
|
|
3431
4081
|
*/
|