minovative-mind-cli 2.10.0 → 2.11.2

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 (34) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/chat.d.ts +1 -1
  3. package/dist/commands/chat.js +2 -1
  4. package/dist/services/agent/commandApproval.js +5 -2
  5. package/dist/services/agent/slashCommands.js +213 -51
  6. package/dist/services/agent-tools.d.ts +4 -3
  7. package/dist/services/agent-tools.js +32 -79
  8. package/dist/services/agent.d.ts +5 -6
  9. package/dist/services/agent.js +38 -15
  10. package/dist/services/ai.d.ts +25 -0
  11. package/dist/services/ai.js +253 -2
  12. package/dist/services/chatHistoryService.d.ts +95 -2
  13. package/dist/services/chatHistoryService.js +236 -9
  14. package/dist/services/contextAgent.js +184 -89
  15. package/dist/services/orchestration/investigationAgent.js +100 -84
  16. package/dist/services/orchestration/investigationOrchestrator.js +6 -2
  17. package/dist/services/orchestration/orchestrator.js +6 -3
  18. package/dist/services/orchestration/scopedTools.js +5 -0
  19. package/dist/services/orchestration/subAgent.d.ts +31 -1
  20. package/dist/services/orchestration/subAgent.js +153 -2
  21. package/dist/services/userProfileService.d.ts +97 -0
  22. package/dist/services/userProfileService.js +410 -0
  23. package/dist/utils/analysisRunner.d.ts +29 -0
  24. package/dist/utils/analysisRunner.js +200 -5
  25. package/dist/utils/contextPrompts.d.ts +19 -3
  26. package/dist/utils/contextPrompts.js +144 -26
  27. package/dist/utils/historyPrompt.d.ts +92 -1
  28. package/dist/utils/historyPrompt.js +166 -2
  29. package/dist/utils/symbolExtractor.d.ts +12 -0
  30. package/dist/utils/symbolExtractor.js +946 -0
  31. package/dist/utils/systemPrompts.d.ts +6 -4
  32. package/dist/utils/systemPrompts.js +77 -9
  33. package/oclif.manifest.json +2 -2
  34. package/package.json +1 -1
@@ -135,56 +135,62 @@ export class InvestigationAgentRunner {
135
135
  success = true;
136
136
  break;
137
137
  }
