minovative-mind-cli 2.10.0 → 2.11.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/dist/services/agent/commandApproval.js +5 -2
- package/dist/services/agent/slashCommands.js +78 -51
- package/dist/services/agent-tools.d.ts +4 -3
- package/dist/services/agent-tools.js +32 -79
- package/dist/services/agent.d.ts +5 -6
- package/dist/services/agent.js +11 -15
- package/dist/services/ai.d.ts +19 -0
- package/dist/services/ai.js +108 -1
- package/dist/services/chatHistoryService.d.ts +95 -2
- package/dist/services/chatHistoryService.js +236 -9
- package/dist/services/contextAgent.js +161 -77
- package/dist/services/orchestration/investigationAgent.js +101 -84
- package/dist/services/orchestration/investigationOrchestrator.js +6 -2
- package/dist/services/orchestration/orchestrator.js +6 -3
- package/dist/services/orchestration/scopedTools.js +5 -0
- package/dist/services/orchestration/subAgent.d.ts +31 -1
- package/dist/services/orchestration/subAgent.js +153 -2
- package/dist/utils/analysisRunner.d.ts +29 -0
- package/dist/utils/analysisRunner.js +200 -5
- package/dist/utils/contextPrompts.d.ts +19 -3
- package/dist/utils/contextPrompts.js +144 -26
- package/dist/utils/historyPrompt.d.ts +92 -1
- package/dist/utils/historyPrompt.js +166 -2
- package/dist/utils/symbolExtractor.d.ts +12 -0
- package/dist/utils/symbolExtractor.js +946 -0
- package/dist/utils/systemPrompts.d.ts +3 -3
- package/dist/utils/systemPrompts.js +10 -7
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -33,6 +33,9 @@ export function isCommandDestructive(command) {
|
|
|
33
33
|
// Massive permission shifts (chmod 777, recursive chowns)
|
|
34
34
|
/\bchmod\s+(?:-[R\w]*\s+)?777\b/i,
|
|
35
35
|
/\bchown\s+-[R\w]*\b/i,
|
|
36
|
+
// Privileged root escalation commands (require explicit interactive password / system-level access)
|
|
37
|
+
/\bsudo\b/i,
|
|
38
|
+
/\bsu\s+/i,
|
|
36
39
|
];
|
|
37
40
|
return destructivePatterns.some((pattern) => pattern.test(command));
|
|
38
41
|
}
|
|
@@ -49,10 +52,10 @@ export function isCommandSafe(command) {
|
|
|
49
52
|
/^\s*echo\b/i,
|
|
50
53
|
/^\s*grep\b/i,
|
|
51
54
|
/^\s*find\b/i,
|
|
52
|
-
/^\s*npm\s+(install|i|ci|run\b)/i,
|
|
55
|
+
/^\s*npm\s+(install|i|ci|run\b|test\b|t\b)/i,
|
|
53
56
|
/^\s*yarn\s+(install|add|build|lint|test|run\b)/i,
|
|
54
57
|
/^\s*pnpm\s+(install|i|add|build|lint|test|run\b)/i,
|
|
55
|
-
/^\s*bun\s+(install|i|add|run\b)/i,
|
|
58
|
+
/^\s*bun\s+(install|i|add|run\b|test\b)/i,
|
|
56
59
|
/^\s*cargo\s+(build|check|add|test|run\b)/i,
|
|
57
60
|
/^\s*go\s+(mod|get|build|test|run\b)/i,
|
|
58
61
|
/^\s*pip\s+(install|list|show)\b/i,
|
|
@@ -563,73 +563,87 @@ export async function handleSlashCommand(command, context) {
|
|
|
563
563
|
p.log.info(`${pc.dim('Workspace:')} ${brandFg(workspaceRoot)}`);
|
|
564
564
|
p.log.info(`${pc.dim('Commands:')} Type ${pc.yellow('/')} to open the command menu and "${pc.yellow('stop')}" to stop the ai generation. Type ${pc.yellow('exit')} to leave.`);
|
|
565
565
|
p.log.success(`Resumed session: ${session.title}`);
|
|
566
|
-
// Print the loaded history
|
|
567
|
-
|
|
566
|
+
// Print the loaded history cleanly: only user and AI conversational messages,
|
|
567
|
+
// with a clean indicator label if the turn involved tool/code changes.
|
|
568
|
+
let turnHadModifications = false;
|
|
569
|
+
let turnHadInspections = false;
|
|
570
|
+
let renderedIndicatorForTurn = false;
|
|
568
571
|
for (const item of session.history) {
|
|
569
572
|
if (item.role === 'user') {
|
|
573
|
+
const hasFunctionResponse = item.parts?.some((p) => p.functionResponse);
|
|
574
|
+
if (hasFunctionResponse) {
|
|
575
|
+
// Internal tool response from the execution loop — do not display as a user message
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
570
578
|
const textParts = item.parts
|
|
571
|
-
|
|
579
|
+
?.filter((p) => p.text)
|
|
572
580
|
.map((p) => p.text)
|
|
573
581
|
.join('');
|
|
574
|
-
if (textParts &&
|
|
582
|
+
if (textParts &&
|
|
583
|
+
!textParts.includes('SYSTEM CHECK: Please objectively verify') &&
|
|
584
|
+
!textParts.includes('[PREVIOUS CONVERSATION SUMMARY]') &&
|
|
585
|
+
!textParts.startsWith('[USER INTERRUPTION]')) {
|
|
586
|
+
// Reset turn tool flags for the new user prompt
|
|
587
|
+
turnHadModifications = false;
|
|
588
|
+
turnHadInspections = false;
|
|
589
|
+
renderedIndicatorForTurn = false;
|
|
575
590
|
p.log.step(pc.bgBlue(pc.white(` ${textParts} `)));
|
|
576
591
|
}
|
|
577
592
|
}
|
|
578
593
|
else if (item.role === 'model') {
|
|
579
|
-
//
|
|
580
|
-
for (const part of item.parts) {
|
|
594
|
+
// Track if any tool actions were called in this turn
|
|
595
|
+
for (const part of item.parts || []) {
|
|
581
596
|
if (part.functionCall) {
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
p.log.message(` ${pc.dim(`[ ] ${index + 1}.`)} ${task}`);
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
else if (call.name === 'update_todo_status') {
|
|
593
|
-
const status = args.status;
|
|
594
|
-
const taskIndex = args.taskIndex;
|
|
595
|
-
const taskText = sessionTasks[taskIndex - 1] || 'completed';
|
|
596
|
-
let icon = '[ ]';
|
|
597
|
-
let color = pc.dim;
|
|
598
|
-
if (status?.toLowerCase() === 'completed' || status?.toLowerCase() === 'done') {
|
|
599
|
-
icon = '[x]';
|
|
600
|
-
color = pc.green;
|
|
601
|
-
}
|
|
602
|
-
else if (status?.toLowerCase() === 'in_progress' || status?.toLowerCase() === 'started') {
|
|
603
|
-
icon = '[/]';
|
|
604
|
-
color = pc.yellow;
|
|
605
|
-
}
|
|
606
|
-
else if (status?.toLowerCase() === 'failed' || status?.toLowerCase() === 'error') {
|
|
607
|
-
icon = '[!]';
|
|
608
|
-
color = pc.red;
|
|
609
|
-
}
|
|
610
|
-
p.log.message(color(` ${icon} ${taskIndex}. ${taskText}`));
|
|
597
|
+
const name = part.functionCall.name;
|
|
598
|
+
if (name === 'modify_file' ||
|
|
599
|
+
name === 'write_file' ||
|
|
600
|
+
name === 'delete_file' ||
|
|
601
|
+
name === 'rename_file' ||
|
|
602
|
+
name === 'run_command') {
|
|
603
|
+
turnHadModifications = true;
|
|
611
604
|
}
|
|
612
|
-
else
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
catch (e) {
|
|
618
|
-
argsStr = String(call.args);
|
|
619
|
-
}
|
|
620
|
-
const argsPreview = argsStr.slice(0, 50);
|
|
621
|
-
p.log.message(pc.dim(` ├─ Executed tool`));
|
|
605
|
+
else if (name === 'read_file' ||
|
|
606
|
+
name === 'grep_search' ||
|
|
607
|
+
name === 'list_directory' ||
|
|
608
|
+
name === 'find_dependencies') {
|
|
609
|
+
turnHadInspections = true;
|
|
622
610
|
}
|
|
623
611
|
}
|
|
624
612
|
}
|
|
625
|
-
//
|
|
626
|
-
|
|
613
|
+
// Aggregate and extract model conversational text
|
|
614
|
+
let textParts = (item.parts || [])
|
|
627
615
|
.filter((p) => p.text && !p.text.includes('[TASK_FINISHED]'))
|
|
628
616
|
.map((p) => p.text)
|
|
629
617
|
.join('');
|
|
630
|
-
if
|
|
618
|
+
// Backward-compatibility: if text is empty, check finish_task args.summary
|
|
619
|
+
if (!textParts.trim()) {
|
|
620
|
+
const finishCall = (item.parts || []).find((p) => p.functionCall?.name === 'finish_task');
|
|
621
|
+
const finishArgs = finishCall?.functionCall?.args;
|
|
622
|
+
if (finishArgs?.summary) {
|
|
623
|
+
textParts = String(finishArgs.summary);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (textParts.includes('[ORCHESTRATOR_EXECUTION]')) {
|
|
627
|
+
turnHadModifications = true;
|
|
628
|
+
}
|
|
629
|
+
const cleanText = textParts
|
|
630
|
+
.replace(/\[TASK_FINISHED\]/g, '')
|
|
631
|
+
.replace(/\[ORCHESTRATOR_EXECUTION\]\n?/g, '')
|
|
632
|
+
.replace(/\[PREVIOUS CONVERSATION SUMMARY\]:[\s\S]*?(?=\n\n|$)/g, '')
|
|
633
|
+
.trim();
|
|
634
|
+
if (cleanText) {
|
|
635
|
+
if (!renderedIndicatorForTurn) {
|
|
636
|
+
if (turnHadModifications) {
|
|
637
|
+
p.log.message(pc.dim(' ⚡ [Mino applied workspace modifications & commands]'));
|
|
638
|
+
renderedIndicatorForTurn = true;
|
|
639
|
+
}
|
|
640
|
+
else if (turnHadInspections) {
|
|
641
|
+
p.log.message(pc.dim(' 🔍 [Inspected workspace context]'));
|
|
642
|
+
renderedIndicatorForTurn = true;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
631
645
|
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim('(History)')}\n`);
|
|
632
|
-
console.log(renderTerminalMarkdown(
|
|
646
|
+
console.log(renderTerminalMarkdown(cleanText));
|
|
633
647
|
}
|
|
634
648
|
}
|
|
635
649
|
}
|
|
@@ -1018,10 +1032,23 @@ export async function handleSlashCommand(command, context) {
|
|
|
1018
1032
|
<diff>
|
|
1019
1033
|
${diffOut}
|
|
1020
1034
|
</diff>`;
|
|
1021
|
-
const commitSystemPrompt =
|
|
1035
|
+
const commitSystemPrompt = `You are a strict Conventional Commits generator and expert software engineer.
|
|
1036
|
+
Output ONLY the raw commit message text. Absolutely NO conversational preambles, NO markdown code blocks (e.g. no \`\`\`), and NO post-explanations.
|
|
1037
|
+
|
|
1038
|
+
Strict Formatting Rules:
|
|
1039
|
+
1. First line must follow Conventional Commits format: type(scope): description
|
|
1040
|
+
- Allowed types: feat, fix, refactor, test, docs, chore, perf, style
|
|
1041
|
+
- Subject line must be in imperative mood (e.g. "add support for...", "fix bug in...") and strictly under 50 characters.
|
|
1042
|
+
2. Leave a single blank line after the subject.
|
|
1043
|
+
3. Provide a concise bulleted list (using '-' or '*') explaining the architectural "what" and "why" of the changes based on the provided diff. If diff spans multiple areas, prioritize the primary architectural change.`;
|
|
1022
1044
|
const commitAgent = new ProxyChatSession(GEMINI_MODELS.FLASH_LITE, commitSystemPrompt, [], {});
|
|
1023
1045
|
const commitResult = await commitAgent.sendMessage(prompt);
|
|
1024
|
-
|
|
1046
|
+
let commitMsg = commitResult.response.text().trim();
|
|
1047
|
+
// Sanitize markdown fences if model included them despite instructions
|
|
1048
|
+
commitMsg = commitMsg
|
|
1049
|
+
.replace(/^```[a-zA-Z]*\n?/gm, '')
|
|
1050
|
+
.replace(/```\s*$/gm, '')
|
|
1051
|
+
.trim();
|
|
1025
1052
|
commitSpinner.message('Committing...');
|
|
1026
1053
|
const tmpMsgPath = path.join(workspaceRoot, '.gemini-commit-msg.tmp');
|
|
1027
1054
|
await fs.writeFile(tmpMsgPath, commitMsg, 'utf-8');
|
|
@@ -180,11 +180,12 @@ export declare function traceDependencies(workspaceRoot: string, filePath: strin
|
|
|
180
180
|
*/
|
|
181
181
|
export declare function findRecentChanges(workspaceRoot: string, dirPath?: string, minutes?: number, maxDepth?: number): Promise<ToolResult>;
|
|
182
182
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
183
|
+
* Executes an ephemeral debugging or validation script in the specified language runtime.
|
|
184
|
+
* Delegates to the sandboxed ephemeral script runner in `os.tmpdir()` with automatic ESM/CJS
|
|
185
|
+
* module resolution and multi-language runtime detection.
|
|
185
186
|
*
|
|
186
187
|
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
187
|
-
* @param language - Runtime language (`'node'`, `'ts-node'`, `'python'`, `'bash'`, `'go'`, or `'
|
|
188
|
+
* @param language - Runtime language (`'node'`, `'ts-node'`, `'python'`, `'bash'`, `'go'`, `'rust'`, `'c'`, `'cpp'`, `'ruby'`, `'php'`, `'java'`, or `'auto'`).
|
|
188
189
|
* @param code - The exact script source code to execute.
|
|
189
190
|
* @param abortSignal - Optional AbortSignal to cancel execution.
|
|
190
191
|
* @returns A promise resolving to a {@link ToolResult} containing execution output.
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { exec } from 'node:child_process';
|
|
15
15
|
import { promises as fs } from 'node:fs';
|
|
16
|
-
import os from 'node:os';
|
|
17
16
|
import path from 'node:path';
|
|
18
17
|
import { promisify } from 'node:util';
|
|
19
18
|
import { SchemaType } from '@google/generative-ai';
|
|
@@ -31,7 +30,7 @@ import { extractSymbols } from '../utils/symbolExtractor.js';
|
|
|
31
30
|
import { getMetricCollector } from './metrics.js';
|
|
32
31
|
import { getCurrentAgentId } from '../utils/asyncContext.js';
|
|
33
32
|
import { recordFileRead, hasAgentReadFile } from '../utils/fileReadGuard.js';
|
|
34
|
-
import { runFuzzProbe, checkHeapDelta as runCheckHeapDelta, checkBehavioralDrift as runCheckBehavioralDrift, } from '../utils/analysisRunner.js';
|
|
33
|
+
import { runFuzzProbe, checkHeapDelta as runCheckHeapDelta, checkBehavioralDrift as runCheckBehavioralDrift, runEphemeralScript, } from '../utils/analysisRunner.js';
|
|
35
34
|
import ignore from 'ignore';
|
|
36
35
|
const execAsync = promisify(exec);
|
|
37
36
|
// ─── Tool Declarations for Gemini Function Calling ───────────────────
|
|
@@ -1112,6 +1111,13 @@ export async function listDirectory(workspaceRoot, dirPath, maxDepth = 3) {
|
|
|
1112
1111
|
* @returns A promise resolving to a {@link ToolResult} containing stdout/stderr or an error.
|
|
1113
1112
|
*/
|
|
1114
1113
|
export async function runCommand(workspaceRoot, command, abortSignal) {
|
|
1114
|
+
// Reject sudo or interactive root commands to prevent non-interactive subshell deadlocks and security escalation
|
|
1115
|
+
if (/\bsudo\b/i.test(command)) {
|
|
1116
|
+
return {
|
|
1117
|
+
output: '',
|
|
1118
|
+
error: "Execution blocked: Commands requiring 'sudo' or root privileges cannot be executed automatically in background tool loops. Please explain to the user in your text response that they must run this command manually in their own terminal.",
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1115
1121
|
try {
|
|
1116
1122
|
const { stdout, stderr } = await execAsync(command, {
|
|
1117
1123
|
cwd: workspaceRoot,
|
|
@@ -1351,102 +1357,49 @@ export async function findRecentChanges(workspaceRoot, dirPath = '.', minutes =
|
|
|
1351
1357
|
}
|
|
1352
1358
|
// ─── Tool Dispatcher ─────────────────────────────────────────────────
|
|
1353
1359
|
/**
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1360
|
+
* Executes an ephemeral debugging or validation script in the specified language runtime.
|
|
1361
|
+
* Delegates to the sandboxed ephemeral script runner in `os.tmpdir()` with automatic ESM/CJS
|
|
1362
|
+
* module resolution and multi-language runtime detection.
|
|
1356
1363
|
*
|
|
1357
1364
|
* @param workspaceRoot - Absolute path to the workspace root directory.
|
|
1358
|
-
* @param language - Runtime language (`'node'`, `'ts-node'`, `'python'`, `'bash'`, `'go'`, or `'
|
|
1365
|
+
* @param language - Runtime language (`'node'`, `'ts-node'`, `'python'`, `'bash'`, `'go'`, `'rust'`, `'c'`, `'cpp'`, `'ruby'`, `'php'`, `'java'`, or `'auto'`).
|
|
1359
1366
|
* @param code - The exact script source code to execute.
|
|
1360
1367
|
* @param abortSignal - Optional AbortSignal to cancel execution.
|
|
1361
1368
|
* @returns A promise resolving to a {@link ToolResult} containing execution output.
|
|
1362
1369
|
*/
|
|
1363
1370
|
export async function runDebugScript(workspaceRoot, language, code, abortSignal) {
|
|
1364
|
-
const extMap = {
|
|
1365
|
-
node: '.js',
|
|
1366
|
-
'ts-node': '.ts',
|
|
1367
|
-
python: '.py',
|
|
1368
|
-
bash: '.sh',
|
|
1369
|
-
go: '.go',
|
|
1370
|
-
rust: '.rs',
|
|
1371
|
-
};
|
|
1372
|
-
const ext = extMap[language.toLowerCase()] || '.txt';
|
|
1373
|
-
const tmpFileName = `.minovative-scratch${ext}`;
|
|
1374
|
-
const absPath = path.join(workspaceRoot, tmpFileName);
|
|
1375
1371
|
try {
|
|
1376
1372
|
const p = await import('@clack/prompts');
|
|
1377
1373
|
const pc = (await import('picocolors')).default;
|
|
1378
|
-
p.log.info(pc.dim(`🛠️ Running temporary ${language} debug script...`));
|
|
1379
|
-
await
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
cmd = `npx ts-node ${tmpFileName}`;
|
|
1387
|
-
break;
|
|
1388
|
-
case 'python':
|
|
1389
|
-
cmd = `python3 ${tmpFileName}`;
|
|
1390
|
-
break;
|
|
1391
|
-
case 'bash':
|
|
1392
|
-
cmd = `bash ${tmpFileName}`;
|
|
1393
|
-
break;
|
|
1394
|
-
case 'go':
|
|
1395
|
-
cmd = `go run ${tmpFileName}`;
|
|
1396
|
-
break;
|
|
1397
|
-
case 'rust': {
|
|
1398
|
-
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
1399
|
-
const binPath = tmpFileName.replace('.rs', binExt);
|
|
1400
|
-
const runCmd = os.platform() === 'win32' ? binPath : `./${binPath}`;
|
|
1401
|
-
cmd = `rustc ${tmpFileName} -o ${binPath} && ${runCmd}`;
|
|
1402
|
-
break;
|
|
1403
|
-
}
|
|
1404
|
-
default:
|
|
1405
|
-
return { output: '', error: `Unsupported language runtime: ${language}` };
|
|
1374
|
+
p.log.info(pc.dim(`🛠️ Running temporary ${language || 'script'} debug script...`));
|
|
1375
|
+
const res = await runEphemeralScript(workspaceRoot, language || 'auto', code, {
|
|
1376
|
+
abortSignal,
|
|
1377
|
+
timeoutMs: 60_000,
|
|
1378
|
+
});
|
|
1379
|
+
let finalOutput = '';
|
|
1380
|
+
if (res.exitCode !== 0) {
|
|
1381
|
+
finalOutput += `Script failed with exit code ${res.exitCode}.\n`;
|
|
1406
1382
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const out = stdout.trim();
|
|
1410
|
-
const errOut = stderr.trim();
|
|
1411
|
-
let finalOutput = '';
|
|
1412
|
-
if (out)
|
|
1413
|
-
finalOutput += `[STDOUT]\n${out}\n`;
|
|
1414
|
-
if (errOut)
|
|
1415
|
-
finalOutput += `[STDERR]\n${errOut}\n`;
|
|
1416
|
-
if (!finalOutput)
|
|
1417
|
-
finalOutput = 'Script executed successfully with no output.';
|
|
1418
|
-
return { output: `<test_results>\n${sanitizeForCDATA(finalOutput)}\n</test_results>` };
|
|
1383
|
+
if (res.stdout) {
|
|
1384
|
+
finalOutput += `[STDOUT]\n${res.stdout}\n`;
|
|
1419
1385
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const errOut = (err.stderr || '').trim();
|
|
1423
|
-
let finalOutput = `Script failed with exit code ${err.code || 1}.\n`;
|
|
1424
|
-
if (out)
|
|
1425
|
-
finalOutput += `[STDOUT]\n${out}\n`;
|
|
1426
|
-
if (errOut)
|
|
1427
|
-
finalOutput += `[STDERR]\n${errOut}\n`;
|
|
1428
|
-
return { output: `<test_results>\n${sanitizeForCDATA(finalOutput)}\n</test_results>` };
|
|
1386
|
+
if (res.stderr) {
|
|
1387
|
+
finalOutput += `[STDERR]\n${res.stderr}\n`;
|
|
1429
1388
|
}
|
|
1389
|
+
if (!finalOutput.trim()) {
|
|
1390
|
+
finalOutput = 'Script executed successfully with no output.';
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
output: `<test_results>\n${sanitizeForCDATA(finalOutput.trim())}\n</test_results>`,
|
|
1394
|
+
...(res.exitCode !== 0 ? { error: `Script failed with exit code ${res.exitCode}` } : {}),
|
|
1395
|
+
};
|
|
1430
1396
|
}
|
|
1431
1397
|
catch (err) {
|
|
1432
1398
|
return {
|
|
1433
1399
|
output: '',
|
|
1434
|
-
error: `Failed to
|
|
1400
|
+
error: `Failed to execute debug script: ${err instanceof Error ? err.message : String(err)}`,
|
|
1435
1401
|
};
|
|
1436
1402
|
}
|
|
1437
|
-
finally {
|
|
1438
|
-
try {
|
|
1439
|
-
await fs.rm(absPath, { force: true });
|
|
1440
|
-
if (language.toLowerCase() === 'rust') {
|
|
1441
|
-
const binExt = os.platform() === 'win32' ? '.exe' : '';
|
|
1442
|
-
const binPath = absPath.replace('.rs', binExt);
|
|
1443
|
-
await fs.rm(binPath, { force: true });
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
catch {
|
|
1447
|
-
// Ignore cleanup errors
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
1403
|
}
|
|
1451
1404
|
/**
|
|
1452
1405
|
* Executes a fuzz probing analysis script and formats the resulting pass/fail and crash statistics.
|
package/dist/services/agent.d.ts
CHANGED
|
@@ -112,15 +112,14 @@ export declare function executeSingleTurn(workspaceRoot: string, userInput: stri
|
|
|
112
112
|
} | void>;
|
|
113
113
|
/**
|
|
114
114
|
* Raw chat history entry count threshold to trigger chat history summarization.
|
|
115
|
-
*
|
|
115
|
+
* 2 entries = 1 complete conversational user/model turn.
|
|
116
116
|
*/
|
|
117
|
-
export declare const HISTORY_SUMMARIZATION_THRESHOLD =
|
|
117
|
+
export declare const HISTORY_SUMMARIZATION_THRESHOLD = 2;
|
|
118
118
|
/**
|
|
119
|
-
* Summarizes
|
|
120
|
-
* exceeds {@link HISTORY_SUMMARIZATION_THRESHOLD} entries.
|
|
119
|
+
* Summarizes chat history entries starting from the first full user/model turn.
|
|
121
120
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
121
|
+
* Compresses preceding conversation history into a structured high-density summary pair
|
|
122
|
+
* in the active session while permanently preserving uncompressed history in fullHistory.
|
|
124
123
|
*
|
|
125
124
|
* @param chat - The active ProxyChatSession instance.
|
|
126
125
|
* @returns A promise resolving to true if history was summarized and updated; false otherwise.
|
package/dist/services/agent.js
CHANGED
|
@@ -544,7 +544,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
|
|
|
544
544
|
parts: [{ text: userInput }],
|
|
545
545
|
}, {
|
|
546
546
|
role: 'model',
|
|
547
|
-
parts: [{ text: handledByOrchestrator }],
|
|
547
|
+
parts: [{ text: `[ORCHESTRATOR_EXECUTION]\n${handledByOrchestrator}` }],
|
|
548
548
|
});
|
|
549
549
|
// Print the summary text just like single-agent mode
|
|
550
550
|
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
|
|
@@ -670,6 +670,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
|
|
|
670
670
|
if (cleanFinalText) {
|
|
671
671
|
console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(${chat.getModel()})`)}\n`);
|
|
672
672
|
console.log(renderTerminalMarkdown(cleanFinalText));
|
|
673
|
+
chat.appendFinalSummary(cleanFinalText);
|
|
673
674
|
}
|
|
674
675
|
}
|
|
675
676
|
if (usage) {
|
|
@@ -989,15 +990,14 @@ async function compressContextFiles(workspaceRoot, contextResult) {
|
|
|
989
990
|
}
|
|
990
991
|
/**
|
|
991
992
|
* Raw chat history entry count threshold to trigger chat history summarization.
|
|
992
|
-
*
|
|
993
|
+
* 2 entries = 1 complete conversational user/model turn.
|
|
993
994
|
*/
|
|
994
|
-
export const HISTORY_SUMMARIZATION_THRESHOLD =
|
|
995
|
+
export const HISTORY_SUMMARIZATION_THRESHOLD = 2;
|
|
995
996
|
/**
|
|
996
|
-
* Summarizes
|
|
997
|
-
* exceeds {@link HISTORY_SUMMARIZATION_THRESHOLD} entries.
|
|
997
|
+
* Summarizes chat history entries starting from the first full user/model turn.
|
|
998
998
|
*
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
999
|
+
* Compresses preceding conversation history into a structured high-density summary pair
|
|
1000
|
+
* in the active session while permanently preserving uncompressed history in fullHistory.
|
|
1001
1001
|
*
|
|
1002
1002
|
* @param chat - The active ProxyChatSession instance.
|
|
1003
1003
|
* @returns A promise resolving to true if history was summarized and updated; false otherwise.
|
|
@@ -1008,12 +1008,8 @@ export async function summarizeHistoryIfNeeded(chat, abortSignal) {
|
|
|
1008
1008
|
return false;
|
|
1009
1009
|
}
|
|
1010
1010
|
try {
|
|
1011
|
-
|
|
1012
|
-
const
|
|
1013
|
-
const olderHistory = history.slice(0, history.length - recentCount);
|
|
1014
|
-
const recentHistory = history.slice(history.length - recentCount);
|
|
1015
|
-
debugLog(`Summarizing ${olderHistory.length} older chat history entries...`);
|
|
1016
|
-
const summaryText = await summarizeChatHistory(olderHistory, abortSignal);
|
|
1011
|
+
debugLog(`Summarizing ${history.length} chat history entries (Flash-Lite Summarizer triggered after first turn)...`);
|
|
1012
|
+
const summaryText = await summarizeChatHistory(history, abortSignal);
|
|
1017
1013
|
if (summaryText && summaryText.trim().length > 0) {
|
|
1018
1014
|
const summaryContent = [
|
|
1019
1015
|
{
|
|
@@ -1025,8 +1021,8 @@ export async function summarizeHistoryIfNeeded(chat, abortSignal) {
|
|
|
1025
1021
|
parts: [{ text: 'Understood. I have reviewed and absorbed the summary of our preceding conversation.' }],
|
|
1026
1022
|
},
|
|
1027
1023
|
];
|
|
1028
|
-
chat.loadCompressedHistory(
|
|
1029
|
-
debugLog(`Chat history compressed successfully from ${history.length} to ${summaryContent.length
|
|
1024
|
+
chat.loadCompressedHistory(summaryContent);
|
|
1025
|
+
debugLog(`Chat history compressed successfully from ${history.length} to ${summaryContent.length} entries.`);
|
|
1030
1026
|
return true;
|
|
1031
1027
|
}
|
|
1032
1028
|
}
|
package/dist/services/ai.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export declare class ProxyChatSession {
|
|
|
22
22
|
loadRawHistory(history: Content[]): void;
|
|
23
23
|
loadCompressedHistory(history: Content[]): void;
|
|
24
24
|
addTurn(userContent: Content, modelContent: Content): void;
|
|
25
|
+
/**
|
|
26
|
+
* Appends or merges the final conversational summary text into the active session history.
|
|
27
|
+
* Ensures that the final response produced by finish_task, PM reconciliation, or final turns
|
|
28
|
+
* is permanently preserved as a model text response in both working history and full transcript.
|
|
29
|
+
*/
|
|
30
|
+
appendFinalSummary(text: string): void;
|
|
25
31
|
/**
|
|
26
32
|
* Retrieves the most recent conversation history as a formatted string.
|
|
27
33
|
* Useful for passing conversation context to stateless background agents.
|
|
@@ -42,6 +48,19 @@ export declare class ProxyChatSession {
|
|
|
42
48
|
* conversational context while preventing OOM crashes.
|
|
43
49
|
*/
|
|
44
50
|
private pruneHistory;
|
|
51
|
+
/**
|
|
52
|
+
* Collapses oversized functionResponse outputs in historical turns (>1 turn old)
|
|
53
|
+
* while strictly maintaining Gemini functionCall and functionResponse pairing.
|
|
54
|
+
*
|
|
55
|
+
* Gemini requires every functionCall in a model turn to have a matching functionResponse
|
|
56
|
+
* in the immediately following user turn. Deleting parts or entries breaks this invariant and
|
|
57
|
+
* causes 400 Bad Request errors. This method mutates oversized response outputs in-place
|
|
58
|
+
* on older turns to drastically conserve context window tokens.
|
|
59
|
+
*
|
|
60
|
+
* @param threshold Maximum characters allowed for a historical tool response before collapsing (default: 1500).
|
|
61
|
+
* @param turnsToKeep Number of recent turns to preserve unpruned (default: 1).
|
|
62
|
+
*/
|
|
63
|
+
pruneToolOutputHistory(threshold?: number, turnsToKeep?: number): void;
|
|
45
64
|
sendMessage(message: string | Array<{
|
|
46
65
|
functionResponse: {
|
|
47
66
|
name: string;
|
package/dist/services/ai.js
CHANGED
|
@@ -62,6 +62,14 @@ const MAX_HISTORY_ENTRIES = 500;
|
|
|
62
62
|
* payload stays within sane memory bounds.
|
|
63
63
|
*/
|
|
64
64
|
const MAX_PART_TEXT_LENGTH = 60_000;
|
|
65
|
+
const HISTORICAL_TOOL_OUTPUT_THRESHOLD = 1500;
|
|
66
|
+
const COLLAPSED_TOOL_OUTPUT_MARKER = '\n... [Historical tool output collapsed to save context]';
|
|
67
|
+
function collapseHistoricalOutput(val, threshold = HISTORICAL_TOOL_OUTPUT_THRESHOLD) {
|
|
68
|
+
if (val.length <= threshold || val.includes('[Historical tool output collapsed')) {
|
|
69
|
+
return val;
|
|
70
|
+
}
|
|
71
|
+
return `${val.substring(0, threshold)}${COLLAPSED_TOOL_OUTPUT_MARKER}`;
|
|
72
|
+
}
|
|
65
73
|
function truncatePartText(text) {
|
|
66
74
|
if (text.length <= MAX_PART_TEXT_LENGTH)
|
|
67
75
|
return text;
|
|
@@ -117,6 +125,64 @@ export class ProxyChatSession {
|
|
|
117
125
|
this.fullHistory.push(JSON.parse(JSON.stringify(userContent)));
|
|
118
126
|
this.fullHistory.push(JSON.parse(JSON.stringify(modelContent)));
|
|
119
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Appends or merges the final conversational summary text into the active session history.
|
|
130
|
+
* Ensures that the final response produced by finish_task, PM reconciliation, or final turns
|
|
131
|
+
* is permanently preserved as a model text response in both working history and full transcript.
|
|
132
|
+
*/
|
|
133
|
+
appendFinalSummary(text) {
|
|
134
|
+
if (!text || !text.trim())
|
|
135
|
+
return;
|
|
136
|
+
const clean = text.replace(/\[TASK_FINISHED\]/g, '').trim();
|
|
137
|
+
if (!clean)
|
|
138
|
+
return;
|
|
139
|
+
// 1. Ensure fullHistory has the model text response
|
|
140
|
+
if (this.fullHistory.length > 0) {
|
|
141
|
+
const lastFull = this.fullHistory[this.fullHistory.length - 1];
|
|
142
|
+
if (lastFull.role === 'model') {
|
|
143
|
+
const hasText = lastFull.parts?.some((p) => p.text && p.text.trim() === clean);
|
|
144
|
+
if (!hasText) {
|
|
145
|
+
lastFull.parts = lastFull.parts || [];
|
|
146
|
+
lastFull.parts.push({ text: clean });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.fullHistory.push({
|
|
151
|
+
role: 'model',
|
|
152
|
+
parts: [{ text: clean }],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
this.fullHistory.push({
|
|
158
|
+
role: 'model',
|
|
159
|
+
parts: [{ text: clean }],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// 2. Ensure working history has the model text response
|
|
163
|
+
if (this.history.length > 0) {
|
|
164
|
+
const lastHist = this.history[this.history.length - 1];
|
|
165
|
+
if (lastHist.role === 'model') {
|
|
166
|
+
const hasText = lastHist.parts?.some((p) => p.text && p.text.trim() === clean);
|
|
167
|
+
if (!hasText) {
|
|
168
|
+
lastHist.parts = lastHist.parts || [];
|
|
169
|
+
lastHist.parts.push({ text: clean });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.history.push({
|
|
174
|
+
role: 'model',
|
|
175
|
+
parts: [{ text: clean }],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
this.history.push({
|
|
181
|
+
role: 'model',
|
|
182
|
+
parts: [{ text: clean }],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
120
186
|
/**
|
|
121
187
|
* Retrieves the most recent conversation history as a formatted string.
|
|
122
188
|
* Useful for passing conversation context to stateless background agents.
|
|
@@ -164,6 +230,7 @@ export class ProxyChatSession {
|
|
|
164
230
|
* conversational context while preventing OOM crashes.
|
|
165
231
|
*/
|
|
166
232
|
async pruneHistory() {
|
|
233
|
+
this.pruneToolOutputHistory();
|
|
167
234
|
if (this.history.length > MAX_HISTORY_ENTRIES) {
|
|
168
235
|
const excess = this.history.length - MAX_HISTORY_ENTRIES;
|
|
169
236
|
const trimCount = excess % 2 === 0 ? excess : excess + 1;
|
|
@@ -181,6 +248,46 @@ export class ProxyChatSession {
|
|
|
181
248
|
}
|
|
182
249
|
}
|
|
183
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Collapses oversized functionResponse outputs in historical turns (>1 turn old)
|
|
253
|
+
* while strictly maintaining Gemini functionCall and functionResponse pairing.
|
|
254
|
+
*
|
|
255
|
+
* Gemini requires every functionCall in a model turn to have a matching functionResponse
|
|
256
|
+
* in the immediately following user turn. Deleting parts or entries breaks this invariant and
|
|
257
|
+
* causes 400 Bad Request errors. This method mutates oversized response outputs in-place
|
|
258
|
+
* on older turns to drastically conserve context window tokens.
|
|
259
|
+
*
|
|
260
|
+
* @param threshold Maximum characters allowed for a historical tool response before collapsing (default: 1500).
|
|
261
|
+
* @param turnsToKeep Number of recent turns to preserve unpruned (default: 1).
|
|
262
|
+
*/
|
|
263
|
+
pruneToolOutputHistory(threshold = HISTORICAL_TOOL_OUTPUT_THRESHOLD, turnsToKeep = 1) {
|
|
264
|
+
// 1 turn = 1 user Content + 1 model Content pair (2 entries)
|
|
265
|
+
const cutoffIndex = Math.max(0, this.history.length - turnsToKeep * 2);
|
|
266
|
+
if (cutoffIndex <= 0)
|
|
267
|
+
return;
|
|
268
|
+
for (let i = 0; i < cutoffIndex; i++) {
|
|
269
|
+
const entry = this.history[i];
|
|
270
|
+
if (entry && entry.role === 'user' && Array.isArray(entry.parts)) {
|
|
271
|
+
for (const part of entry.parts) {
|
|
272
|
+
if (part && typeof part === 'object' && 'functionResponse' in part && part.functionResponse) {
|
|
273
|
+
const funcResp = part.functionResponse;
|
|
274
|
+
if (funcResp.response && typeof funcResp.response === 'object') {
|
|
275
|
+
const respObj = funcResp.response;
|
|
276
|
+
if (typeof respObj.output === 'string') {
|
|
277
|
+
respObj.output = collapseHistoricalOutput(respObj.output, threshold);
|
|
278
|
+
}
|
|
279
|
+
if (typeof respObj.error === 'string') {
|
|
280
|
+
respObj.error = collapseHistoricalOutput(respObj.error, threshold);
|
|
281
|
+
}
|
|
282
|
+
if (typeof respObj.result === 'string') {
|
|
283
|
+
respObj.result = collapseHistoricalOutput(respObj.result, threshold);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
184
291
|
async sendMessage(message, additionalText, abortSignal, onChunk) {
|
|
185
292
|
const idToken = await getAuthorizedIdToken();
|
|
186
293
|
if (!idToken) {
|
|
@@ -773,7 +880,7 @@ export async function generateChatTitle(firstMessage, abortSignal) {
|
|
|
773
880
|
const contents = [{ role: 'user', parts: [{ text: firstMessage.substring(0, 500) }] }];
|
|
774
881
|
let model = getGlobalActiveModel();
|
|
775
882
|
if (model === 'auto' || model.includes('claude'))
|
|
776
|
-
model = GEMINI_MODELS.
|
|
883
|
+
model = GEMINI_MODELS.FLASH_LITE;
|
|
777
884
|
const byokEnabled = await isByokEnabled();
|
|
778
885
|
let result;
|
|
779
886
|
if (byokEnabled) {
|