neoagent 2.5.2-beta.23 → 2.5.2-beta.24
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/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/services/ai/loop/conversation_loop.js +9 -1
- package/server/services/ai/loop/tool_dispatch.js +1 -0
- package/server/services/ai/messagingFallback.js +1 -1
- package/server/services/ai/preModelCompaction.js +31 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/taskAnalysis.js +1 -1
- package/server/services/ai/toolEvidence.js +3 -1
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +103 -0
- package/server/services/workspace/manager.js +55 -0
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6cae41f1f3b746d64f017898666a06bd
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "571651995" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -134794,7 +134794,7 @@ r===$&&A.b()
|
|
|
134794
134794
|
p.push(A.jP(q,A.j9(!1,new A.a_(B.uG,A.d8(new A.cA(B.jt,new A.a7N(r,q),q),q,q),q),!1,B.H,!0),q,q,0,0,0,q))}r=!1
|
|
134795
134795
|
if(!s.ay)if(!s.ch){r=s.e
|
|
134796
134796
|
r===$&&A.b()
|
|
134797
|
-
r=B.b.u("
|
|
134797
|
+
r=B.b.u("mqgvaivu-1294860").length!==0&&r.b}if(r){r=s.d
|
|
134798
134798
|
r===$&&A.b()
|
|
134799
134799
|
r=r.aP&&!r.ai?84:0
|
|
134800
134800
|
s=s.e
|
|
@@ -140506,7 +140506,7 @@ $S:0}
|
|
|
140506
140506
|
A.a_6.prototype={}
|
|
140507
140507
|
A.SQ.prototype={
|
|
140508
140508
|
nb(a){var s=this
|
|
140509
|
-
if(B.b.u("
|
|
140509
|
+
if(B.b.u("mqgvaivu-1294860").length===0||s.a!=null)return
|
|
140510
140510
|
s.AU()
|
|
140511
140511
|
s.a=A.on(B.RH,new A.bc8(s))},
|
|
140512
140512
|
AU(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
|
|
@@ -140524,7 +140524,7 @@ if(!t.f.b(k)){s=1
|
|
|
140524
140524
|
break}i=J.a3(k,"buildId")
|
|
140525
140525
|
h=i==null?null:B.b.u(J.p(i))
|
|
140526
140526
|
j=h==null?"":h
|
|
140527
|
-
if(J.bi(j)===0||J.d(j,"
|
|
140527
|
+
if(J.bi(j)===0||J.d(j,"mqgvaivu-1294860")){s=1
|
|
140528
140528
|
break}n.b=!0
|
|
140529
140529
|
n.F()
|
|
140530
140530
|
p=2
|
|
@@ -140541,7 +140541,7 @@ case 2:return A.i(o.at(-1),r)}})
|
|
|
140541
140541
|
return A.k($async$AU,r)},
|
|
140542
140542
|
vE(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
|
|
140543
140543
|
var $async$vE=A.h(function(a2,a3){if(a2===1){o.push(a3)
|
|
140544
|
-
s=p}for(;;)switch(s){case 0:if(B.b.u("
|
|
140544
|
+
s=p}for(;;)switch(s){case 0:if(B.b.u("mqgvaivu-1294860").length===0||n.c){s=1
|
|
140545
140545
|
break}n.c=!0
|
|
140546
140546
|
n.F()
|
|
140547
140547
|
p=4
|
|
@@ -257,7 +257,13 @@ function summarizeReadTargets(toolExecutions = []) {
|
|
|
257
257
|
if (!item || item.stateChanged) continue; // only read-only steps
|
|
258
258
|
const input = item.input || {};
|
|
259
259
|
let target = '';
|
|
260
|
-
if (
|
|
260
|
+
if (Array.isArray(input.files) && input.files.length) {
|
|
261
|
+
target = input.files
|
|
262
|
+
.map((file) => (typeof file === 'string' ? file : file?.path || file?.file_path || ''))
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.slice(0, 3)
|
|
265
|
+
.join(', ');
|
|
266
|
+
} else if (typeof input.path === 'string' && input.path.trim()) {
|
|
261
267
|
target = input.path.trim();
|
|
262
268
|
} else if (typeof input.command === 'string') {
|
|
263
269
|
const files = input.command.match(/[\w./-]+\.(?:js|ts|tsx|jsx|py|dart|json|md|kt|c|h|ya?ml|sql|txt|sh)\b/g);
|
|
@@ -919,6 +925,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
919
925
|
|
|
920
926
|
tools = selectInitialTools(allTools, analysis.suggested_tools, {
|
|
921
927
|
widgetId: options.widgetId || null,
|
|
928
|
+
includeCoreFileTools: analysis.mode === 'execute' || analysis.mode === 'plan_execute',
|
|
922
929
|
});
|
|
923
930
|
engine.initializeToolRuntime(runId, allTools, tools, options);
|
|
924
931
|
messages.push({
|
|
@@ -928,6 +935,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
928
935
|
buildToolCatalog(allTools),
|
|
929
936
|
'',
|
|
930
937
|
`Active tools: ${tools.map((tool) => tool.name).join(', ')}`,
|
|
938
|
+
'For workspace file inspection/editing, prefer read_files, read_file, search_files, list_directory, edit_file, replace_file_range, and write_file over shell cat/sed/python snippets. Use execute_command for git, tests, package managers, builds, and other shell-native actions.',
|
|
931
939
|
'Use activate_tools with exact catalog names when another schema is required.',
|
|
932
940
|
].join('\n'),
|
|
933
941
|
});
|
|
@@ -81,7 +81,7 @@ function parseToolExecutionSummary(item) {
|
|
|
81
81
|
function toolWorkDescription(toolName) {
|
|
82
82
|
const name = String(toolName || '');
|
|
83
83
|
if (name === 'execute_command') return 'ran shell commands';
|
|
84
|
-
if (name === 'read_file' || name === 'search_files' || name === 'list_directory') return 'checked files';
|
|
84
|
+
if (name === 'read_file' || name === 'read_files' || name === 'search_files' || name === 'list_directory') return 'checked files';
|
|
85
85
|
if (name === 'web_search' || name === 'http_request') return 'looked up supporting information';
|
|
86
86
|
if (name.startsWith('browser_')) return 'checked the browser state';
|
|
87
87
|
if (name.startsWith('android_')) return 'checked the Android state';
|
|
@@ -159,6 +159,35 @@ function compactReadFileResult(result) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function compactReadFilesResult(result) {
|
|
163
|
+
const source = result && typeof result === 'object' ? result : {};
|
|
164
|
+
const rawLength = JSON.stringify(source).length;
|
|
165
|
+
const results = Array.isArray(source.results)
|
|
166
|
+
? source.results.slice(0, 8).map((item) => {
|
|
167
|
+
const compacted = compactTextPayload(item?.content || '', {
|
|
168
|
+
maxChars: 900,
|
|
169
|
+
maxLines: 25,
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
...item,
|
|
173
|
+
content: compacted.text,
|
|
174
|
+
};
|
|
175
|
+
})
|
|
176
|
+
: [];
|
|
177
|
+
const next = { ...source, results };
|
|
178
|
+
const compactedLength = JSON.stringify(next).length;
|
|
179
|
+
return {
|
|
180
|
+
result: next,
|
|
181
|
+
metrics: {
|
|
182
|
+
inputChars: rawLength,
|
|
183
|
+
outputChars: compactedLength,
|
|
184
|
+
reducedChars: Math.max(0, rawLength - compactedLength),
|
|
185
|
+
applied: compactedLength !== rawLength,
|
|
186
|
+
strategies: compactedLength !== rawLength ? ['batch_file_result_truncation'] : [],
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
162
191
|
function compactPayloadForModel(toolName, result) {
|
|
163
192
|
switch (String(toolName || '').trim()) {
|
|
164
193
|
case 'http_request':
|
|
@@ -170,6 +199,8 @@ function compactPayloadForModel(toolName, result) {
|
|
|
170
199
|
return compactSearchResult(result);
|
|
171
200
|
case 'read_file':
|
|
172
201
|
return compactReadFileResult(result);
|
|
202
|
+
case 'read_files':
|
|
203
|
+
return compactReadFilesResult(result);
|
|
173
204
|
default:
|
|
174
205
|
return {
|
|
175
206
|
result,
|
|
@@ -190,7 +190,7 @@ SKILLS
|
|
|
190
190
|
Create or improve a skill only when it is clearly reusable, polished, and likely to matter again. Most completed tasks should not become skills.
|
|
191
191
|
|
|
192
192
|
GITHUB
|
|
193
|
-
When working with a GitHub repository's code (reading files, exploring structure, analysing a codebase), prefer cloning it locally with execute_command (git clone https://github.com/owner/repo /tmp/repo-name) and then using read_file, list_directory, and
|
|
193
|
+
When working with a GitHub repository's code (reading files, exploring structure, analysing a codebase), prefer cloning it locally with execute_command (git clone https://github.com/owner/repo /tmp/repo-name) and then using read_files, read_file, list_directory, search_files, edit_file, and replace_file_range on the local clone. File-by-file GitHub API calls are slow and hit rate limits fast.
|
|
194
194
|
Use github_api_request for metadata and structured GitHub data (issues, PRs, commits, releases, CI runs, repo stats). When calling github_api_request, the path must be the FULL API path starting from the root, e.g. /repos/NeoLabs-Systems/NeoAgent/git/trees/main?recursive=1. You can also pass owner_repo="owner/repo" together with a relative path like /git/trees/main and the prefix is prepended automatically.
|
|
195
195
|
Never fetch a repo's full file tree through the GitHub API when you actually need to read the code — clone it instead.
|
|
196
196
|
|
|
@@ -84,7 +84,7 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
|
|
|
84
84
|
const EXECUTION_GUIDANCE_ACTION_LINES = [
|
|
85
85
|
'Act end-to-end. Run independent searches or inspections in parallel when possible. Prefer native integration tools and structured APIs over browser automation or shell scraping. Use exact IDs and required parameters; list or search first when you do not have them.',
|
|
86
86
|
'For GitHub issue implementation or PR work, fetch the issue once, then establish or reuse a writable local checkout, create a task branch, inspect/edit/test locally, and push/open the PR. Use direct GitHub file mutation tools only as a fallback when a local checkout is unavailable.',
|
|
87
|
-
'Your shell (execute_command) starts in your workspace, and the file tools (read_file, write_file, list_directory, search_files) operate on that same workspace. Clone or create files into the current directory
|
|
87
|
+
'Your shell (execute_command) starts in your workspace, and the file tools (read_file, read_files, write_file, edit_file, replace_file_range, list_directory, search_files) operate on that same workspace. Clone or create files into the current directory, then prefer file tools for inspection and edits instead of shell cat/sed/python snippets. Clone a repo once and reuse it; do not re-clone or re-list the same tree.',
|
|
88
88
|
'Tool results are already present in the conversation as tool output. Do not assume a tool result was persisted to a readable /tmp path unless that exact path was explicitly returned by the tool result.',
|
|
89
89
|
'Use send_interim_update sparingly when a short real update or question would help.',
|
|
90
90
|
'Do not create background tasks for immediate short work. Answer directly unless the user asked to schedule, repeat, monitor, defer, or manage a saved task.',
|
|
@@ -23,7 +23,7 @@ const EVIDENCE_SOURCE_RULES = [
|
|
|
23
23
|
{ source: 'memory', match: (name) => name.startsWith('memory_') || name === 'session_search' },
|
|
24
24
|
{ source: 'search', match: (name) => name === 'web_search' },
|
|
25
25
|
{ source: 'http', match: (name) => name === 'http_request' },
|
|
26
|
-
{ source: 'files', match: (name) => ['read_file', 'search_files', 'list_directory', 'write_file', 'edit_file', 'code_navigate', 'query_structured_data'].includes(name) },
|
|
26
|
+
{ source: 'files', match: (name) => ['read_file', 'read_files', 'search_files', 'list_directory', 'write_file', 'edit_file', 'replace_file_range', 'code_navigate', 'query_structured_data'].includes(name) },
|
|
27
27
|
{ source: 'command', match: (name) => name === 'execute_command' },
|
|
28
28
|
{ source: 'skills', match: (name) => name.includes('skill') },
|
|
29
29
|
{ source: 'tasks', match: (name) => name === 'create_task' || name === 'update_task' || name === 'delete_task' || name === 'list_tasks' || name.includes('widget') },
|
|
@@ -45,6 +45,7 @@ function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '
|
|
|
45
45
|
'web_search',
|
|
46
46
|
'http_request',
|
|
47
47
|
'read_file',
|
|
48
|
+
'read_files',
|
|
48
49
|
'search_files',
|
|
49
50
|
'list_directory',
|
|
50
51
|
'code_navigate',
|
|
@@ -63,6 +64,7 @@ function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '
|
|
|
63
64
|
'execute_command',
|
|
64
65
|
'write_file',
|
|
65
66
|
'edit_file',
|
|
67
|
+
'replace_file_range',
|
|
66
68
|
'send_interim_update',
|
|
67
69
|
'send_message',
|
|
68
70
|
'make_call',
|
|
@@ -91,6 +91,35 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
|
|
|
91
91
|
});
|
|
92
92
|
break;
|
|
93
93
|
|
|
94
|
+
case 'read_files':
|
|
95
|
+
envelope = trimObject({
|
|
96
|
+
tool: toolName,
|
|
97
|
+
count: toolResult?.count || 0,
|
|
98
|
+
truncated: toolResult?.truncated || false,
|
|
99
|
+
results: (toolResult?.results || []).slice(0, 6).map((item) => trimObject({
|
|
100
|
+
path: item.path || item.requestedPath,
|
|
101
|
+
requestedPath: item.requestedPath,
|
|
102
|
+
rangeShown: item.rangeShown,
|
|
103
|
+
error: item.error,
|
|
104
|
+
content: lineExcerpt(item.content || '', 10, Math.floor(softLimit * 0.22))
|
|
105
|
+
}))
|
|
106
|
+
});
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'replace_file_range':
|
|
110
|
+
envelope = trimObject({
|
|
111
|
+
tool: toolName,
|
|
112
|
+
status: toolResult?.success === false || toolResult?.error ? 'error' : 'ok',
|
|
113
|
+
path: toolResult?.path || toolArgs.path,
|
|
114
|
+
startLine: toolResult?.startLine || toolArgs.start_line,
|
|
115
|
+
endLine: toolResult?.endLine || toolArgs.end_line,
|
|
116
|
+
replacedLines: toolResult?.replacedLines,
|
|
117
|
+
insertedLines: toolResult?.insertedLines,
|
|
118
|
+
totalLines: toolResult?.totalLines,
|
|
119
|
+
error: toolResult?.error
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
|
|
94
123
|
case 'search_files':
|
|
95
124
|
envelope = trimObject({
|
|
96
125
|
tool: toolName,
|
|
@@ -15,6 +15,22 @@ const ALWAYS_INCLUDE_BUILT_INS = [
|
|
|
15
15
|
'send_message',
|
|
16
16
|
'send_interim_update',
|
|
17
17
|
];
|
|
18
|
+
const CORE_FILE_TOOLS = [
|
|
19
|
+
'read_file',
|
|
20
|
+
'read_files',
|
|
21
|
+
'list_directory',
|
|
22
|
+
'search_files',
|
|
23
|
+
'edit_file',
|
|
24
|
+
'replace_file_range',
|
|
25
|
+
'write_file',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function requiredToolNames(options = {}) {
|
|
29
|
+
const requiredNames = [...ALWAYS_INCLUDE_BUILT_INS];
|
|
30
|
+
if (options.widgetId) requiredNames.push('save_widget_snapshot');
|
|
31
|
+
if (options.includeCoreFileTools) requiredNames.push(...CORE_FILE_TOOLS);
|
|
32
|
+
return requiredNames;
|
|
33
|
+
}
|
|
18
34
|
|
|
19
35
|
function compactDescription(value, maxChars = 180) {
|
|
20
36
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
@@ -37,8 +53,7 @@ function buildToolCatalog(tools = []) {
|
|
|
37
53
|
|
|
38
54
|
function ensureRequiredTools(selectedTools = [], builtInTools = [], options = {}) {
|
|
39
55
|
const limit = Number(options.maxTools) || MAX_TOOLS;
|
|
40
|
-
const requiredNames =
|
|
41
|
-
if (options.widgetId) requiredNames.push('save_widget_snapshot');
|
|
56
|
+
const requiredNames = requiredToolNames(options);
|
|
42
57
|
if (!requiredNames.length) return selectedTools;
|
|
43
58
|
|
|
44
59
|
const selected = Array.isArray(selectedTools) ? [...selectedTools] : [];
|
|
@@ -83,6 +98,7 @@ function selectInitialTools(allTools = [], suggestedNames = [], options = {}) {
|
|
|
83
98
|
|
|
84
99
|
function activateTools(currentTools = [], allTools = [], requestedNames = [], options = {}) {
|
|
85
100
|
const knownByName = new Map(allTools.map((tool) => [tool?.name, tool]));
|
|
101
|
+
const requiredNames = requiredToolNames(options);
|
|
86
102
|
let next = ensureRequiredTools(currentTools, allTools, options);
|
|
87
103
|
const activated = [];
|
|
88
104
|
const evicted = [];
|
|
@@ -102,7 +118,7 @@ function activateTools(currentTools = [], allTools = [], requestedNames = [], op
|
|
|
102
118
|
if (next.some((item) => item?.name === name)) continue;
|
|
103
119
|
if (next.length >= MAX_TOOLS) {
|
|
104
120
|
const replaceIndex = next.findIndex((item) => (
|
|
105
|
-
!
|
|
121
|
+
!requiredNames.includes(item?.name)
|
|
106
122
|
&& !requested.includes(item?.name)
|
|
107
123
|
));
|
|
108
124
|
if (replaceIndex === -1) {
|
|
@@ -136,6 +152,7 @@ function selectToolsForTask(task, builtInTools = [], mcpTools = [], _options = {
|
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
module.exports = {
|
|
155
|
+
CORE_FILE_TOOLS,
|
|
139
156
|
MAX_TOOLS,
|
|
140
157
|
activateTools,
|
|
141
158
|
buildToolCatalog,
|
|
@@ -896,6 +896,34 @@ function getAvailableTools(app, options = {}) {
|
|
|
896
896
|
required: []
|
|
897
897
|
}
|
|
898
898
|
},
|
|
899
|
+
{
|
|
900
|
+
name: 'read_files',
|
|
901
|
+
description: 'Read multiple workspace files or line ranges in one call. Prefer this over several read_file, cat, or sed commands when inspecting related files. This cannot read VM /tmp files created by execute_command.',
|
|
902
|
+
parameters: {
|
|
903
|
+
type: 'object',
|
|
904
|
+
properties: {
|
|
905
|
+
files: {
|
|
906
|
+
type: 'array',
|
|
907
|
+
description: 'Files to read. Each item accepts path or file_path plus optional start_line/end_line or line_start/line_count.',
|
|
908
|
+
items: {
|
|
909
|
+
type: 'object',
|
|
910
|
+
properties: {
|
|
911
|
+
path: { type: 'string', description: 'Absolute or relative path inside the per-user workspace' },
|
|
912
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
913
|
+
start_line: { type: 'number', description: 'Starting line number (1-indexed, inclusive)' },
|
|
914
|
+
end_line: { type: 'number', description: 'Ending line number (1-indexed, inclusive)' },
|
|
915
|
+
line_start: { type: 'number', description: 'Alias for start_line' },
|
|
916
|
+
line_count: { type: 'number', description: 'Number of lines to read from start_line/line_start' },
|
|
917
|
+
encoding: { type: 'string', description: 'File encoding (default utf-8)' }
|
|
918
|
+
},
|
|
919
|
+
required: []
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
encoding: { type: 'string', description: 'Default file encoding for items without encoding (default utf-8)' }
|
|
923
|
+
},
|
|
924
|
+
required: ['files']
|
|
925
|
+
}
|
|
926
|
+
},
|
|
899
927
|
{
|
|
900
928
|
name: 'write_file',
|
|
901
929
|
description: 'Write or append content to a file. Creates parent directories if they do not exist. IMPORTANT: When writing markdown or code, ensure proper formatting and avoid truncating or overly summarizing content. Write complete, well-formatted, detailed files.',
|
|
@@ -932,6 +960,20 @@ function getAvailableTools(app, options = {}) {
|
|
|
932
960
|
required: ['path', 'edits']
|
|
933
961
|
}
|
|
934
962
|
},
|
|
963
|
+
{
|
|
964
|
+
name: 'replace_file_range',
|
|
965
|
+
description: 'Replace a 1-indexed inclusive line range in a workspace file. Use this when you know line numbers and exact-text edit_file would be brittle. Read the target range first, then replace only the intended lines.',
|
|
966
|
+
parameters: {
|
|
967
|
+
type: 'object',
|
|
968
|
+
properties: {
|
|
969
|
+
path: { type: 'string', description: 'File path inside the per-user workspace' },
|
|
970
|
+
start_line: { type: 'number', description: 'Starting line number (1-indexed, inclusive)' },
|
|
971
|
+
end_line: { type: 'number', description: 'Ending line number (1-indexed, inclusive)' },
|
|
972
|
+
content: { type: 'string', description: 'Replacement content for the range. Empty string deletes the range.' }
|
|
973
|
+
},
|
|
974
|
+
required: ['path', 'start_line', 'end_line', 'content']
|
|
975
|
+
}
|
|
976
|
+
},
|
|
935
977
|
{
|
|
936
978
|
name: 'list_directory',
|
|
937
979
|
description: 'List files and directories with metadata (size, modified time).',
|
|
@@ -2339,6 +2381,49 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2339
2381
|
}
|
|
2340
2382
|
}
|
|
2341
2383
|
|
|
2384
|
+
case 'read_files': {
|
|
2385
|
+
try {
|
|
2386
|
+
const workspace = wc();
|
|
2387
|
+
if (!workspace) return { error: 'Workspace service is unavailable.' };
|
|
2388
|
+
const entries = Array.isArray(args.files) ? args.files : [];
|
|
2389
|
+
if (!entries.length) {
|
|
2390
|
+
return { error: 'read_files requires a non-empty files array.' };
|
|
2391
|
+
}
|
|
2392
|
+
const maxFiles = 8;
|
|
2393
|
+
const results = entries.slice(0, maxFiles).map((entry, index) => {
|
|
2394
|
+
const fileArgs = typeof entry === 'string'
|
|
2395
|
+
? { path: entry }
|
|
2396
|
+
: (entry && typeof entry === 'object' ? entry : {});
|
|
2397
|
+
const normalizedArgs = normalizeReadFileArgs({
|
|
2398
|
+
...fileArgs,
|
|
2399
|
+
encoding: fileArgs.encoding || args.encoding || 'utf-8',
|
|
2400
|
+
});
|
|
2401
|
+
if (!normalizedArgs.path) {
|
|
2402
|
+
return {
|
|
2403
|
+
index,
|
|
2404
|
+
success: false,
|
|
2405
|
+
error: 'Each read_files item requires path or file_path.',
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
const result = workspace.readFile(userId, normalizedArgs);
|
|
2409
|
+
return {
|
|
2410
|
+
index,
|
|
2411
|
+
requestedPath: normalizedArgs.path,
|
|
2412
|
+
success: !result?.error,
|
|
2413
|
+
...result,
|
|
2414
|
+
};
|
|
2415
|
+
});
|
|
2416
|
+
return {
|
|
2417
|
+
success: results.every((result) => result.success !== false && !result.error),
|
|
2418
|
+
count: results.length,
|
|
2419
|
+
truncated: entries.length > maxFiles,
|
|
2420
|
+
results,
|
|
2421
|
+
};
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
return { error: err.message };
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2342
2427
|
case 'write_file': {
|
|
2343
2428
|
try {
|
|
2344
2429
|
const workspace = wc();
|
|
@@ -2366,6 +2451,24 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2366
2451
|
}
|
|
2367
2452
|
}
|
|
2368
2453
|
|
|
2454
|
+
case 'replace_file_range': {
|
|
2455
|
+
try {
|
|
2456
|
+
const workspace = wc();
|
|
2457
|
+
if (!workspace) return { error: 'Workspace service is unavailable.' };
|
|
2458
|
+
if (typeof workspace.replaceFileRange !== 'function') {
|
|
2459
|
+
return { error: 'Workspace service does not support replace_file_range.' };
|
|
2460
|
+
}
|
|
2461
|
+
return workspace.replaceFileRange(userId, {
|
|
2462
|
+
path: args.path,
|
|
2463
|
+
start_line: args.start_line ?? args.startLine,
|
|
2464
|
+
end_line: args.end_line ?? args.endLine,
|
|
2465
|
+
content: args.content,
|
|
2466
|
+
});
|
|
2467
|
+
} catch (err) {
|
|
2468
|
+
return { error: err.message };
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2369
2472
|
case 'list_directory': {
|
|
2370
2473
|
try {
|
|
2371
2474
|
const workspace = wc();
|
|
@@ -294,6 +294,61 @@ class WorkspaceManager {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
replaceFileRange(userId, options = {}) {
|
|
298
|
+
let filePath;
|
|
299
|
+
try {
|
|
300
|
+
filePath = this.resolvePath(userId, options.path || '', 'path');
|
|
301
|
+
if (!fs.existsSync(filePath)) {
|
|
302
|
+
return { success: false, error: `File not found: ${filePath}`, path: filePath };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const start = Number(options.start_line ?? options.startLine);
|
|
306
|
+
const end = Number(options.end_line ?? options.endLine ?? start);
|
|
307
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
308
|
+
return { success: false, error: 'start_line must be a positive integer', path: filePath };
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isInteger(end) || end < 1) {
|
|
311
|
+
return { success: false, error: 'end_line must be a positive integer', path: filePath };
|
|
312
|
+
}
|
|
313
|
+
if (start > end) {
|
|
314
|
+
return { success: false, error: 'start_line must be less than or equal to end_line', path: filePath };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const original = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
318
|
+
const hadFinalNewline = original.endsWith('\n');
|
|
319
|
+
const lines = original.split('\n');
|
|
320
|
+
if (hadFinalNewline) lines.pop();
|
|
321
|
+
const totalLines = Math.max(lines.length, 1);
|
|
322
|
+
if (start > totalLines || end > totalLines) {
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
error: `line range ${start}-${end} is outside file with ${totalLines} lines`,
|
|
326
|
+
path: filePath,
|
|
327
|
+
totalLines,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const replacementText = String(options.content ?? '').replace(/\r\n/g, '\n');
|
|
332
|
+
const replacementLines = replacementText === ''
|
|
333
|
+
? []
|
|
334
|
+
: replacementText.replace(/\n$/, '').split('\n');
|
|
335
|
+
lines.splice(start - 1, end - start + 1, ...replacementLines);
|
|
336
|
+
const next = `${lines.join('\n')}${hadFinalNewline ? '\n' : ''}`;
|
|
337
|
+
fs.writeFileSync(filePath, next, 'utf8');
|
|
338
|
+
return {
|
|
339
|
+
success: true,
|
|
340
|
+
path: filePath,
|
|
341
|
+
startLine: start,
|
|
342
|
+
endLine: end,
|
|
343
|
+
replacedLines: end - start + 1,
|
|
344
|
+
insertedLines: replacementLines.length,
|
|
345
|
+
totalLines: lines.length,
|
|
346
|
+
};
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return { success: false, path: filePath || null, error: err.message };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
297
352
|
listDirectory(userId, options = {}) {
|
|
298
353
|
const dirPath = this.resolvePath(userId, options.path || '.', 'path');
|
|
299
354
|
const depthValue = options.depth != null ? Number(options.depth) : (options.recursive ? 3 : 1);
|