@qwen-code/arena-agent 0.0.10-dev.7 → 0.0.10-dev.8-test.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.
@@ -13,7 +13,8 @@ import mime from "mime/lite";
13
13
  import { executeBraveWebSearch } from "./brave-search.js";
14
14
  import { FetchPageReader } from "./fetch-page.js";
15
15
  import { replaceFirstText, replaceUniqueText } from "./file-edit.js";
16
- import { generatedImageBytesForExtension } from "./image.js";
16
+ import { detectedImageMimeType, generatedImageBytesForExtension } from "./image.js";
17
+ import { supportsImageInput } from "./model-capabilities.js";
17
18
  import { generateImageWithModelRouter, } from "./model-router-media.js";
18
19
  import { executeRoutifyWebFetch, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
19
20
  import { replayResponseFor } from "./replay-responses.js";
@@ -187,6 +188,42 @@ const visibleImageExtensions = new Set([
187
188
  ".png",
188
189
  ".webp",
189
190
  ]);
191
+ const readableImageExtensions = new Set([".jpeg", ".jpg", ".png"]);
192
+ const maximumReadableImageBytes = 10 * 1024 * 1024;
193
+ function readableImageMimeType(data) {
194
+ if (data.length >= 8 &&
195
+ data.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
196
+ return "image/png";
197
+ if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
198
+ return "image/jpeg";
199
+ return undefined;
200
+ }
201
+ function hasReadableImageStructure(data, mediaType) {
202
+ if (detectedImageMimeType(data) !== mediaType)
203
+ return false;
204
+ if (mediaType === "image/jpeg") {
205
+ return data.length >= 4 && data[data.length - 2] === 0xff && data[data.length - 1] === 0xd9;
206
+ }
207
+ if (data.readUInt32BE(16) === 0 || data.readUInt32BE(20) === 0)
208
+ return false;
209
+ let offset = 8;
210
+ let hasImageData = false;
211
+ while (offset + 12 <= data.length) {
212
+ const length = data.readUInt32BE(offset);
213
+ const end = offset + 12 + length;
214
+ if (end > data.length)
215
+ return false;
216
+ const type = data.toString("ascii", offset + 4, offset + 8);
217
+ if (offset === 8 && (type !== "IHDR" || length !== 13))
218
+ return false;
219
+ if (type === "IDAT")
220
+ hasImageData = true;
221
+ if (type === "IEND")
222
+ return length === 0 && hasImageData;
223
+ offset = end;
224
+ }
225
+ return false;
226
+ }
190
227
  // All 39 successful, unmutated list_files captures use this exact order,
191
228
  // independently of whether the requested path is the project root or src/.
192
229
  // It is the seed manifest order, not an alphabetical/filesystem order.
@@ -232,6 +269,7 @@ const visualIntroText = "<code-harness-system-message>\n" +
232
269
  "</code-harness-system-message>";
233
270
  const bashOutputLimit = 20_000;
234
271
  const lintOutputLimit = 10_000;
272
+ const grepLineCharacterLimit = 2_000;
235
273
  // 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
236
274
  // 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
237
275
  const bashTruncationMarker = "\n... output truncated ...\n";
@@ -382,6 +420,14 @@ function workspaceFileKind(filePath, sample) {
382
420
  function json(value) {
383
421
  return JSON.stringify(value);
384
422
  }
423
+ function capGrepLine(value) {
424
+ // Production captures cap every returned grep line independently. This is
425
+ // intentionally a prefix without a marker, including for the matching line.
426
+ return value.slice(0, grepLineCharacterLimit);
427
+ }
428
+ function capGrepLines(values) {
429
+ return values.map(capGrepLine);
430
+ }
385
431
  function errorMessage(error) {
386
432
  return error instanceof Error ? error.message : String(error);
387
433
  }
@@ -755,8 +801,8 @@ export class CodeToolExecutor {
755
801
  this.validators.set("apply_patch", this.ajv.compile(APPLY_PATCH_PARAMETERS));
756
802
  }
757
803
  }
758
- // The tool schemas offered to the model for this session. Identical to the
759
- // canonical trace set, adjusted for the selected workspace runtime.
804
+ // The tools offered to the model for this session, with captured schemas
805
+ // and descriptions adjusted for current capabilities and workspace runtime.
760
806
  availableTools() {
761
807
  return structuredClone(this.definitions);
762
808
  }
@@ -764,6 +810,125 @@ export class CodeToolExecutor {
764
810
  const names = this.definitions.map((tool) => tool.function.name);
765
811
  return `Model tried to call unavailable tool '${name}'. Available tools: ${names.join(", ")}.`;
766
812
  }
813
+ rawArguments(call) {
814
+ return typeof call.function?.arguments === "string"
815
+ ? call.function.arguments
816
+ : "{}";
817
+ }
818
+ normalizeArguments(name, parsedArguments) {
819
+ // Keep the parsed value available to checkpoint hooks while applying the
820
+ // same captured runtime defaults to the independent dispatch value.
821
+ const argumentsValue = (this.config.mode === "react" && typeof parsedArguments === "string"
822
+ ? {}
823
+ : structuredClone(parsedArguments));
824
+ // The captured provider advertises these fields as required, but its
825
+ // runtime still applies the documented defaults when a model omits
826
+ // them. Preserve the byte-identical outgoing schema while matching the
827
+ // observed execution behavior.
828
+ if (name === "read_file") {
829
+ argumentsValue.offset ??= 1;
830
+ argumentsValue.limit ??= 2000;
831
+ }
832
+ else if (name === "list_files" || name === "glob_files") {
833
+ argumentsValue.path ??= "";
834
+ }
835
+ else if (this.config.mode === "full-stack" && name === "bash" &&
836
+ argumentsValue && typeof argumentsValue === "object" &&
837
+ !Array.isArray(argumentsValue) && argumentsValue.workdir === undefined) {
838
+ argumentsValue.workdir = "";
839
+ }
840
+ else if (name === "grep_files") {
841
+ argumentsValue.path ??= "";
842
+ argumentsValue.glob ??= "";
843
+ argumentsValue.output_mode ??= "content";
844
+ argumentsValue["-i"] ??= false;
845
+ argumentsValue["-B"] ??= 0;
846
+ argumentsValue["-A"] ??= 0;
847
+ argumentsValue["-C"] ??= 0;
848
+ argumentsValue.context ??= 0;
849
+ }
850
+ return argumentsValue;
851
+ }
852
+ validationMessage(name, value, validate) {
853
+ if (validate(value))
854
+ return undefined;
855
+ const issues = validationIssues(name, value, validate.errors || [], this.definitions);
856
+ return (`Invalid input for tool ${name}: Type validation failed: ` +
857
+ `Value: ${JSON.stringify(value)}.\n` +
858
+ `Error message: ${JSON.stringify(issues, null, 2)}`);
859
+ }
860
+ inspect(call) {
861
+ const id = String(call.id || "");
862
+ const name = String(call.function?.name || "");
863
+ const rawArguments = this.rawArguments(call);
864
+ const validate = this.validators.get(name);
865
+ if (!validate) {
866
+ let parsedArguments = undefined;
867
+ let parsed = false;
868
+ try {
869
+ parsedArguments = JSON.parse(rawArguments);
870
+ parsed = true;
871
+ }
872
+ catch {
873
+ // Tool-name validation takes precedence. Parsing is best-effort here
874
+ // only so a valid argument payload remains available to the hook.
875
+ }
876
+ return {
877
+ id,
878
+ name,
879
+ rawArguments,
880
+ ...(parsed ? { arguments: parsedArguments } : {}),
881
+ validation: "unknown_tool",
882
+ validationMessage: this.unavailableToolMessage(name),
883
+ };
884
+ }
885
+ let parsedArguments;
886
+ try {
887
+ parsedArguments = JSON.parse(rawArguments);
888
+ }
889
+ catch (error) {
890
+ return {
891
+ id,
892
+ name,
893
+ rawArguments,
894
+ validation: "invalid_json",
895
+ validationMessage: `Invalid input for tool ${name}: JSON parsing failed: Text: ` +
896
+ `${rawArguments}.\nError message: ${errorMessage(error)}`,
897
+ };
898
+ }
899
+ let argumentsValue;
900
+ try {
901
+ argumentsValue = this.normalizeArguments(name, parsedArguments);
902
+ }
903
+ catch (error) {
904
+ return {
905
+ id,
906
+ name,
907
+ rawArguments,
908
+ arguments: parsedArguments,
909
+ validation: "invalid_arguments",
910
+ validationMessage: errorMessage(error),
911
+ };
912
+ }
913
+ const validationMessage = this.validationMessage(name, argumentsValue, validate);
914
+ if (validationMessage !== undefined) {
915
+ return {
916
+ id,
917
+ name,
918
+ rawArguments,
919
+ arguments: parsedArguments,
920
+ validation: "invalid_arguments",
921
+ validationMessage,
922
+ };
923
+ }
924
+ return {
925
+ id,
926
+ name,
927
+ rawArguments,
928
+ arguments: parsedArguments,
929
+ validation: "valid",
930
+ };
931
+ }
767
932
  async execute(call, _context) {
768
933
  const callId = String(call.id || "");
769
934
  const name = String(call.function?.name || "");
@@ -772,40 +937,8 @@ export class CodeToolExecutor {
772
937
  // 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
773
938
  // 之前短路,否则命令会照常 spawn 直到自然结束。
774
939
  this.signal?.throwIfAborted();
775
- const parsedArguments = JSON.parse(typeof call.function?.arguments === "string"
776
- ? call.function.arguments
777
- : "{}");
778
- // React captures normalize a string-valued argument payload to an empty
779
- // object before validating required fields (rather than echoing the string).
780
- const argumentsValue = this.config.mode === "react" && typeof parsedArguments === "string"
781
- ? {}
782
- : parsedArguments;
783
- // The captured provider advertises these fields as required, but its
784
- // runtime still applies the documented defaults when a model omits
785
- // them. Preserve the byte-identical outgoing schema while matching the
786
- // observed execution behavior.
787
- if (name === "read_file") {
788
- argumentsValue.offset ??= 1;
789
- argumentsValue.limit ??= 2000;
790
- }
791
- else if (name === "list_files" || name === "glob_files") {
792
- argumentsValue.path ??= "";
793
- }
794
- else if (this.config.mode === "full-stack" && name === "bash" &&
795
- argumentsValue && typeof argumentsValue === "object" &&
796
- !Array.isArray(argumentsValue) && argumentsValue.workdir === undefined) {
797
- argumentsValue.workdir = "";
798
- }
799
- else if (name === "grep_files") {
800
- argumentsValue.path ??= "";
801
- argumentsValue.glob ??= "";
802
- argumentsValue.output_mode ??= "content";
803
- argumentsValue["-i"] ??= false;
804
- argumentsValue["-B"] ??= 0;
805
- argumentsValue["-A"] ??= 0;
806
- argumentsValue["-C"] ??= 0;
807
- argumentsValue.context ??= 0;
808
- }
940
+ const parsedArguments = JSON.parse(this.rawArguments(call));
941
+ const argumentsValue = this.normalizeArguments(name, parsedArguments);
809
942
  const validate = this.validators.get(name);
810
943
  if (!validate) {
811
944
  return {
@@ -814,14 +947,12 @@ export class CodeToolExecutor {
814
947
  content: this.unavailableToolMessage(name),
815
948
  };
816
949
  }
817
- if (!validate(argumentsValue)) {
818
- const issues = validationIssues(name, argumentsValue, validate.errors || [], this.definitions);
950
+ const validationMessage = this.validationMessage(name, argumentsValue, validate);
951
+ if (validationMessage !== undefined) {
819
952
  return {
820
953
  role: "tool",
821
954
  tool_call_id: callId,
822
- content: `Invalid input for tool ${name}: Type validation failed: ` +
823
- `Value: ${JSON.stringify(argumentsValue)}.\n` +
824
- `Error message: ${JSON.stringify(issues, null, 2)}`,
955
+ content: validationMessage,
825
956
  };
826
957
  }
827
958
  const result = await this.dispatch(name, argumentsValue, callId);
@@ -869,7 +1000,7 @@ export class CodeToolExecutor {
869
1000
  return replayed.output;
870
1001
  switch (name) {
871
1002
  case "read_file":
872
- return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit));
1003
+ return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit), _toolCallId);
873
1004
  case "create_file":
874
1005
  return this.createCodeFile(String(values.path), String(values.content));
875
1006
  case "edit_file":
@@ -1043,7 +1174,7 @@ export class CodeToolExecutor {
1043
1174
  await visit(searchRoot);
1044
1175
  return files;
1045
1176
  }
1046
- async readCodeFile(rawPath, offset, limit) {
1177
+ async readCodeFile(rawPath, offset, limit, toolCallId) {
1047
1178
  const target = await this.confinedPath(rawPath);
1048
1179
  let data;
1049
1180
  try {
@@ -1057,10 +1188,50 @@ export class CodeToolExecutor {
1057
1188
  message: `File "${rawPath}" does not exist`,
1058
1189
  };
1059
1190
  }
1191
+ if (readableImageExtensions.has(path.extname(target).toLowerCase())) {
1192
+ const mediaType = readableImageMimeType(data);
1193
+ if (!mediaType) {
1194
+ return {
1195
+ status: "error",
1196
+ message: `"${rawPath}" is not a valid image — the file contents are not PNG or JPEG. Re-download or regenerate the file, then read it again.`,
1197
+ };
1198
+ }
1199
+ if (!supportsImageInput(this.config)) {
1200
+ return {
1201
+ status: "error",
1202
+ message: `This model cannot view images. Reference "${rawPath}" by its path instead.`,
1203
+ };
1204
+ }
1205
+ if (data.length > maximumReadableImageBytes) {
1206
+ return {
1207
+ status: "error",
1208
+ message: `Image "${rawPath}" is too large to view (${Math.round(data.length / 1024)} KB, max 10240 KB).`,
1209
+ };
1210
+ }
1211
+ if (!hasReadableImageStructure(data, mediaType)) {
1212
+ return {
1213
+ status: "error",
1214
+ message: `"${rawPath}" is not a valid image — the file contents are not PNG or JPEG. Re-download or regenerate the file, then read it again.`,
1215
+ };
1216
+ }
1217
+ const relativePath = this.relativePath(target);
1218
+ this.pendingVisualContent.set(toolCallId, [
1219
+ { image: `data:${mediaType};base64,${data.toString("base64")}` },
1220
+ ]);
1221
+ return {
1222
+ file: {
1223
+ content: `[Image file ${relativePath} (${Math.round(data.length / 1024)} KB, ${mediaType})]`,
1224
+ contentType: "text/plain",
1225
+ path: relativePath,
1226
+ },
1227
+ image: { mediaType, sizeBytes: data.length },
1228
+ status: "success",
1229
+ };
1230
+ }
1060
1231
  if (workspaceFileKind(target, data.subarray(0, 4096)) !== "text") {
1061
1232
  return {
1062
1233
  status: "error",
1063
- message: `File "${rawPath}" is not a supported text file`,
1234
+ message: `Cannot read binary file "${rawPath}". Only text files and PNG/JPEG images are supported.`,
1064
1235
  };
1065
1236
  }
1066
1237
  const lines = decodeText(data).split("\n");
@@ -1478,14 +1649,15 @@ export class CodeToolExecutor {
1478
1649
  const match = {
1479
1650
  path: relative,
1480
1651
  lineNumber: index + 1,
1481
- lineContent: lines[index],
1652
+ lineContent: capGrepLine(lines[index]),
1482
1653
  };
1483
1654
  if (groupedContext) {
1484
1655
  const contextBefore = position === 0
1485
1656
  ? lines.slice(Math.max(0, index - before), index)
1486
1657
  : previousAfter;
1487
- if (contextBefore.length > 0)
1488
- match.contextBefore = contextBefore;
1658
+ if (contextBefore.length > 0) {
1659
+ match.contextBefore = capGrepLines(contextBefore);
1660
+ }
1489
1661
  const nextMatch = matchingLines[position + 1] ?? lines.length;
1490
1662
  previousAfter = lines.slice(index + 1, Math.min(index + 1 + contextSpan, nextMatch));
1491
1663
  if (nextMatch === lines.length &&
@@ -1493,8 +1665,9 @@ export class CodeToolExecutor {
1493
1665
  index + 1 + contextSpan >= lines.length) {
1494
1666
  previousAfter = previousAfter.slice(0, -1);
1495
1667
  }
1496
- if (previousAfter.length > 0)
1497
- match.contextAfter = previousAfter;
1668
+ if (previousAfter.length > 0) {
1669
+ match.contextAfter = capGrepLines(previousAfter);
1670
+ }
1498
1671
  }
1499
1672
  else if (this.config.mode === "react") {
1500
1673
  // The captured adapter attaches context lines to the preceding
@@ -1502,8 +1675,9 @@ export class CodeToolExecutor {
1502
1675
  // match receives contextBefore. Do not duplicate overlapping groups.
1503
1676
  if (position === 0 && before > 0) {
1504
1677
  const contextBefore = lines.slice(Math.max(0, index - before), index);
1505
- if (contextBefore.length)
1506
- match.contextBefore = contextBefore;
1678
+ if (contextBefore.length) {
1679
+ match.contextBefore = capGrepLines(contextBefore);
1680
+ }
1507
1681
  }
1508
1682
  const nextMatch = matchingLines[position + 1];
1509
1683
  const end = Math.min(index + 1 + after, nextMatch ?? searchLineCount);
@@ -1511,15 +1685,16 @@ export class CodeToolExecutor {
1511
1685
  if (nextMatch !== undefined && before > 0) {
1512
1686
  contextAfter.push(...lines.slice(Math.max(end, index + 1, nextMatch - before), nextMatch));
1513
1687
  }
1514
- if (contextAfter.length)
1515
- match.contextAfter = contextAfter;
1688
+ if (contextAfter.length) {
1689
+ match.contextAfter = capGrepLines(contextAfter);
1690
+ }
1516
1691
  }
1517
1692
  else {
1518
1693
  if (before > 0) {
1519
- match.contextBefore = lines.slice(Math.max(0, index - before), index);
1694
+ match.contextBefore = capGrepLines(lines.slice(Math.max(0, index - before), index));
1520
1695
  }
1521
1696
  if (after > 0) {
1522
- match.contextAfter = lines.slice(index + 1, index + 1 + after);
1697
+ match.contextAfter = capGrepLines(lines.slice(index + 1, index + 1 + after));
1523
1698
  }
1524
1699
  }
1525
1700
  matches.push(match);