@pheem49/mint 1.5.2 → 1.5.4
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/GUIDE_TH.md +23 -11
- package/README.md +148 -66
- package/assets/Agent_Mint.png +0 -0
- package/assets/Settings.png +0 -0
- package/install.ps1 +64 -0
- package/install.sh +54 -0
- package/main.js +12 -0
- package/package.json +5 -3
- package/preload.js +4 -0
- package/scripts/install_linux_desktop_entry.js +48 -0
- package/src/AI_Brain/Gemini_API.js +231 -498
- package/src/AI_Brain/autonomous_brain.js +46 -19
- package/src/AI_Brain/headless_agent.js +21 -2
- package/src/AI_Brain/provider_adapter.js +358 -0
- package/src/Automation_Layer/file_operations.js +17 -5
- package/src/CLI/approval_handler.js +5 -0
- package/src/CLI/chat_router.js +7 -0
- package/src/CLI/chat_ui.js +397 -76
- package/src/CLI/cli_colors.js +86 -3
- package/src/CLI/cli_formatters.js +6 -1
- package/src/CLI/code_agent.js +706 -273
- package/src/CLI/interactive_chat.js +311 -149
- package/src/CLI/slash_command_handler.js +2 -2
- package/src/CLI/updater.js +21 -1
- package/src/System/config_manager.js +5 -1
- package/src/System/ipc_handlers.js +95 -1
- package/src/System/picture_store.js +109 -0
- package/src/System/smart_context.js +227 -0
- package/src/System/task_manager.js +127 -0
- package/src/System/tool_registry.js +13 -0
- package/src/System/window_manager.js +16 -8
- package/src/UI/live2d_manager.js +42 -8
- package/src/UI/preload-spotlight.js +1 -0
- package/src/UI/renderer.js +837 -63
- package/src/UI/settings.css +160 -96
- package/src/UI/settings.html +9 -0
- package/src/UI/settings.js +35 -2
- package/src/UI/spotlight.js +13 -9
- package/src/UI/styles.css +1592 -165
- package/privacy.txt +0 -1
package/src/CLI/code_agent.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
2
3
|
const path = require('path');
|
|
3
|
-
const { execFile } = require('child_process');
|
|
4
|
+
const { execFile, execFileSync } = require('child_process');
|
|
4
5
|
const { promisify } = require('util');
|
|
5
|
-
const { GoogleGenAI } = require('@google/genai');
|
|
6
6
|
const axios = require('axios');
|
|
7
7
|
const cheerio = require('cheerio');
|
|
8
|
-
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
8
|
+
const { readConfig, getAvailableProviders, CONFIG_DIR } = require('../System/config_manager');
|
|
9
9
|
const safetyManager = require('../System/safety_manager');
|
|
10
10
|
const memoryStore = require('../AI_Brain/memory_store');
|
|
11
11
|
const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
|
|
12
12
|
const { executeAction } = require('../System/action_executor');
|
|
13
13
|
const toolRegistry = require('../System/tool_registry');
|
|
14
14
|
const sandboxRunner = require('../System/sandbox_runner');
|
|
15
|
+
const providerAdapter = require('../AI_Brain/provider_adapter');
|
|
16
|
+
const taskManager = require('../System/task_manager');
|
|
15
17
|
|
|
16
18
|
async function webSearch(query, onProgress = () => {}) {
|
|
17
19
|
if (!query) throw new Error('Search query required.');
|
|
@@ -118,10 +120,13 @@ async function webSearch(query, onProgress = () => {}) {
|
|
|
118
120
|
|
|
119
121
|
|
|
120
122
|
const execFileAsync = promisify(execFile);
|
|
121
|
-
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
122
123
|
const MAX_TOOL_OUTPUT = 12000;
|
|
123
124
|
const MAX_AGENT_STEPS = 16;
|
|
124
125
|
const MAX_JSON_REPAIR_ATTEMPTS = 2;
|
|
126
|
+
const DEFAULT_VERIFICATION_BUDGET = 2;
|
|
127
|
+
const MINT_CONFIG_DIR = CONFIG_DIR || path.join(os.homedir(), '.config', 'mint');
|
|
128
|
+
const PLAN_FILE_PATH = path.join(MINT_CONFIG_DIR, 'mint_plan.md');
|
|
129
|
+
const PLAN_FILE_LABEL = path.join('~', '.config', 'mint', 'mint_plan.md');
|
|
125
130
|
const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
|
|
126
131
|
|
|
127
132
|
const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a pragmatic, polite, and highly helpful AI assistant that can chat, reason, write code, and search the web.
|
|
@@ -134,10 +139,14 @@ PERSONALITY & TONE:
|
|
|
134
139
|
- If the latest user message contains Thai characters, respond in Thai.
|
|
135
140
|
- If the latest user message is English, ASCII-only, or a short English greeting such as "hi", "hello", "ok", or "thanks", respond in English.
|
|
136
141
|
- Do not use Thai just because your persona mentions Mint/มิ้นท์, previous history was Thai, or app settings use th-TH.
|
|
142
|
+
- This language routing applies to user-facing final answers and ask_user questions.
|
|
143
|
+
- Internal progress notes, the JSON "thought" field, and "plan" action bullet text MUST be written in English.
|
|
137
144
|
- Politeness:
|
|
138
145
|
- **WHEN RESPONDING IN THAI:** Use natural female polite particles such as "ค่ะ" or "นะคะ" where appropriate. Refer to yourself as "มิ้นท์" when it sounds natural.
|
|
139
146
|
- **WHEN RESPONDING IN ENGLISH:** Use a polite, concise, professional tone.
|
|
140
147
|
- Emojis: Avoid emojis in technical, review, debugging, and code-editing responses unless the user explicitly uses or asks for them.
|
|
148
|
+
- For technical/code/debugging tasks, keep progress notes and final summaries factual and compact. Do not cheerlead, over-apologize, roleplay, or add affectionate language.
|
|
149
|
+
- For code edits, final summaries should lead with changed files/behavior and verification. Avoid "เรียบร้อยแล้วค่ะ" repetition and decorative closing lines.
|
|
141
150
|
|
|
142
151
|
Rules:
|
|
143
152
|
1. Respond with valid JSON only.
|
|
@@ -148,13 +157,34 @@ Rules:
|
|
|
148
157
|
6. Use shell commands for inspection, tests, and formatting when useful.
|
|
149
158
|
6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
|
|
150
159
|
7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
|
|
151
|
-
8.
|
|
152
|
-
9. When
|
|
160
|
+
8. Before editing more than one file, you MUST first use the "plan" action and wait for user approval. The plan must be written in English, start with "Plan:", and include one bullet per file, for example "- Update src/CLI/agent.js". After approval, make the edits.
|
|
161
|
+
9. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
|
|
162
|
+
10. Before any "apply_patch" or "write_file" action, the "thought" field MUST explicitly name the file you will edit and why that file is the right target. If the file is under "scratch/" or "tests/fixtures/", call that out and explain why editing disposable/test fixture content is intentional.
|
|
163
|
+
11. When you are done, return "finish" with your final response to the user in the "summary" field.
|
|
164
|
+
|
|
165
|
+
Action safety and intent discipline:
|
|
166
|
+
- The latest user message is authoritative. Do not continue an older unfinished task unless the latest message explicitly asks you to continue or clearly refers to that task.
|
|
167
|
+
- For greetings, name-calls, acknowledgements, or backchannels such as "มิ้น", "มิ้นๆ", "อ๋อ", "โอเค", "ขอบคุณ", "hi", "hello", "ok", or "thanks": use "finish" only. Do not inspect files, run shell commands, search code, or claim you checked anything.
|
|
168
|
+
- If the user asks for a command to type, provide the command in "finish". Do not run it unless the user explicitly asks you to run it.
|
|
169
|
+
- If the user asks not to edit or says this is read-only analysis (for example "ห้ามแก้ไฟล์", "ไม่ต้องแก้", "แค่อ่าน", "แค่สรุป", "do not edit", "no edits", "read only"), do not use "plan", "apply_patch", "write_file", "create_folder", "delete_file", "clipboard_write", or system-changing actions. Inspect with read/search tools and finish with a summary only.
|
|
170
|
+
- If the user explicitly asks to search keywords, method names, class names, or symbols, use "search_code" before repeatedly reading more file ranges. Prefer a scoped search with input.path instead of scanning the whole workspace when the likely area is clear.
|
|
171
|
+
- Search scope heuristics: choose input.path only when that path is visible in the current workspace context or was named by the user. If the repo layout is unclear, use list_files on "." first, then choose the narrowest existing directory. Common scopes include "src", "app", "lib", "packages", "tests", and project-specific folders; in this Mint repo, CLI/terminal/command/approval/chat agent questions usually start in "src/CLI", desktop UI/renderer/settings/widget questions in "src/UI", system/config/safety questions in "src/System", and plugin questions in "src/Plugins". If a scoped search path is missing or finds no useful matches, search the whole workspace.
|
|
172
|
+
- If the user explicitly asks you to run a command or provided code, such as "รันคำสั่ง npm test ให้หน่อย", "รันโค้ดนี้หน่อย", or "run npm test", choose "run_shell" with the exact command when it is clear. The app will ask the user for approval before execution.
|
|
173
|
+
- If the user asks you to run something but no exact command/code is provided, use "ask_user" to request the command instead of guessing.
|
|
174
|
+
- If the user asks what is inside a folder and a concrete path is present in the latest message or recent context, use "list_files" for that path. If no concrete target is clear, ask for clarification instead of guessing.
|
|
175
|
+
- Never say you opened, checked, inspected, or verified a file/folder unless a tool observation in this turn actually supports it.
|
|
176
|
+
|
|
177
|
+
Progress updates:
|
|
178
|
+
- The "thought" field is shown to the user as a live progress note. Do not put private chain-of-thought there.
|
|
179
|
+
- Write "thought" as one short, concrete status sentence in English, even when the user writes in Thai or another language.
|
|
180
|
+
- Mention what you just learned from the previous observation when it matters, then say what you will inspect or change next.
|
|
181
|
+
- Before editing, explain the specific file and behavior you are about to change.
|
|
182
|
+
- Before verifying, explain what check you are running and why.
|
|
153
183
|
|
|
154
184
|
Response format:
|
|
155
185
|
{
|
|
156
186
|
"thought": "short reasoning about what to do next",
|
|
157
|
-
"action": "web_search" | "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "apply_patch" | "write_file" | "ask_user" | "open_url" | "open_app" | "open_file" | "open_folder" | "create_folder" | "system_info" | "system_automation" | "finish",
|
|
187
|
+
"action": "web_search" | "list_files" | "read_file" | "search_code" | "find_path" | "run_shell" | "verify" | "plan" | "apply_patch" | "write_file" | "ask_user" | "open_url" | "search" | "open_app" | "web_automation" | "open_file" | "open_folder" | "create_folder" | "delete_file" | "clipboard_write" | "learn_file" | "learn_folder" | "system_info" | "plugin" | "mcp_tool" | "mouse_move" | "mouse_click" | "type_text" | "key_tap" | "system_automation" | "finish",
|
|
158
188
|
"input": {
|
|
159
189
|
"question": "your question to the user for ask_user",
|
|
160
190
|
"query": "search text for web_search, search_code, or find_path",
|
|
@@ -162,9 +192,12 @@ Response format:
|
|
|
162
192
|
"path": "relative/path",
|
|
163
193
|
"type": "file" | "dir" | "any",
|
|
164
194
|
"command": "shell command",
|
|
195
|
+
"commands": ["npm test", "npm run build"],
|
|
165
196
|
"startLine": 1,
|
|
166
197
|
"endLine": 120,
|
|
167
198
|
"content": "full file content for write_file",
|
|
199
|
+
"plan": ["- Update relative/path.js", "- Add tests in tests/example.test.js"],
|
|
200
|
+
"files": ["relative/path.js", "tests/example.test.js"],
|
|
168
201
|
"summary": "your final conversational or technical response to the user (Matches user language and uses polite particles)",
|
|
169
202
|
"verification": "tests or checks (if applicable)",
|
|
170
203
|
"sessionSummary": "brief persistent summary for the workspace",
|
|
@@ -184,9 +217,11 @@ Tool notes:
|
|
|
184
217
|
- "web_search": search the internet for information when you lack knowledge.
|
|
185
218
|
- "list_files": inspect the workspace or a subdirectory.
|
|
186
219
|
- "read_file": read a file, optionally with startLine/endLine.
|
|
187
|
-
- "search_code": search by text or regex-like pattern.
|
|
220
|
+
- "search_code": search by text or regex-like pattern. Optionally set input.path to a relative file or directory to avoid scanning the whole workspace; use the search scope heuristics above when the user did not name a path.
|
|
188
221
|
- "find_path": find files or directories by path/name when the user is looking for a folder, filename, or location.
|
|
189
222
|
- "run_shell": run a non-destructive command in the workspace.
|
|
223
|
+
- "verify": run the detected or provided test/build/lint commands. If verification fails, inspect the output, patch the issue, and verify again within the remaining budget.
|
|
224
|
+
- "plan": present a user-visible multi-file edit plan before changing more than one file. Use English input.plan bullet strings and input.files as the expected touched files.
|
|
190
225
|
- "apply_patch": update an existing file using one or more exact replacement hunks.
|
|
191
226
|
- "write_file": create a new file or fully rewrite a file when replacement is not practical.
|
|
192
227
|
- "ask_user": ask the user for clarification, preference, or more information before proceeding.
|
|
@@ -194,6 +229,8 @@ Tool notes:
|
|
|
194
229
|
- "open_app": open a local application on the user's computer.
|
|
195
230
|
- "system_info": get system information like CPU, memory, date, or weather.
|
|
196
231
|
- "system_automation": control system settings like volume, brightness, or power.
|
|
232
|
+
- "plugin": run a configured Mint plugin.
|
|
233
|
+
- "mcp_tool": call a configured MCP tool.
|
|
197
234
|
- "finish": stop and reply to the user using the "summary" field.
|
|
198
235
|
`;
|
|
199
236
|
|
|
@@ -231,6 +268,10 @@ function normalizeExecutorAction(action, input = {}) {
|
|
|
231
268
|
}
|
|
232
269
|
|
|
233
270
|
function formatActionPreview(action, input = {}) {
|
|
271
|
+
if (action === 'search_code') {
|
|
272
|
+
const query = input.query || 'search';
|
|
273
|
+
return input.path ? `${query} in ${input.path}` : query;
|
|
274
|
+
}
|
|
234
275
|
if (input.command) return input.command;
|
|
235
276
|
if (input.path) return input.path;
|
|
236
277
|
if (input.target) return input.target;
|
|
@@ -264,75 +305,32 @@ function evaluateActionResult(action, toolResult = '') {
|
|
|
264
305
|
};
|
|
265
306
|
}
|
|
266
307
|
|
|
267
|
-
function
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
mimeType: match[1],
|
|
272
|
-
data: match[2]
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function contentToText(content) {
|
|
277
|
-
if (content && typeof content === 'object' && !Array.isArray(content)) {
|
|
278
|
-
return String(content.text || '');
|
|
308
|
+
function getToolCallStatus(action, toolResult = '', evaluation = null) {
|
|
309
|
+
const text = String(toolResult || '');
|
|
310
|
+
if (/^Error:|User denied|blocked|denied|failed|exception|not found/i.test(text)) {
|
|
311
|
+
return 'failed';
|
|
279
312
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
function contentToGeminiParts(content) {
|
|
284
|
-
const text = contentToText(content);
|
|
285
|
-
const parts = text ? [{ text }] : [];
|
|
286
|
-
if (content && typeof content === 'object' && content.imageDataUri) {
|
|
287
|
-
const image = splitDataUri(content.imageDataUri);
|
|
288
|
-
if (image) {
|
|
289
|
-
parts.push({ inlineData: { mimeType: image.mimeType, data: image.data } });
|
|
290
|
-
}
|
|
313
|
+
if (evaluation && evaluation.status === 'failed') {
|
|
314
|
+
return 'failed';
|
|
291
315
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
function contentToOpenAIContent(content) {
|
|
296
|
-
const text = contentToText(content) || 'Analyze this input.';
|
|
297
|
-
if (content && typeof content === 'object' && content.imageDataUri) {
|
|
298
|
-
return [
|
|
299
|
-
{ type: 'text', text },
|
|
300
|
-
{ type: 'image_url', image_url: { url: content.imageDataUri } }
|
|
301
|
-
];
|
|
316
|
+
if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
|
|
317
|
+
return 'failed';
|
|
302
318
|
}
|
|
303
|
-
return
|
|
319
|
+
return 'success';
|
|
304
320
|
}
|
|
305
321
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const image = splitDataUri(content.imageDataUri);
|
|
310
|
-
if (image) {
|
|
311
|
-
return [
|
|
312
|
-
{ type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } },
|
|
313
|
-
{ type: 'text', text }
|
|
314
|
-
];
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return text;
|
|
322
|
+
function summarizeToolTarget(action, input = {}) {
|
|
323
|
+
if (action === 'plan') return 'Multi-file plan';
|
|
324
|
+
return formatActionPreview(action, input);
|
|
318
325
|
}
|
|
319
326
|
|
|
320
327
|
function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
for (const provider of priority) {
|
|
330
|
-
if (availableProviders.includes(provider) && !ordered.includes(provider)) {
|
|
331
|
-
ordered.push(provider);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return ordered.length > 0 ? ordered : ['gemini'];
|
|
328
|
+
return providerAdapter.getProviderAttemptOrder(config || {}, {
|
|
329
|
+
supported: SUPPORTED_CODE_PROVIDERS,
|
|
330
|
+
availableProviders,
|
|
331
|
+
requested: requestedOverride || (config && config.aiProvider) || 'gemini',
|
|
332
|
+
priority: ['anthropic', 'openai', 'gemini', 'local_openai']
|
|
333
|
+
});
|
|
336
334
|
}
|
|
337
335
|
|
|
338
336
|
function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
|
|
@@ -340,17 +338,7 @@ function selectSupportedCodeProvider(config, availableProviders = getAvailablePr
|
|
|
340
338
|
}
|
|
341
339
|
|
|
342
340
|
function getCodeProviderModel(provider, config = {}) {
|
|
343
|
-
|
|
344
|
-
case 'anthropic':
|
|
345
|
-
return config.anthropicModel || 'claude-3-5-sonnet-latest';
|
|
346
|
-
case 'openai':
|
|
347
|
-
return config.openaiModel || 'gpt-4o';
|
|
348
|
-
case 'local_openai':
|
|
349
|
-
return config.localModelName || 'local-model';
|
|
350
|
-
case 'gemini':
|
|
351
|
-
default:
|
|
352
|
-
return config.geminiModel || DEFAULT_GEMINI_MODEL;
|
|
353
|
-
}
|
|
341
|
+
return providerAdapter.getProviderModel(provider, config);
|
|
354
342
|
}
|
|
355
343
|
|
|
356
344
|
function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
|
|
@@ -432,12 +420,16 @@ function readFileRange(workspaceRoot, targetPath, startLine = 1, endLine = 200)
|
|
|
432
420
|
.join('\n');
|
|
433
421
|
}
|
|
434
422
|
|
|
435
|
-
async function searchCode(workspaceRoot, query) {
|
|
423
|
+
async function searchCode(workspaceRoot, query, targetPath = '.') {
|
|
436
424
|
if (!query || !query.trim()) {
|
|
437
425
|
throw new Error('Search query is required.');
|
|
438
426
|
}
|
|
427
|
+
const searchRoot = resolveWorkspacePath(workspaceRoot, targetPath || '.');
|
|
428
|
+
if (!fs.existsSync(searchRoot)) {
|
|
429
|
+
throw new Error(`Search path does not exist: ${targetPath}`);
|
|
430
|
+
}
|
|
439
431
|
try {
|
|
440
|
-
const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query,
|
|
432
|
+
const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, searchRoot], {
|
|
441
433
|
cwd: workspaceRoot,
|
|
442
434
|
maxBuffer: 1024 * 1024 * 4
|
|
443
435
|
});
|
|
@@ -527,6 +519,66 @@ async function runShell(workspaceRoot, command) {
|
|
|
527
519
|
return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
|
|
528
520
|
}
|
|
529
521
|
|
|
522
|
+
async function runVerificationCommands(workspaceRoot, commands = [], options = {}) {
|
|
523
|
+
const detected = detectTestCommands(workspaceRoot);
|
|
524
|
+
const requested = Array.isArray(commands)
|
|
525
|
+
? commands.map(command => String(command || '').trim()).filter(Boolean)
|
|
526
|
+
: [];
|
|
527
|
+
const commandList = requested.length > 0 ? requested : detected;
|
|
528
|
+
|
|
529
|
+
if (commandList.length === 0) {
|
|
530
|
+
return {
|
|
531
|
+
passed: true,
|
|
532
|
+
output: 'No verification commands detected.'
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const requestApproval = typeof options.requestApproval === 'function'
|
|
537
|
+
? options.requestApproval
|
|
538
|
+
: async () => true;
|
|
539
|
+
const budget = Number.isFinite(options.budget) ? options.budget : DEFAULT_VERIFICATION_BUDGET;
|
|
540
|
+
const attempt = Number.isFinite(options.attempt) ? options.attempt : 1;
|
|
541
|
+
const lines = [
|
|
542
|
+
`Verification attempt ${attempt}/${budget}`,
|
|
543
|
+
`Commands: ${commandList.join(' && ')}`
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
for (const command of commandList) {
|
|
547
|
+
const approved = await requestApproval({
|
|
548
|
+
type: 'verify',
|
|
549
|
+
label: command,
|
|
550
|
+
preview: command
|
|
551
|
+
});
|
|
552
|
+
if (!approved) {
|
|
553
|
+
lines.push(`SKIP ${command}: User denied verification command.`);
|
|
554
|
+
return {
|
|
555
|
+
passed: false,
|
|
556
|
+
output: lines.join('\n')
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const output = await runShell(workspaceRoot, command);
|
|
562
|
+
lines.push(`PASS ${command}`);
|
|
563
|
+
if (output && output !== '(no output)') {
|
|
564
|
+
lines.push(truncate(output, 4000));
|
|
565
|
+
}
|
|
566
|
+
} catch (error) {
|
|
567
|
+
lines.push(`FAIL ${command}`);
|
|
568
|
+
lines.push(truncate([error.stdout, error.stderr, error.message].filter(Boolean).join('\n'), 6000));
|
|
569
|
+
return {
|
|
570
|
+
passed: false,
|
|
571
|
+
output: lines.join('\n')
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
passed: true,
|
|
578
|
+
output: lines.join('\n')
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
530
582
|
function splitDiffLines(text) {
|
|
531
583
|
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
532
584
|
const lines = normalized.split('\n');
|
|
@@ -536,11 +588,66 @@ function splitDiffLines(text) {
|
|
|
536
588
|
return lines;
|
|
537
589
|
}
|
|
538
590
|
|
|
539
|
-
function
|
|
540
|
-
|
|
591
|
+
function normalizeGitNoIndexDiff(stdout, targetPath) {
|
|
592
|
+
const lines = String(stdout || '').replace(/\r\n/g, '\n').split('\n');
|
|
593
|
+
const filtered = [];
|
|
594
|
+
for (const line of lines) {
|
|
595
|
+
if (!line) continue;
|
|
596
|
+
if (line.startsWith('diff --git ') || line.startsWith('index ')) continue;
|
|
597
|
+
if (line.startsWith('--- ')) {
|
|
598
|
+
filtered.push(`--- a/${targetPath}`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (line.startsWith('+++ ')) {
|
|
602
|
+
filtered.push(`+++ b/${targetPath}`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
filtered.push(line);
|
|
606
|
+
}
|
|
607
|
+
return filtered.join('\n');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function buildSimpleFullFileDiff(targetPath, previousContent = '', nextContent = '') {
|
|
611
|
+
const previousLines = splitDiffLines(previousContent);
|
|
612
|
+
const nextLines = splitDiffLines(nextContent || '');
|
|
613
|
+
const oldRange = previousLines.length || 0;
|
|
614
|
+
const newRange = nextLines.length || 0;
|
|
615
|
+
const output = [
|
|
616
|
+
`--- a/${targetPath}`,
|
|
617
|
+
`+++ b/${targetPath}`,
|
|
618
|
+
`@@ -1,${oldRange} +1,${newRange} @@`
|
|
619
|
+
];
|
|
620
|
+
|
|
621
|
+
previousLines.forEach(line => output.push(`-${line}`));
|
|
622
|
+
nextLines.forEach(line => output.push(`+${line}`));
|
|
623
|
+
return output.join('\n');
|
|
541
624
|
}
|
|
542
625
|
|
|
543
|
-
function
|
|
626
|
+
function buildContentDiffPreview(targetPath, previousContent = '', nextContent = '') {
|
|
627
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mint-diff-'));
|
|
628
|
+
const oldPath = path.join(tempDir, 'old');
|
|
629
|
+
const newPath = path.join(tempDir, 'new');
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
fs.writeFileSync(oldPath, previousContent || '', 'utf8');
|
|
633
|
+
fs.writeFileSync(newPath, nextContent || '', 'utf8');
|
|
634
|
+
try {
|
|
635
|
+
const stdout = execFileSync('git', ['diff', '--no-index', '--', oldPath, newPath], {
|
|
636
|
+
encoding: 'utf8',
|
|
637
|
+
maxBuffer: 1024 * 1024 * 4
|
|
638
|
+
});
|
|
639
|
+
return normalizeGitNoIndexDiff(stdout, targetPath);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
const stdout = error.stdout || '';
|
|
642
|
+
if (stdout) return normalizeGitNoIndexDiff(stdout, targetPath);
|
|
643
|
+
return buildSimpleFullFileDiff(targetPath, previousContent, nextContent);
|
|
644
|
+
}
|
|
645
|
+
} finally {
|
|
646
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function buildPatchedContent(workspaceRoot, patchInput) {
|
|
544
651
|
if (!patchInput || !patchInput.path) {
|
|
545
652
|
throw new Error('Patch path is required.');
|
|
546
653
|
}
|
|
@@ -555,47 +662,16 @@ function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
|
|
|
555
662
|
throw new Error('Patch hunks are required.');
|
|
556
663
|
}
|
|
557
664
|
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
hunks.forEach((hunk, index) => {
|
|
566
|
-
if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
|
|
567
|
-
throw new Error(`Patch hunk ${index + 1} is invalid.`);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const offset = content.indexOf(hunk.oldText);
|
|
571
|
-
if (offset === -1) {
|
|
572
|
-
throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const beforeText = content.slice(0, offset);
|
|
576
|
-
const oldStartLine = beforeText.length === 0 ? 1 : splitDiffLines(beforeText).length + 1;
|
|
577
|
-
const fileLines = splitDiffLines(content);
|
|
578
|
-
const oldLines = splitDiffLines(hunk.oldText);
|
|
579
|
-
const newLines = splitDiffLines(hunk.newText);
|
|
580
|
-
const oldStartIndex = oldStartLine - 1;
|
|
581
|
-
const contextStartIndex = Math.max(0, oldStartIndex - contextLines);
|
|
582
|
-
const contextEndIndex = Math.min(fileLines.length, oldStartIndex + oldLines.length + contextLines);
|
|
583
|
-
const beforeContext = fileLines.slice(contextStartIndex, oldStartIndex);
|
|
584
|
-
const afterContext = fileLines.slice(oldStartIndex + oldLines.length, contextEndIndex);
|
|
585
|
-
const oldRangeStart = contextStartIndex + 1;
|
|
586
|
-
const oldRangeCount = beforeContext.length + oldLines.length + afterContext.length;
|
|
587
|
-
const newRangeCount = beforeContext.length + newLines.length + afterContext.length;
|
|
588
|
-
|
|
589
|
-
output.push(`@@ -${formatDiffRange(oldRangeStart, oldRangeCount)} +${formatDiffRange(oldRangeStart, newRangeCount)} @@`);
|
|
590
|
-
beforeContext.forEach(line => output.push(` ${line}`));
|
|
591
|
-
oldLines.forEach(line => output.push(`-${line}`));
|
|
592
|
-
newLines.forEach(line => output.push(`+${line}`));
|
|
593
|
-
afterContext.forEach(line => output.push(` ${line}`));
|
|
594
|
-
|
|
595
|
-
content = `${content.slice(0, offset)}${hunk.newText}${content.slice(offset + hunk.oldText.length)}`;
|
|
596
|
-
});
|
|
665
|
+
const previousContent = fs.readFileSync(resolved, 'utf8');
|
|
666
|
+
return {
|
|
667
|
+
previousContent,
|
|
668
|
+
nextContent: applyHunksToContent(previousContent, hunks, patchInput.path)
|
|
669
|
+
};
|
|
670
|
+
}
|
|
597
671
|
|
|
598
|
-
|
|
672
|
+
function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
|
|
673
|
+
const { previousContent, nextContent } = buildPatchedContent(workspaceRoot, patchInput);
|
|
674
|
+
return buildContentDiffPreview(patchInput.path, previousContent, nextContent);
|
|
599
675
|
}
|
|
600
676
|
|
|
601
677
|
function formatPatchPreview(workspaceRoot, patchInput) {
|
|
@@ -606,6 +682,281 @@ function formatPatchPreview(workspaceRoot, patchInput) {
|
|
|
606
682
|
}
|
|
607
683
|
}
|
|
608
684
|
|
|
685
|
+
function buildFullFileDiffPreview(workspaceRoot, targetPath, nextContent = '') {
|
|
686
|
+
if (!targetPath) {
|
|
687
|
+
throw new Error('Write path is required.');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
|
|
691
|
+
const previousContent = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf8') : '';
|
|
692
|
+
return buildContentDiffPreview(targetPath, previousContent, nextContent || '');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function formatWritePreview(workspaceRoot, targetPath, content) {
|
|
696
|
+
try {
|
|
697
|
+
return buildFullFileDiffPreview(workspaceRoot, targetPath, content);
|
|
698
|
+
} catch (error) {
|
|
699
|
+
return `Write preview failed: ${error.message}\n${targetPath}\n${truncate(content || '', 800)}`;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function normalizeRelativePathForWarning(targetPath = '') {
|
|
704
|
+
return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function contentLooksLikeGuide(text = '') {
|
|
708
|
+
return /(guide|installation|publish|npm|registry|setup|documentation|คู่มือ|ติดตั้ง|เผยแพร่)/i.test(String(text || ''));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function contentLooksLikeBio(text = '') {
|
|
712
|
+
return /(bio|biography|profile|created by|assistant|ประวัติ|โปรไฟล์)/i.test(String(text || ''));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function contentLooksLikeConfig(text = '') {
|
|
716
|
+
return /(apiKey|token|secret|config|settings|\.env|clientSecret|refreshToken)/i.test(String(text || ''));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function buildApprovalWarnings(targetPath = '', nextContent = '') {
|
|
720
|
+
const normalized = normalizeRelativePathForWarning(targetPath);
|
|
721
|
+
const basename = path.basename(normalized).toLowerCase();
|
|
722
|
+
const warnings = [];
|
|
723
|
+
|
|
724
|
+
if (normalized.startsWith('scratch/')) {
|
|
725
|
+
warnings.push('Target is under scratch/, which is usually disposable/test content. Confirm this is intentional.');
|
|
726
|
+
}
|
|
727
|
+
if (normalized.startsWith('tests/fixtures/') || normalized.includes('/tests/fixtures/')) {
|
|
728
|
+
warnings.push('Target is under tests/fixtures/, so this may change test fixture behavior.');
|
|
729
|
+
}
|
|
730
|
+
if (/bio|profile|about/.test(basename) && contentLooksLikeGuide(nextContent)) {
|
|
731
|
+
warnings.push('File name looks like profile/bio content, but the new content looks like a guide or publishing document.');
|
|
732
|
+
}
|
|
733
|
+
if (/(guide|readme|docs?|manual)/.test(basename) && contentLooksLikeBio(nextContent)) {
|
|
734
|
+
warnings.push('File name looks like documentation, but the new content looks like biography/profile content.');
|
|
735
|
+
}
|
|
736
|
+
if (!/(config|settings|env|secret|token)/.test(basename) && contentLooksLikeConfig(nextContent)) {
|
|
737
|
+
warnings.push('New content appears to include config/secret-like terms; verify this file is the right place.');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return warnings;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function normalizePlanItems(plan) {
|
|
744
|
+
if (Array.isArray(plan)) {
|
|
745
|
+
return plan
|
|
746
|
+
.map(item => String(item || '').trim())
|
|
747
|
+
.filter(Boolean);
|
|
748
|
+
}
|
|
749
|
+
return String(plan || '')
|
|
750
|
+
.split('\n')
|
|
751
|
+
.map(line => line.trim())
|
|
752
|
+
.filter(Boolean);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function normalizePlanItemLanguage(item) {
|
|
756
|
+
let text = String(item || '').trim();
|
|
757
|
+
const hasBullet = text.startsWith('- ');
|
|
758
|
+
if (hasBullet) text = text.slice(2).trim();
|
|
759
|
+
|
|
760
|
+
const replacements = [
|
|
761
|
+
[/^แก้\s+(.+)$/i, 'Update $1'],
|
|
762
|
+
[/^แก้ไข\s+(.+)$/i, 'Update $1'],
|
|
763
|
+
[/^อัปเดต\s+(.+)$/i, 'Update $1'],
|
|
764
|
+
[/^ปรับ\s+(.+)$/i, 'Update $1'],
|
|
765
|
+
[/^สร้าง\s+(.+)$/i, 'Create $1'],
|
|
766
|
+
[/^เพิ่ม\s+(.+)$/i, 'Add $1'],
|
|
767
|
+
[/^ลบ\s+(.+)$/i, 'Remove $1'],
|
|
768
|
+
[/^ตรวจสอบ\s+(.+)$/i, 'Verify $1'],
|
|
769
|
+
[/^ทดสอบ\s+(.+)$/i, 'Test $1']
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
for (const [pattern, replacement] of replacements) {
|
|
773
|
+
if (pattern.test(text)) {
|
|
774
|
+
text = text.replace(pattern, replacement);
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return hasBullet ? `- ${text}` : text;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function formatPlanPreview(input = {}) {
|
|
783
|
+
const items = normalizePlanItems(input.plan);
|
|
784
|
+
const files = Array.isArray(input.files)
|
|
785
|
+
? input.files.map(file => String(file || '').trim()).filter(Boolean)
|
|
786
|
+
: [];
|
|
787
|
+
const lines = ['Plan:'];
|
|
788
|
+
|
|
789
|
+
if (items.length > 0) {
|
|
790
|
+
items.forEach(item => {
|
|
791
|
+
const normalizedItem = normalizePlanItemLanguage(item);
|
|
792
|
+
lines.push(normalizedItem.startsWith('- ') ? normalizedItem : `- ${normalizedItem}`);
|
|
793
|
+
});
|
|
794
|
+
} else {
|
|
795
|
+
files.forEach(file => lines.push(`- Update ${file}`));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return lines.join('\n');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function formatPlanApprovalSummary(input = {}) {
|
|
802
|
+
const items = normalizePlanItems(input.plan);
|
|
803
|
+
const files = Array.isArray(input.files)
|
|
804
|
+
? input.files.map(file => String(file || '').trim()).filter(Boolean)
|
|
805
|
+
: [];
|
|
806
|
+
if (files.length > 0) {
|
|
807
|
+
return `${items.length || files.length} planned changes across ${files.length} files.`;
|
|
808
|
+
}
|
|
809
|
+
return `${items.length || 1} planned change${(items.length || 1) === 1 ? '' : 's'} prepared.`;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function formatPlanMarkdown(input = {}, context = {}) {
|
|
813
|
+
const preview = formatPlanPreview(input);
|
|
814
|
+
const files = Array.isArray(input.files)
|
|
815
|
+
? input.files.map(file => String(file || '').trim()).filter(Boolean)
|
|
816
|
+
: [];
|
|
817
|
+
const task = String(context.task || input.task || '').trim();
|
|
818
|
+
const createdAt = context.createdAt || new Date().toISOString();
|
|
819
|
+
const approvalStatus = context.approvalStatus || 'Pending user approval';
|
|
820
|
+
const approvalTime = context.approvalTime || '';
|
|
821
|
+
const lines = [
|
|
822
|
+
'# Mint Plan',
|
|
823
|
+
'',
|
|
824
|
+
`Created: ${createdAt}`
|
|
825
|
+
];
|
|
826
|
+
|
|
827
|
+
if (task) {
|
|
828
|
+
lines.push('', '## Task', '', task);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
lines.push('', '## Plan', '', preview);
|
|
832
|
+
|
|
833
|
+
if (files.length > 0) {
|
|
834
|
+
lines.push('', '## Expected Files', '');
|
|
835
|
+
files.forEach(file => lines.push(`- ${file}`));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
lines.push(
|
|
839
|
+
'',
|
|
840
|
+
'## Approval',
|
|
841
|
+
'',
|
|
842
|
+
`Status: ${approvalStatus}`
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
if (approvalTime) {
|
|
846
|
+
lines.push(`${approvalStatus}: ${approvalTime}`);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
lines.push('');
|
|
850
|
+
|
|
851
|
+
return lines.join('\n');
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function writePlanFile(workspaceRoot, input = {}, context = {}) {
|
|
855
|
+
const planPath = context.planPath || PLAN_FILE_PATH;
|
|
856
|
+
const content = formatPlanMarkdown(input, context);
|
|
857
|
+
fs.mkdirSync(path.dirname(planPath), { recursive: true });
|
|
858
|
+
fs.writeFileSync(planPath, content, 'utf8');
|
|
859
|
+
return {
|
|
860
|
+
path: planPath,
|
|
861
|
+
content
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function updatePlanApprovalStatus(planFile, input = {}, context = {}) {
|
|
866
|
+
const content = formatPlanMarkdown(input, context);
|
|
867
|
+
fs.mkdirSync(path.dirname(planFile.path), { recursive: true });
|
|
868
|
+
fs.writeFileSync(planFile.path, content, 'utf8');
|
|
869
|
+
return {
|
|
870
|
+
...planFile,
|
|
871
|
+
content
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function getEditTargetPath(action, input = {}) {
|
|
876
|
+
if (action === 'apply_patch') {
|
|
877
|
+
return input.patch && input.patch.path ? String(input.patch.path) : '';
|
|
878
|
+
}
|
|
879
|
+
if (action === 'write_file') {
|
|
880
|
+
return input.path ? String(input.path) : '';
|
|
881
|
+
}
|
|
882
|
+
return '';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function requiresMultiFilePlan(action, input = {}, editPlanState = {}) {
|
|
886
|
+
const targetPath = getEditTargetPath(action, input);
|
|
887
|
+
if (!targetPath || editPlanState.approved) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const touchedFiles = editPlanState.touchedFiles instanceof Set
|
|
892
|
+
? editPlanState.touchedFiles
|
|
893
|
+
: new Set(editPlanState.touchedFiles || []);
|
|
894
|
+
return touchedFiles.size > 0 && !touchedFiles.has(targetPath);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function getMissingPlanFiles(editPlanState = {}) {
|
|
898
|
+
const expectedFiles = editPlanState.expectedFiles instanceof Set
|
|
899
|
+
? editPlanState.expectedFiles
|
|
900
|
+
: new Set(editPlanState.expectedFiles || []);
|
|
901
|
+
const touchedFiles = editPlanState.touchedFiles instanceof Set
|
|
902
|
+
? editPlanState.touchedFiles
|
|
903
|
+
: new Set(editPlanState.touchedFiles || []);
|
|
904
|
+
|
|
905
|
+
return Array.from(expectedFiles).filter(file => file && !touchedFiles.has(file));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function isReadOnlyTask(task = '') {
|
|
909
|
+
const text = String(task || '').toLowerCase();
|
|
910
|
+
return /(?:ห้ามแก้|ไม่ต้องแก้|อย่าแก้|ไม่แก้ไฟล์|ห้ามเขียน|แค่อ่าน|อ่านอย่างเดียว|แค่สรุป|สรุปอย่างเดียว|แค่อธิบาย|อธิบายอย่างเดียว|do not edit|don't edit|no edits?|read[-\s]?only|only read|only summarize|summari[sz]e only|do not modify|don't modify|no changes?|analysis only)/i.test(text);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function isWriteLikeAction(action) {
|
|
914
|
+
return new Set([
|
|
915
|
+
'plan',
|
|
916
|
+
'apply_patch',
|
|
917
|
+
'write_file',
|
|
918
|
+
'create_folder',
|
|
919
|
+
'delete_file',
|
|
920
|
+
'clipboard_write',
|
|
921
|
+
'system_automation',
|
|
922
|
+
'mouse_move',
|
|
923
|
+
'mouse_click',
|
|
924
|
+
'type_text',
|
|
925
|
+
'key_tap'
|
|
926
|
+
]).has(action);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function validateEditExplanation(action, input = {}, thought = '') {
|
|
930
|
+
const targetPath = getEditTargetPath(action, input);
|
|
931
|
+
if (!targetPath) return { ok: true };
|
|
932
|
+
|
|
933
|
+
const text = String(thought || '').toLowerCase();
|
|
934
|
+
const normalized = normalizeRelativePathForWarning(targetPath).toLowerCase();
|
|
935
|
+
const basename = path.basename(normalized).toLowerCase();
|
|
936
|
+
const mentionsTarget = text.includes(normalized) || (basename && text.includes(basename));
|
|
937
|
+
const explainsWhy = /(because|why|so that|in order|to update|to change|to edit|เพื่อ|เพราะ|เนื่องจาก|จะปรับ|จะแก้|อัปเดต|แก้ไข)/i.test(thought || '');
|
|
938
|
+
if (!mentionsTarget || !explainsWhy) {
|
|
939
|
+
return {
|
|
940
|
+
ok: false,
|
|
941
|
+
message: `Before editing ${targetPath}, explain in the thought field which file you will edit and why this is the correct target.`
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const sensitiveScratchPath = normalized.startsWith('scratch/') ||
|
|
946
|
+
normalized.startsWith('tests/fixtures/') ||
|
|
947
|
+
normalized.includes('/tests/fixtures/');
|
|
948
|
+
const mentionsSensitiveLocation = /(scratch|fixture|test fixture|tests\/fixtures|ทดลอง|fixture)/i.test(thought || '');
|
|
949
|
+
const marksIntentional = /(intentional|intentionally|disposable|test content|test fixture|ตั้งใจ|ชั่วคราว|เนื้อหาทดลอง|ไฟล์ทดสอบ)/i.test(thought || '');
|
|
950
|
+
if (sensitiveScratchPath && !(mentionsSensitiveLocation && marksIntentional)) {
|
|
951
|
+
return {
|
|
952
|
+
ok: false,
|
|
953
|
+
message: `Before editing ${targetPath}, explicitly mention that it is under scratch/ or tests/fixtures/ and why editing that disposable/test fixture content is intentional.`
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return { ok: true };
|
|
958
|
+
}
|
|
959
|
+
|
|
609
960
|
function applyHunksToContent(content, hunks, filePath) {
|
|
610
961
|
let nextContent = content;
|
|
611
962
|
hunks.forEach((hunk, index) => {
|
|
@@ -647,125 +998,6 @@ function writeFile(workspaceRoot, targetPath, content) {
|
|
|
647
998
|
return `Wrote ${targetPath}`;
|
|
648
999
|
}
|
|
649
1000
|
|
|
650
|
-
class UnifiedAgentClient {
|
|
651
|
-
constructor(provider, config, providerOrder = [provider]) {
|
|
652
|
-
this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
|
|
653
|
-
this.providerOrder = providerOrder.length > 0 ? providerOrder : [this.provider];
|
|
654
|
-
this.config = config;
|
|
655
|
-
this.history = [];
|
|
656
|
-
this.systemInstruction = CODE_AGENT_PROMPT;
|
|
657
|
-
this.lastSuccessfulProvider = null;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
async sendMessage(observation) {
|
|
661
|
-
this.history.push({ role: 'user', content: observation });
|
|
662
|
-
|
|
663
|
-
const failures = [];
|
|
664
|
-
for (const provider of this.providerOrder) {
|
|
665
|
-
this.provider = SUPPORTED_CODE_PROVIDERS.includes(provider) ? provider : 'gemini';
|
|
666
|
-
try {
|
|
667
|
-
let responseText = '';
|
|
668
|
-
if (this.provider === 'anthropic') {
|
|
669
|
-
responseText = await this._callAnthropic();
|
|
670
|
-
} else if (this.provider === 'openai' || this.provider === 'local_openai') {
|
|
671
|
-
responseText = await this._callOpenAI();
|
|
672
|
-
} else {
|
|
673
|
-
responseText = await this._callGemini();
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
this.history.push({ role: 'assistant', content: responseText });
|
|
677
|
-
this.lastSuccessfulProvider = this.provider;
|
|
678
|
-
return responseText;
|
|
679
|
-
} catch (error) {
|
|
680
|
-
const message = error.message || error.code || 'unknown error';
|
|
681
|
-
failures.push(`${this.provider}: ${message}`);
|
|
682
|
-
if (process.env.MINT_DEBUG === '1') {
|
|
683
|
-
console.error(`[Code Agent Fallback] Provider '${this.provider}' failed: ${message}`);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
throw new Error(`All code agent providers failed. ${failures.join(' | ')}`);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
async _callAnthropic() {
|
|
692
|
-
const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
693
|
-
const messages = this.history.map(m => ({
|
|
694
|
-
role: m.role,
|
|
695
|
-
content: contentToAnthropicContent(m.content)
|
|
696
|
-
}));
|
|
697
|
-
|
|
698
|
-
const response = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
699
|
-
model: this.config.anthropicModel || 'claude-3-5-sonnet-latest',
|
|
700
|
-
max_tokens: 8192,
|
|
701
|
-
system: this.systemInstruction,
|
|
702
|
-
messages: messages
|
|
703
|
-
}, {
|
|
704
|
-
headers: {
|
|
705
|
-
'x-api-key': apiKey,
|
|
706
|
-
'anthropic-version': '2023-06-01',
|
|
707
|
-
'content-type': 'application/json'
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
return response.data.content[0].text;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
async _callOpenAI() {
|
|
714
|
-
const isLocal = this.provider === 'local_openai';
|
|
715
|
-
const apiKey = isLocal ? 'not-needed' : (this.config.openaiApiKey || process.env.OPENAI_API_KEY);
|
|
716
|
-
const baseUrl = isLocal ? (this.config.localApiBaseUrl || 'http://localhost:1234/v1') : 'https://api.openai.com/v1';
|
|
717
|
-
const model = isLocal ? (this.config.localModelName || 'local-model') : (this.config.openaiModel || 'gpt-4o');
|
|
718
|
-
|
|
719
|
-
const messages = [
|
|
720
|
-
{ role: 'system', content: this.systemInstruction },
|
|
721
|
-
...this.history.map(m => ({
|
|
722
|
-
role: m.role,
|
|
723
|
-
content: contentToOpenAIContent(m.content)
|
|
724
|
-
}))
|
|
725
|
-
];
|
|
726
|
-
|
|
727
|
-
const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
|
728
|
-
model: model,
|
|
729
|
-
messages: messages,
|
|
730
|
-
response_format: isLocal ? undefined : { type: "json_object" }
|
|
731
|
-
}, {
|
|
732
|
-
headers: {
|
|
733
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
734
|
-
'Content-Type': 'application/json'
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
return response.data.choices[0].message.content;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
async _callGemini() {
|
|
741
|
-
const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
|
|
742
|
-
const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
|
|
743
|
-
const ai = new GoogleGenAI({ apiKey });
|
|
744
|
-
|
|
745
|
-
const recentHistory = this.history.slice(-16);
|
|
746
|
-
const priorHistory = recentHistory.slice(0, -1);
|
|
747
|
-
const lastEntry = recentHistory[recentHistory.length - 1] || { content: '' };
|
|
748
|
-
|
|
749
|
-
// Convert history for Gemini, ensuring parts are correctly structured
|
|
750
|
-
const geminiHistory = priorHistory.map(m => ({
|
|
751
|
-
role: m.role === 'assistant' ? 'model' : 'user',
|
|
752
|
-
parts: contentToGeminiParts(m.content)
|
|
753
|
-
}));
|
|
754
|
-
|
|
755
|
-
const chat = ai.chats.create({
|
|
756
|
-
model,
|
|
757
|
-
config: {
|
|
758
|
-
systemInstruction: this.systemInstruction,
|
|
759
|
-
responseMimeType: 'application/json'
|
|
760
|
-
},
|
|
761
|
-
history: geminiHistory
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
const response = await chat.sendMessage({ message: contentToGeminiParts(lastEntry.content) });
|
|
765
|
-
return typeof response.text === 'function' ? response.text() : response.text;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
1001
|
async function getAgentDecision(client, observation, options = {}) {
|
|
770
1002
|
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
771
1003
|
const step = options.step || 0;
|
|
@@ -877,7 +1109,14 @@ async function executeCodeTask(task, options = {}) {
|
|
|
877
1109
|
const availableProviders = getAvailableProviders(config);
|
|
878
1110
|
const providerOrder = getSupportedCodeProviderOrder(config, availableProviders, options.provider);
|
|
879
1111
|
const provider = providerOrder[0];
|
|
880
|
-
const client = new
|
|
1112
|
+
const client = new providerAdapter.AgentProviderClient({
|
|
1113
|
+
provider,
|
|
1114
|
+
config,
|
|
1115
|
+
providerOrder,
|
|
1116
|
+
systemInstruction: CODE_AGENT_PROMPT,
|
|
1117
|
+
responseMimeType: 'application/json',
|
|
1118
|
+
maxTokens: 8192
|
|
1119
|
+
});
|
|
881
1120
|
|
|
882
1121
|
const initialObservationText = await buildInitialObservation(task, workspaceRoot, history);
|
|
883
1122
|
const relevantMemoryCount = memoryStore.searchInteractions(task, 5).length;
|
|
@@ -902,6 +1141,25 @@ async function executeCodeTask(task, options = {}) {
|
|
|
902
1141
|
let finalVerification = '';
|
|
903
1142
|
let finalSessionSummary = '';
|
|
904
1143
|
let executedSteps = 0;
|
|
1144
|
+
const readOnlyTask = isReadOnlyTask(task);
|
|
1145
|
+
const editPlanState = {
|
|
1146
|
+
approved: false,
|
|
1147
|
+
touchedFiles: new Set(),
|
|
1148
|
+
expectedFiles: new Set()
|
|
1149
|
+
};
|
|
1150
|
+
let verificationAttempts = 0;
|
|
1151
|
+
const verificationBudget = Number.isFinite(options.verificationBudget)
|
|
1152
|
+
? options.verificationBudget
|
|
1153
|
+
: DEFAULT_VERIFICATION_BUDGET;
|
|
1154
|
+
|
|
1155
|
+
if (options.taskId) {
|
|
1156
|
+
taskManager.addCheckpoint(options.taskId, {
|
|
1157
|
+
phase: 'code_agent_start',
|
|
1158
|
+
message: task,
|
|
1159
|
+
provider,
|
|
1160
|
+
providerOrder
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
905
1163
|
|
|
906
1164
|
for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
|
|
907
1165
|
executedSteps = step;
|
|
@@ -921,28 +1179,24 @@ async function executeCodeTask(task, options = {}) {
|
|
|
921
1179
|
continue;
|
|
922
1180
|
}
|
|
923
1181
|
|
|
924
|
-
// Immediately show the agent's thought/reasoning
|
|
925
|
-
onProgress({
|
|
926
|
-
step,
|
|
927
|
-
phase: 'acting',
|
|
928
|
-
action: 'thinking',
|
|
929
|
-
thought: decision.thought
|
|
930
|
-
});
|
|
931
|
-
|
|
932
1182
|
if (action === 'finish') {
|
|
1183
|
+
const missingPlanFiles = getMissingPlanFiles(editPlanState);
|
|
1184
|
+
if (missingPlanFiles.length > 0) {
|
|
1185
|
+
observation = [
|
|
1186
|
+
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1187
|
+
'Action: finish',
|
|
1188
|
+
'Observation:',
|
|
1189
|
+
[
|
|
1190
|
+
'Error: Approved plan is not complete yet.',
|
|
1191
|
+
`Missing planned file edits: ${missingPlanFiles.join(', ')}`,
|
|
1192
|
+
'Complete every file listed in the approved plan before finishing, or create a new plan if the scope changed.'
|
|
1193
|
+
].join('\n')
|
|
1194
|
+
].join('\n');
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
933
1197
|
finalSessionSummary = input.sessionSummary || input.summary || task;
|
|
934
1198
|
finalSummary = input.summary || 'Task complete.';
|
|
935
1199
|
finalVerification = input.verification || 'Not specified.';
|
|
936
|
-
if (onFinalSummary) {
|
|
937
|
-
await onFinalSummary({
|
|
938
|
-
summary: finalSummary,
|
|
939
|
-
verification: finalVerification,
|
|
940
|
-
providerInfo: {
|
|
941
|
-
provider: client.lastSuccessfulProvider || client.provider || provider,
|
|
942
|
-
model: getCodeProviderModel(client.lastSuccessfulProvider || client.provider || provider, config)
|
|
943
|
-
}
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
1200
|
writeWorkspaceSession(workspaceRoot, {
|
|
947
1201
|
summary: finalSessionSummary,
|
|
948
1202
|
lastTask: task,
|
|
@@ -953,6 +1207,57 @@ async function executeCodeTask(task, options = {}) {
|
|
|
953
1207
|
|
|
954
1208
|
let toolResult = '';
|
|
955
1209
|
try {
|
|
1210
|
+
if (readOnlyTask && isWriteLikeAction(action)) {
|
|
1211
|
+
observation = [
|
|
1212
|
+
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1213
|
+
`Action: ${action}`,
|
|
1214
|
+
'Observation:',
|
|
1215
|
+
[
|
|
1216
|
+
'Error: The latest user request is read-only and explicitly forbids edits or changes.',
|
|
1217
|
+
'Do not create a plan or request approval for edits.',
|
|
1218
|
+
'Use read_file/search_code/find_path as needed, then finish with an analysis summary.'
|
|
1219
|
+
].join('\n')
|
|
1220
|
+
].join('\n');
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (requiresMultiFilePlan(action, input, editPlanState)) {
|
|
1225
|
+
const nextPath = getEditTargetPath(action, input);
|
|
1226
|
+
observation = [
|
|
1227
|
+
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1228
|
+
`Action: ${action}`,
|
|
1229
|
+
'Observation:',
|
|
1230
|
+
[
|
|
1231
|
+
'Error: Multi-file edit plan required before editing another file.',
|
|
1232
|
+
'Use the "plan" action first with input.plan starting with "Plan:" bullets and input.files listing every file you expect to touch.',
|
|
1233
|
+
`Already edited: ${Array.from(editPlanState.touchedFiles).join(', ')}`,
|
|
1234
|
+
`Next requested file: ${nextPath}`
|
|
1235
|
+
].join('\n')
|
|
1236
|
+
].join('\n');
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (action === 'apply_patch' || action === 'write_file') {
|
|
1241
|
+
const explanation = validateEditExplanation(action, input, decision.thought);
|
|
1242
|
+
if (!explanation.ok) {
|
|
1243
|
+
observation = [
|
|
1244
|
+
`Previous thought: ${decision.thought || '(none)'}`,
|
|
1245
|
+
`Action: ${action}`,
|
|
1246
|
+
'Observation:',
|
|
1247
|
+
`Error: ${explanation.message}`
|
|
1248
|
+
].join('\n');
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Show progress only after the action passes local validation, so retry attempts do not spam near-duplicate notes.
|
|
1254
|
+
onProgress({
|
|
1255
|
+
step,
|
|
1256
|
+
phase: 'acting',
|
|
1257
|
+
action: 'thinking',
|
|
1258
|
+
thought: decision.thought
|
|
1259
|
+
});
|
|
1260
|
+
|
|
956
1261
|
switch (action) {
|
|
957
1262
|
case 'web_search':
|
|
958
1263
|
toolResult = await webSearch(input.query, onProgress);
|
|
@@ -964,7 +1269,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
964
1269
|
toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
|
|
965
1270
|
break;
|
|
966
1271
|
case 'search_code':
|
|
967
|
-
toolResult = await searchCode(workspaceRoot, input.query);
|
|
1272
|
+
toolResult = await searchCode(workspaceRoot, input.query, input.path || '.');
|
|
968
1273
|
break;
|
|
969
1274
|
case 'find_path':
|
|
970
1275
|
toolResult = await findPaths(workspaceRoot, input.query, input.type);
|
|
@@ -995,12 +1300,85 @@ async function executeCodeTask(task, options = {}) {
|
|
|
995
1300
|
toolResult = await runShell(workspaceRoot, input.command);
|
|
996
1301
|
break;
|
|
997
1302
|
}
|
|
1303
|
+
case 'verify': {
|
|
1304
|
+
verificationAttempts += 1;
|
|
1305
|
+
const result = await runVerificationCommands(workspaceRoot, input.commands, {
|
|
1306
|
+
requestApproval,
|
|
1307
|
+
budget: verificationBudget,
|
|
1308
|
+
attempt: verificationAttempts
|
|
1309
|
+
});
|
|
1310
|
+
toolResult = result.output;
|
|
1311
|
+
if (options.taskId) {
|
|
1312
|
+
taskManager.addCheckpoint(options.taskId, {
|
|
1313
|
+
phase: 'verification',
|
|
1314
|
+
attempt: verificationAttempts,
|
|
1315
|
+
passed: result.passed,
|
|
1316
|
+
output: truncate(result.output, 4000)
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (!result.passed && verificationAttempts >= verificationBudget) {
|
|
1320
|
+
toolResult += '\nVerification budget exhausted. Finish with the remaining failure clearly explained.';
|
|
1321
|
+
}
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
case 'plan': {
|
|
1325
|
+
const createdAt = new Date().toISOString();
|
|
1326
|
+
let planFile = writePlanFile(workspaceRoot, input, { task, createdAt });
|
|
1327
|
+
const approved = await requestApproval({
|
|
1328
|
+
type: 'plan',
|
|
1329
|
+
label: PLAN_FILE_LABEL,
|
|
1330
|
+
preview: planFile.content,
|
|
1331
|
+
summary: formatPlanApprovalSummary(input),
|
|
1332
|
+
openPath: planFile.path
|
|
1333
|
+
});
|
|
1334
|
+
if (!approved) {
|
|
1335
|
+
planFile = updatePlanApprovalStatus(planFile, input, {
|
|
1336
|
+
task,
|
|
1337
|
+
createdAt,
|
|
1338
|
+
approvalStatus: 'Denied',
|
|
1339
|
+
approvalTime: new Date().toISOString()
|
|
1340
|
+
});
|
|
1341
|
+
toolResult = 'User denied multi-file plan.';
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
planFile = updatePlanApprovalStatus(planFile, input, {
|
|
1345
|
+
task,
|
|
1346
|
+
createdAt,
|
|
1347
|
+
approvalStatus: 'Approved',
|
|
1348
|
+
approvalTime: new Date().toISOString()
|
|
1349
|
+
});
|
|
1350
|
+
editPlanState.approved = true;
|
|
1351
|
+
editPlanState.expectedFiles = new Set(
|
|
1352
|
+
Array.isArray(input.files)
|
|
1353
|
+
? input.files.map(file => String(file || '').trim()).filter(Boolean)
|
|
1354
|
+
: []
|
|
1355
|
+
);
|
|
1356
|
+
safetyManager.appendActionLog({
|
|
1357
|
+
source: 'code_agent',
|
|
1358
|
+
action: 'plan',
|
|
1359
|
+
path: planFile.path,
|
|
1360
|
+
preview: planFile.content,
|
|
1361
|
+
approved
|
|
1362
|
+
});
|
|
1363
|
+
toolResult = `User approved multi-file plan at ${PLAN_FILE_LABEL}:\n${planFile.content}`;
|
|
1364
|
+
break;
|
|
1365
|
+
}
|
|
998
1366
|
case 'apply_patch': {
|
|
999
1367
|
const patchInput = input.patch || {};
|
|
1368
|
+
let patchWarnings = [];
|
|
1369
|
+
try {
|
|
1370
|
+
patchWarnings = buildApprovalWarnings(
|
|
1371
|
+
patchInput.path,
|
|
1372
|
+
buildPatchedContent(workspaceRoot, patchInput).nextContent
|
|
1373
|
+
);
|
|
1374
|
+
} catch (_) {
|
|
1375
|
+
patchWarnings = buildApprovalWarnings(patchInput.path, '');
|
|
1376
|
+
}
|
|
1000
1377
|
const approved = await requestApproval({
|
|
1001
1378
|
type: 'patch',
|
|
1002
1379
|
label: patchInput.path,
|
|
1003
|
-
preview: formatPatchPreview(workspaceRoot, patchInput)
|
|
1380
|
+
preview: formatPatchPreview(workspaceRoot, patchInput),
|
|
1381
|
+
warnings: patchWarnings
|
|
1004
1382
|
});
|
|
1005
1383
|
if (!approved) {
|
|
1006
1384
|
toolResult = `User denied patch for ${patchInput.path}`;
|
|
@@ -1013,13 +1391,15 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1013
1391
|
approved
|
|
1014
1392
|
});
|
|
1015
1393
|
toolResult = applyPatch(workspaceRoot, patchInput);
|
|
1394
|
+
editPlanState.touchedFiles.add(patchInput.path);
|
|
1016
1395
|
break;
|
|
1017
1396
|
}
|
|
1018
1397
|
case 'write_file': {
|
|
1019
1398
|
const approved = await requestApproval({
|
|
1020
1399
|
type: 'write_file',
|
|
1021
1400
|
label: input.path,
|
|
1022
|
-
preview:
|
|
1401
|
+
preview: formatWritePreview(workspaceRoot, input.path, input.content),
|
|
1402
|
+
warnings: buildApprovalWarnings(input.path, input.content)
|
|
1023
1403
|
});
|
|
1024
1404
|
if (!approved) {
|
|
1025
1405
|
toolResult = `User denied full file write for ${input.path}`;
|
|
@@ -1032,6 +1412,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1032
1412
|
approved
|
|
1033
1413
|
});
|
|
1034
1414
|
toolResult = writeFile(workspaceRoot, input.path, input.content);
|
|
1415
|
+
editPlanState.touchedFiles.add(input.path);
|
|
1035
1416
|
break;
|
|
1036
1417
|
}
|
|
1037
1418
|
case 'ask_user': {
|
|
@@ -1040,11 +1421,23 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1040
1421
|
break;
|
|
1041
1422
|
}
|
|
1042
1423
|
case 'open_url':
|
|
1424
|
+
case 'search':
|
|
1043
1425
|
case 'open_app':
|
|
1426
|
+
case 'web_automation':
|
|
1044
1427
|
case 'open_file':
|
|
1045
1428
|
case 'open_folder':
|
|
1046
1429
|
case 'create_folder':
|
|
1430
|
+
case 'delete_file':
|
|
1431
|
+
case 'clipboard_write':
|
|
1432
|
+
case 'learn_file':
|
|
1433
|
+
case 'learn_folder':
|
|
1047
1434
|
case 'system_info':
|
|
1435
|
+
case 'plugin':
|
|
1436
|
+
case 'mcp_tool':
|
|
1437
|
+
case 'mouse_move':
|
|
1438
|
+
case 'mouse_click':
|
|
1439
|
+
case 'type_text':
|
|
1440
|
+
case 'key_tap':
|
|
1048
1441
|
case 'system_automation': {
|
|
1049
1442
|
const executorAction = normalizeExecutorAction(action, input);
|
|
1050
1443
|
const safety = safetyManager.classifyAction(executorAction);
|
|
@@ -1077,6 +1470,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1077
1470
|
}
|
|
1078
1471
|
|
|
1079
1472
|
const evaluation = evaluateActionResult(action, toolResult);
|
|
1473
|
+
const toolStatus = getToolCallStatus(action, toolResult, evaluation);
|
|
1080
1474
|
if (evaluation) {
|
|
1081
1475
|
onProgress({
|
|
1082
1476
|
step,
|
|
@@ -1092,6 +1486,14 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1092
1486
|
].join('\n');
|
|
1093
1487
|
}
|
|
1094
1488
|
|
|
1489
|
+
onProgress({
|
|
1490
|
+
step,
|
|
1491
|
+
phase: 'tool_call',
|
|
1492
|
+
action,
|
|
1493
|
+
status: toolStatus,
|
|
1494
|
+
target: summarizeToolTarget(action, input)
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1095
1497
|
// Log the finished step with result
|
|
1096
1498
|
let resultSummary = '';
|
|
1097
1499
|
if (action === 'search_code') {
|
|
@@ -1105,7 +1507,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1105
1507
|
step,
|
|
1106
1508
|
phase: 'finished',
|
|
1107
1509
|
action,
|
|
1108
|
-
target: (
|
|
1510
|
+
target: summarizeToolTarget(action, input) + resultSummary
|
|
1109
1511
|
});
|
|
1110
1512
|
|
|
1111
1513
|
// Format tool result to be more readable and structured for the agent
|
|
@@ -1122,7 +1524,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1122
1524
|
].join('\n'); }
|
|
1123
1525
|
|
|
1124
1526
|
// Check for Agent Collaboration (Review) - Disabled by default to save tokens
|
|
1125
|
-
if (config.enableAgentCollaboration === true && executedSteps > 8 && finalSummary) {
|
|
1527
|
+
if (config.enableAgentCollaboration === true && !readOnlyTask && executedSteps > 8 && finalSummary) {
|
|
1126
1528
|
const availableProviders = getAvailableProviders(config);
|
|
1127
1529
|
// Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
|
|
1128
1530
|
const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
|
|
@@ -1135,7 +1537,14 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1135
1537
|
if (reviewerProvider && finalSummary) {
|
|
1136
1538
|
onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
|
|
1137
1539
|
|
|
1138
|
-
const reviewerClient = new
|
|
1540
|
+
const reviewerClient = new providerAdapter.AgentProviderClient({
|
|
1541
|
+
provider: reviewerProvider,
|
|
1542
|
+
config,
|
|
1543
|
+
providerOrder: [reviewerProvider],
|
|
1544
|
+
systemInstruction: CODE_AGENT_PROMPT,
|
|
1545
|
+
responseMimeType: 'application/json',
|
|
1546
|
+
maxTokens: 4096
|
|
1547
|
+
});
|
|
1139
1548
|
reviewerClient.systemInstruction = CODE_AGENT_PROMPT + "\n\nYou are the Reviewer Agent. Review the primary agent's changes, test output, and verification. If you spot a critical bug, point it out. Otherwise, confirm it looks good. Return JSON with action: 'finish' and your review in the 'summary' field.";
|
|
1140
1549
|
|
|
1141
1550
|
const reviewPrompt = `The primary agent (${provider}) just completed the task: "${task}".\nSummary: ${finalSummary}\nVerification: ${finalVerification}\nGit Status: ${(await getGitContext(workspaceRoot)).status}\n\nPlease review this. Return JSON with action: 'finish'.`;
|
|
@@ -1155,15 +1564,20 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1155
1564
|
if (finalSummary) {
|
|
1156
1565
|
memoryStore.recordInteraction(task, finalSummary);
|
|
1157
1566
|
const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
|
|
1158
|
-
|
|
1567
|
+
const result = {
|
|
1159
1568
|
summary: finalSummary,
|
|
1160
1569
|
verification: finalVerification,
|
|
1161
1570
|
steps: executedSteps,
|
|
1162
1571
|
providerInfo: {
|
|
1163
1572
|
provider: answeredProvider,
|
|
1164
|
-
model: getCodeProviderModel(answeredProvider, config)
|
|
1573
|
+
model: getCodeProviderModel(answeredProvider, config),
|
|
1574
|
+
usage: client.getUsageSummary()
|
|
1165
1575
|
}
|
|
1166
1576
|
};
|
|
1577
|
+
if (onFinalSummary) {
|
|
1578
|
+
await onFinalSummary(result);
|
|
1579
|
+
}
|
|
1580
|
+
return result;
|
|
1167
1581
|
}
|
|
1168
1582
|
|
|
1169
1583
|
writeWorkspaceSession(workspaceRoot, {
|
|
@@ -1179,7 +1593,8 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1179
1593
|
steps: executedSteps || MAX_AGENT_STEPS,
|
|
1180
1594
|
providerInfo: {
|
|
1181
1595
|
provider: answeredProvider,
|
|
1182
|
-
model: getCodeProviderModel(answeredProvider, config)
|
|
1596
|
+
model: getCodeProviderModel(answeredProvider, config),
|
|
1597
|
+
usage: client.getUsageSummary()
|
|
1183
1598
|
}
|
|
1184
1599
|
};
|
|
1185
1600
|
}
|
|
@@ -1193,8 +1608,26 @@ module.exports = {
|
|
|
1193
1608
|
findPaths,
|
|
1194
1609
|
listFiles,
|
|
1195
1610
|
searchCode,
|
|
1611
|
+
runVerificationCommands,
|
|
1196
1612
|
walkDirectory,
|
|
1197
1613
|
buildUnifiedDiffPreview,
|
|
1198
|
-
|
|
1614
|
+
buildFullFileDiffPreview,
|
|
1615
|
+
buildApprovalWarnings,
|
|
1616
|
+
validateEditExplanation,
|
|
1617
|
+
formatPatchPreview,
|
|
1618
|
+
formatWritePreview,
|
|
1619
|
+
formatPlanPreview,
|
|
1620
|
+
formatPlanApprovalSummary,
|
|
1621
|
+
formatPlanMarkdown,
|
|
1622
|
+
writePlanFile,
|
|
1623
|
+
updatePlanApprovalStatus,
|
|
1624
|
+
normalizePlanItems,
|
|
1625
|
+
normalizePlanItemLanguage,
|
|
1626
|
+
requiresMultiFilePlan,
|
|
1627
|
+
getMissingPlanFiles,
|
|
1628
|
+
isReadOnlyTask,
|
|
1629
|
+
isWriteLikeAction,
|
|
1630
|
+
getEditTargetPath,
|
|
1631
|
+
PLAN_FILE_PATH
|
|
1199
1632
|
}
|
|
1200
1633
|
};
|