@qwen-code/arena-agent 0.0.10-dev.0 → 0.0.10-dev.1
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/README.md +12 -7
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +22 -10
- package/dist/agent.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/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/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.js +408 -31
- 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
|
@@ -349,6 +349,180 @@ function json(value) {
|
|
|
349
349
|
function errorMessage(error) {
|
|
350
350
|
return error instanceof Error ? error.message : String(error);
|
|
351
351
|
}
|
|
352
|
+
function jsonPointerPath(instancePath) {
|
|
353
|
+
if (!instancePath)
|
|
354
|
+
return [];
|
|
355
|
+
return instancePath
|
|
356
|
+
.slice(1)
|
|
357
|
+
.split("/")
|
|
358
|
+
.map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
359
|
+
.map((part) => (/^(0|[1-9]\d*)$/.test(part) ? Number(part) : part));
|
|
360
|
+
}
|
|
361
|
+
function valueAtPath(value, pathParts) {
|
|
362
|
+
let current = value;
|
|
363
|
+
for (const part of pathParts) {
|
|
364
|
+
if (current === null || typeof current !== "object")
|
|
365
|
+
return undefined;
|
|
366
|
+
current = current[part];
|
|
367
|
+
}
|
|
368
|
+
return current;
|
|
369
|
+
}
|
|
370
|
+
function zodTypeName(type) {
|
|
371
|
+
return type === "integer" ? "number" : type;
|
|
372
|
+
}
|
|
373
|
+
function receivedType(value) {
|
|
374
|
+
if (value === null)
|
|
375
|
+
return "null";
|
|
376
|
+
if (Array.isArray(value))
|
|
377
|
+
return "array";
|
|
378
|
+
return typeof value;
|
|
379
|
+
}
|
|
380
|
+
function arenaValidationIssue(error, value) {
|
|
381
|
+
const pathParts = jsonPointerPath(error.instancePath);
|
|
382
|
+
if (error.keyword === "required") {
|
|
383
|
+
const missingProperty = String(error.params.missingProperty);
|
|
384
|
+
const parentSchema = error.parentSchema;
|
|
385
|
+
const expected = zodTypeName(parentSchema?.properties?.[missingProperty]?.type || "unknown");
|
|
386
|
+
return {
|
|
387
|
+
expected,
|
|
388
|
+
code: "invalid_type",
|
|
389
|
+
path: [...pathParts, missingProperty],
|
|
390
|
+
message: `Invalid input: expected ${expected}, received undefined`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (error.keyword === "type") {
|
|
394
|
+
const expected = zodTypeName(String(error.params.type));
|
|
395
|
+
return {
|
|
396
|
+
expected,
|
|
397
|
+
code: "invalid_type",
|
|
398
|
+
path: pathParts,
|
|
399
|
+
message: `Invalid input: expected ${expected}, received ` +
|
|
400
|
+
receivedType(valueAtPath(value, pathParts)),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (error.keyword === "maxLength" || error.keyword === "minLength") {
|
|
404
|
+
const tooBig = error.keyword === "maxLength";
|
|
405
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
406
|
+
const limit = Number(error.params.limit);
|
|
407
|
+
return {
|
|
408
|
+
origin: "string",
|
|
409
|
+
code: tooBig ? "too_big" : "too_small",
|
|
410
|
+
[limitKey]: limit,
|
|
411
|
+
inclusive: true,
|
|
412
|
+
path: pathParts,
|
|
413
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected string to have ` +
|
|
414
|
+
`${tooBig ? "<=" : ">="}${limit} characters`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (error.keyword === "maximum" || error.keyword === "minimum") {
|
|
418
|
+
const tooBig = error.keyword === "maximum";
|
|
419
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
420
|
+
const limit = Number(error.params.limit);
|
|
421
|
+
return {
|
|
422
|
+
origin: "number",
|
|
423
|
+
code: tooBig ? "too_big" : "too_small",
|
|
424
|
+
[limitKey]: limit,
|
|
425
|
+
inclusive: true,
|
|
426
|
+
path: pathParts,
|
|
427
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected number to be ` +
|
|
428
|
+
`${tooBig ? "<=" : ">="}${limit}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (error.keyword === "maxItems" || error.keyword === "minItems") {
|
|
432
|
+
const tooBig = error.keyword === "maxItems";
|
|
433
|
+
const limitKey = tooBig ? "maximum" : "minimum";
|
|
434
|
+
const limit = Number(error.params.limit);
|
|
435
|
+
return {
|
|
436
|
+
origin: "array",
|
|
437
|
+
code: tooBig ? "too_big" : "too_small",
|
|
438
|
+
[limitKey]: limit,
|
|
439
|
+
inclusive: true,
|
|
440
|
+
path: pathParts,
|
|
441
|
+
message: `${tooBig ? "Too big" : "Too small"}: expected array to have ` +
|
|
442
|
+
`${tooBig ? "<=" : ">="}${limit} items`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (error.keyword === "enum") {
|
|
446
|
+
const values = error.params.allowedValues;
|
|
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
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (error.keyword === "format") {
|
|
457
|
+
return {
|
|
458
|
+
origin: "string",
|
|
459
|
+
code: "invalid_format",
|
|
460
|
+
format: error.params.format === "uri" ? "url" : error.params.format,
|
|
461
|
+
path: pathParts,
|
|
462
|
+
message: error.params.format === "uri" ? "Invalid URL" : "Invalid string",
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
code: "custom",
|
|
467
|
+
path: pathParts,
|
|
468
|
+
message: error.message || "Invalid input",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function arenaValidationIssues(error, value) {
|
|
472
|
+
const issues = [arenaValidationIssue(error, value)];
|
|
473
|
+
if (error.keyword !== "type" || error.params.type !== "array")
|
|
474
|
+
return issues;
|
|
475
|
+
const pathParts = jsonPointerPath(error.instancePath);
|
|
476
|
+
const received = valueAtPath(value, pathParts);
|
|
477
|
+
if (typeof received !== "string")
|
|
478
|
+
return issues;
|
|
479
|
+
const parentSchema = error.parentSchema;
|
|
480
|
+
const maximum = parentSchema?.maxItems;
|
|
481
|
+
if (typeof maximum === "number" && received.length > maximum) {
|
|
482
|
+
issues.push({
|
|
483
|
+
origin: "string",
|
|
484
|
+
code: "too_big",
|
|
485
|
+
maximum,
|
|
486
|
+
inclusive: true,
|
|
487
|
+
path: pathParts,
|
|
488
|
+
message: `Too big: expected string to have <=${maximum} characters`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const minimum = parentSchema?.minItems;
|
|
492
|
+
if (typeof minimum === "number" && received.length < minimum) {
|
|
493
|
+
issues.push({
|
|
494
|
+
origin: "string",
|
|
495
|
+
code: "too_small",
|
|
496
|
+
minimum,
|
|
497
|
+
inclusive: true,
|
|
498
|
+
path: pathParts,
|
|
499
|
+
message: `Too small: expected string to have >=${minimum} characters`,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return issues;
|
|
503
|
+
}
|
|
504
|
+
function arenaValidationError(toolName, value, errors) {
|
|
505
|
+
const issues = (errors || []).flatMap((error) => arenaValidationIssues(error, value));
|
|
506
|
+
return (`Invalid input for tool ${toolName}: Type validation failed: Value: ` +
|
|
507
|
+
`${JSON.stringify(value)}.\nError message: ${JSON.stringify(issues, null, 2)}`);
|
|
508
|
+
}
|
|
509
|
+
function arenaJsonParsingError(toolName, text, error) {
|
|
510
|
+
return (`Invalid input for tool ${toolName}: JSON parsing failed: Text: ${text}.\n` +
|
|
511
|
+
`Error message: ${errorMessage(error)}`);
|
|
512
|
+
}
|
|
513
|
+
function unavailableToolError(toolName, availableToolNames) {
|
|
514
|
+
return (`Model tried to call unavailable tool '${toolName}'. Available tools: ` +
|
|
515
|
+
`${[...availableToolNames, "compact"].join(", ")}.`);
|
|
516
|
+
}
|
|
517
|
+
function prefixedFailure(prefix, message) {
|
|
518
|
+
return message.startsWith(prefix) ? message : `${prefix}${message}`;
|
|
519
|
+
}
|
|
520
|
+
function fetchPageFailure(message) {
|
|
521
|
+
const status = message.match(/(?:fetch_page|Firecrawl|Jina Reader) failed \((\d{3})\b/i)?.[1];
|
|
522
|
+
return status
|
|
523
|
+
? `Failed to fetch page (HTTP ${status})`
|
|
524
|
+
: "Failed to fetch page";
|
|
525
|
+
}
|
|
352
526
|
function displayByteSize(value) {
|
|
353
527
|
if (value >= 1024 * 1024) {
|
|
354
528
|
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -384,7 +558,14 @@ class ArenaToolExecutor {
|
|
|
384
558
|
routifyPageReader;
|
|
385
559
|
processManager;
|
|
386
560
|
voiceRegistry;
|
|
387
|
-
ajv = new Ajv({
|
|
561
|
+
ajv = new Ajv({
|
|
562
|
+
allErrors: true,
|
|
563
|
+
strict: false,
|
|
564
|
+
verbose: true,
|
|
565
|
+
// The captured runtime uses Zod object parsing: unknown object keys are
|
|
566
|
+
// stripped before dispatch instead of being emitted as validation issues.
|
|
567
|
+
removeAdditional: "all",
|
|
568
|
+
});
|
|
388
569
|
// ask_user needs a handler: TUI/WebUI collect real user input, while
|
|
389
570
|
// headless `-p` may delegate to a configured remote User Agent. add_voice
|
|
390
571
|
// remains available without one and deterministically registers a fallback.
|
|
@@ -409,6 +590,13 @@ class ArenaToolExecutor {
|
|
|
409
590
|
signal: options.signal,
|
|
410
591
|
});
|
|
411
592
|
addFormats(this.ajv);
|
|
593
|
+
// WHATWG URL parsing matches the captured fetch_page behavior. ajv-formats'
|
|
594
|
+
// RFC URI checker rejects a recorded URL containing `|` in its query even
|
|
595
|
+
// though Arena accepted and fetched it successfully.
|
|
596
|
+
this.ajv.addFormat("uri", {
|
|
597
|
+
type: "string",
|
|
598
|
+
validate: (value) => URL.canParse(value),
|
|
599
|
+
});
|
|
412
600
|
for (const definition of toolDefinitions(this.workspaceContext)) {
|
|
413
601
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
414
602
|
}
|
|
@@ -431,23 +619,82 @@ class ArenaToolExecutor {
|
|
|
431
619
|
try {
|
|
432
620
|
// 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
|
|
433
621
|
// 之前短路,否则命令会照常 spawn 直到自然结束。
|
|
434
|
-
this.signal?.
|
|
622
|
+
if (this.signal?.aborted) {
|
|
623
|
+
return {
|
|
624
|
+
role: "tool",
|
|
625
|
+
tool_call_id: callId,
|
|
626
|
+
content: json({ status: "aborted" }),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
435
629
|
if (this.config.disallowedTools?.includes(name)) {
|
|
436
630
|
throw new Error(`tool is disallowed: ${name}`);
|
|
437
631
|
}
|
|
438
|
-
const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
|
|
439
|
-
? call.function.arguments
|
|
440
|
-
: "{}");
|
|
441
632
|
const validate = this.validators.get(name);
|
|
442
|
-
if (!validate)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
.map((
|
|
447
|
-
|
|
448
|
-
|
|
633
|
+
if (!validate) {
|
|
634
|
+
return {
|
|
635
|
+
role: "tool",
|
|
636
|
+
tool_call_id: callId,
|
|
637
|
+
content: unavailableToolError(name, toolDefinitions(this.workspaceContext).map((definition) => definition.function.name)),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const argumentsText = typeof call.function?.arguments === "string"
|
|
641
|
+
? call.function.arguments
|
|
642
|
+
: "{}";
|
|
643
|
+
let argumentsValue;
|
|
644
|
+
try {
|
|
645
|
+
argumentsValue = JSON.parse(argumentsText);
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
return {
|
|
649
|
+
role: "tool",
|
|
650
|
+
tool_call_id: callId,
|
|
651
|
+
content: arenaJsonParsingError(name, argumentsText, error),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const malformedArguments = argumentsValue !== null &&
|
|
655
|
+
typeof argumentsValue === "object" &&
|
|
656
|
+
!Array.isArray(argumentsValue)
|
|
657
|
+
? argumentsValue.__malformedToolArguments
|
|
658
|
+
: undefined;
|
|
659
|
+
if (typeof malformedArguments === "string") {
|
|
660
|
+
try {
|
|
661
|
+
JSON.parse(malformedArguments);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
return {
|
|
665
|
+
role: "tool",
|
|
666
|
+
tool_call_id: callId,
|
|
667
|
+
content: arenaJsonParsingError(name, malformedArguments, error),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const validatedArguments = structuredClone(argumentsValue);
|
|
672
|
+
if (!validate(validatedArguments)) {
|
|
673
|
+
return {
|
|
674
|
+
role: "tool",
|
|
675
|
+
tool_call_id: callId,
|
|
676
|
+
content: arenaValidationError(name, argumentsValue, validate.errors),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const startedAt = Date.now();
|
|
680
|
+
let result;
|
|
681
|
+
try {
|
|
682
|
+
result = await this.dispatch(name, validatedArguments, callId, context);
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (error instanceof AskUserHandlerError) {
|
|
686
|
+
return {
|
|
687
|
+
role: "tool",
|
|
688
|
+
tool_call_id: callId,
|
|
689
|
+
content: json({ status: "error", message: error.message }),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
role: "tool",
|
|
694
|
+
tool_call_id: callId,
|
|
695
|
+
content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt)),
|
|
696
|
+
};
|
|
449
697
|
}
|
|
450
|
-
const result = await this.dispatch(name, argumentsValue, callId, context);
|
|
451
698
|
const content = typeof result === "string" ? result : json(result);
|
|
452
699
|
if (content.length > toolResultCharacterLimit) {
|
|
453
700
|
return {
|
|
@@ -468,13 +715,6 @@ class ArenaToolExecutor {
|
|
|
468
715
|
};
|
|
469
716
|
}
|
|
470
717
|
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
718
|
return {
|
|
479
719
|
role: "tool",
|
|
480
720
|
tool_call_id: callId,
|
|
@@ -485,6 +725,83 @@ class ArenaToolExecutor {
|
|
|
485
725
|
};
|
|
486
726
|
}
|
|
487
727
|
}
|
|
728
|
+
runtimeFailureResult(name, values, error, startedAt) {
|
|
729
|
+
if (this.signal?.aborted)
|
|
730
|
+
return { status: "aborted" };
|
|
731
|
+
const message = errorMessage(error);
|
|
732
|
+
const durationMs = Date.now() - startedAt;
|
|
733
|
+
if (name === "bash") {
|
|
734
|
+
const stderr = this.virtualizeBashOutput(message);
|
|
735
|
+
return {
|
|
736
|
+
status: "shell_error",
|
|
737
|
+
stderr,
|
|
738
|
+
stdout: "",
|
|
739
|
+
exit_code: null,
|
|
740
|
+
duration_ms: durationMs,
|
|
741
|
+
stderr_truncated: false,
|
|
742
|
+
stdout_truncated: false,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
if (name === "start_process") {
|
|
746
|
+
const cwd = String(values.cwd || "/home/user");
|
|
747
|
+
const code = error?.code;
|
|
748
|
+
const logTail = code === "ENOENT"
|
|
749
|
+
? `[invalid_argument] cwd '${cwd}' does not exist`
|
|
750
|
+
: message;
|
|
751
|
+
return {
|
|
752
|
+
status: "shell_error",
|
|
753
|
+
process_id: null,
|
|
754
|
+
pid: null,
|
|
755
|
+
exit_code: null,
|
|
756
|
+
log_tail: logTail,
|
|
757
|
+
listening_ports: [],
|
|
758
|
+
new_ports: [],
|
|
759
|
+
duration_ms: durationMs,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (name === "get_process_output") {
|
|
763
|
+
return {
|
|
764
|
+
status: "shell_error",
|
|
765
|
+
log_tail: message,
|
|
766
|
+
exit_code: null,
|
|
767
|
+
listening_ports: [],
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (name === "stop_process") {
|
|
771
|
+
return { status: "shell_error", log_tail: message };
|
|
772
|
+
}
|
|
773
|
+
if (name === "fetch_page") {
|
|
774
|
+
return { status: "error", error: fetchPageFailure(message) };
|
|
775
|
+
}
|
|
776
|
+
if (name === "add_voice") {
|
|
777
|
+
return {
|
|
778
|
+
status: "error",
|
|
779
|
+
message: prefixedFailure("Failed to audition voices: ", message),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
if (name === "generate_image") {
|
|
783
|
+
return {
|
|
784
|
+
status: "error",
|
|
785
|
+
message: prefixedFailure("Failed to generate image: ", message),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (name === "generate_speech") {
|
|
789
|
+
return {
|
|
790
|
+
status: "error",
|
|
791
|
+
message: prefixedFailure("Failed to generate speech: ", message),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
if (name === "image_search") {
|
|
795
|
+
return {
|
|
796
|
+
status: "error",
|
|
797
|
+
message: prefixedFailure("Image search failed: ", message),
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (name === "present_file") {
|
|
801
|
+
return { status: "error", message: "Error calling present file tool" };
|
|
802
|
+
}
|
|
803
|
+
return { status: "error", message };
|
|
804
|
+
}
|
|
488
805
|
takeVisualMessage(toolCallIds) {
|
|
489
806
|
// 生产契约:同一组 tool call 的全部图片汇入一条 user message,
|
|
490
807
|
// 由一个 arena-system-message 文本 part 领头。
|
|
@@ -820,8 +1137,24 @@ class ArenaToolExecutor {
|
|
|
820
1137
|
};
|
|
821
1138
|
}
|
|
822
1139
|
async editFile(rawPath, oldText, newText) {
|
|
823
|
-
|
|
824
|
-
|
|
1140
|
+
let target;
|
|
1141
|
+
let source;
|
|
1142
|
+
try {
|
|
1143
|
+
target = await this.confinedPath(rawPath);
|
|
1144
|
+
if (!(await stat(target)).isFile()) {
|
|
1145
|
+
return {
|
|
1146
|
+
status: "error",
|
|
1147
|
+
message: `File not found: ${rawPath}`,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
source = await readFile(target, "utf8");
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
return {
|
|
1154
|
+
status: "error",
|
|
1155
|
+
message: `Failed to read workspace file: ${rawPath}`,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
825
1158
|
// 仅精确匹配:曾有空白容错回退,但 T1(b) 回放证实生产没有——case-003
|
|
826
1159
|
// 四个 old_text 精确缺失的 edit 生产全部返回 Context not found,而回退
|
|
827
1160
|
// 会静默改错文件;全部参考成功样本也都能走精确路径复现。
|
|
@@ -834,7 +1167,15 @@ class ArenaToolExecutor {
|
|
|
834
1167
|
message: "Context not found. Read the file to verify the text exists.",
|
|
835
1168
|
};
|
|
836
1169
|
}
|
|
837
|
-
|
|
1170
|
+
try {
|
|
1171
|
+
await writeFile(target, source.replace(oldText, newText), "utf8");
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
return {
|
|
1175
|
+
status: "error",
|
|
1176
|
+
message: `Failed to write workspace file: ${rawPath}`,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
838
1179
|
return { status: "success", message: `Edited ${rawPath}.` };
|
|
839
1180
|
}
|
|
840
1181
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
@@ -878,6 +1219,15 @@ class ArenaToolExecutor {
|
|
|
878
1219
|
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
879
1220
|
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
880
1221
|
}
|
|
1222
|
+
// Current Arena v2 captures consistently expose local PDFs as binary
|
|
1223
|
+
// metadata. PDF text extraction belongs to fetch_page, not read_file.
|
|
1224
|
+
if (extension === ".pdf") {
|
|
1225
|
+
return {
|
|
1226
|
+
kind: "unsupported",
|
|
1227
|
+
mediaType: "application/pdf",
|
|
1228
|
+
size: fileSize,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
881
1231
|
const mainModelSupportsImages = kind === "image" ? supportsImageInput(this.config) : false;
|
|
882
1232
|
const fallbackModel = mainModelSupportsImages
|
|
883
1233
|
? undefined
|
|
@@ -975,9 +1325,18 @@ class ArenaToolExecutor {
|
|
|
975
1325
|
};
|
|
976
1326
|
}
|
|
977
1327
|
async writeWorkspaceFile(rawPath, content) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1328
|
+
let target;
|
|
1329
|
+
try {
|
|
1330
|
+
target = await this.confinedPath(rawPath);
|
|
1331
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
1332
|
+
await writeFile(target, content, "utf8");
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
return {
|
|
1336
|
+
status: "error",
|
|
1337
|
+
message: `Failed to write workspace file: ${rawPath}`,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
981
1340
|
return { status: "success" };
|
|
982
1341
|
}
|
|
983
1342
|
async webSearch(query, depth) {
|
|
@@ -1038,13 +1397,25 @@ class ArenaToolExecutor {
|
|
|
1038
1397
|
}
|
|
1039
1398
|
}
|
|
1040
1399
|
}
|
|
1400
|
+
if (!saved.length) {
|
|
1401
|
+
return {
|
|
1402
|
+
status: "error",
|
|
1403
|
+
message: "Image search failed: Strict safe search filtered all results for this query. " +
|
|
1404
|
+
"Try a different or more specific query.",
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1041
1407
|
return { status: "success", results: saved };
|
|
1042
1408
|
}
|
|
1043
1409
|
async presentFile(rawPath) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1410
|
+
try {
|
|
1411
|
+
const target = await this.confinedPath(rawPath);
|
|
1412
|
+
if (!(await stat(target)).isFile())
|
|
1413
|
+
throw new Error(rawPath);
|
|
1414
|
+
return { path: rawPath, status: "success" };
|
|
1415
|
+
}
|
|
1416
|
+
catch {
|
|
1417
|
+
return { status: "error", message: "Error calling present file tool" };
|
|
1418
|
+
}
|
|
1048
1419
|
}
|
|
1049
1420
|
async generateImage(rawPath, prompt, images) {
|
|
1050
1421
|
const target = await this.confinedPath(rawPath);
|
|
@@ -1062,7 +1433,13 @@ class ArenaToolExecutor {
|
|
|
1062
1433
|
if (!mimeType?.startsWith("image/")) {
|
|
1063
1434
|
throw new Error(`unsupported reference image format: ${imagePath}`);
|
|
1064
1435
|
}
|
|
1065
|
-
|
|
1436
|
+
let bytes;
|
|
1437
|
+
try {
|
|
1438
|
+
bytes = await readFile(source);
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
throw new Error(`Failed to read workspace file: ${imagePath}`);
|
|
1442
|
+
}
|
|
1066
1443
|
if (bytes.length > 20 * 1024 * 1024) {
|
|
1067
1444
|
throw new Error(`reference image exceeds 20 MB: ${imagePath}`);
|
|
1068
1445
|
}
|