minovative-mind-cli 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +51 -45
  2. package/dist/commands/chat.js +7 -2
  3. package/dist/services/agent/slashCommands.js +156 -30
  4. package/dist/services/agent/toolLoop.d.ts +1 -1
  5. package/dist/services/agent/toolLoop.js +7 -2
  6. package/dist/services/agent/types.d.ts +2 -0
  7. package/dist/services/agent-tools.d.ts +9 -4
  8. package/dist/services/agent-tools.js +145 -21
  9. package/dist/services/agent.d.ts +8 -0
  10. package/dist/services/agent.js +285 -40
  11. package/dist/services/ai.d.ts +19 -5
  12. package/dist/services/ai.js +167 -35
  13. package/dist/services/changeLogger.d.ts +142 -0
  14. package/dist/services/changeLogger.js +132 -3
  15. package/dist/services/contextAgent.d.ts +6 -1
  16. package/dist/services/contextAgent.js +95 -14
  17. package/dist/services/embeddingIndex.d.ts +82 -0
  18. package/dist/services/embeddingIndex.js +613 -0
  19. package/dist/services/investigationComplexity.d.ts +45 -0
  20. package/dist/services/investigationComplexity.js +91 -0
  21. package/dist/services/metrics.d.ts +18 -0
  22. package/dist/services/metrics.js +7 -0
  23. package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
  24. package/dist/services/orchestration/fileLockRegistry.js +276 -0
  25. package/dist/services/orchestration/investigationAgent.d.ts +85 -0
  26. package/dist/services/orchestration/investigationAgent.js +359 -0
  27. package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
  28. package/dist/services/orchestration/investigationOrchestrator.js +180 -0
  29. package/dist/services/orchestration/messageBus.d.ts +162 -0
  30. package/dist/services/orchestration/messageBus.js +225 -0
  31. package/dist/services/orchestration/orchestrator.d.ts +45 -0
  32. package/dist/services/orchestration/orchestrator.js +214 -0
  33. package/dist/services/orchestration/readCache.d.ts +79 -0
  34. package/dist/services/orchestration/readCache.js +108 -0
  35. package/dist/services/orchestration/scopedTools.d.ts +57 -0
  36. package/dist/services/orchestration/scopedTools.js +172 -0
  37. package/dist/services/orchestration/subAgent.d.ts +58 -0
  38. package/dist/services/orchestration/subAgent.js +187 -0
  39. package/dist/services/orchestration/taskGraph.d.ts +129 -0
  40. package/dist/services/orchestration/taskGraph.js +254 -0
  41. package/dist/services/proxyClient.d.ts +25 -0
  42. package/dist/services/proxyClient.js +60 -0
  43. package/dist/utils/asyncContext.d.ts +16 -0
  44. package/dist/utils/asyncContext.js +25 -0
  45. package/dist/utils/config.d.ts +3 -1
  46. package/dist/utils/config.js +3 -1
  47. package/dist/utils/contextPrompts.js +3 -2
  48. package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
  49. package/dist/utils/dependencyTracer/modules/api.js +62 -0
  50. package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
  51. package/dist/utils/dependencyTracer/modules/graph.js +23 -0
  52. package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
  53. package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
  54. package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
  55. package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
  56. package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
  57. package/dist/utils/dependencyTracer/modules/types.js +1 -0
  58. package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
  59. package/dist/utils/dependencyTracer/modules/walker.js +48 -0
  60. package/dist/utils/dependencyTracer.js +31 -17
  61. package/dist/utils/excludedExtensions.js +0 -1
  62. package/dist/utils/historyPrompt.d.ts +9 -0
  63. package/dist/utils/historyPrompt.js +87 -0
  64. package/dist/utils/logo.js +7 -7
  65. package/dist/utils/paste.d.ts +21 -0
  66. package/dist/utils/paste.js +22 -1
  67. package/dist/utils/profiles.d.ts +2 -0
  68. package/dist/utils/profiles.js +44 -0
  69. package/dist/utils/projectStorage.js +10 -7
  70. package/dist/utils/systemPrompts.d.ts +6 -3
  71. package/dist/utils/systemPrompts.js +106 -5
  72. package/dist/utils/types.d.ts +33 -0
  73. package/dist/utils/types.js +1 -0
  74. package/oclif.manifest.json +2 -2
  75. package/package.json +4 -3
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @fileoverview Shared Read Cache for Parallel Investigation.
3
+ *
4
+ * A lightweight in-memory cache that investigation sub-agents use to avoid
5
+ * redundant file reads. When multiple agents are investigating different
6
+ * domains simultaneously, they frequently encounter overlapping files
7
+ * (e.g., shared types, config files, package.json). The ReadCache ensures
8
+ * each file is read from disk exactly once.
9
+ *
10
+ * Design decisions:
11
+ * - **Separate from MessageBus**: The full message bus provides semantic
12
+ * signals, activity logs, and disk persistence — all overkill for read-only
13
+ * agents. ReadCache is a simple `Map<string, string>` with LRU eviction.
14
+ * - **Thread-safe by default**: Node.js is single-threaded, so `Map` ops
15
+ * are atomic. No locking needed.
16
+ * - **Memory-bounded**: Total cached content is capped at MAX_CACHE_BYTES
17
+ * to prevent OOM on very large monorepos.
18
+ */
19
+ /**
20
+ * In-memory file content cache shared across parallel investigation agents.
21
+ *
22
+ * Usage:
23
+ * ```ts
24
+ * const cache = new ReadCache()
25
+ * // In tool wrapper:
26
+ * if (cache.has(filePath)) return cache.get(filePath)!
27
+ * const content = await fs.readFile(absPath, 'utf-8')
28
+ * cache.set(filePath, content)
29
+ * ```
30
+ */
31
+ export declare class ReadCache {
32
+ private cache;
33
+ private insertionOrder;
34
+ private totalBytes;
35
+ private hitCount;
36
+ private missCount;
37
+ /**
38
+ * Checks whether a file's content is already cached.
39
+ * @param filePath - Relative file path from workspace root.
40
+ */
41
+ has(filePath: string): boolean;
42
+ /**
43
+ * Retrieves cached content for a file.
44
+ * @param filePath - Relative file path from workspace root.
45
+ * @returns The file content, or undefined if not cached.
46
+ */
47
+ get(filePath: string): {
48
+ text: string;
49
+ inlineData?: any;
50
+ } | undefined;
51
+ /**
52
+ * Stores file content in the cache. If adding this entry would exceed
53
+ * the memory cap, older entries are evicted in insertion order (LRU).
54
+ *
55
+ * @param filePath - Relative file path from workspace root.
56
+ * @param content - The file's text content.
57
+ */
58
+ set(filePath: string, content: {
59
+ text: string;
60
+ inlineData?: any;
61
+ }): void;
62
+ /**
63
+ * Returns all cached file entries. Used by the Reducer to assemble
64
+ * the merged file set without re-reading from disk.
65
+ */
66
+ getAll(): Map<string, {
67
+ text: string;
68
+ inlineData?: any;
69
+ }>;
70
+ /**
71
+ * Returns diagnostic statistics for terminal display.
72
+ */
73
+ getStats(): {
74
+ hitCount: number;
75
+ missCount: number;
76
+ fileCount: number;
77
+ totalBytes: number;
78
+ };
79
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @fileoverview Shared Read Cache for Parallel Investigation.
3
+ *
4
+ * A lightweight in-memory cache that investigation sub-agents use to avoid
5
+ * redundant file reads. When multiple agents are investigating different
6
+ * domains simultaneously, they frequently encounter overlapping files
7
+ * (e.g., shared types, config files, package.json). The ReadCache ensures
8
+ * each file is read from disk exactly once.
9
+ *
10
+ * Design decisions:
11
+ * - **Separate from MessageBus**: The full message bus provides semantic
12
+ * signals, activity logs, and disk persistence — all overkill for read-only
13
+ * agents. ReadCache is a simple `Map<string, string>` with LRU eviction.
14
+ * - **Thread-safe by default**: Node.js is single-threaded, so `Map` ops
15
+ * are atomic. No locking needed.
16
+ * - **Memory-bounded**: Total cached content is capped at MAX_CACHE_BYTES
17
+ * to prevent OOM on very large monorepos.
18
+ */
19
+ import { debugLog } from '../../utils/logger.js';
20
+ // ─── Constants ───────────────────────────────────────────────────────
21
+ /** Maximum total bytes of cached file content before LRU eviction kicks in. */
22
+ const MAX_CACHE_BYTES = 5 * 1024 * 1024; // 5 MB
23
+ // ─── Read Cache Implementation ───────────────────────────────────────
24
+ /**
25
+ * In-memory file content cache shared across parallel investigation agents.
26
+ *
27
+ * Usage:
28
+ * ```ts
29
+ * const cache = new ReadCache()
30
+ * // In tool wrapper:
31
+ * if (cache.has(filePath)) return cache.get(filePath)!
32
+ * const content = await fs.readFile(absPath, 'utf-8')
33
+ * cache.set(filePath, content)
34
+ * ```
35
+ */
36
+ export class ReadCache {
37
+ cache = new Map();
38
+ insertionOrder = [];
39
+ totalBytes = 0;
40
+ hitCount = 0;
41
+ missCount = 0;
42
+ /**
43
+ * Checks whether a file's content is already cached.
44
+ * @param filePath - Relative file path from workspace root.
45
+ */
46
+ has(filePath) {
47
+ const found = this.cache.has(filePath);
48
+ if (found) {
49
+ this.hitCount++;
50
+ }
51
+ else {
52
+ this.missCount++;
53
+ }
54
+ return found;
55
+ }
56
+ /**
57
+ * Retrieves cached content for a file.
58
+ * @param filePath - Relative file path from workspace root.
59
+ * @returns The file content, or undefined if not cached.
60
+ */
61
+ get(filePath) {
62
+ return this.cache.get(filePath);
63
+ }
64
+ /**
65
+ * Stores file content in the cache. If adding this entry would exceed
66
+ * the memory cap, older entries are evicted in insertion order (LRU).
67
+ *
68
+ * @param filePath - Relative file path from workspace root.
69
+ * @param content - The file's text content.
70
+ */
71
+ set(filePath, content) {
72
+ // Don't re-insert if already cached
73
+ if (this.cache.has(filePath))
74
+ return;
75
+ const entryBytes = Buffer.byteLength(content.text, 'utf-8');
76
+ // Evict oldest entries until there is room
77
+ while (this.totalBytes + entryBytes > MAX_CACHE_BYTES && this.insertionOrder.length > 0) {
78
+ const oldest = this.insertionOrder.shift();
79
+ const evicted = this.cache.get(oldest);
80
+ if (evicted !== undefined) {
81
+ this.totalBytes -= Buffer.byteLength(evicted.text, 'utf-8');
82
+ this.cache.delete(oldest);
83
+ debugLog(`ReadCache: Evicted "${oldest}" to stay under ${MAX_CACHE_BYTES} byte cap.`);
84
+ }
85
+ }
86
+ this.cache.set(filePath, content);
87
+ this.insertionOrder.push(filePath);
88
+ this.totalBytes += entryBytes;
89
+ }
90
+ /**
91
+ * Returns all cached file entries. Used by the Reducer to assemble
92
+ * the merged file set without re-reading from disk.
93
+ */
94
+ getAll() {
95
+ return new Map(this.cache);
96
+ }
97
+ /**
98
+ * Returns diagnostic statistics for terminal display.
99
+ */
100
+ getStats() {
101
+ return {
102
+ hitCount: this.hitCount,
103
+ missCount: this.missCount,
104
+ fileCount: this.cache.size,
105
+ totalBytes: this.totalBytes,
106
+ };
107
+ }
108
+ }
@@ -0,0 +1,57 @@
1
+ import { MessageBus } from './messageBus.js';
2
+ import { FileLockRegistry } from './fileLockRegistry.js';
3
+ /**
4
+ * Extended tool declarations for sub-agents, merging the standard tools
5
+ * with orchestration-specific ones (e.g., post_message, read_messages).
6
+ */
7
+ export declare function getScopedToolDeclarations(): (import("@google/generative-ai").FunctionDeclaration | {
8
+ name: string;
9
+ description: string;
10
+ parameters: {
11
+ type: string;
12
+ properties: {
13
+ type: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ content: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ toAgent: {
22
+ type: string;
23
+ description: string;
24
+ };
25
+ affectedFiles: {
26
+ type: string;
27
+ items: {
28
+ type: string;
29
+ };
30
+ description: string;
31
+ };
32
+ };
33
+ required: string[];
34
+ };
35
+ } | {
36
+ name: string;
37
+ description: string;
38
+ parameters: {
39
+ type: string;
40
+ properties: {
41
+ type?: undefined;
42
+ content?: undefined;
43
+ toAgent?: undefined;
44
+ affectedFiles?: undefined;
45
+ };
46
+ required?: undefined;
47
+ };
48
+ })[];
49
+ /**
50
+ * Wraps the global executeTool to provide sub-agent context.
51
+ *
52
+ * 1. Captures tool execution into the MessageBus (Layer 1: zero-cost logs).
53
+ * 2. Implements FileLocks for write/modify operations to prevent race conditions.
54
+ * 3. Handles `post_message` and `read_messages` directly.
55
+ * 4. Calls a heartbeat callback to notify the orchestrator this agent is alive.
56
+ */
57
+ export declare function executeScopedTool(name: string, args: Record<string, any>, workspaceRoot: string, agentId: string, bus: MessageBus, locks: FileLockRegistry, onHeartbeat: () => void): Promise<any>;
@@ -0,0 +1,172 @@
1
+ import { executeTool, getToolDeclarations as getBaseToolDeclarations } from '../agent-tools.js';
2
+ import { MessageBus } from './messageBus.js';
3
+ import { debugLog } from '../../utils/logger.js';
4
+ /**
5
+ * Extended tool declarations for sub-agents, merging the standard tools
6
+ * with orchestration-specific ones (e.g., post_message, read_messages).
7
+ */
8
+ export function getScopedToolDeclarations() {
9
+ return [
10
+ ...getBaseToolDeclarations(),
11
+ {
12
+ name: 'post_message',
13
+ description: 'Post a semantic message to the orchestration bus to coordinate with other agents.',
14
+ parameters: {
15
+ type: 'OBJECT',
16
+ properties: {
17
+ type: {
18
+ type: 'STRING',
19
+ description: 'Type of signal: "discovery", "warning", "request", "completion"',
20
+ },
21
+ content: {
22
+ type: 'STRING',
23
+ description: 'The semantic intent or message content',
24
+ },
25
+ toAgent: {
26
+ type: 'STRING',
27
+ description: 'Optional target agent ID (required for "request")',
28
+ },
29
+ affectedFiles: {
30
+ type: 'ARRAY',
31
+ items: { type: 'STRING' },
32
+ description: 'Files involved (required for "discovery")',
33
+ },
34
+ },
35
+ required: ['type', 'content'],
36
+ },
37
+ },
38
+ {
39
+ name: 'read_messages',
40
+ description: 'Fetch any new messages or activity from other agents on the bus.',
41
+ parameters: {
42
+ type: 'OBJECT',
43
+ properties: {},
44
+ },
45
+ },
46
+ ];
47
+ }
48
+ /**
49
+ * Wraps the global executeTool to provide sub-agent context.
50
+ *
51
+ * 1. Captures tool execution into the MessageBus (Layer 1: zero-cost logs).
52
+ * 2. Implements FileLocks for write/modify operations to prevent race conditions.
53
+ * 3. Handles `post_message` and `read_messages` directly.
54
+ * 4. Calls a heartbeat callback to notify the orchestrator this agent is alive.
55
+ */
56
+ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus, locks, onHeartbeat) {
57
+ // Update heartbeat so the orchestrator knows we are making progress
58
+ onHeartbeat();
59
+ const timestamp = Date.now();
60
+ let actionDesc = 'Executed';
61
+ let targetDesc = 'workspace';
62
+ let status = 'success';
63
+ let result = null;
64
+ let resultSummary = undefined;
65
+ try {
66
+ // ─── Orchestration-Specific Tools ────────────────────────────────
67
+ if (name === 'post_message') {
68
+ const type = args.type;
69
+ const signal = {
70
+ type,
71
+ timestamp,
72
+ fromAgent: agentId,
73
+ content: args.content,
74
+ };
75
+ if (type === 'discovery')
76
+ signal.affectedFiles = args.affectedFiles || [];
77
+ if (type === 'request')
78
+ signal.toAgent = args.toAgent || 'all';
79
+ if (type === 'completion')
80
+ signal.summary = args.content; // map completion summary
81
+ const accepted = bus.postSignal(signal);
82
+ if (!accepted) {
83
+ return { error: `Message bus rejected signal. You have hit the max signal cap (${MessageBus.MAX_SIGNALS_PER_AGENT}). Stop sending signals.` };
84
+ }
85
+ return { success: true, message: 'Message posted to bus.' };
86
+ }
87
+ if (name === 'read_messages') {
88
+ const unread = bus.getUnread(agentId);
89
+ return {
90
+ activityLogs: MessageBus.formatActivityEntries(unread.activities),
91
+ semanticSignals: MessageBus.formatSignals(unread.signals),
92
+ };
93
+ }
94
+ // ─── Standard Tool Execution with Locking ────────────────────────
95
+ // Handle lock acquisition for write-oriented tools
96
+ let lockedFile = null;
97
+ let diffContext = null;
98
+ if (name === 'write_file' || name === 'modify_file' || name === 'delete_file') {
99
+ lockedFile = args.filePath;
100
+ if (lockedFile) {
101
+ debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (tool: ${name})`);
102
+ const lockRes = await locks.acquire(lockedFile, agentId);
103
+ diffContext = lockRes.previousDiff;
104
+ if (lockRes.forceReleased) {
105
+ debugLog(`Agent "${agentId}" got forced lock on "${lockedFile}" (previous owner stalled)`);
106
+ }
107
+ }
108
+ }
109
+ // Execute the underlying standard tool
110
+ try {
111
+ result = await executeTool(workspaceRoot, name, args);
112
+ // If we got a file lock and had context from a previous writer, we should
113
+ // inject that diff info back into the agent's tool return payload so it
114
+ // knows someone else changed the file while it was queued.
115
+ if (diffContext && typeof result === 'object') {
116
+ result._orchestrationWarning = `NOTICE: Another agent modified this file while you were waiting. Diff context: ${diffContext}`;
117
+ }
118
+ }
119
+ finally {
120
+ // Release the lock if we grabbed one
121
+ if (lockedFile) {
122
+ // We do a simple release. The actual diff context extraction happens at the orchestrator
123
+ // layer during verification, but we release immediately to unblock others.
124
+ locks.release(lockedFile, agentId, `Action: ${name} completed`);
125
+ }
126
+ }
127
+ // ─── Formatting Log Outputs ──────────────────────────────────────
128
+ if (name === 'write_file' || name === 'modify_file') {
129
+ targetDesc = args.filePath;
130
+ actionDesc = name === 'write_file' ? 'Created/Replaced' : 'Modified';
131
+ resultSummary = typeof result === 'string' ? `${result.length} chars` : 'Success';
132
+ }
133
+ else if (name === 'read_file') {
134
+ targetDesc = args.filePath;
135
+ actionDesc = 'Read';
136
+ const len = typeof result === 'string' ? result.length : (result.fileContents?.length || 0);
137
+ resultSummary = `${len} chars`;
138
+ }
139
+ else if (name === 'run_command') {
140
+ targetDesc = args.command;
141
+ actionDesc = 'Ran command';
142
+ }
143
+ else if (name === 'grep_search') {
144
+ targetDesc = args.pattern;
145
+ actionDesc = 'Searched';
146
+ const matchCount = Array.isArray(result) ? result.length : 0;
147
+ resultSummary = `${matchCount} matches`;
148
+ }
149
+ return result;
150
+ }
151
+ catch (error) {
152
+ status = 'error';
153
+ resultSummary = error.message;
154
+ throw error;
155
+ }
156
+ finally {
157
+ // ─── Logging to Bus ──────────────────────────────────────────────
158
+ // We only log standard tools, not orchestration tools
159
+ if (name !== 'post_message' && name !== 'read_messages') {
160
+ bus.logActivity({
161
+ type: 'activity',
162
+ timestamp,
163
+ agentId,
164
+ tool: name,
165
+ target: targetDesc,
166
+ action: actionDesc,
167
+ status,
168
+ resultSummary,
169
+ });
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @fileoverview Sub-Agent Runner for Orchestration.
3
+ *
4
+ * Implements the lifecycle, health monitoring, and tool-loop execution for a single
5
+ * parallelized sub-agent.
6
+ */
7
+ import { MessageBus } from './messageBus.js';
8
+ import { FileLockRegistry } from './fileLockRegistry.js';
9
+ /**
10
+ * Result returned when a sub-agent completes its execution.
11
+ */
12
+ export interface SubAgentResult {
13
+ /** Whether the agent successfully completed its intent */
14
+ success: boolean;
15
+ /** Output or summary produced by the agent */
16
+ summary: string;
17
+ /** Total credits (tokens) consumed by this agent */
18
+ creditsUsed: number;
19
+ /** Did the agent hit a stall timeout or crash? */
20
+ crashed: boolean;
21
+ }
22
+ /**
23
+ * Executes a sub-agent task with full health monitoring, tool wrapping,
24
+ * and orchestration integration.
25
+ */
26
+ export declare class SubAgentRunner {
27
+ private readonly taskId;
28
+ private readonly intent;
29
+ private readonly workspaceRoot;
30
+ private readonly bus;
31
+ private readonly locks;
32
+ private readonly globalContext;
33
+ private chat;
34
+ private lastHeartbeat;
35
+ private creditsUsed;
36
+ /** Max time without a tool call or response before the agent is considered stalled */
37
+ static readonly STALL_TIMEOUT_MS = 60000;
38
+ constructor(taskId: string, intent: string, workspaceRoot: string, bus: MessageBus, locks: FileLockRegistry, globalContext: string);
39
+ /**
40
+ * Constructs the base system instruction for this specific agent.
41
+ */
42
+ private buildSystemInstruction;
43
+ /**
44
+ * Updates the heartbeat. Passed to `executeScopedTool` to ensure the agent
45
+ * isn't marked as stalled while performing long-running commands.
46
+ */
47
+ private pingHeartbeat;
48
+ /**
49
+ * Executes the sub-agent with a health monitor harness.
50
+ * Runs the tool loop until the model stops returning function calls,
51
+ * an error occurs, or the stall timeout is hit.
52
+ */
53
+ execute(signal: AbortSignal): Promise<SubAgentResult>;
54
+ /**
55
+ * Accumulates token usage from the chat session.
56
+ */
57
+ private updateUsage;
58
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @fileoverview Sub-Agent Runner for Orchestration.
3
+ *
4
+ * Implements the lifecycle, health monitoring, and tool-loop execution for a single
5
+ * parallelized sub-agent.
6
+ */
7
+ import { ProxyChatSession } from '../ai.js';
8
+ import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
9
+ import { executeScopedTool, getScopedToolDeclarations } from './scopedTools.js';
10
+ import { debugLog } from '../../utils/logger.js';
11
+ import { runWithAgentId } from '../../utils/asyncContext.js';
12
+ /**
13
+ * Executes a sub-agent task with full health monitoring, tool wrapping,
14
+ * and orchestration integration.
15
+ */
16
+ export class SubAgentRunner {
17
+ taskId;
18
+ intent;
19
+ workspaceRoot;
20
+ bus;
21
+ locks;
22
+ globalContext;
23
+ chat;
24
+ lastHeartbeat = Date.now();
25
+ creditsUsed = 0;
26
+ /** Max time without a tool call or response before the agent is considered stalled */
27
+ static STALL_TIMEOUT_MS = 60_000;
28
+ constructor(taskId, intent, workspaceRoot, bus, locks, globalContext) {
29
+ this.taskId = taskId;
30
+ this.intent = intent;
31
+ this.workspaceRoot = workspaceRoot;
32
+ this.bus = bus;
33
+ this.locks = locks;
34
+ this.globalContext = globalContext;
35
+ // Sub-agents default to flash-3.5 for better reasoning capabilities
36
+ this.chat = new ProxyChatSession(GEMINI_MODELS.FLASH_3_5, this.buildSystemInstruction(), [{ functionDeclarations: getScopedToolDeclarations() }], {
37
+ maxOutputTokens: MAX_OUTPUT_TOKENS,
38
+ temperature: 0.3, // Lower temperature for more focused execution
39
+ topP: 0.95,
40
+ topK: 40,
41
+ });
42
+ }
43
+ /**
44
+ * Constructs the base system instruction for this specific agent.
45
+ */
46
+ buildSystemInstruction() {
47
+ return (`You are an autonomous sub-agent executing a specific portion of a larger task.\n` +
48
+ `Your task ID is: ${this.taskId}\n` +
49
+ `Your objective: ${this.intent}\n\n` +
50
+ `=== GLOBAL CONTEXT & ORIGINAL USER REQUEST ===\n` +
51
+ `${this.globalContext}\n` +
52
+ `==============================================\n\n` +
53
+ `Guidelines:\n` +
54
+ `1. Focus ONLY on your assigned objective. Do not stray into other files or tasks.\n` +
55
+ `2. You are part of a parallelized system. Use 'post_message' to coordinate if you discover breaking changes.\n` +
56
+ `3. When you have completed your objective, stop using tools and provide a final summary of your work.\n` +
57
+ `4. If you encounter an insurmountable error, provide a summary of what went wrong so the orchestrator can re-assign or fix it.`);
58
+ }
59
+ /**
60
+ * Updates the heartbeat. Passed to `executeScopedTool` to ensure the agent
61
+ * isn't marked as stalled while performing long-running commands.
62
+ */
63
+ pingHeartbeat = () => {
64
+ this.lastHeartbeat = Date.now();
65
+ };
66
+ /**
67
+ * Executes the sub-agent with a health monitor harness.
68
+ * Runs the tool loop until the model stops returning function calls,
69
+ * an error occurs, or the stall timeout is hit.
70
+ */
71
+ async execute(signal) {
72
+ return runWithAgentId(this.taskId, async () => {
73
+ debugLog(`SubAgent [${this.taskId}]: Starting execution.`);
74
+ this.lastHeartbeat = Date.now();
75
+ let success = false;
76
+ let finalSummary = '';
77
+ let crashed = false;
78
+ // Health Monitor Timer
79
+ const healthMonitor = setInterval(() => {
80
+ if (Date.now() - this.lastHeartbeat > SubAgentRunner.STALL_TIMEOUT_MS) {
81
+ debugLog(`SubAgent [${this.taskId}]: STALL DETECTED. No heartbeat for ${SubAgentRunner.STALL_TIMEOUT_MS}ms.`);
82
+ crashed = true;
83
+ // Note: the orchestrator will handle the actual force-releasing of locks
84
+ // when this promise resolves/rejects, based on the `crashed` flag.
85
+ }
86
+ }, 5000);
87
+ try {
88
+ // Send initial prompt
89
+ const prompt = `Begin execution for task: ${this.taskId}\nObjective: ${this.intent}\n\nYou must use tools to achieve this objective. Do not stop until the objective is fully complete.`;
90
+ let turnResult = await this.chat.sendMessage(prompt, undefined, signal);
91
+ this.updateUsage(turnResult);
92
+ // Tool Loop
93
+ const MAX_TURNS = Infinity;
94
+ let turns = 0;
95
+ // Anti-loop tracking
96
+ const recentCalls = [];
97
+ while (turns < MAX_TURNS && !crashed && !signal.aborted) {
98
+ this.pingHeartbeat();
99
+ const calls = turnResult.response.functionCalls();
100
+ // If the model stopped calling tools, it is done
101
+ if (!calls || calls.length === 0) {
102
+ success = true;
103
+ finalSummary = turnResult.response.text() || 'Task completed without text summary.';
104
+ break;
105
+ }
106
+ const toolResponses = [];
107
+ // Execute all requested tools sequentially (to avoid parallel lock contention from the same agent)
108
+ for (const call of calls) {
109
+ if (crashed || signal.aborted)
110
+ break;
111
+ // Anti-loop check: Hash the call to detect exact repetitions
112
+ const callHash = JSON.stringify({ name: call.name, args: call.args });
113
+ recentCalls.push(callHash);
114
+ if (recentCalls.length > 5)
115
+ recentCalls.shift();
116
+ if (recentCalls.length === 5 && recentCalls.every((c) => c === callHash)) {
117
+ debugLog(`SubAgent [${this.taskId}]: Infinite loop detected on tool ${call.name}. Aborting.`);
118
+ crashed = true;
119
+ finalSummary = `Agent got stuck in an infinite loop repeating the exact same call to ${call.name}.`;
120
+ break;
121
+ }
122
+ this.pingHeartbeat();
123
+ debugLog(`SubAgent [${this.taskId}]: Executing tool ${call.name}`);
124
+ let responseData;
125
+ try {
126
+ responseData = await executeScopedTool(call.name, call.args, this.workspaceRoot, this.taskId, this.bus, this.locks, this.pingHeartbeat);
127
+ }
128
+ catch (err) {
129
+ responseData = { error: err.message || String(err) };
130
+ }
131
+ toolResponses.push({
132
+ functionResponse: {
133
+ name: call.name,
134
+ response: {
135
+ output: responseData.output !== undefined ? responseData.output : responseData,
136
+ ...(responseData.error ? { error: responseData.error } : {}),
137
+ ...(responseData.inlineData ? { inlineData: responseData.inlineData } : {}),
138
+ ...(responseData._orchestrationWarning ? { _orchestrationWarning: responseData._orchestrationWarning } : {}),
139
+ },
140
+ },
141
+ });
142
+ }
143
+ if (crashed || signal.aborted)
144
+ break;
145
+ this.pingHeartbeat();
146
+ // Feed tool results back to the model
147
+ turnResult = await this.chat.sendMessage(toolResponses, undefined, signal);
148
+ this.updateUsage(turnResult);
149
+ turns++;
150
+ }
151
+ if (turns >= MAX_TURNS) {
152
+ debugLog(`SubAgent [${this.taskId}]: Hit max turns (${MAX_TURNS}).`);
153
+ success = false;
154
+ finalSummary = 'Agent reached maximum tool turns without completing the objective.';
155
+ }
156
+ }
157
+ catch (err) {
158
+ debugLog(`SubAgent [${this.taskId}]: Execution error: ${err.message}`);
159
+ crashed = true;
160
+ finalSummary = `Crashed: ${err.message}`;
161
+ }
162
+ finally {
163
+ clearInterval(healthMonitor);
164
+ }
165
+ if (signal.aborted) {
166
+ crashed = true;
167
+ finalSummary = 'Aborted by orchestrator.';
168
+ }
169
+ debugLog(`SubAgent [${this.taskId}]: Finished. Success=${success}, Crashed=${crashed}, Tokens=${this.creditsUsed}`);
170
+ return {
171
+ success: success && !crashed,
172
+ summary: finalSummary,
173
+ creditsUsed: this.creditsUsed,
174
+ crashed,
175
+ };
176
+ });
177
+ }
178
+ /**
179
+ * Accumulates token usage from the chat session.
180
+ */
181
+ updateUsage(result) {
182
+ const usage = result.response.usageMetadata?.();
183
+ if (usage && usage.totalTokenCount) {
184
+ this.creditsUsed += usage.totalTokenCount;
185
+ }
186
+ }
187
+ }