138
- const functionResponses = [];
139
- for (const call of functionCalls) {
140
- if (crashed || abortSignal.aborted)
141
- break;
138
+ if (onTool) {
139
+ for (const call of functionCalls) {
140
+ onTool(formatToolCall(call.name, (call.args || {})));
141
+ }
142
+ }
143
+ // Execute tool calls concurrently while preserving 1:1 Gemini response positional ordering
144
+ const functionResponses = await Promise.all(functionCalls.map(async (call) => {
145
+ if (crashed || abortSignal.aborted) {
146
+ return {
147
+ functionResponse: {
148
+ name: call.name,
149
+ response: { error: 'Operation aborted or agent stopped.' },
150
+ },
151
+ };
152
+ }
142
153
  this.pingHeartbeat();
143
- const args = call.args;
154
+ const args = (call.args || {});
144
155
  const logPrefix = `[${this.agentLabel}]`;
145
- if (onTool) {
146
- onTool(formatToolCall(call.name, args));
147
- }
148
156
  if (call.name === 'finish_investigation') {
149
157
  summary = args.summary || '';
150
158
  const filesToRead = args.relevantFiles || [];
151
159
  if (onProgress)
152
160
  onProgress(`${logPrefix} Finished investigation (${filesToRead.length} files)`);
153
161
  debugLog(`InvestigationAgent ${logPrefix}: Finished. Selected files: ${JSON.stringify(filesToRead)}`);
154
- for (const filePath of filesToRead) {
155
- if (!relevantFiles.has(filePath)) {
156
- // Check shared cache first
157
- if (this.readCache.has(filePath)) {
158
- relevantFiles.set(filePath, this.readCache.get(filePath));
159
- }
160
- else {
161
- const readResult = await readFile(this.workspaceRoot, filePath);
162
- if (!readResult.error) {
163
- const contentObj = { text: readResult.output, inlineData: readResult.inlineData };
164
- relevantFiles.set(filePath, contentObj);
165
- this.readCache.set(filePath, contentObj);
166
- }
162
+ await Promise.all(filesToRead.map(async (filePath) => {
163
+ if (this.readCache.has(filePath)) {
164
+ relevantFiles.set(filePath, this.readCache.get(filePath));
165
+ }
166
+ else {
167
+ const readResult = await readFile(this.workspaceRoot, filePath);
168
+ if (!readResult.error) {
169
+ const contentObj = { text: readResult.output, inlineData: readResult.inlineData };
170
+ relevantFiles.set(filePath, contentObj);
171
+ this.readCache.set(filePath, contentObj);
167
172
  }
168
173
  }
169
- }
174
+ }));
170
175
  isFinished = true;
171
176
  success = true;
172
- functionResponses.push({
173
- functionResponse: { name: call.name, response: { output: 'Investigation finished.' } },
174
- });
175
- break;
177
+ return {
178
+ functionResponse: {
179
+ name: call.name,
180
+ response: { output: 'Investigation finished.' },
181
+ },
182
+ };
176
183
  }
177
184
  else if (call.name === 'select_files') {
178
185
  const filesToRead = args.files || [];
179
186
  if (onProgress)
180
187
  onProgress(`${logPrefix} Selected ${filesToRead.length} files`);
181
- let output = '';
182
- for (const filePath of filesToRead) {
188
+ const fileOutputs = await Promise.all(filesToRead.map(async (filePath) => {
183
189
  // Check cache first
184
190
  if (this.readCache.has(filePath)) {
185
191
  const cached = this.readCache.get(filePath);
186
192
  relevantFiles.set(filePath, cached);
187
- output += `\n--- File: ${filePath} ---\n${cached.text}\n`;
193
+ return `\n--- File: ${filePath} ---\n${cached.text}\n`;
188
194
  }
189
195
  else {
190
196
  const readResult = await readFile(this.workspaceRoot, filePath);
@@ -192,99 +198,98 @@ export class InvestigationAgentRunner {
192
198
  const contentObj = { text: readResult.output, inlineData: readResult.inlineData };
193
199
  relevantFiles.set(filePath, contentObj);
194
200
  this.readCache.set(filePath, contentObj);
195
- output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
201
+ return `\n--- File: ${filePath} ---\n${readResult.output}\n`;
196
202
  }
197
203
  else {
198
- output += `\n--- File: ${filePath} ---\nError: ${readResult.error}\n`;
204
+ return `\n--- File: ${filePath} (Error: ${readResult.error}) ---\n`;
199
205
  }
200
206
  }
201
- }
202
- functionResponses.push({
203
- functionResponse: { name: call.name, response: { output: output || 'No files read.' } },
204
- });
207
+ }));
208
+ const output = fileOutputs.join('');
209
+ return {
210
+ functionResponse: {
211
+ name: call.name,
212
+ response: { output: output || 'No files selected.' },
213
+ },
214
+ };
205
215
  }
206
216
  else if (call.name === 'list_directory') {
207
217
  if (onProgress)
208
- onProgress(`${logPrefix} Listing: ${args.dirPath}`);
209
- const listRes = await listDirectory(this.workspaceRoot, args.dirPath, args.maxDepth || 1);
210
- functionResponses.push({
218
+ onProgress(`${logPrefix} Listing directory: ${args.dirPath || '.'}`);
219
+ const listRes = await listDirectory(this.workspaceRoot, args.dirPath || '.', args.maxDepth);
220
+ return {
211
221
  functionResponse: {
212
222
  name: call.name,
213
- response: { output: listRes.output, ...(listRes.error ? { error: listRes.error } : {}) },
223
+ response: {
224
+ output: listRes.output,
225
+ ...(listRes.error ? { error: listRes.error } : {}),
226
+ },
214
227
  },
215
- });
228
+ };
216
229
  }
217
230
  else if (call.name === 'search_codebase') {
218
231
  if (onProgress)
219
232
  onProgress(`${logPrefix} Searching: "${args.pattern}"`);
220
- const grepRes = await grepSearch(this.workspaceRoot, args.pattern, args.fileGlob);
221
- functionResponses.push({
222
- functionResponse: { name: call.name, response: { output: grepRes.error ? grepRes.error : grepRes.output } },
223
- });
233
+ const grepRes = await grepSearch(this.workspaceRoot, args.pattern || '', args.fileGlob, args.fixedStrings, args.dirPath);
234
+ return {
235
+ functionResponse: {
236
+ name: call.name,
237
+ response: { output: grepRes.error ? grepRes.error : grepRes.output },
238
+ },
239
+ };
224
240
  }
225
241
  else if (call.name === 'read_file') {
226
- const filePath = args.filePath;
242
+ const filePath = args.filePath || '';
243
+ const startLine = args.startLine;
244
+ const endLine = args.endLine;
245
+ const targetElements = args.targetElements;
227
246
  if (onProgress)
228
247
  onProgress(`${logPrefix} Reading: ${filePath}`);
229
- // Check cache for full-file reads (no line range or target elements)
230
- if (!args.startLine && !args.endLine && !args.targetElements && this.readCache.has(filePath)) {
248
+ // Check read cache only for full-file reads
249
+ if (this.readCache.has(filePath) && !startLine && !endLine && (!targetElements || targetElements.length === 0)) {
231
250
  const cached = this.readCache.get(filePath);
232
251
  relevantFiles.set(filePath, cached);
233
- functionResponses.push({ functionResponse: { name: call.name, response: { output: cached.text } } });
252
+ return {
253
+ functionResponse: {
254
+ name: call.name,
255
+ response: { output: cached.text },
256
+ },
257
+ };
234
258
  }
235
- else {
236
- const readRes = await readFile(this.workspaceRoot, filePath, args.startLine, args.endLine, args.targetElements);
237
- if (!readRes.error) {
238
- const contentObj = { text: readRes.output, inlineData: readRes.inlineData };
239
- relevantFiles.set(filePath, contentObj);
240
- // Only cache full-file reads (partial reads shouldn't overwrite full content)
241
- if (!args.startLine && !args.endLine && !args.targetElements) {
242
- this.readCache.set(filePath, contentObj);
243
- }
244
- }
245
- functionResponses.push({
246
- functionResponse: { name: call.name, response: { output: readRes.error ? readRes.error : readRes.output } },
247
- });
248
- }
249
- }
250
- else if (call.name === 'perform_web_search') {
251
- if (onProgress)
252
- onProgress(`${logPrefix} Web search: "${args.query}"`);
253
- try {
254
- const { createWebSearchAgentSession } = await import('../ai.js');
255
- const webSession = createWebSearchAgentSession();
256
- const webResult = await webSession.sendMessage(`Please search the web for the following query and summarize your findings:\n"${args.query}"`, undefined, abortSignal, () => this.pingHeartbeat());
257
- const searchSummary = webResult.response.text()?.trim() || 'No relevant information found.';
258
- webSearchSummary += `\nQuery: ${args.query}\nFindings:\n${searchSummary}\n`;
259
- functionResponses.push({ functionResponse: { name: call.name, response: { output: searchSummary } } });
260
- }
261
- catch (e) {
262
- functionResponses.push({
263
- functionResponse: { name: call.name, response: { error: e.message || 'Failed to search the web' } },
264
- });
259
+ const readRes = await readFile(this.workspaceRoot, filePath, startLine, endLine, targetElements);
260
+ if (!readRes.error && !startLine && !endLine && (!targetElements || targetElements.length === 0)) {
261
+ const contentObj = { text: readRes.output, inlineData: readRes.inlineData };
262
+ relevantFiles.set(filePath, contentObj);
263
+ this.readCache.set(filePath, contentObj);
265
264
  }
265
+ return {
266
+ functionResponse: {
267
+ name: call.name,
268
+ response: { output: readRes.error ? readRes.error : readRes.output },
269
+ },
270
+ };
266
271
  }
267
272
  else if (call.name === 'find_dependencies') {
268
273
  if (onProgress)
269
274
  onProgress(`${logPrefix} Tracing dependencies: ${args.filePath}`);
270
275
  const depResult = await traceDependencies(this.workspaceRoot, args.filePath, args.direction, args.maxDepth);
271
- functionResponses.push({
276
+ return {
272
277
  functionResponse: {
273
278
  name: call.name,
274
279
  response: { output: depResult.error ? depResult.error : depResult.output },
275
280
  },
276
- });
281
+ };
277
282
  }
278
283
  else if (call.name === 'find_recent_changes') {
279
284
  if (onProgress)
280
285
  onProgress(`${logPrefix} Finding recent changes`);
281
286
  const recentRes = await findRecentChanges(this.workspaceRoot, args.dirPath, args.minutes, args.maxDepth);
282
- functionResponses.push({
287
+ return {
283
288
  functionResponse: {
284
289
  name: call.name,
285
290
  response: { output: recentRes.error ? recentRes.error : recentRes.output },
286
291
  },
287
- });
292
+ };
288
293
  }
289
294
  else if (call.name === 'run_analysis_script') {
290
295
  if (onProgress)
@@ -295,9 +300,20 @@ export class InvestigationAgentRunner {
295
300
  const output = analysisResult.exitCode === 0
296
301
  ? analysisResult.stdout || '(script produced no output)'
297
302
  : `Script failed (exit ${analysisResult.exitCode}):\n${analysisResult.stderr}`;
298
- functionResponses.push({ functionResponse: { name: call.name, response: { output } } });
303
+ return {
304
+ functionResponse: {
305
+ name: call.name,
306
+ response: { output },
307
+ },
308
+ };
299
309
  }
300
- }
310
+ return {
311
+ functionResponse: {
312
+ name: call.name,
313
+ response: { output: `Unknown tool: ${call.name}` },
314
+ },
315
+ };
316
+ }));
301
317
  if (crashed || abortSignal.aborted || isFinished)
302
318
  break;
303
319
  // Feed tool results back to the model
@@ -64,6 +64,8 @@ export class InvestigationOrchestrator {
64
64
  const MAX_CONCURRENT = 2;
65
65
  const results = [];
66
66
  for (let i = 0; i < agents.length; i += MAX_CONCURRENT) {
67
+ if (abortSignal.aborted)
68
+ break;
67
69
  const chunk = agents.slice(i, i + MAX_CONCURRENT);
68
70
  const chunkAssignments = agentAssignments.slice(i, i + MAX_CONCURRENT);
69
71
  const chunkPromises = chunk.map((agent, chunkIndex) => {
@@ -107,8 +109,10 @@ export class InvestigationOrchestrator {
107
109
  // 5. Reduce: Merge results
108
110
  const mergedResult = this.reduceResults(successfulResults, projectTree, projectType);
109
111
  // 6. Auto-trace reverse dependencies (same logic as contextAgent.ts)
110
- const allSelectedFiles = Array.from(mergedResult.relevantFiles.keys());
111
- await this.autoTraceReverseDeps(workspaceRoot, allSelectedFiles, mergedResult.relevantFiles, onProgress);
112
+ if (!abortSignal.aborted) {
113
+ const allSelectedFiles = Array.from(mergedResult.relevantFiles.keys());
114
+ await this.autoTraceReverseDeps(workspaceRoot, allSelectedFiles, mergedResult.relevantFiles, onProgress);
115
+ }
112
116
  // 7. Log summary
113
117
  const cacheStats = readCache.getStats();
114
118
  const totalTokens = results.reduce((sum, r) => sum + r.creditsUsed, 0);
@@ -27,12 +27,13 @@ You must output ONLY raw JSON representing the TaskGraph object. Do not wrap in
27
27
  </identity>
28
28
 
29
29
  <requirements>
30
- - Break the work down logically based on file isolation and dependency chains.
31
- - Tasks that don't depend on each other will run in parallel.
30
+ - Break the work down logically based on file isolation, domain boundaries, and dependency chains.
31
+ - Tasks that don't depend on each other will run in parallel (up to 2 concurrent agents per wave).
32
32
  - If a task depends on another, it must list its ID in "dependsOn".
33
33
  - Do NOT create cyclic dependencies (A -> B -> A).
34
34
  - If the entire objective is very simple and only requires modifying 1-2 files sequentially, output a graph with exactly 1 task.
35
- - Ensure 'targetFiles' lists all files the task will modify.
35
+ - Ensure 'targetFiles' lists all files the task will modify. Isolate targetFiles across parallel tasks in the same wave to avoid file lock contention.
36
+ - Use 'readOnlyFiles' for files the task needs to inspect without modifying.
36
37
  </requirements>
37
38
 
38
39
  <json_schema>
@@ -123,6 +124,8 @@ export class Orchestrator {
123
124
  const MAX_CONCURRENT = 2;
124
125
  const toolLogs = [];
125
126
  for (let i = 0; i < wave.taskIds.length; i += MAX_CONCURRENT) {
127
+ if (signal.aborted)
128
+ break;
126
129
  const chunk = wave.taskIds.slice(i, i + MAX_CONCURRENT);
127
130
  const wavePromises = chunk.map((taskId) => {
128
131
  const taskDef = graph.tasks.find((t) => t.id === taskId);
@@ -160,6 +160,11 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
160
160
  targetDesc = String(args.filePath);
161
161
  actionDesc = 'Deleted';
162
162
  }
163
+ else if (name === 'run_debug_script') {
164
+ targetDesc = String(args.language ?? 'node');
165
+ actionDesc = 'Ran debug script';
166
+ resultSummary = typeof result === 'object' && result?.error ? result.error : 'Completed debug script';
167
+ }
163
168
  else if (name === 'run_fuzz_probe') {
164
169
  targetDesc = String(args.language ?? 'node');
165
170
  actionDesc = 'Ran fuzz probe';
@@ -6,6 +6,25 @@
6
6
  */
7
7
  import { MessageBus } from './messageBus.js';
8
8
  import { FileLockRegistry } from './fileLockRegistry.js';
9
+ /**
10
+ * Scopes workspace file blocks within a context string by replacing full implementation
11
+ * bodies with compact AST declaration outlines (types, classes, function signatures),
12
+ * drastically conserving token overhead for parallel sub-agents.
13
+ *
14
+ * @param contextStr The raw context string containing workspace_file blocks.
15
+ * @returns Context string with scoped workspace_file blocks.
16
+ */
17
+ export declare function scopeContext(contextStr: string): string;
18
+ /**
19
+ * Generates a concise reference string for completed historical tool executions
20
+ * to eliminate token bloat while keeping execution history coherent for the model.
21
+ *
22
+ * @param toolName Name of the executed tool.
23
+ * @param output Output text or object from the tool execution.
24
+ * @param args Arguments passed to the tool call.
25
+ * @returns Compacted tool execution reference string.
26
+ */
27
+ export declare function createCompactToolReference(toolName: string, output: any, args?: Record<string, any>): string;
9
28
  /**
10
29
  * Result returned when a sub-agent completes its execution.
11
30
  */
@@ -43,7 +62,9 @@ export declare class SubAgentRunner {
43
62
  static readonly STALL_TIMEOUT_MS = 300000;
44
63
  constructor(taskId: string, intent: string, workspaceRoot: string, bus: MessageBus, locks: FileLockRegistry, globalContext: string, onProgress?: ((msg: string) => void) | undefined, onTool?: ((msg: string) => void) | undefined);
45
64
  /**
46
- * Constructs the base system instruction for this specific agent.
65
+ * Constructs the base system instruction for this specific agent,
66
+ * injecting scoped AST outlines for referenced workspace files to eliminate
67
+ * multi-megabyte context bloat during parallel sub-agent execution.
47
68
  */
48
69
  private buildSystemInstruction;
49
70
  /**
@@ -57,6 +78,15 @@ export declare class SubAgentRunner {
57
78
  * an error occurs, or the stall timeout is hit.
58
79
  */
59
80
  execute(signal: AbortSignal): Promise<SubAgentResult>;
81
+ /**
82
+ * Compacts older tool responses in the chat session history into concise references
83
+ * (e.g. `[Read X lines from file.ts - tool execution completed]`), drastically reducing
84
+ * context window bloat across multi-turn sub-agent executions while preserving
85
+ * Gemini's functionCall <-> functionResponse protocol.
86
+ *
87
+ * @param turnsToKeep Number of recent turns to preserve in full (default: 1).
88
+ */
89
+ compactOlderToolResponses(turnsToKeep?: number): void;
60
90
  /**
61
91
  * Accumulates token usage from the chat session.
62
92
  */
@@ -11,6 +11,108 @@ import { debugLog } from '../../utils/logger.js';
11
11
  import { runWithAgentId } from '../../utils/asyncContext.js';
12
12
  import { clearReadHistory } from '../../utils/fileReadGuard.js';
13
13
  import { formatToolCall } from '../agent/toolLoop.js';
14
+ import { extractDeclarationsOutline } from '../../utils/symbolExtractor.js';
15
+ import { sanitizeForCDATA } from '../../utils/contextPrompts.js';
16
+ /**
17
+ * Scopes workspace file blocks within a context string by replacing full implementation
18
+ * bodies with compact AST declaration outlines (types, classes, function signatures),
19
+ * drastically conserving token overhead for parallel sub-agents.
20
+ *
21
+ * @param contextStr The raw context string containing workspace_file blocks.
22
+ * @returns Context string with scoped workspace_file blocks.
23
+ */
24
+ export function scopeContext(contextStr) {
25
+ if (!contextStr)
26
+ return '';
27
+ return contextStr.replace(/<workspace_file path="([^"]+)"([^>]*)>[\s\r\n]*<content_data><!\[CDATA\[([\s\S]*?)\]\](?:\\u200B)?>[\s\r\n]*<\/content_data>[\s\r\n]*<\/workspace_file>/g, (match, filePath, attrs, rawContent) => {
28
+ if (attrs.includes('scoped="outline"'))
29
+ return match;
30
+ const unescaped = rawContent.replace(/\]\]\\u200B>/g, ']]>');
31
+ const outline = extractDeclarationsOutline(unescaped, filePath) || unescaped;
32
+ const sanitized = sanitizeForCDATA(outline);
33
+ return `<workspace_file path="${filePath}"${attrs} scoped="outline">\n<content_data><![CDATA[\n${sanitized}\n]]\\u200B></content_data>\n</workspace_file>`;
34
+ });
35
+ }
36
+ /**
37
+ * Locates the matching functionCall in the preceding model response entry.
38
+ *
39
+ * @param modelEntry The preceding Content entry from the model.
40
+ * @param toolName The name of the function call.
41
+ * @param partIndex Index of the function response part.
42
+ * @returns The matching functionCall object with args if found.
43
+ */
44
+ function findMatchingFunctionCall(modelEntry, toolName, partIndex) {
45
+ if (!modelEntry || modelEntry.role !== 'model' || !Array.isArray(modelEntry.parts))
46
+ return null;
47
+ const callParts = modelEntry.parts.filter((p) => p && typeof p === 'object' && 'functionCall' in p && p.functionCall);
48
+ if (partIndex < callParts.length) {
49
+ const call = callParts[partIndex].functionCall;
50
+ if (call && call.name === toolName)
51
+ return call;
52
+ }
53
+ // Fallback: search by toolName
54
+ const match = callParts.find((p) => p.functionCall?.name === toolName);
55
+ return match ? match.functionCall : null;
56
+ }
57
+ /**
58
+ * Generates a concise reference string for completed historical tool executions
59
+ * to eliminate token bloat while keeping execution history coherent for the model.
60
+ *
61
+ * @param toolName Name of the executed tool.
62
+ * @param output Output text or object from the tool execution.
63
+ * @param args Arguments passed to the tool call.
64
+ * @returns Compacted tool execution reference string.
65
+ */
66
+ export function createCompactToolReference(toolName, output, args) {
67
+ if (typeof output !== 'string') {
68
+ return `[${toolName} tool execution completed]`;
69
+ }
70
+ // If already compacted, return as-is
71
+ if (output.startsWith('[') && output.endsWith('completed]')) {
72
+ return output;
73
+ }
74
+ const lineCount = output.split('\n').length;
75
+ if (toolName === 'read_file') {
76
+ const file = args?.filePath ? `from ${args.filePath}` : 'from file';
77
+ return `[Read ${lineCount} lines ${file} - tool execution completed]`;
78
+ }
79
+ if (toolName === 'grep_search' || toolName === 'search_codebase') {
80
+ const pattern = args?.pattern ? `for "${args.pattern}"` : '';
81
+ return `[Grep search ${pattern} completed - ${lineCount} lines returned]`.replace(/\s+/g, ' ');
82
+ }
83
+ if (toolName === 'list_directory') {
84
+ const dir = args?.dirPath ? `for "${args.dirPath}"` : '';
85
+ return `[Directory listing ${dir} completed - ${lineCount} entries]`.replace(/\s+/g, ' ');
86
+ }
87
+ if (toolName === 'modify_file') {
88
+ const file = args?.filePath ? `${args.filePath}` : 'file';
89
+ return `[Modified ${file} - tool execution completed]`;
90
+ }
91
+ if (toolName === 'write_file') {
92
+ const file = args?.filePath ? `${args.filePath}` : 'file';
93
+ return `[Wrote ${file} - tool execution completed]`;
94
+ }
95
+ if (toolName === 'find_dependencies') {
96
+ const file = args?.filePath ? `for ${args.filePath}` : '';
97
+ return `[Dependency trace ${file} completed]`.replace(/\s+/g, ' ');
98
+ }
99
+ if (toolName === 'find_recent_changes') {
100
+ return `[Recent changes lookup completed]`;
101
+ }
102
+ if (toolName === 'run_command' ||
103
+ toolName === 'run_debug_script' ||
104
+ toolName === 'run_analysis_script' ||
105
+ toolName === 'run_fuzz_probe' ||
106
+ toolName === 'check_heap_delta' ||
107
+ toolName === 'check_behavioral_drift') {
108
+ const cmd = args?.command ? ` "${args.command.slice(0, 50)}"` : '';
109
+ return `[Command/Script${cmd} execution completed (${lineCount} lines output)]`;
110
+ }
111
+ if (output.length > 300) {
112
+ return `[${toolName} tool execution completed (${lineCount} lines)]`;
113
+ }
114
+ return output;
115
+ }
14
116
  /**
15
117
  * Executes a sub-agent task with full health monitoring, tool wrapping,
16
118
  * and orchestration integration.
@@ -52,9 +154,12 @@ export class SubAgentRunner {
52
154
  });
53
155
  }
54
156
  /**
55
- * Constructs the base system instruction for this specific agent.
157
+ * Constructs the base system instruction for this specific agent,
158
+ * injecting scoped AST outlines for referenced workspace files to eliminate
159
+ * multi-megabyte context bloat during parallel sub-agent execution.
56
160
  */
57
161
  buildSystemInstruction() {
162
+ const scopedContext = scopeContext(this.globalContext);
58
163
  return (`<identity>\n` +
59
164
  `You are an autonomous Senior software developer sub-agent executing a specific portion of a larger task.\n` +
60
165
  `</identity>\n\n` +
@@ -64,7 +169,7 @@ export class SubAgentRunner {
64
169
  `</task_info>\n\n` +
65
170
  `<reference_context>\n` +
66
171
  `DO NOT IMPLEMENT THIS FULL REQUEST. THIS IS JUST FOR CONTEXT.\n\n` +
67
- `${this.globalContext}\n` +
172
+ `${scopedContext}\n` +
68
173
  `</reference_context>\n\n` +
69
174
  `<critical_guidelines>\n` +
70
175
  `1. You are ONE worker in a team. Focus EXCLUSIVELY on your specific objective: "${this.intent}".\n` +
@@ -162,6 +267,10 @@ export class SubAgentRunner {
162
267
  }
163
268
  if (crashed || signal.aborted)
164
269
  break;
270
+ // Compact older tool responses in chat history before sending new turn
271
+ if (turns > 0) {
272
+ this.compactOlderToolResponses(1);
273
+ }
165
274
  this.pingHeartbeat();
166
275
  // Feed tool results back to the model
167
276
  turnResult = await this.chat.sendMessage(toolResponses, undefined, signal);
@@ -199,6 +308,48 @@ export class SubAgentRunner {
199
308
  };
200
309
  });
201
310
  }
311
+ /**
312
+ * Compacts older tool responses in the chat session history into concise references
313
+ * (e.g. `[Read X lines from file.ts - tool execution completed]`), drastically reducing
314
+ * context window bloat across multi-turn sub-agent executions while preserving
315
+ * Gemini's functionCall <-> functionResponse protocol.
316
+ *
317
+ * @param turnsToKeep Number of recent turns to preserve in full (default: 1).
318
+ */
319
+ compactOlderToolResponses(turnsToKeep = 1) {
320
+ const rawHistory = this.chat.getRawHistory();
321
+ // 1 turn = 1 user Content + 1 model Content pair (2 entries)
322
+ const cutoffIndex = Math.max(0, rawHistory.length - turnsToKeep * 2);
323
+ if (cutoffIndex <= 0)
324
+ return;
325
+ for (let i = 0; i < cutoffIndex; i++) {
326
+ const entry = rawHistory[i];
327
+ if (entry && entry.role === 'user' && Array.isArray(entry.parts)) {
328
+ const prevModelEntry = i > 0 ? rawHistory[i - 1] : undefined;
329
+ let funcRespIdx = 0;
330
+ for (const part of entry.parts) {
331
+ if (part && typeof part === 'object' && 'functionResponse' in part && part.functionResponse) {
332
+ const funcResp = part.functionResponse;
333
+ const toolName = funcResp.name || 'tool';
334
+ const matchingCall = findMatchingFunctionCall(prevModelEntry, toolName, funcRespIdx);
335
+ const args = matchingCall?.args;
336
+ if (funcResp.response && typeof funcResp.response === 'object') {
337
+ const respObj = funcResp.response;
338
+ if (typeof respObj.output === 'string') {
339
+ respObj.output = createCompactToolReference(toolName, respObj.output, args);
340
+ }
341
+ if (typeof respObj.result === 'string') {
342
+ respObj.result = createCompactToolReference(toolName, respObj.result, args);
343
+ }
344
+ }
345
+ funcRespIdx++;
346
+ }
347
+ }
348
+ }
349
+ }
350
+ // Also trigger proxy chat session pruning to collapse any remaining long fields
351
+ this.chat.pruneToolOutputHistory();
352
+ }
202
353
  /**
203
354
  * Accumulates token usage from the chat session.
204
355
  */