@qwen-code/arena-agent 0.0.10-dev.0 → 0.0.10-dev.2
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 +5 -0
- package/README.md +19 -7
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +42 -16
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.js +21 -22
- package/dist/anthropic.js.map +1 -1
- package/dist/cli.js +23 -8
- package/dist/cli.js.map +1 -1
- package/dist/compaction.js +25 -8
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +22 -0
- package/dist/config.js.map +1 -1
- package/dist/history.d.ts +21 -0
- package/dist/history.js +301 -15
- package/dist/history.js.map +1 -1
- package/dist/process-manager.js +16 -10
- package/dist/process-manager.js.map +1 -1
- package/dist/provider.d.ts +12 -4
- package/dist/provider.js +31 -12
- package/dist/provider.js.map +1 -1
- package/dist/sdk.js +9 -1
- package/dist/sdk.js.map +1 -1
- package/dist/server.js +113 -42
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +3 -0
- package/dist/tools.js +426 -33
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +9 -4
- package/dist/tui.js.map +1 -1
- package/dist/web/app.js +17 -3
- package/dist/web/app.js.map +1 -1
- package/docs/SDK.md +5 -4
- package/package.json +2 -1
package/dist/tools.js
CHANGED
|
@@ -18,10 +18,22 @@ import { imageTextFromFallbackModel } from "./image-input-fallback.js";
|
|
|
18
18
|
import { isCodeArenaMode } from "./mode.js";
|
|
19
19
|
import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
|
|
20
20
|
import { processManagerForWorkspace, } from "./process-manager.js";
|
|
21
|
-
import { HttpModelClient } from "./provider.js";
|
|
21
|
+
import { HttpModelClient, } from "./provider.js";
|
|
22
22
|
import { RoutifyFetchPageReader, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
23
23
|
import { toolDefinitions } from "./spec.js";
|
|
24
24
|
import { voiceRegistryForWorkspace, } from "./voice-registry.js";
|
|
25
|
+
export function imageInputFallbackConnection(config) {
|
|
26
|
+
if (!config.imageInputFallbackBaseUrl &&
|
|
27
|
+
!config.imageInputFallbackApiKey &&
|
|
28
|
+
!config.imageInputFallbackProtocol) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
baseUrl: config.imageInputFallbackBaseUrl,
|
|
33
|
+
apiKey: config.imageInputFallbackApiKey,
|
|
34
|
+
protocol: config.imageInputFallbackProtocol,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
25
37
|
const knownTextApplicationMimeTypes = new Set([
|
|
26
38
|
"application/ecmascript",
|
|
27
39
|
"application/javascript",
|
|
@@ -349,6 +361,180 @@ function json(value) {
|
|
|
349
361
|
function errorMessage(error) {
|
|
350
362
|
return error instanceof Error ? error.message : String(error);
|
|
351
363
|
}
|
|
364
|
+
function jsonPointerPath(instancePath) {
|
|
365
|
+
if (!instancePath)
|
|
366
|
+
return [];
|
|
367
|
+
return instancePath
|
|
368
|
+
.slice(1)
|
|
369
|
+
.split("/")
|
|
370
|
+
.map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
371
|
+
.map((part) => (/^(0|[1-9]\d*)$/.test(part) ? Number(part) : part));
|
|
372
|
+
}
|
|
373
|
+
function valueAtPath(value, pathParts) {
|
|
374
|
+
let current = value;
|
|
375
|
+
for (const part of pathParts) {
|
|
376
|
+
if (current === null || typeof current !== "object")
|
|
377
|
+
return undefined;
|
|
378
|
+
current = current[part];
|
|
379
|
+
}
|
|
380
|
+
return current;
|
|
381
|
+
}
|
|
382
|
+
function zodTypeName(type) {
|
|
383
|
+
return type === "integer" ? "number" : type;
|
|
384
|
+
}
|
|
385
|
+
function receivedType(value) {
|
|
386
|
+
if (value === null)
|
|
387
|
+
return "null";
|
|
388
|
+
if (Array.isArray(value))
|
|
389
|
+
return "array";
|
|
390
|
+
return typeof value;
|
|
391
|
+
}
|
|
392
|
+
function arenaValidationIssue(error, value) {
|
|
393
|
+
const pathParts = jsonPointerPath(error.instancePath);
|
|
394
|
+
if (error.keyword === "required") {
|
|
395
|
+
const missingProperty = String(error.params.missingProperty);
|
|
396
|
+
const parentSchema = error.parentSchema;
|
|
397
|
+
const expected = zodTypeName(parentSchema?.properties?.[missingProperty]?.type || "unknown");
|
|
398
|
+
return {
|
|
399
|
+
expected,
|
|
400
|
+
code: "invalid_type",
|
|
401
|
+
path: [...pathParts, missingProperty],
|
|
402
|
+
message: `Invalid input: expected ${expected}, received undefined`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (error.keyword === "type") {
|
|
406
|
+
const expected = zodTypeName(String(error.params.type));
|
|
407
|
+
return {
|
|
408
|
+
expected,
|
|
409
|
+
code: "invalid_type",
|
|
410
|
+
path: pathParts,
|
|
411
|
+
message: `Invalid input: expected ${expected}, received ` +
|
|
412
|
+
receivedType(valueAtPath(value, pathParts)),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (error.keyword === "maxLength" || error.keyword === "minLength") {
|
|
416
|
+
const tooBig = error.keyword === "maxLength";
|
|
417
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
418
|
+
const limit = Number(error.params.limit);
|
|
419
|
+
return {
|
|
420
|
+
origin: "string",
|
|
421
|
+
code: tooBig ? "too_big" : "too_small",
|
|
422
|
+
[limitKey]: limit,
|
|
423
|
+
inclusive: true,
|
|
424
|
+
path: pathParts,
|
|
425
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected string to have ` +
|
|
426
|
+
`${tooBig ? "<=" : ">="}${limit} characters`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (error.keyword === "maximum" || error.keyword === "minimum") {
|
|
430
|
+
const tooBig = error.keyword === "maximum";
|
|
431
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
432
|
+
const limit = Number(error.params.limit);
|
|
433
|
+
return {
|
|
434
|
+
origin: "number",
|
|
435
|
+
code: tooBig ? "too_big" : "too_small",
|
|
436
|
+
[limitKey]: limit,
|
|
437
|
+
inclusive: true,
|
|
438
|
+
path: pathParts,
|
|
439
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected number to be ` +
|
|
440
|
+
`${tooBig ? "<=" : ">="}${limit}`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (error.keyword === "maxItems" || error.keyword === "minItems") {
|
|
444
|
+
const tooBig = error.keyword === "maxItems";
|
|
445
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
446
|
+
const limit = Number(error.params.limit);
|
|
447
|
+
return {
|
|
448
|
+
origin: "array",
|
|
449
|
+
code: tooBig ? "too_big" : "too_small",
|
|
450
|
+
[limitKey]: limit,
|
|
451
|
+
inclusive: true,
|
|
452
|
+
path: pathParts,
|
|
453
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected array to have ` +
|
|
454
|
+
`${tooBig ? "<=" : ">="}${limit} items`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (error.keyword === "enum") {
|
|
458
|
+
const values = error.params.allowedValues;
|
|
459
|
+
return {
|
|
460
|
+
code: "invalid_value",
|
|
461
|
+
values,
|
|
462
|
+
path: pathParts,
|
|
463
|
+
message: `Invalid option: expected one of ${values
|
|
464
|
+
.map((item) => JSON.stringify(item))
|
|
465
|
+
.join("|")}`,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (error.keyword === "format") {
|
|
469
|
+
return {
|
|
470
|
+
origin: "string",
|
|
471
|
+
code: "invalid_format",
|
|
472
|
+
format: error.params.format === "uri" ? "url" : error.params.format,
|
|
473
|
+
path: pathParts,
|
|
474
|
+
message: error.params.format === "uri" ? "Invalid URL" : "Invalid string",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
code: "custom",
|
|
479
|
+
path: pathParts,
|
|
480
|
+
message: error.message || "Invalid input",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function arenaValidationIssues(error, value) {
|
|
484
|
+
const issues = [arenaValidationIssue(error, value)];
|
|
485
|
+
if (error.keyword !== "type" || error.params.type !== "array")
|
|
486
|
+
return issues;
|
|
487
|
+
const pathParts = jsonPointerPath(error.instancePath);
|
|
488
|
+
const received = valueAtPath(value, pathParts);
|
|
489
|
+
if (typeof received !== "string")
|
|
490
|
+
return issues;
|
|
491
|
+
const parentSchema = error.parentSchema;
|
|
492
|
+
const maximum = parentSchema?.maxItems;
|
|
493
|
+
if (typeof maximum === "number" && received.length > maximum) {
|
|
494
|
+
issues.push({
|
|
495
|
+
origin: "string",
|
|
496
|
+
code: "too_big",
|
|
497
|
+
maximum,
|
|
498
|
+
inclusive: true,
|
|
499
|
+
path: pathParts,
|
|
500
|
+
message: `Too big: expected string to have <=${maximum} characters`,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
const minimum = parentSchema?.minItems;
|
|
504
|
+
if (typeof minimum === "number" && received.length < minimum) {
|
|
505
|
+
issues.push({
|
|
506
|
+
origin: "string",
|
|
507
|
+
code: "too_small",
|
|
508
|
+
minimum,
|
|
509
|
+
inclusive: true,
|
|
510
|
+
path: pathParts,
|
|
511
|
+
message: `Too small: expected string to have >=${minimum} characters`,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return issues;
|
|
515
|
+
}
|
|
516
|
+
function arenaValidationError(toolName, value, errors) {
|
|
517
|
+
const issues = (errors || []).flatMap((error) => arenaValidationIssues(error, value));
|
|
518
|
+
return (`Invalid input for tool ${toolName}: Type validation failed: Value: ` +
|
|
519
|
+
`${JSON.stringify(value)}.\nError message: ${JSON.stringify(issues, null, 2)}`);
|
|
520
|
+
}
|
|
521
|
+
function arenaJsonParsingError(toolName, text, error) {
|
|
522
|
+
return (`Invalid input for tool ${toolName}: JSON parsing failed: Text: ${text}.\n` +
|
|
523
|
+
`Error message: ${errorMessage(error)}`);
|
|
524
|
+
}
|
|
525
|
+
function unavailableToolError(toolName, availableToolNames) {
|
|
526
|
+
return (`Model tried to call unavailable tool '${toolName}'. Available tools: ` +
|
|
527
|
+
`${[...availableToolNames, "compact"].join(", ")}.`);
|
|
528
|
+
}
|
|
529
|
+
function prefixedFailure(prefix, message) {
|
|
530
|
+
return message.startsWith(prefix) ? message : `${prefix}${message}`;
|
|
531
|
+
}
|
|
532
|
+
function fetchPageFailure(message) {
|
|
533
|
+
const status = message.match(/(?:fetch_page|Firecrawl|Jina Reader) failed \((\d{3})\b/i)?.[1];
|
|
534
|
+
return status
|
|
535
|
+
? `Failed to fetch page (HTTP ${status})`
|
|
536
|
+
: "Failed to fetch page";
|
|
537
|
+
}
|
|
352
538
|
function displayByteSize(value) {
|
|
353
539
|
if (value >= 1024 * 1024) {
|
|
354
540
|
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -384,7 +570,14 @@ class ArenaToolExecutor {
|
|
|
384
570
|
routifyPageReader;
|
|
385
571
|
processManager;
|
|
386
572
|
voiceRegistry;
|
|
387
|
-
ajv = new Ajv({
|
|
573
|
+
ajv = new Ajv({
|
|
574
|
+
allErrors: true,
|
|
575
|
+
strict: false,
|
|
576
|
+
verbose: true,
|
|
577
|
+
// The captured runtime uses Zod object parsing: unknown object keys are
|
|
578
|
+
// stripped before dispatch instead of being emitted as validation issues.
|
|
579
|
+
removeAdditional: "all",
|
|
580
|
+
});
|
|
388
581
|
// ask_user needs a handler: TUI/WebUI collect real user input, while
|
|
389
582
|
// headless `-p` may delegate to a configured remote User Agent. add_voice
|
|
390
583
|
// remains available without one and deterministically registers a fallback.
|
|
@@ -399,8 +592,12 @@ class ArenaToolExecutor {
|
|
|
399
592
|
this.askUserHandler = options.askUserHandler;
|
|
400
593
|
this.askUserEnabled = Boolean(options.askUserHandler);
|
|
401
594
|
if (config.imageInputFallbackModel?.trim()) {
|
|
595
|
+
const connection = imageInputFallbackConnection(config);
|
|
402
596
|
this.imageInputFallbackClient =
|
|
403
|
-
options.
|
|
597
|
+
options.imageInputFallbackClient ||
|
|
598
|
+
(connection
|
|
599
|
+
? new HttpModelClient(config, { connection })
|
|
600
|
+
: options.modelClient || new HttpModelClient(config));
|
|
404
601
|
}
|
|
405
602
|
this.signal = options.signal;
|
|
406
603
|
this.workspaceContext = options.workspaceContext;
|
|
@@ -409,6 +606,13 @@ class ArenaToolExecutor {
|
|
|
409
606
|
signal: options.signal,
|
|
410
607
|
});
|
|
411
608
|
addFormats(this.ajv);
|
|
609
|
+
// WHATWG URL parsing matches the captured fetch_page behavior. ajv-formats'
|
|
610
|
+
// RFC URI checker rejects a recorded URL containing `|` in its query even
|
|
611
|
+
// though Arena accepted and fetched it successfully.
|
|
612
|
+
this.ajv.addFormat("uri", {
|
|
613
|
+
type: "string",
|
|
614
|
+
validate: (value) => URL.canParse(value),
|
|
615
|
+
});
|
|
412
616
|
for (const definition of toolDefinitions(this.workspaceContext)) {
|
|
413
617
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
414
618
|
}
|
|
@@ -431,23 +635,82 @@ class ArenaToolExecutor {
|
|
|
431
635
|
try {
|
|
432
636
|
// 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
|
|
433
637
|
// 之前短路,否则命令会照常 spawn 直到自然结束。
|
|
434
|
-
this.signal?.
|
|
638
|
+
if (this.signal?.aborted) {
|
|
639
|
+
return {
|
|
640
|
+
role: "tool",
|
|
641
|
+
tool_call_id: callId,
|
|
642
|
+
content: json({ status: "aborted" }),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
435
645
|
if (this.config.disallowedTools?.includes(name)) {
|
|
436
646
|
throw new Error(`tool is disallowed: ${name}`);
|
|
437
647
|
}
|
|
438
|
-
const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
|
|
439
|
-
? call.function.arguments
|
|
440
|
-
: "{}");
|
|
441
648
|
const validate = this.validators.get(name);
|
|
442
|
-
if (!validate)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
.map((
|
|
447
|
-
|
|
448
|
-
|
|
649
|
+
if (!validate) {
|
|
650
|
+
return {
|
|
651
|
+
role: "tool",
|
|
652
|
+
tool_call_id: callId,
|
|
653
|
+
content: unavailableToolError(name, toolDefinitions(this.workspaceContext).map((definition) => definition.function.name)),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const argumentsText = typeof call.function?.arguments === "string"
|
|
657
|
+
? call.function.arguments
|
|
658
|
+
: "{}";
|
|
659
|
+
let argumentsValue;
|
|
660
|
+
try {
|
|
661
|
+
argumentsValue = JSON.parse(argumentsText);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
return {
|
|
665
|
+
role: "tool",
|
|
666
|
+
tool_call_id: callId,
|
|
667
|
+
content: arenaJsonParsingError(name, argumentsText, error),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const malformedArguments = argumentsValue !== null &&
|
|
671
|
+
typeof argumentsValue === "object" &&
|
|
672
|
+
!Array.isArray(argumentsValue)
|
|
673
|
+
? argumentsValue.__malformedToolArguments
|
|
674
|
+
: undefined;
|
|
675
|
+
if (typeof malformedArguments === "string") {
|
|
676
|
+
try {
|
|
677
|
+
JSON.parse(malformedArguments);
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
return {
|
|
681
|
+
role: "tool",
|
|
682
|
+
tool_call_id: callId,
|
|
683
|
+
content: arenaJsonParsingError(name, malformedArguments, error),
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const validatedArguments = structuredClone(argumentsValue);
|
|
688
|
+
if (!validate(validatedArguments)) {
|
|
689
|
+
return {
|
|
690
|
+
role: "tool",
|
|
691
|
+
tool_call_id: callId,
|
|
692
|
+
content: arenaValidationError(name, argumentsValue, validate.errors),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const startedAt = Date.now();
|
|
696
|
+
let result;
|
|
697
|
+
try {
|
|
698
|
+
result = await this.dispatch(name, validatedArguments, callId, context);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
if (error instanceof AskUserHandlerError) {
|
|
702
|
+
return {
|
|
703
|
+
role: "tool",
|
|
704
|
+
tool_call_id: callId,
|
|
705
|
+
content: json({ status: "error", message: error.message }),
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
role: "tool",
|
|
710
|
+
tool_call_id: callId,
|
|
711
|
+
content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt)),
|
|
712
|
+
};
|
|
449
713
|
}
|
|
450
|
-
const result = await this.dispatch(name, argumentsValue, callId, context);
|
|
451
714
|
const content = typeof result === "string" ? result : json(result);
|
|
452
715
|
if (content.length > toolResultCharacterLimit) {
|
|
453
716
|
return {
|
|
@@ -468,13 +731,6 @@ class ArenaToolExecutor {
|
|
|
468
731
|
};
|
|
469
732
|
}
|
|
470
733
|
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
734
|
return {
|
|
479
735
|
role: "tool",
|
|
480
736
|
tool_call_id: callId,
|
|
@@ -485,6 +741,83 @@ class ArenaToolExecutor {
|
|
|
485
741
|
};
|
|
486
742
|
}
|
|
487
743
|
}
|
|
744
|
+
runtimeFailureResult(name, values, error, startedAt) {
|
|
745
|
+
if (this.signal?.aborted)
|
|
746
|
+
return { status: "aborted" };
|
|
747
|
+
const message = errorMessage(error);
|
|
748
|
+
const durationMs = Date.now() - startedAt;
|
|
749
|
+
if (name === "bash") {
|
|
750
|
+
const stderr = this.virtualizeBashOutput(message);
|
|
751
|
+
return {
|
|
752
|
+
status: "shell_error",
|
|
753
|
+
stderr,
|
|
754
|
+
stdout: "",
|
|
755
|
+
exit_code: null,
|
|
756
|
+
duration_ms: durationMs,
|
|
757
|
+
stderr_truncated: false,
|
|
758
|
+
stdout_truncated: false,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
if (name === "start_process") {
|
|
762
|
+
const cwd = String(values.cwd || "/home/user");
|
|
763
|
+
const code = error?.code;
|
|
764
|
+
const logTail = code === "ENOENT"
|
|
765
|
+
? `[invalid_argument] cwd '${cwd}' does not exist`
|
|
766
|
+
: message;
|
|
767
|
+
return {
|
|
768
|
+
status: "shell_error",
|
|
769
|
+
process_id: null,
|
|
770
|
+
pid: null,
|
|
771
|
+
exit_code: null,
|
|
772
|
+
log_tail: logTail,
|
|
773
|
+
listening_ports: [],
|
|
774
|
+
new_ports: [],
|
|
775
|
+
duration_ms: durationMs,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
if (name === "get_process_output") {
|
|
779
|
+
return {
|
|
780
|
+
status: "shell_error",
|
|
781
|
+
log_tail: message,
|
|
782
|
+
exit_code: null,
|
|
783
|
+
listening_ports: [],
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
if (name === "stop_process") {
|
|
787
|
+
return { status: "shell_error", log_tail: message };
|
|
788
|
+
}
|
|
789
|
+
if (name === "fetch_page") {
|
|
790
|
+
return { status: "error", error: fetchPageFailure(message) };
|
|
791
|
+
}
|
|
792
|
+
if (name === "add_voice") {
|
|
793
|
+
return {
|
|
794
|
+
status: "error",
|
|
795
|
+
message: prefixedFailure("Failed to audition voices: ", message),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
if (name === "generate_image") {
|
|
799
|
+
return {
|
|
800
|
+
status: "error",
|
|
801
|
+
message: prefixedFailure("Failed to generate image: ", message),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
if (name === "generate_speech") {
|
|
805
|
+
return {
|
|
806
|
+
status: "error",
|
|
807
|
+
message: prefixedFailure("Failed to generate speech: ", message),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
if (name === "image_search") {
|
|
811
|
+
return {
|
|
812
|
+
status: "error",
|
|
813
|
+
message: prefixedFailure("Image search failed: ", message),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (name === "present_file") {
|
|
817
|
+
return { status: "error", message: "Error calling present file tool" };
|
|
818
|
+
}
|
|
819
|
+
return { status: "error", message };
|
|
820
|
+
}
|
|
488
821
|
takeVisualMessage(toolCallIds) {
|
|
489
822
|
// 生产契约:同一组 tool call 的全部图片汇入一条 user message,
|
|
490
823
|
// 由一个 arena-system-message 文本 part 领头。
|
|
@@ -820,8 +1153,24 @@ class ArenaToolExecutor {
|
|
|
820
1153
|
};
|
|
821
1154
|
}
|
|
822
1155
|
async editFile(rawPath, oldText, newText) {
|
|
823
|
-
|
|
824
|
-
|
|
1156
|
+
let target;
|
|
1157
|
+
let source;
|
|
1158
|
+
try {
|
|
1159
|
+
target = await this.confinedPath(rawPath);
|
|
1160
|
+
if (!(await stat(target)).isFile()) {
|
|
1161
|
+
return {
|
|
1162
|
+
status: "error",
|
|
1163
|
+
message: `File not found: ${rawPath}`,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
source = await readFile(target, "utf8");
|
|
1167
|
+
}
|
|
1168
|
+
catch {
|
|
1169
|
+
return {
|
|
1170
|
+
status: "error",
|
|
1171
|
+
message: `Failed to read workspace file: ${rawPath}`,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
825
1174
|
// 仅精确匹配:曾有空白容错回退,但 T1(b) 回放证实生产没有——case-003
|
|
826
1175
|
// 四个 old_text 精确缺失的 edit 生产全部返回 Context not found,而回退
|
|
827
1176
|
// 会静默改错文件;全部参考成功样本也都能走精确路径复现。
|
|
@@ -834,7 +1183,15 @@ class ArenaToolExecutor {
|
|
|
834
1183
|
message: "Context not found. Read the file to verify the text exists.",
|
|
835
1184
|
};
|
|
836
1185
|
}
|
|
837
|
-
|
|
1186
|
+
try {
|
|
1187
|
+
await writeFile(target, source.replace(oldText, newText), "utf8");
|
|
1188
|
+
}
|
|
1189
|
+
catch {
|
|
1190
|
+
return {
|
|
1191
|
+
status: "error",
|
|
1192
|
+
message: `Failed to write workspace file: ${rawPath}`,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
838
1195
|
return { status: "success", message: `Edited ${rawPath}.` };
|
|
839
1196
|
}
|
|
840
1197
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
@@ -878,6 +1235,15 @@ class ArenaToolExecutor {
|
|
|
878
1235
|
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
879
1236
|
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
880
1237
|
}
|
|
1238
|
+
// Current Arena v2 captures consistently expose local PDFs as binary
|
|
1239
|
+
// metadata. PDF text extraction belongs to fetch_page, not read_file.
|
|
1240
|
+
if (extension === ".pdf") {
|
|
1241
|
+
return {
|
|
1242
|
+
kind: "unsupported",
|
|
1243
|
+
mediaType: "application/pdf",
|
|
1244
|
+
size: fileSize,
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
881
1247
|
const mainModelSupportsImages = kind === "image" ? supportsImageInput(this.config) : false;
|
|
882
1248
|
const fallbackModel = mainModelSupportsImages
|
|
883
1249
|
? undefined
|
|
@@ -975,9 +1341,18 @@ class ArenaToolExecutor {
|
|
|
975
1341
|
};
|
|
976
1342
|
}
|
|
977
1343
|
async writeWorkspaceFile(rawPath, content) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1344
|
+
let target;
|
|
1345
|
+
try {
|
|
1346
|
+
target = await this.confinedPath(rawPath);
|
|
1347
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
1348
|
+
await writeFile(target, content, "utf8");
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
return {
|
|
1352
|
+
status: "error",
|
|
1353
|
+
message: `Failed to write workspace file: ${rawPath}`,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
981
1356
|
return { status: "success" };
|
|
982
1357
|
}
|
|
983
1358
|
async webSearch(query, depth) {
|
|
@@ -1038,13 +1413,25 @@ class ArenaToolExecutor {
|
|
|
1038
1413
|
}
|
|
1039
1414
|
}
|
|
1040
1415
|
}
|
|
1416
|
+
if (!saved.length) {
|
|
1417
|
+
return {
|
|
1418
|
+
status: "error",
|
|
1419
|
+
message: "Image search failed: Strict safe search filtered all results for this query. " +
|
|
1420
|
+
"Try a different or more specific query.",
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1041
1423
|
return { status: "success", results: saved };
|
|
1042
1424
|
}
|
|
1043
1425
|
async presentFile(rawPath) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1426
|
+
try {
|
|
1427
|
+
const target = await this.confinedPath(rawPath);
|
|
1428
|
+
if (!(await stat(target)).isFile())
|
|
1429
|
+
throw new Error(rawPath);
|
|
1430
|
+
return { path: rawPath, status: "success" };
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
return { status: "error", message: "Error calling present file tool" };
|
|
1434
|
+
}
|
|
1048
1435
|
}
|
|
1049
1436
|
async generateImage(rawPath, prompt, images) {
|
|
1050
1437
|
const target = await this.confinedPath(rawPath);
|
|
@@ -1062,7 +1449,13 @@ class ArenaToolExecutor {
|
|
|
1062
1449
|
if (!mimeType?.startsWith("image/")) {
|
|
1063
1450
|
throw new Error(`unsupported reference image format: ${imagePath}`);
|
|
1064
1451
|
}
|
|
1065
|
-
|
|
1452
|
+
let bytes;
|
|
1453
|
+
try {
|
|
1454
|
+
bytes = await readFile(source);
|
|
1455
|
+
}
|
|
1456
|
+
catch {
|
|
1457
|
+
throw new Error(`Failed to read workspace file: ${imagePath}`);
|
|
1458
|
+
}
|
|
1066
1459
|
if (bytes.length > 20 * 1024 * 1024) {
|
|
1067
1460
|
throw new Error(`reference image exceeds 20 MB: ${imagePath}`);
|
|
1068
1461
|
}
|