@qwen-code/arena-agent 0.0.10-dev.6 → 0.0.10-dev.7

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.
@@ -12,11 +12,13 @@ import iconv from "iconv-lite";
12
12
  import mime from "mime/lite";
13
13
  import { executeBraveWebSearch } from "./brave-search.js";
14
14
  import { FetchPageReader } from "./fetch-page.js";
15
- import { replaceUniqueText } from "./file-edit.js";
15
+ import { replaceFirstText, replaceUniqueText } from "./file-edit.js";
16
16
  import { generatedImageBytesForExtension } from "./image.js";
17
17
  import { generateImageWithModelRouter, } from "./model-router-media.js";
18
18
  import { executeRoutifyWebFetch, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
19
+ import { replayResponseFor } from "./replay-responses.js";
19
20
  import { toolDefinitions } from "./spec.js";
21
+ import { boundedWebFetchResult, truncateCodeArenaText } from "./web-fetch-output.js";
20
22
  const knownTextApplicationMimeTypes = new Set([
21
23
  "application/ecmascript",
22
24
  "application/javascript",
@@ -451,9 +453,14 @@ function matchesGlob(filePath, pattern) {
451
453
  matcher.test(path.posix.basename(normalized))));
452
454
  });
453
455
  }
454
- function packageNameIsSafe(value) {
456
+ function packageNameIsSafe(value, mode) {
455
457
  if (!value || value.startsWith("-") || /\s/.test(value))
456
458
  return false;
459
+ if (mode === "react") {
460
+ // React captures reject version ranges before invoking npm, while accepting
461
+ // exact versions and tags such as latest. Keep other modes' validator intact.
462
+ return /^(?:@[\w.-]+\/)?[\w.-]+(?:@[\w.+-]+)?$/.test(value);
463
+ }
457
464
  return /^(?:@[\w.-]+\/)?[\w.-]+(?:@(?:[\w*^<>=|.+-]+))?$/.test(value);
458
465
  }
459
466
  function valueAtPath(value, instancePath) {
@@ -765,9 +772,14 @@ export class CodeToolExecutor {
765
772
  // 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
766
773
  // 之前短路,否则命令会照常 spawn 直到自然结束。
767
774
  this.signal?.throwIfAborted();
768
- const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
775
+ const parsedArguments = JSON.parse(typeof call.function?.arguments === "string"
769
776
  ? call.function.arguments
770
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;
771
783
  // The captured provider advertises these fields as required, but its
772
784
  // runtime still applies the documented defaults when a model omits
773
785
  // them. Preserve the byte-identical outgoing schema while matching the
@@ -779,6 +791,11 @@ export class CodeToolExecutor {
779
791
  else if (name === "list_files" || name === "glob_files") {
780
792
  argumentsValue.path ??= "";
781
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
+ }
782
799
  else if (name === "grep_files") {
783
800
  argumentsValue.path ??= "";
784
801
  argumentsValue.glob ??= "";
@@ -843,6 +860,13 @@ export class CodeToolExecutor {
843
860
  };
844
861
  }
845
862
  async dispatch(name, values, _toolCallId) {
863
+ // Replay mode (CODE_HARNESS_REPLAY_RESPONSES set): serve recorded outputs
864
+ // verbatim for nondeterministic tools (web_search/web_fetch/fetch_media/…)
865
+ // so replayed sessions stay offline and deterministic. Unlisted calls fall
866
+ // through to real execution.
867
+ const replayed = replayResponseFor(name, values);
868
+ if (replayed)
869
+ return replayed.output;
846
870
  switch (name) {
847
871
  case "read_file":
848
872
  return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit));
@@ -1064,7 +1088,9 @@ export class CodeToolExecutor {
1064
1088
  status: "success",
1065
1089
  file: {
1066
1090
  path: this.relativePath(target),
1067
- content: `${numbered}\n\n${footer}`,
1091
+ content: this.config.mode === "react"
1092
+ ? truncateCodeArenaText(`${numbered}\n\n${footer}`)
1093
+ : `${numbered}\n\n${footer}`,
1068
1094
  contentType: contentTypeForFile(target),
1069
1095
  },
1070
1096
  totalLines: lines.length,
@@ -1147,15 +1173,24 @@ export class CodeToolExecutor {
1147
1173
  message: `File "${rawPath}" does not exist`,
1148
1174
  };
1149
1175
  }
1150
- const edited = replaceUniqueText(source, context, replacement, {
1176
+ // React replaces the first exact occurrence, including when an indented
1177
+ // context also occurs on a later, more deeply indented line. Keep the
1178
+ // existing uniqueness check for fuzzy fallback and the other modes.
1179
+ const replace = this.config.mode === "react" && context.length > 0 && source.includes(context)
1180
+ ? replaceFirstText : replaceUniqueText;
1181
+ const edited = replace(source, context, replacement, {
1151
1182
  whitespaceInsensitiveMultiline: true,
1183
+ whitespaceInsensitiveSingleLine: this.config.mode === "react",
1184
+ preserveNewlines: this.config.mode === "react",
1152
1185
  });
1153
1186
  if (edited === undefined) {
1154
1187
  // Captured sessions re-issue an edit after compaction or a retry. The
1155
1188
  // provider reports success without touching the file when the context
1156
1189
  // is already gone and the replacement is in place (f39 re-issues four
1157
1190
  // dashboard edits this way).
1158
- if (replacement && source.includes(replacement)) {
1191
+ // That retry behavior comes from Full Stack captures. React still
1192
+ // reports Context not found even when the replacement is present.
1193
+ if (this.config.mode !== "react" && replacement && source.includes(replacement)) {
1159
1194
  return this.mutationResult("Edited", rawPath);
1160
1195
  }
1161
1196
  return {
@@ -1198,6 +1233,19 @@ export class CodeToolExecutor {
1198
1233
  }
1199
1234
  current?.lines.push(line);
1200
1235
  }
1236
+ if (this.config.mode === "react") {
1237
+ // React parses duplicate operation paths before applying any writes.
1238
+ // In particular, Delete + Add of the same file is an Add duplicate.
1239
+ const seen = new Set();
1240
+ for (const operation of operations) {
1241
+ const target = await this.confinedPath(operation.path);
1242
+ if (seen.has(target)) {
1243
+ const label = operation.kind[0].toUpperCase() + operation.kind.slice(1);
1244
+ return { status: "error", message: `${label} File Error: Duplicate Path: ${operation.path}` };
1245
+ }
1246
+ seen.add(target);
1247
+ }
1248
+ }
1201
1249
  for (const operation of operations) {
1202
1250
  const target = await this.confinedPath(operation.path);
1203
1251
  if (operation.kind === "add") {
@@ -1313,6 +1361,11 @@ export class CodeToolExecutor {
1313
1361
  return { status: "success" };
1314
1362
  }
1315
1363
  async listCodeFiles(rawPath) {
1364
+ if (this.config.mode === "react" && rawPath === "/")
1365
+ rawPath = ".";
1366
+ if (this.config.mode === "react" && (path.isAbsolute(rawPath) || rawPath.includes(".."))) {
1367
+ return { status: "error", message: "Path must be relative to project root (no absolute paths or '..' traversal)" };
1368
+ }
1316
1369
  const target = await this.confinedPath(rawPath || ".");
1317
1370
  if (!(await this.existsWithExactCase(rawPath || ".")) ||
1318
1371
  !existsSync(target)) {
@@ -1321,7 +1374,8 @@ export class CodeToolExecutor {
1321
1374
  const files = this.orderListedFiles(await this.walkFiles(target));
1322
1375
  return {
1323
1376
  status: "success",
1324
- files: files.map((filePath) => ({ path: filePath })),
1377
+ files: (this.config.mode === "react" ? files.slice(0, 500) : files)
1378
+ .map((filePath) => ({ path: filePath })),
1325
1379
  };
1326
1380
  }
1327
1381
  async grepCodeFiles(values) {
@@ -1408,8 +1462,10 @@ export class CodeToolExecutor {
1408
1462
  continue;
1409
1463
  }
1410
1464
  const lines = content.split("\n");
1465
+ const searchLineCount = this.config.mode === "react" && lines.at(-1) === ""
1466
+ ? lines.length - 1 : lines.length;
1411
1467
  const matchingLines = [];
1412
- for (let index = 0; index < lines.length; index += 1) {
1468
+ for (let index = 0; index < searchLineCount; index += 1) {
1413
1469
  if (expression.test(lines[index]))
1414
1470
  matchingLines.push(index);
1415
1471
  }
@@ -1440,6 +1496,24 @@ export class CodeToolExecutor {
1440
1496
  if (previousAfter.length > 0)
1441
1497
  match.contextAfter = previousAfter;
1442
1498
  }
1499
+ else if (this.config.mode === "react") {
1500
+ // The captured adapter attaches context lines to the preceding
1501
+ // match, even across ripgrep group separators. Only a file's first
1502
+ // match receives contextBefore. Do not duplicate overlapping groups.
1503
+ if (position === 0 && before > 0) {
1504
+ const contextBefore = lines.slice(Math.max(0, index - before), index);
1505
+ if (contextBefore.length)
1506
+ match.contextBefore = contextBefore;
1507
+ }
1508
+ const nextMatch = matchingLines[position + 1];
1509
+ const end = Math.min(index + 1 + after, nextMatch ?? searchLineCount);
1510
+ const contextAfter = lines.slice(index + 1, end);
1511
+ if (nextMatch !== undefined && before > 0) {
1512
+ contextAfter.push(...lines.slice(Math.max(end, index + 1, nextMatch - before), nextMatch));
1513
+ }
1514
+ if (contextAfter.length)
1515
+ match.contextAfter = contextAfter;
1516
+ }
1443
1517
  else {
1444
1518
  if (before > 0) {
1445
1519
  match.contextBefore = lines.slice(Math.max(0, index - before), index);
@@ -1495,7 +1569,7 @@ export class CodeToolExecutor {
1495
1569
  return { status: "success", paths: [], truncated: false };
1496
1570
  }
1497
1571
  const allPaths = files
1498
- .filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern))
1572
+ .filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern) || (this.config.mode === "react" && matchesGlob(this.relativePath(filePath), pattern)))
1499
1573
  .map((filePath) => this.relativePath(filePath));
1500
1574
  return {
1501
1575
  status: "success",
@@ -1568,11 +1642,11 @@ export class CodeToolExecutor {
1568
1642
  });
1569
1643
  }
1570
1644
  async installNpmPackages(packages) {
1571
- const invalid = packages.find((packageName) => !packageNameIsSafe(packageName));
1645
+ const invalid = packages.find((packageName) => !packageNameIsSafe(packageName, this.config.mode));
1572
1646
  if (invalid) {
1573
1647
  return {
1574
1648
  status: "error",
1575
- message: `Invalid package name: ${invalid}`,
1649
+ message: `Invalid package name: ${this.config.mode === "react" ? JSON.stringify(invalid) : invalid}`,
1576
1650
  };
1577
1651
  }
1578
1652
  const args = ["install", "-S"];
@@ -1611,11 +1685,15 @@ export class CodeToolExecutor {
1611
1685
  };
1612
1686
  }
1613
1687
  async fullStackBash(command, workdir) {
1688
+ const cwd = workdir.trim() || ".";
1689
+ if (path.isAbsolute(cwd) || cwd.includes("..")) {
1690
+ throw new Error(`Invalid workdir: ${cwd}. Commands run from the project root by default. Only use workdir for relative subdirectories, never absolute paths.`);
1691
+ }
1614
1692
  const configuredTimeout = Number(process.env.CODE_HARNESS_BASH_TIMEOUT_SECONDS || 300);
1615
1693
  const timeoutSeconds = Number.isFinite(configuredTimeout) && configuredTimeout > 0
1616
1694
  ? configuredTimeout
1617
1695
  : 300;
1618
- const result = await this.bash(command, workdir.trim() || ".", timeoutSeconds);
1696
+ const result = await this.bash(command, cwd, timeoutSeconds);
1619
1697
  const exitCode = result.exit_code;
1620
1698
  const stdout = String(result.stdout || "");
1621
1699
  const stderr = String(result.stderr || "");
@@ -1886,11 +1964,7 @@ export class CodeToolExecutor {
1886
1964
  const result = routifyWebFetchEnabled()
1887
1965
  ? await executeRoutifyWebFetch(url, format, { signal: this.signal })
1888
1966
  : await this.pageReader.readFormatted(url, format);
1889
- return {
1890
- status: "success",
1891
- title: String(result.title || "No Title Found"),
1892
- content: String(result.content || ""),
1893
- };
1967
+ return boundedWebFetchResult(result);
1894
1968
  }
1895
1969
  async pexelsRequest(pathname, parameters) {
1896
1970
  const apiKey = process.env.PEXELS_API_KEY;
@@ -1929,7 +2003,7 @@ export class CodeToolExecutor {
1929
2003
  },
1930
2004
  });
1931
2005
  }
1932
- mapPexelsPhotos(payload) {
2006
+ mapPexelsPhotos(payload, orientation) {
1933
2007
  const photos = Array.isArray(payload.photos) ? payload.photos : [];
1934
2008
  return photos.map((value) => {
1935
2009
  const photo = record(value);
@@ -1938,12 +2012,14 @@ export class CodeToolExecutor {
1938
2012
  type: "image",
1939
2013
  id: Number(photo.id),
1940
2014
  pexelsUrl: stringValue(photo.url),
1941
- recommendedUrl: stringValue(source.large2x) ||
2015
+ recommendedUrl: (orientation === "landscape" || orientation === "portrait"
2016
+ ? stringValue(source[orientation]) : "") ||
2017
+ stringValue(source.large2x) ||
1942
2018
  stringValue(source.large) ||
1943
2019
  stringValue(source.original),
1944
- thumbnailUrl: stringValue(source.medium) ||
1945
- stringValue(source.small) ||
1946
- stringValue(source.tiny),
2020
+ thumbnailUrl: stringValue(source.tiny) ||
2021
+ stringValue(source.medium) ||
2022
+ stringValue(source.small),
1947
2023
  width: Number(photo.width),
1948
2024
  height: Number(photo.height),
1949
2025
  creatorName: stringValue(photo.photographer),
@@ -1963,20 +2039,29 @@ export class CodeToolExecutor {
1963
2039
  const files = Array.isArray(video.video_files)
1964
2040
  ? video.video_files.map(record)
1965
2041
  : [];
1966
- const preferred = files.find((item) => stringValue(item.quality) === "hd") ||
1967
- files[0] ||
1968
- {};
2042
+ // Captures contain full-resolution HD/UHD files. A quality label alone
2043
+ // does not order renditions; compare their actual pixel dimensions.
2044
+ const playable = files.filter((item) => stringValue(item.file_type) === "video/mp4" && stringValue(item.link));
2045
+ const preferred = playable.sort((left, right) => (Number(right.width) || 0) * (Number(right.height) || 0) -
2046
+ (Number(left.width) || 0) * (Number(left.height) || 0))[0] || files[0] || {};
1969
2047
  return {
1970
2048
  type: "video",
1971
2049
  id: Number(video.id),
1972
2050
  pexelsUrl: stringValue(video.url),
1973
2051
  recommendedUrl: stringValue(preferred.link),
1974
- thumbnailUrl: stringValue(pictures[0]?.picture),
2052
+ thumbnailUrl: stringValue(video.image) || stringValue(pictures[0]?.picture),
1975
2053
  width: Number(video.width),
1976
2054
  height: Number(video.height),
1977
2055
  duration: Number(video.duration),
1978
2056
  creatorName: stringValue(user.name),
1979
2057
  creatorUrl: stringValue(user.url),
2058
+ videoFile: {
2059
+ url: stringValue(preferred.link),
2060
+ width: Number(preferred.width),
2061
+ height: Number(preferred.height),
2062
+ ...(preferred.quality !== undefined ? { quality: preferred.quality } : {}),
2063
+ fileType: stringValue(preferred.file_type),
2064
+ },
1980
2065
  };
1981
2066
  });
1982
2067
  }
@@ -1994,7 +2079,7 @@ export class CodeToolExecutor {
1994
2079
  if (mediaType === "image" || mediaType === "both") {
1995
2080
  const photos = await this.pexelsRequest("/v1/search", common);
1996
2081
  totalResults += Number(photos.total_results || 0);
1997
- photoResults = this.mapPexelsPhotos(photos);
2082
+ photoResults = this.mapPexelsPhotos(photos, orientation);
1998
2083
  }
1999
2084
  if (mediaType === "video" || mediaType === "both") {
2000
2085
  const videos = await this.pexelsRequest("/v1/videos/search", common);
@@ -2016,13 +2101,20 @@ export class CodeToolExecutor {
2016
2101
  }
2017
2102
  async fetchMedia(query, mediaType, count, orientation, size, locale) {
2018
2103
  const requestedCount = Math.min(10, Math.max(1, Math.trunc(count)));
2019
- return this.fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale);
2104
+ try {
2105
+ return await this.fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale);
2106
+ }
2107
+ catch (error) {
2108
+ throw new Error(`Media fetch failed: ${errorMessage(error)}`);
2109
+ }
2020
2110
  }
2021
2111
  async generateImage(rawPath, prompt, images) {
2022
2112
  if (this.generatedImageCount >= 10) {
2023
2113
  return {
2024
2114
  status: "error",
2025
- message: "generate_image can be used at most 10 times per session",
2115
+ message: this.config.mode === "react"
2116
+ ? "Failed to generate image: Image generation limit of 10 reached for this session. Use CSS gradients, SVG, or placeholder images for remaining visuals."
2117
+ : "generate_image can be used at most 10 times per session",
2026
2118
  };
2027
2119
  }
2028
2120
  this.generatedImageCount += 1;