@pheem49/mint 1.5.1 → 1.5.3
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 +7 -7
- package/README.md +140 -66
- package/assets/Agent_Mint.png +0 -0
- package/assets/Settings.png +0 -0
- package/main.js +12 -0
- package/mint-cli.js +148 -921
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
- package/package.json +20 -21
- package/preload.js +2 -0
- package/scripts/install_linux_desktop_entry.js +48 -0
- package/src/AI_Brain/Gemini_API.js +194 -491
- package/src/AI_Brain/autonomous_brain.js +46 -19
- package/src/AI_Brain/headless_agent.js +21 -2
- package/src/AI_Brain/proactive_engine.js +12 -2
- package/src/AI_Brain/provider_adapter.js +358 -0
- package/src/Automation_Layer/browser_automation.js +26 -24
- package/src/CLI/approval_handler.js +47 -0
- package/src/CLI/chat_router.js +7 -0
- package/src/CLI/chat_ui.js +586 -80
- package/src/CLI/cli_colors.js +115 -0
- package/src/CLI/cli_formatters.js +94 -0
- package/src/CLI/code_agent.js +825 -283
- package/src/CLI/intent_detectors.js +181 -0
- package/src/CLI/interactive_chat.js +641 -0
- package/src/CLI/list_features.js +3 -0
- package/src/CLI/repo_summarizer.js +282 -0
- package/src/CLI/semantic_code_search.js +312 -0
- package/src/CLI/skill_manager.js +41 -0
- package/src/CLI/slash_command_handler.js +418 -0
- package/src/CLI/symbol_indexer.js +231 -0
- package/src/CLI/updater.js +21 -1
- package/src/Channels/discord_bridge.js +11 -13
- package/src/Channels/line_bridge.js +10 -10
- package/src/Channels/slack_bridge.js +7 -12
- package/src/Channels/telegram_bridge.js +6 -14
- package/src/Channels/whatsapp_bridge.js +11 -9
- package/src/System/chat_history_manager.js +20 -12
- package/src/System/config_manager.js +4 -1
- package/src/System/ipc_handlers.js +10 -0
- package/src/System/optional_require.js +23 -0
- package/src/System/picture_store.js +109 -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 +246 -14
- package/src/UI/renderer.js +620 -45
- package/src/UI/settings.css +738 -439
- package/src/UI/settings.html +487 -432
- package/src/UI/settings.js +44 -10
- package/src/UI/styles.css +1403 -106
- package/privacy.txt +0 -1
package/src/CLI/code_agent.js
CHANGED
|
@@ -1,118 +1,190 @@
|
|
|
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.');
|
|
18
20
|
const config = readConfig();
|
|
21
|
+
const debug = process.env.MINT_DEBUG === '1';
|
|
22
|
+
const errors = [];
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
const formatResults = (source, hits) => {
|
|
25
|
+
const instruction = `[CRITICAL AGENT INSTRUCTION: You MUST start your response by explicitly telling the user that you found this information using ${source}. Example: "อ้างอิงจากข้อมูลบน ${source}..." or "According to ${source}..."]\n\n`;
|
|
26
|
+
return instruction + `[Source: ${source}]\n\n` + hits;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// 1. Google Custom Search API (requires googleSearchApiKey + googleSearchCx in config)
|
|
21
30
|
if (config.googleSearchApiKey && config.googleSearchCx) {
|
|
22
31
|
try {
|
|
23
32
|
const GoogleSearch = require('../Channels/google_search_bridge');
|
|
24
33
|
const google = new GoogleSearch({ apiKey: config.googleSearchApiKey, cx: config.googleSearchCx });
|
|
25
34
|
const results = await google.search(query);
|
|
26
35
|
if (results.length > 0) {
|
|
27
|
-
return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
|
|
36
|
+
return formatResults('Google Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
|
|
28
37
|
}
|
|
29
|
-
} catch (e) {
|
|
30
|
-
|
|
38
|
+
} catch (e) {
|
|
39
|
+
errors.push(`Google: ${e.message}`);
|
|
40
|
+
if (debug) console.error('[webSearch] Google failed:', e.message);
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
// 2.
|
|
44
|
+
// 2. Brave Search API (requires braveSearchApiKey in config)
|
|
35
45
|
if (config.braveSearchApiKey) {
|
|
36
46
|
try {
|
|
37
47
|
const BraveSearch = require('../Channels/brave_search_bridge');
|
|
38
48
|
const brave = new BraveSearch({ apiKey: config.braveSearchApiKey });
|
|
39
49
|
const results = await brave.search(query);
|
|
40
50
|
if (results.length > 0) {
|
|
41
|
-
return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
|
|
51
|
+
return formatResults('Brave Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
|
|
42
52
|
}
|
|
43
|
-
} catch (e) {
|
|
44
|
-
|
|
53
|
+
} catch (e) {
|
|
54
|
+
errors.push(`Brave: ${e.message}`);
|
|
55
|
+
if (debug) console.error('[webSearch] Brave failed:', e.message);
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
// 3. Fallback
|
|
59
|
+
// 3. Fallback: DuckDuckGo HTML (No key required, but might get blocked by Captcha)
|
|
49
60
|
try {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
const cheerio = require('cheerio');
|
|
62
|
+
const ddgResponse = await axios.get(
|
|
63
|
+
`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
|
|
64
|
+
{
|
|
65
|
+
timeout: 8000,
|
|
66
|
+
headers: {
|
|
67
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
68
|
+
'Accept-Language': 'en-US,en;q=0.9'
|
|
69
|
+
}
|
|
53
70
|
}
|
|
54
|
-
|
|
55
|
-
const $ = cheerio.load(
|
|
56
|
-
const
|
|
57
|
-
$('.result__body').each((i, el) => {
|
|
71
|
+
);
|
|
72
|
+
const $ddg = cheerio.load(ddgResponse.data);
|
|
73
|
+
const ddgResults = [];
|
|
74
|
+
$ddg('.result__body').each((i, el) => {
|
|
58
75
|
if (i >= 5) return false;
|
|
59
|
-
const title
|
|
60
|
-
const snippet = $(el).find('.result__snippet').text().trim();
|
|
61
|
-
const link
|
|
62
|
-
if (title && link) {
|
|
63
|
-
results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
|
|
64
|
-
}
|
|
76
|
+
const title = $ddg(el).find('.result__title').text().trim();
|
|
77
|
+
const snippet = $ddg(el).find('.result__snippet').text().trim();
|
|
78
|
+
const link = $ddg(el).find('.result__url').attr('href');
|
|
79
|
+
if (title && link) ddgResults.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
|
|
65
80
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
onProgress({ phase: 'error', action: 'web_search', message: 'DuckDuckGo scraping returned no results. It might be blocking us.' });
|
|
81
|
+
if (ddgResults.length > 0) {
|
|
82
|
+
return formatResults('DuckDuckGo', ddgResults.join('\n\n'));
|
|
69
83
|
}
|
|
84
|
+
errors.push('DuckDuckGo: no results (captcha?)');
|
|
85
|
+
if (debug) console.error('[webSearch] DuckDuckGo returned no results');
|
|
86
|
+
} catch (e) {
|
|
87
|
+
errors.push(`DuckDuckGo: ${e.message}`);
|
|
88
|
+
if (debug) console.error('[webSearch] DuckDuckGo failed:', e.message);
|
|
89
|
+
}
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
// 4. Fallback: Wikipedia API (Free, no key required, good for factual queries)
|
|
92
|
+
try {
|
|
93
|
+
const wikiResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
|
|
94
|
+
params: { action: 'query', list: 'search', srsearch: query, format: 'json', srlimit: 3 },
|
|
95
|
+
timeout: 5000,
|
|
96
|
+
headers: { 'User-Agent': 'Mint-CLI/1.5 (https://github.com/pheem49/mint)' }
|
|
97
|
+
});
|
|
98
|
+
const hits = wikiResponse.data?.query?.search || [];
|
|
99
|
+
if (hits.length > 0) {
|
|
100
|
+
return formatResults('Wikipedia API', hits.map(r => `Title: ${r.title}\nSnippet: ${r.snippet.replace(/<[^>]+>/g, '')}\nURL: https://en.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/ /g, '_'))}`).join('\n\n'));
|
|
101
|
+
}
|
|
102
|
+
errors.push('Wikipedia: no results');
|
|
72
103
|
} catch (e) {
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
errors.push(`Wikipedia: ${e.message}`);
|
|
105
|
+
if (debug) console.error('[webSearch] Wikipedia failed:', e.message);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// All engines exhausted — inform agent clearly WHY it failed
|
|
109
|
+
const hasKeys = !!(config.googleSearchApiKey || config.braveSearchApiKey);
|
|
110
|
+
const summary = errors.length > 0 ? errors.join(' | ') : 'all search engines unavailable';
|
|
111
|
+
|
|
112
|
+
if (!hasKeys) {
|
|
113
|
+
onProgress({ phase: 'warn', action: 'web_search', message: `No Search API keys configured. Using training knowledge.` });
|
|
114
|
+
return `CRITICAL SYSTEM INSTRUCTION: Web search failed because no API keys are configured. You MUST inform the user that they need to set 'googleSearchApiKey' or 'braveSearchApiKey' in their Mint config file (~/.config/mint/config.json) to enable real-time internet search. Then, answer their query using your training knowledge.`;
|
|
115
|
+
} else {
|
|
116
|
+
onProgress({ phase: 'warn', action: 'web_search', message: `Web search unavailable (${summary}). Answering from training knowledge.` });
|
|
117
|
+
return `CRITICAL SYSTEM INSTRUCTION: Web search is temporarily unavailable. You MUST inform the user that live search failed, and then answer their query using only your training knowledge.`;
|
|
75
118
|
}
|
|
76
119
|
}
|
|
77
120
|
|
|
78
121
|
|
|
79
122
|
const execFileAsync = promisify(execFile);
|
|
80
|
-
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
81
123
|
const MAX_TOOL_OUTPUT = 12000;
|
|
82
124
|
const MAX_AGENT_STEPS = 16;
|
|
83
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');
|
|
84
130
|
const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
|
|
85
131
|
|
|
86
|
-
const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a
|
|
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.
|
|
87
133
|
You work in an inspect -> plan -> act -> verify loop.
|
|
88
134
|
|
|
89
135
|
PERSONALITY & TONE:
|
|
90
136
|
- Gender: Female.
|
|
91
|
-
- Persona: Friendly,
|
|
137
|
+
- Persona: Friendly, calm, concise, and technically direct. Avoid excessive praise, roleplay, or filler.
|
|
92
138
|
- Language routing is mandatory and based on the user's latest message:
|
|
93
139
|
- If the latest user message contains Thai characters, respond in Thai.
|
|
94
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.
|
|
95
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.
|
|
96
144
|
- Politeness:
|
|
97
|
-
- **WHEN RESPONDING IN THAI:**
|
|
98
|
-
- **WHEN RESPONDING IN ENGLISH:** Use a
|
|
99
|
-
- Emojis:
|
|
145
|
+
- **WHEN RESPONDING IN THAI:** Use natural female polite particles such as "ค่ะ" or "นะคะ" where appropriate. Refer to yourself as "มิ้นท์" when it sounds natural.
|
|
146
|
+
- **WHEN RESPONDING IN ENGLISH:** Use a polite, concise, professional tone.
|
|
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.
|
|
100
150
|
|
|
101
151
|
Rules:
|
|
102
152
|
1. Respond with valid JSON only.
|
|
103
153
|
2. If the user asks a conversational question, you can just use "finish" to reply directly.
|
|
104
154
|
3. If you need information, use "web_search", "read_file", or "ask_user" before replying.
|
|
105
|
-
4.
|
|
106
|
-
5.
|
|
155
|
+
4. When using "web_search", always explicitly mention the source engine you used in your final summary (e.g. "According to Brave Search..." or "อ้างอิงจากข้อมูลบน Google..."). Match the language of your response.
|
|
156
|
+
5. Make focused edits that preserve existing project style.
|
|
157
|
+
6. Use shell commands for inspection, tests, and formatting when useful.
|
|
107
158
|
6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
|
|
108
159
|
7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
|
|
109
|
-
8.
|
|
110
|
-
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.
|
|
111
183
|
|
|
112
184
|
Response format:
|
|
113
185
|
{
|
|
114
186
|
"thought": "short reasoning about what to do next",
|
|
115
|
-
"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",
|
|
116
188
|
"input": {
|
|
117
189
|
"question": "your question to the user for ask_user",
|
|
118
190
|
"query": "search text for web_search, search_code, or find_path",
|
|
@@ -120,9 +192,12 @@ Response format:
|
|
|
120
192
|
"path": "relative/path",
|
|
121
193
|
"type": "file" | "dir" | "any",
|
|
122
194
|
"command": "shell command",
|
|
195
|
+
"commands": ["npm test", "npm run build"],
|
|
123
196
|
"startLine": 1,
|
|
124
197
|
"endLine": 120,
|
|
125
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"],
|
|
126
201
|
"summary": "your final conversational or technical response to the user (Matches user language and uses polite particles)",
|
|
127
202
|
"verification": "tests or checks (if applicable)",
|
|
128
203
|
"sessionSummary": "brief persistent summary for the workspace",
|
|
@@ -142,9 +217,11 @@ Tool notes:
|
|
|
142
217
|
- "web_search": search the internet for information when you lack knowledge.
|
|
143
218
|
- "list_files": inspect the workspace or a subdirectory.
|
|
144
219
|
- "read_file": read a file, optionally with startLine/endLine.
|
|
145
|
-
- "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.
|
|
146
221
|
- "find_path": find files or directories by path/name when the user is looking for a folder, filename, or location.
|
|
147
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.
|
|
148
225
|
- "apply_patch": update an existing file using one or more exact replacement hunks.
|
|
149
226
|
- "write_file": create a new file or fully rewrite a file when replacement is not practical.
|
|
150
227
|
- "ask_user": ask the user for clarification, preference, or more information before proceeding.
|
|
@@ -152,6 +229,8 @@ Tool notes:
|
|
|
152
229
|
- "open_app": open a local application on the user's computer.
|
|
153
230
|
- "system_info": get system information like CPU, memory, date, or weather.
|
|
154
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.
|
|
155
234
|
- "finish": stop and reply to the user using the "summary" field.
|
|
156
235
|
`;
|
|
157
236
|
|
|
@@ -189,6 +268,10 @@ function normalizeExecutorAction(action, input = {}) {
|
|
|
189
268
|
}
|
|
190
269
|
|
|
191
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
|
+
}
|
|
192
275
|
if (input.command) return input.command;
|
|
193
276
|
if (input.path) return input.path;
|
|
194
277
|
if (input.target) return input.target;
|
|
@@ -222,75 +305,32 @@ function evaluateActionResult(action, toolResult = '') {
|
|
|
222
305
|
};
|
|
223
306
|
}
|
|
224
307
|
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
mimeType: match[1],
|
|
230
|
-
data: match[2]
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function contentToText(content) {
|
|
235
|
-
if (content && typeof content === 'object' && !Array.isArray(content)) {
|
|
236
|
-
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';
|
|
237
312
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
function contentToGeminiParts(content) {
|
|
242
|
-
const text = contentToText(content);
|
|
243
|
-
const parts = text ? [{ text }] : [];
|
|
244
|
-
if (content && typeof content === 'object' && content.imageDataUri) {
|
|
245
|
-
const image = splitDataUri(content.imageDataUri);
|
|
246
|
-
if (image) {
|
|
247
|
-
parts.push({ inlineData: { mimeType: image.mimeType, data: image.data } });
|
|
248
|
-
}
|
|
313
|
+
if (evaluation && evaluation.status === 'failed') {
|
|
314
|
+
return 'failed';
|
|
249
315
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
function contentToOpenAIContent(content) {
|
|
254
|
-
const text = contentToText(content) || 'Analyze this input.';
|
|
255
|
-
if (content && typeof content === 'object' && content.imageDataUri) {
|
|
256
|
-
return [
|
|
257
|
-
{ type: 'text', text },
|
|
258
|
-
{ type: 'image_url', image_url: { url: content.imageDataUri } }
|
|
259
|
-
];
|
|
316
|
+
if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
|
|
317
|
+
return 'failed';
|
|
260
318
|
}
|
|
261
|
-
return
|
|
319
|
+
return 'success';
|
|
262
320
|
}
|
|
263
321
|
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const image = splitDataUri(content.imageDataUri);
|
|
268
|
-
if (image) {
|
|
269
|
-
return [
|
|
270
|
-
{ type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } },
|
|
271
|
-
{ type: 'text', text }
|
|
272
|
-
];
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return text;
|
|
322
|
+
function summarizeToolTarget(action, input = {}) {
|
|
323
|
+
if (action === 'plan') return 'Multi-file plan';
|
|
324
|
+
return formatActionPreview(action, input);
|
|
276
325
|
}
|
|
277
326
|
|
|
278
327
|
function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
for (const provider of priority) {
|
|
288
|
-
if (availableProviders.includes(provider) && !ordered.includes(provider)) {
|
|
289
|
-
ordered.push(provider);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
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
|
+
});
|
|
294
334
|
}
|
|
295
335
|
|
|
296
336
|
function selectSupportedCodeProvider(config, availableProviders = getAvailableProviders(config || {})) {
|
|
@@ -298,17 +338,7 @@ function selectSupportedCodeProvider(config, availableProviders = getAvailablePr
|
|
|
298
338
|
}
|
|
299
339
|
|
|
300
340
|
function getCodeProviderModel(provider, config = {}) {
|
|
301
|
-
|
|
302
|
-
case 'anthropic':
|
|
303
|
-
return config.anthropicModel || 'claude-3-5-sonnet-latest';
|
|
304
|
-
case 'openai':
|
|
305
|
-
return config.openaiModel || 'gpt-4o';
|
|
306
|
-
case 'local_openai':
|
|
307
|
-
return config.localModelName || 'local-model';
|
|
308
|
-
case 'gemini':
|
|
309
|
-
default:
|
|
310
|
-
return config.geminiModel || DEFAULT_GEMINI_MODEL;
|
|
311
|
-
}
|
|
341
|
+
return providerAdapter.getProviderModel(provider, config);
|
|
312
342
|
}
|
|
313
343
|
|
|
314
344
|
function resolveWorkspacePath(workspaceRoot, targetPath = '.') {
|
|
@@ -390,12 +420,16 @@ function readFileRange(workspaceRoot, targetPath, startLine = 1, endLine = 200)
|
|
|
390
420
|
.join('\n');
|
|
391
421
|
}
|
|
392
422
|
|
|
393
|
-
async function searchCode(workspaceRoot, query) {
|
|
423
|
+
async function searchCode(workspaceRoot, query, targetPath = '.') {
|
|
394
424
|
if (!query || !query.trim()) {
|
|
395
425
|
throw new Error('Search query is required.');
|
|
396
426
|
}
|
|
427
|
+
const searchRoot = resolveWorkspacePath(workspaceRoot, targetPath || '.');
|
|
428
|
+
if (!fs.existsSync(searchRoot)) {
|
|
429
|
+
throw new Error(`Search path does not exist: ${targetPath}`);
|
|
430
|
+
}
|
|
397
431
|
try {
|
|
398
|
-
const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query,
|
|
432
|
+
const { stdout } = await execFileAsync('rg', ['-n', '--hidden', '--glob', '!.git', query, searchRoot], {
|
|
399
433
|
cwd: workspaceRoot,
|
|
400
434
|
maxBuffer: 1024 * 1024 * 4
|
|
401
435
|
});
|
|
@@ -485,29 +519,139 @@ async function runShell(workspaceRoot, command) {
|
|
|
485
519
|
return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
|
|
486
520
|
}
|
|
487
521
|
|
|
488
|
-
function
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
.
|
|
504
|
-
|
|
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
|
+
};
|
|
505
580
|
}
|
|
506
581
|
|
|
507
|
-
function
|
|
582
|
+
function splitDiffLines(text) {
|
|
583
|
+
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
584
|
+
const lines = normalized.split('\n');
|
|
585
|
+
if (normalized.endsWith('\n')) {
|
|
586
|
+
lines.pop();
|
|
587
|
+
}
|
|
588
|
+
return lines;
|
|
589
|
+
}
|
|
590
|
+
|
|
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');
|
|
624
|
+
}
|
|
625
|
+
|
|
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) {
|
|
508
651
|
if (!patchInput || !patchInput.path) {
|
|
509
652
|
throw new Error('Patch path is required.');
|
|
510
653
|
}
|
|
654
|
+
|
|
511
655
|
const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
|
|
512
656
|
if (!fs.existsSync(resolved)) {
|
|
513
657
|
throw new Error(`Patch target does not exist: ${patchInput.path}`);
|
|
@@ -518,145 +662,340 @@ function applyPatch(workspaceRoot, patchInput) {
|
|
|
518
662
|
throw new Error('Patch hunks are required.');
|
|
519
663
|
}
|
|
520
664
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
|
|
528
|
-
}
|
|
529
|
-
content = content.replace(hunk.oldText, hunk.newText);
|
|
530
|
-
});
|
|
665
|
+
const previousContent = fs.readFileSync(resolved, 'utf8');
|
|
666
|
+
return {
|
|
667
|
+
previousContent,
|
|
668
|
+
nextContent: applyHunksToContent(previousContent, hunks, patchInput.path)
|
|
669
|
+
};
|
|
670
|
+
}
|
|
531
671
|
|
|
532
|
-
|
|
533
|
-
|
|
672
|
+
function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
|
|
673
|
+
const { previousContent, nextContent } = buildPatchedContent(workspaceRoot, patchInput);
|
|
674
|
+
return buildContentDiffPreview(patchInput.path, previousContent, nextContent);
|
|
534
675
|
}
|
|
535
676
|
|
|
536
|
-
function
|
|
677
|
+
function formatPatchPreview(workspaceRoot, patchInput) {
|
|
678
|
+
try {
|
|
679
|
+
return buildUnifiedDiffPreview(workspaceRoot, patchInput);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
return `Patch preview failed: ${error.message}`;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function buildFullFileDiffPreview(workspaceRoot, targetPath, nextContent = '') {
|
|
686
|
+
if (!targetPath) {
|
|
687
|
+
throw new Error('Write path is required.');
|
|
688
|
+
}
|
|
689
|
+
|
|
537
690
|
const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
|
|
538
|
-
fs.
|
|
539
|
-
|
|
540
|
-
return `Wrote ${targetPath}`;
|
|
691
|
+
const previousContent = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf8') : '';
|
|
692
|
+
return buildContentDiffPreview(targetPath, previousContent, nextContent || '');
|
|
541
693
|
}
|
|
542
694
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
this.history = [];
|
|
549
|
-
this.systemInstruction = CODE_AGENT_PROMPT;
|
|
550
|
-
this.lastSuccessfulProvider = null;
|
|
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)}`;
|
|
551
700
|
}
|
|
701
|
+
}
|
|
552
702
|
|
|
553
|
-
|
|
554
|
-
|
|
703
|
+
function normalizeRelativePathForWarning(targetPath = '') {
|
|
704
|
+
return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
705
|
+
}
|
|
555
706
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
try {
|
|
560
|
-
let responseText = '';
|
|
561
|
-
if (this.provider === 'anthropic') {
|
|
562
|
-
responseText = await this._callAnthropic();
|
|
563
|
-
} else if (this.provider === 'openai' || this.provider === 'local_openai') {
|
|
564
|
-
responseText = await this._callOpenAI();
|
|
565
|
-
} else {
|
|
566
|
-
responseText = await this._callGemini();
|
|
567
|
-
}
|
|
707
|
+
function contentLooksLikeGuide(text = '') {
|
|
708
|
+
return /(guide|installation|publish|npm|registry|setup|documentation|คู่มือ|ติดตั้ง|เผยแพร่)/i.test(String(text || ''));
|
|
709
|
+
}
|
|
568
710
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
console.error(`[Code Agent Fallback] Provider '${this.provider}' failed: ${message}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
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
|
+
}
|
|
580
718
|
|
|
581
|
-
|
|
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.');
|
|
582
738
|
}
|
|
583
739
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
model: this.config.anthropicModel || 'claude-3-5-sonnet-latest',
|
|
593
|
-
max_tokens: 8192,
|
|
594
|
-
system: this.systemInstruction,
|
|
595
|
-
messages: messages
|
|
596
|
-
}, {
|
|
597
|
-
headers: {
|
|
598
|
-
'x-api-key': apiKey,
|
|
599
|
-
'anthropic-version': '2023-06-01',
|
|
600
|
-
'content-type': 'application/json'
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
return response.data.content[0].text;
|
|
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);
|
|
604
748
|
}
|
|
749
|
+
return String(plan || '')
|
|
750
|
+
.split('\n')
|
|
751
|
+
.map(line => line.trim())
|
|
752
|
+
.filter(Boolean);
|
|
753
|
+
}
|
|
605
754
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
]
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
return response.data.choices[0].message.content;
|
|
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
|
+
}
|
|
631
777
|
}
|
|
632
778
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const chat = ai.chats.create({
|
|
649
|
-
model,
|
|
650
|
-
config: {
|
|
651
|
-
systemInstruction: this.systemInstruction,
|
|
652
|
-
responseMimeType: 'application/json'
|
|
653
|
-
},
|
|
654
|
-
history: geminiHistory
|
|
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}`);
|
|
655
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
|
+
}
|
|
656
874
|
|
|
657
|
-
|
|
658
|
-
|
|
875
|
+
function getEditTargetPath(action, input = {}) {
|
|
876
|
+
if (action === 'apply_patch') {
|
|
877
|
+
return input.patch && input.patch.path ? String(input.patch.path) : '';
|
|
659
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
|
+
|
|
960
|
+
function applyHunksToContent(content, hunks, filePath) {
|
|
961
|
+
let nextContent = content;
|
|
962
|
+
hunks.forEach((hunk, index) => {
|
|
963
|
+
if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
|
|
964
|
+
throw new Error(`Patch hunk ${index + 1} is invalid.`);
|
|
965
|
+
}
|
|
966
|
+
if (!nextContent.includes(hunk.oldText)) {
|
|
967
|
+
throw new Error(`Patch hunk ${index + 1} oldText not found in ${filePath}`);
|
|
968
|
+
}
|
|
969
|
+
nextContent = nextContent.replace(hunk.oldText, hunk.newText);
|
|
970
|
+
});
|
|
971
|
+
return nextContent;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function applyPatch(workspaceRoot, patchInput) {
|
|
975
|
+
if (!patchInput || !patchInput.path) {
|
|
976
|
+
throw new Error('Patch path is required.');
|
|
977
|
+
}
|
|
978
|
+
const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
|
|
979
|
+
if (!fs.existsSync(resolved)) {
|
|
980
|
+
throw new Error(`Patch target does not exist: ${patchInput.path}`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
|
|
984
|
+
if (hunks.length === 0) {
|
|
985
|
+
throw new Error('Patch hunks are required.');
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const content = applyHunksToContent(fs.readFileSync(resolved, 'utf8'), hunks, patchInput.path);
|
|
989
|
+
|
|
990
|
+
fs.writeFileSync(resolved, content, 'utf8');
|
|
991
|
+
return `Patched ${patchInput.path} with ${hunks.length} hunk(s).`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function writeFile(workspaceRoot, targetPath, content) {
|
|
995
|
+
const resolved = resolveWorkspacePath(workspaceRoot, targetPath);
|
|
996
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
997
|
+
fs.writeFileSync(resolved, content || '', 'utf8');
|
|
998
|
+
return `Wrote ${targetPath}`;
|
|
660
999
|
}
|
|
661
1000
|
|
|
662
1001
|
async function getAgentDecision(client, observation, options = {}) {
|
|
@@ -770,7 +1109,14 @@ async function executeCodeTask(task, options = {}) {
|
|
|
770
1109
|
const availableProviders = getAvailableProviders(config);
|
|
771
1110
|
const providerOrder = getSupportedCodeProviderOrder(config, availableProviders, options.provider);
|
|
772
1111
|
const provider = providerOrder[0];
|
|
773
|
-
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
|
+
});
|
|
774
1120
|
|
|
775
1121
|
const initialObservationText = await buildInitialObservation(task, workspaceRoot, history);
|
|
776
1122
|
const relevantMemoryCount = memoryStore.searchInteractions(task, 5).length;
|
|
@@ -795,6 +1141,25 @@ async function executeCodeTask(task, options = {}) {
|
|
|
795
1141
|
let finalVerification = '';
|
|
796
1142
|
let finalSessionSummary = '';
|
|
797
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
|
+
}
|
|
798
1163
|
|
|
799
1164
|
for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
|
|
800
1165
|
executedSteps = step;
|
|
@@ -814,28 +1179,24 @@ async function executeCodeTask(task, options = {}) {
|
|
|
814
1179
|
continue;
|
|
815
1180
|
}
|
|
816
1181
|
|
|
817
|
-
// Immediately show the agent's thought/reasoning
|
|
818
|
-
onProgress({
|
|
819
|
-
step,
|
|
820
|
-
phase: 'acting',
|
|
821
|
-
action: 'thinking',
|
|
822
|
-
thought: decision.thought
|
|
823
|
-
});
|
|
824
|
-
|
|
825
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
|
+
}
|
|
826
1197
|
finalSessionSummary = input.sessionSummary || input.summary || task;
|
|
827
1198
|
finalSummary = input.summary || 'Task complete.';
|
|
828
1199
|
finalVerification = input.verification || 'Not specified.';
|
|
829
|
-
if (onFinalSummary) {
|
|
830
|
-
await onFinalSummary({
|
|
831
|
-
summary: finalSummary,
|
|
832
|
-
verification: finalVerification,
|
|
833
|
-
providerInfo: {
|
|
834
|
-
provider: client.lastSuccessfulProvider || client.provider || provider,
|
|
835
|
-
model: getCodeProviderModel(client.lastSuccessfulProvider || client.provider || provider, config)
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
1200
|
writeWorkspaceSession(workspaceRoot, {
|
|
840
1201
|
summary: finalSessionSummary,
|
|
841
1202
|
lastTask: task,
|
|
@@ -846,6 +1207,57 @@ async function executeCodeTask(task, options = {}) {
|
|
|
846
1207
|
|
|
847
1208
|
let toolResult = '';
|
|
848
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
|
+
|
|
849
1261
|
switch (action) {
|
|
850
1262
|
case 'web_search':
|
|
851
1263
|
toolResult = await webSearch(input.query, onProgress);
|
|
@@ -857,7 +1269,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
857
1269
|
toolResult = readFileRange(workspaceRoot, input.path, input.startLine, input.endLine);
|
|
858
1270
|
break;
|
|
859
1271
|
case 'search_code':
|
|
860
|
-
toolResult = await searchCode(workspaceRoot, input.query);
|
|
1272
|
+
toolResult = await searchCode(workspaceRoot, input.query, input.path || '.');
|
|
861
1273
|
break;
|
|
862
1274
|
case 'find_path':
|
|
863
1275
|
toolResult = await findPaths(workspaceRoot, input.query, input.type);
|
|
@@ -888,12 +1300,85 @@ async function executeCodeTask(task, options = {}) {
|
|
|
888
1300
|
toolResult = await runShell(workspaceRoot, input.command);
|
|
889
1301
|
break;
|
|
890
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
|
+
}
|
|
891
1366
|
case 'apply_patch': {
|
|
892
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
|
+
}
|
|
893
1377
|
const approved = await requestApproval({
|
|
894
1378
|
type: 'patch',
|
|
895
1379
|
label: patchInput.path,
|
|
896
|
-
preview: formatPatchPreview(patchInput)
|
|
1380
|
+
preview: formatPatchPreview(workspaceRoot, patchInput),
|
|
1381
|
+
warnings: patchWarnings
|
|
897
1382
|
});
|
|
898
1383
|
if (!approved) {
|
|
899
1384
|
toolResult = `User denied patch for ${patchInput.path}`;
|
|
@@ -906,13 +1391,15 @@ async function executeCodeTask(task, options = {}) {
|
|
|
906
1391
|
approved
|
|
907
1392
|
});
|
|
908
1393
|
toolResult = applyPatch(workspaceRoot, patchInput);
|
|
1394
|
+
editPlanState.touchedFiles.add(patchInput.path);
|
|
909
1395
|
break;
|
|
910
1396
|
}
|
|
911
1397
|
case 'write_file': {
|
|
912
1398
|
const approved = await requestApproval({
|
|
913
1399
|
type: 'write_file',
|
|
914
1400
|
label: input.path,
|
|
915
|
-
preview:
|
|
1401
|
+
preview: formatWritePreview(workspaceRoot, input.path, input.content),
|
|
1402
|
+
warnings: buildApprovalWarnings(input.path, input.content)
|
|
916
1403
|
});
|
|
917
1404
|
if (!approved) {
|
|
918
1405
|
toolResult = `User denied full file write for ${input.path}`;
|
|
@@ -925,6 +1412,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
925
1412
|
approved
|
|
926
1413
|
});
|
|
927
1414
|
toolResult = writeFile(workspaceRoot, input.path, input.content);
|
|
1415
|
+
editPlanState.touchedFiles.add(input.path);
|
|
928
1416
|
break;
|
|
929
1417
|
}
|
|
930
1418
|
case 'ask_user': {
|
|
@@ -933,11 +1421,23 @@ async function executeCodeTask(task, options = {}) {
|
|
|
933
1421
|
break;
|
|
934
1422
|
}
|
|
935
1423
|
case 'open_url':
|
|
1424
|
+
case 'search':
|
|
936
1425
|
case 'open_app':
|
|
1426
|
+
case 'web_automation':
|
|
937
1427
|
case 'open_file':
|
|
938
1428
|
case 'open_folder':
|
|
939
1429
|
case 'create_folder':
|
|
1430
|
+
case 'delete_file':
|
|
1431
|
+
case 'clipboard_write':
|
|
1432
|
+
case 'learn_file':
|
|
1433
|
+
case 'learn_folder':
|
|
940
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':
|
|
941
1441
|
case 'system_automation': {
|
|
942
1442
|
const executorAction = normalizeExecutorAction(action, input);
|
|
943
1443
|
const safety = safetyManager.classifyAction(executorAction);
|
|
@@ -970,6 +1470,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
970
1470
|
}
|
|
971
1471
|
|
|
972
1472
|
const evaluation = evaluateActionResult(action, toolResult);
|
|
1473
|
+
const toolStatus = getToolCallStatus(action, toolResult, evaluation);
|
|
973
1474
|
if (evaluation) {
|
|
974
1475
|
onProgress({
|
|
975
1476
|
step,
|
|
@@ -985,6 +1486,14 @@ async function executeCodeTask(task, options = {}) {
|
|
|
985
1486
|
].join('\n');
|
|
986
1487
|
}
|
|
987
1488
|
|
|
1489
|
+
onProgress({
|
|
1490
|
+
step,
|
|
1491
|
+
phase: 'tool_call',
|
|
1492
|
+
action,
|
|
1493
|
+
status: toolStatus,
|
|
1494
|
+
target: summarizeToolTarget(action, input)
|
|
1495
|
+
});
|
|
1496
|
+
|
|
988
1497
|
// Log the finished step with result
|
|
989
1498
|
let resultSummary = '';
|
|
990
1499
|
if (action === 'search_code') {
|
|
@@ -998,7 +1507,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
998
1507
|
step,
|
|
999
1508
|
phase: 'finished',
|
|
1000
1509
|
action,
|
|
1001
|
-
target: (
|
|
1510
|
+
target: summarizeToolTarget(action, input) + resultSummary
|
|
1002
1511
|
});
|
|
1003
1512
|
|
|
1004
1513
|
// Format tool result to be more readable and structured for the agent
|
|
@@ -1015,7 +1524,7 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1015
1524
|
].join('\n'); }
|
|
1016
1525
|
|
|
1017
1526
|
// Check for Agent Collaboration (Review) - Disabled by default to save tokens
|
|
1018
|
-
if (config.enableAgentCollaboration === true && executedSteps > 8 && finalSummary) {
|
|
1527
|
+
if (config.enableAgentCollaboration === true && !readOnlyTask && executedSteps > 8 && finalSummary) {
|
|
1019
1528
|
const availableProviders = getAvailableProviders(config);
|
|
1020
1529
|
// Exclude providers that often need special local setup or are slow/unreliable for tiny reviews
|
|
1021
1530
|
const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface' && p !== 'local_openai');
|
|
@@ -1028,7 +1537,14 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1028
1537
|
if (reviewerProvider && finalSummary) {
|
|
1029
1538
|
onProgress({ phase: 'reviewing', action: 'reviewer_start', message: `Invoking Reviewer Agent (${reviewerProvider})...` });
|
|
1030
1539
|
|
|
1031
|
-
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
|
+
});
|
|
1032
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.";
|
|
1033
1549
|
|
|
1034
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'.`;
|
|
@@ -1048,15 +1564,20 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1048
1564
|
if (finalSummary) {
|
|
1049
1565
|
memoryStore.recordInteraction(task, finalSummary);
|
|
1050
1566
|
const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
|
|
1051
|
-
|
|
1567
|
+
const result = {
|
|
1052
1568
|
summary: finalSummary,
|
|
1053
1569
|
verification: finalVerification,
|
|
1054
1570
|
steps: executedSteps,
|
|
1055
1571
|
providerInfo: {
|
|
1056
1572
|
provider: answeredProvider,
|
|
1057
|
-
model: getCodeProviderModel(answeredProvider, config)
|
|
1573
|
+
model: getCodeProviderModel(answeredProvider, config),
|
|
1574
|
+
usage: client.getUsageSummary()
|
|
1058
1575
|
}
|
|
1059
1576
|
};
|
|
1577
|
+
if (onFinalSummary) {
|
|
1578
|
+
await onFinalSummary(result);
|
|
1579
|
+
}
|
|
1580
|
+
return result;
|
|
1060
1581
|
}
|
|
1061
1582
|
|
|
1062
1583
|
writeWorkspaceSession(workspaceRoot, {
|
|
@@ -1072,7 +1593,8 @@ async function executeCodeTask(task, options = {}) {
|
|
|
1072
1593
|
steps: executedSteps || MAX_AGENT_STEPS,
|
|
1073
1594
|
providerInfo: {
|
|
1074
1595
|
provider: answeredProvider,
|
|
1075
|
-
model: getCodeProviderModel(answeredProvider, config)
|
|
1596
|
+
model: getCodeProviderModel(answeredProvider, config),
|
|
1597
|
+
usage: client.getUsageSummary()
|
|
1076
1598
|
}
|
|
1077
1599
|
};
|
|
1078
1600
|
}
|
|
@@ -1086,6 +1608,26 @@ module.exports = {
|
|
|
1086
1608
|
findPaths,
|
|
1087
1609
|
listFiles,
|
|
1088
1610
|
searchCode,
|
|
1089
|
-
|
|
1611
|
+
runVerificationCommands,
|
|
1612
|
+
walkDirectory,
|
|
1613
|
+
buildUnifiedDiffPreview,
|
|
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
|
|
1090
1632
|
}
|
|
1091
1633
|
};
|