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

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.
Files changed (51) hide show
  1. package/.env.example +20 -9
  2. package/CHANGELOG.md +188 -0
  3. package/README.md +21 -19
  4. package/dist/agent.d.ts +8 -0
  5. package/dist/agent.js +32 -12
  6. package/dist/agent.js.map +1 -1
  7. package/dist/cli.js +87 -20
  8. package/dist/cli.js.map +1 -1
  9. package/dist/code-cli.js +0 -0
  10. package/dist/code-tools.d.ts +5 -0
  11. package/dist/code-tools.js +254 -64
  12. package/dist/code-tools.js.map +1 -1
  13. package/dist/compaction.d.ts +5 -0
  14. package/dist/compaction.js +57 -6
  15. package/dist/compaction.js.map +1 -1
  16. package/dist/config.d.ts +0 -2
  17. package/dist/config.js +1 -3
  18. package/dist/config.js.map +1 -1
  19. package/dist/file-edit.d.ts +2 -0
  20. package/dist/file-edit.js +9 -4
  21. package/dist/file-edit.js.map +1 -1
  22. package/dist/history.js +17 -0
  23. package/dist/history.js.map +1 -1
  24. package/dist/midturn-checkpoint-hook.d.ts +16 -0
  25. package/dist/midturn-checkpoint-hook.js +30 -0
  26. package/dist/midturn-checkpoint-hook.js.map +1 -0
  27. package/dist/provider.d.ts +0 -1
  28. package/dist/provider.js +13 -42
  29. package/dist/provider.js.map +1 -1
  30. package/dist/replay-responses.d.ts +17 -0
  31. package/dist/replay-responses.js +100 -0
  32. package/dist/replay-responses.js.map +1 -0
  33. package/dist/routify-web-fetch.js +18 -4
  34. package/dist/routify-web-fetch.js.map +1 -1
  35. package/dist/snapshot-client.d.ts +10 -0
  36. package/dist/snapshot-client.js +39 -0
  37. package/dist/snapshot-client.js.map +1 -0
  38. package/dist/tools.d.ts +2 -0
  39. package/dist/tools.js +134 -23
  40. package/dist/tools.js.map +1 -1
  41. package/dist/tui.js +1 -1
  42. package/dist/tui.js.map +1 -1
  43. package/dist/web-fetch-output.d.ts +9 -0
  44. package/dist/web-fetch-output.js +38 -0
  45. package/dist/web-fetch-output.js.map +1 -0
  46. package/docs/BINARY_DISTRIBUTION.md +25 -16
  47. package/docs/MODES_AND_RELEASES.md +28 -5
  48. package/package.json +5 -1
  49. package/scripts/README.md +62 -0
  50. package/scripts/check-changelog.mjs +66 -0
  51. package/dist/web/app.d.ts +0 -1
@@ -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",
@@ -230,6 +232,7 @@ const visualIntroText = "<code-harness-system-message>\n" +
230
232
  "</code-harness-system-message>";
231
233
  const bashOutputLimit = 20_000;
232
234
  const lintOutputLimit = 10_000;
235
+ const grepLineCharacterLimit = 2_000;
233
236
  // 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
234
237
  // 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
235
238
  const bashTruncationMarker = "\n... output truncated ...\n";
@@ -380,6 +383,14 @@ function workspaceFileKind(filePath, sample) {
380
383
  function json(value) {
381
384
  return JSON.stringify(value);
382
385
  }
386
+ function capGrepLine(value) {
387
+ // Production captures cap every returned grep line independently. This is
388
+ // intentionally a prefix without a marker, including for the matching line.
389
+ return value.slice(0, grepLineCharacterLimit);
390
+ }
391
+ function capGrepLines(values) {
392
+ return values.map(capGrepLine);
393
+ }
383
394
  function errorMessage(error) {
384
395
  return error instanceof Error ? error.message : String(error);
385
396
  }
@@ -451,9 +462,14 @@ function matchesGlob(filePath, pattern) {
451
462
  matcher.test(path.posix.basename(normalized))));
452
463
  });
453
464
  }
