minovative-mind-cli 1.5.1 → 2.1.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.
- package/README.md +59 -45
- package/dist/commands/chat.js +10 -2
- package/dist/services/agent/slashCommands.js +369 -42
- package/dist/services/agent/toolLoop.d.ts +1 -1
- package/dist/services/agent/toolLoop.js +7 -2
- package/dist/services/agent/types.d.ts +2 -0
- package/dist/services/agent-tools.d.ts +9 -4
- package/dist/services/agent-tools.js +272 -34
- package/dist/services/agent.d.ts +8 -0
- package/dist/services/agent.js +288 -40
- package/dist/services/ai.d.ts +19 -5
- package/dist/services/ai.js +182 -36
- package/dist/services/changeLogger.d.ts +142 -0
- package/dist/services/changeLogger.js +132 -3
- package/dist/services/contextAgent.d.ts +6 -1
- package/dist/services/contextAgent.js +112 -19
- package/dist/services/embeddingIndex.d.ts +82 -0
- package/dist/services/embeddingIndex.js +613 -0
- package/dist/services/investigationComplexity.d.ts +45 -0
- package/dist/services/investigationComplexity.js +91 -0
- package/dist/services/metrics.d.ts +18 -0
- package/dist/services/metrics.js +7 -0
- package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
- package/dist/services/orchestration/fileLockRegistry.js +276 -0
- package/dist/services/orchestration/investigationAgent.d.ts +85 -0
- package/dist/services/orchestration/investigationAgent.js +362 -0
- package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
- package/dist/services/orchestration/investigationOrchestrator.js +180 -0
- package/dist/services/orchestration/messageBus.d.ts +162 -0
- package/dist/services/orchestration/messageBus.js +225 -0
- package/dist/services/orchestration/orchestrator.d.ts +45 -0
- package/dist/services/orchestration/orchestrator.js +217 -0
- package/dist/services/orchestration/readCache.d.ts +79 -0
- package/dist/services/orchestration/readCache.js +108 -0
- package/dist/services/orchestration/scopedTools.d.ts +57 -0
- package/dist/services/orchestration/scopedTools.js +172 -0
- package/dist/services/orchestration/subAgent.d.ts +58 -0
- package/dist/services/orchestration/subAgent.js +190 -0
- package/dist/services/orchestration/taskGraph.d.ts +129 -0
- package/dist/services/orchestration/taskGraph.js +254 -0
- package/dist/services/proxyClient.d.ts +25 -0
- package/dist/services/proxyClient.js +60 -0
- package/dist/services/workspaceRegistry.d.ts +137 -0
- package/dist/services/workspaceRegistry.js +270 -0
- package/dist/utils/asyncContext.d.ts +16 -0
- package/dist/utils/asyncContext.js +25 -0
- package/dist/utils/config.d.ts +3 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/contextPrompts.js +10 -3
- package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/api.js +62 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
- package/dist/utils/dependencyTracer/modules/graph.js +23 -0
- package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
- package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
- package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
- package/dist/utils/dependencyTracer/modules/types.js +1 -0
- package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
- package/dist/utils/dependencyTracer/modules/walker.js +48 -0
- package/dist/utils/dependencyTracer.js +31 -17
- package/dist/utils/excludedExtensions.js +0 -1
- package/dist/utils/historyPrompt.d.ts +9 -0
- package/dist/utils/historyPrompt.js +87 -0
- package/dist/utils/logo.js +7 -7
- package/dist/utils/paste.d.ts +21 -0
- package/dist/utils/paste.js +22 -1
- package/dist/utils/pathSecurity.d.ts +31 -0
- package/dist/utils/pathSecurity.js +48 -0
- package/dist/utils/profiles.d.ts +2 -0
- package/dist/utils/profiles.js +44 -0
- package/dist/utils/projectStorage.js +10 -7
- package/dist/utils/systemPrompts.d.ts +6 -3
- package/dist/utils/systemPrompts.js +111 -6
- package/dist/utils/types.d.ts +33 -0
- package/dist/utils/types.js +1 -0
- package/oclif.manifest.json +2 -2
- package/package.json +4 -3
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Read-Only Investigation Sub-Agent Runner.
|
|
3
|
+
*
|
|
4
|
+
* Implements the lifecycle, health monitoring, and tool-loop execution for a single
|
|
5
|
+
* parallelized investigation agent. Unlike execution sub-agents in `subAgent.ts`,
|
|
6
|
+
* investigation agents are **strictly read-only** — they have no filesystem write
|
|
7
|
+
* tools, no file locks, and no message bus. They share a `ReadCache` to avoid
|
|
8
|
+
* redundant file reads across sibling agents.
|
|
9
|
+
*
|
|
10
|
+
* Each investigation agent is scoped to a set of domains (e.g., "Frontend auth
|
|
11
|
+
* components" + "Auth UI styling") and produces an `InvestigationResult` containing
|
|
12
|
+
* the files it found and a summary of its findings.
|
|
13
|
+
*/
|
|
14
|
+
import { ProxyChatSession, getContextToolDeclarations, getGlobalActiveModel } from '../ai.js';
|
|
15
|
+
import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
|
|
16
|
+
import { CONTEXT_SYSTEM_INSTRUCTION } from '../../utils/systemPrompts.js';
|
|
17
|
+
import { debugLog } from '../../utils/logger.js';
|
|
18
|
+
import { listDirectory, grepSearch, readFile, traceDependencies, findRecentChanges } from '../agent-tools.js';
|
|
19
|
+
import { runEphemeralScript } from '../../utils/analysisRunner.js';
|
|
20
|
+
// ─── Investigation Agent Runner ──────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Executes a read-only investigation within a scoped set of domains.
|
|
23
|
+
*
|
|
24
|
+
* Key differences from `SubAgentRunner` (execution):
|
|
25
|
+
* - **No write tools**: Cannot modify, create, or delete files
|
|
26
|
+
* - **No file locks**: Read-only access means zero contention
|
|
27
|
+
* - **No message bus**: Agents don't need to coordinate — they work independently
|
|
28
|
+
* - **Shared ReadCache**: Checks cache before hitting disk to avoid redundant reads
|
|
29
|
+
* - **Shorter stall timeout**: 30s vs 60s — investigation should be faster
|
|
30
|
+
*/
|
|
31
|
+
export class InvestigationAgentRunner {
|
|
32
|
+
agentLabel;
|
|
33
|
+
domains;
|
|
34
|
+
workspaceRoot;
|
|
35
|
+
readCache;
|
|
36
|
+
projectTree;
|
|
37
|
+
projectType;
|
|
38
|
+
chat;
|
|
39
|
+
lastHeartbeat = Date.now();
|
|
40
|
+
creditsUsed = 0;
|
|
41
|
+
/** Shorter stall timeout for investigation agents (120s to allow for heavy vision/web tasks) */
|
|
42
|
+
static STALL_TIMEOUT_MS = 120_000;
|
|
43
|
+
constructor(agentLabel, domains, workspaceRoot, readCache, projectTree, projectType) {
|
|
44
|
+
this.agentLabel = agentLabel;
|
|
45
|
+
this.domains = domains;
|
|
46
|
+
this.workspaceRoot = workspaceRoot;
|
|
47
|
+
this.readCache = readCache;
|
|
48
|
+
this.projectTree = projectTree;
|
|
49
|
+
this.projectType = projectType;
|
|
50
|
+
let model = getGlobalActiveModel();
|
|
51
|
+
if (model === GEMINI_MODELS.AUTO)
|
|
52
|
+
model = GEMINI_MODELS.FLASH_3_5;
|
|
53
|
+
this.chat = new ProxyChatSession(model, this.buildSystemInstruction(), getContextToolDeclarations(), {
|
|
54
|
+
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
55
|
+
temperature: 1,
|
|
56
|
+
topP: 0.95,
|
|
57
|
+
topK: 40,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Builds a domain-scoped system instruction. Extends the base Context Agent
|
|
62
|
+
* instruction with a domain focus preamble.
|
|
63
|
+
*/
|
|
64
|
+
buildSystemInstruction() {
|
|
65
|
+
const domainList = this.domains.map((d) => ` - ${d}`).join('\n');
|
|
66
|
+
return (CONTEXT_SYSTEM_INSTRUCTION +
|
|
67
|
+
`\n\n<investigation_scope>` +
|
|
68
|
+
`\nYou are investigation agent "${this.agentLabel}", part of a parallel investigation team.` +
|
|
69
|
+
`\nYour assigned investigation domains:\n${domainList}` +
|
|
70
|
+
`\n\nFocus your investigation ONLY on files and code related to your assigned domains.` +
|
|
71
|
+
`\nOther agents are simultaneously investigating other domains — do not duplicate their work.` +
|
|
72
|
+
`\nBe thorough within your scope, but do not stray outside it.` +
|
|
73
|
+
`\n</investigation_scope>`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Updates the heartbeat timestamp. Prevents the stall detector from
|
|
77
|
+
* killing the agent during long-running tool calls.
|
|
78
|
+
*/
|
|
79
|
+
pingHeartbeat = () => {
|
|
80
|
+
this.lastHeartbeat = Date.now();
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Executes the investigation agent's tool loop.
|
|
84
|
+
*
|
|
85
|
+
* @param userRequest - The original user prompt.
|
|
86
|
+
* @param chatHistory - Recent conversation history for context.
|
|
87
|
+
* @param abortSignal - Signal to cancel execution.
|
|
88
|
+
* @param onProgress - Callback for spinner/terminal updates.
|
|
89
|
+
* @returns The investigation result with discovered files and summary.
|
|
90
|
+
*/
|
|
91
|
+
async execute(userRequest, chatHistory, abortSignal, onProgress) {
|
|
92
|
+
debugLog(`InvestigationAgent [${this.agentLabel}]: Starting execution for domains: ${this.domains.join(', ')}`);
|
|
93
|
+
this.lastHeartbeat = Date.now();
|
|
94
|
+
const relevantFiles = new Map();
|
|
95
|
+
let summary = 'No relevant context found.';
|
|
96
|
+
let webSearchSummary = '';
|
|
97
|
+
let success = false;
|
|
98
|
+
let crashed = false;
|
|
99
|
+
// Health Monitor
|
|
100
|
+
const healthMonitor = setInterval(() => {
|
|
101
|
+
if (Date.now() - this.lastHeartbeat > InvestigationAgentRunner.STALL_TIMEOUT_MS) {
|
|
102
|
+
debugLog(`InvestigationAgent [${this.agentLabel}]: STALL DETECTED. No heartbeat for ${InvestigationAgentRunner.STALL_TIMEOUT_MS}ms.`);
|
|
103
|
+
crashed = true;
|
|
104
|
+
}
|
|
105
|
+
}, 5000);
|
|
106
|
+
try {
|
|
107
|
+
// Build the initial investigation prompt
|
|
108
|
+
let currentMessage = `User Request: "${userRequest}"\n\nProject Type: ${this.projectType}\n\nProject Structure:\n${this.projectTree}`;
|
|
109
|
+
if (chatHistory) {
|
|
110
|
+
currentMessage = `Previous Conversation Context:\n${chatHistory}\n\n` + currentMessage;
|
|
111
|
+
}
|
|
112
|
+
currentMessage += `\n\nStart investigating to find relevant files within your assigned domains.`;
|
|
113
|
+
let isFinished = false;
|
|
114
|
+
// Tool loop — mirrors the existing context agent loop in contextAgent.ts
|
|
115
|
+
while (!crashed && !abortSignal.aborted && !isFinished) {
|
|
116
|
+
this.pingHeartbeat();
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = await this.chat.sendMessage(currentMessage, undefined, abortSignal, () => this.pingHeartbeat());
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
if (e.name === 'AbortError' || e.message?.includes('abort'))
|
|
123
|
+
break;
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
this.updateUsage(result);
|
|
127
|
+
const functionCalls = result.response.functionCalls();
|
|
128
|
+
if (!functionCalls || functionCalls.length === 0) {
|
|
129
|
+
// Model returned text — take as summary
|
|
130
|
+
summary = result.response.text();
|
|
131
|
+
success = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
const functionResponses = [];
|
|
135
|
+
for (const call of functionCalls) {
|
|
136
|
+
if (crashed || abortSignal.aborted)
|
|
137
|
+
break;
|
|
138
|
+
this.pingHeartbeat();
|
|
139
|
+
const args = call.args;
|
|
140
|
+
const logPrefix = `[${this.agentLabel}]`;
|
|
141
|
+
if (call.name === 'finish_investigation') {
|
|
142
|
+
summary = args.summary || '';
|
|
143
|
+
const filesToRead = args.relevantFiles || [];
|
|
144
|
+
if (onProgress)
|
|
145
|
+
onProgress(`${logPrefix} Finished investigation (${filesToRead.length} files)`);
|
|
146
|
+
debugLog(`InvestigationAgent ${logPrefix}: Finished. Selected files: ${JSON.stringify(filesToRead)}`);
|
|
147
|
+
for (const filePath of filesToRead) {
|
|
148
|
+
if (!relevantFiles.has(filePath)) {
|
|
149
|
+
// Check shared cache first
|
|
150
|
+
if (this.readCache.has(filePath)) {
|
|
151
|
+
relevantFiles.set(filePath, this.readCache.get(filePath));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const readResult = await readFile(this.workspaceRoot, filePath);
|
|
155
|
+
if (!readResult.error) {
|
|
156
|
+
const contentObj = { text: readResult.output, inlineData: readResult.inlineData };
|
|
157
|
+
relevantFiles.set(filePath, contentObj);
|
|
158
|
+
this.readCache.set(filePath, contentObj);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
isFinished = true;
|
|
164
|
+
success = true;
|
|
165
|
+
functionResponses.push({
|
|
166
|
+
functionResponse: { name: call.name, response: { output: 'Investigation finished.' } },
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
else if (call.name === 'select_files') {
|
|
171
|
+
const filesToRead = args.files || [];
|
|
172
|
+
if (onProgress)
|
|
173
|
+
onProgress(`${logPrefix} Selected ${filesToRead.length} files`);
|
|
174
|
+
let output = '';
|
|
175
|
+
for (const filePath of filesToRead) {
|
|
176
|
+
// Check cache first
|
|
177
|
+
if (this.readCache.has(filePath)) {
|
|
178
|
+
const cached = this.readCache.get(filePath);
|
|
179
|
+
relevantFiles.set(filePath, cached);
|
|
180
|
+
output += `\n--- File: ${filePath} ---\n${cached.text}\n`;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const readResult = await readFile(this.workspaceRoot, filePath);
|
|
184
|
+
if (!readResult.error) {
|
|
185
|
+
const contentObj = { text: readResult.output, inlineData: readResult.inlineData };
|
|
186
|
+
relevantFiles.set(filePath, contentObj);
|
|
187
|
+
this.readCache.set(filePath, contentObj);
|
|
188
|
+
output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
output += `\n--- File: ${filePath} ---\nError: ${readResult.error}\n`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
functionResponses.push({
|
|
196
|
+
functionResponse: { name: call.name, response: { output: output || 'No files read.' } },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else if (call.name === 'list_directory') {
|
|
200
|
+
if (onProgress)
|
|
201
|
+
onProgress(`${logPrefix} Listing: ${args.dirPath}`);
|
|
202
|
+
const listRes = await listDirectory(this.workspaceRoot, args.dirPath, args.maxDepth || 1);
|
|
203
|
+
functionResponses.push({
|
|
204
|
+
functionResponse: {
|
|
205
|
+
name: call.name,
|
|
206
|
+
response: { output: listRes.output, ...(listRes.error ? { error: listRes.error } : {}) },
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (call.name === 'search_codebase') {
|
|
211
|
+
if (onProgress)
|
|
212
|
+
onProgress(`${logPrefix} Searching: "${args.pattern}"`);
|
|
213
|
+
const grepRes = await grepSearch(this.workspaceRoot, args.pattern, args.fileGlob);
|
|
214
|
+
functionResponses.push({
|
|
215
|
+
functionResponse: { name: call.name, response: { output: grepRes.error ? grepRes.error : grepRes.output } },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else if (call.name === 'read_file') {
|
|
219
|
+
const filePath = args.filePath;
|
|
220
|
+
if (onProgress)
|
|
221
|
+
onProgress(`${logPrefix} Reading: ${filePath}`);
|
|
222
|
+
// Check cache for full-file reads (no line range or target elements)
|
|
223
|
+
if (!args.startLine && !args.endLine && !args.targetElements && this.readCache.has(filePath)) {
|
|
224
|
+
const cached = this.readCache.get(filePath);
|
|
225
|
+
relevantFiles.set(filePath, cached);
|
|
226
|
+
functionResponses.push({ functionResponse: { name: call.name, response: { output: cached.text } } });
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const readRes = await readFile(this.workspaceRoot, filePath, args.startLine, args.endLine, args.targetElements);
|
|
230
|
+
if (!readRes.error) {
|
|
231
|
+
const contentObj = { text: readRes.output, inlineData: readRes.inlineData };
|
|
232
|
+
relevantFiles.set(filePath, contentObj);
|
|
233
|
+
// Only cache full-file reads (partial reads shouldn't overwrite full content)
|
|
234
|
+
if (!args.startLine && !args.endLine && !args.targetElements) {
|
|
235
|
+
this.readCache.set(filePath, contentObj);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
functionResponses.push({
|
|
239
|
+
functionResponse: { name: call.name, response: { output: readRes.error ? readRes.error : readRes.output } },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (call.name === 'semantic_search') {
|
|
244
|
+
const query = args.query;
|
|
245
|
+
const topK = args.topK || 5;
|
|
246
|
+
if (onProgress)
|
|
247
|
+
onProgress(`${logPrefix} Semantic search: "${query}"`);
|
|
248
|
+
let output = '';
|
|
249
|
+
try {
|
|
250
|
+
const { getEmbeddingIndex } = await import('../embeddingIndex.js');
|
|
251
|
+
const index = getEmbeddingIndex();
|
|
252
|
+
if (!index.isReady()) {
|
|
253
|
+
const loaded = await index.load(this.workspaceRoot);
|
|
254
|
+
if (!loaded) {
|
|
255
|
+
output = 'Semantic search index not available. Use search_codebase instead.';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (index.isReady()) {
|
|
259
|
+
const results = await index.search(query, topK);
|
|
260
|
+
output =
|
|
261
|
+
results.length === 0
|
|
262
|
+
? 'No semantically similar code found.'
|
|
263
|
+
: results
|
|
264
|
+
.map((r) => `[Score: ${r.score.toFixed(3)}] ${r.filePath}:${r.startLine}-${r.endLine}\n${r.preview}`)
|
|
265
|
+
.join('\n---\n');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
output = `Semantic search failed: ${e.message}`;
|
|
270
|
+
}
|
|
271
|
+
functionResponses.push({ functionResponse: { name: call.name, response: { output } } });
|
|
272
|
+
}
|
|
273
|
+
else if (call.name === 'perform_web_search') {
|
|
274
|
+
if (onProgress)
|
|
275
|
+
onProgress(`${logPrefix} Web search: "${args.query}"`);
|
|
276
|
+
try {
|
|
277
|
+
const { createWebSearchAgentSession } = await import('../ai.js');
|
|
278
|
+
const webSession = createWebSearchAgentSession();
|
|
279
|
+
const webResult = await webSession.sendMessage(`Please search the web for the following query and summarize your findings:\n"${args.query}"`, undefined, abortSignal, () => this.pingHeartbeat());
|
|
280
|
+
const searchSummary = webResult.response.text()?.trim() || 'No relevant information found.';
|
|
281
|
+
webSearchSummary += `\nQuery: ${args.query}\nFindings:\n${searchSummary}\n`;
|
|
282
|
+
functionResponses.push({ functionResponse: { name: call.name, response: { output: searchSummary } } });
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
functionResponses.push({
|
|
286
|
+
functionResponse: { name: call.name, response: { error: e.message || 'Failed to search the web' } },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (call.name === 'find_dependencies') {
|
|
291
|
+
if (onProgress)
|
|
292
|
+
onProgress(`${logPrefix} Tracing dependencies: ${args.filePath}`);
|
|
293
|
+
const depResult = await traceDependencies(this.workspaceRoot, args.filePath, args.direction, args.maxDepth);
|
|
294
|
+
functionResponses.push({
|
|
295
|
+
functionResponse: {
|
|
296
|
+
name: call.name,
|
|
297
|
+
response: { output: depResult.error ? depResult.error : depResult.output },
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else if (call.name === 'find_recent_changes') {
|
|
302
|
+
if (onProgress)
|
|
303
|
+
onProgress(`${logPrefix} Finding recent changes`);
|
|
304
|
+
const recentRes = await findRecentChanges(this.workspaceRoot, args.dirPath, args.minutes, args.maxDepth);
|
|
305
|
+
functionResponses.push({
|
|
306
|
+
functionResponse: {
|
|
307
|
+
name: call.name,
|
|
308
|
+
response: { output: recentRes.error ? recentRes.error : recentRes.output },
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
else if (call.name === 'run_analysis_script') {
|
|
313
|
+
if (onProgress)
|
|
314
|
+
onProgress(`${logPrefix} Running analysis script`);
|
|
315
|
+
const analysisResult = await runEphemeralScript(this.workspaceRoot, args.language, args.code, {
|
|
316
|
+
abortSignal,
|
|
317
|
+
});
|
|
318
|
+
const output = analysisResult.exitCode === 0
|
|
319
|
+
? analysisResult.stdout || '(script produced no output)'
|
|
320
|
+
: `Script failed (exit ${analysisResult.exitCode}):\n${analysisResult.stderr}`;
|
|
321
|
+
functionResponses.push({ functionResponse: { name: call.name, response: { output } } });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (crashed || abortSignal.aborted || isFinished)
|
|
325
|
+
break;
|
|
326
|
+
// Feed tool results back to the model
|
|
327
|
+
currentMessage = functionResponses;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
debugLog(`InvestigationAgent [${this.agentLabel}]: Execution error: ${err.message}`);
|
|
332
|
+
crashed = true;
|
|
333
|
+
summary = `Investigation agent crashed: ${err.message}`;
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
clearInterval(healthMonitor);
|
|
337
|
+
}
|
|
338
|
+
if (abortSignal.aborted) {
|
|
339
|
+
crashed = true;
|
|
340
|
+
summary = 'Investigation aborted by user.';
|
|
341
|
+
}
|
|
342
|
+
debugLog(`InvestigationAgent [${this.agentLabel}]: Finished. Success=${success && !crashed}, Files=${relevantFiles.size}, Tokens=${this.creditsUsed}`);
|
|
343
|
+
return {
|
|
344
|
+
domains: this.domains,
|
|
345
|
+
agentLabel: this.agentLabel,
|
|
346
|
+
relevantFiles,
|
|
347
|
+
summary,
|
|
348
|
+
webSearchSummary,
|
|
349
|
+
creditsUsed: this.creditsUsed,
|
|
350
|
+
success: success && !crashed,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Accumulates token usage from the chat session.
|
|
355
|
+
*/
|
|
356
|
+
updateUsage(result) {
|
|
357
|
+
const usage = result.response.usageMetadata?.();
|
|
358
|
+
if (usage && usage.totalTokenCount) {
|
|
359
|
+
this.creditsUsed += usage.totalTokenCount;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Investigation Orchestrator (Reducer) for Parallel Context Gathering.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates parallel investigation agents and merges their results into a single
|
|
5
|
+
* `ContextAgentResult` — the exact same type the existing single Context Agent
|
|
6
|
+
* produces. This makes parallel investigation completely transparent to the
|
|
7
|
+
* downstream execution phase (compression, context injection, model selection).
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle:
|
|
10
|
+
* 1. Creates a shared `ReadCache` instance
|
|
11
|
+
* 2. Spawns N `InvestigationAgentRunner` instances (one per agent assignment)
|
|
12
|
+
* 3. Runs all agents in parallel via `Promise.all()`
|
|
13
|
+
* 4. Reducer merges all `InvestigationResult` objects
|
|
14
|
+
* 5. Runs auto-trace reverse dependencies (same as single-agent path)
|
|
15
|
+
* 6. Returns merged `ContextAgentResult`
|
|
16
|
+
*
|
|
17
|
+
* Error handling: If any agent crashes, its results are discarded but the
|
|
18
|
+
* orchestrator continues with surviving agents. If ALL crash, returns null
|
|
19
|
+
* so the caller can fall back to the single Context Agent.
|
|
20
|
+
*/
|
|
21
|
+
import type { ContextAgentResult } from '../contextAgent.js';
|
|
22
|
+
import type { AgentAssignment } from '../investigationComplexity.js';
|
|
23
|
+
export declare class InvestigationOrchestrator {
|
|
24
|
+
/**
|
|
25
|
+
* Runs parallel investigation across multiple domain-scoped agents,
|
|
26
|
+
* merges their results, and returns a unified `ContextAgentResult`.
|
|
27
|
+
*
|
|
28
|
+
* @param userRequest - The original user prompt.
|
|
29
|
+
* @param agentAssignments - PM-generated agent assignments with domains.
|
|
30
|
+
* @param workspaceRoot - Absolute path to the workspace root.
|
|
31
|
+
* @param projectTree - Pre-generated project tree string.
|
|
32
|
+
* @param projectType - Detected project type (e.g., "Node.js / TypeScript").
|
|
33
|
+
* @param chatHistory - Recent conversation history.
|
|
34
|
+
* @param abortSignal - Signal to cancel all agents.
|
|
35
|
+
* @param onProgress - Callback for spinner/terminal updates.
|
|
36
|
+
* @returns Merged context result, or null if all agents failed.
|
|
37
|
+
*/
|
|
38
|
+
runParallelInvestigation(userRequest: string, agentAssignments: AgentAssignment[], workspaceRoot: string, projectTree: string, projectType: string, chatHistory: string, abortSignal: AbortSignal, onProgress?: (msg: string) => void): Promise<ContextAgentResult | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Merges multiple `InvestigationResult` objects into a single `ContextAgentResult`.
|
|
41
|
+
*
|
|
42
|
+
* - Unions all `relevantFiles` maps (Map.set handles dedup naturally)
|
|
43
|
+
* - Concatenates summaries with domain headers
|
|
44
|
+
* - Concatenates web search summaries
|
|
45
|
+
* - Caps total files at MAX_TOTAL_FILES
|
|
46
|
+
*/
|
|
47
|
+
private reduceResults;
|
|
48
|
+
/**
|
|
49
|
+
* Auto-traces reverse dependencies for all discovered files.
|
|
50
|
+
* Identical logic to the single-agent path in contextAgent.ts (lines 335-374).
|
|
51
|
+
*/
|
|
52
|
+
private autoTraceReverseDeps;
|
|
53
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Investigation Orchestrator (Reducer) for Parallel Context Gathering.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates parallel investigation agents and merges their results into a single
|
|
5
|
+
* `ContextAgentResult` — the exact same type the existing single Context Agent
|
|
6
|
+
* produces. This makes parallel investigation completely transparent to the
|
|
7
|
+
* downstream execution phase (compression, context injection, model selection).
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle:
|
|
10
|
+
* 1. Creates a shared `ReadCache` instance
|
|
11
|
+
* 2. Spawns N `InvestigationAgentRunner` instances (one per agent assignment)
|
|
12
|
+
* 3. Runs all agents in parallel via `Promise.all()`
|
|
13
|
+
* 4. Reducer merges all `InvestigationResult` objects
|
|
14
|
+
* 5. Runs auto-trace reverse dependencies (same as single-agent path)
|
|
15
|
+
* 6. Returns merged `ContextAgentResult`
|
|
16
|
+
*
|
|
17
|
+
* Error handling: If any agent crashes, its results are discarded but the
|
|
18
|
+
* orchestrator continues with surviving agents. If ALL crash, returns null
|
|
19
|
+
* so the caller can fall back to the single Context Agent.
|
|
20
|
+
*/
|
|
21
|
+
import * as p from '@clack/prompts';
|
|
22
|
+
import pc from 'picocolors';
|
|
23
|
+
import { ReadCache } from './readCache.js';
|
|
24
|
+
import { InvestigationAgentRunner } from './investigationAgent.js';
|
|
25
|
+
import { readFile } from '../agent-tools.js';
|
|
26
|
+
import { buildDependencyGraph } from '../../utils/dependencyTracer.js';
|
|
27
|
+
import { debugLog } from '../../utils/logger.js';
|
|
28
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
29
|
+
/** Maximum total relevant files across all agents after merge. */
|
|
30
|
+
const MAX_TOTAL_FILES = 15;
|
|
31
|
+
// ─── Investigation Orchestrator ──────────────────────────────────────
|
|
32
|
+
export class InvestigationOrchestrator {
|
|
33
|
+
/**
|
|
34
|
+
* Runs parallel investigation across multiple domain-scoped agents,
|
|
35
|
+
* merges their results, and returns a unified `ContextAgentResult`.
|
|
36
|
+
*
|
|
37
|
+
* @param userRequest - The original user prompt.
|
|
38
|
+
* @param agentAssignments - PM-generated agent assignments with domains.
|
|
39
|
+
* @param workspaceRoot - Absolute path to the workspace root.
|
|
40
|
+
* @param projectTree - Pre-generated project tree string.
|
|
41
|
+
* @param projectType - Detected project type (e.g., "Node.js / TypeScript").
|
|
42
|
+
* @param chatHistory - Recent conversation history.
|
|
43
|
+
* @param abortSignal - Signal to cancel all agents.
|
|
44
|
+
* @param onProgress - Callback for spinner/terminal updates.
|
|
45
|
+
* @returns Merged context result, or null if all agents failed.
|
|
46
|
+
*/
|
|
47
|
+
async runParallelInvestigation(userRequest, agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress) {
|
|
48
|
+
const agentCount = agentAssignments.length;
|
|
49
|
+
p.log.info(pc.cyan(`🔍 Parallel Investigation (${agentCount} agents)`) +
|
|
50
|
+
'\n' +
|
|
51
|
+
agentAssignments
|
|
52
|
+
.map((a, i) => pc.dim(` Agent ${i + 1}/${agentCount}: ${a.agentLabel} → [${a.domains.join(', ')}]`))
|
|
53
|
+
.join('\n'));
|
|
54
|
+
// 1. Create shared read cache
|
|
55
|
+
const readCache = new ReadCache();
|
|
56
|
+
// 2. Spawn investigation agents
|
|
57
|
+
const agents = agentAssignments.map((assignment) => new InvestigationAgentRunner(assignment.agentLabel, assignment.domains, workspaceRoot, readCache, projectTree, projectType));
|
|
58
|
+
// 3. Run all agents in parallel
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
const results = await Promise.all(agents.map((agent, i) => agent.execute(userRequest, chatHistory, abortSignal, (msg) => {
|
|
61
|
+
if (onProgress) {
|
|
62
|
+
onProgress(`Agent ${i + 1}/${agentCount}: ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
})));
|
|
65
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
66
|
+
// 4. Filter out crashed agents
|
|
67
|
+
const successfulResults = results.filter((r) => r.success);
|
|
68
|
+
const failedResults = results.filter((r) => !r.success);
|
|
69
|
+
if (failedResults.length > 0) {
|
|
70
|
+
for (const failed of failedResults) {
|
|
71
|
+
debugLog(`InvestigationOrchestrator: Agent "${failed.agentLabel}" failed: ${failed.summary}`);
|
|
72
|
+
}
|
|
73
|
+
if (onProgress) {
|
|
74
|
+
onProgress(`${failedResults.length} agent(s) failed — using results from ${successfulResults.length} surviving agent(s)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// If ALL agents crashed, signal the caller to fall back
|
|
78
|
+
if (successfulResults.length === 0) {
|
|
79
|
+
p.log.warn(pc.yellow('All investigation agents failed. Falling back to single Context Agent.'));
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// 5. Reduce: Merge results
|
|
83
|
+
const mergedResult = this.reduceResults(successfulResults, projectTree, projectType);
|
|
84
|
+
// 6. Auto-trace reverse dependencies (same logic as contextAgent.ts)
|
|
85
|
+
const allSelectedFiles = Array.from(mergedResult.relevantFiles.keys());
|
|
86
|
+
await this.autoTraceReverseDeps(workspaceRoot, allSelectedFiles, mergedResult.relevantFiles, onProgress);
|
|
87
|
+
// 7. Log summary
|
|
88
|
+
const cacheStats = readCache.getStats();
|
|
89
|
+
const totalTokens = results.reduce((sum, r) => sum + r.creditsUsed, 0);
|
|
90
|
+
const totalFilesBeforeDedup = results.reduce((sum, r) => sum + r.relevantFiles.size, 0);
|
|
91
|
+
p.log.info(`${pc.green('✓')} Parallel Investigation complete.\n` +
|
|
92
|
+
` Agents: ${agentCount} (${failedResults.length} failed) | ` +
|
|
93
|
+
`Files: ${mergedResult.relevantFiles.size} (deduped from ${totalFilesBeforeDedup}) | ` +
|
|
94
|
+
`Cache hits: ${cacheStats.hitCount}\n` +
|
|
95
|
+
` Duration: ${duration}s | Tokens: ${totalTokens.toLocaleString()}`);
|
|
96
|
+
return mergedResult;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Merges multiple `InvestigationResult` objects into a single `ContextAgentResult`.
|
|
100
|
+
*
|
|
101
|
+
* - Unions all `relevantFiles` maps (Map.set handles dedup naturally)
|
|
102
|
+
* - Concatenates summaries with domain headers
|
|
103
|
+
* - Concatenates web search summaries
|
|
104
|
+
* - Caps total files at MAX_TOTAL_FILES
|
|
105
|
+
*/
|
|
106
|
+
reduceResults(results, projectTree, projectType) {
|
|
107
|
+
const mergedFiles = new Map();
|
|
108
|
+
const summaryParts = [];
|
|
109
|
+
let webSearchSummary = '';
|
|
110
|
+
for (const result of results) {
|
|
111
|
+
// Merge files
|
|
112
|
+
for (const [filePath, content] of result.relevantFiles) {
|
|
113
|
+
if (mergedFiles.size >= MAX_TOTAL_FILES)
|
|
114
|
+
break;
|
|
115
|
+
if (!mergedFiles.has(filePath)) {
|
|
116
|
+
mergedFiles.set(filePath, content);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Merge summaries with domain headers
|
|
120
|
+
const domainHeader = `[${result.agentLabel}: ${result.domains.join(', ')}]`;
|
|
121
|
+
summaryParts.push(`${domainHeader}\n${result.summary}`);
|
|
122
|
+
// Merge web search results
|
|
123
|
+
if (result.webSearchSummary) {
|
|
124
|
+
webSearchSummary += result.webSearchSummary;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const combinedSummary = summaryParts.join('\n\n');
|
|
128
|
+
debugLog(`InvestigationOrchestrator Reducer: Merged ${results.length} results → ` +
|
|
129
|
+
`${mergedFiles.size} files, summary ${combinedSummary.length} chars`);
|
|
130
|
+
return {
|
|
131
|
+
projectTree,
|
|
132
|
+
projectType,
|
|
133
|
+
relevantFiles: mergedFiles,
|
|
134
|
+
summary: combinedSummary,
|
|
135
|
+
webSearchSummary: webSearchSummary || undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Auto-traces reverse dependencies for all discovered files.
|
|
140
|
+
* Identical logic to the single-agent path in contextAgent.ts (lines 335-374).
|
|
141
|
+
*/
|
|
142
|
+
async autoTraceReverseDeps(workspaceRoot, selectedFiles, relevantFiles, onProgress) {
|
|
143
|
+
try {
|
|
144
|
+
const graph = await buildDependencyGraph(workspaceRoot);
|
|
145
|
+
const autoDiscovered = new Set();
|
|
146
|
+
for (const filePath of selectedFiles) {
|
|
147
|
+
const reverseDeps = graph.getImportedBy(filePath);
|
|
148
|
+
for (const dep of reverseDeps) {
|
|
149
|
+
if (!selectedFiles.includes(dep) && !autoDiscovered.has(dep)) {
|
|
150
|
+
autoDiscovered.add(dep);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Merge auto-discovered dependents, respecting the file cap
|
|
155
|
+
const remaining = MAX_TOTAL_FILES - relevantFiles.size;
|
|
156
|
+
let added = 0;
|
|
157
|
+
for (const dep of autoDiscovered) {
|
|
158
|
+
if (added >= remaining)
|
|
159
|
+
break;
|
|
160
|
+
if (!relevantFiles.has(dep)) {
|
|
161
|
+
const readResult = await readFile(workspaceRoot, dep);
|
|
162
|
+
if (!readResult.error) {
|
|
163
|
+
relevantFiles.set(dep, { text: readResult.output, inlineData: readResult.inlineData });
|
|
164
|
+
added++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (added > 0) {
|
|
169
|
+
const depMsg = `Auto-traced ${added} reverse dependent(s) into context`;
|
|
170
|
+
if (onProgress)
|
|
171
|
+
onProgress(depMsg);
|
|
172
|
+
else
|
|
173
|
+
debugLog(`InvestigationOrchestrator: ${depMsg}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Dependency tracing is best-effort — don't block investigation
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|