@qwen-code/arena-agent 0.0.10-dev.3 → 0.0.10-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/code-tools.d.ts +0 -1
- package/dist/code-tools.js +121 -49
- package/dist/code-tools.js.map +1 -1
- package/dist/file-edit.d.ts +6 -0
- package/dist/file-edit.js +58 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/tools.js +129 -76
- package/dist/tools.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function fuzzyMatchRanges(source, context) {
|
|
2
|
+
const trimmed = context.trim();
|
|
3
|
+
if (!trimmed)
|
|
4
|
+
return [];
|
|
5
|
+
const escapedTokens = trimmed
|
|
6
|
+
.split(/\s+/)
|
|
7
|
+
.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
8
|
+
const matcher = new RegExp(escapedTokens.join("\\s+"), "gu");
|
|
9
|
+
const matches = [...source.matchAll(matcher)];
|
|
10
|
+
const ranges = [];
|
|
11
|
+
for (const match of matches) {
|
|
12
|
+
if (match.index === undefined)
|
|
13
|
+
continue;
|
|
14
|
+
let start = match.index;
|
|
15
|
+
let end = start + match[0].length;
|
|
16
|
+
if (/^\s/.test(context)) {
|
|
17
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
18
|
+
// Production tolerates indentation drift, but does not let a multiline
|
|
19
|
+
// context that starts with whitespace match halfway through a source line.
|
|
20
|
+
if (source.slice(lineStart, start).trim())
|
|
21
|
+
continue;
|
|
22
|
+
start = lineStart;
|
|
23
|
+
}
|
|
24
|
+
if (/\s$/.test(context)) {
|
|
25
|
+
while (end < source.length && /[ \t]/.test(source[end]))
|
|
26
|
+
end += 1;
|
|
27
|
+
}
|
|
28
|
+
ranges.push({ start, end });
|
|
29
|
+
}
|
|
30
|
+
return ranges;
|
|
31
|
+
}
|
|
32
|
+
function replaceText(source, context, replacement, requireUnique, options) {
|
|
33
|
+
const first = source.indexOf(context);
|
|
34
|
+
if (first !== -1) {
|
|
35
|
+
const second = source.indexOf(context, first + context.length);
|
|
36
|
+
if (requireUnique && second !== -1)
|
|
37
|
+
return undefined;
|
|
38
|
+
return (source.slice(0, first) +
|
|
39
|
+
replacement +
|
|
40
|
+
source.slice(first + context.length));
|
|
41
|
+
}
|
|
42
|
+
if (!options.whitespaceInsensitiveMultiline || !context.includes("\n")) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const matches = fuzzyMatchRanges(source, context);
|
|
46
|
+
if (matches.length === 0 || (requireUnique && matches.length !== 1)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const match = matches[0];
|
|
50
|
+
return source.slice(0, match.start) + replacement + source.slice(match.end);
|
|
51
|
+
}
|
|
52
|
+
export function replaceFirstText(source, context, replacement, options = {}) {
|
|
53
|
+
return replaceText(source, context, replacement, false, options);
|
|
54
|
+
}
|
|
55
|
+
export function replaceUniqueText(source, context, replacement, options = {}) {
|
|
56
|
+
return replaceText(source, context, replacement, true, options);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=file-edit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-edit.js","sourceRoot":"","sources":["../src/file-edit.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, realpathSync
|
|
3
|
-
import { open, readFile, realpath, stat, writeFile
|
|
2
|
+
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { open, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Ajv } from "ajv";
|
|
6
6
|
import addFormats from "ajv-formats";
|
|
@@ -13,12 +13,13 @@ import { CodeToolExecutor } from "./code-tools.js";
|
|
|
13
13
|
import { supportsImageInput } from "./config.js";
|
|
14
14
|
import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
|
|
15
15
|
import { FetchPageReader } from "./fetch-page.js";
|
|
16
|
+
import { replaceFirstText } from "./file-edit.js";
|
|
16
17
|
import { generatedImageBytesForExtension, prepareImageForModel, } from "./image.js";
|
|
17
18
|
import { imageTextFromFallbackModel } from "./image-input-fallback.js";
|
|
18
19
|
import { isCodeArenaMode } from "./mode.js";
|
|
19
20
|
import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
|
|
20
21
|
import { processManagerForWorkspace, } from "./process-manager.js";
|
|
21
|
-
import { HttpModelClient
|
|
22
|
+
import { HttpModelClient } from "./provider.js";
|
|
22
23
|
import { RoutifyFetchPageReader, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
23
24
|
import { toolDefinitions } from "./spec.js";
|
|
24
25
|
import { voiceRegistryForWorkspace, } from "./voice-registry.js";
|
|
@@ -214,7 +215,7 @@ const visualIntroText = "<arena-system-message>\n" +
|
|
|
214
215
|
"treat them as the authentic output of the tool call.\n" +
|
|
215
216
|
"</arena-system-message>";
|
|
216
217
|
const bashOutputLimit = 20_000;
|
|
217
|
-
const
|
|
218
|
+
const textFileOutputByteLimit = 256 * 1024;
|
|
218
219
|
const toolResultCharacterLimit = 1_000_000;
|
|
219
220
|
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
220
221
|
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
@@ -228,6 +229,24 @@ function capBashOutput(value) {
|
|
|
228
229
|
truncated: true,
|
|
229
230
|
};
|
|
230
231
|
}
|
|
232
|
+
function readTextResult(data) {
|
|
233
|
+
if (data.length <= textFileOutputByteLimit) {
|
|
234
|
+
return { content: decodeText(data), truncated: false };
|
|
235
|
+
}
|
|
236
|
+
const half = textFileOutputByteLimit / 2;
|
|
237
|
+
const prefixBreak = data.lastIndexOf(0x0a, half);
|
|
238
|
+
const suffixBreak = data.indexOf(0x0a, data.length - half);
|
|
239
|
+
const prefixEnd = prefixBreak === -1 ? half : prefixBreak;
|
|
240
|
+
const suffixStart = suffixBreak === -1 ? data.length - half : suffixBreak + 1;
|
|
241
|
+
const elidedBytes = Math.max(0, suffixStart - prefixEnd);
|
|
242
|
+
const marker = `\n\n[${Math.round(elidedBytes / 1024)} KB elided from the middle of the file]\n\n`;
|
|
243
|
+
return {
|
|
244
|
+
content: decodeText(data.subarray(0, prefixEnd)) +
|
|
245
|
+
marker +
|
|
246
|
+
decodeText(data.subarray(suffixStart)),
|
|
247
|
+
truncated: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
231
250
|
const mimeTypes = {
|
|
232
251
|
".aac": "audio/aac",
|
|
233
252
|
".bmp": "image/bmp",
|
|
@@ -281,8 +300,7 @@ function decodeUtf32(data, littleEndian) {
|
|
|
281
300
|
const codePoint = littleEndian
|
|
282
301
|
? data.readUInt32LE(index)
|
|
283
302
|
: data.readUInt32BE(index);
|
|
284
|
-
characters.push(codePoint <= 0x10ffff &&
|
|
285
|
-
!(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
303
|
+
characters.push(codePoint <= 0x10ffff && !(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
286
304
|
? String.fromCodePoint(codePoint)
|
|
287
305
|
: "\ufffd");
|
|
288
306
|
}
|
|
@@ -560,12 +578,15 @@ class ArenaToolExecutor {
|
|
|
560
578
|
config;
|
|
561
579
|
workspace;
|
|
562
580
|
workspaceReal;
|
|
581
|
+
tmpReal;
|
|
582
|
+
varTmpReal;
|
|
563
583
|
validators = new Map();
|
|
564
584
|
pendingVisualContent = new Map();
|
|
565
585
|
askUserHandler;
|
|
566
586
|
imageInputFallbackClient;
|
|
567
587
|
signal;
|
|
568
588
|
workspaceContext;
|
|
589
|
+
nativeSandboxPaths;
|
|
569
590
|
pageReader;
|
|
570
591
|
routifyPageReader;
|
|
571
592
|
processManager;
|
|
@@ -587,6 +608,13 @@ class ArenaToolExecutor {
|
|
|
587
608
|
this.workspace = path.resolve(config.workspace);
|
|
588
609
|
mkdirSync(this.workspace, { recursive: true });
|
|
589
610
|
this.workspaceReal = realpathSync(this.workspace);
|
|
611
|
+
// A Linux /home/user workspace is already an OS-level sandbox. Other
|
|
612
|
+
// callers still confine ordinary workspace paths, while /tmp and /var/tmp
|
|
613
|
+
// always refer to the actual system directories seen by Bash.
|
|
614
|
+
this.nativeSandboxPaths =
|
|
615
|
+
process.platform === "linux" && this.workspaceReal === "/home/user";
|
|
616
|
+
this.tmpReal = realpathSync("/tmp");
|
|
617
|
+
this.varTmpReal = realpathSync("/var/tmp");
|
|
590
618
|
this.processManager = processManagerForWorkspace(this.workspaceReal);
|
|
591
619
|
this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
|
|
592
620
|
this.askUserHandler = options.askUserHandler;
|
|
@@ -747,10 +775,9 @@ class ArenaToolExecutor {
|
|
|
747
775
|
const message = errorMessage(error);
|
|
748
776
|
const durationMs = Date.now() - startedAt;
|
|
749
777
|
if (name === "bash") {
|
|
750
|
-
const stderr = this.virtualizeBashOutput(message);
|
|
751
778
|
return {
|
|
752
779
|
status: "shell_error",
|
|
753
|
-
stderr,
|
|
780
|
+
stderr: message,
|
|
754
781
|
stdout: "",
|
|
755
782
|
exit_code: null,
|
|
756
783
|
duration_ms: durationMs,
|
|
@@ -892,28 +919,57 @@ class ArenaToolExecutor {
|
|
|
892
919
|
if (!raw)
|
|
893
920
|
throw new Error("path must not be empty");
|
|
894
921
|
let candidate;
|
|
895
|
-
|
|
896
|
-
|
|
922
|
+
let confinementRoot = this.workspace;
|
|
923
|
+
let confinementRootReal = this.workspaceReal;
|
|
924
|
+
const expanded = raw === "~"
|
|
925
|
+
? "/home/user"
|
|
926
|
+
: raw.startsWith("~/")
|
|
927
|
+
? `/home/user/${raw.slice(2)}`
|
|
928
|
+
: raw;
|
|
929
|
+
const virtualAbsolute = path.posix.resolve("/home/user", expanded);
|
|
930
|
+
let suffix;
|
|
931
|
+
if (virtualAbsolute === "/var/tmp" ||
|
|
932
|
+
virtualAbsolute.startsWith("/var/tmp/")) {
|
|
933
|
+
confinementRoot = "/var/tmp";
|
|
934
|
+
confinementRootReal = this.varTmpReal;
|
|
935
|
+
suffix = virtualAbsolute.slice("/var/tmp".length).replace(/^\/+/, "");
|
|
936
|
+
candidate = path.join(confinementRoot, suffix);
|
|
897
937
|
}
|
|
898
|
-
else if (
|
|
899
|
-
|
|
938
|
+
else if (virtualAbsolute === "/tmp" ||
|
|
939
|
+
virtualAbsolute.startsWith("/tmp/")) {
|
|
940
|
+
confinementRoot = "/tmp";
|
|
941
|
+
confinementRootReal = this.tmpReal;
|
|
942
|
+
suffix = virtualAbsolute.slice("/tmp".length).replace(/^\/+/, "");
|
|
943
|
+
candidate = path.join(confinementRoot, suffix);
|
|
900
944
|
}
|
|
901
|
-
else if (
|
|
945
|
+
else if (this.nativeSandboxPaths && path.posix.isAbsolute(raw)) {
|
|
946
|
+
confinementRoot = "/";
|
|
947
|
+
confinementRootReal = "/";
|
|
948
|
+
candidate = raw;
|
|
949
|
+
}
|
|
950
|
+
else if (!this.workspaceContext) {
|
|
951
|
+
if (virtualAbsolute === "/home/user" ||
|
|
952
|
+
virtualAbsolute.startsWith("/home/user/")) {
|
|
953
|
+
suffix = virtualAbsolute.slice("/home/user".length).replace(/^\/+/, "");
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
throw new Error(`path is outside the workspace: ${raw}`);
|
|
957
|
+
}
|
|
958
|
+
candidate = path.join(confinementRoot, suffix);
|
|
959
|
+
}
|
|
960
|
+
else if (raw === "/home/user") {
|
|
902
961
|
candidate = this.workspace;
|
|
903
962
|
}
|
|
904
|
-
else if (
|
|
905
|
-
candidate = path.join(this.workspace, raw.slice(
|
|
963
|
+
else if (raw.startsWith("/home/user/")) {
|
|
964
|
+
candidate = path.join(this.workspace, raw.slice("/home/user/".length));
|
|
906
965
|
}
|
|
907
966
|
else {
|
|
908
|
-
const defaultCwd = this.workspaceContext
|
|
909
|
-
? path.resolve(this.workspaceContext.cwd)
|
|
910
|
-
: this.workspace;
|
|
967
|
+
const defaultCwd = path.resolve(this.workspaceContext.cwd);
|
|
911
968
|
candidate = path.isAbsolute(raw) ? raw : path.join(defaultCwd, raw);
|
|
912
969
|
}
|
|
913
970
|
const absolute = path.resolve(candidate);
|
|
914
|
-
const lexicalRelative = path.relative(
|
|
915
|
-
if (lexicalRelative.startsWith("..") ||
|
|
916
|
-
path.isAbsolute(lexicalRelative)) {
|
|
971
|
+
const lexicalRelative = path.relative(confinementRoot, absolute);
|
|
972
|
+
if (lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
917
973
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
918
974
|
}
|
|
919
975
|
let ancestor = absolute;
|
|
@@ -925,26 +981,36 @@ class ArenaToolExecutor {
|
|
|
925
981
|
}
|
|
926
982
|
const ancestorReal = await realpath(ancestor);
|
|
927
983
|
const resolved = path.resolve(ancestorReal, path.relative(ancestor, absolute));
|
|
928
|
-
const realRelative = path.relative(
|
|
984
|
+
const realRelative = path.relative(confinementRootReal, resolved);
|
|
929
985
|
if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) {
|
|
930
986
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
931
987
|
}
|
|
932
988
|
return resolved;
|
|
933
989
|
}
|
|
934
990
|
relativePath(target) {
|
|
991
|
+
for (const [root, virtualRoot] of [
|
|
992
|
+
[this.varTmpReal, "/var/tmp"],
|
|
993
|
+
[this.tmpReal, "/tmp"],
|
|
994
|
+
]) {
|
|
995
|
+
const relative = path.relative(root, target);
|
|
996
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
997
|
+
const suffix = relative.split(path.sep).join("/");
|
|
998
|
+
return suffix ? `${virtualRoot}/${suffix}` : virtualRoot;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
935
1001
|
return path.relative(this.workspaceReal, target).split(path.sep).join("/");
|
|
936
1002
|
}
|
|
937
|
-
|
|
938
|
-
if (this.workspaceContext)
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1003
|
+
displayWorkspacePath(rawPath) {
|
|
1004
|
+
if (!this.workspaceContext) {
|
|
1005
|
+
if (rawPath === "/home/user" || rawPath === "~")
|
|
1006
|
+
return ".";
|
|
1007
|
+
if (rawPath.startsWith("/home/user/")) {
|
|
1008
|
+
return rawPath.slice("/home/user/".length);
|
|
1009
|
+
}
|
|
1010
|
+
if (rawPath.startsWith("~/"))
|
|
1011
|
+
return rawPath.slice(2);
|
|
946
1012
|
}
|
|
947
|
-
return
|
|
1013
|
+
return rawPath;
|
|
948
1014
|
}
|
|
949
1015
|
sandboxEnvironment() {
|
|
950
1016
|
const env = {
|
|
@@ -962,17 +1028,12 @@ class ArenaToolExecutor {
|
|
|
962
1028
|
if (!(await stat(workdir)).isDirectory()) {
|
|
963
1029
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
964
1030
|
}
|
|
965
|
-
// Legacy callers without runtime context retain the captured /home/user
|
|
966
|
-
// contract. Runtime-aware callers execute and report their actual paths.
|
|
967
|
-
const translatedCommand = this.workspaceContext
|
|
968
|
-
? command
|
|
969
|
-
: command.replaceAll("/home/user", "\"$ARENA_WORKSPACE\"");
|
|
970
1031
|
return new Promise((resolve, reject) => {
|
|
971
1032
|
const env = this.sandboxEnvironment();
|
|
972
1033
|
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
973
1034
|
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
974
1035
|
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
975
|
-
const child = spawn("/bin/bash", ["-lc",
|
|
1036
|
+
const child = spawn("/bin/bash", ["-lc", command], {
|
|
976
1037
|
cwd: workdir,
|
|
977
1038
|
env,
|
|
978
1039
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1017,8 +1078,8 @@ class ArenaToolExecutor {
|
|
|
1017
1078
|
reject(this.signal.reason || new Error("operation cancelled"));
|
|
1018
1079
|
return;
|
|
1019
1080
|
}
|
|
1020
|
-
const out = capBashOutput(
|
|
1021
|
-
const err = capBashOutput(
|
|
1081
|
+
const out = capBashOutput(stdout);
|
|
1082
|
+
const err = capBashOutput(stderr);
|
|
1022
1083
|
resolve({
|
|
1023
1084
|
status: timedOut ? "timeout" : signal ? "killed" : "completed",
|
|
1024
1085
|
stderr: err.text,
|
|
@@ -1071,12 +1132,9 @@ class ArenaToolExecutor {
|
|
|
1071
1132
|
if (!(await stat(workdir)).isDirectory()) {
|
|
1072
1133
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
1073
1134
|
}
|
|
1074
|
-
const translatedCommand = this.workspaceContext
|
|
1075
|
-
? command
|
|
1076
|
-
: command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
|
|
1077
1135
|
return this.processManager.start({
|
|
1078
1136
|
name,
|
|
1079
|
-
command
|
|
1137
|
+
command,
|
|
1080
1138
|
cwd: workdir,
|
|
1081
1139
|
startupWaitSeconds,
|
|
1082
1140
|
env: this.sandboxEnvironment(),
|
|
@@ -1085,10 +1143,12 @@ class ArenaToolExecutor {
|
|
|
1085
1143
|
}
|
|
1086
1144
|
async askUser(questions, toolCallId, context) {
|
|
1087
1145
|
if (this.askUserHandler) {
|
|
1088
|
-
return this.askUserHandler(toolCallId, questions, context
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1146
|
+
return this.askUserHandler(toolCallId, questions, context
|
|
1147
|
+
? {
|
|
1148
|
+
...context,
|
|
1149
|
+
signal: this.signal || context.signal,
|
|
1150
|
+
}
|
|
1151
|
+
: undefined);
|
|
1092
1152
|
}
|
|
1093
1153
|
// ask_user is not offered in non-interactive sessions; if the model still
|
|
1094
1154
|
// emits it (e.g. resumed history or a hallucinated call), report an error
|
|
@@ -1168,23 +1228,25 @@ class ArenaToolExecutor {
|
|
|
1168
1228
|
catch {
|
|
1169
1229
|
return {
|
|
1170
1230
|
status: "error",
|
|
1171
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1231
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1172
1232
|
};
|
|
1173
1233
|
}
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (!source.includes(oldText)) {
|
|
1234
|
+
// Agent Arena replaces the first match and tolerates multiline whitespace
|
|
1235
|
+
// drift. Code Arena deliberately uses a separate unique-match policy.
|
|
1236
|
+
const edited = replaceFirstText(source, oldText, newText, {
|
|
1237
|
+
whitespaceInsensitiveMultiline: true,
|
|
1238
|
+
});
|
|
1239
|
+
if (edited === undefined) {
|
|
1181
1240
|
return {
|
|
1182
1241
|
status: "error",
|
|
1183
1242
|
message: "Context not found. Read the file to verify the text exists.",
|
|
1184
1243
|
};
|
|
1185
1244
|
}
|
|
1186
1245
|
try {
|
|
1187
|
-
|
|
1246
|
+
// Replacement text is literal. String.replace(string, string) expands
|
|
1247
|
+
// $&, $`, $' and $$ sequences and therefore corrupts JavaScript/jQuery
|
|
1248
|
+
// snippets observed in production trajectories.
|
|
1249
|
+
await writeFile(target, edited, "utf8");
|
|
1188
1250
|
}
|
|
1189
1251
|
catch {
|
|
1190
1252
|
return {
|
|
@@ -1195,10 +1257,11 @@ class ArenaToolExecutor {
|
|
|
1195
1257
|
return { status: "success", message: `Edited ${rawPath}.` };
|
|
1196
1258
|
}
|
|
1197
1259
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
1198
|
-
|
|
1260
|
+
let target;
|
|
1199
1261
|
let fileSize;
|
|
1200
1262
|
let sample;
|
|
1201
1263
|
try {
|
|
1264
|
+
target = await this.confinedPath(rawPath);
|
|
1202
1265
|
const metadata = await stat(target);
|
|
1203
1266
|
if (!metadata.isFile())
|
|
1204
1267
|
throw new Error("not a regular file");
|
|
@@ -1215,23 +1278,13 @@ class ArenaToolExecutor {
|
|
|
1215
1278
|
}
|
|
1216
1279
|
}
|
|
1217
1280
|
catch {
|
|
1218
|
-
// 逐字对齐生产错误文案,路径按调用方原样回显。
|
|
1219
1281
|
return {
|
|
1220
1282
|
status: "error",
|
|
1221
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1283
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1222
1284
|
};
|
|
1223
1285
|
}
|
|
1224
1286
|
const extension = path.extname(target).toLowerCase();
|
|
1225
1287
|
const kind = workspaceFileKind(target, sample);
|
|
1226
|
-
if (kind === "text" && fileSize > textFileByteLimit) {
|
|
1227
|
-
return {
|
|
1228
|
-
status: "error",
|
|
1229
|
-
message: `File content (${displayByteSize(fileSize)}) exceeds the maximum allowed size ` +
|
|
1230
|
-
`(${displayByteSize(textFileByteLimit)}). Use bash with sed/head/tail to read ` +
|
|
1231
|
-
"specific portions of the file, or grep/rg to search for specific content " +
|
|
1232
|
-
"instead of reading the whole file.",
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
1288
|
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
1236
1289
|
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
1237
1290
|
}
|
|
@@ -1258,7 +1311,7 @@ class ArenaToolExecutor {
|
|
|
1258
1311
|
catch {
|
|
1259
1312
|
return {
|
|
1260
1313
|
status: "error",
|
|
1261
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1314
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1262
1315
|
};
|
|
1263
1316
|
}
|
|
1264
1317
|
if (isStructuredDocument(target)) {
|
|
@@ -1286,12 +1339,13 @@ class ArenaToolExecutor {
|
|
|
1286
1339
|
return result;
|
|
1287
1340
|
}
|
|
1288
1341
|
if (kind === "text") {
|
|
1289
|
-
const
|
|
1342
|
+
const text = readTextResult(data);
|
|
1290
1343
|
return {
|
|
1291
1344
|
kind: "text",
|
|
1292
|
-
lines:
|
|
1293
|
-
size:
|
|
1294
|
-
content,
|
|
1345
|
+
lines: data.reduce((count, byte) => count + (byte === 0x0a ? 1 : 0), 1),
|
|
1346
|
+
size: data.length,
|
|
1347
|
+
content: text.content,
|
|
1348
|
+
...(text.truncated ? { truncated: true } : {}),
|
|
1295
1349
|
};
|
|
1296
1350
|
}
|
|
1297
1351
|
const mimeType = mime.getType(target) ||
|
|
@@ -1341,9 +1395,8 @@ class ArenaToolExecutor {
|
|
|
1341
1395
|
};
|
|
1342
1396
|
}
|
|
1343
1397
|
async writeWorkspaceFile(rawPath, content) {
|
|
1344
|
-
let target;
|
|
1345
1398
|
try {
|
|
1346
|
-
target = await this.confinedPath(rawPath);
|
|
1399
|
+
const target = await this.confinedPath(rawPath);
|
|
1347
1400
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1348
1401
|
await writeFile(target, content, "utf8");
|
|
1349
1402
|
}
|