@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/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({ allErrors: true, strict: false });
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?.throwIfAborted();
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
- throw new Error(`unknown tool: ${name}`);
444
- if (!validate(argumentsValue)) {
445
- const errors = (validate.errors || [])
446
- .map((error) => `${error.instancePath || "/"} ${error.message}`)
447
- .join("; ");
448
- throw new Error(`invalid arguments: ${errors}`);
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
- const target = await this.confinedPath(rawPath);
824
- const source = await readFile(target, "utf8");
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
- await writeFile(target, source.replace(oldText, newText), "utf8");
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
- const target = await this.confinedPath(rawPath);
979
- mkdirSync(path.dirname(target), { recursive: true });
980
- await writeFile(target, content, "utf8");
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
- const target = await this.confinedPath(rawPath);
1045
- if (!(await stat(target)).isFile())
1046
- throw new Error(rawPath);
1047
- return { path: rawPath, status: "success" };
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
- const bytes = await readFile(source);
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
  }