@qwen-code/arena-agent 0.0.10-dev.0 → 0.0.10-dev.10
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/.env.example +57 -14
- package/CHANGELOG.md +228 -0
- package/README.md +240 -57
- package/dist/agent.d.ts +30 -2
- package/dist/agent.js +351 -104
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.js +23 -22
- package/dist/anthropic.js.map +1 -1
- package/dist/ask-user.js +2 -0
- package/dist/ask-user.js.map +1 -1
- package/dist/attachments.js +4 -3
- package/dist/attachments.js.map +1 -1
- package/dist/brave-search.js +1 -3
- package/dist/brave-search.js.map +1 -1
- package/dist/canonical-data.js +2 -2
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +197 -34
- package/dist/cli.js.map +1 -1
- package/dist/code-tools.d.ts +8 -2
- package/dist/code-tools.js +709 -117
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.d.ts +49 -3
- package/dist/compaction.js +383 -170
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +17 -2
- package/dist/config.js +47 -5
- package/dist/config.js.map +1 -1
- package/dist/fetch-page.js +9 -33
- package/dist/fetch-page.js.map +1 -1
- package/dist/file-edit.d.ts +8 -0
- package/dist/file-edit.js +63 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/history.d.ts +35 -1
- package/dist/history.js +400 -16
- package/dist/history.js.map +1 -1
- package/dist/legacy-compaction.d.ts +20 -0
- package/dist/legacy-compaction.js +209 -0
- package/dist/legacy-compaction.js.map +1 -0
- package/dist/midturn-checkpoint-hook.d.ts +31 -0
- package/dist/midturn-checkpoint-hook.js +38 -0
- package/dist/midturn-checkpoint-hook.js.map +1 -0
- package/dist/mock.js +5 -0
- package/dist/mock.js.map +1 -1
- package/dist/mode.js +4 -2
- package/dist/mode.js.map +1 -1
- package/dist/model-router-media.d.ts +1 -0
- package/dist/model-router-media.js +71 -1
- package/dist/model-router-media.js.map +1 -1
- package/dist/openai-responses.js +2 -0
- package/dist/openai-responses.js.map +1 -1
- package/dist/process-manager.js +18 -11
- package/dist/process-manager.js.map +1 -1
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +6 -2
- package/dist/prompts.js.map +1 -1
- package/dist/protocol.d.ts +5 -1
- package/dist/protocol.js +24 -2
- package/dist/protocol.js.map +1 -1
- package/dist/provider.d.ts +12 -5
- package/dist/provider.js +60 -52
- package/dist/provider.js.map +1 -1
- package/dist/repetitive-output.d.ts +12 -0
- package/dist/repetitive-output.js +80 -0
- package/dist/repetitive-output.js.map +1 -0
- package/dist/replay-responses.d.ts +17 -0
- package/dist/replay-responses.js +100 -0
- package/dist/replay-responses.js.map +1 -0
- package/dist/routify-web-fetch.js +20 -7
- package/dist/routify-web-fetch.js.map +1 -1
- package/dist/sandbox-file-write.d.ts +10 -0
- package/dist/sandbox-file-write.js +58 -0
- package/dist/sandbox-file-write.js.map +1 -0
- package/dist/sdk.js +9 -1
- package/dist/sdk.js.map +1 -1
- package/dist/server.js +127 -44
- package/dist/server.js.map +1 -1
- package/dist/session-memory-prompts.d.ts +5 -0
- package/dist/session-memory-prompts.js +7 -0
- package/dist/session-memory-prompts.js.map +1 -0
- package/dist/snapshot-client.d.ts +10 -0
- package/dist/snapshot-client.js +39 -0
- package/dist/snapshot-client.js.map +1 -0
- package/dist/spec.js +18 -10
- package/dist/spec.js.map +1 -1
- package/dist/speech-audio.d.ts +9 -0
- package/dist/speech-audio.js +130 -0
- package/dist/speech-audio.js.map +1 -0
- package/dist/speech-model.d.ts +3 -0
- package/dist/speech-model.js +8 -0
- package/dist/speech-model.js.map +1 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +744 -121
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +20 -12
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +28 -3
- package/dist/voice-registry.d.ts +2 -0
- package/dist/voice-registry.js +10 -2
- package/dist/voice-registry.js.map +1 -1
- package/dist/web/app.js +17 -3
- package/dist/web/app.js.map +1 -1
- package/dist/web-fetch-output.d.ts +9 -0
- package/dist/web-fetch-output.js +38 -0
- package/dist/web-fetch-output.js.map +1 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +11 -6
- package/dist/workspace.js.map +1 -1
- package/docs/BINARY_DISTRIBUTION.md +27 -16
- package/docs/MODES_AND_RELEASES.md +28 -5
- package/docs/SDK.md +10 -4
- package/modes/code-arena/capture-provenance.json +57 -0
- package/modes/code-arena/full-stack/system-prompt.txt +1 -1
- package/modes/code-arena/full-stack/template/eslint.config.mjs +3 -4
- package/modes/code-arena/full-stack/template/postcss.config.mjs +2 -2
- package/modes/code-arena/full-stack/tools.json +186 -186
- package/modes/code-arena/html/system-prompt.txt +1 -1
- package/modes/code-arena/html/tools.json +123 -123
- package/modes/code-arena/react/system-prompt.txt +4 -3
- package/modes/code-arena/react/template/package.json +4 -4
- package/modes/code-arena/react/template/src/App.tsx +1 -1
- package/modes/code-arena/react/template/src/main.tsx +1 -1
- package/modes/code-arena/react/template/tsconfig.json +6 -0
- package/modes/code-arena/react/template/vite.config.ts +1 -0
- package/modes/code-arena/react/tools.json +178 -184
- package/package.json +15 -4
- package/scripts/README.md +62 -0
- package/scripts/check-changelog.mjs +66 -0
- package/vendor/audio/darwin-arm64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/darwin-arm64/LAME.LICENSE +481 -0
- package/vendor/audio/darwin-arm64/Opus.LICENSE +44 -0
- package/vendor/audio/darwin-arm64/ffmpeg.gz +0 -0
- package/vendor/audio/darwin-arm64/manifest.json +39 -0
- package/vendor/audio/darwin-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/darwin-x64/LAME.LICENSE +481 -0
- package/vendor/audio/darwin-x64/Opus.LICENSE +44 -0
- package/vendor/audio/darwin-x64/ffmpeg.gz +0 -0
- package/vendor/audio/darwin-x64/manifest.json +39 -0
- package/vendor/audio/linux-arm64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/linux-arm64/LAME.LICENSE +481 -0
- package/vendor/audio/linux-arm64/Opus.LICENSE +44 -0
- package/vendor/audio/linux-arm64/ffmpeg.gz +0 -0
- package/vendor/audio/linux-arm64/manifest.json +39 -0
- package/vendor/audio/linux-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/linux-x64/LAME.LICENSE +481 -0
- package/vendor/audio/linux-x64/Opus.LICENSE +44 -0
- package/vendor/audio/linux-x64/ffmpeg.gz +0 -0
- package/vendor/audio/linux-x64/manifest.json +39 -0
- package/vendor/audio/sources/build-audio-encoder.sh +76 -0
- package/vendor/audio/sources/ffmpeg-7.0.2.tar.xz +0 -0
- package/vendor/audio/sources/lame-3.100.tar.gz +0 -0
- package/vendor/audio/sources/opus-1.5.2.tar.gz +0 -0
- package/vendor/audio/sources/pkgconf-2.3.0.tar.xz +0 -0
- package/vendor/audio/sources/sources.lock.json +30 -0
- package/vendor/audio/win32-x64/FFmpeg.LICENSE +502 -0
- package/vendor/audio/win32-x64/LAME.LICENSE +481 -0
- package/vendor/audio/win32-x64/Opus.LICENSE +44 -0
- package/vendor/audio/win32-x64/ffmpeg.gz +0 -0
- package/vendor/audio/win32-x64/manifest.json +39 -0
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 { copyFileSync, 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,15 +13,30 @@ 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
22
|
import { HttpModelClient } from "./provider.js";
|
|
23
|
+
import { replayResponseFor, } from "./replay-responses.js";
|
|
22
24
|
import { RoutifyFetchPageReader, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
23
25
|
import { toolDefinitions } from "./spec.js";
|
|
26
|
+
import { createSandboxFile, privilegedFileCreationEnabled } from "./sandbox-file-write.js";
|
|
24
27
|
import { voiceRegistryForWorkspace, } from "./voice-registry.js";
|
|
28
|
+
export function imageInputFallbackConnection(config) {
|
|
29
|
+
if (!config.imageInputFallbackBaseUrl &&
|
|
30
|
+
!config.imageInputFallbackApiKey &&
|
|
31
|
+
!config.imageInputFallbackProtocol) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
baseUrl: config.imageInputFallbackBaseUrl,
|
|
36
|
+
apiKey: config.imageInputFallbackApiKey,
|
|
37
|
+
protocol: config.imageInputFallbackProtocol,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
25
40
|
const knownTextApplicationMimeTypes = new Set([
|
|
26
41
|
"application/ecmascript",
|
|
27
42
|
"application/javascript",
|
|
@@ -202,7 +217,7 @@ const visualIntroText = "<arena-system-message>\n" +
|
|
|
202
217
|
"treat them as the authentic output of the tool call.\n" +
|
|
203
218
|
"</arena-system-message>";
|
|
204
219
|
const bashOutputLimit = 20_000;
|
|
205
|
-
const
|
|
220
|
+
const textFileOutputByteLimit = 256 * 1024;
|
|
206
221
|
const toolResultCharacterLimit = 1_000_000;
|
|
207
222
|
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
208
223
|
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
@@ -216,6 +231,39 @@ function capBashOutput(value) {
|
|
|
216
231
|
truncated: true,
|
|
217
232
|
};
|
|
218
233
|
}
|
|
234
|
+
class BashOutputBuffer {
|
|
235
|
+
text = "";
|
|
236
|
+
truncated = false;
|
|
237
|
+
append(chunk) {
|
|
238
|
+
// Once capped, text still contains the original prefix and latest suffix.
|
|
239
|
+
// Capping again after an append therefore preserves the same head/tail as
|
|
240
|
+
// capping the full stream, without ever retaining the discarded middle.
|
|
241
|
+
const output = capBashOutput(this.text + chunk.toString("utf8"));
|
|
242
|
+
this.text = output.text;
|
|
243
|
+
this.truncated ||= output.truncated;
|
|
244
|
+
}
|
|
245
|
+
result() {
|
|
246
|
+
return { text: this.text, truncated: this.truncated };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function readTextResult(data) {
|
|
250
|
+
if (data.length <= textFileOutputByteLimit) {
|
|
251
|
+
return { content: decodeText(data), truncated: false };
|
|
252
|
+
}
|
|
253
|
+
const half = textFileOutputByteLimit / 2;
|
|
254
|
+
const prefixBreak = data.lastIndexOf(0x0a, half);
|
|
255
|
+
const suffixBreak = data.indexOf(0x0a, data.length - half);
|
|
256
|
+
const prefixEnd = prefixBreak === -1 ? half : prefixBreak;
|
|
257
|
+
const suffixStart = suffixBreak === -1 ? data.length - half : suffixBreak + 1;
|
|
258
|
+
const elidedBytes = Math.max(0, suffixStart - prefixEnd);
|
|
259
|
+
const marker = `\n\n[${Math.round(elidedBytes / 1024)} KB elided from the middle of the file]\n\n`;
|
|
260
|
+
return {
|
|
261
|
+
content: decodeText(data.subarray(0, prefixEnd)) +
|
|
262
|
+
marker +
|
|
263
|
+
decodeText(data.subarray(suffixStart)),
|
|
264
|
+
truncated: true,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
219
267
|
const mimeTypes = {
|
|
220
268
|
".aac": "audio/aac",
|
|
221
269
|
".bmp": "image/bmp",
|
|
@@ -269,8 +317,7 @@ function decodeUtf32(data, littleEndian) {
|
|
|
269
317
|
const codePoint = littleEndian
|
|
270
318
|
? data.readUInt32LE(index)
|
|
271
319
|
: data.readUInt32BE(index);
|
|
272
|
-
characters.push(codePoint <= 0x10ffff &&
|
|
273
|
-
!(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
320
|
+
characters.push(codePoint <= 0x10ffff && !(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
274
321
|
? String.fromCodePoint(codePoint)
|
|
275
322
|
: "\ufffd");
|
|
276
323
|
}
|
|
@@ -349,6 +396,182 @@ function json(value) {
|
|
|
349
396
|
function errorMessage(error) {
|
|
350
397
|
return error instanceof Error ? error.message : String(error);
|
|
351
398
|
}
|
|
399
|
+
function jsonPointerPath(instancePath) {
|
|
400
|
+
if (!instancePath)
|
|
401
|
+
return [];
|
|
402
|
+
return instancePath
|
|
403
|
+
.slice(1)
|
|
404
|
+
.split("/")
|
|
405
|
+
.map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
406
|
+
.map((part) => (/^(0|[1-9]\d*)$/.test(part) ? Number(part) : part));
|
|
407
|
+
}
|
|
408
|
+
function valueAtPath(value, pathParts) {
|
|
409
|
+
let current = value;
|
|
410
|
+
for (const part of pathParts) {
|
|
411
|
+
if (current === null || typeof current !== "object")
|
|
412
|
+
return undefined;
|
|
413
|
+
current = current[part];
|
|
414
|
+
}
|
|
415
|
+
return current;
|
|
416
|
+
}
|
|
417
|
+
function zodTypeName(type) {
|
|
418
|
+
return type === "integer" ? "number" : type;
|
|
419
|
+
}
|
|
420
|
+
function receivedType(value) {
|
|
421
|
+
if (value === null)
|
|
422
|
+
return "null";
|
|
423
|
+
if (Array.isArray(value))
|
|
424
|
+
return "array";
|
|
425
|
+
return typeof value;
|
|
426
|
+
}
|
|
427
|
+
function arenaEnumIssue(values, pathParts) {
|
|
428
|
+
return {
|
|
429
|
+
code: "invalid_value", values, path: pathParts,
|
|
430
|
+
message: `Invalid option: expected one of ${values.map((item) => JSON.stringify(item)).join("|")}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function arenaValidationIssue(error, value) {
|
|
434
|
+
const pathParts = jsonPointerPath(error.instancePath);
|
|
435
|
+
if (error.keyword === "required") {
|
|
436
|
+
const missingProperty = String(error.params.missingProperty);
|
|
437
|
+
const parentSchema = error.parentSchema;
|
|
438
|
+
const missingSchema = parentSchema?.properties?.[missingProperty];
|
|
439
|
+
if (missingSchema?.enum) {
|
|
440
|
+
return arenaEnumIssue(missingSchema.enum, [...pathParts, missingProperty]);
|
|
441
|
+
}
|
|
442
|
+
const expected = zodTypeName(missingSchema?.type || "unknown");
|
|
443
|
+
return {
|
|
444
|
+
expected,
|
|
445
|
+
code: "invalid_type",
|
|
446
|
+
path: [...pathParts, missingProperty],
|
|
447
|
+
message: `Invalid input: expected ${expected}, received undefined`,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (error.keyword === "type") {
|
|
451
|
+
const expected = zodTypeName(String(error.params.type));
|
|
452
|
+
return {
|
|
453
|
+
expected,
|
|
454
|
+
code: "invalid_type",
|
|
455
|
+
path: pathParts,
|
|
456
|
+
message: `Invalid input: expected ${expected}, received ` +
|
|
457
|
+
receivedType(valueAtPath(value, pathParts)),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (error.keyword === "maxLength" || error.keyword === "minLength") {
|
|
461
|
+
const tooBig = error.keyword === "maxLength";
|
|
462
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
463
|
+
const limit = Number(error.params.limit);
|
|
464
|
+
return {
|
|
465
|
+
origin: "string",
|
|
466
|
+
code: tooBig ? "too_big" : "too_small",
|
|
467
|
+
[limitKey]: limit,
|
|
468
|
+
inclusive: true,
|
|
469
|
+
path: pathParts,
|
|
470
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected string to have ` +
|
|
471
|
+
`${tooBig ? "<=" : ">="}${limit} characters`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (error.keyword === "maximum" || error.keyword === "minimum") {
|
|
475
|
+
const tooBig = error.keyword === "maximum";
|
|
476
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
477
|
+
const limit = Number(error.params.limit);
|
|
478
|
+
return {
|
|
479
|
+
origin: "number",
|
|
480
|
+
code: tooBig ? "too_big" : "too_small",
|
|
481
|
+
[limitKey]: limit,
|
|
482
|
+
inclusive: true,
|
|
483
|
+
path: pathParts,
|
|
484
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected number to be ` +
|
|
485
|
+
`${tooBig ? "<=" : ">="}${limit}`,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (error.keyword === "maxItems" || error.keyword === "minItems") {
|
|
489
|
+
const tooBig = error.keyword === "maxItems";
|
|
490
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
491
|
+
const limit = Number(error.params.limit);
|
|
492
|
+
return {
|
|
493
|
+
origin: "array",
|
|
494
|
+
code: tooBig ? "too_big" : "too_small",
|
|
495
|
+
[limitKey]: limit,
|
|
496
|
+
inclusive: true,
|
|
497
|
+
path: pathParts,
|
|
498
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected array to have ` +
|
|
499
|
+
`${tooBig ? "<=" : ">="}${limit} items`,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (error.keyword === "enum") {
|
|
503
|
+
return arenaEnumIssue(error.params.allowedValues, pathParts);
|
|
504
|
+
}
|
|
505
|
+
if (error.keyword === "format") {
|
|
506
|
+
return {
|
|
507
|
+
...(error.params.format === "uri" ? {} : { origin: "string" }),
|
|
508
|
+
code: "invalid_format",
|
|
509
|
+
format: error.params.format === "uri" ? "url" : error.params.format,
|
|
510
|
+
path: pathParts,
|
|
511
|
+
message: error.params.format === "uri" ? "Invalid URL" : "Invalid string",
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
code: "custom",
|
|
516
|
+
path: pathParts,
|
|
517
|
+
message: error.message || "Invalid input",
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function arenaValidationIssues(error, value) {
|
|
521
|
+
const issues = [arenaValidationIssue(error, value)];
|
|
522
|
+
if (error.keyword !== "type" || error.params.type !== "array")
|
|
523
|
+
return issues;
|
|
524
|
+
const pathParts = jsonPointerPath(error.instancePath);
|
|
525
|
+
const received = valueAtPath(value, pathParts);
|
|
526
|
+
if (typeof received !== "string")
|
|
527
|
+
return issues;
|
|
528
|
+
const parentSchema = error.parentSchema;
|
|
529
|
+
const maximum = parentSchema?.maxItems;
|
|
530
|
+
if (typeof maximum === "number" && received.length > maximum) {
|
|
531
|
+
issues.push({
|
|
532
|
+
origin: "string",
|
|
533
|
+
code: "too_big",
|
|
534
|
+
maximum,
|
|
535
|
+
inclusive: true,
|
|
536
|
+
path: pathParts,
|
|
537
|
+
message: `Too big: expected string to have <=${maximum} characters`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const minimum = parentSchema?.minItems;
|
|
541
|
+
if (typeof minimum === "number" && received.length < minimum) {
|
|
542
|
+
issues.push({
|
|
543
|
+
origin: "string",
|
|
544
|
+
code: "too_small",
|
|
545
|
+
minimum,
|
|
546
|
+
inclusive: true,
|
|
547
|
+
path: pathParts,
|
|
548
|
+
message: `Too small: expected string to have >=${minimum} characters`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return issues;
|
|
552
|
+
}
|
|
553
|
+
function arenaValidationError(toolName, value, errors, customIssues = []) {
|
|
554
|
+
const issues = (errors || []).flatMap((error) => arenaValidationIssues(error, value)).concat(customIssues);
|
|
555
|
+
return (`Invalid input for tool ${toolName}: Type validation failed: Value: ` +
|
|
556
|
+
`${JSON.stringify(value)}.\nError message: ${JSON.stringify(issues, null, 2)}`);
|
|
557
|
+
}
|
|
558
|
+
function arenaJsonParsingError(toolName, text, error) {
|
|
559
|
+
return (`Invalid input for tool ${toolName}: JSON parsing failed: Text: ${text}.\n` +
|
|
560
|
+
`Error message: ${errorMessage(error)}`);
|
|
561
|
+
}
|
|
562
|
+
function unavailableToolError(toolName, availableToolNames) {
|
|
563
|
+
return (`Model tried to call unavailable tool '${toolName}'. Available tools: ` +
|
|
564
|
+
`${[...availableToolNames, "compact"].join(", ")}.`);
|
|
565
|
+
}
|
|
566
|
+
function prefixedFailure(prefix, message) {
|
|
567
|
+
return message.startsWith(prefix) ? message : `${prefix}${message}`;
|
|
568
|
+
}
|
|
569
|
+
function fetchPageFailure(message) {
|
|
570
|
+
const status = message.match(/(?:fetch_page|Firecrawl|Jina Reader) failed \((\d{3})\b/i)?.[1];
|
|
571
|
+
return status
|
|
572
|
+
? `Failed to fetch page (HTTP ${status})`
|
|
573
|
+
: "Failed to fetch page";
|
|
574
|
+
}
|
|
352
575
|
function displayByteSize(value) {
|
|
353
576
|
if (value >= 1024 * 1024) {
|
|
354
577
|
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -374,17 +597,27 @@ class ArenaToolExecutor {
|
|
|
374
597
|
config;
|
|
375
598
|
workspace;
|
|
376
599
|
workspaceReal;
|
|
600
|
+
tmpReal;
|
|
601
|
+
varTmpReal;
|
|
377
602
|
validators = new Map();
|
|
378
603
|
pendingVisualContent = new Map();
|
|
379
604
|
askUserHandler;
|
|
380
605
|
imageInputFallbackClient;
|
|
381
606
|
signal;
|
|
382
607
|
workspaceContext;
|
|
608
|
+
nativeSandboxPaths;
|
|
383
609
|
pageReader;
|
|
384
610
|
routifyPageReader;
|
|
385
611
|
processManager;
|
|
386
612
|
voiceRegistry;
|
|
387
|
-
ajv = new Ajv({
|
|
613
|
+
ajv = new Ajv({
|
|
614
|
+
allErrors: true,
|
|
615
|
+
strict: false,
|
|
616
|
+
verbose: true,
|
|
617
|
+
// The captured runtime uses Zod object parsing: unknown object keys are
|
|
618
|
+
// stripped before dispatch instead of being emitted as validation issues.
|
|
619
|
+
removeAdditional: "all",
|
|
620
|
+
});
|
|
388
621
|
// ask_user needs a handler: TUI/WebUI collect real user input, while
|
|
389
622
|
// headless `-p` may delegate to a configured remote User Agent. add_voice
|
|
390
623
|
// remains available without one and deterministically registers a fallback.
|
|
@@ -394,13 +627,24 @@ class ArenaToolExecutor {
|
|
|
394
627
|
this.workspace = path.resolve(config.workspace);
|
|
395
628
|
mkdirSync(this.workspace, { recursive: true });
|
|
396
629
|
this.workspaceReal = realpathSync(this.workspace);
|
|
630
|
+
// A Linux /home/user workspace is already an OS-level sandbox. Other
|
|
631
|
+
// callers still confine ordinary workspace paths, while /tmp and /var/tmp
|
|
632
|
+
// always refer to the actual system directories seen by Bash.
|
|
633
|
+
this.nativeSandboxPaths =
|
|
634
|
+
process.platform === "linux" && this.workspaceReal === "/home/user";
|
|
635
|
+
this.tmpReal = realpathSync("/tmp");
|
|
636
|
+
this.varTmpReal = realpathSync("/var/tmp");
|
|
397
637
|
this.processManager = processManagerForWorkspace(this.workspaceReal);
|
|
398
638
|
this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
|
|
399
639
|
this.askUserHandler = options.askUserHandler;
|
|
400
640
|
this.askUserEnabled = Boolean(options.askUserHandler);
|
|
401
641
|
if (config.imageInputFallbackModel?.trim()) {
|
|
642
|
+
const connection = imageInputFallbackConnection(config);
|
|
402
643
|
this.imageInputFallbackClient =
|
|
403
|
-
options.
|
|
644
|
+
options.imageInputFallbackClient ||
|
|
645
|
+
(connection
|
|
646
|
+
? new HttpModelClient(config, { connection })
|
|
647
|
+
: options.modelClient || new HttpModelClient(config));
|
|
404
648
|
}
|
|
405
649
|
this.signal = options.signal;
|
|
406
650
|
this.workspaceContext = options.workspaceContext;
|
|
@@ -409,6 +653,13 @@ class ArenaToolExecutor {
|
|
|
409
653
|
signal: options.signal,
|
|
410
654
|
});
|
|
411
655
|
addFormats(this.ajv);
|
|
656
|
+
// WHATWG URL parsing matches the captured fetch_page behavior. ajv-formats'
|
|
657
|
+
// RFC URI checker rejects a recorded URL containing `|` in its query even
|
|
658
|
+
// though Arena accepted and fetched it successfully.
|
|
659
|
+
this.ajv.addFormat("uri", {
|
|
660
|
+
type: "string",
|
|
661
|
+
validate: (value) => URL.canParse(value),
|
|
662
|
+
});
|
|
412
663
|
for (const definition of toolDefinitions(this.workspaceContext)) {
|
|
413
664
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
414
665
|
}
|
|
@@ -424,6 +675,102 @@ class ArenaToolExecutor {
|
|
|
424
675
|
const disallowed = new Set(this.config.disallowedTools || []);
|
|
425
676
|
return definitions.filter((definition) => !disallowed.has(definition.function.name));
|
|
426
677
|
}
|
|
678
|
+
parseArguments(call) {
|
|
679
|
+
const rawArguments = typeof call.function?.arguments === "string"
|
|
680
|
+
? call.function.arguments
|
|
681
|
+
: "{}";
|
|
682
|
+
let value;
|
|
683
|
+
try {
|
|
684
|
+
// Captured empty arguments validate as an empty object, while whitespace
|
|
685
|
+
// and other incomplete JSON still produce parsing errors.
|
|
686
|
+
value = JSON.parse(rawArguments === "" ? "{}" : rawArguments);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
return { rawArguments, invalidJsonText: rawArguments, error };
|
|
690
|
+
}
|
|
691
|
+
const malformedArguments = value !== null && typeof value === "object" && !Array.isArray(value)
|
|
692
|
+
? value.__malformedToolArguments
|
|
693
|
+
: undefined;
|
|
694
|
+
if (typeof malformedArguments === "string") {
|
|
695
|
+
if (malformedArguments === "")
|
|
696
|
+
return { rawArguments, value: {} };
|
|
697
|
+
try {
|
|
698
|
+
JSON.parse(malformedArguments);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
return { rawArguments, invalidJsonText: malformedArguments, error };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { rawArguments, value };
|
|
705
|
+
}
|
|
706
|
+
validateArguments(name, value, validate) {
|
|
707
|
+
const validatedArguments = structuredClone(value);
|
|
708
|
+
if (!validate(validatedArguments)) {
|
|
709
|
+
return {
|
|
710
|
+
validationMessage: arenaValidationError(name, value, validate.errors),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (name === "ask_user") {
|
|
714
|
+
const issues = validatedArguments.questions.flatMap((question, index) => {
|
|
715
|
+
const ids = question.options.map((option) => option.id);
|
|
716
|
+
return new Set(ids).size === ids.length ? [] : [{
|
|
717
|
+
code: "custom", path: ["questions", index, "options"],
|
|
718
|
+
message: "Option IDs must be unique within a question",
|
|
719
|
+
}];
|
|
720
|
+
});
|
|
721
|
+
if (issues.length)
|
|
722
|
+
return { validationMessage: arenaValidationError(name, value, [], issues) };
|
|
723
|
+
}
|
|
724
|
+
return { value: validatedArguments };
|
|
725
|
+
}
|
|
726
|
+
inspect(call) {
|
|
727
|
+
const id = String(call.id || "");
|
|
728
|
+
const name = String(call.function?.name || "");
|
|
729
|
+
const originalRawArguments = typeof call.function?.arguments === "string"
|
|
730
|
+
? call.function.arguments
|
|
731
|
+
: "{}";
|
|
732
|
+
const parsed = this.parseArguments(call);
|
|
733
|
+
const validate = this.validators.get(name);
|
|
734
|
+
if (this.config.disallowedTools?.includes(name) || !validate) {
|
|
735
|
+
return {
|
|
736
|
+
id,
|
|
737
|
+
name,
|
|
738
|
+
rawArguments: originalRawArguments,
|
|
739
|
+
...(parsed.error === undefined ? { arguments: parsed.value } : {}),
|
|
740
|
+
validation: "unknown_tool",
|
|
741
|
+
validationMessage: this.config.disallowedTools?.includes(name)
|
|
742
|
+
? `tool is disallowed: ${name}`
|
|
743
|
+
: unavailableToolError(name, toolDefinitions(this.workspaceContext).map((definition) => definition.function.name)),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
if (parsed.error !== undefined) {
|
|
747
|
+
return {
|
|
748
|
+
id,
|
|
749
|
+
name,
|
|
750
|
+
rawArguments: parsed.rawArguments,
|
|
751
|
+
validation: "invalid_json",
|
|
752
|
+
validationMessage: arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const validated = this.validateArguments(name, parsed.value, validate);
|
|
756
|
+
if (validated.validationMessage !== undefined) {
|
|
757
|
+
return {
|
|
758
|
+
id,
|
|
759
|
+
name,
|
|
760
|
+
rawArguments: parsed.rawArguments,
|
|
761
|
+
arguments: parsed.value,
|
|
762
|
+
validation: "invalid_arguments",
|
|
763
|
+
validationMessage: validated.validationMessage,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
id,
|
|
768
|
+
name,
|
|
769
|
+
rawArguments: parsed.rawArguments,
|
|
770
|
+
arguments: parsed.value,
|
|
771
|
+
validation: "valid",
|
|
772
|
+
};
|
|
773
|
+
}
|
|
427
774
|
async execute(call, context) {
|
|
428
775
|
const callId = String(call.id || "");
|
|
429
776
|
const name = String(call.function?.name || "");
|
|
@@ -431,23 +778,91 @@ class ArenaToolExecutor {
|
|
|
431
778
|
try {
|
|
432
779
|
// 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
|
|
433
780
|
// 之前短路,否则命令会照常 spawn 直到自然结束。
|
|
434
|
-
this.signal?.
|
|
781
|
+
if (this.signal?.aborted) {
|
|
782
|
+
return {
|
|
783
|
+
role: "tool",
|
|
784
|
+
tool_call_id: callId,
|
|
785
|
+
content: json({ status: "aborted" }),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
435
788
|
if (this.config.disallowedTools?.includes(name)) {
|
|
436
789
|
throw new Error(`tool is disallowed: ${name}`);
|
|
437
790
|
}
|
|
438
|
-
const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
|
|
439
|
-
? call.function.arguments
|
|
440
|
-
: "{}");
|
|
441
791
|
const validate = this.validators.get(name);
|
|
442
|
-
if (!validate)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
.map((
|
|
447
|
-
|
|
448
|
-
|
|
792
|
+
if (!validate) {
|
|
793
|
+
return {
|
|
794
|
+
role: "tool",
|
|
795
|
+
tool_call_id: callId,
|
|
796
|
+
content: unavailableToolError(name, toolDefinitions(this.workspaceContext).map((definition) => definition.function.name)),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const parsed = this.parseArguments(call);
|
|
800
|
+
if (parsed.error !== undefined) {
|
|
801
|
+
return {
|
|
802
|
+
role: "tool",
|
|
803
|
+
tool_call_id: callId,
|
|
804
|
+
content: arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const argumentsValue = parsed.value;
|
|
808
|
+
// Replay mode (CODE_HARNESS_REPLAY_RESPONSES set): serve recorded
|
|
809
|
+
// outputs BEFORE schema validation. Recorded arguments can violate the
|
|
810
|
+
// current schema — PII-redacted placeholder URLs fail format=url, or
|
|
811
|
+
// the schema evolved since recording (voice_identity vs voice_id) —
|
|
812
|
+
// and the recording must still replay verbatim. Canned lookup consumes
|
|
813
|
+
// the entry, so this must be the single serving point: dispatch() only
|
|
814
|
+
// sees calls without a recorded response. Unlisted calls fall through
|
|
815
|
+
// to validation and real execution.
|
|
816
|
+
const replayed = replayResponseFor(name, argumentsValue);
|
|
817
|
+
if (replayed) {
|
|
818
|
+
for (const artifact of replayed.artifacts ?? []) {
|
|
819
|
+
this.materializeReplayArtifact(artifact);
|
|
820
|
+
}
|
|
821
|
+
const replayContent = typeof replayed.output === "string"
|
|
822
|
+
? replayed.output
|
|
823
|
+
: json(replayed.output);
|
|
824
|
+
if (replayContent.length > toolResultCharacterLimit) {
|
|
825
|
+
return {
|
|
826
|
+
role: "tool",
|
|
827
|
+
tool_call_id: callId,
|
|
828
|
+
content: json({
|
|
829
|
+
status: "error",
|
|
830
|
+
message: `Tool result from ${name} (${replayContent.length} characters) exceeds the ` +
|
|
831
|
+
`maximum allowed size (${toolResultCharacterLimit} characters). ` +
|
|
832
|
+
"Retry with narrower arguments or inspect the data in smaller portions.",
|
|
833
|
+
}),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
return { role: "tool", tool_call_id: callId, content: replayContent };
|
|
837
|
+
}
|
|
838
|
+
const validated = this.validateArguments(name, argumentsValue, validate);
|
|
839
|
+
if (validated.validationMessage !== undefined) {
|
|
840
|
+
return {
|
|
841
|
+
role: "tool",
|
|
842
|
+
tool_call_id: callId,
|
|
843
|
+
content: validated.validationMessage,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
const validatedArguments = validated.value;
|
|
847
|
+
const startedAt = Date.now();
|
|
848
|
+
let result;
|
|
849
|
+
try {
|
|
850
|
+
result = await this.dispatch(name, validatedArguments, callId, context);
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
if (error instanceof AskUserHandlerError) {
|
|
854
|
+
return {
|
|
855
|
+
role: "tool",
|
|
856
|
+
tool_call_id: callId,
|
|
857
|
+
content: json({ status: "error", message: error.message }),
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
role: "tool",
|
|
862
|
+
tool_call_id: callId,
|
|
863
|
+
content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt)),
|
|
864
|
+
};
|
|
449
865
|
}
|
|
450
|
-
const result = await this.dispatch(name, argumentsValue, callId, context);
|
|
451
866
|
const content = typeof result === "string" ? result : json(result);
|
|
452
867
|
if (content.length > toolResultCharacterLimit) {
|
|
453
868
|
return {
|
|
@@ -468,13 +883,6 @@ class ArenaToolExecutor {
|
|
|
468
883
|
};
|
|
469
884
|
}
|
|
470
885
|
catch (error) {
|
|
471
|
-
if (error instanceof AskUserHandlerError) {
|
|
472
|
-
return {
|
|
473
|
-
role: "tool",
|
|
474
|
-
tool_call_id: callId,
|
|
475
|
-
content: json({ status: "error", message: error.message }),
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
886
|
return {
|
|
479
887
|
role: "tool",
|
|
480
888
|
tool_call_id: callId,
|
|
@@ -485,6 +893,82 @@ class ArenaToolExecutor {
|
|
|
485
893
|
};
|
|
486
894
|
}
|
|
487
895
|
}
|
|
896
|
+
runtimeFailureResult(name, values, error, startedAt) {
|
|
897
|
+
if (this.signal?.aborted)
|
|
898
|
+
return { status: "aborted" };
|
|
899
|
+
const message = errorMessage(error);
|
|
900
|
+
const durationMs = Date.now() - startedAt;
|
|
901
|
+
if (name === "bash") {
|
|
902
|
+
return {
|
|
903
|
+
status: "shell_error",
|
|
904
|
+
stderr: message,
|
|
905
|
+
stdout: "",
|
|
906
|
+
exit_code: null,
|
|
907
|
+
duration_ms: durationMs,
|
|
908
|
+
stderr_truncated: false,
|
|
909
|
+
stdout_truncated: false,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (name === "start_process") {
|
|
913
|
+
const cwd = String(values.cwd || "/home/user");
|
|
914
|
+
const code = error?.code;
|
|
915
|
+
const logTail = code === "ENOENT"
|
|
916
|
+
? `[invalid_argument] cwd '${cwd}' does not exist`
|
|
917
|
+
: message;
|
|
918
|
+
return {
|
|
919
|
+
status: "shell_error",
|
|
920
|
+
process_id: null,
|
|
921
|
+
pid: null,
|
|
922
|
+
exit_code: null,
|
|
923
|
+
log_tail: logTail,
|
|
924
|
+
listening_ports: [],
|
|
925
|
+
new_ports: [],
|
|
926
|
+
duration_ms: durationMs,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (name === "get_process_output") {
|
|
930
|
+
return {
|
|
931
|
+
status: "shell_error",
|
|
932
|
+
log_tail: message,
|
|
933
|
+
exit_code: null,
|
|
934
|
+
listening_ports: [],
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
if (name === "stop_process") {
|
|
938
|
+
return { status: "shell_error", log_tail: message };
|
|
939
|
+
}
|
|
940
|
+
if (name === "fetch_page") {
|
|
941
|
+
return { status: "error", error: fetchPageFailure(message) };
|
|
942
|
+
}
|
|
943
|
+
if (name === "add_voice") {
|
|
944
|
+
return {
|
|
945
|
+
status: "error",
|
|
946
|
+
message: prefixedFailure("Failed to audition voices: ", message),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
if (name === "generate_image") {
|
|
950
|
+
return {
|
|
951
|
+
status: "error",
|
|
952
|
+
message: prefixedFailure("Failed to generate image: ", message),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (name === "generate_speech") {
|
|
956
|
+
return {
|
|
957
|
+
status: "error",
|
|
958
|
+
message: prefixedFailure("Failed to generate speech: ", message),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
if (name === "image_search") {
|
|
962
|
+
return {
|
|
963
|
+
status: "error",
|
|
964
|
+
message: prefixedFailure("Image search failed: ", message),
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (name === "present_file") {
|
|
968
|
+
return { status: "error", message: "Error calling present file tool" };
|
|
969
|
+
}
|
|
970
|
+
return { status: "error", message };
|
|
971
|
+
}
|
|
488
972
|
takeVisualMessage(toolCallIds) {
|
|
489
973
|
// 生产契约:同一组 tool call 的全部图片汇入一条 user message,
|
|
490
974
|
// 由一个 arena-system-message 文本 part 领头。
|
|
@@ -503,6 +987,8 @@ class ArenaToolExecutor {
|
|
|
503
987
|
};
|
|
504
988
|
}
|
|
505
989
|
async dispatch(name, values, toolCallId, context) {
|
|
990
|
+
// Canned replay responses are served in execute() before schema
|
|
991
|
+
// validation (the lookup consumes entries — single serving point).
|
|
506
992
|
switch (name) {
|
|
507
993
|
case "bash":
|
|
508
994
|
return this.bash(String(values.command), String(values.cwd ||
|
|
@@ -555,32 +1041,76 @@ class ArenaToolExecutor {
|
|
|
555
1041
|
throw new Error(`unknown tool: ${name}`);
|
|
556
1042
|
}
|
|
557
1043
|
}
|
|
1044
|
+
// Copy a pre-staged replay artifact blob into the workspace. Recorded
|
|
1045
|
+
// artifact paths must stay workspace-relative: an absolute path or one
|
|
1046
|
+
// escaping the workspace is a corrupt recording, so reject it loudly.
|
|
1047
|
+
materializeReplayArtifact(artifact) {
|
|
1048
|
+
const normalized = path.posix.normalize(artifact.path);
|
|
1049
|
+
if (path.posix.isAbsolute(artifact.path) ||
|
|
1050
|
+
path.posix.isAbsolute(normalized) ||
|
|
1051
|
+
normalized === ".." ||
|
|
1052
|
+
normalized.startsWith("../")) {
|
|
1053
|
+
throw new Error(`replay artifact path escapes the workspace: ${JSON.stringify(artifact.path)}`);
|
|
1054
|
+
}
|
|
1055
|
+
const target = path.resolve(this.workspace, normalized);
|
|
1056
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
1057
|
+
copyFileSync(artifact.file, target);
|
|
1058
|
+
}
|
|
558
1059
|
async confinedPath(raw) {
|
|
559
1060
|
if (!raw)
|
|
560
1061
|
throw new Error("path must not be empty");
|
|
561
1062
|
let candidate;
|
|
562
|
-
|
|
563
|
-
|
|
1063
|
+
let confinementRoot = this.workspace;
|
|
1064
|
+
let confinementRootReal = this.workspaceReal;
|
|
1065
|
+
const expanded = raw === "~"
|
|
1066
|
+
? "/home/user"
|
|
1067
|
+
: raw.startsWith("~/")
|
|
1068
|
+
? `/home/user/${raw.slice(2)}`
|
|
1069
|
+
: raw;
|
|
1070
|
+
const virtualAbsolute = path.posix.resolve("/home/user", expanded);
|
|
1071
|
+
let suffix;
|
|
1072
|
+
if (virtualAbsolute === "/var/tmp" ||
|
|
1073
|
+
virtualAbsolute.startsWith("/var/tmp/")) {
|
|
1074
|
+
confinementRoot = "/var/tmp";
|
|
1075
|
+
confinementRootReal = this.varTmpReal;
|
|
1076
|
+
suffix = virtualAbsolute.slice("/var/tmp".length).replace(/^\/+/, "");
|
|
1077
|
+
candidate = path.join(confinementRoot, suffix);
|
|
564
1078
|
}
|
|
565
|
-
else if (
|
|
566
|
-
|
|
1079
|
+
else if (virtualAbsolute === "/tmp" ||
|
|
1080
|
+
virtualAbsolute.startsWith("/tmp/")) {
|
|
1081
|
+
confinementRoot = "/tmp";
|
|
1082
|
+
confinementRootReal = this.tmpReal;
|
|
1083
|
+
suffix = virtualAbsolute.slice("/tmp".length).replace(/^\/+/, "");
|
|
1084
|
+
candidate = path.join(confinementRoot, suffix);
|
|
1085
|
+
}
|
|
1086
|
+
else if (this.nativeSandboxPaths && path.posix.isAbsolute(raw)) {
|
|
1087
|
+
confinementRoot = "/";
|
|
1088
|
+
confinementRootReal = "/";
|
|
1089
|
+
candidate = raw;
|
|
567
1090
|
}
|
|
568
|
-
else if (!this.workspaceContext
|
|
1091
|
+
else if (!this.workspaceContext) {
|
|
1092
|
+
if (virtualAbsolute === "/home/user" ||
|
|
1093
|
+
virtualAbsolute.startsWith("/home/user/")) {
|
|
1094
|
+
suffix = virtualAbsolute.slice("/home/user".length).replace(/^\/+/, "");
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
throw new Error(`path is outside the workspace: ${raw}`);
|
|
1098
|
+
}
|
|
1099
|
+
candidate = path.join(confinementRoot, suffix);
|
|
1100
|
+
}
|
|
1101
|
+
else if (raw === "/home/user") {
|
|
569
1102
|
candidate = this.workspace;
|
|
570
1103
|
}
|
|
571
|
-
else if (
|
|
572
|
-
candidate = path.join(this.workspace, raw.slice(
|
|
1104
|
+
else if (raw.startsWith("/home/user/")) {
|
|
1105
|
+
candidate = path.join(this.workspace, raw.slice("/home/user/".length));
|
|
573
1106
|
}
|
|
574
1107
|
else {
|
|
575
|
-
const defaultCwd = this.workspaceContext
|
|
576
|
-
? path.resolve(this.workspaceContext.cwd)
|
|
577
|
-
: this.workspace;
|
|
1108
|
+
const defaultCwd = path.resolve(this.workspaceContext.cwd);
|
|
578
1109
|
candidate = path.isAbsolute(raw) ? raw : path.join(defaultCwd, raw);
|
|
579
1110
|
}
|
|
580
1111
|
const absolute = path.resolve(candidate);
|
|
581
|
-
const lexicalRelative = path.relative(
|
|
582
|
-
if (lexicalRelative.startsWith("..") ||
|
|
583
|
-
path.isAbsolute(lexicalRelative)) {
|
|
1112
|
+
const lexicalRelative = path.relative(confinementRoot, absolute);
|
|
1113
|
+
if (lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
584
1114
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
585
1115
|
}
|
|
586
1116
|
let ancestor = absolute;
|
|
@@ -592,33 +1122,45 @@ class ArenaToolExecutor {
|
|
|
592
1122
|
}
|
|
593
1123
|
const ancestorReal = await realpath(ancestor);
|
|
594
1124
|
const resolved = path.resolve(ancestorReal, path.relative(ancestor, absolute));
|
|
595
|
-
const realRelative = path.relative(
|
|
1125
|
+
const realRelative = path.relative(confinementRootReal, resolved);
|
|
596
1126
|
if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) {
|
|
597
1127
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
598
1128
|
}
|
|
599
1129
|
return resolved;
|
|
600
1130
|
}
|
|
601
1131
|
relativePath(target) {
|
|
1132
|
+
for (const [root, virtualRoot] of [
|
|
1133
|
+
[this.varTmpReal, "/var/tmp"],
|
|
1134
|
+
[this.tmpReal, "/tmp"],
|
|
1135
|
+
]) {
|
|
1136
|
+
const relative = path.relative(root, target);
|
|
1137
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
1138
|
+
const suffix = relative.split(path.sep).join("/");
|
|
1139
|
+
return suffix ? `${virtualRoot}/${suffix}` : virtualRoot;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
602
1142
|
return path.relative(this.workspaceReal, target).split(path.sep).join("/");
|
|
603
1143
|
}
|
|
604
|
-
|
|
605
|
-
if (this.workspaceContext)
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
return
|
|
1144
|
+
displayWorkspacePath(rawPath) {
|
|
1145
|
+
if (!this.workspaceContext) {
|
|
1146
|
+
if (rawPath === "/home/user" || rawPath === "~")
|
|
1147
|
+
return ".";
|
|
1148
|
+
if (rawPath.startsWith("/home/user/")) {
|
|
1149
|
+
return rawPath.slice("/home/user/".length);
|
|
1150
|
+
}
|
|
1151
|
+
if (rawPath.startsWith("~/"))
|
|
1152
|
+
return rawPath.slice(2);
|
|
1153
|
+
}
|
|
1154
|
+
return rawPath;
|
|
615
1155
|
}
|
|
616
1156
|
sandboxEnvironment() {
|
|
617
1157
|
const env = {
|
|
618
1158
|
...process.env,
|
|
619
1159
|
ARENA_WORKSPACE: this.workspace,
|
|
620
|
-
|
|
621
|
-
|
|
1160
|
+
// Agent Arena captures use C character classification (grep -P, wc -w).
|
|
1161
|
+
// This controls command behavior, not the UTF-8 encoding of stored files.
|
|
1162
|
+
LANG: "C",
|
|
1163
|
+
LC_ALL: "C",
|
|
622
1164
|
};
|
|
623
1165
|
delete env.FORCE_COLOR;
|
|
624
1166
|
delete env.CLICOLOR_FORCE;
|
|
@@ -626,28 +1168,30 @@ class ArenaToolExecutor {
|
|
|
626
1168
|
}
|
|
627
1169
|
async bash(command, cwd, timeoutSeconds) {
|
|
628
1170
|
const workdir = await this.confinedPath(cwd);
|
|
629
|
-
|
|
1171
|
+
const metadata = await stat(workdir).catch((error) => {
|
|
1172
|
+
// A missing cwd prevents shell startup; captured Bash stderr is empty.
|
|
1173
|
+
// Keep other filesystem failures and start_process diagnostics intact.
|
|
1174
|
+
if (error.code === "ENOENT")
|
|
1175
|
+
throw new Error("");
|
|
1176
|
+
throw error;
|
|
1177
|
+
});
|
|
1178
|
+
if (!metadata.isDirectory()) {
|
|
630
1179
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
631
1180
|
}
|
|
632
|
-
// Legacy callers without runtime context retain the captured /home/user
|
|
633
|
-
// contract. Runtime-aware callers execute and report their actual paths.
|
|
634
|
-
const translatedCommand = this.workspaceContext
|
|
635
|
-
? command
|
|
636
|
-
: command.replaceAll("/home/user", "\"$ARENA_WORKSPACE\"");
|
|
637
1181
|
return new Promise((resolve, reject) => {
|
|
638
1182
|
const env = this.sandboxEnvironment();
|
|
639
1183
|
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
640
1184
|
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
641
1185
|
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
642
|
-
const child = spawn("/bin/bash", ["-lc",
|
|
1186
|
+
const child = spawn("/bin/bash", ["-lc", command], {
|
|
643
1187
|
cwd: workdir,
|
|
644
1188
|
env,
|
|
645
1189
|
stdio: ["ignore", "pipe", "pipe"],
|
|
646
1190
|
detached: process.platform !== "win32",
|
|
647
1191
|
});
|
|
648
1192
|
const startedAt = Date.now();
|
|
649
|
-
|
|
650
|
-
|
|
1193
|
+
const stdout = new BashOutputBuffer();
|
|
1194
|
+
const stderr = new BashOutputBuffer();
|
|
651
1195
|
let timedOut = false;
|
|
652
1196
|
let settled = false;
|
|
653
1197
|
let forceSettle;
|
|
@@ -684,8 +1228,8 @@ class ArenaToolExecutor {
|
|
|
684
1228
|
reject(this.signal.reason || new Error("operation cancelled"));
|
|
685
1229
|
return;
|
|
686
1230
|
}
|
|
687
|
-
const out =
|
|
688
|
-
const err =
|
|
1231
|
+
const out = stdout.result();
|
|
1232
|
+
const err = stderr.result();
|
|
689
1233
|
resolve({
|
|
690
1234
|
status: timedOut ? "timeout" : signal ? "killed" : "completed",
|
|
691
1235
|
stderr: err.text,
|
|
@@ -715,10 +1259,10 @@ class ArenaToolExecutor {
|
|
|
715
1259
|
};
|
|
716
1260
|
this.signal?.addEventListener("abort", abort, { once: true });
|
|
717
1261
|
child.stdout.on("data", (chunk) => {
|
|
718
|
-
stdout
|
|
1262
|
+
stdout.append(chunk);
|
|
719
1263
|
});
|
|
720
1264
|
child.stderr.on("data", (chunk) => {
|
|
721
|
-
stderr
|
|
1265
|
+
stderr.append(chunk);
|
|
722
1266
|
});
|
|
723
1267
|
child.once("error", (error) => {
|
|
724
1268
|
if (settled)
|
|
@@ -738,12 +1282,9 @@ class ArenaToolExecutor {
|
|
|
738
1282
|
if (!(await stat(workdir)).isDirectory()) {
|
|
739
1283
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
740
1284
|
}
|
|
741
|
-
const translatedCommand = this.workspaceContext
|
|
742
|
-
? command
|
|
743
|
-
: command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
|
|
744
1285
|
return this.processManager.start({
|
|
745
1286
|
name,
|
|
746
|
-
command
|
|
1287
|
+
command,
|
|
747
1288
|
cwd: workdir,
|
|
748
1289
|
startupWaitSeconds,
|
|
749
1290
|
env: this.sandboxEnvironment(),
|
|
@@ -752,10 +1293,12 @@ class ArenaToolExecutor {
|
|
|
752
1293
|
}
|
|
753
1294
|
async askUser(questions, toolCallId, context) {
|
|
754
1295
|
if (this.askUserHandler) {
|
|
755
|
-
return this.askUserHandler(toolCallId, questions, context
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1296
|
+
return this.askUserHandler(toolCallId, questions, context
|
|
1297
|
+
? {
|
|
1298
|
+
...context,
|
|
1299
|
+
signal: this.signal || context.signal,
|
|
1300
|
+
}
|
|
1301
|
+
: undefined);
|
|
759
1302
|
}
|
|
760
1303
|
// ask_user is not offered in non-interactive sessions; if the model still
|
|
761
1304
|
// emits it (e.g. resumed history or a hallucinated call), report an error
|
|
@@ -820,28 +1363,55 @@ class ArenaToolExecutor {
|
|
|
820
1363
|
};
|
|
821
1364
|
}
|
|
822
1365
|
async editFile(rawPath, oldText, newText) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1366
|
+
let target;
|
|
1367
|
+
let source;
|
|
1368
|
+
try {
|
|
1369
|
+
target = await this.confinedPath(rawPath);
|
|
1370
|
+
if (!(await stat(target)).isFile()) {
|
|
1371
|
+
return {
|
|
1372
|
+
status: "error",
|
|
1373
|
+
message: `File not found: ${rawPath}`,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
source = await readFile(target, "utf8");
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
return {
|
|
1380
|
+
status: "error",
|
|
1381
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
// Agent Arena replaces the first match and tolerates multiline whitespace
|
|
1385
|
+
// drift. Code Arena deliberately uses a separate unique-match policy.
|
|
1386
|
+
const edited = replaceFirstText(source, oldText, newText, {
|
|
1387
|
+
whitespaceInsensitiveMultiline: true,
|
|
1388
|
+
});
|
|
1389
|
+
if (edited === undefined) {
|
|
832
1390
|
return {
|
|
833
1391
|
status: "error",
|
|
834
1392
|
message: "Context not found. Read the file to verify the text exists.",
|
|
835
1393
|
};
|
|
836
1394
|
}
|
|
837
|
-
|
|
1395
|
+
try {
|
|
1396
|
+
// Replacement text is literal. String.replace(string, string) expands
|
|
1397
|
+
// $&, $`, $' and $$ sequences and therefore corrupts JavaScript/jQuery
|
|
1398
|
+
// snippets observed in production trajectories.
|
|
1399
|
+
await writeFile(target, edited, "utf8");
|
|
1400
|
+
}
|
|
1401
|
+
catch {
|
|
1402
|
+
return {
|
|
1403
|
+
status: "error",
|
|
1404
|
+
message: `Failed to write workspace file: ${rawPath}`,
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
838
1407
|
return { status: "success", message: `Edited ${rawPath}.` };
|
|
839
1408
|
}
|
|
840
1409
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
841
|
-
|
|
1410
|
+
let target;
|
|
842
1411
|
let fileSize;
|
|
843
1412
|
let sample;
|
|
844
1413
|
try {
|
|
1414
|
+
target = await this.confinedPath(rawPath);
|
|
845
1415
|
const metadata = await stat(target);
|
|
846
1416
|
if (!metadata.isFile())
|
|
847
1417
|
throw new Error("not a regular file");
|
|
@@ -858,26 +1428,25 @@ class ArenaToolExecutor {
|
|
|
858
1428
|
}
|
|
859
1429
|
}
|
|
860
1430
|
catch {
|
|
861
|
-
// 逐字对齐生产错误文案,路径按调用方原样回显。
|
|
862
1431
|
return {
|
|
863
1432
|
status: "error",
|
|
864
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1433
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
865
1434
|
};
|
|
866
1435
|
}
|
|
867
1436
|
const extension = path.extname(target).toLowerCase();
|
|
868
1437
|
const kind = workspaceFileKind(target, sample);
|
|
869
|
-
if (kind === "text" && fileSize > textFileByteLimit) {
|
|
870
|
-
return {
|
|
871
|
-
status: "error",
|
|
872
|
-
message: `File content (${displayByteSize(fileSize)}) exceeds the maximum allowed size ` +
|
|
873
|
-
`(${displayByteSize(textFileByteLimit)}). Use bash with sed/head/tail to read ` +
|
|
874
|
-
"specific portions of the file, or grep/rg to search for specific content " +
|
|
875
|
-
"instead of reading the whole file.",
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
1438
|
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
879
1439
|
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
880
1440
|
}
|
|
1441
|
+
// Current Arena v2 captures consistently expose local PDFs as binary
|
|
1442
|
+
// metadata. PDF text extraction belongs to fetch_page, not read_file.
|
|
1443
|
+
if (extension === ".pdf") {
|
|
1444
|
+
return {
|
|
1445
|
+
kind: "unsupported",
|
|
1446
|
+
mediaType: "application/pdf",
|
|
1447
|
+
size: fileSize,
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
881
1450
|
const mainModelSupportsImages = kind === "image" ? supportsImageInput(this.config) : false;
|
|
882
1451
|
const fallbackModel = mainModelSupportsImages
|
|
883
1452
|
? undefined
|
|
@@ -892,7 +1461,7 @@ class ArenaToolExecutor {
|
|
|
892
1461
|
catch {
|
|
893
1462
|
return {
|
|
894
1463
|
status: "error",
|
|
895
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1464
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
896
1465
|
};
|
|
897
1466
|
}
|
|
898
1467
|
if (isStructuredDocument(target)) {
|
|
@@ -920,12 +1489,13 @@ class ArenaToolExecutor {
|
|
|
920
1489
|
return result;
|
|
921
1490
|
}
|
|
922
1491
|
if (kind === "text") {
|
|
923
|
-
const
|
|
1492
|
+
const text = readTextResult(data);
|
|
924
1493
|
return {
|
|
925
1494
|
kind: "text",
|
|
926
|
-
lines:
|
|
927
|
-
size:
|
|
928
|
-
content,
|
|
1495
|
+
lines: data.reduce((count, byte) => count + (byte === 0x0a ? 1 : 0), 1),
|
|
1496
|
+
size: data.length,
|
|
1497
|
+
content: text.content,
|
|
1498
|
+
...(text.truncated ? { truncated: true } : {}),
|
|
929
1499
|
};
|
|
930
1500
|
}
|
|
931
1501
|
const mimeType = mime.getType(target) ||
|
|
@@ -975,9 +1545,29 @@ class ArenaToolExecutor {
|
|
|
975
1545
|
};
|
|
976
1546
|
}
|
|
977
1547
|
async writeWorkspaceFile(rawPath, content) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1548
|
+
try {
|
|
1549
|
+
const target = await this.confinedPath(rawPath);
|
|
1550
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
1551
|
+
try {
|
|
1552
|
+
await writeFile(target, content, "utf8");
|
|
1553
|
+
}
|
|
1554
|
+
catch (error) {
|
|
1555
|
+
const code = error.code;
|
|
1556
|
+
if (!["EACCES", "EPERM"].includes(code || "") || !privilegedFileCreationEnabled({
|
|
1557
|
+
platform: process.platform, workspace: this.workspaceReal,
|
|
1558
|
+
docker: existsSync("/.dockerenv"), flag: process.env.ARENA_SANDBOX_PRIVILEGED_FILE_CREATION,
|
|
1559
|
+
uid: process.getuid?.(),
|
|
1560
|
+
}))
|
|
1561
|
+
throw error;
|
|
1562
|
+
await createSandboxFile(target, content);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
catch {
|
|
1566
|
+
return {
|
|
1567
|
+
status: "error",
|
|
1568
|
+
message: `Failed to write workspace file: ${rawPath}`,
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
981
1571
|
return { status: "success" };
|
|
982
1572
|
}
|
|
983
1573
|
async webSearch(query, depth) {
|
|
@@ -993,18 +1583,21 @@ class ArenaToolExecutor {
|
|
|
993
1583
|
const slug = query
|
|
994
1584
|
.toLowerCase()
|
|
995
1585
|
.replace(/[^a-z0-9]+/g, "-")
|
|
996
|
-
.
|
|
1586
|
+
.replace(/^-+|-+$/g, "")
|
|
1587
|
+
.slice(0, 40) || "results";
|
|
997
1588
|
for (const value of results) {
|
|
998
1589
|
if (saved.length >= count)
|
|
999
1590
|
break;
|
|
1000
1591
|
const item = record(value);
|
|
1001
1592
|
const properties = record(item.properties);
|
|
1002
1593
|
const thumbnail = record(item.thumbnail);
|
|
1003
|
-
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1594
|
+
// Brave's result URL is the source page; properties.url is an image asset.
|
|
1595
|
+
const sourceUrl = stringValue(item.url);
|
|
1596
|
+
const imageUrl = stringValue(properties.url) || stringValue(thumbnail.src);
|
|
1597
|
+
const thumbnailUrl = stringValue(thumbnail.src) || imageUrl;
|
|
1598
|
+
if (!imageUrl)
|
|
1006
1599
|
continue;
|
|
1007
|
-
const downloadUrls = [...new Set([
|
|
1600
|
+
const downloadUrls = [...new Set([imageUrl, thumbnailUrl])];
|
|
1008
1601
|
for (const downloadUrl of downloadUrls) {
|
|
1009
1602
|
try {
|
|
1010
1603
|
const imageResponse = await fetch(downloadUrl, {
|
|
@@ -1019,7 +1612,9 @@ class ArenaToolExecutor {
|
|
|
1019
1612
|
.split(";")[0]
|
|
1020
1613
|
?.trim()
|
|
1021
1614
|
.toLowerCase();
|
|
1022
|
-
const extension =
|
|
1615
|
+
const extension = normalizedType === "image/jpeg"
|
|
1616
|
+
? ".jpg"
|
|
1617
|
+
: Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
|
|
1023
1618
|
const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${extension}`);
|
|
1024
1619
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1025
1620
|
await writeFile(target, await responseBytes(imageResponse));
|
|
@@ -1038,22 +1633,40 @@ class ArenaToolExecutor {
|
|
|
1038
1633
|
}
|
|
1039
1634
|
}
|
|
1040
1635
|
}
|
|
1636
|
+
if (!saved.length) {
|
|
1637
|
+
return {
|
|
1638
|
+
status: "error",
|
|
1639
|
+
message: "Image search failed: Strict safe search filtered all results for this query. " +
|
|
1640
|
+
"Try a different or more specific query.",
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1041
1643
|
return { status: "success", results: saved };
|
|
1042
1644
|
}
|
|
1043
1645
|
async presentFile(rawPath) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1646
|
+
try {
|
|
1647
|
+
const target = await this.confinedPath(rawPath);
|
|
1648
|
+
if (!(await stat(target)).isFile())
|
|
1649
|
+
throw new Error(rawPath);
|
|
1650
|
+
return { path: rawPath, status: "success" };
|
|
1651
|
+
}
|
|
1652
|
+
catch {
|
|
1653
|
+
return { status: "error", message: "Error calling present file tool" };
|
|
1654
|
+
}
|
|
1048
1655
|
}
|
|
1049
1656
|
async generateImage(rawPath, prompt, images) {
|
|
1657
|
+
if (!rawPath) {
|
|
1658
|
+
return { status: "error", message: "File path cannot be empty." };
|
|
1659
|
+
}
|
|
1050
1660
|
const target = await this.confinedPath(rawPath);
|
|
1051
1661
|
const extension = path.extname(target).toLowerCase();
|
|
1052
1662
|
if (![".jpg", ".jpeg", ".png"].includes(extension)) {
|
|
1053
|
-
|
|
1663
|
+
return {
|
|
1664
|
+
status: "error",
|
|
1665
|
+
message: "File path must end in .jpg, .jpeg, or .png.",
|
|
1666
|
+
};
|
|
1054
1667
|
}
|
|
1055
1668
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1056
|
-
const model = process.env.ARENA_IMAGE_MODEL || "gemini-
|
|
1669
|
+
const model = process.env.ARENA_IMAGE_MODEL || "gemini-3.1-flash-image";
|
|
1057
1670
|
const references = [];
|
|
1058
1671
|
for (const imagePath of images || []) {
|
|
1059
1672
|
const source = await this.confinedPath(imagePath);
|
|
@@ -1062,7 +1675,13 @@ class ArenaToolExecutor {
|
|
|
1062
1675
|
if (!mimeType?.startsWith("image/")) {
|
|
1063
1676
|
throw new Error(`unsupported reference image format: ${imagePath}`);
|
|
1064
1677
|
}
|
|
1065
|
-
|
|
1678
|
+
let bytes;
|
|
1679
|
+
try {
|
|
1680
|
+
bytes = await readFile(source);
|
|
1681
|
+
}
|
|
1682
|
+
catch {
|
|
1683
|
+
throw new Error(`Failed to read workspace file: ${imagePath}`);
|
|
1684
|
+
}
|
|
1066
1685
|
if (bytes.length > 20 * 1024 * 1024) {
|
|
1067
1686
|
throw new Error(`reference image exceeds 20 MB: ${imagePath}`);
|
|
1068
1687
|
}
|
|
@@ -1082,7 +1701,7 @@ class ArenaToolExecutor {
|
|
|
1082
1701
|
await writeFile(target, await generatedImageBytesForExtension(generated.bytes, generated.mimeType, extension));
|
|
1083
1702
|
return {
|
|
1084
1703
|
status: "success",
|
|
1085
|
-
file_path:
|
|
1704
|
+
file_path: rawPath,
|
|
1086
1705
|
};
|
|
1087
1706
|
}
|
|
1088
1707
|
async generateSpeech(rawPath, text, voiceId, language) {
|
|
@@ -1095,7 +1714,7 @@ class ArenaToolExecutor {
|
|
|
1095
1714
|
".opus": "opus",
|
|
1096
1715
|
}[extension];
|
|
1097
1716
|
if (!format) {
|
|
1098
|
-
throw new Error("ModelRouter
|
|
1717
|
+
throw new Error("ModelRouter TTS output supports .mp3, .wav, .ogg, or .opus");
|
|
1099
1718
|
}
|
|
1100
1719
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1101
1720
|
const registered = this.voiceRegistry.resolve(voiceId);
|
|
@@ -1111,14 +1730,15 @@ class ArenaToolExecutor {
|
|
|
1111
1730
|
const voice = process.env.ARENA_SPEECH_VOICE || registered.providerVoice;
|
|
1112
1731
|
await writeFile(target, await generateSpeechWithModelRouter({
|
|
1113
1732
|
format,
|
|
1114
|
-
model:
|
|
1733
|
+
model: registered.model,
|
|
1734
|
+
language: language || registered.language,
|
|
1115
1735
|
signal: this.signal,
|
|
1116
1736
|
text,
|
|
1117
1737
|
voice,
|
|
1118
1738
|
}));
|
|
1119
1739
|
return {
|
|
1120
1740
|
status: "success",
|
|
1121
|
-
file_path:
|
|
1741
|
+
file_path: rawPath,
|
|
1122
1742
|
};
|
|
1123
1743
|
}
|
|
1124
1744
|
}
|
|
@@ -1143,6 +1763,9 @@ export class ToolExecutor {
|
|
|
1143
1763
|
availableTools() {
|
|
1144
1764
|
return this.implementation.availableTools();
|
|
1145
1765
|
}
|
|
1766
|
+
inspect(call) {
|
|
1767
|
+
return this.implementation.inspect(call);
|
|
1768
|
+
}
|
|
1146
1769
|
execute(call, context) {
|
|
1147
1770
|
return this.implementation.execute(call, context);
|
|
1148
1771
|
}
|