attocode 0.1.6 → 0.1.8
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 +23 -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 +35 -1
- package/dist/src/adapters.js.map +1 -1
- package/dist/src/agent.d.ts +80 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +874 -96
- 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 +126 -5
- package/dist/src/commands/handler.js.map +1 -1
- package/dist/src/defaults.d.ts +33 -1
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +61 -3
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/integrations/agent-registry.d.ts +14 -0
- package/dist/src/integrations/agent-registry.d.ts.map +1 -1
- package/dist/src/integrations/agent-registry.js +4 -4
- package/dist/src/integrations/agent-registry.js.map +1 -1
- package/dist/src/integrations/cancellation.d.ts +62 -0
- package/dist/src/integrations/cancellation.d.ts.map +1 -1
- package/dist/src/integrations/cancellation.js +174 -0
- package/dist/src/integrations/cancellation.js.map +1 -1
- package/dist/src/integrations/dead-letter-queue.js +1 -1
- package/dist/src/integrations/dead-letter-queue.js.map +1 -1
- package/dist/src/integrations/economics.d.ts +41 -0
- package/dist/src/integrations/economics.d.ts.map +1 -1
- package/dist/src/integrations/economics.js +114 -8
- package/dist/src/integrations/economics.js.map +1 -1
- package/dist/src/integrations/history.d.ts +72 -0
- package/dist/src/integrations/history.d.ts.map +1 -0
- package/dist/src/integrations/history.js +165 -0
- package/dist/src/integrations/history.js.map +1 -0
- package/dist/src/integrations/index.d.ts +5 -3
- package/dist/src/integrations/index.d.ts.map +1 -1
- package/dist/src/integrations/index.js +6 -2
- package/dist/src/integrations/index.js.map +1 -1
- package/dist/src/integrations/resources.d.ts +5 -0
- package/dist/src/integrations/resources.d.ts.map +1 -1
- package/dist/src/integrations/resources.js +7 -0
- package/dist/src/integrations/resources.js.map +1 -1
- package/dist/src/integrations/safety.d.ts +3 -1
- package/dist/src/integrations/safety.d.ts.map +1 -1
- package/dist/src/integrations/safety.js +22 -5
- 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 +17 -1
- 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 +15 -0
- package/dist/src/modes/tui.js.map +1 -1
- package/dist/src/modes.d.ts +23 -0
- package/dist/src/modes.d.ts.map +1 -1
- package/dist/src/modes.js +61 -0
- 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/adapters/openrouter.js +2 -2
- package/dist/src/providers/adapters/openrouter.js.map +1 -1
- package/dist/src/tools/agent.d.ts +18 -1
- package/dist/src/tools/agent.d.ts.map +1 -1
- package/dist/src/tools/agent.js +51 -3
- package/dist/src/tools/agent.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 +459 -114
- 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/DebugPanel.d.ts +41 -0
- package/dist/src/tui/components/DebugPanel.d.ts.map +1 -0
- package/dist/src/tui/components/DebugPanel.js +104 -0
- package/dist/src/tui/components/DebugPanel.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/ErrorDetailPanel.d.ts +49 -0
- package/dist/src/tui/components/ErrorDetailPanel.d.ts.map +1 -0
- package/dist/src/tui/components/ErrorDetailPanel.js +109 -0
- package/dist/src/tui/components/ErrorDetailPanel.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/ToolCallItem.d.ts +3 -4
- package/dist/src/tui/components/ToolCallItem.d.ts.map +1 -1
- package/dist/src/tui/components/ToolCallItem.js +51 -15
- package/dist/src/tui/components/ToolCallItem.js.map +1 -1
- package/dist/src/tui/components/index.d.ts +5 -0
- package/dist/src/tui/components/index.d.ts.map +1 -1
- package/dist/src/tui/components/index.js +10 -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/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 +94 -0
- package/dist/src/types.d.ts.map +1 -1
- 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,
|
|
24
|
+
import { HookManager, MemoryManager, PlanningManager, ObservabilityManager, SafetyManager, RoutingManager, MultiAgentManager, ReActManager, ExecutionPolicyManager, ThreadManager, RulesManager, DEFAULT_RULE_SOURCES, ExecutionEconomicsManager, STANDARD_BUDGET, SUBAGENT_BUDGET, TIMEOUT_WRAPUP_PROMPT, AgentRegistry, filterToolsForAgent, formatAgentList, createCancellationManager, isCancellationError, createLinkedToken, createGracefulTimeout, race, createResourceManager, createLSPManager, createSemanticCacheManager, createSkillManager, formatSkillList, createContextEngineering, stableStringify, createCodebaseContext, buildContextFromChunks, createPendingPlanManager, createInteractivePlanner, createRecursiveContext, createLearningStore, createCompactor, createAutoCompactionManager, createFileChangeTracker, createCapabilitiesRegistry, createSharedBlackboard, createTaskManager, } from './integrations/index.js';
|
|
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,15 +74,25 @@ export class ProductionAgent {
|
|
|
72
74
|
capabilitiesRegistry = null;
|
|
73
75
|
toolResolver = null;
|
|
74
76
|
blackboard = null;
|
|
77
|
+
taskManager = null;
|
|
78
|
+
store = null;
|
|
75
79
|
// Duplicate spawn prevention - tracks recently spawned tasks to prevent doom loops
|
|
76
80
|
// Map<taskKey, { timestamp: number; result: string; queuedChanges: number }>
|
|
77
81
|
spawnedTasks = new Map();
|
|
78
82
|
static SPAWN_DEDUP_WINDOW_MS = 60000; // 60 seconds
|
|
79
83
|
// Parent iteration tracking for total budget calculation
|
|
80
84
|
parentIterations = 0;
|
|
85
|
+
// External cancellation token (for subagent timeout propagation)
|
|
86
|
+
// When set, the agent will check this token in addition to its own cancellation manager
|
|
87
|
+
externalCancellationToken = null;
|
|
88
|
+
// Graceful wrapup support (for subagent timeout wrapup phase)
|
|
89
|
+
wrapupRequested = false;
|
|
90
|
+
wrapupReason = null;
|
|
81
91
|
// Initialization tracking
|
|
82
92
|
initPromises = [];
|
|
83
93
|
initComplete = false;
|
|
94
|
+
// Event listener cleanup tracking (prevents memory leaks in long sessions)
|
|
95
|
+
unsubscribers = [];
|
|
84
96
|
// State
|
|
85
97
|
state = {
|
|
86
98
|
status: 'idle',
|
|
@@ -95,6 +107,10 @@ export class ProductionAgent {
|
|
|
95
107
|
llmCalls: 0,
|
|
96
108
|
toolCalls: 0,
|
|
97
109
|
duration: 0,
|
|
110
|
+
successCount: 0,
|
|
111
|
+
failureCount: 0,
|
|
112
|
+
cancelCount: 0,
|
|
113
|
+
retryCount: 0,
|
|
98
114
|
},
|
|
99
115
|
iteration: 0,
|
|
100
116
|
};
|
|
@@ -221,11 +237,13 @@ export class ProductionAgent {
|
|
|
221
237
|
}));
|
|
222
238
|
}
|
|
223
239
|
// Economics System (Token Budget) - always enabled
|
|
240
|
+
// Use custom budget if provided (subagents use SUBAGENT_BUDGET), otherwise STANDARD_BUDGET
|
|
241
|
+
const baseBudget = this.config.budget ?? STANDARD_BUDGET;
|
|
224
242
|
this.economics = new ExecutionEconomicsManager({
|
|
225
|
-
...
|
|
243
|
+
...baseBudget,
|
|
226
244
|
// Use maxIterations from config as absolute safety cap
|
|
227
245
|
maxIterations: this.config.maxIterations,
|
|
228
|
-
targetIterations: Math.min(20, this.config.maxIterations),
|
|
246
|
+
targetIterations: Math.min(baseBudget.targetIterations ?? 20, this.config.maxIterations),
|
|
229
247
|
});
|
|
230
248
|
// Agent Registry - always enabled for subagent support
|
|
231
249
|
this.agentRegistry = new AgentRegistry();
|
|
@@ -234,20 +252,39 @@ export class ProductionAgent {
|
|
|
234
252
|
console.warn('[ProductionAgent] Failed to load user agents:', err);
|
|
235
253
|
}));
|
|
236
254
|
// Register spawn_agent tool so LLM can delegate to subagents
|
|
237
|
-
const boundSpawnTool = createBoundSpawnAgentTool((name, task) => this.spawnAgent(name, task));
|
|
255
|
+
const boundSpawnTool = createBoundSpawnAgentTool((name, task, constraints) => this.spawnAgent(name, task, constraints));
|
|
238
256
|
this.tools.set(boundSpawnTool.name, boundSpawnTool);
|
|
239
257
|
// Register spawn_agents_parallel tool for parallel subagent execution
|
|
240
258
|
const boundParallelSpawnTool = createBoundSpawnAgentsParallelTool((tasks) => this.spawnAgentsParallel(tasks));
|
|
241
259
|
this.tools.set(boundParallelSpawnTool.name, boundParallelSpawnTool);
|
|
260
|
+
// Task Manager - Claude Code-style task system for coordination
|
|
261
|
+
this.taskManager = createTaskManager();
|
|
262
|
+
// Forward task events (with cleanup tracking for EventEmitter-based managers)
|
|
263
|
+
const taskCreatedHandler = (data) => {
|
|
264
|
+
this.emit({ type: 'task.created', task: data.task });
|
|
265
|
+
};
|
|
266
|
+
this.taskManager.on('task.created', taskCreatedHandler);
|
|
267
|
+
this.unsubscribers.push(() => this.taskManager?.off('task.created', taskCreatedHandler));
|
|
268
|
+
const taskUpdatedHandler = (data) => {
|
|
269
|
+
this.emit({ type: 'task.updated', task: data.task });
|
|
270
|
+
};
|
|
271
|
+
this.taskManager.on('task.updated', taskUpdatedHandler);
|
|
272
|
+
this.unsubscribers.push(() => this.taskManager?.off('task.updated', taskUpdatedHandler));
|
|
273
|
+
// Register task tools
|
|
274
|
+
const taskTools = createTaskTools(this.taskManager);
|
|
275
|
+
for (const tool of taskTools) {
|
|
276
|
+
this.tools.set(tool.name, tool);
|
|
277
|
+
}
|
|
242
278
|
// Cancellation Support
|
|
243
279
|
if (isFeatureEnabled(this.config.cancellation)) {
|
|
244
280
|
this.cancellation = createCancellationManager();
|
|
245
|
-
// Forward cancellation events
|
|
246
|
-
this.cancellation.subscribe(event => {
|
|
281
|
+
// Forward cancellation events (with cleanup tracking)
|
|
282
|
+
const unsubCancellation = this.cancellation.subscribe(event => {
|
|
247
283
|
if (event.type === 'cancellation.requested') {
|
|
248
284
|
this.emit({ type: 'cancellation.requested', reason: event.reason });
|
|
249
285
|
}
|
|
250
286
|
});
|
|
287
|
+
this.unsubscribers.push(unsubCancellation);
|
|
251
288
|
}
|
|
252
289
|
// Resource Monitoring
|
|
253
290
|
if (isFeatureEnabled(this.config.resources)) {
|
|
@@ -278,8 +315,8 @@ export class ProductionAgent {
|
|
|
278
315
|
maxSize: this.config.semanticCache.maxSize,
|
|
279
316
|
ttl: this.config.semanticCache.ttl,
|
|
280
317
|
});
|
|
281
|
-
// Forward cache events
|
|
282
|
-
this.semanticCache.subscribe(event => {
|
|
318
|
+
// Forward cache events (with cleanup tracking)
|
|
319
|
+
const unsubSemanticCache = this.semanticCache.subscribe(event => {
|
|
283
320
|
if (event.type === 'cache.hit') {
|
|
284
321
|
this.emit({ type: 'cache.hit', query: event.query, similarity: event.similarity });
|
|
285
322
|
}
|
|
@@ -290,6 +327,7 @@ export class ProductionAgent {
|
|
|
290
327
|
this.emit({ type: 'cache.set', query: event.query });
|
|
291
328
|
}
|
|
292
329
|
});
|
|
330
|
+
this.unsubscribers.push(unsubSemanticCache);
|
|
293
331
|
}
|
|
294
332
|
// Skills Support
|
|
295
333
|
if (isFeatureEnabled(this.config.skills)) {
|
|
@@ -341,8 +379,8 @@ export class ProductionAgent {
|
|
|
341
379
|
this.codebaseContext.setLSPManager(this.lspManager);
|
|
342
380
|
}
|
|
343
381
|
}
|
|
344
|
-
// Forward context engineering events
|
|
345
|
-
this.contextEngineering.on(event => {
|
|
382
|
+
// Forward context engineering events (with cleanup tracking)
|
|
383
|
+
const unsubContextEngineering = this.contextEngineering.on(event => {
|
|
346
384
|
switch (event.type) {
|
|
347
385
|
case 'failure.recorded':
|
|
348
386
|
this.observability?.logger?.warn('Failure recorded', {
|
|
@@ -364,6 +402,7 @@ export class ProductionAgent {
|
|
|
364
402
|
break;
|
|
365
403
|
}
|
|
366
404
|
});
|
|
405
|
+
this.unsubscribers.push(unsubContextEngineering);
|
|
367
406
|
// Interactive Planning (conversational + editable planning)
|
|
368
407
|
if (isFeatureEnabled(this.config.interactivePlanning)) {
|
|
369
408
|
const interactiveConfig = typeof this.config.interactivePlanning === 'object'
|
|
@@ -375,8 +414,8 @@ export class ProductionAgent {
|
|
|
375
414
|
maxCheckpoints: 20,
|
|
376
415
|
autoPauseAtDecisions: true,
|
|
377
416
|
});
|
|
378
|
-
// Forward planner events to observability
|
|
379
|
-
this.interactivePlanner.on(event => {
|
|
417
|
+
// Forward planner events to observability (with cleanup tracking)
|
|
418
|
+
const unsubInteractivePlanner = this.interactivePlanner.on(event => {
|
|
380
419
|
switch (event.type) {
|
|
381
420
|
case 'plan.created':
|
|
382
421
|
this.observability?.logger?.info('Interactive plan created', {
|
|
@@ -400,6 +439,7 @@ export class ProductionAgent {
|
|
|
400
439
|
break;
|
|
401
440
|
}
|
|
402
441
|
});
|
|
442
|
+
this.unsubscribers.push(unsubInteractivePlanner);
|
|
403
443
|
}
|
|
404
444
|
// Recursive Context (RLM - Recursive Language Models)
|
|
405
445
|
// Enables on-demand context exploration for large codebases
|
|
@@ -416,8 +456,8 @@ export class ProductionAgent {
|
|
|
416
456
|
});
|
|
417
457
|
// Note: File system source should be registered when needed with proper glob/readFile functions
|
|
418
458
|
// This is deferred to allow flexible configuration
|
|
419
|
-
// Forward RLM events
|
|
420
|
-
this.recursiveContext.on(event => {
|
|
459
|
+
// Forward RLM events (with cleanup tracking)
|
|
460
|
+
const unsubRecursiveContext = this.recursiveContext.on(event => {
|
|
421
461
|
switch (event.type) {
|
|
422
462
|
case 'process.started':
|
|
423
463
|
this.observability?.logger?.debug('RLM process started', {
|
|
@@ -444,6 +484,7 @@ export class ProductionAgent {
|
|
|
444
484
|
break;
|
|
445
485
|
}
|
|
446
486
|
});
|
|
487
|
+
this.unsubscribers.push(unsubRecursiveContext);
|
|
447
488
|
}
|
|
448
489
|
// Learning Store (cross-session learning from failures)
|
|
449
490
|
// Connects to the failure tracker in contextEngineering for automatic learning extraction
|
|
@@ -464,8 +505,8 @@ export class ProductionAgent {
|
|
|
464
505
|
this.learningStore.connectFailureTracker(failureTracker);
|
|
465
506
|
}
|
|
466
507
|
}
|
|
467
|
-
// Forward learning events to observability
|
|
468
|
-
this.learningStore.on(event => {
|
|
508
|
+
// Forward learning events to observability (with cleanup tracking)
|
|
509
|
+
const unsubLearningStore = this.learningStore.on(event => {
|
|
469
510
|
switch (event.type) {
|
|
470
511
|
case 'learning.proposed':
|
|
471
512
|
this.observability?.logger?.info('Learning proposed', {
|
|
@@ -503,6 +544,7 @@ export class ProductionAgent {
|
|
|
503
544
|
break;
|
|
504
545
|
}
|
|
505
546
|
});
|
|
547
|
+
this.unsubscribers.push(unsubLearningStore);
|
|
506
548
|
}
|
|
507
549
|
// Auto-Compaction Manager (sophisticated context compaction)
|
|
508
550
|
// Uses the Compactor for LLM-based summarization with threshold monitoring
|
|
@@ -565,8 +607,8 @@ export class ProductionAgent {
|
|
|
565
607
|
maxContextTokens, // Dynamic from model registry or config
|
|
566
608
|
compactHandler, // Use reversible compaction when contextEngineering is available
|
|
567
609
|
});
|
|
568
|
-
// Forward compactor events to observability
|
|
569
|
-
this.compactor.on(event => {
|
|
610
|
+
// Forward compactor events to observability (with cleanup tracking)
|
|
611
|
+
const unsubCompactor = this.compactor.on(event => {
|
|
570
612
|
switch (event.type) {
|
|
571
613
|
case 'compaction.start':
|
|
572
614
|
this.observability?.logger?.info('Compaction started', {
|
|
@@ -587,8 +629,9 @@ export class ProductionAgent {
|
|
|
587
629
|
break;
|
|
588
630
|
}
|
|
589
631
|
});
|
|
590
|
-
|
|
591
|
-
|
|
632
|
+
this.unsubscribers.push(unsubCompactor);
|
|
633
|
+
// Forward auto-compaction events (with cleanup tracking)
|
|
634
|
+
const unsubAutoCompaction = this.autoCompactionManager.on((event) => {
|
|
592
635
|
switch (event.type) {
|
|
593
636
|
case 'autocompaction.warning':
|
|
594
637
|
this.observability?.logger?.warn('Context approaching limit', {
|
|
@@ -635,6 +678,7 @@ export class ProductionAgent {
|
|
|
635
678
|
break;
|
|
636
679
|
}
|
|
637
680
|
});
|
|
681
|
+
this.unsubscribers.push(unsubAutoCompaction);
|
|
638
682
|
}
|
|
639
683
|
// Note: FileChangeTracker requires a database instance which is not
|
|
640
684
|
// available at this point. Use initFileChangeTracker() to enable it
|
|
@@ -723,6 +767,7 @@ export class ProductionAgent {
|
|
|
723
767
|
// Finalize
|
|
724
768
|
const duration = Date.now() - startTime;
|
|
725
769
|
this.state.metrics.duration = duration;
|
|
770
|
+
this.state.metrics.successCount = (this.state.metrics.successCount ?? 0) + 1;
|
|
726
771
|
await this.observability?.tracer?.endTrace();
|
|
727
772
|
const result = {
|
|
728
773
|
success: true,
|
|
@@ -753,6 +798,7 @@ export class ProductionAgent {
|
|
|
753
798
|
const cleanupDuration = Date.now() - cleanupStart;
|
|
754
799
|
this.emit({ type: 'cancellation.completed', cleanupDuration });
|
|
755
800
|
this.observability?.logger?.info('Agent cancelled', { reason: error.message, cleanupDuration });
|
|
801
|
+
this.state.metrics.cancelCount = (this.state.metrics.cancelCount ?? 0) + 1;
|
|
756
802
|
// Lesson 26: End trace capture on cancellation
|
|
757
803
|
if (this.traceCollector?.isTaskActive()) {
|
|
758
804
|
await this.traceCollector.endTask({ success: false, failureReason: `Cancelled: ${error.message}` });
|
|
@@ -771,6 +817,7 @@ export class ProductionAgent {
|
|
|
771
817
|
}
|
|
772
818
|
this.observability?.tracer?.recordError(error);
|
|
773
819
|
await this.observability?.tracer?.endTrace();
|
|
820
|
+
this.state.metrics.failureCount = (this.state.metrics.failureCount ?? 0) + 1;
|
|
774
821
|
this.emit({ type: 'error', error: error.message });
|
|
775
822
|
this.observability?.logger?.error('Agent failed', { error: error.message });
|
|
776
823
|
// Lesson 26: End trace capture on error
|
|
@@ -850,6 +897,9 @@ export class ProductionAgent {
|
|
|
850
897
|
: 0.8;
|
|
851
898
|
let reflectionAttempt = 0;
|
|
852
899
|
let lastResponse = '';
|
|
900
|
+
let incompleteActionRetries = 0;
|
|
901
|
+
const requestedArtifact = this.extractRequestedArtifact(task);
|
|
902
|
+
const executedToolNames = new Set();
|
|
853
903
|
// Outer loop for reflection (if enabled)
|
|
854
904
|
while (reflectionAttempt < maxReflectionAttempts) {
|
|
855
905
|
reflectionAttempt++;
|
|
@@ -863,6 +913,9 @@ export class ProductionAgent {
|
|
|
863
913
|
});
|
|
864
914
|
// =======================================================================
|
|
865
915
|
// CANCELLATION CHECK
|
|
916
|
+
// Checks internal cancellation (ESC key) — always immediate.
|
|
917
|
+
// External cancellation (parent timeout) is checked after economics
|
|
918
|
+
// to allow graceful wrapup when wrapup has been requested.
|
|
866
919
|
// =======================================================================
|
|
867
920
|
if (this.cancellation?.isCancelled) {
|
|
868
921
|
this.cancellation.token.throwIfCancellationRequested();
|
|
@@ -916,6 +969,7 @@ export class ProductionAgent {
|
|
|
916
969
|
attempt: 1,
|
|
917
970
|
maxAttempts: 1,
|
|
918
971
|
});
|
|
972
|
+
this.state.metrics.retryCount = (this.state.metrics.retryCount ?? 0) + 1;
|
|
919
973
|
// Mark that we've attempted recovery to prevent infinite loops
|
|
920
974
|
this.state._recoveryAttempted = true;
|
|
921
975
|
const tokensBefore = this.estimateContextTokens(messages);
|
|
@@ -1009,6 +1063,26 @@ export class ProductionAgent {
|
|
|
1009
1063
|
}
|
|
1010
1064
|
}
|
|
1011
1065
|
// =======================================================================
|
|
1066
|
+
// GRACEFUL WRAPUP CHECK
|
|
1067
|
+
// If a wrapup has been requested (e.g., timeout approaching), convert
|
|
1068
|
+
// to forceTextOnly + inject wrapup prompt for structured summary.
|
|
1069
|
+
// Must come after economics check (which may also set forceTextOnly).
|
|
1070
|
+
// =======================================================================
|
|
1071
|
+
if (this.wrapupRequested && !forceTextOnly) {
|
|
1072
|
+
forceTextOnly = true;
|
|
1073
|
+
budgetInjectedPrompt = TIMEOUT_WRAPUP_PROMPT;
|
|
1074
|
+
this.wrapupRequested = false;
|
|
1075
|
+
}
|
|
1076
|
+
// =======================================================================
|
|
1077
|
+
// EXTERNAL CANCELLATION CHECK (deferred from above)
|
|
1078
|
+
// Checked after wrapup so that graceful wrapup can intercept the timeout.
|
|
1079
|
+
// If wrapup was already requested and converted to forceTextOnly above,
|
|
1080
|
+
// we skip throwing here to allow one more text-only turn for the summary.
|
|
1081
|
+
// =======================================================================
|
|
1082
|
+
if (this.externalCancellationToken?.isCancellationRequested && !forceTextOnly) {
|
|
1083
|
+
this.externalCancellationToken.throwIfCancellationRequested();
|
|
1084
|
+
}
|
|
1085
|
+
// =======================================================================
|
|
1012
1086
|
// INTELLIGENT LOOP DETECTION & NUDGE INJECTION
|
|
1013
1087
|
// Uses economics system for doom loops, exploration saturation, etc.
|
|
1014
1088
|
// =======================================================================
|
|
@@ -1107,6 +1181,40 @@ export class ProductionAgent {
|
|
|
1107
1181
|
const MAX_CONTINUATIONS = resilienceConfig.maxContinuations ?? 3;
|
|
1108
1182
|
const AUTO_CONTINUE = resilienceConfig.autoContinue ?? true;
|
|
1109
1183
|
const MIN_CONTENT_LENGTH = resilienceConfig.minContentLength ?? 1;
|
|
1184
|
+
const INCOMPLETE_ACTION_RECOVERY = resilienceConfig.incompleteActionRecovery ?? true;
|
|
1185
|
+
const MAX_INCOMPLETE_ACTION_RETRIES = resilienceConfig.maxIncompleteActionRetries ?? 2;
|
|
1186
|
+
const ENFORCE_REQUESTED_ARTIFACTS = resilienceConfig.enforceRequestedArtifacts ?? true;
|
|
1187
|
+
// =================================================================
|
|
1188
|
+
// PRE-FLIGHT BUDGET CHECK: Estimate if LLM call would exceed budget
|
|
1189
|
+
// Catches cases where we're at e.g. 120k and next call adds ~35k
|
|
1190
|
+
// =================================================================
|
|
1191
|
+
if (this.economics && !forceTextOnly) {
|
|
1192
|
+
const estimatedInputTokens = this.estimateContextTokens(messages);
|
|
1193
|
+
const estimatedOutputTokens = 4096; // Conservative output estimate
|
|
1194
|
+
const currentUsage = this.economics.getUsage();
|
|
1195
|
+
const budget = this.economics.getBudget();
|
|
1196
|
+
const projectedTotal = currentUsage.tokens + estimatedInputTokens + estimatedOutputTokens;
|
|
1197
|
+
if (projectedTotal > budget.maxTokens) {
|
|
1198
|
+
this.observability?.logger?.warn('Pre-flight budget check: projected overshoot', {
|
|
1199
|
+
currentTokens: currentUsage.tokens,
|
|
1200
|
+
estimatedInput: estimatedInputTokens,
|
|
1201
|
+
projectedTotal,
|
|
1202
|
+
maxTokens: budget.maxTokens,
|
|
1203
|
+
});
|
|
1204
|
+
// Inject wrap-up prompt if not already injected
|
|
1205
|
+
if (!budgetInjectedPrompt) {
|
|
1206
|
+
messages.push({
|
|
1207
|
+
role: 'user',
|
|
1208
|
+
content: '[System] BUDGET CRITICAL: This is your LAST response. Summarize findings concisely and stop. Do NOT call tools.',
|
|
1209
|
+
});
|
|
1210
|
+
this.state.messages.push({
|
|
1211
|
+
role: 'user',
|
|
1212
|
+
content: '[System] BUDGET CRITICAL: This is your LAST response. Summarize findings concisely and stop. Do NOT call tools.',
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
forceTextOnly = true;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1110
1218
|
let response = await this.callLLM(messages);
|
|
1111
1219
|
let emptyRetries = 0;
|
|
1112
1220
|
let continuations = 0;
|
|
@@ -1136,6 +1244,7 @@ export class ProductionAgent {
|
|
|
1136
1244
|
attempt: emptyRetries,
|
|
1137
1245
|
maxAttempts: MAX_EMPTY_RETRIES,
|
|
1138
1246
|
});
|
|
1247
|
+
this.state.metrics.retryCount = (this.state.metrics.retryCount ?? 0) + 1;
|
|
1139
1248
|
this.observability?.logger?.warn('Empty LLM response, retrying', {
|
|
1140
1249
|
attempt: emptyRetries,
|
|
1141
1250
|
maxAttempts: MAX_EMPTY_RETRIES,
|
|
@@ -1193,10 +1302,51 @@ export class ProductionAgent {
|
|
|
1193
1302
|
});
|
|
1194
1303
|
}
|
|
1195
1304
|
}
|
|
1305
|
+
// Phase 2b: Handle truncated tool calls (stopReason=max_tokens with tool calls present)
|
|
1306
|
+
// When a model hits max_tokens mid-tool-call, the JSON arguments are truncated and unparseable.
|
|
1307
|
+
// Instead of executing broken tool calls, strip them and ask the LLM to retry smaller.
|
|
1308
|
+
if (resilienceEnabled && response.stopReason === 'max_tokens' && response.toolCalls?.length) {
|
|
1309
|
+
this.emit({
|
|
1310
|
+
type: 'resilience.truncated_tool_call',
|
|
1311
|
+
toolNames: response.toolCalls.map(tc => tc.name),
|
|
1312
|
+
});
|
|
1313
|
+
this.observability?.logger?.warn('Tool call truncated at max_tokens', {
|
|
1314
|
+
toolNames: response.toolCalls.map(tc => tc.name),
|
|
1315
|
+
outputTokens: response.usage?.outputTokens,
|
|
1316
|
+
});
|
|
1317
|
+
// Strip truncated tool calls, inject recovery message
|
|
1318
|
+
const truncatedResponse = response;
|
|
1319
|
+
response = { ...response, toolCalls: undefined };
|
|
1320
|
+
const recoveryMessage = {
|
|
1321
|
+
role: 'user',
|
|
1322
|
+
content: '[System: Your previous tool call was truncated because the output exceeded the token limit. ' +
|
|
1323
|
+
'The tool call arguments were cut off and could not be parsed. ' +
|
|
1324
|
+
'Please retry with a smaller approach: for write_file, break the content into smaller chunks ' +
|
|
1325
|
+
'or use edit_file for targeted changes instead of rewriting entire files.]',
|
|
1326
|
+
};
|
|
1327
|
+
messages.push({ role: 'assistant', content: truncatedResponse.content || '' });
|
|
1328
|
+
messages.push(recoveryMessage);
|
|
1329
|
+
this.state.messages.push({ role: 'assistant', content: truncatedResponse.content || '' });
|
|
1330
|
+
this.state.messages.push(recoveryMessage);
|
|
1331
|
+
response = await this.callLLM(messages);
|
|
1332
|
+
}
|
|
1196
1333
|
// Record LLM usage for economics
|
|
1197
1334
|
if (this.economics && response.usage) {
|
|
1198
1335
|
this.economics.recordLLMUsage(response.usage.inputTokens, response.usage.outputTokens, this.config.model, response.usage.cost // Use actual cost from provider when available
|
|
1199
1336
|
);
|
|
1337
|
+
// =================================================================
|
|
1338
|
+
// POST-LLM BUDGET CHECK: Prevent tool execution if over budget
|
|
1339
|
+
// A single LLM call can push us over - catch it before running tools
|
|
1340
|
+
// =================================================================
|
|
1341
|
+
if (!forceTextOnly) {
|
|
1342
|
+
const postCheck = this.economics.checkBudget();
|
|
1343
|
+
if (!postCheck.canContinue) {
|
|
1344
|
+
this.observability?.logger?.warn('Budget exceeded after LLM call, skipping tool execution', {
|
|
1345
|
+
reason: postCheck.reason,
|
|
1346
|
+
});
|
|
1347
|
+
forceTextOnly = true;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1200
1350
|
}
|
|
1201
1351
|
// Add assistant message
|
|
1202
1352
|
const assistantMessage = {
|
|
@@ -1227,6 +1377,61 @@ export class ProductionAgent {
|
|
|
1227
1377
|
iteration: this.state.iteration,
|
|
1228
1378
|
});
|
|
1229
1379
|
}
|
|
1380
|
+
const incompleteAction = this.detectIncompleteActionResponse(response.content || '');
|
|
1381
|
+
const missingRequiredArtifact = ENFORCE_REQUESTED_ARTIFACTS
|
|
1382
|
+
? this.isRequestedArtifactMissing(requestedArtifact, executedToolNames)
|
|
1383
|
+
: false;
|
|
1384
|
+
const shouldRecoverIncompleteAction = resilienceEnabled
|
|
1385
|
+
&& INCOMPLETE_ACTION_RECOVERY
|
|
1386
|
+
&& !forceTextOnly
|
|
1387
|
+
&& (incompleteAction || missingRequiredArtifact);
|
|
1388
|
+
if (shouldRecoverIncompleteAction) {
|
|
1389
|
+
if (incompleteActionRetries < MAX_INCOMPLETE_ACTION_RETRIES) {
|
|
1390
|
+
incompleteActionRetries++;
|
|
1391
|
+
const reason = missingRequiredArtifact && requestedArtifact
|
|
1392
|
+
? `missing_requested_artifact:${requestedArtifact}`
|
|
1393
|
+
: 'future_intent_without_action';
|
|
1394
|
+
this.emit({
|
|
1395
|
+
type: 'resilience.incomplete_action_detected',
|
|
1396
|
+
reason,
|
|
1397
|
+
attempt: incompleteActionRetries,
|
|
1398
|
+
maxAttempts: MAX_INCOMPLETE_ACTION_RETRIES,
|
|
1399
|
+
requiresArtifact: missingRequiredArtifact,
|
|
1400
|
+
});
|
|
1401
|
+
this.observability?.logger?.warn('Incomplete action detected, retrying with nudge', {
|
|
1402
|
+
reason,
|
|
1403
|
+
attempt: incompleteActionRetries,
|
|
1404
|
+
maxAttempts: MAX_INCOMPLETE_ACTION_RETRIES,
|
|
1405
|
+
});
|
|
1406
|
+
const nudgeMessage = {
|
|
1407
|
+
role: 'user',
|
|
1408
|
+
content: missingRequiredArtifact && requestedArtifact
|
|
1409
|
+
? `[System: You said you would complete the next action, but no tool call was made. The task requires creating or updating "${requestedArtifact}". Execute the required tool now, or explicitly explain why it cannot be produced.]`
|
|
1410
|
+
: '[System: You described a next action but did not execute it. If work remains, call the required tool now. If the task is complete, provide a final answer with no pending action language.]',
|
|
1411
|
+
};
|
|
1412
|
+
messages.push(nudgeMessage);
|
|
1413
|
+
this.state.messages.push(nudgeMessage);
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const failureReason = missingRequiredArtifact && requestedArtifact
|
|
1417
|
+
? `incomplete_action_missing_artifact:${requestedArtifact}`
|
|
1418
|
+
: 'incomplete_action_unresolved';
|
|
1419
|
+
this.emit({
|
|
1420
|
+
type: 'resilience.incomplete_action_failed',
|
|
1421
|
+
reason: failureReason,
|
|
1422
|
+
attempts: incompleteActionRetries,
|
|
1423
|
+
maxAttempts: MAX_INCOMPLETE_ACTION_RETRIES,
|
|
1424
|
+
});
|
|
1425
|
+
throw new Error(`LLM failed to complete requested action after ${incompleteActionRetries} retries (${failureReason})`);
|
|
1426
|
+
}
|
|
1427
|
+
if (incompleteActionRetries > 0) {
|
|
1428
|
+
this.emit({
|
|
1429
|
+
type: 'resilience.incomplete_action_recovered',
|
|
1430
|
+
reason: 'incomplete_action',
|
|
1431
|
+
attempts: incompleteActionRetries,
|
|
1432
|
+
});
|
|
1433
|
+
incompleteActionRetries = 0;
|
|
1434
|
+
}
|
|
1230
1435
|
// No tool calls (or forced to ignore), agent is done - compact tool outputs to save context
|
|
1231
1436
|
// The model has "consumed" the tool outputs and produced a response,
|
|
1232
1437
|
// so we can replace verbose outputs with compact summaries
|
|
@@ -1267,6 +1472,7 @@ export class ProductionAgent {
|
|
|
1267
1472
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
1268
1473
|
const toolCall = toolCalls[i];
|
|
1269
1474
|
const result = toolResults[i];
|
|
1475
|
+
executedToolNames.add(toolCall.name);
|
|
1270
1476
|
this.economics?.recordToolCall(toolCall.name, toolCall.arguments, result?.result);
|
|
1271
1477
|
}
|
|
1272
1478
|
// Add tool results to messages (with truncation and proactive budget management)
|
|
@@ -1314,8 +1520,11 @@ export class ProductionAgent {
|
|
|
1314
1520
|
this.compactToolOutputs();
|
|
1315
1521
|
}
|
|
1316
1522
|
}
|
|
1523
|
+
const toolCallNameById = new Map(toolCalls.map(tc => [tc.id, tc.name]));
|
|
1317
1524
|
for (const result of toolResults) {
|
|
1318
1525
|
let content = typeof result.result === 'string' ? result.result : stableStringify(result.result);
|
|
1526
|
+
const sourceToolName = toolCallNameById.get(result.callId);
|
|
1527
|
+
const isExpensiveResult = sourceToolName === 'spawn_agent' || sourceToolName === 'spawn_agents_parallel';
|
|
1319
1528
|
// Truncate long outputs to save context
|
|
1320
1529
|
if (content.length > MAX_TOOL_OUTPUT_CHARS) {
|
|
1321
1530
|
content = content.slice(0, MAX_TOOL_OUTPUT_CHARS) + `\n\n... [truncated ${content.length - MAX_TOOL_OUTPUT_CHARS} chars]`;
|
|
@@ -1350,6 +1559,15 @@ export class ProductionAgent {
|
|
|
1350
1559
|
role: 'tool',
|
|
1351
1560
|
content,
|
|
1352
1561
|
toolCallId: result.callId,
|
|
1562
|
+
...(isExpensiveResult
|
|
1563
|
+
? {
|
|
1564
|
+
metadata: {
|
|
1565
|
+
preserveFromCompaction: true,
|
|
1566
|
+
costToRegenerate: 'high',
|
|
1567
|
+
source: sourceToolName,
|
|
1568
|
+
},
|
|
1569
|
+
}
|
|
1570
|
+
: {}),
|
|
1353
1571
|
};
|
|
1354
1572
|
messages.push(toolMessage);
|
|
1355
1573
|
this.state.messages.push(toolMessage);
|
|
@@ -1715,6 +1933,19 @@ export class ProductionAgent {
|
|
|
1715
1933
|
throw error;
|
|
1716
1934
|
}
|
|
1717
1935
|
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Execute an async callback while excluding wall-clock wait time from duration budgeting.
|
|
1938
|
+
* Used for external waits such as approval dialogs and delegation confirmation.
|
|
1939
|
+
*/
|
|
1940
|
+
async withPausedDuration(fn) {
|
|
1941
|
+
this.economics?.pauseDuration();
|
|
1942
|
+
try {
|
|
1943
|
+
return await fn();
|
|
1944
|
+
}
|
|
1945
|
+
finally {
|
|
1946
|
+
this.economics?.resumeDuration();
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1718
1949
|
/**
|
|
1719
1950
|
* Execute tool calls with safety checks and execution policy enforcement.
|
|
1720
1951
|
*/
|
|
@@ -1772,6 +2003,7 @@ export class ProductionAgent {
|
|
|
1772
2003
|
// =====================================================================
|
|
1773
2004
|
// EXECUTION POLICY ENFORCEMENT (Lesson 23)
|
|
1774
2005
|
// =====================================================================
|
|
2006
|
+
let policyApprovedByUser = false;
|
|
1775
2007
|
if (this.executionPolicy) {
|
|
1776
2008
|
const policyContext = {
|
|
1777
2009
|
messages: this.state.messages,
|
|
@@ -1819,11 +2051,13 @@ export class ProductionAgent {
|
|
|
1819
2051
|
// Handle prompt policy - requires approval
|
|
1820
2052
|
if (evaluation.policy === 'prompt' && evaluation.requiresApproval) {
|
|
1821
2053
|
// Try to get approval through safety manager's human-in-loop
|
|
1822
|
-
|
|
1823
|
-
|
|
2054
|
+
const humanInLoop = this.safety?.humanInLoop;
|
|
2055
|
+
if (humanInLoop) {
|
|
2056
|
+
const approval = await this.withPausedDuration(() => humanInLoop.requestApproval(toolCall, `Policy requires approval: ${evaluation.reason}`));
|
|
1824
2057
|
if (!approval.approved) {
|
|
1825
2058
|
throw new Error(`Denied by user: ${approval.reason || 'No reason provided'}`);
|
|
1826
2059
|
}
|
|
2060
|
+
policyApprovedByUser = true;
|
|
1827
2061
|
// Create a grant for future similar calls if approved
|
|
1828
2062
|
this.executionPolicy.createGrant({
|
|
1829
2063
|
toolName: toolCall.name,
|
|
@@ -1851,7 +2085,8 @@ export class ProductionAgent {
|
|
|
1851
2085
|
// SAFETY VALIDATION (Lesson 20-21)
|
|
1852
2086
|
// =====================================================================
|
|
1853
2087
|
if (this.safety) {
|
|
1854
|
-
const
|
|
2088
|
+
const safety = this.safety;
|
|
2089
|
+
const validation = await this.withPausedDuration(() => safety.validateAndApprove(toolCall, `Executing tool: ${toolCall.name}`, { skipHumanApproval: policyApprovedByUser }));
|
|
1855
2090
|
if (!validation.allowed) {
|
|
1856
2091
|
throw new Error(`Tool call blocked: ${validation.reason}`);
|
|
1857
2092
|
}
|
|
@@ -1900,17 +2135,21 @@ export class ProductionAgent {
|
|
|
1900
2135
|
// Execute tool (with sandbox if available)
|
|
1901
2136
|
let result;
|
|
1902
2137
|
if (this.safety?.sandbox) {
|
|
1903
|
-
// CRITICAL: spawn_agent
|
|
2138
|
+
// CRITICAL: spawn_agent and spawn_agents_parallel need MUCH longer timeouts
|
|
1904
2139
|
// The default 60s sandbox timeout would kill subagents prematurely
|
|
1905
2140
|
// Subagents may run for minutes (per their own timeout config)
|
|
1906
2141
|
const isSpawnAgent = toolCall.name === 'spawn_agent';
|
|
2142
|
+
const isSpawnParallel = toolCall.name === 'spawn_agents_parallel';
|
|
2143
|
+
const isSubagentTool = isSpawnAgent || isSpawnParallel;
|
|
1907
2144
|
const subagentConfig = this.config.subagent;
|
|
1908
2145
|
const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
|
|
1909
2146
|
const subagentTimeout = hasSubagentConfig
|
|
1910
2147
|
? subagentConfig.defaultTimeout ?? 600000 // 10 min default
|
|
1911
2148
|
: 600000;
|
|
1912
|
-
// Use subagent timeout + buffer for
|
|
1913
|
-
|
|
2149
|
+
// Use subagent timeout + buffer for spawn tools, default for others
|
|
2150
|
+
// For spawn_agents_parallel, multiply by number of agents (they run in parallel,
|
|
2151
|
+
// but the total wall-clock time should still allow the slowest agent to complete)
|
|
2152
|
+
const toolTimeout = isSubagentTool ? subagentTimeout + 30000 : undefined;
|
|
1914
2153
|
result = await this.safety.sandbox.executeWithLimits(() => tool.execute(toolCall.arguments), toolTimeout);
|
|
1915
2154
|
}
|
|
1916
2155
|
else {
|
|
@@ -2256,7 +2495,14 @@ export class ProductionAgent {
|
|
|
2256
2495
|
*/
|
|
2257
2496
|
getMetrics() {
|
|
2258
2497
|
if (this.observability?.metrics) {
|
|
2259
|
-
|
|
2498
|
+
const observed = this.observability.metrics.getMetrics();
|
|
2499
|
+
return {
|
|
2500
|
+
...observed,
|
|
2501
|
+
successCount: this.state.metrics.successCount ?? 0,
|
|
2502
|
+
failureCount: this.state.metrics.failureCount ?? 0,
|
|
2503
|
+
cancelCount: this.state.metrics.cancelCount ?? 0,
|
|
2504
|
+
retryCount: this.state.metrics.retryCount ?? 0,
|
|
2505
|
+
};
|
|
2260
2506
|
}
|
|
2261
2507
|
return this.state.metrics;
|
|
2262
2508
|
}
|
|
@@ -2294,6 +2540,13 @@ export class ProductionAgent {
|
|
|
2294
2540
|
getTraceCollector() {
|
|
2295
2541
|
return this.traceCollector;
|
|
2296
2542
|
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Set a trace collector for this agent.
|
|
2545
|
+
* Used for subagents to share the parent's trace collector (with subagent context).
|
|
2546
|
+
*/
|
|
2547
|
+
setTraceCollector(collector) {
|
|
2548
|
+
this.traceCollector = collector;
|
|
2549
|
+
}
|
|
2297
2550
|
/**
|
|
2298
2551
|
* Get the learning store for cross-session learning.
|
|
2299
2552
|
* Returns null if learning store is not enabled.
|
|
@@ -2381,6 +2634,10 @@ export class ProductionAgent {
|
|
|
2381
2634
|
llmCalls: 0,
|
|
2382
2635
|
toolCalls: 0,
|
|
2383
2636
|
duration: 0,
|
|
2637
|
+
successCount: 0,
|
|
2638
|
+
failureCount: 0,
|
|
2639
|
+
cancelCount: 0,
|
|
2640
|
+
retryCount: 0,
|
|
2384
2641
|
},
|
|
2385
2642
|
iteration: 0,
|
|
2386
2643
|
};
|
|
@@ -2526,6 +2783,10 @@ export class ProductionAgent {
|
|
|
2526
2783
|
toolCalls: sanitized.metrics.toolCalls ?? 0,
|
|
2527
2784
|
duration: sanitized.metrics.duration ?? 0,
|
|
2528
2785
|
reflectionAttempts: sanitized.metrics.reflectionAttempts,
|
|
2786
|
+
successCount: sanitized.metrics.successCount ?? 0,
|
|
2787
|
+
failureCount: sanitized.metrics.failureCount ?? 0,
|
|
2788
|
+
cancelCount: sanitized.metrics.cancelCount ?? 0,
|
|
2789
|
+
retryCount: sanitized.metrics.retryCount ?? 0,
|
|
2529
2790
|
};
|
|
2530
2791
|
}
|
|
2531
2792
|
// Restore plan if present
|
|
@@ -2573,10 +2834,20 @@ export class ProductionAgent {
|
|
|
2573
2834
|
*/
|
|
2574
2835
|
compactToolOutputs() {
|
|
2575
2836
|
const COMPACT_PREVIEW_LENGTH = 200; // Keep first 200 chars as preview
|
|
2837
|
+
const MAX_PRESERVED_EXPENSIVE_RESULTS = 6;
|
|
2576
2838
|
let compactedCount = 0;
|
|
2577
2839
|
let savedChars = 0;
|
|
2578
|
-
|
|
2840
|
+
const preservedExpensiveIndexes = this.state.messages
|
|
2841
|
+
.map((msg, index) => ({ msg, index }))
|
|
2842
|
+
.filter(({ msg }) => msg.role === 'tool' && msg.metadata?.preserveFromCompaction === true)
|
|
2843
|
+
.map(({ index }) => index);
|
|
2844
|
+
const preserveSet = new Set(preservedExpensiveIndexes.slice(-MAX_PRESERVED_EXPENSIVE_RESULTS));
|
|
2845
|
+
for (let i = 0; i < this.state.messages.length; i++) {
|
|
2846
|
+
const msg = this.state.messages[i];
|
|
2579
2847
|
if (msg.role === 'tool' && msg.content && msg.content.length > COMPACT_PREVIEW_LENGTH * 2) {
|
|
2848
|
+
if (msg.metadata?.preserveFromCompaction === true && preserveSet.has(i)) {
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2580
2851
|
const originalLength = msg.content.length;
|
|
2581
2852
|
const preview = msg.content.slice(0, COMPACT_PREVIEW_LENGTH).replace(/\n/g, ' ');
|
|
2582
2853
|
msg.content = `[${preview}...] (${originalLength} chars, compacted)`;
|
|
@@ -2608,6 +2879,42 @@ export class ProductionAgent {
|
|
|
2608
2879
|
}
|
|
2609
2880
|
return Math.ceil(totalChars / 4); // ~4 chars per token
|
|
2610
2881
|
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Extract a requested markdown artifact filename from a task prompt.
|
|
2884
|
+
* Returns null when no explicit artifact requirement is detected.
|
|
2885
|
+
*/
|
|
2886
|
+
extractRequestedArtifact(task) {
|
|
2887
|
+
const markdownArtifactMatch = task.match(/(?:write|save|create)[^.\n]{0,120}\b([A-Za-z0-9._/-]+\.md)\b/i);
|
|
2888
|
+
return markdownArtifactMatch?.[1] ?? null;
|
|
2889
|
+
}
|
|
2890
|
+
/**
|
|
2891
|
+
* Check whether a requested artifact appears to be missing based on executed tools.
|
|
2892
|
+
*/
|
|
2893
|
+
isRequestedArtifactMissing(requestedArtifact, executedToolNames) {
|
|
2894
|
+
if (!requestedArtifact) {
|
|
2895
|
+
return false;
|
|
2896
|
+
}
|
|
2897
|
+
const artifactWriteTools = ['write_file', 'edit_file', 'apply_patch', 'append_file'];
|
|
2898
|
+
return !artifactWriteTools.some(toolName => executedToolNames.has(toolName));
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Detect "future-intent" responses that imply the model has not completed work.
|
|
2902
|
+
*/
|
|
2903
|
+
detectIncompleteActionResponse(content) {
|
|
2904
|
+
const trimmed = content.trim();
|
|
2905
|
+
if (!trimmed) {
|
|
2906
|
+
return false;
|
|
2907
|
+
}
|
|
2908
|
+
const lower = trimmed.toLowerCase();
|
|
2909
|
+
const futureIntentPatterns = [
|
|
2910
|
+
/^(now|next|then)\s+(i\s+will|i'll|let me)\b/,
|
|
2911
|
+
/^i\s+(will|am going to|can)\b/,
|
|
2912
|
+
/^(let me|i'll|i will)\s+(create|write|save|do|make|generate|start)\b/,
|
|
2913
|
+
/^(now|next|then)\s+i(?:'ll| will)\b/,
|
|
2914
|
+
];
|
|
2915
|
+
const completionSignals = /\b(done|completed|finished|here is|created|saved|wrote)\b/;
|
|
2916
|
+
return futureIntentPatterns.some(pattern => pattern.test(lower)) && !completionSignals.test(lower);
|
|
2917
|
+
}
|
|
2611
2918
|
/**
|
|
2612
2919
|
* Get audit log (if human-in-loop is enabled).
|
|
2613
2920
|
*/
|
|
@@ -2630,8 +2937,8 @@ export class ProductionAgent {
|
|
|
2630
2937
|
for (const role of roles) {
|
|
2631
2938
|
this.multiAgent.registerRole(role);
|
|
2632
2939
|
}
|
|
2633
|
-
// Set up event forwarding
|
|
2634
|
-
this.multiAgent.on(event => {
|
|
2940
|
+
// Set up event forwarding (unsubscribe after operation to prevent memory leaks)
|
|
2941
|
+
const unsubMultiAgent = this.multiAgent.on(event => {
|
|
2635
2942
|
switch (event.type) {
|
|
2636
2943
|
case 'agent.spawn':
|
|
2637
2944
|
this.emit({ type: 'multiagent.spawn', agentId: event.agentId, role: event.role });
|
|
@@ -2647,14 +2954,19 @@ export class ProductionAgent {
|
|
|
2647
2954
|
break;
|
|
2648
2955
|
}
|
|
2649
2956
|
});
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2957
|
+
try {
|
|
2958
|
+
const result = await this.multiAgent.runWithTeam(task, {
|
|
2959
|
+
roles,
|
|
2960
|
+
consensusStrategy: this.config.multiAgent && isFeatureEnabled(this.config.multiAgent)
|
|
2961
|
+
? this.config.multiAgent.consensusStrategy || 'voting'
|
|
2962
|
+
: 'voting',
|
|
2963
|
+
communicationMode: 'broadcast',
|
|
2964
|
+
});
|
|
2965
|
+
return result;
|
|
2966
|
+
}
|
|
2967
|
+
finally {
|
|
2968
|
+
unsubMultiAgent();
|
|
2969
|
+
}
|
|
2658
2970
|
}
|
|
2659
2971
|
/**
|
|
2660
2972
|
* Add a role to the multi-agent manager.
|
|
@@ -2677,8 +2989,8 @@ export class ProductionAgent {
|
|
|
2677
2989
|
throw new Error('ReAct not enabled. Enable it in config to use runWithReAct()');
|
|
2678
2990
|
}
|
|
2679
2991
|
this.observability?.logger?.info('Running with ReAct', { task });
|
|
2680
|
-
// Set up event forwarding
|
|
2681
|
-
this.react.on(event => {
|
|
2992
|
+
// Set up event forwarding (unsubscribe after operation to prevent memory leaks)
|
|
2993
|
+
const unsubReact = this.react.on(event => {
|
|
2682
2994
|
switch (event.type) {
|
|
2683
2995
|
case 'react.thought':
|
|
2684
2996
|
this.emit({ type: 'react.thought', step: event.step, thought: event.thought });
|
|
@@ -2694,15 +3006,20 @@ export class ProductionAgent {
|
|
|
2694
3006
|
break;
|
|
2695
3007
|
}
|
|
2696
3008
|
});
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
this.memory.
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3009
|
+
try {
|
|
3010
|
+
const trace = await this.react.run(task);
|
|
3011
|
+
// Store trace in memory if available
|
|
3012
|
+
if (this.memory && trace.finalAnswer) {
|
|
3013
|
+
this.memory.storeConversation([
|
|
3014
|
+
{ role: 'user', content: task },
|
|
3015
|
+
{ role: 'assistant', content: trace.finalAnswer },
|
|
3016
|
+
]);
|
|
3017
|
+
}
|
|
3018
|
+
return trace;
|
|
3019
|
+
}
|
|
3020
|
+
finally {
|
|
3021
|
+
unsubReact();
|
|
2704
3022
|
}
|
|
2705
|
-
return trace;
|
|
2706
3023
|
}
|
|
2707
3024
|
/**
|
|
2708
3025
|
* Get the ReAct trace formatted as a string.
|
|
@@ -2813,6 +3130,11 @@ export class ProductionAgent {
|
|
|
2813
3130
|
if (!this.threadManager) {
|
|
2814
3131
|
throw new Error('Thread management not enabled. Enable it in config to use createCheckpoint()');
|
|
2815
3132
|
}
|
|
3133
|
+
// CRITICAL: Sync current state.messages to threadManager before checkpoint
|
|
3134
|
+
// The run() method adds messages directly to this.state.messages but doesn't sync
|
|
3135
|
+
// to threadManager, so thread.messages would be empty without this sync
|
|
3136
|
+
const thread = this.threadManager.getActiveThread();
|
|
3137
|
+
thread.messages = [...this.state.messages];
|
|
2816
3138
|
const checkpoint = this.threadManager.createCheckpoint({
|
|
2817
3139
|
label,
|
|
2818
3140
|
agentState: this.state,
|
|
@@ -2987,8 +3309,12 @@ export class ProductionAgent {
|
|
|
2987
3309
|
/**
|
|
2988
3310
|
* Spawn an agent to execute a task.
|
|
2989
3311
|
* Returns the result when the agent completes.
|
|
3312
|
+
*
|
|
3313
|
+
* @param agentName - Name of the agent to spawn (researcher, coder, etc.)
|
|
3314
|
+
* @param task - The task description for the agent
|
|
3315
|
+
* @param constraints - Optional constraints to keep the subagent focused
|
|
2990
3316
|
*/
|
|
2991
|
-
async spawnAgent(agentName, task) {
|
|
3317
|
+
async spawnAgent(agentName, task, constraints) {
|
|
2992
3318
|
if (!this.agentRegistry) {
|
|
2993
3319
|
return {
|
|
2994
3320
|
success: false,
|
|
@@ -3004,10 +3330,10 @@ export class ProductionAgent {
|
|
|
3004
3330
|
metrics: { tokens: 0, duration: 0, toolCalls: 0 },
|
|
3005
3331
|
};
|
|
3006
3332
|
}
|
|
3007
|
-
// DUPLICATE SPAWN PREVENTION
|
|
3008
|
-
//
|
|
3333
|
+
// DUPLICATE SPAWN PREVENTION with SEMANTIC SIMILARITY
|
|
3334
|
+
// First try exact string match, then check semantic similarity for similar tasks
|
|
3335
|
+
const SEMANTIC_SIMILARITY_THRESHOLD = 0.75; // 75% similarity = duplicate
|
|
3009
3336
|
const taskKey = `${agentName}:${task.slice(0, 150).toLowerCase().replace(/\s+/g, ' ').trim()}`;
|
|
3010
|
-
const existing = this.spawnedTasks.get(taskKey);
|
|
3011
3337
|
const now = Date.now();
|
|
3012
3338
|
// Clean up old entries (older than dedup window)
|
|
3013
3339
|
for (const [key, entry] of this.spawnedTasks.entries()) {
|
|
@@ -3015,32 +3341,63 @@ export class ProductionAgent {
|
|
|
3015
3341
|
this.spawnedTasks.delete(key);
|
|
3016
3342
|
}
|
|
3017
3343
|
}
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3344
|
+
// Check for exact match first
|
|
3345
|
+
let existingMatch = this.spawnedTasks.get(taskKey);
|
|
3346
|
+
let matchType = 'exact';
|
|
3347
|
+
// If no exact match, check for semantic similarity among same agent's tasks
|
|
3348
|
+
if (!existingMatch) {
|
|
3349
|
+
for (const [key, entry] of this.spawnedTasks.entries()) {
|
|
3350
|
+
// Only compare tasks from the same agent type
|
|
3351
|
+
if (!key.startsWith(`${agentName}:`))
|
|
3352
|
+
continue;
|
|
3353
|
+
if (now - entry.timestamp >= ProductionAgent.SPAWN_DEDUP_WINDOW_MS)
|
|
3354
|
+
continue;
|
|
3355
|
+
// Extract the task portion from the key
|
|
3356
|
+
const existingTask = key.slice(agentName.length + 1);
|
|
3357
|
+
const similarity = calculateTaskSimilarity(task, existingTask);
|
|
3358
|
+
if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
|
|
3359
|
+
existingMatch = entry;
|
|
3360
|
+
matchType = 'semantic';
|
|
3361
|
+
this.observability?.logger?.debug('Semantic duplicate detected', {
|
|
3362
|
+
agent: agentName,
|
|
3363
|
+
newTask: task.slice(0, 80),
|
|
3364
|
+
existingTask: existingTask.slice(0, 80),
|
|
3365
|
+
similarity: (similarity * 100).toFixed(1) + '%',
|
|
3366
|
+
});
|
|
3367
|
+
break;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
if (existingMatch && now - existingMatch.timestamp < ProductionAgent.SPAWN_DEDUP_WINDOW_MS) {
|
|
3372
|
+
// Same or semantically similar task spawned within the dedup window
|
|
3021
3373
|
this.observability?.logger?.warn('Duplicate spawn prevented', {
|
|
3022
3374
|
agent: agentName,
|
|
3023
3375
|
task: task.slice(0, 100),
|
|
3024
|
-
|
|
3025
|
-
|
|
3376
|
+
matchType,
|
|
3377
|
+
originalTimestamp: existingMatch.timestamp,
|
|
3378
|
+
elapsedMs: now - existingMatch.timestamp,
|
|
3026
3379
|
});
|
|
3027
|
-
const duplicateMessage = `[DUPLICATE SPAWN PREVENTED]\n` +
|
|
3028
|
-
`This task was already spawned ${Math.round((now -
|
|
3029
|
-
`${
|
|
3030
|
-
? `The previous spawn queued ${
|
|
3380
|
+
const duplicateMessage = `[DUPLICATE SPAWN PREVENTED${matchType === 'semantic' ? ' - SEMANTIC MATCH' : ''}]\n` +
|
|
3381
|
+
`This task was already spawned ${Math.round((now - existingMatch.timestamp) / 1000)}s ago.\n` +
|
|
3382
|
+
`${existingMatch.queuedChanges > 0
|
|
3383
|
+
? `The previous spawn queued ${existingMatch.queuedChanges} change(s) to the pending plan.\n` +
|
|
3031
3384
|
`These changes are already in your plan - do NOT spawn again.\n`
|
|
3032
|
-
: ''}Previous result summary:\n${
|
|
3385
|
+
: ''}Previous result summary:\n${existingMatch.result.slice(0, 500)}`;
|
|
3033
3386
|
return {
|
|
3034
3387
|
success: true, // Mark as success since original task completed
|
|
3035
3388
|
output: duplicateMessage,
|
|
3036
3389
|
metrics: { tokens: 0, duration: 0, toolCalls: 0 },
|
|
3037
3390
|
};
|
|
3038
3391
|
}
|
|
3039
|
-
this
|
|
3392
|
+
// Generate a unique ID for this agent instance that will be used consistently
|
|
3393
|
+
// throughout the agent's lifecycle (spawn event, token events, completion events)
|
|
3394
|
+
const agentId = `spawn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3395
|
+
this.emit({ type: 'agent.spawn', agentId, name: agentName, task });
|
|
3040
3396
|
this.observability?.logger?.info('Spawning agent', { name: agentName, task });
|
|
3041
3397
|
const startTime = Date.now();
|
|
3042
3398
|
const childSessionId = `subagent-${agentName}-${Date.now()}`;
|
|
3043
3399
|
const childTraceId = `trace-${childSessionId}`;
|
|
3400
|
+
let workerResultId;
|
|
3044
3401
|
try {
|
|
3045
3402
|
// Filter tools for this agent
|
|
3046
3403
|
const agentTools = filterToolsForAgent(agentDef, Array.from(this.tools.values()));
|
|
@@ -3049,17 +3406,108 @@ export class ProductionAgent {
|
|
|
3049
3406
|
const resolvedModel = (agentDef.model && agentDef.model.includes('/'))
|
|
3050
3407
|
? agentDef.model
|
|
3051
3408
|
: this.config.model;
|
|
3052
|
-
//
|
|
3053
|
-
|
|
3409
|
+
// Persist subagent task lifecycle in durable storage when available
|
|
3410
|
+
if (this.store?.hasWorkerResultsFeature()) {
|
|
3411
|
+
try {
|
|
3412
|
+
workerResultId = this.store.createWorkerResult(agentId, task.slice(0, 500), resolvedModel || 'default');
|
|
3413
|
+
}
|
|
3414
|
+
catch (storeErr) {
|
|
3415
|
+
this.observability?.logger?.warn('Failed to create worker result record', {
|
|
3416
|
+
agentId,
|
|
3417
|
+
error: storeErr.message,
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
// Get subagent config with agent-type-specific timeouts and iteration limits
|
|
3422
|
+
// Uses dynamic configuration based on agent type (researcher needs more time than reviewer)
|
|
3054
3423
|
const subagentConfig = this.config.subagent;
|
|
3055
3424
|
const hasSubagentConfig = subagentConfig !== false && subagentConfig !== undefined;
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3425
|
+
// Agent-type-specific timeout: researchers get 5min, reviewers get 2min, etc.
|
|
3426
|
+
const agentTypeTimeout = getSubagentTimeout(agentName);
|
|
3427
|
+
const configTimeout = hasSubagentConfig
|
|
3428
|
+
? subagentConfig.defaultTimeout
|
|
3429
|
+
: undefined;
|
|
3430
|
+
const subagentTimeout = configTimeout ?? agentTypeTimeout;
|
|
3431
|
+
// Agent-type-specific iteration limit: researchers get 25, documenters get 10, etc.
|
|
3432
|
+
const agentTypeMaxIter = getSubagentMaxIterations(agentName);
|
|
3433
|
+
const configMaxIter = hasSubagentConfig
|
|
3434
|
+
? subagentConfig.defaultMaxIterations
|
|
3435
|
+
: undefined;
|
|
3436
|
+
const defaultMaxIterations = agentDef.maxIterations ?? configMaxIter ?? agentTypeMaxIter;
|
|
3437
|
+
// BLACKBOARD CONTEXT INJECTION
|
|
3438
|
+
// Gather relevant context from the blackboard for the subagent
|
|
3439
|
+
let blackboardContext = '';
|
|
3440
|
+
const parentAgentId = `parent-${Date.now()}`;
|
|
3441
|
+
if (this.blackboard) {
|
|
3442
|
+
// Post parent's exploration context before spawning
|
|
3443
|
+
this.blackboard.post(parentAgentId, {
|
|
3444
|
+
topic: 'spawn.parent_context',
|
|
3445
|
+
content: `Parent spawning ${agentName} for task: ${task.slice(0, 200)}`,
|
|
3446
|
+
type: 'progress',
|
|
3447
|
+
confidence: 1,
|
|
3448
|
+
metadata: { agentName, taskPreview: task.slice(0, 100) },
|
|
3449
|
+
});
|
|
3450
|
+
// Gather recent findings that might help the subagent
|
|
3451
|
+
const recentFindings = this.blackboard.query({
|
|
3452
|
+
limit: 5,
|
|
3453
|
+
types: ['discovery', 'analysis', 'progress'],
|
|
3454
|
+
minConfidence: 0.7,
|
|
3455
|
+
});
|
|
3456
|
+
if (recentFindings.length > 0) {
|
|
3457
|
+
const findingsSummary = recentFindings
|
|
3458
|
+
.map(f => `- [${f.agentId}] ${f.topic}: ${f.content.slice(0, 150)}${f.content.length > 150 ? '...' : ''}`)
|
|
3459
|
+
.join('\n');
|
|
3460
|
+
blackboardContext = `\n\n**BLACKBOARD CONTEXT (from parent/sibling agents):**\n${findingsSummary}\n`;
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
// Check for files already being modified in parent's pending plan
|
|
3464
|
+
const currentPlan = this.pendingPlanManager.getPendingPlan();
|
|
3465
|
+
if (currentPlan && currentPlan.proposedChanges.length > 0) {
|
|
3466
|
+
const pendingFiles = currentPlan.proposedChanges
|
|
3467
|
+
.filter((c) => c.tool === 'write_file' || c.tool === 'edit_file')
|
|
3468
|
+
.map((c) => c.args.path || c.args.file_path)
|
|
3469
|
+
.filter(Boolean);
|
|
3470
|
+
if (pendingFiles.length > 0) {
|
|
3471
|
+
blackboardContext += `\n**FILES ALREADY IN PENDING PLAN (do not duplicate):**\n${pendingFiles.slice(0, 10).join('\n')}\n`;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
// CONSTRAINT INJECTION
|
|
3475
|
+
// Add constraints to the subagent's context if provided
|
|
3476
|
+
// Also always include budget awareness so subagents know their limits
|
|
3477
|
+
const constraintParts = [];
|
|
3478
|
+
// BUDGET AWARENESS: Always inject so subagent understands its limits
|
|
3479
|
+
const subagentBudgetTokens = constraints?.maxTokens ?? SUBAGENT_BUDGET.maxTokens ?? 100000;
|
|
3480
|
+
const subagentBudgetMinutes = Math.round((SUBAGENT_BUDGET.maxDuration ?? 240000) / 60000);
|
|
3481
|
+
constraintParts.push(`**RESOURCE AWARENESS (CRITICAL):**\n` +
|
|
3482
|
+
`- Token budget: ~${(subagentBudgetTokens / 1000).toFixed(0)}k tokens\n` +
|
|
3483
|
+
`- Time limit: ~${subagentBudgetMinutes} minutes\n` +
|
|
3484
|
+
`- You will receive warnings at 70% usage. When warned, WRAP UP immediately.\n` +
|
|
3485
|
+
`- Do not explore indefinitely - be focused and efficient.\n` +
|
|
3486
|
+
`- If approaching limits, summarize findings and return.\n` +
|
|
3487
|
+
`- **STRUCTURED WRAPUP:** When told to wrap up, respond with ONLY this JSON (no tool calls):\n` +
|
|
3488
|
+
` {"findings":[...], "actionsTaken":[...], "failures":[...], "remainingWork":[...], "suggestedNextSteps":[...]}`);
|
|
3489
|
+
if (constraints) {
|
|
3490
|
+
if (constraints.focusAreas && constraints.focusAreas.length > 0) {
|
|
3491
|
+
constraintParts.push(`**FOCUS AREAS (limit exploration to these paths):**\n${constraints.focusAreas.map(a => ` - ${a}`).join('\n')}`);
|
|
3492
|
+
}
|
|
3493
|
+
if (constraints.excludeAreas && constraints.excludeAreas.length > 0) {
|
|
3494
|
+
constraintParts.push(`**EXCLUDED AREAS (do NOT explore these):**\n${constraints.excludeAreas.map(a => ` - ${a}`).join('\n')}`);
|
|
3495
|
+
}
|
|
3496
|
+
if (constraints.requiredDeliverables && constraints.requiredDeliverables.length > 0) {
|
|
3497
|
+
constraintParts.push(`**REQUIRED DELIVERABLES (you must produce these):**\n${constraints.requiredDeliverables.map(d => ` - ${d}`).join('\n')}`);
|
|
3498
|
+
}
|
|
3499
|
+
if (constraints.timeboxMinutes) {
|
|
3500
|
+
constraintParts.push(`**TIME LIMIT:** ${constraints.timeboxMinutes} minutes (soft limit - wrap up if approaching)`);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
const constraintContext = `\n\n**EXECUTION CONSTRAINTS:**\n${constraintParts.join('\n\n')}\n`;
|
|
3504
|
+
// Build subagent system prompt with subagent-specific plan mode addition
|
|
3505
|
+
const parentMode = this.getMode();
|
|
3506
|
+
const subagentSystemPrompt = parentMode === 'plan'
|
|
3507
|
+
? `${agentDef.systemPrompt}\n\n${SUBAGENT_PLAN_MODE_ADDITION}${blackboardContext}${constraintContext}`
|
|
3508
|
+
: `${agentDef.systemPrompt}${blackboardContext}${constraintContext}`;
|
|
3062
3509
|
// Create a sub-agent with the agent's config
|
|
3510
|
+
// Use SUBAGENT_BUDGET to constrain resource usage (prevents runaway token consumption)
|
|
3063
3511
|
const subAgent = new ProductionAgent({
|
|
3064
3512
|
provider: this.provider,
|
|
3065
3513
|
tools: agentTools,
|
|
@@ -3067,7 +3515,7 @@ export class ProductionAgent {
|
|
|
3067
3515
|
toolResolver: this.toolResolver || undefined,
|
|
3068
3516
|
// Pass MCP tool summaries so subagent knows what tools are available
|
|
3069
3517
|
mcpToolSummaries: this.config.mcpToolSummaries,
|
|
3070
|
-
systemPrompt:
|
|
3518
|
+
systemPrompt: subagentSystemPrompt,
|
|
3071
3519
|
model: resolvedModel,
|
|
3072
3520
|
maxIterations: agentDef.maxIterations || defaultMaxIterations,
|
|
3073
3521
|
// Inherit some features but keep subagent simpler
|
|
@@ -3087,32 +3535,89 @@ export class ProductionAgent {
|
|
|
3087
3535
|
},
|
|
3088
3536
|
// Share parent's blackboard for coordination between parallel subagents
|
|
3089
3537
|
blackboard: this.blackboard || undefined,
|
|
3538
|
+
// CONSTRAINED BUDGET: Subagents get smaller budget to prevent runaway consumption
|
|
3539
|
+
// Uses SUBAGENT_BUDGET (100k tokens, 4 min) vs STANDARD_BUDGET (200k, 5 min)
|
|
3540
|
+
budget: constraints?.maxTokens
|
|
3541
|
+
? { ...SUBAGENT_BUDGET, maxTokens: constraints.maxTokens }
|
|
3542
|
+
: SUBAGENT_BUDGET,
|
|
3090
3543
|
});
|
|
3091
3544
|
// CRITICAL: Subagent inherits parent's mode
|
|
3092
3545
|
// This ensures that if parent is in plan mode:
|
|
3093
3546
|
// - Subagent's read operations execute immediately (visible exploration)
|
|
3094
3547
|
// - Subagent's write operations get queued in the subagent's pending plan
|
|
3095
3548
|
// - User maintains control over what actually gets written
|
|
3096
|
-
const parentMode = this.getMode();
|
|
3097
3549
|
if (parentMode !== 'build') {
|
|
3098
3550
|
subAgent.setMode(parentMode);
|
|
3099
3551
|
}
|
|
3100
3552
|
// Pass parent's iteration count to subagent for accurate budget tracking
|
|
3101
3553
|
// This prevents subagents from consuming excessive iterations when parent already used many
|
|
3102
3554
|
subAgent.setParentIterations(this.getTotalIterations());
|
|
3103
|
-
//
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3555
|
+
// UNIFIED TRACING: Share parent's trace collector with subagent context
|
|
3556
|
+
// This ensures all subagent events are written to the same trace file as the parent,
|
|
3557
|
+
// tagged with subagent context for proper aggregation in /trace output
|
|
3558
|
+
if (this.traceCollector) {
|
|
3559
|
+
const subagentTraceView = this.traceCollector.createSubagentView({
|
|
3560
|
+
parentSessionId: this.traceCollector.getSessionId() || 'unknown',
|
|
3561
|
+
agentType: agentName,
|
|
3562
|
+
spawnedAtIteration: this.state.iteration,
|
|
3563
|
+
});
|
|
3564
|
+
subAgent.setTraceCollector(subagentTraceView);
|
|
3565
|
+
}
|
|
3566
|
+
// GRACEFUL TIMEOUT with WRAPUP PHASE
|
|
3567
|
+
// Instead of instant death on timeout, the subagent gets a wrapup window
|
|
3568
|
+
// to produce a structured summary before being killed:
|
|
3569
|
+
// 1. Normal operation: progress extends idle timer
|
|
3570
|
+
// 2. Wrapup phase: 30s before hard kill, wrapup callback fires → forceTextOnly
|
|
3571
|
+
// 3. Hard kill: race() throws CancellationError after wrapup window
|
|
3572
|
+
const IDLE_TIMEOUT = 120000; // 2 minutes without progress = timeout
|
|
3573
|
+
let WRAPUP_WINDOW = 30000;
|
|
3574
|
+
let IDLE_CHECK_INTERVAL = 5000;
|
|
3575
|
+
if (this.config.subagent) {
|
|
3576
|
+
WRAPUP_WINDOW = this.config.subagent.wrapupWindowMs ?? WRAPUP_WINDOW;
|
|
3577
|
+
IDLE_CHECK_INTERVAL = this.config.subagent.idleCheckIntervalMs ?? IDLE_CHECK_INTERVAL;
|
|
3578
|
+
}
|
|
3579
|
+
const progressAwareTimeout = createGracefulTimeout(subagentTimeout, // Max total time (hard limit from agent type config)
|
|
3580
|
+
IDLE_TIMEOUT, // Idle timeout (soft limit - no progress triggers this)
|
|
3581
|
+
WRAPUP_WINDOW, // Wrapup window before hard kill
|
|
3582
|
+
IDLE_CHECK_INTERVAL);
|
|
3583
|
+
// Register wrapup callback — fires 30s before hard kill
|
|
3584
|
+
// This triggers the subagent's forceTextOnly path for a structured summary
|
|
3585
|
+
progressAwareTimeout.onWrapupWarning(() => {
|
|
3586
|
+
this.emit({
|
|
3587
|
+
type: 'subagent.wrapup.started',
|
|
3588
|
+
agentId,
|
|
3589
|
+
agentType: agentName,
|
|
3590
|
+
reason: 'Timeout approaching - graceful wrapup window opened',
|
|
3591
|
+
elapsedMs: Date.now() - startTime,
|
|
3592
|
+
});
|
|
3593
|
+
subAgent.requestWrapup('Timeout approaching — produce structured summary');
|
|
3594
|
+
});
|
|
3595
|
+
// Forward events from subagent with context (track for cleanup)
|
|
3596
|
+
// Also report progress to the timeout tracker
|
|
3597
|
+
const unsubSubAgent = subAgent.subscribe(event => {
|
|
3598
|
+
// Tag event with subagent source AND unique ID so TUI can properly attribute
|
|
3599
|
+
// events to the specific agent instance (critical for multiple same-type agents)
|
|
3600
|
+
const taggedEvent = { ...event, subagent: agentName, subagentId: agentId };
|
|
3107
3601
|
this.emit(taggedEvent);
|
|
3602
|
+
// Report progress for timeout extension
|
|
3603
|
+
// Progress events: tool calls, LLM responses, token updates
|
|
3604
|
+
const progressEvents = ['tool.start', 'tool.complete', 'llm.start', 'llm.complete'];
|
|
3605
|
+
if (progressEvents.includes(event.type)) {
|
|
3606
|
+
progressAwareTimeout.reportProgress();
|
|
3607
|
+
}
|
|
3108
3608
|
});
|
|
3109
|
-
//
|
|
3110
|
-
const timeoutSource = createTimeoutToken(subagentTimeout);
|
|
3111
|
-
// Link parent's cancellation with subagent timeout so ESC propagates to subagents
|
|
3609
|
+
// Link parent's cancellation with progress-aware timeout so ESC propagates to subagents
|
|
3112
3610
|
const parentSource = this.cancellation?.getSource();
|
|
3113
3611
|
const effectiveSource = parentSource
|
|
3114
|
-
? createLinkedToken(parentSource,
|
|
3115
|
-
:
|
|
3612
|
+
? createLinkedToken(parentSource, progressAwareTimeout)
|
|
3613
|
+
: progressAwareTimeout;
|
|
3614
|
+
// CRITICAL: Pass the cancellation token to the subagent so it can check and stop
|
|
3615
|
+
// gracefully when timeout fires. Without this, the subagent continues running as
|
|
3616
|
+
// a "zombie" even after race() returns with a timeout error.
|
|
3617
|
+
subAgent.setExternalCancellation(effectiveSource.token);
|
|
3618
|
+
// Pause parent's duration timer while subagent runs to prevent
|
|
3619
|
+
// the parent from timing out on wall-clock while waiting for subagent
|
|
3620
|
+
this.economics?.pauseDuration();
|
|
3116
3621
|
try {
|
|
3117
3622
|
// Run the task with cancellation propagation from parent
|
|
3118
3623
|
const result = await race(subAgent.run(task), effectiveSource.token);
|
|
@@ -3165,6 +3670,8 @@ export class ProductionAgent {
|
|
|
3165
3670
|
const finalOutput = queuedChangeSummary
|
|
3166
3671
|
? (result.response || '') + queuedChangeSummary
|
|
3167
3672
|
: (result.response || result.error || '');
|
|
3673
|
+
// Parse structured closure report from agent's response (if it produced one)
|
|
3674
|
+
const structured = parseStructuredClosureReport(result.response || '', 'completed');
|
|
3168
3675
|
const spawnResultFinal = {
|
|
3169
3676
|
success: result.success,
|
|
3170
3677
|
output: finalOutput,
|
|
@@ -3173,13 +3680,43 @@ export class ProductionAgent {
|
|
|
3173
3680
|
duration,
|
|
3174
3681
|
toolCalls: result.metrics.toolCalls,
|
|
3175
3682
|
},
|
|
3683
|
+
structured,
|
|
3176
3684
|
};
|
|
3685
|
+
if (workerResultId && this.store?.hasWorkerResultsFeature()) {
|
|
3686
|
+
try {
|
|
3687
|
+
this.store.completeWorkerResult(workerResultId, {
|
|
3688
|
+
fullOutput: finalOutput,
|
|
3689
|
+
summary: finalOutput.slice(0, 500),
|
|
3690
|
+
artifacts: structured ? [{ type: 'structured_report', data: structured }] : undefined,
|
|
3691
|
+
metrics: {
|
|
3692
|
+
tokens: result.metrics.totalTokens,
|
|
3693
|
+
duration,
|
|
3694
|
+
toolCalls: result.metrics.toolCalls,
|
|
3695
|
+
},
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
catch (storeErr) {
|
|
3699
|
+
this.observability?.logger?.warn('Failed to persist worker result', {
|
|
3700
|
+
agentId,
|
|
3701
|
+
error: storeErr.message,
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3177
3705
|
this.emit({
|
|
3178
3706
|
type: 'agent.complete',
|
|
3179
|
-
agentId
|
|
3707
|
+
agentId, // Use unique spawn ID for precise tracking
|
|
3708
|
+
agentType: agentName, // Keep type for display purposes
|
|
3180
3709
|
success: result.success,
|
|
3181
3710
|
output: finalOutput.slice(0, 500), // Include output preview
|
|
3182
3711
|
});
|
|
3712
|
+
if (progressAwareTimeout.isInWrapupPhase()) {
|
|
3713
|
+
this.emit({
|
|
3714
|
+
type: 'subagent.wrapup.completed',
|
|
3715
|
+
agentId,
|
|
3716
|
+
agentType: agentName,
|
|
3717
|
+
elapsedMs: Date.now() - startTime,
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3183
3720
|
// Enhanced tracing: Record subagent completion
|
|
3184
3721
|
this.traceCollector?.record({
|
|
3185
3722
|
type: 'subagent.link',
|
|
@@ -3206,6 +3743,8 @@ export class ProductionAgent {
|
|
|
3206
3743
|
},
|
|
3207
3744
|
},
|
|
3208
3745
|
});
|
|
3746
|
+
// Unsubscribe from subagent events before cleanup
|
|
3747
|
+
unsubSubAgent();
|
|
3209
3748
|
await subAgent.cleanup();
|
|
3210
3749
|
// Cache result for duplicate spawn prevention
|
|
3211
3750
|
// Use the same taskKey from the dedup check above
|
|
@@ -3223,8 +3762,31 @@ export class ProductionAgent {
|
|
|
3223
3762
|
const isUserCancellation = parentSource?.isCancellationRequested;
|
|
3224
3763
|
const reason = isUserCancellation
|
|
3225
3764
|
? 'User cancelled'
|
|
3226
|
-
: `Timed out after ${subagentTimeout}ms`;
|
|
3227
|
-
this.emit({ type: 'agent.error', agentId: agentName, error: reason });
|
|
3765
|
+
: err.reason || `Timed out after ${subagentTimeout}ms`;
|
|
3766
|
+
this.emit({ type: 'agent.error', agentId, agentType: agentName, error: reason });
|
|
3767
|
+
if (!isUserCancellation) {
|
|
3768
|
+
this.emit({
|
|
3769
|
+
type: 'subagent.timeout.hard_kill',
|
|
3770
|
+
agentId,
|
|
3771
|
+
agentType: agentName,
|
|
3772
|
+
reason,
|
|
3773
|
+
elapsedMs: Date.now() - startTime,
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
// =======================================================================
|
|
3777
|
+
// PRESERVE PARTIAL RESULTS
|
|
3778
|
+
// Instead of discarding all work, capture whatever the subagent produced
|
|
3779
|
+
// before timeout. This prevents the "zombie agent" problem where tokens
|
|
3780
|
+
// are consumed but results are lost.
|
|
3781
|
+
// =======================================================================
|
|
3782
|
+
const subagentState = subAgent.getState();
|
|
3783
|
+
const subagentMetrics = subAgent.getMetrics();
|
|
3784
|
+
// Extract partial response from the last assistant message
|
|
3785
|
+
const assistantMessages = subagentState.messages.filter(m => m.role === 'assistant');
|
|
3786
|
+
const lastAssistantMsg = assistantMessages[assistantMessages.length - 1];
|
|
3787
|
+
const partialResponse = typeof lastAssistantMsg?.content === 'string'
|
|
3788
|
+
? lastAssistantMsg.content
|
|
3789
|
+
: '';
|
|
3228
3790
|
// Extract pending plan before cleanup (even on cancellation, preserve any queued work)
|
|
3229
3791
|
let cancelledQueuedSummary = '';
|
|
3230
3792
|
if (subAgent.hasPendingPlan()) {
|
|
@@ -3260,33 +3822,99 @@ export class ProductionAgent {
|
|
|
3260
3822
|
this.pendingPlanManager.appendExplorationFinding(`[${agentName}] ${subPlan.explorationSummary}`);
|
|
3261
3823
|
}
|
|
3262
3824
|
}
|
|
3263
|
-
//
|
|
3825
|
+
// Unsubscribe from subagent events and cleanup gracefully
|
|
3826
|
+
unsubSubAgent();
|
|
3264
3827
|
try {
|
|
3265
3828
|
await subAgent.cleanup();
|
|
3266
3829
|
}
|
|
3267
3830
|
catch {
|
|
3268
3831
|
// Ignore cleanup errors on cancellation
|
|
3269
3832
|
}
|
|
3833
|
+
// Build output message with partial results
|
|
3270
3834
|
const baseOutput = isUserCancellation
|
|
3271
3835
|
? `Subagent '${agentName}' was cancelled by user.`
|
|
3272
|
-
: `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s
|
|
3836
|
+
: `Subagent '${agentName}' timed out after ${Math.round(subagentTimeout / 1000)}s.`;
|
|
3837
|
+
// Include partial response if we have one
|
|
3838
|
+
const partialResultSection = partialResponse
|
|
3839
|
+
? `\n\n[PARTIAL RESULTS BEFORE TIMEOUT]\n${partialResponse.slice(0, 2000)}${partialResponse.length > 2000 ? '...(truncated)' : ''}`
|
|
3840
|
+
: '';
|
|
3841
|
+
// Enhanced tracing: Record subagent timeout with partial results
|
|
3842
|
+
this.traceCollector?.record({
|
|
3843
|
+
type: 'subagent.link',
|
|
3844
|
+
data: {
|
|
3845
|
+
parentSessionId: this.traceCollector.getSessionId() || 'unknown',
|
|
3846
|
+
childSessionId,
|
|
3847
|
+
childTraceId,
|
|
3848
|
+
childConfig: {
|
|
3849
|
+
agentType: agentName,
|
|
3850
|
+
model: resolvedModel || 'default',
|
|
3851
|
+
task,
|
|
3852
|
+
tools: agentTools.map(t => t.name),
|
|
3853
|
+
},
|
|
3854
|
+
spawnContext: {
|
|
3855
|
+
reason: `Delegated task: ${task.slice(0, 100)}`,
|
|
3856
|
+
expectedOutcome: agentDef.description,
|
|
3857
|
+
parentIteration: this.state.iteration,
|
|
3858
|
+
},
|
|
3859
|
+
result: {
|
|
3860
|
+
success: false,
|
|
3861
|
+
summary: `[TIMEOUT] ${baseOutput}\n${partialResponse.slice(0, 200)}`,
|
|
3862
|
+
tokensUsed: subagentMetrics.totalTokens,
|
|
3863
|
+
durationMs: duration,
|
|
3864
|
+
},
|
|
3865
|
+
},
|
|
3866
|
+
});
|
|
3867
|
+
// Parse structured closure report from partial response
|
|
3868
|
+
const exitReason = isUserCancellation ? 'cancelled' : 'timeout_graceful';
|
|
3869
|
+
const structured = parseStructuredClosureReport(partialResponse, exitReason, task);
|
|
3870
|
+
if (workerResultId && this.store?.hasWorkerResultsFeature()) {
|
|
3871
|
+
try {
|
|
3872
|
+
this.store.failWorkerResult(workerResultId, reason);
|
|
3873
|
+
}
|
|
3874
|
+
catch (storeErr) {
|
|
3875
|
+
this.observability?.logger?.warn('Failed to mark cancelled worker result as failed', {
|
|
3876
|
+
agentId,
|
|
3877
|
+
error: storeErr.message,
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3273
3881
|
return {
|
|
3274
3882
|
success: false,
|
|
3275
|
-
output: baseOutput + cancelledQueuedSummary,
|
|
3276
|
-
|
|
3883
|
+
output: baseOutput + partialResultSection + cancelledQueuedSummary,
|
|
3884
|
+
// IMPORTANT: Use actual metrics instead of zeros
|
|
3885
|
+
// This ensures accurate token tracking in /trace output
|
|
3886
|
+
metrics: {
|
|
3887
|
+
tokens: subagentMetrics.totalTokens,
|
|
3888
|
+
duration,
|
|
3889
|
+
toolCalls: subagentMetrics.toolCalls,
|
|
3890
|
+
},
|
|
3891
|
+
structured,
|
|
3277
3892
|
};
|
|
3278
3893
|
}
|
|
3279
3894
|
throw err; // Re-throw non-cancellation errors
|
|
3280
3895
|
}
|
|
3281
3896
|
finally {
|
|
3897
|
+
// Resume parent's duration timer now that subagent is done
|
|
3898
|
+
this.economics?.resumeDuration();
|
|
3282
3899
|
// Dispose both sources (linked source disposes its internal state, timeout source handles its timer)
|
|
3283
3900
|
effectiveSource.dispose();
|
|
3284
|
-
|
|
3901
|
+
progressAwareTimeout.dispose();
|
|
3285
3902
|
}
|
|
3286
3903
|
}
|
|
3287
3904
|
catch (err) {
|
|
3288
3905
|
const error = err instanceof Error ? err.message : String(err);
|
|
3289
|
-
this.emit({ type: 'agent.error', agentId: agentName, error });
|
|
3906
|
+
this.emit({ type: 'agent.error', agentId, agentType: agentName, error });
|
|
3907
|
+
if (workerResultId && this.store?.hasWorkerResultsFeature()) {
|
|
3908
|
+
try {
|
|
3909
|
+
this.store.failWorkerResult(workerResultId, error);
|
|
3910
|
+
}
|
|
3911
|
+
catch (storeErr) {
|
|
3912
|
+
this.observability?.logger?.warn('Failed to mark worker result as failed', {
|
|
3913
|
+
agentId,
|
|
3914
|
+
error: storeErr.message,
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3290
3918
|
return {
|
|
3291
3919
|
success: false,
|
|
3292
3920
|
output: `Agent error: ${error}`,
|
|
@@ -3297,6 +3925,9 @@ export class ProductionAgent {
|
|
|
3297
3925
|
/**
|
|
3298
3926
|
* Spawn multiple agents in parallel to work on independent tasks.
|
|
3299
3927
|
* Uses the shared blackboard for coordination and conflict prevention.
|
|
3928
|
+
*
|
|
3929
|
+
* Uses Promise.allSettled to handle partial failures gracefully - if one
|
|
3930
|
+
* agent fails or times out, others can still complete successfully.
|
|
3300
3931
|
*/
|
|
3301
3932
|
async spawnAgentsParallel(tasks) {
|
|
3302
3933
|
// Emit start event for TUI visibility
|
|
@@ -3305,9 +3936,28 @@ export class ProductionAgent {
|
|
|
3305
3936
|
count: tasks.length,
|
|
3306
3937
|
agents: tasks.map(t => t.agent),
|
|
3307
3938
|
});
|
|
3308
|
-
// Execute all tasks in parallel
|
|
3939
|
+
// Execute all tasks in parallel using allSettled to handle partial failures
|
|
3309
3940
|
const promises = tasks.map(({ agent, task }) => this.spawnAgent(agent, task));
|
|
3310
|
-
const
|
|
3941
|
+
const settled = await Promise.allSettled(promises);
|
|
3942
|
+
// Convert settled results to SpawnResult array
|
|
3943
|
+
const results = settled.map((result, i) => {
|
|
3944
|
+
if (result.status === 'fulfilled') {
|
|
3945
|
+
return result.value;
|
|
3946
|
+
}
|
|
3947
|
+
// Handle rejected promises (shouldn't happen since spawnAgent catches errors internally,
|
|
3948
|
+
// but this is a safety net for unexpected failures)
|
|
3949
|
+
const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
3950
|
+
this.emit({
|
|
3951
|
+
type: 'agent.error',
|
|
3952
|
+
agentId: tasks[i].agent,
|
|
3953
|
+
error: `Unexpected parallel spawn error: ${error}`,
|
|
3954
|
+
});
|
|
3955
|
+
return {
|
|
3956
|
+
success: false,
|
|
3957
|
+
output: `Parallel spawn error: ${error}`,
|
|
3958
|
+
metrics: { tokens: 0, duration: 0, toolCalls: 0 },
|
|
3959
|
+
};
|
|
3960
|
+
});
|
|
3311
3961
|
// Emit completion event
|
|
3312
3962
|
this.emit({
|
|
3313
3963
|
type: 'parallel.spawn.complete',
|
|
@@ -3456,7 +4106,7 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3456
4106
|
const topSuggestion = suggestions[0];
|
|
3457
4107
|
// If confirmation callback provided, ask user
|
|
3458
4108
|
if (confirmDelegate && topSuggestion) {
|
|
3459
|
-
const confirmed = await confirmDelegate(topSuggestion.agent, topSuggestion.reason);
|
|
4109
|
+
const confirmed = await this.withPausedDuration(() => confirmDelegate(topSuggestion.agent, topSuggestion.reason));
|
|
3460
4110
|
if (!confirmed) {
|
|
3461
4111
|
// User declined, run with main agent
|
|
3462
4112
|
return this.run(task);
|
|
@@ -3513,6 +4163,14 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3513
4163
|
getResourceStatus() {
|
|
3514
4164
|
return this.resourceManager?.getStatusString() || null;
|
|
3515
4165
|
}
|
|
4166
|
+
/**
|
|
4167
|
+
* Reset CPU time counter for the resource manager.
|
|
4168
|
+
* Call this when starting a new prompt to allow per-prompt time limits
|
|
4169
|
+
* instead of session-wide limits.
|
|
4170
|
+
*/
|
|
4171
|
+
resetResourceTimer() {
|
|
4172
|
+
this.resourceManager?.resetCpuTime();
|
|
4173
|
+
}
|
|
3516
4174
|
// =========================================================================
|
|
3517
4175
|
// LSP (LANGUAGE SERVER) METHODS
|
|
3518
4176
|
// =========================================================================
|
|
@@ -3663,6 +4321,37 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3663
4321
|
setParentIterations(count) {
|
|
3664
4322
|
this.parentIterations = count;
|
|
3665
4323
|
}
|
|
4324
|
+
/**
|
|
4325
|
+
* Set an external cancellation token for this agent.
|
|
4326
|
+
* Used when spawning subagents to propagate parent timeout/cancellation.
|
|
4327
|
+
* The agent will check this token in its main loop and stop gracefully
|
|
4328
|
+
* when cancellation is requested, preserving partial results.
|
|
4329
|
+
*/
|
|
4330
|
+
setExternalCancellation(token) {
|
|
4331
|
+
this.externalCancellationToken = token;
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Set a SQLite store instance for durable persistence features.
|
|
4335
|
+
*/
|
|
4336
|
+
setStore(store) {
|
|
4337
|
+
this.store = store;
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Check if external cancellation has been requested.
|
|
4341
|
+
* Returns true if the external token signals cancellation.
|
|
4342
|
+
*/
|
|
4343
|
+
isExternallyCancelled() {
|
|
4344
|
+
return this.externalCancellationToken?.isCancellationRequested ?? false;
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* Request a graceful wrapup of the agent's current work.
|
|
4348
|
+
* On the next main loop iteration, the agent will produce a structured summary
|
|
4349
|
+
* instead of making more tool calls.
|
|
4350
|
+
*/
|
|
4351
|
+
requestWrapup(reason) {
|
|
4352
|
+
this.wrapupRequested = true;
|
|
4353
|
+
this.wrapupReason = reason || 'Timeout approaching';
|
|
4354
|
+
}
|
|
3666
4355
|
/**
|
|
3667
4356
|
* Get total iterations (this agent + parent).
|
|
3668
4357
|
* Used for accurate budget tracking across subagent hierarchies.
|
|
@@ -3844,6 +4533,12 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3844
4533
|
getAgentRegistry() {
|
|
3845
4534
|
return this.agentRegistry;
|
|
3846
4535
|
}
|
|
4536
|
+
/**
|
|
4537
|
+
* Get the task manager instance for task tracking.
|
|
4538
|
+
*/
|
|
4539
|
+
getTaskManager() {
|
|
4540
|
+
return this.taskManager;
|
|
4541
|
+
}
|
|
3847
4542
|
/**
|
|
3848
4543
|
* Get all loaded skills.
|
|
3849
4544
|
*/
|
|
@@ -3930,6 +4625,29 @@ If the task is a simple question or doesn't need specialized handling, set bestA
|
|
|
3930
4625
|
* Cleanup resources.
|
|
3931
4626
|
*/
|
|
3932
4627
|
async cleanup() {
|
|
4628
|
+
// Unsubscribe all event listeners (prevents memory leaks in long sessions)
|
|
4629
|
+
for (const unsub of this.unsubscribers) {
|
|
4630
|
+
try {
|
|
4631
|
+
unsub();
|
|
4632
|
+
}
|
|
4633
|
+
catch {
|
|
4634
|
+
// Ignore unsubscribe errors during cleanup
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
this.unsubscribers = [];
|
|
4638
|
+
// Flush trace collector before cleanup
|
|
4639
|
+
await this.traceCollector?.flush();
|
|
4640
|
+
// Clear blackboard (releases file claim locks)
|
|
4641
|
+
this.blackboard?.clear();
|
|
4642
|
+
// Wait for any pending init before cleanup
|
|
4643
|
+
if (this.initPromises.length > 0) {
|
|
4644
|
+
try {
|
|
4645
|
+
await Promise.all(this.initPromises);
|
|
4646
|
+
}
|
|
4647
|
+
catch {
|
|
4648
|
+
// Ignore init errors during cleanup
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
3933
4651
|
this.cancellation?.cleanup();
|
|
3934
4652
|
this.resourceManager?.cleanup();
|
|
3935
4653
|
await this.lspManager?.cleanup();
|
|
@@ -4137,4 +4855,64 @@ export class ProductionAgentBuilder {
|
|
|
4137
4855
|
export function buildAgent() {
|
|
4138
4856
|
return new ProductionAgentBuilder();
|
|
4139
4857
|
}
|
|
4858
|
+
// =============================================================================
|
|
4859
|
+
// STRUCTURED CLOSURE REPORT PARSER
|
|
4860
|
+
// =============================================================================
|
|
4861
|
+
/**
|
|
4862
|
+
* Parse a structured closure report from a subagent's text response.
|
|
4863
|
+
* The subagent may have produced JSON in response to a TIMEOUT_WRAPUP_PROMPT.
|
|
4864
|
+
*
|
|
4865
|
+
* @param text - The subagent's last response text
|
|
4866
|
+
* @param defaultExitReason - Exit reason to use (completed, timeout_graceful, cancelled, etc.)
|
|
4867
|
+
* @param fallbackTask - Original task description for fallback remainingWork
|
|
4868
|
+
* @returns Parsed StructuredClosureReport, or undefined if no JSON found and no fallback needed
|
|
4869
|
+
*/
|
|
4870
|
+
export function parseStructuredClosureReport(text, defaultExitReason, fallbackTask) {
|
|
4871
|
+
if (!text) {
|
|
4872
|
+
// No text at all — create a hard timeout fallback if we have a task
|
|
4873
|
+
if (fallbackTask) {
|
|
4874
|
+
return {
|
|
4875
|
+
findings: [],
|
|
4876
|
+
actionsTaken: [],
|
|
4877
|
+
failures: ['Timeout before producing structured summary'],
|
|
4878
|
+
remainingWork: [fallbackTask],
|
|
4879
|
+
exitReason: 'timeout_hard',
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
return undefined;
|
|
4883
|
+
}
|
|
4884
|
+
try {
|
|
4885
|
+
// Try to extract JSON from the response
|
|
4886
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
4887
|
+
if (jsonMatch) {
|
|
4888
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4889
|
+
// Validate that it looks like a closure report (has at least one expected field)
|
|
4890
|
+
if (parsed.findings || parsed.actionsTaken || parsed.failures || parsed.remainingWork) {
|
|
4891
|
+
return {
|
|
4892
|
+
findings: Array.isArray(parsed.findings) ? parsed.findings : [],
|
|
4893
|
+
actionsTaken: Array.isArray(parsed.actionsTaken) ? parsed.actionsTaken : [],
|
|
4894
|
+
failures: Array.isArray(parsed.failures) ? parsed.failures : [],
|
|
4895
|
+
remainingWork: Array.isArray(parsed.remainingWork) ? parsed.remainingWork : [],
|
|
4896
|
+
exitReason: defaultExitReason,
|
|
4897
|
+
suggestedNextSteps: Array.isArray(parsed.suggestedNextSteps) ? parsed.suggestedNextSteps : undefined,
|
|
4898
|
+
};
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
catch {
|
|
4903
|
+
// JSON parse failed — fall through to fallback
|
|
4904
|
+
}
|
|
4905
|
+
// Fallback: LLM didn't produce valid JSON but we have text
|
|
4906
|
+
if (defaultExitReason !== 'completed') {
|
|
4907
|
+
return {
|
|
4908
|
+
findings: [text.slice(0, 500)],
|
|
4909
|
+
actionsTaken: [],
|
|
4910
|
+
failures: ['Did not produce structured JSON summary'],
|
|
4911
|
+
remainingWork: fallbackTask ? [fallbackTask] : [],
|
|
4912
|
+
exitReason: defaultExitReason === 'timeout_graceful' ? 'timeout_hard' : defaultExitReason,
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
// For completed agents, don't force a structured report if they didn't produce one
|
|
4916
|
+
return undefined;
|
|
4917
|
+
}
|
|
4140
4918
|
//# sourceMappingURL=agent.js.map
|