454
- function packageNameIsSafe(value) {
465
+ function packageNameIsSafe(value, mode) {
455
466
  if (!value || value.startsWith("-") || /\s/.test(value))
456
467
  return false;
468
+ if (mode === "react") {
469
+ // React captures reject version ranges before invoking npm, while accepting
470
+ // exact versions and tags such as latest. Keep other modes' validator intact.
471
+ return /^(?:@[\w.-]+\/)?[\w.-]+(?:@[\w.+-]+)?$/.test(value);
472
+ }
457
473
  return /^(?:@[\w.-]+\/)?[\w.-]+(?:@(?:[\w*^<>=|.+-]+))?$/.test(value);
458
474
  }
459
475
  function valueAtPath(value, instancePath) {
@@ -757,6 +773,125 @@ export class CodeToolExecutor {
757
773
  const names = this.definitions.map((tool) => tool.function.name);
758
774
  return `Model tried to call unavailable tool '${name}'. Available tools: ${names.join(", ")}.`;
759
775
  }
776
+ rawArguments(call) {
777
+ return typeof call.function?.arguments === "string"
778
+ ? call.function.arguments
779
+ : "{}";
780
+ }
781
+ normalizeArguments(name, parsedArguments) {
782
+ // Keep the parsed value available to checkpoint hooks while applying the
783
+ // same captured runtime defaults to the independent dispatch value.
784
+ const argumentsValue = (this.config.mode === "react" && typeof parsedArguments === "string"
785
+ ? {}
786
+ : structuredClone(parsedArguments));
787
+ // The captured provider advertises these fields as required, but its
788
+ // runtime still applies the documented defaults when a model omits
789
+ // them. Preserve the byte-identical outgoing schema while matching the
790
+ // observed execution behavior.
791
+ if (name === "read_file") {
792
+ argumentsValue.offset ??= 1;
793
+ argumentsValue.limit ??= 2000;
794
+ }
795
+ else if (name === "list_files" || name === "glob_files") {
796
+ argumentsValue.path ??= "";
797
+ }
798
+ else if (this.config.mode === "full-stack" && name === "bash" &&
799
+ argumentsValue && typeof argumentsValue === "object" &&
800
+ !Array.isArray(argumentsValue) && argumentsValue.workdir === undefined) {
801
+ argumentsValue.workdir = "";
802
+ }
803
+ else if (name === "grep_files") {
804
+ argumentsValue.path ??= "";
805
+ argumentsValue.glob ??= "";
806
+ argumentsValue.output_mode ??= "content";
807
+ argumentsValue["-i"] ??= false;
808
+ argumentsValue["-B"] ??= 0;
809
+ argumentsValue["-A"] ??= 0;
810
+ argumentsValue["-C"] ??= 0;
811
+ argumentsValue.context ??= 0;
812
+ }
813
+ return argumentsValue;
814
+ }
815
+ validationMessage(name, value, validate) {
816
+ if (validate(value))
817
+ return undefined;
818
+ const issues = validationIssues(name, value, validate.errors || [], this.definitions);
819
+ return (`Invalid input for tool ${name}: Type validation failed: ` +
820
+ `Value: ${JSON.stringify(value)}.\n` +
821
+ `Error message: ${JSON.stringify(issues, null, 2)}`);
822
+ }
823
+ inspect(call) {
824
+ const id = String(call.id || "");
825
+ const name = String(call.function?.name || "");
826
+ const rawArguments = this.rawArguments(call);
827
+ const validate = this.validators.get(name);
828
+ if (!validate) {
829
+ let parsedArguments = undefined;
830
+ let parsed = false;
831
+ try {
832
+ parsedArguments = JSON.parse(rawArguments);
833
+ parsed = true;
834
+ }
835
+ catch {
836
+ // Tool-name validation takes precedence. Parsing is best-effort here
837
+ // only so a valid argument payload remains available to the hook.
838
+ }
839
+ return {
840
+ id,
841
+ name,
842
+ rawArguments,
843
+ ...(parsed ? { arguments: parsedArguments } : {}),
844
+ validation: "unknown_tool",
845
+ validationMessage: this.unavailableToolMessage(name),
846
+ };
847
+ }
848
+ let parsedArguments;
849
+ try {
850
+ parsedArguments = JSON.parse(rawArguments);
851
+ }
852
+ catch (error) {
853
+ return {
854
+ id,
855
+ name,
856
+ rawArguments,
857
+ validation: "invalid_json",
858
+ validationMessage: `Invalid input for tool ${name}: JSON parsing failed: Text: ` +
859
+ `${rawArguments}.\nError message: ${errorMessage(error)}`,
860
+ };
861
+ }
862
+ let argumentsValue;
863
+ try {
864
+ argumentsValue = this.normalizeArguments(name, parsedArguments);
865
+ }
866
+ catch (error) {
867
+ return {
868
+ id,
869
+ name,
870
+ rawArguments,
871
+ arguments: parsedArguments,
872
+ validation: "invalid_arguments",
873
+ validationMessage: errorMessage(error),
874
+ };
875
+ }
876
+ const validationMessage = this.validationMessage(name, argumentsValue, validate);
877
+ if (validationMessage !== undefined) {
878
+ return {
879
+ id,
880
+ name,
881
+ rawArguments,
882
+ arguments: parsedArguments,
883
+ validation: "invalid_arguments",
884
+ validationMessage,
885
+ };
886
+ }
887
+ return {
888
+ id,
889
+ name,
890
+ rawArguments,
891
+ arguments: parsedArguments,
892
+ validation: "valid",
893
+ };
894
+ }
760
895
  async execute(call, _context) {
761
896
  const callId = String(call.id || "");
762
897
  const name = String(call.function?.name || "");
@@ -765,30 +900,8 @@ export class CodeToolExecutor {
765
900
  // 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
766
901
  // 之前短路,否则命令会照常 spawn 直到自然结束。
767
902
  this.signal?.throwIfAborted();
768
- const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
769
- ? call.function.arguments
770
- : "{}");
771
- // The captured provider advertises these fields as required, but its
772
- // runtime still applies the documented defaults when a model omits
773
- // them. Preserve the byte-identical outgoing schema while matching the
774
- // observed execution behavior.
775
- if (name === "read_file") {
776
- argumentsValue.offset ??= 1;
777
- argumentsValue.limit ??= 2000;
778
- }
779
- else if (name === "list_files" || name === "glob_files") {
780
- argumentsValue.path ??= "";
781
- }
782
- else if (name === "grep_files") {
783
- argumentsValue.path ??= "";
784
- argumentsValue.glob ??= "";
785
- argumentsValue.output_mode ??= "content";
786
- argumentsValue["-i"] ??= false;
787
- argumentsValue["-B"] ??= 0;
788
- argumentsValue["-A"] ??= 0;
789
- argumentsValue["-C"] ??= 0;
790
- argumentsValue.context ??= 0;
791
- }
903
+ const parsedArguments = JSON.parse(this.rawArguments(call));
904
+ const argumentsValue = this.normalizeArguments(name, parsedArguments);
792
905
  const validate = this.validators.get(name);
793
906
  if (!validate) {
794
907
  return {
@@ -797,14 +910,12 @@ export class CodeToolExecutor {
797
910
  content: this.unavailableToolMessage(name),
798
911
  };
799
912
  }
800
- if (!validate(argumentsValue)) {
801
- const issues = validationIssues(name, argumentsValue, validate.errors || [], this.definitions);
913
+ const validationMessage = this.validationMessage(name, argumentsValue, validate);
914
+ if (validationMessage !== undefined) {
802
915
  return {
803
916
  role: "tool",
804
917
  tool_call_id: callId,
805
- content: `Invalid input for tool ${name}: Type validation failed: ` +
806
- `Value: ${JSON.stringify(argumentsValue)}.\n` +
807
- `Error message: ${JSON.stringify(issues, null, 2)}`,
918
+ content: validationMessage,
808
919
  };
809
920
  }
810
921
  const result = await this.dispatch(name, argumentsValue, callId);
@@ -843,6 +954,13 @@ export class CodeToolExecutor {
843
954
  };
844
955
  }
845
956
  async dispatch(name, values, _toolCallId) {
957
+ // Replay mode (CODE_HARNESS_REPLAY_RESPONSES set): serve recorded outputs
958
+ // verbatim for nondeterministic tools (web_search/web_fetch/fetch_media/…)
959
+ // so replayed sessions stay offline and deterministic. Unlisted calls fall
960
+ // through to real execution.
961
+ const replayed = replayResponseFor(name, values);
962
+ if (replayed)
963
+ return replayed.output;
846
964
  switch (name) {
847
965
  case "read_file":
848
966
  return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit));
@@ -1064,7 +1182,9 @@ export class CodeToolExecutor {
1064
1182
  status: "success",
1065
1183
  file: {
1066
1184
  path: this.relativePath(target),
1067
- content: `${numbered}\n\n${footer}`,
1185
+ content: this.config.mode === "react"
1186
+ ? truncateCodeArenaText(`${numbered}\n\n${footer}`)
1187
+ : `${numbered}\n\n${footer}`,
1068
1188
  contentType: contentTypeForFile(target),
1069
1189
  },
1070
1190
  totalLines: lines.length,
@@ -1147,15 +1267,24 @@ export class CodeToolExecutor {
1147
1267
  message: `File "${rawPath}" does not exist`,
1148
1268
  };
1149
1269
  }
1150
- const edited = replaceUniqueText(source, context, replacement, {
1270
+ // React replaces the first exact occurrence, including when an indented
1271
+ // context also occurs on a later, more deeply indented line. Keep the
1272
+ // existing uniqueness check for fuzzy fallback and the other modes.
1273
+ const replace = this.config.mode === "react" && context.length > 0 && source.includes(context)
1274
+ ? replaceFirstText : replaceUniqueText;
1275
+ const edited = replace(source, context, replacement, {
1151
1276
  whitespaceInsensitiveMultiline: true,
1277
+ whitespaceInsensitiveSingleLine: this.config.mode === "react",
1278
+ preserveNewlines: this.config.mode === "react",
1152
1279
  });
1153
1280
  if (edited === undefined) {
1154
1281
  // Captured sessions re-issue an edit after compaction or a retry. The
1155
1282
  // provider reports success without touching the file when the context
1156
1283
  // is already gone and the replacement is in place (f39 re-issues four
1157
1284
  // dashboard edits this way).
1158
- if (replacement && source.includes(replacement)) {
1285
+ // That retry behavior comes from Full Stack captures. React still
1286
+ // reports Context not found even when the replacement is present.
1287
+ if (this.config.mode !== "react" && replacement && source.includes(replacement)) {
1159
1288
  return this.mutationResult("Edited", rawPath);
1160
1289
  }
1161
1290
  return {
@@ -1198,6 +1327,19 @@ export class CodeToolExecutor {
1198
1327
  }
1199
1328
  current?.lines.push(line);
1200
1329
  }
1330
+ if (this.config.mode === "react") {
1331
+ // React parses duplicate operation paths before applying any writes.
1332
+ // In particular, Delete + Add of the same file is an Add duplicate.
1333
+ const seen = new Set();
1334
+ for (const operation of operations) {
1335
+ const target = await this.confinedPath(operation.path);
1336
+ if (seen.has(target)) {
1337
+ const label = operation.kind[0].toUpperCase() + operation.kind.slice(1);
1338
+ return { status: "error", message: `${label} File Error: Duplicate Path: ${operation.path}` };
1339
+ }
1340
+ seen.add(target);
1341
+ }
1342
+ }
1201
1343
  for (const operation of operations) {
1202
1344
  const target = await this.confinedPath(operation.path);
1203
1345
  if (operation.kind === "add") {
@@ -1313,6 +1455,11 @@ export class CodeToolExecutor {
1313
1455
  return { status: "success" };
1314
1456
  }
1315
1457
  async listCodeFiles(rawPath) {
1458
+ if (this.config.mode === "react" && rawPath === "/")
1459
+ rawPath = ".";
1460
+ if (this.config.mode === "react" && (path.isAbsolute(rawPath) || rawPath.includes(".."))) {
1461
+ return { status: "error", message: "Path must be relative to project root (no absolute paths or '..' traversal)" };
1462
+ }
1316
1463
  const target = await this.confinedPath(rawPath || ".");
1317
1464
  if (!(await this.existsWithExactCase(rawPath || ".")) ||
1318
1465
  !existsSync(target)) {
@@ -1321,7 +1468,8 @@ export class CodeToolExecutor {
1321
1468
  const files = this.orderListedFiles(await this.walkFiles(target));
1322
1469
  return {
1323
1470
  status: "success",
1324
- files: files.map((filePath) => ({ path: filePath })),
1471
+ files: (this.config.mode === "react" ? files.slice(0, 500) : files)
1472
+ .map((filePath) => ({ path: filePath })),
1325
1473
  };
1326
1474
  }
1327
1475
  async grepCodeFiles(values) {
@@ -1408,8 +1556,10 @@ export class CodeToolExecutor {
1408
1556
  continue;
1409
1557
  }
1410
1558
  const lines = content.split("\n");
1559
+ const searchLineCount = this.config.mode === "react" && lines.at(-1) === ""
1560
+ ? lines.length - 1 : lines.length;
1411
1561
  const matchingLines = [];
1412
- for (let index = 0; index < lines.length; index += 1) {
1562
+ for (let index = 0; index < searchLineCount; index += 1) {
1413
1563
  if (expression.test(lines[index]))
1414
1564
  matchingLines.push(index);
1415
1565
  }
@@ -1422,14 +1572,15 @@ export class CodeToolExecutor {
1422
1572
  const match = {
1423
1573
  path: relative,
1424
1574
  lineNumber: index + 1,
1425
- lineContent: lines[index],
1575
+ lineContent: capGrepLine(lines[index]),
1426
1576
  };
1427
1577
  if (groupedContext) {
1428
1578
  const contextBefore = position === 0
1429
1579
  ? lines.slice(Math.max(0, index - before), index)
1430
1580
  : previousAfter;
1431
- if (contextBefore.length > 0)
1432
- match.contextBefore = contextBefore;
1581
+ if (contextBefore.length > 0) {
1582
+ match.contextBefore = capGrepLines(contextBefore);
1583
+ }
1433
1584
  const nextMatch = matchingLines[position + 1] ?? lines.length;
1434
1585
  previousAfter = lines.slice(index + 1, Math.min(index + 1 + contextSpan, nextMatch));
1435
1586
  if (nextMatch === lines.length &&
@@ -1437,15 +1588,36 @@ export class CodeToolExecutor {
1437
1588
  index + 1 + contextSpan >= lines.length) {
1438
1589
  previousAfter = previousAfter.slice(0, -1);
1439
1590
  }
1440
- if (previousAfter.length > 0)
1441
- match.contextAfter = previousAfter;
1591
+ if (previousAfter.length > 0) {
1592
+ match.contextAfter = capGrepLines(previousAfter);
1593
+ }
1594
+ }
1595
+ else if (this.config.mode === "react") {
1596
+ // The captured adapter attaches context lines to the preceding
1597
+ // match, even across ripgrep group separators. Only a file's first
1598
+ // match receives contextBefore. Do not duplicate overlapping groups.
1599
+ if (position === 0 && before > 0) {
1600
+ const contextBefore = lines.slice(Math.max(0, index - before), index);
1601
+ if (contextBefore.length) {
1602
+ match.contextBefore = capGrepLines(contextBefore);
1603
+ }
1604
+ }
1605
+ const nextMatch = matchingLines[position + 1];
1606
+ const end = Math.min(index + 1 + after, nextMatch ?? searchLineCount);
1607
+ const contextAfter = lines.slice(index + 1, end);
1608
+ if (nextMatch !== undefined && before > 0) {
1609
+ contextAfter.push(...lines.slice(Math.max(end, index + 1, nextMatch - before), nextMatch));
1610
+ }
1611
+ if (contextAfter.length) {
1612
+ match.contextAfter = capGrepLines(contextAfter);
1613
+ }
1442
1614
  }
1443
1615
  else {
1444
1616
  if (before > 0) {
1445
- match.contextBefore = lines.slice(Math.max(0, index - before), index);
1617
+ match.contextBefore = capGrepLines(lines.slice(Math.max(0, index - before), index));
1446
1618
  }
1447
1619
  if (after > 0) {
1448
- match.contextAfter = lines.slice(index + 1, index + 1 + after);
1620
+ match.contextAfter = capGrepLines(lines.slice(index + 1, index + 1 + after));
1449
1621
  }
1450
1622
  }
1451
1623
  matches.push(match);
@@ -1495,7 +1667,7 @@ export class CodeToolExecutor {
1495
1667
  return { status: "success", paths: [], truncated: false };
1496
1668
  }
1497
1669
  const allPaths = files
1498
- .filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern))
1670
+ .filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern) || (this.config.mode === "react" && matchesGlob(this.relativePath(filePath), pattern)))
1499
1671
  .map((filePath) => this.relativePath(filePath));
1500
1672
  return {
1501
1673
  status: "success",
@@ -1568,11 +1740,11 @@ export class CodeToolExecutor {
1568
1740
  });
1569
1741
  }
1570
1742
  async installNpmPackages(packages) {
1571
- const invalid = packages.find((packageName) => !packageNameIsSafe(packageName));
1743
+ const invalid = packages.find((packageName) => !packageNameIsSafe(packageName, this.config.mode));
1572
1744
  if (invalid) {
1573
1745
  return {
1574
1746
  status: "error",
1575
- message: `Invalid package name: ${invalid}`,
1747
+ message: `Invalid package name: ${this.config.mode === "react" ? JSON.stringify(invalid) : invalid}`,
1576
1748
  };
1577
1749
  }
1578
1750
  const args = ["install", "-S"];
@@ -1611,11 +1783,15 @@ export class CodeToolExecutor {
1611
1783
  };
1612
1784
  }
1613
1785
  async fullStackBash(command, workdir) {
1786
+ const cwd = workdir.trim() || ".";
1787
+ if (path.isAbsolute(cwd) || cwd.includes("..")) {
1788
+ throw new Error(`Invalid workdir: ${cwd}. Commands run from the project root by default. Only use workdir for relative subdirectories, never absolute paths.`);
1789
+ }
1614
1790
  const configuredTimeout = Number(process.env.CODE_HARNESS_BASH_TIMEOUT_SECONDS || 300);
1615
1791
  const timeoutSeconds = Number.isFinite(configuredTimeout) && configuredTimeout > 0
1616
1792
  ? configuredTimeout
1617
1793
  : 300;
1618
- const result = await this.bash(command, workdir.trim() || ".", timeoutSeconds);
1794
+ const result = await this.bash(command, cwd, timeoutSeconds);
1619
1795
  const exitCode = result.exit_code;
1620
1796
  const stdout = String(result.stdout || "");
1621
1797
  const stderr = String(result.stderr || "");
@@ -1886,11 +2062,7 @@ export class CodeToolExecutor {
1886
2062
  const result = routifyWebFetchEnabled()
1887
2063
  ? await executeRoutifyWebFetch(url, format, { signal: this.signal })
1888
2064
  : 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
- };
2065
+ return boundedWebFetchResult(result);
1894
2066
  }
1895
2067
  async pexelsRequest(pathname, parameters) {
1896
2068
  const apiKey = process.env.PEXELS_API_KEY;
@@ -1929,7 +2101,7 @@ export class CodeToolExecutor {
1929
2101
  },
1930
2102
  });
1931
2103
  }
1932
- mapPexelsPhotos(payload) {
2104
+ mapPexelsPhotos(payload, orientation) {
1933
2105
  const photos = Array.isArray(payload.photos) ? payload.photos : [];
1934
2106
  return photos.map((value) => {
1935
2107
  const photo = record(value);
@@ -1938,12 +2110,14 @@ export class CodeToolExecutor {
1938
2110
  type: "image",
1939
2111
  id: Number(photo.id),
1940
2112
  pexelsUrl: stringValue(photo.url),
1941
- recommendedUrl: stringValue(source.large2x) ||
2113
+ recommendedUrl: (orientation === "landscape" || orientation === "portrait"
2114
+ ? stringValue(source[orientation]) : "") ||
2115
+ stringValue(source.large2x) ||
1942
2116
  stringValue(source.large) ||
1943
2117
  stringValue(source.original),
1944
- thumbnailUrl: stringValue(source.medium) ||
1945
- stringValue(source.small) ||
1946
- stringValue(source.tiny),
2118
+ thumbnailUrl: stringValue(source.tiny) ||
2119
+ stringValue(source.medium) ||
2120
+ stringValue(source.small),
1947
2121
  width: Number(photo.width),
1948
2122
  height: Number(photo.height),
1949
2123
  creatorName: stringValue(photo.photographer),
@@ -1963,20 +2137,29 @@ export class CodeToolExecutor {
1963
2137
  const files = Array.isArray(video.video_files)
1964
2138
  ? video.video_files.map(record)
1965
2139
  : [];
1966
- const preferred = files.find((item) => stringValue(item.quality) === "hd") ||
1967
- files[0] ||
1968
- {};
2140
+ // Captures contain full-resolution HD/UHD files. A quality label alone
2141
+ // does not order renditions; compare their actual pixel dimensions.
2142
+ const playable = files.filter((item) => stringValue(item.file_type) === "video/mp4" && stringValue(item.link));
2143
+ const preferred = playable.sort((left, right) => (Number(right.width) || 0) * (Number(right.height) || 0) -
2144
+ (Number(left.width) || 0) * (Number(left.height) || 0))[0] || files[0] || {};
1969
2145
  return {
1970
2146
  type: "video",
1971
2147
  id: Number(video.id),
1972
2148
  pexelsUrl: stringValue(video.url),
1973
2149
  recommendedUrl: stringValue(preferred.link),
1974
- thumbnailUrl: stringValue(pictures[0]?.picture),
2150
+ thumbnailUrl: stringValue(video.image) || stringValue(pictures[0]?.picture),
1975
2151
  width: Number(video.width),
1976
2152
  height: Number(video.height),
1977
2153
  duration: Number(video.duration),
1978
2154
  creatorName: stringValue(user.name),
1979
2155
  creatorUrl: stringValue(user.url),
2156
+ videoFile: {
2157
+ url: stringValue(preferred.link),
2158
+ width: Number(preferred.width),
2159
+ height: Number(preferred.height),
2160
+ ...(preferred.quality !== undefined ? { quality: preferred.quality } : {}),
2161
+ fileType: stringValue(preferred.file_type),
2162
+ },
1980
2163
  };
1981
2164
  });
1982
2165
  }
@@ -1994,7 +2177,7 @@ export class CodeToolExecutor {
1994
2177
  if (mediaType === "image" || mediaType === "both") {
1995
2178
  const photos = await this.pexelsRequest("/v1/search", common);
1996
2179
  totalResults += Number(photos.total_results || 0);
1997
- photoResults = this.mapPexelsPhotos(photos);
2180
+ photoResults = this.mapPexelsPhotos(photos, orientation);
1998
2181
  }
1999
2182
  if (mediaType === "video" || mediaType === "both") {
2000
2183
  const videos = await this.pexelsRequest("/v1/videos/search", common);
@@ -2016,13 +2199,20 @@ export class CodeToolExecutor {
2016
2199
  }
2017
2200
  async fetchMedia(query, mediaType, count, orientation, size, locale) {
2018
2201
  const requestedCount = Math.min(10, Math.max(1, Math.trunc(count)));
2019
- return this.fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale);
2202
+ try {
2203
+ return await this.fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale);
2204
+ }
2205
+ catch (error) {
2206
+ throw new Error(`Media fetch failed: ${errorMessage(error)}`);
2207
+ }
2020
2208
  }
2021
2209
  async generateImage(rawPath, prompt, images) {
2022
2210
  if (this.generatedImageCount >= 10) {
2023
2211
  return {
2024
2212
  status: "error",
2025
- message: "generate_image can be used at most 10 times per session",
2213
+ message: this.config.mode === "react"
2214
+ ? "Failed to generate image: Image generation limit of 10 reached for this session. Use CSS gradients, SVG, or placeholder images for remaining visuals."
2215
+ : "generate_image can be used at most 10 times per session",
2026
2216
  };
2027
2217
  }
2028
2218
  this.generatedImageCount += 1;