@qwen-code/arena-agent 0.0.10-dev.1 → 0.0.10-dev.11
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 +68 -14
- package/CHANGELOG.md +245 -0
- package/README.md +307 -57
- package/dist/agent.d.ts +28 -2
- package/dist/agent.js +389 -109
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.js +32 -35
- 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.d.ts +2 -0
- package/dist/canonical-data.js +4 -2
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +184 -36
- 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 +50 -3
- package/dist/compaction.js +390 -185
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +23 -2
- package/dist/config.js +52 -5
- package/dist/config.js.map +1 -1
- package/dist/connector-mcp.d.ts +47 -0
- package/dist/connector-mcp.js +314 -0
- package/dist/connector-mcp.js.map +1 -0
- package/dist/connectors.d.ts +8 -0
- package/dist/connectors.js +57 -0
- package/dist/connectors.js.map +1 -0
- 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 +16 -3
- package/dist/history.js +117 -19
- package/dist/history.js.map +1 -1
- package/dist/midturn-checkpoint-hook.d.ts +44 -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 +121 -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 +2 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/prompts.d.ts +2 -1
- package/dist/prompts.js +15 -37
- 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 +63 -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/server.js +17 -5
- 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.d.ts +5 -4
- package/dist/spec.js +31 -17
- 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/summary-compaction.d.ts +21 -0
- package/dist/summary-compaction.js +211 -0
- package/dist/summary-compaction.js.map +1 -0
- package/dist/tools.d.ts +8 -1
- package/dist/tools.js +407 -135
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +13 -10
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +30 -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-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 +60 -6
- package/docs/SDK.md +5 -0
- 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 +80 -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";
|
|
@@ -10,18 +10,34 @@ import mime from "mime/lite";
|
|
|
10
10
|
import { AskUserHandlerError } from "./ask-user.js";
|
|
11
11
|
import { executeBraveImageSearch, executeBraveWebSearch, } from "./brave-search.js";
|
|
12
12
|
import { CodeToolExecutor } from "./code-tools.js";
|
|
13
|
+
import { ConnectorTools, connectorFailureText } from "./connector-mcp.js";
|
|
13
14
|
import { supportsImageInput } from "./config.js";
|
|
14
15
|
import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
|
|
15
16
|
import { FetchPageReader } from "./fetch-page.js";
|
|
17
|
+
import { replaceFirstText } from "./file-edit.js";
|
|
16
18
|
import { generatedImageBytesForExtension, prepareImageForModel, } from "./image.js";
|
|
17
19
|
import { imageTextFromFallbackModel } from "./image-input-fallback.js";
|
|
18
20
|
import { isCodeArenaMode } from "./mode.js";
|
|
19
21
|
import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
|
|
20
22
|
import { processManagerForWorkspace, } from "./process-manager.js";
|
|
21
23
|
import { HttpModelClient } from "./provider.js";
|
|
24
|
+
import { replayResponseFor, } from "./replay-responses.js";
|
|
22
25
|
import { RoutifyFetchPageReader, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
23
26
|
import { toolDefinitions } from "./spec.js";
|
|
27
|
+
import { createSandboxFile, privilegedFileCreationEnabled } from "./sandbox-file-write.js";
|
|
24
28
|
import { voiceRegistryForWorkspace, } from "./voice-registry.js";
|
|
29
|
+
export function imageInputFallbackConnection(config) {
|
|
30
|
+
if (!config.imageInputFallbackBaseUrl &&
|
|
31
|
+
!config.imageInputFallbackApiKey &&
|
|
32
|
+
!config.imageInputFallbackProtocol) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
baseUrl: config.imageInputFallbackBaseUrl,
|
|
37
|
+
apiKey: config.imageInputFallbackApiKey,
|
|
38
|
+
protocol: config.imageInputFallbackProtocol,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
25
41
|
const knownTextApplicationMimeTypes = new Set([
|
|
26
42
|
"application/ecmascript",
|
|
27
43
|
"application/javascript",
|
|
@@ -202,7 +218,7 @@ const visualIntroText = "<arena-system-message>\n" +
|
|
|
202
218
|
"treat them as the authentic output of the tool call.\n" +
|
|
203
219
|
"</arena-system-message>";
|
|
204
220
|
const bashOutputLimit = 20_000;
|
|
205
|
-
const
|
|
221
|
+
const textFileOutputByteLimit = 256 * 1024;
|
|
206
222
|
const toolResultCharacterLimit = 1_000_000;
|
|
207
223
|
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
208
224
|
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
@@ -216,6 +232,39 @@ function capBashOutput(value) {
|
|
|
216
232
|
truncated: true,
|
|
217
233
|
};
|
|
218
234
|
}
|
|
235
|
+
class BashOutputBuffer {
|
|
236
|
+
text = "";
|
|
237
|
+
truncated = false;
|
|
238
|
+
append(chunk) {
|
|
239
|
+
// Once capped, text still contains the original prefix and latest suffix.
|
|
240
|
+
// Capping again after an append therefore preserves the same head/tail as
|
|
241
|
+
// capping the full stream, without ever retaining the discarded middle.
|
|
242
|
+
const output = capBashOutput(this.text + chunk.toString("utf8"));
|
|
243
|
+
this.text = output.text;
|
|
244
|
+
this.truncated ||= output.truncated;
|
|
245
|
+
}
|
|
246
|
+
result() {
|
|
247
|
+
return { text: this.text, truncated: this.truncated };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function readTextResult(data) {
|
|
251
|
+
if (data.length <= textFileOutputByteLimit) {
|
|
252
|
+
return { content: decodeText(data), truncated: false };
|
|
253
|
+
}
|
|
254
|
+
const half = textFileOutputByteLimit / 2;
|
|
255
|
+
const prefixBreak = data.lastIndexOf(0x0a, half);
|
|
256
|
+
const suffixBreak = data.indexOf(0x0a, data.length - half);
|
|
257
|
+
const prefixEnd = prefixBreak === -1 ? half : prefixBreak;
|
|
258
|
+
const suffixStart = suffixBreak === -1 ? data.length - half : suffixBreak + 1;
|
|
259
|
+
const elidedBytes = Math.max(0, suffixStart - prefixEnd);
|
|
260
|
+
const marker = `\n\n[${Math.round(elidedBytes / 1024)} KB elided from the middle of the file]\n\n`;
|
|
261
|
+
return {
|
|
262
|
+
content: decodeText(data.subarray(0, prefixEnd)) +
|
|
263
|
+
marker +
|
|
264
|
+
decodeText(data.subarray(suffixStart)),
|
|
265
|
+
truncated: true,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
219
268
|
const mimeTypes = {
|
|
220
269
|
".aac": "audio/aac",
|
|
221
270
|
".bmp": "image/bmp",
|
|
@@ -269,8 +318,7 @@ function decodeUtf32(data, littleEndian) {
|
|
|
269
318
|
const codePoint = littleEndian
|
|
270
319
|
? data.readUInt32LE(index)
|
|
271
320
|
: data.readUInt32BE(index);
|
|
272
|
-
characters.push(codePoint <= 0x10ffff &&
|
|
273
|
-
!(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
321
|
+
characters.push(codePoint <= 0x10ffff && !(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
274
322
|
? String.fromCodePoint(codePoint)
|
|
275
323
|
: "\ufffd");
|
|
276
324
|
}
|
|
@@ -377,12 +425,22 @@ function receivedType(value) {
|
|
|
377
425
|
return "array";
|
|
378
426
|
return typeof value;
|
|
379
427
|
}
|
|
428
|
+
function arenaEnumIssue(values, pathParts) {
|
|
429
|
+
return {
|
|
430
|
+
code: "invalid_value", values, path: pathParts,
|
|
431
|
+
message: `Invalid option: expected one of ${values.map((item) => JSON.stringify(item)).join("|")}`,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
380
434
|
function arenaValidationIssue(error, value) {
|
|
381
435
|
const pathParts = jsonPointerPath(error.instancePath);
|
|
382
436
|
if (error.keyword === "required") {
|
|
383
437
|
const missingProperty = String(error.params.missingProperty);
|
|
384
438
|
const parentSchema = error.parentSchema;
|
|
385
|
-
const
|
|
439
|
+
const missingSchema = parentSchema?.properties?.[missingProperty];
|
|
440
|
+
if (missingSchema?.enum) {
|
|
441
|
+
return arenaEnumIssue(missingSchema.enum, [...pathParts, missingProperty]);
|
|
442
|
+
}
|
|
443
|
+
const expected = zodTypeName(missingSchema?.type || "unknown");
|
|
386
444
|
return {
|
|
387
445
|
expected,
|
|
388
446
|
code: "invalid_type",
|
|
@@ -443,19 +501,11 @@ function arenaValidationIssue(error, value) {
|
|
|
443
501
|
};
|
|
444
502
|
}
|
|
445
503
|
if (error.keyword === "enum") {
|
|
446
|
-
|
|
447
|
-
return {
|
|
448
|
-
code: "invalid_value",
|
|
449
|
-
values,
|
|
450
|
-
path: pathParts,
|
|
451
|
-
message: `Invalid option: expected one of ${values
|
|
452
|
-
.map((item) => JSON.stringify(item))
|
|
453
|
-
.join("|")}`,
|
|
454
|
-
};
|
|
504
|
+
return arenaEnumIssue(error.params.allowedValues, pathParts);
|
|
455
505
|
}
|
|
456
506
|
if (error.keyword === "format") {
|
|
457
507
|
return {
|
|
458
|
-
origin: "string",
|
|
508
|
+
...(error.params.format === "uri" ? {} : { origin: "string" }),
|
|
459
509
|
code: "invalid_format",
|
|
460
510
|
format: error.params.format === "uri" ? "url" : error.params.format,
|
|
461
511
|
path: pathParts,
|
|
@@ -501,8 +551,8 @@ function arenaValidationIssues(error, value) {
|
|
|
501
551
|
}
|
|
502
552
|
return issues;
|
|
503
553
|
}
|
|
504
|
-
function arenaValidationError(toolName, value, errors) {
|
|
505
|
-
const issues = (errors || []).flatMap((error) => arenaValidationIssues(error, value));
|
|
554
|
+
function arenaValidationError(toolName, value, errors, customIssues = []) {
|
|
555
|
+
const issues = (errors || []).flatMap((error) => arenaValidationIssues(error, value)).concat(customIssues);
|
|
506
556
|
return (`Invalid input for tool ${toolName}: Type validation failed: Value: ` +
|
|
507
557
|
`${JSON.stringify(value)}.\nError message: ${JSON.stringify(issues, null, 2)}`);
|
|
508
558
|
}
|
|
@@ -548,12 +598,16 @@ class ArenaToolExecutor {
|
|
|
548
598
|
config;
|
|
549
599
|
workspace;
|
|
550
600
|
workspaceReal;
|
|
601
|
+
tmpReal;
|
|
602
|
+
varTmpReal;
|
|
551
603
|
validators = new Map();
|
|
604
|
+
connectors;
|
|
552
605
|
pendingVisualContent = new Map();
|
|
553
606
|
askUserHandler;
|
|
554
607
|
imageInputFallbackClient;
|
|
555
608
|
signal;
|
|
556
609
|
workspaceContext;
|
|
610
|
+
nativeSandboxPaths;
|
|
557
611
|
pageReader;
|
|
558
612
|
routifyPageReader;
|
|
559
613
|
processManager;
|
|
@@ -575,16 +629,30 @@ class ArenaToolExecutor {
|
|
|
575
629
|
this.workspace = path.resolve(config.workspace);
|
|
576
630
|
mkdirSync(this.workspace, { recursive: true });
|
|
577
631
|
this.workspaceReal = realpathSync(this.workspace);
|
|
632
|
+
// A Linux /home/user workspace is already an OS-level sandbox. Other
|
|
633
|
+
// callers still confine ordinary workspace paths, while /tmp and /var/tmp
|
|
634
|
+
// always refer to the actual system directories seen by Bash.
|
|
635
|
+
this.nativeSandboxPaths =
|
|
636
|
+
process.platform === "linux" && this.workspaceReal === "/home/user";
|
|
637
|
+
this.tmpReal = realpathSync("/tmp");
|
|
638
|
+
this.varTmpReal = realpathSync("/var/tmp");
|
|
578
639
|
this.processManager = processManagerForWorkspace(this.workspaceReal);
|
|
579
640
|
this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
|
|
580
641
|
this.askUserHandler = options.askUserHandler;
|
|
581
642
|
this.askUserEnabled = Boolean(options.askUserHandler);
|
|
582
643
|
if (config.imageInputFallbackModel?.trim()) {
|
|
644
|
+
const connection = imageInputFallbackConnection(config);
|
|
583
645
|
this.imageInputFallbackClient =
|
|
584
|
-
options.
|
|
646
|
+
options.imageInputFallbackClient ||
|
|
647
|
+
(connection
|
|
648
|
+
? new HttpModelClient(config, { connection })
|
|
649
|
+
: options.modelClient || new HttpModelClient(config));
|
|
585
650
|
}
|
|
586
651
|
this.signal = options.signal;
|
|
587
652
|
this.workspaceContext = options.workspaceContext;
|
|
653
|
+
if (config.connectors?.length && !config.disallowedTools?.includes("list_connector_tools")) {
|
|
654
|
+
this.connectors = new ConnectorTools(config.connectors, config.mergeConnectors, config.disallowedTools, options.signal);
|
|
655
|
+
}
|
|
588
656
|
this.pageReader = new FetchPageReader({ signal: options.signal });
|
|
589
657
|
this.routifyPageReader = new RoutifyFetchPageReader({
|
|
590
658
|
signal: options.signal,
|
|
@@ -597,7 +665,7 @@ class ArenaToolExecutor {
|
|
|
597
665
|
type: "string",
|
|
598
666
|
validate: (value) => URL.canParse(value),
|
|
599
667
|
});
|
|
600
|
-
for (const definition of toolDefinitions(this.workspaceContext)) {
|
|
668
|
+
for (const definition of toolDefinitions(this.workspaceContext, "arena", config.connectors)) {
|
|
601
669
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
602
670
|
}
|
|
603
671
|
}
|
|
@@ -605,13 +673,117 @@ class ArenaToolExecutor {
|
|
|
605
673
|
// canonical trace set, adjusted for the selected workspace runtime and minus
|
|
606
674
|
// ask_user when no handler is wired. add_voice stays available headlessly.
|
|
607
675
|
availableTools() {
|
|
608
|
-
let definitions =
|
|
676
|
+
let definitions = this.allDefinitions();
|
|
609
677
|
if (!this.askUserEnabled) {
|
|
610
678
|
definitions = definitions.filter((definition) => definition.function.name !== "ask_user");
|
|
611
679
|
}
|
|
612
680
|
const disallowed = new Set(this.config.disallowedTools || []);
|
|
613
681
|
return definitions.filter((definition) => !disallowed.has(definition.function.name));
|
|
614
682
|
}
|
|
683
|
+
allDefinitions() {
|
|
684
|
+
return [...toolDefinitions(this.workspaceContext, "arena", this.config.connectors),
|
|
685
|
+
...(this.connectors?.definitions() || [])];
|
|
686
|
+
}
|
|
687
|
+
async beginConnectorTurn(history) {
|
|
688
|
+
await this.connectors?.beginTurn(history);
|
|
689
|
+
}
|
|
690
|
+
async closeConnectors() { await this.connectors?.close(); }
|
|
691
|
+
parseArguments(call) {
|
|
692
|
+
const rawArguments = typeof call.function?.arguments === "string"
|
|
693
|
+
? call.function.arguments
|
|
694
|
+
: "{}";
|
|
695
|
+
let value;
|
|
696
|
+
try {
|
|
697
|
+
// Captured empty arguments validate as an empty object, while whitespace
|
|
698
|
+
// and other incomplete JSON still produce parsing errors.
|
|
699
|
+
value = JSON.parse(rawArguments === "" ? "{}" : rawArguments);
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
return { rawArguments, invalidJsonText: rawArguments, error };
|
|
703
|
+
}
|
|
704
|
+
const malformedArguments = value !== null && typeof value === "object" && !Array.isArray(value)
|
|
705
|
+
? value.__malformedToolArguments
|
|
706
|
+
: undefined;
|
|
707
|
+
if (typeof malformedArguments === "string") {
|
|
708
|
+
if (malformedArguments === "")
|
|
709
|
+
return { rawArguments, value: {} };
|
|
710
|
+
try {
|
|
711
|
+
JSON.parse(malformedArguments);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
return { rawArguments, invalidJsonText: malformedArguments, error };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return { rawArguments, value };
|
|
718
|
+
}
|
|
719
|
+
validateArguments(name, value, validate) {
|
|
720
|
+
const validatedArguments = structuredClone(value);
|
|
721
|
+
if (!validate(validatedArguments)) {
|
|
722
|
+
return {
|
|
723
|
+
validationMessage: this.connectors?.has(name) ? connectorFailureText : arenaValidationError(name, value, validate.errors),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (name === "ask_user") {
|
|
727
|
+
const issues = validatedArguments.questions.flatMap((question, index) => {
|
|
728
|
+
const ids = question.options.map((option) => option.id);
|
|
729
|
+
return new Set(ids).size === ids.length ? [] : [{
|
|
730
|
+
code: "custom", path: ["questions", index, "options"],
|
|
731
|
+
message: "Option IDs must be unique within a question",
|
|
732
|
+
}];
|
|
733
|
+
});
|
|
734
|
+
if (issues.length)
|
|
735
|
+
return { validationMessage: arenaValidationError(name, value, [], issues) };
|
|
736
|
+
}
|
|
737
|
+
return { value: validatedArguments };
|
|
738
|
+
}
|
|
739
|
+
inspect(call) {
|
|
740
|
+
const id = String(call.id || "");
|
|
741
|
+
const name = String(call.function?.name || "");
|
|
742
|
+
const originalRawArguments = typeof call.function?.arguments === "string"
|
|
743
|
+
? call.function.arguments
|
|
744
|
+
: "{}";
|
|
745
|
+
const parsed = this.parseArguments(call);
|
|
746
|
+
const validate = this.validators.get(name) || this.connectors?.validator(name);
|
|
747
|
+
if (this.config.disallowedTools?.includes(name) || !validate) {
|
|
748
|
+
return {
|
|
749
|
+
id,
|
|
750
|
+
name,
|
|
751
|
+
rawArguments: originalRawArguments,
|
|
752
|
+
...(parsed.error === undefined ? { arguments: parsed.value } : {}),
|
|
753
|
+
validation: "unknown_tool",
|
|
754
|
+
validationMessage: this.config.disallowedTools?.includes(name)
|
|
755
|
+
? `tool is disallowed: ${name}`
|
|
756
|
+
: this.connectors?.recognizes(name) ? connectorFailureText : unavailableToolError(name, this.allDefinitions().map((definition) => definition.function.name)),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (parsed.error !== undefined) {
|
|
760
|
+
return {
|
|
761
|
+
id,
|
|
762
|
+
name,
|
|
763
|
+
rawArguments: parsed.rawArguments,
|
|
764
|
+
validation: "invalid_json",
|
|
765
|
+
validationMessage: this.connectors?.has(name) ? connectorFailureText : arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
const validated = this.validateArguments(name, parsed.value, validate);
|
|
769
|
+
if (validated.validationMessage !== undefined) {
|
|
770
|
+
return {
|
|
771
|
+
id,
|
|
772
|
+
name,
|
|
773
|
+
rawArguments: parsed.rawArguments,
|
|
774
|
+
arguments: parsed.value,
|
|
775
|
+
validation: "invalid_arguments",
|
|
776
|
+
validationMessage: validated.validationMessage,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
id,
|
|
781
|
+
name,
|
|
782
|
+
rawArguments: parsed.rawArguments,
|
|
783
|
+
arguments: parsed.value,
|
|
784
|
+
validation: "valid",
|
|
785
|
+
};
|
|
786
|
+
}
|
|
615
787
|
async execute(call, context) {
|
|
616
788
|
const callId = String(call.id || "");
|
|
617
789
|
const name = String(call.function?.name || "");
|
|
@@ -629,53 +801,62 @@ class ArenaToolExecutor {
|
|
|
629
801
|
if (this.config.disallowedTools?.includes(name)) {
|
|
630
802
|
throw new Error(`tool is disallowed: ${name}`);
|
|
631
803
|
}
|
|
632
|
-
const validate = this.validators.get(name);
|
|
804
|
+
const validate = this.validators.get(name) || this.connectors?.validator(name);
|
|
633
805
|
if (!validate) {
|
|
634
806
|
return {
|
|
635
807
|
role: "tool",
|
|
636
808
|
tool_call_id: callId,
|
|
637
|
-
content: unavailableToolError(name,
|
|
809
|
+
content: this.connectors?.recognizes(name) ? connectorFailureText : unavailableToolError(name, this.allDefinitions().map((definition) => definition.function.name)),
|
|
638
810
|
};
|
|
639
811
|
}
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
: "{}";
|
|
643
|
-
let argumentsValue;
|
|
644
|
-
try {
|
|
645
|
-
argumentsValue = JSON.parse(argumentsText);
|
|
646
|
-
}
|
|
647
|
-
catch (error) {
|
|
812
|
+
const parsed = this.parseArguments(call);
|
|
813
|
+
if (parsed.error !== undefined) {
|
|
648
814
|
return {
|
|
649
815
|
role: "tool",
|
|
650
816
|
tool_call_id: callId,
|
|
651
|
-
content: arenaJsonParsingError(name,
|
|
817
|
+
content: this.connectors?.has(name) ? connectorFailureText : arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
|
|
652
818
|
};
|
|
653
819
|
}
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
820
|
+
const argumentsValue = parsed.value;
|
|
821
|
+
// Replay mode (CODE_HARNESS_REPLAY_RESPONSES set): serve recorded
|
|
822
|
+
// outputs BEFORE schema validation. Recorded arguments can violate the
|
|
823
|
+
// current schema — PII-redacted placeholder URLs fail format=url, or
|
|
824
|
+
// the schema evolved since recording (voice_identity vs voice_id) —
|
|
825
|
+
// and the recording must still replay verbatim. Canned lookup consumes
|
|
826
|
+
// the entry, so this must be the single serving point: dispatch() only
|
|
827
|
+
// sees calls without a recorded response. Unlisted calls fall through
|
|
828
|
+
// to validation and real execution.
|
|
829
|
+
const replayed = replayResponseFor(name, argumentsValue);
|
|
830
|
+
if (replayed) {
|
|
831
|
+
for (const artifact of replayed.artifacts ?? []) {
|
|
832
|
+
this.materializeReplayArtifact(artifact);
|
|
662
833
|
}
|
|
663
|
-
|
|
834
|
+
const replayContent = typeof replayed.output === "string"
|
|
835
|
+
? replayed.output
|
|
836
|
+
: json(replayed.output);
|
|
837
|
+
if (replayContent.length > toolResultCharacterLimit) {
|
|
664
838
|
return {
|
|
665
839
|
role: "tool",
|
|
666
840
|
tool_call_id: callId,
|
|
667
|
-
content:
|
|
841
|
+
content: json({
|
|
842
|
+
status: "error",
|
|
843
|
+
message: `Tool result from ${name} (${replayContent.length} characters) exceeds the ` +
|
|
844
|
+
`maximum allowed size (${toolResultCharacterLimit} characters). ` +
|
|
845
|
+
"Retry with narrower arguments or inspect the data in smaller portions.",
|
|
846
|
+
}),
|
|
668
847
|
};
|
|
669
848
|
}
|
|
849
|
+
return { role: "tool", tool_call_id: callId, content: replayContent };
|
|
670
850
|
}
|
|
671
|
-
const
|
|
672
|
-
if (
|
|
851
|
+
const validated = this.validateArguments(name, argumentsValue, validate);
|
|
852
|
+
if (validated.validationMessage !== undefined) {
|
|
673
853
|
return {
|
|
674
854
|
role: "tool",
|
|
675
855
|
tool_call_id: callId,
|
|
676
|
-
content:
|
|
856
|
+
content: validated.validationMessage,
|
|
677
857
|
};
|
|
678
858
|
}
|
|
859
|
+
const validatedArguments = validated.value;
|
|
679
860
|
const startedAt = Date.now();
|
|
680
861
|
let result;
|
|
681
862
|
try {
|
|
@@ -731,10 +912,9 @@ class ArenaToolExecutor {
|
|
|
731
912
|
const message = errorMessage(error);
|
|
732
913
|
const durationMs = Date.now() - startedAt;
|
|
733
914
|
if (name === "bash") {
|
|
734
|
-
const stderr = this.virtualizeBashOutput(message);
|
|
735
915
|
return {
|
|
736
916
|
status: "shell_error",
|
|
737
|
-
stderr,
|
|
917
|
+
stderr: message,
|
|
738
918
|
stdout: "",
|
|
739
919
|
exit_code: null,
|
|
740
920
|
duration_ms: durationMs,
|
|
@@ -820,6 +1000,13 @@ class ArenaToolExecutor {
|
|
|
820
1000
|
};
|
|
821
1001
|
}
|
|
822
1002
|
async dispatch(name, values, toolCallId, context) {
|
|
1003
|
+
if (name === "list_connector_tools" && this.connectors) {
|
|
1004
|
+
return this.connectors.discover(String(values.service));
|
|
1005
|
+
}
|
|
1006
|
+
if (this.connectors?.has(name))
|
|
1007
|
+
return this.connectors.call(name, values);
|
|
1008
|
+
// Canned replay responses are served in execute() before schema
|
|
1009
|
+
// validation (the lookup consumes entries — single serving point).
|
|
823
1010
|
switch (name) {
|
|
824
1011
|
case "bash":
|
|
825
1012
|
return this.bash(String(values.command), String(values.cwd ||
|
|
@@ -872,32 +1059,76 @@ class ArenaToolExecutor {
|
|
|
872
1059
|
throw new Error(`unknown tool: ${name}`);
|
|
873
1060
|
}
|
|
874
1061
|
}
|
|
1062
|
+
// Copy a pre-staged replay artifact blob into the workspace. Recorded
|
|
1063
|
+
// artifact paths must stay workspace-relative: an absolute path or one
|
|
1064
|
+
// escaping the workspace is a corrupt recording, so reject it loudly.
|
|
1065
|
+
materializeReplayArtifact(artifact) {
|
|
1066
|
+
const normalized = path.posix.normalize(artifact.path);
|
|
1067
|
+
if (path.posix.isAbsolute(artifact.path) ||
|
|
1068
|
+
path.posix.isAbsolute(normalized) ||
|
|
1069
|
+
normalized === ".." ||
|
|
1070
|
+
normalized.startsWith("../")) {
|
|
1071
|
+
throw new Error(`replay artifact path escapes the workspace: ${JSON.stringify(artifact.path)}`);
|
|
1072
|
+
}
|
|
1073
|
+
const target = path.resolve(this.workspace, normalized);
|
|
1074
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
1075
|
+
copyFileSync(artifact.file, target);
|
|
1076
|
+
}
|
|
875
1077
|
async confinedPath(raw) {
|
|
876
1078
|
if (!raw)
|
|
877
1079
|
throw new Error("path must not be empty");
|
|
878
1080
|
let candidate;
|
|
879
|
-
|
|
880
|
-
|
|
1081
|
+
let confinementRoot = this.workspace;
|
|
1082
|
+
let confinementRootReal = this.workspaceReal;
|
|
1083
|
+
const expanded = raw === "~"
|
|
1084
|
+
? "/home/user"
|
|
1085
|
+
: raw.startsWith("~/")
|
|
1086
|
+
? `/home/user/${raw.slice(2)}`
|
|
1087
|
+
: raw;
|
|
1088
|
+
const virtualAbsolute = path.posix.resolve("/home/user", expanded);
|
|
1089
|
+
let suffix;
|
|
1090
|
+
if (virtualAbsolute === "/var/tmp" ||
|
|
1091
|
+
virtualAbsolute.startsWith("/var/tmp/")) {
|
|
1092
|
+
confinementRoot = "/var/tmp";
|
|
1093
|
+
confinementRootReal = this.varTmpReal;
|
|
1094
|
+
suffix = virtualAbsolute.slice("/var/tmp".length).replace(/^\/+/, "");
|
|
1095
|
+
candidate = path.join(confinementRoot, suffix);
|
|
881
1096
|
}
|
|
882
|
-
else if (
|
|
883
|
-
|
|
1097
|
+
else if (virtualAbsolute === "/tmp" ||
|
|
1098
|
+
virtualAbsolute.startsWith("/tmp/")) {
|
|
1099
|
+
confinementRoot = "/tmp";
|
|
1100
|
+
confinementRootReal = this.tmpReal;
|
|
1101
|
+
suffix = virtualAbsolute.slice("/tmp".length).replace(/^\/+/, "");
|
|
1102
|
+
candidate = path.join(confinementRoot, suffix);
|
|
1103
|
+
}
|
|
1104
|
+
else if (this.nativeSandboxPaths && path.posix.isAbsolute(raw)) {
|
|
1105
|
+
confinementRoot = "/";
|
|
1106
|
+
confinementRootReal = "/";
|
|
1107
|
+
candidate = raw;
|
|
884
1108
|
}
|
|
885
|
-
else if (!this.workspaceContext
|
|
1109
|
+
else if (!this.workspaceContext) {
|
|
1110
|
+
if (virtualAbsolute === "/home/user" ||
|
|
1111
|
+
virtualAbsolute.startsWith("/home/user/")) {
|
|
1112
|
+
suffix = virtualAbsolute.slice("/home/user".length).replace(/^\/+/, "");
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
throw new Error(`path is outside the workspace: ${raw}`);
|
|
1116
|
+
}
|
|
1117
|
+
candidate = path.join(confinementRoot, suffix);
|
|
1118
|
+
}
|
|
1119
|
+
else if (raw === "/home/user") {
|
|
886
1120
|
candidate = this.workspace;
|
|
887
1121
|
}
|
|
888
|
-
else if (
|
|
889
|
-
candidate = path.join(this.workspace, raw.slice(
|
|
1122
|
+
else if (raw.startsWith("/home/user/")) {
|
|
1123
|
+
candidate = path.join(this.workspace, raw.slice("/home/user/".length));
|
|
890
1124
|
}
|
|
891
1125
|
else {
|
|
892
|
-
const defaultCwd = this.workspaceContext
|
|
893
|
-
? path.resolve(this.workspaceContext.cwd)
|
|
894
|
-
: this.workspace;
|
|
1126
|
+
const defaultCwd = path.resolve(this.workspaceContext.cwd);
|
|
895
1127
|
candidate = path.isAbsolute(raw) ? raw : path.join(defaultCwd, raw);
|
|
896
1128
|
}
|
|
897
1129
|
const absolute = path.resolve(candidate);
|
|
898
|
-
const lexicalRelative = path.relative(
|
|
899
|
-
if (lexicalRelative.startsWith("..") ||
|
|
900
|
-
path.isAbsolute(lexicalRelative)) {
|
|
1130
|
+
const lexicalRelative = path.relative(confinementRoot, absolute);
|
|
1131
|
+
if (lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
901
1132
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
902
1133
|
}
|
|
903
1134
|
let ancestor = absolute;
|
|
@@ -909,33 +1140,45 @@ class ArenaToolExecutor {
|
|
|
909
1140
|
}
|
|
910
1141
|
const ancestorReal = await realpath(ancestor);
|
|
911
1142
|
const resolved = path.resolve(ancestorReal, path.relative(ancestor, absolute));
|
|
912
|
-
const realRelative = path.relative(
|
|
1143
|
+
const realRelative = path.relative(confinementRootReal, resolved);
|
|
913
1144
|
if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) {
|
|
914
1145
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
915
1146
|
}
|
|
916
1147
|
return resolved;
|
|
917
1148
|
}
|
|
918
1149
|
relativePath(target) {
|
|
1150
|
+
for (const [root, virtualRoot] of [
|
|
1151
|
+
[this.varTmpReal, "/var/tmp"],
|
|
1152
|
+
[this.tmpReal, "/tmp"],
|
|
1153
|
+
]) {
|
|
1154
|
+
const relative = path.relative(root, target);
|
|
1155
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
1156
|
+
const suffix = relative.split(path.sep).join("/");
|
|
1157
|
+
return suffix ? `${virtualRoot}/${suffix}` : virtualRoot;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
919
1160
|
return path.relative(this.workspaceReal, target).split(path.sep).join("/");
|
|
920
1161
|
}
|
|
921
|
-
|
|
922
|
-
if (this.workspaceContext)
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1162
|
+
displayWorkspacePath(rawPath) {
|
|
1163
|
+
if (!this.workspaceContext) {
|
|
1164
|
+
if (rawPath === "/home/user" || rawPath === "~")
|
|
1165
|
+
return ".";
|
|
1166
|
+
if (rawPath.startsWith("/home/user/")) {
|
|
1167
|
+
return rawPath.slice("/home/user/".length);
|
|
1168
|
+
}
|
|
1169
|
+
if (rawPath.startsWith("~/"))
|
|
1170
|
+
return rawPath.slice(2);
|
|
930
1171
|
}
|
|
931
|
-
return
|
|
1172
|
+
return rawPath;
|
|
932
1173
|
}
|
|
933
1174
|
sandboxEnvironment() {
|
|
934
1175
|
const env = {
|
|
935
1176
|
...process.env,
|
|
936
1177
|
ARENA_WORKSPACE: this.workspace,
|
|
937
|
-
|
|
938
|
-
|
|
1178
|
+
// Agent Arena captures use C character classification (grep -P, wc -w).
|
|
1179
|
+
// This controls command behavior, not the UTF-8 encoding of stored files.
|
|
1180
|
+
LANG: "C",
|
|
1181
|
+
LC_ALL: "C",
|
|
939
1182
|
};
|
|
940
1183
|
delete env.FORCE_COLOR;
|
|
941
1184
|
delete env.CLICOLOR_FORCE;
|
|
@@ -943,28 +1186,30 @@ class ArenaToolExecutor {
|
|
|
943
1186
|
}
|
|
944
1187
|
async bash(command, cwd, timeoutSeconds) {
|
|
945
1188
|
const workdir = await this.confinedPath(cwd);
|
|
946
|
-
|
|
1189
|
+
const metadata = await stat(workdir).catch((error) => {
|
|
1190
|
+
// A missing cwd prevents shell startup; captured Bash stderr is empty.
|
|
1191
|
+
// Keep other filesystem failures and start_process diagnostics intact.
|
|
1192
|
+
if (error.code === "ENOENT")
|
|
1193
|
+
throw new Error("");
|
|
1194
|
+
throw error;
|
|
1195
|
+
});
|
|
1196
|
+
if (!metadata.isDirectory()) {
|
|
947
1197
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
948
1198
|
}
|
|
949
|
-
// Legacy callers without runtime context retain the captured /home/user
|
|
950
|
-
// contract. Runtime-aware callers execute and report their actual paths.
|
|
951
|
-
const translatedCommand = this.workspaceContext
|
|
952
|
-
? command
|
|
953
|
-
: command.replaceAll("/home/user", "\"$ARENA_WORKSPACE\"");
|
|
954
1199
|
return new Promise((resolve, reject) => {
|
|
955
1200
|
const env = this.sandboxEnvironment();
|
|
956
1201
|
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
957
1202
|
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
958
1203
|
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
959
|
-
const child = spawn("/bin/bash", ["-lc",
|
|
1204
|
+
const child = spawn("/bin/bash", ["-lc", command], {
|
|
960
1205
|
cwd: workdir,
|
|
961
1206
|
env,
|
|
962
1207
|
stdio: ["ignore", "pipe", "pipe"],
|
|
963
1208
|
detached: process.platform !== "win32",
|
|
964
1209
|
});
|
|
965
1210
|
const startedAt = Date.now();
|
|
966
|
-
|
|
967
|
-
|
|
1211
|
+
const stdout = new BashOutputBuffer();
|
|
1212
|
+
const stderr = new BashOutputBuffer();
|
|
968
1213
|
let timedOut = false;
|
|
969
1214
|
let settled = false;
|
|
970
1215
|
let forceSettle;
|
|
@@ -1001,8 +1246,8 @@ class ArenaToolExecutor {
|
|
|
1001
1246
|
reject(this.signal.reason || new Error("operation cancelled"));
|
|
1002
1247
|
return;
|
|
1003
1248
|
}
|
|
1004
|
-
const out =
|
|
1005
|
-
const err =
|
|
1249
|
+
const out = stdout.result();
|
|
1250
|
+
const err = stderr.result();
|
|
1006
1251
|
resolve({
|
|
1007
1252
|
status: timedOut ? "timeout" : signal ? "killed" : "completed",
|
|
1008
1253
|
stderr: err.text,
|
|
@@ -1032,10 +1277,10 @@ class ArenaToolExecutor {
|
|
|
1032
1277
|
};
|
|
1033
1278
|
this.signal?.addEventListener("abort", abort, { once: true });
|
|
1034
1279
|
child.stdout.on("data", (chunk) => {
|
|
1035
|
-
stdout
|
|
1280
|
+
stdout.append(chunk);
|
|
1036
1281
|
});
|
|
1037
1282
|
child.stderr.on("data", (chunk) => {
|
|
1038
|
-
stderr
|
|
1283
|
+
stderr.append(chunk);
|
|
1039
1284
|
});
|
|
1040
1285
|
child.once("error", (error) => {
|
|
1041
1286
|
if (settled)
|
|
@@ -1055,12 +1300,9 @@ class ArenaToolExecutor {
|
|
|
1055
1300
|
if (!(await stat(workdir)).isDirectory()) {
|
|
1056
1301
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
1057
1302
|
}
|
|
1058
|
-
const translatedCommand = this.workspaceContext
|
|
1059
|
-
? command
|
|
1060
|
-
: command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
|
|
1061
1303
|
return this.processManager.start({
|
|
1062
1304
|
name,
|
|
1063
|
-
command
|
|
1305
|
+
command,
|
|
1064
1306
|
cwd: workdir,
|
|
1065
1307
|
startupWaitSeconds,
|
|
1066
1308
|
env: this.sandboxEnvironment(),
|
|
@@ -1069,10 +1311,12 @@ class ArenaToolExecutor {
|
|
|
1069
1311
|
}
|
|
1070
1312
|
async askUser(questions, toolCallId, context) {
|
|
1071
1313
|
if (this.askUserHandler) {
|
|
1072
|
-
return this.askUserHandler(toolCallId, questions, context
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1314
|
+
return this.askUserHandler(toolCallId, questions, context
|
|
1315
|
+
? {
|
|
1316
|
+
...context,
|
|
1317
|
+
signal: this.signal || context.signal,
|
|
1318
|
+
}
|
|
1319
|
+
: undefined);
|
|
1076
1320
|
}
|
|
1077
1321
|
// ask_user is not offered in non-interactive sessions; if the model still
|
|
1078
1322
|
// emits it (e.g. resumed history or a hallucinated call), report an error
|
|
@@ -1152,23 +1396,25 @@ class ArenaToolExecutor {
|
|
|
1152
1396
|
catch {
|
|
1153
1397
|
return {
|
|
1154
1398
|
status: "error",
|
|
1155
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1399
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1156
1400
|
};
|
|
1157
1401
|
}
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if (!source.includes(oldText)) {
|
|
1402
|
+
// Agent Arena replaces the first match and tolerates multiline whitespace
|
|
1403
|
+
// drift. Code Arena deliberately uses a separate unique-match policy.
|
|
1404
|
+
const edited = replaceFirstText(source, oldText, newText, {
|
|
1405
|
+
whitespaceInsensitiveMultiline: true,
|
|
1406
|
+
});
|
|
1407
|
+
if (edited === undefined) {
|
|
1165
1408
|
return {
|
|
1166
1409
|
status: "error",
|
|
1167
1410
|
message: "Context not found. Read the file to verify the text exists.",
|
|
1168
1411
|
};
|
|
1169
1412
|
}
|
|
1170
1413
|
try {
|
|
1171
|
-
|
|
1414
|
+
// Replacement text is literal. String.replace(string, string) expands
|
|
1415
|
+
// $&, $`, $' and $$ sequences and therefore corrupts JavaScript/jQuery
|
|
1416
|
+
// snippets observed in production trajectories.
|
|
1417
|
+
await writeFile(target, edited, "utf8");
|
|
1172
1418
|
}
|
|
1173
1419
|
catch {
|
|
1174
1420
|
return {
|
|
@@ -1179,10 +1425,11 @@ class ArenaToolExecutor {
|
|
|
1179
1425
|
return { status: "success", message: `Edited ${rawPath}.` };
|
|
1180
1426
|
}
|
|
1181
1427
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
1182
|
-
|
|
1428
|
+
let target;
|
|
1183
1429
|
let fileSize;
|
|
1184
1430
|
let sample;
|
|
1185
1431
|
try {
|
|
1432
|
+
target = await this.confinedPath(rawPath);
|
|
1186
1433
|
const metadata = await stat(target);
|
|
1187
1434
|
if (!metadata.isFile())
|
|
1188
1435
|
throw new Error("not a regular file");
|
|
@@ -1199,23 +1446,13 @@ class ArenaToolExecutor {
|
|
|
1199
1446
|
}
|
|
1200
1447
|
}
|
|
1201
1448
|
catch {
|
|
1202
|
-
// 逐字对齐生产错误文案,路径按调用方原样回显。
|
|
1203
1449
|
return {
|
|
1204
1450
|
status: "error",
|
|
1205
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1451
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1206
1452
|
};
|
|
1207
1453
|
}
|
|
1208
1454
|
const extension = path.extname(target).toLowerCase();
|
|
1209
1455
|
const kind = workspaceFileKind(target, sample);
|
|
1210
|
-
if (kind === "text" && fileSize > textFileByteLimit) {
|
|
1211
|
-
return {
|
|
1212
|
-
status: "error",
|
|
1213
|
-
message: `File content (${displayByteSize(fileSize)}) exceeds the maximum allowed size ` +
|
|
1214
|
-
`(${displayByteSize(textFileByteLimit)}). Use bash with sed/head/tail to read ` +
|
|
1215
|
-
"specific portions of the file, or grep/rg to search for specific content " +
|
|
1216
|
-
"instead of reading the whole file.",
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
1456
|
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
1220
1457
|
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
1221
1458
|
}
|
|
@@ -1242,7 +1479,7 @@ class ArenaToolExecutor {
|
|
|
1242
1479
|
catch {
|
|
1243
1480
|
return {
|
|
1244
1481
|
status: "error",
|
|
1245
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1482
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1246
1483
|
};
|
|
1247
1484
|
}
|
|
1248
1485
|
if (isStructuredDocument(target)) {
|
|
@@ -1270,12 +1507,13 @@ class ArenaToolExecutor {
|
|
|
1270
1507
|
return result;
|
|
1271
1508
|
}
|
|
1272
1509
|
if (kind === "text") {
|
|
1273
|
-
const
|
|
1510
|
+
const text = readTextResult(data);
|
|
1274
1511
|
return {
|
|
1275
1512
|
kind: "text",
|
|
1276
|
-
lines:
|
|
1277
|
-
size:
|
|
1278
|
-
content,
|
|
1513
|
+
lines: data.reduce((count, byte) => count + (byte === 0x0a ? 1 : 0), 1),
|
|
1514
|
+
size: data.length,
|
|
1515
|
+
content: text.content,
|
|
1516
|
+
...(text.truncated ? { truncated: true } : {}),
|
|
1279
1517
|
};
|
|
1280
1518
|
}
|
|
1281
1519
|
const mimeType = mime.getType(target) ||
|
|
@@ -1325,11 +1563,22 @@ class ArenaToolExecutor {
|
|
|
1325
1563
|
};
|
|
1326
1564
|
}
|
|
1327
1565
|
async writeWorkspaceFile(rawPath, content) {
|
|
1328
|
-
let target;
|
|
1329
1566
|
try {
|
|
1330
|
-
target = await this.confinedPath(rawPath);
|
|
1567
|
+
const target = await this.confinedPath(rawPath);
|
|
1331
1568
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1332
|
-
|
|
1569
|
+
try {
|
|
1570
|
+
await writeFile(target, content, "utf8");
|
|
1571
|
+
}
|
|
1572
|
+
catch (error) {
|
|
1573
|
+
const code = error.code;
|
|
1574
|
+
if (!["EACCES", "EPERM"].includes(code || "") || !privilegedFileCreationEnabled({
|
|
1575
|
+
platform: process.platform, workspace: this.workspaceReal,
|
|
1576
|
+
docker: existsSync("/.dockerenv"), flag: process.env.ARENA_SANDBOX_PRIVILEGED_FILE_CREATION,
|
|
1577
|
+
uid: process.getuid?.(),
|
|
1578
|
+
}))
|
|
1579
|
+
throw error;
|
|
1580
|
+
await createSandboxFile(target, content);
|
|
1581
|
+
}
|
|
1333
1582
|
}
|
|
1334
1583
|
catch {
|
|
1335
1584
|
return {
|
|
@@ -1352,18 +1601,21 @@ class ArenaToolExecutor {
|
|
|
1352
1601
|
const slug = query
|
|
1353
1602
|
.toLowerCase()
|
|
1354
1603
|
.replace(/[^a-z0-9]+/g, "-")
|
|
1355
|
-
.
|
|
1604
|
+
.replace(/^-+|-+$/g, "")
|
|
1605
|
+
.slice(0, 40) || "results";
|
|
1356
1606
|
for (const value of results) {
|
|
1357
1607
|
if (saved.length >= count)
|
|
1358
1608
|
break;
|
|
1359
1609
|
const item = record(value);
|
|
1360
1610
|
const properties = record(item.properties);
|
|
1361
1611
|
const thumbnail = record(item.thumbnail);
|
|
1362
|
-
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1612
|
+
// Brave's result URL is the source page; properties.url is an image asset.
|
|
1613
|
+
const sourceUrl = stringValue(item.url);
|
|
1614
|
+
const imageUrl = stringValue(properties.url) || stringValue(thumbnail.src);
|
|
1615
|
+
const thumbnailUrl = stringValue(thumbnail.src) || imageUrl;
|
|
1616
|
+
if (!imageUrl)
|
|
1365
1617
|
continue;
|
|
1366
|
-
const downloadUrls = [...new Set([
|
|
1618
|
+
const downloadUrls = [...new Set([imageUrl, thumbnailUrl])];
|
|
1367
1619
|
for (const downloadUrl of downloadUrls) {
|
|
1368
1620
|
try {
|
|
1369
1621
|
const imageResponse = await fetch(downloadUrl, {
|
|
@@ -1378,7 +1630,9 @@ class ArenaToolExecutor {
|
|
|
1378
1630
|
.split(";")[0]
|
|
1379
1631
|
?.trim()
|
|
1380
1632
|
.toLowerCase();
|
|
1381
|
-
const extension =
|
|
1633
|
+
const extension = normalizedType === "image/jpeg"
|
|
1634
|
+
? ".jpg"
|
|
1635
|
+
: Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
|
|
1382
1636
|
const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${extension}`);
|
|
1383
1637
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1384
1638
|
await writeFile(target, await responseBytes(imageResponse));
|
|
@@ -1418,13 +1672,19 @@ class ArenaToolExecutor {
|
|
|
1418
1672
|
}
|
|
1419
1673
|
}
|
|
1420
1674
|
async generateImage(rawPath, prompt, images) {
|
|
1675
|
+
if (!rawPath) {
|
|
1676
|
+
return { status: "error", message: "File path cannot be empty." };
|
|
1677
|
+
}
|
|
1421
1678
|
const target = await this.confinedPath(rawPath);
|
|
1422
1679
|
const extension = path.extname(target).toLowerCase();
|
|
1423
1680
|
if (![".jpg", ".jpeg", ".png"].includes(extension)) {
|
|
1424
|
-
|
|
1681
|
+
return {
|
|
1682
|
+
status: "error",
|
|
1683
|
+
message: "File path must end in .jpg, .jpeg, or .png.",
|
|
1684
|
+
};
|
|
1425
1685
|
}
|
|
1426
1686
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1427
|
-
const model = process.env.ARENA_IMAGE_MODEL || "gemini-
|
|
1687
|
+
const model = process.env.ARENA_IMAGE_MODEL || "gemini-3.1-flash-image";
|
|
1428
1688
|
const references = [];
|
|
1429
1689
|
for (const imagePath of images || []) {
|
|
1430
1690
|
const source = await this.confinedPath(imagePath);
|
|
@@ -1459,7 +1719,7 @@ class ArenaToolExecutor {
|
|
|
1459
1719
|
await writeFile(target, await generatedImageBytesForExtension(generated.bytes, generated.mimeType, extension));
|
|
1460
1720
|
return {
|
|
1461
1721
|
status: "success",
|
|
1462
|
-
file_path:
|
|
1722
|
+
file_path: rawPath,
|
|
1463
1723
|
};
|
|
1464
1724
|
}
|
|
1465
1725
|
async generateSpeech(rawPath, text, voiceId, language) {
|
|
@@ -1472,7 +1732,7 @@ class ArenaToolExecutor {
|
|
|
1472
1732
|
".opus": "opus",
|
|
1473
1733
|
}[extension];
|
|
1474
1734
|
if (!format) {
|
|
1475
|
-
throw new Error("ModelRouter
|
|
1735
|
+
throw new Error("ModelRouter TTS output supports .mp3, .wav, .ogg, or .opus");
|
|
1476
1736
|
}
|
|
1477
1737
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1478
1738
|
const registered = this.voiceRegistry.resolve(voiceId);
|
|
@@ -1488,14 +1748,15 @@ class ArenaToolExecutor {
|
|
|
1488
1748
|
const voice = process.env.ARENA_SPEECH_VOICE || registered.providerVoice;
|
|
1489
1749
|
await writeFile(target, await generateSpeechWithModelRouter({
|
|
1490
1750
|
format,
|
|
1491
|
-
model:
|
|
1751
|
+
model: registered.model,
|
|
1752
|
+
language: language || registered.language,
|
|
1492
1753
|
signal: this.signal,
|
|
1493
1754
|
text,
|
|
1494
1755
|
voice,
|
|
1495
1756
|
}));
|
|
1496
1757
|
return {
|
|
1497
1758
|
status: "success",
|
|
1498
|
-
file_path:
|
|
1759
|
+
file_path: rawPath,
|
|
1499
1760
|
};
|
|
1500
1761
|
}
|
|
1501
1762
|
}
|
|
@@ -1520,6 +1781,17 @@ export class ToolExecutor {
|
|
|
1520
1781
|
availableTools() {
|
|
1521
1782
|
return this.implementation.availableTools();
|
|
1522
1783
|
}
|
|
1784
|
+
async beginConnectorTurn(history) {
|
|
1785
|
+
if (this.implementation instanceof ArenaToolExecutor)
|
|
1786
|
+
await this.implementation.beginConnectorTurn(history);
|
|
1787
|
+
}
|
|
1788
|
+
async closeConnectors() {
|
|
1789
|
+
if (this.implementation instanceof ArenaToolExecutor)
|
|
1790
|
+
await this.implementation.closeConnectors();
|
|
1791
|
+
}
|
|
1792
|
+
inspect(call) {
|
|
1793
|
+
return this.implementation.inspect(call);
|
|
1794
|
+
}
|
|
1523
1795
|
execute(call, context) {
|
|
1524
1796
|
return this.implementation.execute(call, context);
|
|
1525
1797
|
}
|