mcp-fs-shell-windows 0.2.20 → 0.2.29
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/LICENSE +2 -1
- package/README.md +155 -4
- package/dist/analyze_project/handler.js +62 -0
- package/dist/analyze_project/schema.js +5 -0
- package/dist/browser/browserActions.js +128 -0
- package/dist/browser/fuzzySearch.js +49 -0
- package/dist/browser/handler.js +227 -0
- package/dist/browser/launcher.js +103 -0
- package/dist/browser/schema.js +37 -0
- package/dist/browser/session.js +14 -0
- package/dist/compat/handler.js +254 -0
- package/dist/compat/schema.js +97 -0
- package/dist/gh/handler.js +406 -0
- package/dist/gh/schema.js +36 -0
- package/dist/git/handler.js +209 -0
- package/dist/git/schema.js +23 -0
- package/dist/launch_file/handler.js +3 -1
- package/dist/query_database/handler.js +27 -0
- package/dist/query_database/schema.js +5 -0
- package/dist/rag/handler.js +103 -0
- package/dist/rag/helpers.js +126 -0
- package/dist/rag/schema.js +16 -0
- package/dist/read_document/handler.js +61 -0
- package/dist/read_document/schema.js +4 -0
- package/dist/server.js +864 -1
- package/dist/shell/handler.js +8 -0
- package/dist/subagent/handler.js +752 -0
- package/dist/subagent/handoffMessage.js +56 -0
- package/dist/subagent/schema.js +18 -0
- package/dist/subagent/subAgentToolCallParser.js +491 -0
- package/dist/subagent/toolCallValidator.js +97 -0
- package/dist/system/handler.js +239 -0
- package/dist/system/schema.js +21 -0
- package/dist/web/ddgParse.js +51 -0
- package/dist/web/handler.js +286 -0
- package/dist/web/schema.js +18 -0
- package/package.json +22 -2
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
// subagent/handler.ts — consult_secondary_agent (MCP filesystem fork).
|
|
2
|
+
//
|
|
3
|
+
// Port of the Beledarian reference implementation (beledarians-lm-studio-tools
|
|
4
|
+
// src/toolsProvider.ts L2465-3236): a delegated agent loop against the
|
|
5
|
+
// LM Studio /v1/chat/completions endpoint (default http://localhost:1234/v1,
|
|
6
|
+
// model "local-model" = the main loaded model). It parses JSON tool calls out
|
|
7
|
+
// of prose (subAgentToolCallParser.ts), validates them (toolCallValidator.ts),
|
|
8
|
+
// executes them against the FORK's own in-process handlers (no JSON-RPC
|
|
9
|
+
// round-trip), auto-saves code blocks from the FINAL response, runs the
|
|
10
|
+
// auto-debug reviewer pass when the primary task modified files, and returns:
|
|
11
|
+
//
|
|
12
|
+
// { response, generated_files, filesModified, handoff_message? }
|
|
13
|
+
// or
|
|
14
|
+
// { error, filesModified? }
|
|
15
|
+
//
|
|
16
|
+
// Environment overrides:
|
|
17
|
+
// MCP_SUBAGENT_ENDPOINT default http://localhost:1234/v1
|
|
18
|
+
// MCP_SUBAGENT_MODEL default local-model
|
|
19
|
+
// MCP_SUBAGENT_CWD sub-agent working dir (default: process.cwd())
|
|
20
|
+
// MCP_SUBAGENT_PROFILES JSON string {role: personaText} (default: built-ins)
|
|
21
|
+
// MCP_SUBAGENT_DEBUG "1" to enable [Sub-Agent] console logging
|
|
22
|
+
//
|
|
23
|
+
// Auth: Bearer token from the API token file (MCP_API_TOKEN_FILE, or the
|
|
24
|
+
// reference machine's overnight token file when unset) when present
|
|
25
|
+
// (keyless fallback when missing) — replicates the patched Beledarian
|
|
26
|
+
// behavior.
|
|
27
|
+
//
|
|
28
|
+
// Allowed-tool mapping (allow_tools=true) -> fork in-process handlers:
|
|
29
|
+
// read_file -> read_files
|
|
30
|
+
// list_directory -> list_directory
|
|
31
|
+
// save_file -> write_new_files (overwrite semantics)
|
|
32
|
+
// replace_text_in_file -> edit_files (replaceAll: false)
|
|
33
|
+
// delete_files_by_pattern -> delete_files_by_pattern
|
|
34
|
+
// rag_local_files -> rag_local_files
|
|
35
|
+
// fuzzy_find_local_files -> fuzzy_find_files
|
|
36
|
+
// search_file_content -> search_regex
|
|
37
|
+
// wikipedia_search -> wikipedia_search
|
|
38
|
+
// web_search / duckduckgo_search-> web_search
|
|
39
|
+
// fetch_web_content -> fetch_web_content
|
|
40
|
+
// rag_web_content -> rag_web_content
|
|
41
|
+
// browser_session_open/control/close -> same names
|
|
42
|
+
// run_python -> run_python (in the sub-agent CWD)
|
|
43
|
+
// run_javascript -> run_javascript
|
|
44
|
+
// finish_task -> termination signal (loop exit, no execution)
|
|
45
|
+
import { readFile, writeFile, appendFile, stat, mkdir, readdir } from "fs/promises";
|
|
46
|
+
import { join, dirname, resolve, relative, isAbsolute } from "path";
|
|
47
|
+
import { parseSubAgentResponseMessage, } from "./subAgentToolCallParser.js";
|
|
48
|
+
import { validateToolCall } from "./toolCallValidator.js";
|
|
49
|
+
import { extractHandoffMessage } from "./handoffMessage.js";
|
|
50
|
+
// The FORK's own in-process handlers (executed directly; no JSON-RPC round-trip)
|
|
51
|
+
import { handleReadFiles } from "../read_files/handler.js";
|
|
52
|
+
import { handleListDirectory } from "../list_directory/handler.js";
|
|
53
|
+
import { handleWriteNewFiles } from "../write_new_files/handler.js";
|
|
54
|
+
import { handleEditFiles } from "../edit_files/handler.js";
|
|
55
|
+
import { handleDeleteFilesByPattern } from "../delete_files_by_pattern/handler.js";
|
|
56
|
+
import { handleRagLocalFiles, handleRagWebContent } from "../rag/handler.js";
|
|
57
|
+
import { handleFuzzyFindFiles } from "../fuzzy_find_files/handler.js";
|
|
58
|
+
import { handleSearchRegex } from "../search_regex/handler.js";
|
|
59
|
+
import { handleWebSearch, handleFetchWebContent, handleWikipediaSearch, } from "../web/handler.js";
|
|
60
|
+
import { handleBrowserSessionOpen, handleBrowserSessionControl, handleBrowserSessionClose, } from "../browser/handler.js";
|
|
61
|
+
import { handleRunPython, handleRunJavascript } from "../compat/handler.js";
|
|
62
|
+
// --- Security Helper (ported from the reference L20-30) ---
|
|
63
|
+
function validatePath(baseDir, requestedPath) {
|
|
64
|
+
const resolved = resolve(baseDir, requestedPath);
|
|
65
|
+
// Use relative pathing to ensure the resolved path stays within baseDir
|
|
66
|
+
const rel = relative(baseDir, resolved);
|
|
67
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
68
|
+
throw new Error(`Access Denied: Path '${requestedPath}' is outside the workspace.`);
|
|
69
|
+
}
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
// (ported from the reference L32-62)
|
|
73
|
+
function extractLikelyFilePath(text) {
|
|
74
|
+
const isPlausiblePath = (value) => {
|
|
75
|
+
const candidate = value.trim();
|
|
76
|
+
if (!candidate)
|
|
77
|
+
return false;
|
|
78
|
+
if (/[\r\n]/.test(candidate))
|
|
79
|
+
return false;
|
|
80
|
+
if (candidate.includes("=") && !candidate.includes("\\") && !candidate.includes("/"))
|
|
81
|
+
return false;
|
|
82
|
+
if (/[<>|*?]/.test(candidate))
|
|
83
|
+
return false;
|
|
84
|
+
const extensionMatch = candidate.match(/\.([A-Za-z0-9_-]{1,15})$/);
|
|
85
|
+
if (!extensionMatch)
|
|
86
|
+
return false;
|
|
87
|
+
const extension = extensionMatch[1];
|
|
88
|
+
if (!/[A-Za-z]/.test(extension))
|
|
89
|
+
return false; // reject ".0" and similar numeric pseudo-extensions
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
const patterns = [
|
|
93
|
+
/['"]([A-Za-z]:\\[^'"\r\n]+)['"]/,
|
|
94
|
+
/\b([A-Za-z]:\\[^\s'"]+(?:\.[A-Za-z0-9_-]+)?)\b/,
|
|
95
|
+
/['"]((?:\.{0,2}[\\/])?[^'"\r\n]+\.[A-Za-z0-9_-]+)['"]/,
|
|
96
|
+
];
|
|
97
|
+
for (const pattern of patterns) {
|
|
98
|
+
const match = text.match(pattern);
|
|
99
|
+
if (!match?.[1])
|
|
100
|
+
continue;
|
|
101
|
+
const candidate = match[1].replace(/[),.;]+$/, "").trim();
|
|
102
|
+
if (!isPlausiblePath(candidate))
|
|
103
|
+
continue;
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// LM Studio API token file (2026-09-04 auth enablement). When the file exists,
|
|
109
|
+
// requests carry an Authorization: Bearer header; otherwise keyless (works
|
|
110
|
+
// when auth is off). Same file + fallback the patched Beledarian plugin uses.
|
|
111
|
+
// Reference machine's overnight token file (kept for back-compat; on other
|
|
112
|
+
// machines the file is simply absent -> keyless mode).
|
|
113
|
+
const LEGACY_API_TOKEN_FILE = "C:\\Users\\Gerar\\.beledarians-llm-toolbox\\workspace\\overnight\\lm_api_token.txt";
|
|
114
|
+
const API_TOKEN_FILE = process.env.MCP_API_TOKEN_FILE?.trim() || LEGACY_API_TOKEN_FILE;
|
|
115
|
+
// Built-in persona profiles:
|
|
116
|
+
// - 'summarizer'/'coder': the reference's config default (subAgentProfiles,
|
|
117
|
+
// beledarians config.ts L148)
|
|
118
|
+
// - 'reviewer': the reference's inline fallback persona (toolsProvider L2547)
|
|
119
|
+
const BUILT_IN_PROFILES = {
|
|
120
|
+
summarizer: "You are a summarization expert. Summarize the content concisely.",
|
|
121
|
+
coder: "You are a software engineer. Write efficient and safe code.",
|
|
122
|
+
reviewer: "You are a Senior Code Reviewer. Your job is to analyze code, find bugs, security issues, or logic errors, and FIX them.\n\nIMPORTANT: To fix a file, you MUST use the 'save_file' tool with the complete, corrected content. DO NOT use 'container.exec' or diff formats. Just overwrite the file with the fixed version using 'save_file'.",
|
|
123
|
+
};
|
|
124
|
+
// Full allowed-tools set for the fork: the reference gates these behind
|
|
125
|
+
// subAgentAllowFileSystem / subAgentAllowWeb / subAgentAllowCode /
|
|
126
|
+
// subAgentAllowBrowserControl settings (all on except code/browser by
|
|
127
|
+
// default). The fork has no settings UI, so allow_tools=true enables the
|
|
128
|
+
// full mapped set below.
|
|
129
|
+
const FULL_ALLOWED_TOOLS = [
|
|
130
|
+
"read_file", "list_directory", "save_file", "replace_text_in_file",
|
|
131
|
+
"delete_files_by_pattern", "rag_local_files", "fuzzy_find_local_files",
|
|
132
|
+
"search_file_content",
|
|
133
|
+
"wikipedia_search", "web_search", "duckduckgo_search",
|
|
134
|
+
"fetch_web_content", "rag_web_content",
|
|
135
|
+
"browser_session_open", "browser_session_control", "browser_session_close",
|
|
136
|
+
"run_python", "run_javascript",
|
|
137
|
+
];
|
|
138
|
+
export async function handleConsultSecondaryAgent(task, agentRole, context, allowTools, allowedDirectories) {
|
|
139
|
+
let endpoint = process.env.MCP_SUBAGENT_ENDPOINT || "http://localhost:1234/v1";
|
|
140
|
+
let modelId = process.env.MCP_SUBAGENT_MODEL || "local-model";
|
|
141
|
+
let handoffMessage = undefined;
|
|
142
|
+
let finalResponse = "";
|
|
143
|
+
const subAgentDebugLogging = process.env.MCP_SUBAGENT_DEBUG === "1";
|
|
144
|
+
// Sub-agent working directory (reference: plugin-state currentWorkingDirectory)
|
|
145
|
+
const currentWorkingDirectory = process.env.MCP_SUBAGENT_CWD || process.cwd();
|
|
146
|
+
// Persona profiles: env JSON override, else built-in defaults
|
|
147
|
+
let subAgentProfiles = { ...BUILT_IN_PROFILES };
|
|
148
|
+
const profilesEnv = process.env.MCP_SUBAGENT_PROFILES;
|
|
149
|
+
if (profilesEnv && profilesEnv.trim()) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(profilesEnv);
|
|
152
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
153
|
+
subAgentProfiles = parsed;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch { /* invalid JSON - keep built-ins */ }
|
|
157
|
+
}
|
|
158
|
+
// The fork has no settings UI; use the reference's config defaults:
|
|
159
|
+
// subAgentAutoSave=true, showFullCodeOutput=false. (The reference's
|
|
160
|
+
// enableDebugMode=false default is overridden here: the auto-debug
|
|
161
|
+
// reviewer pass runs for coding tasks, i.e. whenever the primary task
|
|
162
|
+
// modified files.)
|
|
163
|
+
const autoSave = true;
|
|
164
|
+
const showFullCode = false;
|
|
165
|
+
// LM Studio API auth (enabled 2026-09-04): send an Authorization header when
|
|
166
|
+
// the token file exists; otherwise stay keyless (works when auth is off).
|
|
167
|
+
let apiToken = "";
|
|
168
|
+
try {
|
|
169
|
+
const t = await readFile(API_TOKEN_FILE, "utf-8");
|
|
170
|
+
if (t.trim())
|
|
171
|
+
apiToken = t.trim();
|
|
172
|
+
}
|
|
173
|
+
catch { /* no token file - keyless mode */ }
|
|
174
|
+
const maxSubAgentToolOutputChars = 30000;
|
|
175
|
+
const truncateOutput = (s) => s.length > maxSubAgentToolOutputChars
|
|
176
|
+
? `${s.substring(0, maxSubAgentToolOutputChars)}\n... (truncated ${s.length - maxSubAgentToolOutputChars} chars)`
|
|
177
|
+
: s;
|
|
178
|
+
// Helper to run an agent loop (ported from the reference runAgentLoop)
|
|
179
|
+
const runAgentLoop = async (role, taskPrompt, contextData, loopLimit = 8, forceTools = false) => {
|
|
180
|
+
let currentSystemPrompt = "You are a helpful assistant.";
|
|
181
|
+
// Load Instructions
|
|
182
|
+
const instructionsPath = join(currentWorkingDirectory, "SUB_AGENT_INSTRUCTIONS.md");
|
|
183
|
+
try {
|
|
184
|
+
const instructions = await readFile(instructionsPath, "utf-8");
|
|
185
|
+
if (instructions.trim())
|
|
186
|
+
currentSystemPrompt = instructions;
|
|
187
|
+
}
|
|
188
|
+
catch { /* Ignore if instructions file doesn't exist */ }
|
|
189
|
+
// Inject Project Info
|
|
190
|
+
const infoPath = join(currentWorkingDirectory, "beledarian_info.md");
|
|
191
|
+
try {
|
|
192
|
+
const projectInfo = await readFile(infoPath, "utf-8");
|
|
193
|
+
if (projectInfo.trim()) {
|
|
194
|
+
currentSystemPrompt += `\n\n## ? Current Project Info (beledarian_info.md)\n${projectInfo}\n`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch { /* Ignore if info file doesn't exist */ }
|
|
198
|
+
// Add current working directory to system prompt for context
|
|
199
|
+
currentSystemPrompt += `\n\n## ? Current Workspace\nYour current working directory is: \n\n${currentWorkingDirectory}\nAlways assume relative paths are from this directory.`;
|
|
200
|
+
// Append specific profile if available
|
|
201
|
+
try {
|
|
202
|
+
if (subAgentProfiles[role]) {
|
|
203
|
+
currentSystemPrompt += `\n\n## Your Persona\n${subAgentProfiles[role]}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch { /* ignore */ }
|
|
207
|
+
// Append Tools
|
|
208
|
+
let toolsReminder = "";
|
|
209
|
+
const toolsEnabled = allowTools || forceTools;
|
|
210
|
+
if (toolsEnabled) {
|
|
211
|
+
const toolsList = FULL_ALLOWED_TOOLS.join(", ");
|
|
212
|
+
currentSystemPrompt += `\n\n## Allowed Tools\nYou have access to the following tools via JSON output: ${toolsList}.\nRefer to the "Tool Usage" section above for the JSON format.\n`;
|
|
213
|
+
toolsReminder = `\n\n[SYSTEM REMINDER: You have access to tools: ${toolsList}. If you need information you don't have, USE A TOOL. Do not refuse. Format tool calls exactly as: {"tool": "tool_name", "args": {"arg_name": "value"}}]`;
|
|
214
|
+
currentSystemPrompt += `\n\n## Browser Navigation Rule\nFor multi-step browsing/navigation, you MUST use browser_session_open -> browser_session_control -> browser_session_close.\nUse browser_open_page only for one-shot page reads.`;
|
|
215
|
+
}
|
|
216
|
+
currentSystemPrompt += `\n\n## Optional Handoff Message\nIf you want the main agent to relay your findings, include either:\n1) [HANDOFF_MESSAGE]...[/HANDOFF_MESSAGE]\nOR\n2) JSON with a \`handoff_message\` field (optionally with \`response\` or \`final_response\`).`;
|
|
217
|
+
currentSystemPrompt += `\n\n## Task Completion & Early Exit\nIf you have successfully completed your task, output 'TASK_COMPLETED'.\nIf you cannot complete the task (e.g., due to system limitations, missing files, or inaccessible paths), output 'TASK_FAILED' to abort early.`;
|
|
218
|
+
const msgList = [
|
|
219
|
+
{ role: "system", content: currentSystemPrompt },
|
|
220
|
+
{ role: "user", content: `Task: ${taskPrompt}\n\nContext: ${contextData}${toolsReminder}` },
|
|
221
|
+
];
|
|
222
|
+
let loops = 0;
|
|
223
|
+
let noToolCallCount = 0;
|
|
224
|
+
let executedToolCallCount = 0;
|
|
225
|
+
let finalContent = "";
|
|
226
|
+
let filesModified = [];
|
|
227
|
+
let handoffMessage = "";
|
|
228
|
+
const suggestedReadPath = toolsEnabled
|
|
229
|
+
? extractLikelyFilePath(`${taskPrompt}\n${contextData}`)
|
|
230
|
+
: null;
|
|
231
|
+
while (loops < loopLimit) {
|
|
232
|
+
try {
|
|
233
|
+
const subAgentHeaders = { "Content-Type": "application/json" };
|
|
234
|
+
if (apiToken)
|
|
235
|
+
subAgentHeaders["Authorization"] = `Bearer ${apiToken}`;
|
|
236
|
+
const response = await fetch(`${endpoint}/chat/completions`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: subAgentHeaders,
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
model: modelId,
|
|
241
|
+
messages: msgList,
|
|
242
|
+
temperature: 0.7,
|
|
243
|
+
stream: false
|
|
244
|
+
})
|
|
245
|
+
});
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
const errorBody = await response.text().catch(() => "");
|
|
248
|
+
const compactErrorBody = errorBody.replace(/\s+/g, " ").trim().substring(0, 600);
|
|
249
|
+
if (subAgentDebugLogging) {
|
|
250
|
+
console.log(`[Sub-Agent] API error status=${response.status} body=${compactErrorBody}`);
|
|
251
|
+
}
|
|
252
|
+
const details = compactErrorBody ? ` - ${compactErrorBody}` : "";
|
|
253
|
+
return { error: `API Error: ${response.status}${details}`, filesModified };
|
|
254
|
+
}
|
|
255
|
+
const data = await response.json();
|
|
256
|
+
const message = data?.choices?.[0]?.message;
|
|
257
|
+
const parsedMessage = parseSubAgentResponseMessage(message);
|
|
258
|
+
const content = parsedMessage.content;
|
|
259
|
+
let toolCall = parsedMessage.toolCall;
|
|
260
|
+
if (subAgentDebugLogging) {
|
|
261
|
+
const rawContent = (typeof message === "string" ? message : JSON.stringify(message)) ?? "";
|
|
262
|
+
console.log(`[Sub-Agent] RAW content received: ${rawContent.substring(0, 1000)}...`);
|
|
263
|
+
const preview = content.substring(0, 200);
|
|
264
|
+
console.log(`[Sub-Agent] Parse result source=${parsedMessage.toolCallSource} hasToolCall=${Boolean(toolCall)} preview=${preview}`);
|
|
265
|
+
}
|
|
266
|
+
// Always capture the latest content as the finalContent candidate
|
|
267
|
+
finalContent = content;
|
|
268
|
+
if (!toolsEnabled) {
|
|
269
|
+
const extracted = extractHandoffMessage(content);
|
|
270
|
+
// If the only output is a bare tool-call JSON (model tried to use tools it wasn't
|
|
271
|
+
// given), substitute a clear failure message rather than leaking raw JSON.
|
|
272
|
+
const looksLikePureToolCall = extracted.response.trimStart().startsWith("{") &&
|
|
273
|
+
parsedMessage.toolCall !== null &&
|
|
274
|
+
extracted.response.trim().length < 500;
|
|
275
|
+
const safeResponse = looksLikePureToolCall
|
|
276
|
+
? "[Sub-agent did not produce a prose response. It attempted a tool call but tools are disabled for this invocation.]"
|
|
277
|
+
: extracted.response;
|
|
278
|
+
return { response: safeResponse, filesModified, handoff_message: extracted.handoffMessage };
|
|
279
|
+
}
|
|
280
|
+
const trimmed = content.trim();
|
|
281
|
+
if (!toolCall && trimmed) {
|
|
282
|
+
const refusalKeywords = [
|
|
283
|
+
"i cannot browse", "i don't have access", "i can't access",
|
|
284
|
+
"unable to browse", "real-time news", "no internet access",
|
|
285
|
+
"as an ai", "i do not have the ability", "cannot access the internet"
|
|
286
|
+
];
|
|
287
|
+
if (refusalKeywords.some(kw => trimmed.toLowerCase().includes(kw))) {
|
|
288
|
+
msgList.push({ role: "assistant", content: content });
|
|
289
|
+
// role must stay "user": mid-conversation "system" messages break chat templates
|
|
290
|
+
// that require the system message first (e.g. HauhauCS Qwen templates raise)
|
|
291
|
+
msgList.push({ role: "user", content: "SYSTEM ERROR: You HAVE access to tools. USE THEM." });
|
|
292
|
+
loops++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (toolCall && toolCall.tool) {
|
|
297
|
+
noToolCallCount = 0;
|
|
298
|
+
executedToolCallCount++;
|
|
299
|
+
msgList.push({ role: "assistant", content: content });
|
|
300
|
+
let toolResult = "";
|
|
301
|
+
let finishNow = false;
|
|
302
|
+
const args = toolCall.args || {};
|
|
303
|
+
// Use shared validator to prevent duplication between prod and tests
|
|
304
|
+
const toolValidationError = validateToolCall(toolCall.tool, args);
|
|
305
|
+
if (toolValidationError) {
|
|
306
|
+
toolResult = `TOOL_VALIDATION_ERROR: ${toolValidationError}`;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
try {
|
|
310
|
+
// --- File System (fork handler mappings) ---
|
|
311
|
+
if (toolCall.tool === "read_file" && toolCall.args?.file_name) {
|
|
312
|
+
const fpath = validatePath(currentWorkingDirectory, toolCall.args.file_name);
|
|
313
|
+
const readContent = await handleReadFiles([fpath], allowedDirectories);
|
|
314
|
+
toolResult = truncateOutput(readContent);
|
|
315
|
+
}
|
|
316
|
+
else if (toolCall.tool === "list_directory") {
|
|
317
|
+
const dirPath = toolCall.args?.path
|
|
318
|
+
? validatePath(currentWorkingDirectory, toolCall.args.path)
|
|
319
|
+
: currentWorkingDirectory;
|
|
320
|
+
toolResult = truncateOutput(await handleListDirectory(dirPath, allowedDirectories));
|
|
321
|
+
}
|
|
322
|
+
else if (toolCall.tool === "save_file") {
|
|
323
|
+
// Handle batch files (some models return { files: [...] })
|
|
324
|
+
if (Array.isArray(toolCall.args?.files)) {
|
|
325
|
+
const fileEntries = [];
|
|
326
|
+
for (const fileObj of toolCall.args.files) {
|
|
327
|
+
const fName = fileObj.file_name || fileObj.name || fileObj.path;
|
|
328
|
+
const fContent = fileObj.content || fileObj.data;
|
|
329
|
+
if (fName && fContent) {
|
|
330
|
+
const fpath = validatePath(currentWorkingDirectory, fName);
|
|
331
|
+
fileEntries.push({ path: fpath, content: fContent, overwrite: true });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (fileEntries.length > 0) {
|
|
335
|
+
const res = await handleWriteNewFiles(fileEntries, allowedDirectories);
|
|
336
|
+
const savedList = fileEntries
|
|
337
|
+
.filter((f) => res.includes(`Successfully wrote to ${f.path}`))
|
|
338
|
+
.map((f) => {
|
|
339
|
+
const relName = relative(currentWorkingDirectory, f.path);
|
|
340
|
+
return relName.startsWith("..") || isAbsolute(relName) ? f.path : relName;
|
|
341
|
+
});
|
|
342
|
+
for (const name of savedList)
|
|
343
|
+
filesModified.push(name);
|
|
344
|
+
toolResult = savedList.length > 0
|
|
345
|
+
? `Success: Saved ${savedList.length} files: ${savedList.join(", ")}`
|
|
346
|
+
: `Error: No valid files found in batch.\n${res}`;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
toolResult = "Error: No valid files found in batch.";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Handle varying argument names (some models use name/data instead of file_name/content)
|
|
354
|
+
const fileName = toolCall.args?.file_name || toolCall.args?.name || toolCall.args?.path;
|
|
355
|
+
const content = toolCall.args?.content || toolCall.args?.data;
|
|
356
|
+
if (fileName && content) {
|
|
357
|
+
const fpath = validatePath(currentWorkingDirectory, fileName);
|
|
358
|
+
const res = await handleWriteNewFiles([{ path: fpath, content, overwrite: true }], allowedDirectories);
|
|
359
|
+
if (res.includes(`Successfully wrote to ${fpath}`)) {
|
|
360
|
+
toolResult = `Success: File saved to ${fpath}`;
|
|
361
|
+
filesModified.push(fileName);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
toolResult = `Error: ${res}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
toolResult = "Error: Missing 'file_name' (or 'name', 'path') or 'content' (or 'data') arguments.";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (toolCall.tool === "replace_text_in_file" && toolCall.args?.file_name && toolCall.args?.old_string && toolCall.args?.new_string) {
|
|
373
|
+
const fpath = validatePath(currentWorkingDirectory, toolCall.args.file_name);
|
|
374
|
+
const res = await handleEditFiles([{
|
|
375
|
+
path: fpath,
|
|
376
|
+
edits: [{
|
|
377
|
+
oldString: toolCall.args.old_string,
|
|
378
|
+
newString: toolCall.args.new_string,
|
|
379
|
+
replaceAll: false,
|
|
380
|
+
}],
|
|
381
|
+
}], allowedDirectories);
|
|
382
|
+
if (res.includes("1 files edited successfully")) {
|
|
383
|
+
toolResult = "Success: Text replaced.";
|
|
384
|
+
filesModified.push(toolCall.args.file_name);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
toolResult = `Error: ${res}`;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (toolCall.tool === "delete_files_by_pattern" && toolCall.args?.pattern) {
|
|
391
|
+
if (toolCall.args.pattern.length > 100)
|
|
392
|
+
throw new Error("Pattern too complex");
|
|
393
|
+
const regex = new RegExp(toolCall.args.pattern);
|
|
394
|
+
// ReDoS check
|
|
395
|
+
const start = Date.now();
|
|
396
|
+
regex.test("safe_test_string_for_redos_check_1234567890_safe_test_string_for_redos_check_1234567890");
|
|
397
|
+
if (Date.now() - start > 100)
|
|
398
|
+
throw new Error("Pattern too complex/slow");
|
|
399
|
+
toolResult = truncateOutput(await handleDeleteFilesByPattern(currentWorkingDirectory, toolCall.args.pattern, toolCall.args?.include_directories === true, allowedDirectories));
|
|
400
|
+
}
|
|
401
|
+
else if (toolCall.tool === "rag_local_files") {
|
|
402
|
+
if (toolCall.args?.query) {
|
|
403
|
+
toolResult = truncateOutput(await handleRagLocalFiles(toolCall.args.query, toolCall.args?.path, toolCall.args?.file_pattern, allowedDirectories));
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
toolResult = "Error: 'query' is required.";
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (toolCall.tool === "fuzzy_find_local_files" && toolCall.args?.query) {
|
|
410
|
+
const targetDir = toolCall.args?.path
|
|
411
|
+
? validatePath(currentWorkingDirectory, toolCall.args.path)
|
|
412
|
+
: currentWorkingDirectory;
|
|
413
|
+
const maxResults = Math.min(Math.max(Number(toolCall.args?.max_results ?? 5), 1), 20);
|
|
414
|
+
const scanLimit = Math.min(Math.max(Number(toolCall.args?.scan_limit ?? 20000), 1), 200000);
|
|
415
|
+
toolResult = truncateOutput(await handleFuzzyFindFiles(toolCall.args.query, targetDir, toolCall.args?.recursive !== false, maxResults, scanLimit, allowedDirectories));
|
|
416
|
+
}
|
|
417
|
+
else if (toolCall.tool === "search_file_content" && toolCall.args?.pattern) {
|
|
418
|
+
const targetPath = toolCall.args?.path
|
|
419
|
+
? validatePath(currentWorkingDirectory, toolCall.args.path)
|
|
420
|
+
: currentWorkingDirectory;
|
|
421
|
+
const maxResults = Math.min(Math.max(Number(toolCall.args?.max_results ?? 100), 1), 500);
|
|
422
|
+
toolResult = truncateOutput(await handleSearchRegex(targetPath, toolCall.args.pattern, Array.isArray(toolCall.args?.file_patterns) ? toolCall.args.file_patterns : [], Array.isArray(toolCall.args?.exclude_patterns) ? toolCall.args.exclude_patterns : [], maxResults, toolCall.args?.case_sensitive === true, 1, allowedDirectories, toolCall.args?.count_only === true));
|
|
423
|
+
// --- Web (fork handler mappings) ---
|
|
424
|
+
}
|
|
425
|
+
else if (toolCall.tool === "wikipedia_search" && toolCall.args?.query) {
|
|
426
|
+
toolResult = truncateOutput(await handleWikipediaSearch(toolCall.args.query, toolCall.args?.lang || "en"));
|
|
427
|
+
}
|
|
428
|
+
else if (toolCall.tool === "web_search" || toolCall.tool === "duckduckgo_search") {
|
|
429
|
+
if (toolCall.args?.query) {
|
|
430
|
+
toolResult = truncateOutput(await handleWebSearch(toolCall.args.query));
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
toolResult = "Error: 'query' is required.";
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else if (toolCall.tool === "fetch_web_content" && toolCall.args?.url) {
|
|
437
|
+
toolResult = truncateOutput(await handleFetchWebContent(toolCall.args.url));
|
|
438
|
+
}
|
|
439
|
+
else if (toolCall.tool === "rag_web_content" && toolCall.args?.url && toolCall.args?.query) {
|
|
440
|
+
toolResult = truncateOutput(await handleRagWebContent(toolCall.args.url, toolCall.args.query));
|
|
441
|
+
// --- Browser (fork handler mappings) ---
|
|
442
|
+
}
|
|
443
|
+
else if (toolCall.tool === "browser_session_open" && toolCall.args?.url) {
|
|
444
|
+
toolResult = truncateOutput(await handleBrowserSessionOpen(toolCall.args.url, toolCall.args?.wait_for_selector, toolCall.args?.include_page_text));
|
|
445
|
+
}
|
|
446
|
+
else if (toolCall.tool === "browser_session_control") {
|
|
447
|
+
toolResult = truncateOutput(await handleBrowserSessionControl(toolCall.args ?? {}, allowedDirectories));
|
|
448
|
+
}
|
|
449
|
+
else if (toolCall.tool === "browser_session_close") {
|
|
450
|
+
toolResult = truncateOutput(await handleBrowserSessionClose());
|
|
451
|
+
// --- Code (fork handler mappings) ---
|
|
452
|
+
}
|
|
453
|
+
else if (toolCall.tool === "run_python" && toolCall.args?.python) {
|
|
454
|
+
toolResult = truncateOutput(await handleRunPython(toolCall.args.python, toolCall.args?.timeout_seconds, currentWorkingDirectory));
|
|
455
|
+
}
|
|
456
|
+
else if (toolCall.tool === "run_javascript" && toolCall.args?.javascript) {
|
|
457
|
+
toolResult = truncateOutput(await handleRunJavascript(toolCall.args.javascript, toolCall.args?.timeout_seconds));
|
|
458
|
+
// finish_task: termination signal, not an executable tool (the
|
|
459
|
+
// reference's validator special-cases it; the loop exits cleanly)
|
|
460
|
+
}
|
|
461
|
+
else if (toolCall.tool === "finish_task") {
|
|
462
|
+
if (toolCall.args?.message && !finalContent) {
|
|
463
|
+
finalContent = String(toolCall.args.message);
|
|
464
|
+
}
|
|
465
|
+
finishNow = true;
|
|
466
|
+
}
|
|
467
|
+
if (!toolResult)
|
|
468
|
+
toolResult = "Error: Tool not found/allowed.";
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
toolResult = `Error: ${err.message}`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (finishNow)
|
|
475
|
+
break;
|
|
476
|
+
msgList.push({ role: "user", content: `Tool Output: ${toolResult}` });
|
|
477
|
+
loops++;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// NO TOOL CALL DETECTED
|
|
481
|
+
const shouldAutoFallbackRead = toolsEnabled &&
|
|
482
|
+
executedToolCallCount === 0 &&
|
|
483
|
+
noToolCallCount === 0 &&
|
|
484
|
+
typeof suggestedReadPath === "string" &&
|
|
485
|
+
suggestedReadPath.length > 0;
|
|
486
|
+
if (shouldAutoFallbackRead) {
|
|
487
|
+
try {
|
|
488
|
+
const autoReadPath = validatePath(currentWorkingDirectory, suggestedReadPath);
|
|
489
|
+
const autoReadStats = await stat(autoReadPath);
|
|
490
|
+
if (!autoReadStats.isFile()) {
|
|
491
|
+
throw new Error(`Not a file: ${autoReadPath}`);
|
|
492
|
+
}
|
|
493
|
+
const autoReadContent = await readFile(autoReadPath, "utf-8");
|
|
494
|
+
const boundedContent = autoReadContent.length > 30000
|
|
495
|
+
? `${autoReadContent.substring(0, 30000)}\n... (truncated)`
|
|
496
|
+
: autoReadContent;
|
|
497
|
+
if (trimmed.length > 0) {
|
|
498
|
+
msgList.push({ role: "assistant", content: content });
|
|
499
|
+
}
|
|
500
|
+
msgList.push({
|
|
501
|
+
role: "user",
|
|
502
|
+
content: `Tool Output: AUTO_FALLBACK read_file(${suggestedReadPath})\n${boundedContent}`,
|
|
503
|
+
});
|
|
504
|
+
executedToolCallCount++;
|
|
505
|
+
loops++;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (subAgentDebugLogging) {
|
|
510
|
+
console.log(`[Sub-Agent] Auto fallback read_file failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const autoFiles = await readdir(currentWorkingDirectory);
|
|
514
|
+
const limitedFiles = autoFiles.slice(0, 200);
|
|
515
|
+
if (trimmed.length > 0) {
|
|
516
|
+
msgList.push({ role: "assistant", content: content });
|
|
517
|
+
}
|
|
518
|
+
msgList.push({
|
|
519
|
+
role: "user",
|
|
520
|
+
content: `Tool Output: AUTO_FALLBACK list_directory(.)\n${JSON.stringify(limitedFiles)}`,
|
|
521
|
+
});
|
|
522
|
+
executedToolCallCount++;
|
|
523
|
+
loops++;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
// ignore and continue to normal no-tool fallback behavior
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Check for explicit completion phrase or strict loop limit
|
|
532
|
+
const planningLikeText = /(?:\bI(?:'ll| will)\b|\blet me\b|\bnext\b|\bfirst\b)/i.test(trimmed);
|
|
533
|
+
const shouldTreatAsFinalResponse = trimmed.length >= 120 &&
|
|
534
|
+
!planningLikeText;
|
|
535
|
+
// Increment no-tool counter
|
|
536
|
+
noToolCallCount++;
|
|
537
|
+
if (content.includes("TASK_COMPLETED") || content.includes("TASK_FAILED") || shouldTreatAsFinalResponse || noToolCallCount >= 3 || loops >= loopLimit - 1) {
|
|
538
|
+
break; // Done
|
|
539
|
+
}
|
|
540
|
+
if (content.trim().length > 0) {
|
|
541
|
+
msgList.push({ role: "assistant", content: content });
|
|
542
|
+
}
|
|
543
|
+
let reminder = "SYSTEM NOTICE: You did not call a tool. If you are finished, output 'TASK_COMPLETED'. If you cannot complete the task, output 'TASK_FAILED'. If not, USE A TOOL now and return a single JSON tool-call object only (no prose).";
|
|
544
|
+
if (toolsEnabled) {
|
|
545
|
+
if (suggestedReadPath && noToolCallCount <= 3) {
|
|
546
|
+
const escapedPath = suggestedReadPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
547
|
+
reminder += `\nSuggested next step: {"tool":"read_file","args":{"file_name":"${escapedPath}"}}`;
|
|
548
|
+
}
|
|
549
|
+
else if (noToolCallCount <= 3) {
|
|
550
|
+
reminder += `\nSuggested next step: {"tool":"list_directory","args":{}}`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// role must stay "user" — see note on the SYSTEM ERROR push above
|
|
554
|
+
msgList.push({ role: "user", content: reminder });
|
|
555
|
+
loops++;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
return { error: err.message, filesModified };
|
|
560
|
+
}
|
|
561
|
+
// Prevent unbounded memory growth
|
|
562
|
+
if (msgList.length > 20) {
|
|
563
|
+
// Keep system message (index 0) and last 18 messages
|
|
564
|
+
const systemMsg = msgList[0];
|
|
565
|
+
const recentMsgs = msgList.slice(-18);
|
|
566
|
+
msgList.length = 0;
|
|
567
|
+
msgList.push(systemMsg, ...recentMsgs);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (finalContent) {
|
|
571
|
+
const extracted = extractHandoffMessage(finalContent);
|
|
572
|
+
finalContent = extracted.response;
|
|
573
|
+
handoffMessage = extracted.handoffMessage || "";
|
|
574
|
+
}
|
|
575
|
+
// --- Auto-Save Logic (ported from the reference) ---
|
|
576
|
+
if (autoSave && finalContent) {
|
|
577
|
+
// Regex matches: ```lang (optional space/newline) code ```
|
|
578
|
+
// Relaxed to not strictly require \n, handling ```html code...
|
|
579
|
+
const codeBlockRegex = /```\s*(\w+)?\s*([\s\S]*?)```/g;
|
|
580
|
+
// Get all matches from the ORIGINAL string
|
|
581
|
+
const matches = Array.from(finalContent.matchAll(codeBlockRegex));
|
|
582
|
+
const processedFiles = new Set();
|
|
583
|
+
// Iterate BACKWARDS to preserve indices for replacement
|
|
584
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
585
|
+
const match = matches[i];
|
|
586
|
+
const fullBlock = match[0];
|
|
587
|
+
const lang = (match[1] || "txt").toLowerCase();
|
|
588
|
+
const code = match[2];
|
|
589
|
+
const index = match.index || 0;
|
|
590
|
+
let handledAsBatch = false;
|
|
591
|
+
// Smart JSON Unpacking
|
|
592
|
+
if (lang === "json") {
|
|
593
|
+
try {
|
|
594
|
+
const parsed = JSON.parse(code);
|
|
595
|
+
if (Array.isArray(parsed)) {
|
|
596
|
+
let extractedCount = 0;
|
|
597
|
+
for (const item of parsed) {
|
|
598
|
+
const fName = item.path || item.file_name || item.name;
|
|
599
|
+
const fContent = item.content || item.data || item.code;
|
|
600
|
+
if (fName && typeof fName === "string" && fContent && typeof fContent === "string") {
|
|
601
|
+
const fpath = validatePath(currentWorkingDirectory, fName);
|
|
602
|
+
await mkdir(dirname(fpath), { recursive: true });
|
|
603
|
+
await writeFile(fpath, fContent, "utf-8");
|
|
604
|
+
filesModified.push(fName);
|
|
605
|
+
processedFiles.add(fName);
|
|
606
|
+
extractedCount++;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (extractedCount > 0) {
|
|
610
|
+
handledAsBatch = true;
|
|
611
|
+
const replacement = `\n[System: Successfully extracted and saved ${extractedCount} files from JSON block.]\n`;
|
|
612
|
+
finalContent = finalContent.slice(0, index) + replacement + finalContent.slice(index + fullBlock.length);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// Not valid JSON or not the structure we want, fall through to normal save
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!handledAsBatch && code.trim().length > 50) {
|
|
621
|
+
// Lookback in the ORIGINAL string (match.input is safe)
|
|
622
|
+
const lookback = finalContent.substring(Math.max(0, index - 500), index);
|
|
623
|
+
// Regex to find filenames like `### src/App.tsx`, `**App.tsx**`, `filename: App.tsx`
|
|
624
|
+
const nameMatch = lookback.match(/(?:`|\*\*|###|filename:|file:)[\s\S]*?([\w\-\/\\.]+\.(?:tsx|ts|jsx|js|html|css|json|md|py|sh|java|rs|go|sql|yaml|yml|c|cpp|h|hpp|txt))/i);
|
|
625
|
+
let fileName = "";
|
|
626
|
+
if (nameMatch) {
|
|
627
|
+
fileName = nameMatch[1].trim();
|
|
628
|
+
}
|
|
629
|
+
// Fallback: Check the first line of the code block for a filename comment
|
|
630
|
+
// e.g. // src/App.tsx or # filename: utils.py
|
|
631
|
+
if (!fileName) {
|
|
632
|
+
const firstLine = code.split('\n')[0].trim();
|
|
633
|
+
const commentMatch = firstLine.match(/^(?:\/\/|#|<!--|;)\s*(?:filename:|file:)?\s*([\w\-\/\\.]+\.(?:tsx|ts|jsx|js|html|css|json|md|py|sh|java|rs|go|sql|yaml|yml|c|cpp|h|hpp|txt))/i);
|
|
634
|
+
if (commentMatch) {
|
|
635
|
+
fileName = commentMatch[1].trim();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Block Shell/Console snippets from being auto-saved as "auto_gen" files
|
|
639
|
+
// unless there is an EXPLICIT filename match above.
|
|
640
|
+
const isShell = ["bash", "sh", "cmd", "powershell", "console", "zsh", "terminal"].includes(lang);
|
|
641
|
+
if (isShell && !fileName) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
// If we didn't find a filename, skip saving this block.
|
|
645
|
+
// This prevents "auto_gen" files from cluttering the workspace.
|
|
646
|
+
if (!fileName) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
// Deduplication: If we already processed this file in this turn, skip saving it again
|
|
650
|
+
// (the LAST occurrence we are processing is the definitive one).
|
|
651
|
+
if (processedFiles.has(fileName)) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const fpath = join(currentWorkingDirectory, fileName);
|
|
655
|
+
try {
|
|
656
|
+
await mkdir(dirname(fpath), { recursive: true });
|
|
657
|
+
await writeFile(fpath, code, "utf-8");
|
|
658
|
+
filesModified.push(fileName);
|
|
659
|
+
processedFiles.add(fileName);
|
|
660
|
+
// Replace the block in finalContent using string slicing with the original index
|
|
661
|
+
const replacement = `\n[System: File '${fileName}' created successfully.]\n`;
|
|
662
|
+
finalContent = finalContent.slice(0, index) + replacement + finalContent.slice(index + fullBlock.length);
|
|
663
|
+
}
|
|
664
|
+
catch (e) {
|
|
665
|
+
console.error(`Failed to auto-save file ${fileName}:`, e);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// --- Auto-Update Project Info (ported from the reference) ---
|
|
671
|
+
if (filesModified.length > 0) {
|
|
672
|
+
const infoPath = join(currentWorkingDirectory, "beledarian_info.md");
|
|
673
|
+
const timestamp = new Date().toISOString();
|
|
674
|
+
const logEntry = `\n- **[${timestamp}]** Task: "${taskPrompt.substring(0, 50)}..." | Modified: ${filesModified.join(", ")}`;
|
|
675
|
+
try {
|
|
676
|
+
await appendFile(infoPath, logEntry, "utf-8");
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// If append fails, maybe file doesn't exist, try write
|
|
680
|
+
try {
|
|
681
|
+
await writeFile(infoPath, `# Project History\n${logEntry}`, "utf-8");
|
|
682
|
+
}
|
|
683
|
+
catch { /* ignore */ }
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return { response: finalContent, filesModified, handoff_message: handoffMessage || undefined };
|
|
687
|
+
};
|
|
688
|
+
// --- 1. Primary Agent Loop ---
|
|
689
|
+
const primaryResult = await runAgentLoop(agentRole || "general", task, context || "", 8, false);
|
|
690
|
+
if (primaryResult.error)
|
|
691
|
+
return JSON.stringify({ error: primaryResult.error, filesModified: primaryResult.filesModified });
|
|
692
|
+
finalResponse = primaryResult.response || "";
|
|
693
|
+
handoffMessage = primaryResult.handoff_message;
|
|
694
|
+
const generatedFiles = [...primaryResult.filesModified];
|
|
695
|
+
// --- 2. Auto-Debug Loop (ported from the reference; the fork runs it for
|
|
696
|
+
// coding tasks = whenever the primary task modified files) ---
|
|
697
|
+
if (primaryResult.filesModified.length > 0) {
|
|
698
|
+
const filesToCheck = primaryResult.filesModified.join(", ");
|
|
699
|
+
const debugTask = `Review the code in these files: ${filesToCheck}. Check for bugs, syntax errors, or logic flaws. If you find any, use 'save_file' to FIX them. If they are correct, confirm it.`;
|
|
700
|
+
// Read content of modified files to pass as context
|
|
701
|
+
let debugContext = "Here is the content of the created files:\n";
|
|
702
|
+
for (const f of primaryResult.filesModified) {
|
|
703
|
+
try {
|
|
704
|
+
const c = await readFile(join(currentWorkingDirectory, f), "utf-8");
|
|
705
|
+
debugContext += `\n--- ${f} ---\n${c}\n`;
|
|
706
|
+
}
|
|
707
|
+
catch { /* ignore unreadable */ }
|
|
708
|
+
}
|
|
709
|
+
const debugResult = await runAgentLoop("reviewer", debugTask, debugContext, 5, true);
|
|
710
|
+
finalResponse += "\n\n--- Auto-Debug Report ---\n" + (debugResult.response || (debugResult.error ? `Debug pass failed: ${debugResult.error}` : "Debug pass completed."));
|
|
711
|
+
if (debugResult.filesModified.length > 0) {
|
|
712
|
+
finalResponse += `\n(The reviewer fixed these files: ${debugResult.filesModified.join(", ")})`;
|
|
713
|
+
}
|
|
714
|
+
if (!handoffMessage && debugResult.handoff_message) {
|
|
715
|
+
handoffMessage = debugResult.handoff_message;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Append generated file list for Main Agent visibility
|
|
719
|
+
if (primaryResult.filesModified.length > 0) {
|
|
720
|
+
const fullPaths = primaryResult.filesModified.map(f => {
|
|
721
|
+
if (isAbsolute(f))
|
|
722
|
+
return f;
|
|
723
|
+
return join(currentWorkingDirectory, f);
|
|
724
|
+
});
|
|
725
|
+
finalResponse += `\n\n[GENERATED_FILES]: ${fullPaths.join(", ")}`;
|
|
726
|
+
if (showFullCode) {
|
|
727
|
+
finalResponse += `\n\n### Generated Code Content:\n`;
|
|
728
|
+
for (const f of primaryResult.filesModified) {
|
|
729
|
+
try {
|
|
730
|
+
const fpath = isAbsolute(f) ? f : join(currentWorkingDirectory, f);
|
|
731
|
+
const content = await readFile(fpath, "utf-8");
|
|
732
|
+
const ext = f.split('.').pop() || 'txt';
|
|
733
|
+
finalResponse += `\n**${f}**\n\`\`\`${ext}\n${content}\n\`\`\`\n`;
|
|
734
|
+
}
|
|
735
|
+
catch { /* ignore unreadable */ }
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Only hide code blocks when files were actually saved.
|
|
740
|
+
// If nothing was written to disk, leave the raw response intact so the main agent
|
|
741
|
+
// can see what the sub-agent actually did (or didn't do) rather than being misled
|
|
742
|
+
// by a false "code has been handled" success message.
|
|
743
|
+
if (!showFullCode && primaryResult.filesModified.length > 0) {
|
|
744
|
+
finalResponse = finalResponse.replace(/```[\s\S]*?```/g, "\n[System: Code Block Hidden for Brevity. The code has been handled/saved by the sub-agent. Do NOT request it again. Proceed.]\n");
|
|
745
|
+
}
|
|
746
|
+
return JSON.stringify({
|
|
747
|
+
response: finalResponse,
|
|
748
|
+
generated_files: generatedFiles,
|
|
749
|
+
filesModified: primaryResult.filesModified,
|
|
750
|
+
handoff_message: handoffMessage,
|
|
751
|
+
});
|
|
752
|
+
}
|