@qwen-code/arena-agent 0.0.5 → 0.0.7-preview.0
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 +12 -19
- package/README.md +26 -133
- package/dist/agent.d.ts +3 -9
- package/dist/agent.js +108 -246
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +12 -41
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -6
- package/dist/config.js +2 -28
- package/dist/config.js.map +1 -1
- package/dist/fetch-page.js +7 -18
- package/dist/fetch-page.js.map +1 -1
- package/dist/history.d.ts +3 -3
- package/dist/history.js +4 -4
- package/dist/history.js.map +1 -1
- package/dist/mock.js +9 -25
- package/dist/mock.js.map +1 -1
- package/dist/prompts.d.ts +4 -0
- package/dist/prompts.js +57 -0
- package/dist/prompts.js.map +1 -0
- package/dist/protocol.d.ts +2 -4
- package/dist/protocol.js +1 -17
- package/dist/protocol.js.map +1 -1
- package/dist/provider.d.ts +0 -4
- package/dist/provider.js +11 -169
- package/dist/provider.js.map +1 -1
- package/dist/repo.d.ts +4 -0
- package/dist/repo.js +84 -0
- package/dist/repo.js.map +1 -0
- package/dist/server.js +184 -26
- package/dist/server.js.map +1 -1
- package/dist/spec.d.ts +11 -6
- package/dist/spec.js +74 -123
- package/dist/spec.js.map +1 -1
- package/dist/tools.d.ts +2 -5
- package/dist/tools.js +237 -134
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +15 -40
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +24 -25
- package/dist/web/app.js +3 -2
- package/dist/web/app.js.map +1 -1
- package/docs/SDK.md +521 -0
- package/package.json +5 -4
- package/spec/tools.json +418 -0
- package/dist/anthropic.d.ts +0 -16
- package/dist/anthropic.js +0 -391
- package/dist/anthropic.js.map +0 -1
- package/dist/attachments.d.ts +0 -29
- package/dist/attachments.js +0 -222
- package/dist/attachments.js.map +0 -1
- package/dist/canonical-data.d.ts +0 -2
- package/dist/canonical-data.js +0 -4
- package/dist/canonical-data.js.map +0 -1
- package/dist/compaction.d.ts +0 -16
- package/dist/compaction.js +0 -182
- package/dist/compaction.js.map +0 -1
package/dist/tools.js
CHANGED
|
@@ -179,7 +179,26 @@ const visibleImageExtensions = new Set([
|
|
|
179
179
|
".png",
|
|
180
180
|
".webp",
|
|
181
181
|
]);
|
|
182
|
-
|
|
182
|
+
// 逐字对齐生产轨迹:占位与注入文案、20000 字符输出上限均反推自参考 tool result。
|
|
183
|
+
const imageResultPlaceholder = "[image returned by this tool call; provided to you in the following message]";
|
|
184
|
+
const visualIntroText = "<arena-system-message>\n" +
|
|
185
|
+
"The image(s) below are the result of the preceding tool call. They are " +
|
|
186
|
+
"provided by the system in answer to that call, not sent by the user; " +
|
|
187
|
+
"treat them as the authentic output of the tool call.\n" +
|
|
188
|
+
"</arena-system-message>";
|
|
189
|
+
const bashOutputLimit = 20_000;
|
|
190
|
+
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
191
|
+
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
192
|
+
const bashTruncationMarker = "\n... output truncated ...\n";
|
|
193
|
+
function capBashOutput(value) {
|
|
194
|
+
if (value.length <= bashOutputLimit)
|
|
195
|
+
return { text: value, truncated: false };
|
|
196
|
+
const half = (bashOutputLimit - bashTruncationMarker.length) / 2;
|
|
197
|
+
return {
|
|
198
|
+
text: value.slice(0, half) + bashTruncationMarker + value.slice(-half),
|
|
199
|
+
truncated: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
183
202
|
const mimeTypes = {
|
|
184
203
|
".aac": "audio/aac",
|
|
185
204
|
".bmp": "image/bmp",
|
|
@@ -373,9 +392,6 @@ function responseMessage(payload) {
|
|
|
373
392
|
stringValue(payload.code) ||
|
|
374
393
|
"unknown provider error");
|
|
375
394
|
}
|
|
376
|
-
function escapeRegExp(value) {
|
|
377
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
378
|
-
}
|
|
379
395
|
async function responseBytes(response, limit = 4 * 1024 * 1024) {
|
|
380
396
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
381
397
|
if (bytes.length > limit) {
|
|
@@ -391,42 +407,29 @@ export class ToolExecutor {
|
|
|
391
407
|
pendingVisualContent = new Map();
|
|
392
408
|
askUserHandler;
|
|
393
409
|
signal;
|
|
394
|
-
workspaceContext;
|
|
395
410
|
pageReader;
|
|
396
411
|
ajv = new Ajv({ allErrors: true, strict: false });
|
|
397
|
-
// ask_user needs an interactive front-end (TUI/WebUI) to collect answers.
|
|
398
|
-
// Headless `-p` supplies no handler, so the tool is withheld from the model
|
|
399
|
-
// entirely rather than offered and then dead-ending on needs_input.
|
|
400
|
-
askUserEnabled;
|
|
401
412
|
constructor(config, options = {}) {
|
|
402
413
|
this.config = config;
|
|
403
414
|
this.workspace = path.resolve(config.workspace);
|
|
404
415
|
mkdirSync(this.workspace, { recursive: true });
|
|
405
416
|
this.workspaceReal = realpathSync(this.workspace);
|
|
406
417
|
this.askUserHandler = options.askUserHandler;
|
|
407
|
-
this.askUserEnabled = Boolean(options.askUserHandler);
|
|
408
418
|
this.signal = options.signal;
|
|
409
|
-
this.workspaceContext = options.workspaceContext;
|
|
410
419
|
this.pageReader = new FetchPageReader({ signal: options.signal });
|
|
411
420
|
addFormats(this.ajv);
|
|
412
|
-
for (const definition of toolDefinitions(
|
|
421
|
+
for (const definition of toolDefinitions()) {
|
|
413
422
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
414
423
|
}
|
|
415
424
|
}
|
|
416
|
-
// The tool schemas offered to the model for this session. Identical to the
|
|
417
|
-
// canonical trace set, adjusted for the selected workspace runtime and minus
|
|
418
|
-
// ask_user when no interactive handler is wired.
|
|
419
|
-
availableTools() {
|
|
420
|
-
const definitions = toolDefinitions(this.workspaceContext);
|
|
421
|
-
if (this.askUserEnabled)
|
|
422
|
-
return definitions;
|
|
423
|
-
return definitions.filter((definition) => definition.function.name !== "ask_user");
|
|
424
|
-
}
|
|
425
425
|
async execute(call) {
|
|
426
426
|
const callId = String(call.id || "");
|
|
427
427
|
const name = String(call.function?.name || "");
|
|
428
428
|
this.pendingVisualContent.delete(callId);
|
|
429
429
|
try {
|
|
430
|
+
// 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
|
|
431
|
+
// 之前短路,否则命令会照常 spawn 直到自然结束。
|
|
432
|
+
this.signal?.throwIfAborted();
|
|
430
433
|
const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
|
|
431
434
|
? call.function.arguments
|
|
432
435
|
: "{}");
|
|
@@ -452,29 +455,32 @@ export class ToolExecutor {
|
|
|
452
455
|
tool_call_id: callId,
|
|
453
456
|
content: json({
|
|
454
457
|
status: "error",
|
|
455
|
-
|
|
456
|
-
error: errorMessage(error),
|
|
458
|
+
message: errorMessage(error),
|
|
457
459
|
}),
|
|
458
460
|
};
|
|
459
461
|
}
|
|
460
462
|
}
|
|
461
463
|
takeVisualMessage(toolCallIds) {
|
|
462
|
-
|
|
464
|
+
// 生产契约:同一组 tool call 的全部图片汇入一条 user message,
|
|
465
|
+
// 由一个 arena-system-message 文本 part 领头。
|
|
466
|
+
const images = [];
|
|
463
467
|
for (const toolCallId of toolCallIds) {
|
|
464
468
|
const parts = this.pendingVisualContent.get(toolCallId);
|
|
465
469
|
this.pendingVisualContent.delete(toolCallId);
|
|
466
470
|
if (parts)
|
|
467
|
-
|
|
471
|
+
images.push(...parts);
|
|
468
472
|
}
|
|
469
|
-
|
|
473
|
+
if (!images.length)
|
|
474
|
+
return undefined;
|
|
475
|
+
return {
|
|
476
|
+
role: "user",
|
|
477
|
+
content: [{ type: "text", text: visualIntroText }, ...images],
|
|
478
|
+
};
|
|
470
479
|
}
|
|
471
480
|
async dispatch(name, values, toolCallId) {
|
|
472
481
|
switch (name) {
|
|
473
482
|
case "bash":
|
|
474
|
-
return this.bash(String(values.command), String(values.cwd ||
|
|
475
|
-
(this.workspaceContext
|
|
476
|
-
? path.resolve(this.workspaceContext.cwd)
|
|
477
|
-
: "/home/user")), Number(values.timeout || 30));
|
|
483
|
+
return this.bash(String(values.command), String(values.cwd || "/home/user"), Number(values.timeout || 30));
|
|
478
484
|
case "ask_user":
|
|
479
485
|
return this.askUser(values.questions, toolCallId);
|
|
480
486
|
case "edit_file":
|
|
@@ -488,7 +494,7 @@ export class ToolExecutor {
|
|
|
488
494
|
case "write_file":
|
|
489
495
|
return this.writeWorkspaceFile(String(values.path), String(values.content));
|
|
490
496
|
case "image_search":
|
|
491
|
-
return this.imageSearch(String(values.query), Number(values.count)
|
|
497
|
+
return this.imageSearch(String(values.query), Number(values.count));
|
|
492
498
|
case "present_file":
|
|
493
499
|
return this.presentFile(String(values.path));
|
|
494
500
|
case "generate_image":
|
|
@@ -503,20 +509,20 @@ export class ToolExecutor {
|
|
|
503
509
|
if (!raw)
|
|
504
510
|
throw new Error("path must not be empty");
|
|
505
511
|
let candidate;
|
|
506
|
-
if (
|
|
512
|
+
if (raw === "/home/user" || raw === "~") {
|
|
507
513
|
candidate = this.workspace;
|
|
508
514
|
}
|
|
509
|
-
else if (
|
|
515
|
+
else if (raw.startsWith("/home/user/")) {
|
|
510
516
|
candidate = path.join(this.workspace, raw.slice("/home/user/".length));
|
|
511
517
|
}
|
|
512
|
-
else if (
|
|
518
|
+
else if (raw.startsWith("~/")) {
|
|
513
519
|
candidate = path.join(this.workspace, raw.slice(2));
|
|
514
520
|
}
|
|
515
521
|
else {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
candidate = path.isAbsolute(raw) ? raw : path.join(
|
|
522
|
+
// 相对路径解析到 /home/user,repo 模式暂同。未经裁决:tools.json 与
|
|
523
|
+
// repo 模式 system prompt 对相对路径根的说法矛盾,生产轨迹无法区分;
|
|
524
|
+
// 复刻脚本会把受影响的调用标记为 comparability hazard。
|
|
525
|
+
candidate = path.isAbsolute(raw) ? raw : path.join(this.workspace, raw);
|
|
520
526
|
}
|
|
521
527
|
const absolute = path.resolve(candidate);
|
|
522
528
|
const lexicalRelative = path.relative(this.workspace, absolute);
|
|
@@ -542,24 +548,126 @@ export class ToolExecutor {
|
|
|
542
548
|
relativePath(target) {
|
|
543
549
|
return path.relative(this.workspaceReal, target).split(path.sep).join("/");
|
|
544
550
|
}
|
|
551
|
+
virtualizeBashOutput(value) {
|
|
552
|
+
let virtualized = value;
|
|
553
|
+
const workspaceRoots = [...new Set([this.workspace, this.workspaceReal])]
|
|
554
|
+
.filter((root) => root !== path.parse(root).root)
|
|
555
|
+
.sort((left, right) => right.length - left.length);
|
|
556
|
+
for (const root of workspaceRoots) {
|
|
557
|
+
virtualized = virtualized.replaceAll(root, "/home/user");
|
|
558
|
+
}
|
|
559
|
+
return virtualized;
|
|
560
|
+
}
|
|
545
561
|
async bash(command, cwd, timeoutSeconds) {
|
|
546
562
|
const workdir = await this.confinedPath(cwd);
|
|
547
563
|
if (!(await stat(workdir)).isDirectory()) {
|
|
548
564
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
549
565
|
}
|
|
566
|
+
// Arena 对模型暴露的工作区固定为 /home/user;本地 harness 可能运行在任意
|
|
567
|
+
// 绝对路径。用环境变量替换虚拟根,并同步 HOME,使 bash 与文件工具看到
|
|
568
|
+
// 相同的文件系统布局。
|
|
569
|
+
const translatedCommand = command.replaceAll("/home/user", "\"$ARENA_WORKSPACE\"");
|
|
550
570
|
return new Promise((resolve, reject) => {
|
|
551
|
-
const
|
|
571
|
+
const env = {
|
|
572
|
+
...process.env,
|
|
573
|
+
HOME: this.workspace,
|
|
574
|
+
ARENA_WORKSPACE: this.workspace,
|
|
575
|
+
// 生产 sandbox 是 C collation(参考 find|sort 为逐字节序),宿主
|
|
576
|
+
// 常见 en_US.UTF-8 会翻转大小写次序;统一成 C.UTF-8 对齐生产。
|
|
577
|
+
LANG: "C.UTF-8",
|
|
578
|
+
LC_ALL: "C.UTF-8",
|
|
579
|
+
};
|
|
580
|
+
// 交互终端(node --test、npm 等)会向子进程注入强制着色变量,
|
|
581
|
+
// 管道输出被 ANSI 色码污染;生产轨迹无色码,剥离之。
|
|
582
|
+
delete env.FORCE_COLOR;
|
|
583
|
+
delete env.CLICOLOR_FORCE;
|
|
584
|
+
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
585
|
+
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
586
|
+
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
587
|
+
const child = spawn("/bin/bash", ["-lc", translatedCommand], {
|
|
552
588
|
cwd: workdir,
|
|
589
|
+
env,
|
|
553
590
|
stdio: ["ignore", "pipe", "pipe"],
|
|
591
|
+
detached: process.platform !== "win32",
|
|
554
592
|
});
|
|
593
|
+
const startedAt = Date.now();
|
|
555
594
|
let stdout = "";
|
|
556
595
|
let stderr = "";
|
|
557
596
|
let timedOut = false;
|
|
597
|
+
let settled = false;
|
|
598
|
+
let killRequested = false;
|
|
599
|
+
let forceSettle;
|
|
600
|
+
const killTree = (signal) => {
|
|
601
|
+
try {
|
|
602
|
+
if (process.platform !== "win32" && child.pid) {
|
|
603
|
+
process.kill(-child.pid, signal);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
child.kill(signal);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
// ESRCH:组已消失。EPERM:Darwin 在组正被回收时对重复 kill 的
|
|
611
|
+
// 返回值,同样视作已处理。
|
|
612
|
+
const code = error.code;
|
|
613
|
+
if (code !== "ESRCH" && code !== "EPERM")
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const finish = (code) => {
|
|
618
|
+
if (settled)
|
|
619
|
+
return;
|
|
620
|
+
settled = true;
|
|
621
|
+
clearTimeout(timeout);
|
|
622
|
+
if (forceSettle)
|
|
623
|
+
clearTimeout(forceSettle);
|
|
624
|
+
// 组长退出会立刻触发 close 并取消宽限定时器;取消/超时一旦请求过
|
|
625
|
+
// 击杀,就必须在结算时补一发组级 SIGKILL,否则忽略 TERM 且关闭了
|
|
626
|
+
// 继承管道的后台子进程会活过取消。正常退出不清场——模型可能有意
|
|
627
|
+
// 留下后台服务供后续调用使用。
|
|
628
|
+
if (killRequested)
|
|
629
|
+
killTree("SIGKILL");
|
|
630
|
+
this.signal?.removeEventListener("abort", abort);
|
|
631
|
+
if (this.signal?.aborted) {
|
|
632
|
+
reject(this.signal.reason || new Error("operation cancelled"));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// 生产契约:status 恒为 "completed"(包括非零退出码),超时无参考样本,
|
|
636
|
+
// 按最小猜测同样返回 completed + exit_code null,stderr 附超时说明。
|
|
637
|
+
if (timedOut) {
|
|
638
|
+
stderr += (stderr ? "\n" : "") + `command exceeded ${timeoutSeconds}s`;
|
|
639
|
+
}
|
|
640
|
+
const out = capBashOutput(this.virtualizeBashOutput(stdout));
|
|
641
|
+
const err = capBashOutput(this.virtualizeBashOutput(stderr));
|
|
642
|
+
resolve({
|
|
643
|
+
status: "completed",
|
|
644
|
+
stderr: err.text,
|
|
645
|
+
stdout: out.text,
|
|
646
|
+
exit_code: timedOut ? null : code,
|
|
647
|
+
duration_ms: Date.now() - startedAt,
|
|
648
|
+
stderr_truncated: err.truncated,
|
|
649
|
+
stdout_truncated: out.truncated,
|
|
650
|
+
});
|
|
651
|
+
};
|
|
652
|
+
// 宽限期结束时升级为组级 SIGKILL 再结算:abort 只发 SIGTERM,
|
|
653
|
+
// trap TERM 的进程会活过 Promise 拒绝并继续改写工作区。
|
|
654
|
+
const armForceSettle = () => {
|
|
655
|
+
forceSettle ??= setTimeout(() => {
|
|
656
|
+
killTree("SIGKILL");
|
|
657
|
+
finish(null);
|
|
658
|
+
}, 2000);
|
|
659
|
+
};
|
|
558
660
|
const timeout = setTimeout(() => {
|
|
559
661
|
timedOut = true;
|
|
560
|
-
|
|
662
|
+
killRequested = true;
|
|
663
|
+
killTree("SIGKILL");
|
|
664
|
+
armForceSettle();
|
|
561
665
|
}, timeoutSeconds * 1000);
|
|
562
|
-
const abort = () =>
|
|
666
|
+
const abort = () => {
|
|
667
|
+
killRequested = true;
|
|
668
|
+
killTree("SIGTERM");
|
|
669
|
+
armForceSettle();
|
|
670
|
+
};
|
|
563
671
|
this.signal?.addEventListener("abort", abort, { once: true });
|
|
564
672
|
child.stdout.on("data", (chunk) => {
|
|
565
673
|
stdout += chunk.toString("utf8");
|
|
@@ -567,78 +675,68 @@ export class ToolExecutor {
|
|
|
567
675
|
child.stderr.on("data", (chunk) => {
|
|
568
676
|
stderr += chunk.toString("utf8");
|
|
569
677
|
});
|
|
570
|
-
child.once("error",
|
|
571
|
-
|
|
678
|
+
child.once("error", (error) => {
|
|
679
|
+
if (settled)
|
|
680
|
+
return;
|
|
681
|
+
settled = true;
|
|
572
682
|
clearTimeout(timeout);
|
|
683
|
+
if (forceSettle)
|
|
684
|
+
clearTimeout(forceSettle);
|
|
573
685
|
this.signal?.removeEventListener("abort", abort);
|
|
574
|
-
|
|
575
|
-
reject(this.signal.reason || new Error("operation cancelled"));
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
if (timedOut) {
|
|
579
|
-
resolve({
|
|
580
|
-
status: "timeout",
|
|
581
|
-
exit_code: null,
|
|
582
|
-
stdout: "",
|
|
583
|
-
stderr: `command exceeded ${timeoutSeconds}s`,
|
|
584
|
-
});
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
resolve({
|
|
588
|
-
status: code === 0 ? "success" : "error",
|
|
589
|
-
exit_code: code,
|
|
590
|
-
stdout,
|
|
591
|
-
stderr,
|
|
592
|
-
});
|
|
686
|
+
reject(error);
|
|
593
687
|
});
|
|
688
|
+
child.once("close", (code) => finish(code));
|
|
594
689
|
});
|
|
595
690
|
}
|
|
596
691
|
async askUser(questions, toolCallId) {
|
|
597
692
|
if (this.askUserHandler) {
|
|
598
693
|
return this.askUserHandler(toolCallId, questions);
|
|
599
694
|
}
|
|
600
|
-
// ask_user is not offered in non-interactive sessions; if the model still
|
|
601
|
-
// emits it (e.g. resumed history or a hallucinated call), report an error
|
|
602
|
-
// so the turn continues instead of stalling on needs_input.
|
|
603
695
|
return {
|
|
604
|
-
status: "
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
"Proceed with reasonable defaults and state any assumptions you make.",
|
|
696
|
+
status: "needs_input",
|
|
697
|
+
questions,
|
|
698
|
+
message: "The current headless request cannot collect interactive input.",
|
|
608
699
|
};
|
|
609
700
|
}
|
|
610
701
|
async editFile(rawPath, oldText, newText) {
|
|
611
702
|
const target = await this.confinedPath(rawPath);
|
|
612
703
|
const source = await readFile(target, "utf8");
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
updated = source.replace(pattern, newText);
|
|
704
|
+
// 仅精确匹配:曾有空白容错回退,但 T1(b) 回放证实生产没有——case-003
|
|
705
|
+
// 四个 old_text 精确缺失的 edit 生产全部返回 Context not found,而回退
|
|
706
|
+
// 会静默改错文件;全部参考成功样本也都能走精确路径复现。
|
|
707
|
+
// 注意:spec/tools.json 的 edit_file description(与生产逐字一致,受
|
|
708
|
+
// T1(c) 门禁保护)声称支持 fuzzy matching——那是上游自身的描述与行为
|
|
709
|
+
// 不一致,按实测行为对齐,描述不改。见 TRAJECTORY_FIDELITY.md §5。
|
|
710
|
+
if (!source.includes(oldText)) {
|
|
711
|
+
return {
|
|
712
|
+
status: "error",
|
|
713
|
+
message: "Context not found. Read the file to verify the text exists.",
|
|
714
|
+
};
|
|
627
715
|
}
|
|
628
|
-
await writeFile(target,
|
|
716
|
+
await writeFile(target, source.replace(oldText, newText), "utf8");
|
|
629
717
|
return { status: "success", message: `Edited ${rawPath}.` };
|
|
630
718
|
}
|
|
631
719
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
632
720
|
const target = await this.confinedPath(rawPath);
|
|
633
|
-
|
|
721
|
+
let data;
|
|
722
|
+
try {
|
|
723
|
+
data = await readFile(target);
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
// 逐字对齐生产错误文案,路径按调用方原样回显。
|
|
727
|
+
return {
|
|
728
|
+
status: "error",
|
|
729
|
+
message: `Failed to read workspace file: ${rawPath}`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
634
732
|
const extension = path.extname(target).toLowerCase();
|
|
635
733
|
const kind = workspaceFileKind(target, data.subarray(0, 4096));
|
|
636
|
-
const relativePath = this.relativePath(target);
|
|
637
|
-
const result = {
|
|
638
|
-
status: "success",
|
|
639
|
-
path: relativePath,
|
|
640
|
-
};
|
|
641
734
|
if (isStructuredDocument(target)) {
|
|
735
|
+
// 文档提取无参考样本(known-unknown),保留本地扩展键。
|
|
736
|
+
const result = {
|
|
737
|
+
status: "success",
|
|
738
|
+
path: this.relativePath(target),
|
|
739
|
+
};
|
|
642
740
|
const extracted = await extractStructuredDocument(target, data, this.signal);
|
|
643
741
|
result.content = extracted.content;
|
|
644
742
|
result.mime_type =
|
|
@@ -655,34 +753,41 @@ export class ToolExecutor {
|
|
|
655
753
|
result.truncated = true;
|
|
656
754
|
if (extracted.warning)
|
|
657
755
|
result.warning = extracted.warning;
|
|
756
|
+
return result;
|
|
658
757
|
}
|
|
659
|
-
|
|
660
|
-
|
|
758
|
+
if (kind === "text") {
|
|
759
|
+
const content = decodeText(data);
|
|
760
|
+
return {
|
|
761
|
+
kind: "text",
|
|
762
|
+
lines: content.split("\n").length,
|
|
763
|
+
size: Buffer.byteLength(content, "utf8"),
|
|
764
|
+
content,
|
|
765
|
+
};
|
|
661
766
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (kind === "image") {
|
|
669
|
-
if (data.length > 20 * 1024 * 1024) {
|
|
670
|
-
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
671
|
-
}
|
|
672
|
-
result.content = imagePlaceholder;
|
|
673
|
-
this.pendingVisualContent.set(toolCallId, [
|
|
674
|
-
{
|
|
675
|
-
text: "<arena-system-message>" +
|
|
676
|
-
`The image below is the genuine content returned by read_file for ${JSON.stringify(relativePath)}.` +
|
|
677
|
-
"</arena-system-message>",
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
image: `data:${mimeType};base64,${data.toString("base64")}`,
|
|
681
|
-
},
|
|
682
|
-
]);
|
|
767
|
+
const mimeType = mime.getType(target) ||
|
|
768
|
+
mimeTypes[extension] ||
|
|
769
|
+
"application/octet-stream";
|
|
770
|
+
if (kind === "image") {
|
|
771
|
+
if (data.length > 20 * 1024 * 1024) {
|
|
772
|
+
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
683
773
|
}
|
|
774
|
+
this.pendingVisualContent.set(toolCallId, [
|
|
775
|
+
{
|
|
776
|
+
type: "image_url",
|
|
777
|
+
image_url: {
|
|
778
|
+
url: `data:${mimeType};base64,${data.toString("base64")}`,
|
|
779
|
+
detail: "auto",
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
]);
|
|
783
|
+
return [{ type: "text", text: imageResultPlaceholder }];
|
|
684
784
|
}
|
|
685
|
-
return
|
|
785
|
+
return {
|
|
786
|
+
status: "success",
|
|
787
|
+
path: this.relativePath(target),
|
|
788
|
+
mime_type: mimeType,
|
|
789
|
+
size: data.length,
|
|
790
|
+
};
|
|
686
791
|
}
|
|
687
792
|
async writeWorkspaceFile(rawPath, content) {
|
|
688
793
|
const target = await this.confinedPath(rawPath);
|
|
@@ -720,28 +825,25 @@ export class ToolExecutor {
|
|
|
720
825
|
const searchResults = Array.isArray(searchInfo.search_results)
|
|
721
826
|
? searchInfo.search_results
|
|
722
827
|
: [];
|
|
723
|
-
const choices = Array.isArray(output.choices) ? output.choices : [];
|
|
724
|
-
const answer = stringValue(record(record(choices[0]).message).content);
|
|
725
828
|
const limit = { "1": 3, "2": 5, "3": 8 }[depth] || 5;
|
|
829
|
+
// 参考轨迹条目:{id, url, title, description[, pageAge]};
|
|
830
|
+
// DashScope 无 pageAge,按可选键省略。
|
|
726
831
|
return {
|
|
727
832
|
status: "success",
|
|
728
|
-
query,
|
|
729
|
-
answer,
|
|
730
833
|
results: searchResults.slice(0, limit).map((value, index) => {
|
|
731
834
|
const item = record(value);
|
|
732
835
|
return {
|
|
733
836
|
id: Number(item.index) || index + 1,
|
|
734
|
-
title: stringValue(item.title),
|
|
735
837
|
url: stringValue(item.url),
|
|
736
|
-
|
|
838
|
+
title: stringValue(item.title),
|
|
839
|
+
description: stringValue(item.content) ||
|
|
737
840
|
stringValue(item.snippet) ||
|
|
738
841
|
stringValue(item.summary),
|
|
739
|
-
site_name: stringValue(item.site_name),
|
|
740
842
|
};
|
|
741
843
|
}),
|
|
742
844
|
};
|
|
743
845
|
}
|
|
744
|
-
async imageSearch(query, count
|
|
846
|
+
async imageSearch(query, count) {
|
|
745
847
|
const model = process.env.ARENA_IMAGE_SEARCH_MODEL || "qwen3.7-plus";
|
|
746
848
|
const payload = await dashScopeJson("compatible", "/responses", {
|
|
747
849
|
method: "POST",
|
|
@@ -770,13 +872,14 @@ export class ToolExecutor {
|
|
|
770
872
|
throw new Error("image_search returned malformed image results");
|
|
771
873
|
}
|
|
772
874
|
}
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
.
|
|
777
|
-
.
|
|
875
|
+
const saved = [];
|
|
876
|
+
// 参考轨迹命名:image-search/<query slug 前 40 字符>-<序号>.<扩展名>。
|
|
877
|
+
const slug = query
|
|
878
|
+
.toLowerCase()
|
|
879
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
880
|
+
.slice(0, 40);
|
|
778
881
|
for (const value of results) {
|
|
779
|
-
if (
|
|
882
|
+
if (saved.length >= count)
|
|
780
883
|
break;
|
|
781
884
|
const item = record(value);
|
|
782
885
|
const sourceUrl = stringValue(item.url) ||
|
|
@@ -794,13 +897,14 @@ export class ToolExecutor {
|
|
|
794
897
|
continue;
|
|
795
898
|
const normalizedType = contentType.split(";")[0]?.trim().toLowerCase();
|
|
796
899
|
const extension = Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
|
|
797
|
-
const target = await this.confinedPath(`
|
|
900
|
+
const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${extension}`);
|
|
798
901
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
799
902
|
await writeFile(target, await responseBytes(imageResponse));
|
|
800
|
-
|
|
801
|
-
path: this.relativePath(target),
|
|
802
|
-
source_url: sourceUrl,
|
|
903
|
+
saved.push({
|
|
803
904
|
title: stringValue(item.title),
|
|
905
|
+
file_path: this.relativePath(target),
|
|
906
|
+
source_url: sourceUrl,
|
|
907
|
+
thumbnail_url: stringValue(item.thumbnail_url) || sourceUrl,
|
|
804
908
|
});
|
|
805
909
|
}
|
|
806
910
|
catch (error) {
|
|
@@ -809,7 +913,7 @@ export class ToolExecutor {
|
|
|
809
913
|
// A single inaccessible search result should not fail the whole search.
|
|
810
914
|
}
|
|
811
915
|
}
|
|
812
|
-
return { status: "success",
|
|
916
|
+
return { status: "success", results: saved };
|
|
813
917
|
}
|
|
814
918
|
async presentFile(rawPath) {
|
|
815
919
|
const target = await this.confinedPath(rawPath);
|
|
@@ -912,7 +1016,6 @@ export class ToolExecutor {
|
|
|
912
1016
|
return {
|
|
913
1017
|
status: "success",
|
|
914
1018
|
file_path: this.relativePath(target),
|
|
915
|
-
task_id: taskId,
|
|
916
1019
|
};
|
|
917
1020
|
}
|
|
918
1021
|
async generateSpeech(rawPath, text, language, voiceIdentity = {}) {
|