@qwen-code/arena-agent 0.0.10-dev.14 → 0.0.10-dev.16

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 (97) hide show
  1. package/.env.example +32 -6
  2. package/CHANGELOG.md +36 -1
  3. package/README.md +144 -31
  4. package/dist/agent.d.ts +8 -2
  5. package/dist/agent.js +101 -28
  6. package/dist/agent.js.map +1 -1
  7. package/dist/arena-history.d.ts +12 -0
  8. package/dist/arena-history.js +119 -0
  9. package/dist/arena-history.js.map +1 -0
  10. package/dist/brave-search.js +7 -4
  11. package/dist/brave-search.js.map +1 -1
  12. package/dist/canonical-data.d.ts +7 -0
  13. package/dist/canonical-data.js +10 -3
  14. package/dist/canonical-data.js.map +1 -1
  15. package/dist/cli.js +75 -13
  16. package/dist/cli.js.map +1 -1
  17. package/dist/code-tools.js +2 -4
  18. package/dist/code-tools.js.map +1 -1
  19. package/dist/compaction.d.ts +4 -0
  20. package/dist/compaction.js +54 -10
  21. package/dist/compaction.js.map +1 -1
  22. package/dist/config.d.ts +4 -2
  23. package/dist/config.js +5 -5
  24. package/dist/config.js.map +1 -1
  25. package/dist/connector-errors.d.ts +16 -0
  26. package/dist/connector-errors.js +69 -0
  27. package/dist/connector-errors.js.map +1 -0
  28. package/dist/connector-files.d.ts +31 -0
  29. package/dist/connector-files.js +249 -0
  30. package/dist/connector-files.js.map +1 -0
  31. package/dist/connector-input.d.ts +10 -0
  32. package/dist/connector-input.js +79 -0
  33. package/dist/connector-input.js.map +1 -0
  34. package/dist/connector-mcp.d.ts +15 -2
  35. package/dist/connector-mcp.js +201 -41
  36. package/dist/connector-mcp.js.map +1 -1
  37. package/dist/connectors.d.ts +7 -2
  38. package/dist/connectors.js +42 -30
  39. package/dist/connectors.js.map +1 -1
  40. package/dist/fetch-page.js +2 -2
  41. package/dist/fetch-page.js.map +1 -1
  42. package/dist/history.d.ts +4 -2
  43. package/dist/history.js +4 -1
  44. package/dist/history.js.map +1 -1
  45. package/dist/image-choice.d.ts +23 -0
  46. package/dist/image-choice.js +190 -0
  47. package/dist/image-choice.js.map +1 -0
  48. package/dist/image-model.d.ts +6 -0
  49. package/dist/image-model.js +32 -0
  50. package/dist/image-model.js.map +1 -0
  51. package/dist/model-router-media.js +5 -3
  52. package/dist/model-router-media.js.map +1 -1
  53. package/dist/process-manager.js +9 -9
  54. package/dist/process-manager.js.map +1 -1
  55. package/dist/prompts.d.ts +0 -1
  56. package/dist/prompts.js +3 -27
  57. package/dist/prompts.js.map +1 -1
  58. package/dist/provider.d.ts +18 -0
  59. package/dist/provider.js +52 -16
  60. package/dist/provider.js.map +1 -1
  61. package/dist/repo.d.ts +1 -1
  62. package/dist/repo.js +26 -4
  63. package/dist/repo.js.map +1 -1
  64. package/dist/routify-web-fetch.js +2 -2
  65. package/dist/routify-web-fetch.js.map +1 -1
  66. package/dist/server.js +24 -12
  67. package/dist/server.js.map +1 -1
  68. package/dist/session-memory-prompts.d.ts +2 -1
  69. package/dist/session-memory-prompts.js +2 -1
  70. package/dist/session-memory-prompts.js.map +1 -1
  71. package/dist/spec.d.ts +4 -4
  72. package/dist/spec.js +25 -12
  73. package/dist/spec.js.map +1 -1
  74. package/dist/startup-config.d.ts +39 -0
  75. package/dist/startup-config.js +346 -0
  76. package/dist/startup-config.js.map +1 -0
  77. package/dist/startup-main-config.d.ts +63 -0
  78. package/dist/startup-main-config.js +94 -0
  79. package/dist/startup-main-config.js.map +1 -0
  80. package/dist/startup-tool-config.d.ts +8 -0
  81. package/dist/startup-tool-config.js +116 -0
  82. package/dist/startup-tool-config.js.map +1 -0
  83. package/dist/summary-compaction.d.ts +2 -0
  84. package/dist/summary-compaction.js +17 -3
  85. package/dist/summary-compaction.js.map +1 -1
  86. package/dist/tools.d.ts +2 -0
  87. package/dist/tools.js +133 -49
  88. package/dist/tools.js.map +1 -1
  89. package/dist/tui.js +15 -2
  90. package/dist/tui.js.map +1 -1
  91. package/dist/types.d.ts +6 -0
  92. package/dist/web/app.js +8 -0
  93. package/dist/web/app.js.map +1 -1
  94. package/docs/BINARY_DISTRIBUTION.md +43 -8
  95. package/docs/MODES_AND_RELEASES.md +23 -5
  96. package/package.json +1 -1
  97. package/web/styles.css +22 -0
package/dist/tools.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { copyFileSync, existsSync, mkdirSync, realpathSync } from "node:fs";
3
3
  import { open, readFile, realpath, stat, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { createHash } from "node:crypto";
5
6
  import { Ajv } from "ajv";
6
7
  import addFormats from "ajv-formats";
7
8
  import { detect as detectEncoding } from "chardet";
@@ -10,6 +11,7 @@ import mime from "mime/lite";
10
11
  import { AskUserHandlerError } from "./ask-user.js";
11
12
  import { executeBraveImageSearch, executeBraveWebSearch, } from "./brave-search.js";
12
13
  import { CodeToolExecutor } from "./code-tools.js";
14
+ import { schemaOrderedValue } from "./arena-history.js";
13
15
  import { ConnectorTools, connectorFailureText } from "./connector-mcp.js";
14
16
  import { supportsImageInput } from "./config.js";
15
17
  import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
@@ -17,6 +19,8 @@ import { FetchPageReader } from "./fetch-page.js";
17
19
  import { replaceFirstText } from "./file-edit.js";
18
20
  import { generatedImageBytesForExtension, prepareImageForModel, } from "./image.js";
19
21
  import { imageTextFromFallbackModel } from "./image-input-fallback.js";
22
+ import { offerGeneratedImages } from "./image-choice.js";
23
+ import { arenaImageModel, imageOptionModels } from "./image-model.js";
20
24
  import { isCodeArenaMode } from "./mode.js";
21
25
  import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
22
26
  import { processManagerForWorkspace, } from "./process-manager.js";
@@ -596,6 +600,8 @@ async function responseBytes(response, limit = 4 * 1024 * 1024) {
596
600
  }
597
601
  class ArenaToolExecutor {
598
602
  config;
603
+ interactionBoundaries = new Set();
604
+ interactionQueue = Promise.resolve();
599
605
  workspace;
600
606
  workspaceReal;
601
607
  tmpReal;
@@ -619,6 +625,7 @@ class ArenaToolExecutor {
619
625
  // The captured runtime uses Zod object parsing: unknown object keys are
620
626
  // stripped before dispatch instead of being emitted as validation issues.
621
627
  removeAdditional: "all",
628
+ useDefaults: true,
622
629
  });
623
630
  // ask_user needs a handler: TUI/WebUI collect real user input, while
624
631
  // headless `-p` may delegate to a configured remote User Agent. add_voice
@@ -638,7 +645,18 @@ class ArenaToolExecutor {
638
645
  this.varTmpReal = realpathSync("/var/tmp");
639
646
  this.processManager = processManagerForWorkspace(this.workspaceReal);
640
647
  this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
641
- this.askUserHandler = options.askUserHandler;
648
+ const askUserHandler = options.askUserHandler;
649
+ this.askUserHandler = askUserHandler ? (callId, questions, context) => {
650
+ // TUI/WebUI own one active question panel. Parallel tools can prepare
651
+ // auditions concurrently, but must not overwrite another pending answer.
652
+ const response = this.interactionQueue.then(() => {
653
+ (this.signal || context?.signal)?.throwIfAborted();
654
+ this.interactionBoundaries.add(callId);
655
+ return askUserHandler(callId, questions, context);
656
+ });
657
+ this.interactionQueue = response.then(() => { }, () => { });
658
+ return response;
659
+ } : undefined;
642
660
  this.askUserEnabled = Boolean(options.askUserHandler);
643
661
  if (config.imageInputFallbackModel?.trim()) {
644
662
  const connection = imageInputFallbackConnection(config);
@@ -650,8 +668,8 @@ class ArenaToolExecutor {
650
668
  }
651
669
  this.signal = options.signal;
652
670
  this.workspaceContext = options.workspaceContext;
653
- if (config.connectors?.length && !config.disallowedTools?.includes("list_connector_tools")) {
654
- this.connectors = new ConnectorTools(config.connectors, config.mergeConnectors, config.disallowedTools, options.signal);
671
+ if ((config.connectors?.length || config.connectedConnectors !== undefined) && !config.disallowedTools?.includes("list_connector_tools")) {
672
+ this.connectors = new ConnectorTools(config.connectors || [], config.mergeConnectors, config.disallowedTools, options.signal, { workspace: this.workspace, connected: config.connectedConnectors, resolvePath: raw => this.confinedPath(raw) });
655
673
  }
656
674
  this.pageReader = new FetchPageReader({ signal: options.signal });
657
675
  this.routifyPageReader = new RoutifyFetchPageReader({
@@ -665,8 +683,14 @@ class ArenaToolExecutor {
665
683
  type: "string",
666
684
  validate: (value) => URL.canParse(value),
667
685
  });
668
- for (const definition of toolDefinitions(this.workspaceContext, "arena", config.connectors)) {
669
- this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
686
+ for (const definition of toolDefinitions(this.workspaceContext, "arena", config.connectors, config.connectedConnectors)) {
687
+ const parameters = structuredClone(definition.function.parameters);
688
+ // The latest wire schema moves this limit into the description. Keep the
689
+ // documented command bound enforced internally without changing that schema.
690
+ if (definition.function.name === "bash" || definition.function.name === "start_process") {
691
+ record(record(parameters.properties).command).maxLength = 64_000;
692
+ }
693
+ this.validators.set(definition.function.name, this.ajv.compile(parameters));
670
694
  }
671
695
  }
672
696
  // The tool schemas offered to the model for this session. Identical to the
@@ -681,13 +705,35 @@ class ArenaToolExecutor {
681
705
  return definitions.filter((definition) => !disallowed.has(definition.function.name));
682
706
  }
683
707
  allDefinitions() {
684
- return [...toolDefinitions(this.workspaceContext, "arena", this.config.connectors),
708
+ return [...toolDefinitions(this.workspaceContext, "arena", this.config.connectors, this.config.connectedConnectors),
685
709
  ...(this.connectors?.definitions() || [])];
686
710
  }
687
711
  async beginConnectorTurn(history) {
688
712
  await this.connectors?.beginTurn(history);
689
713
  }
690
714
  async closeConnectors() { await this.connectors?.close(); }
715
+ historyCall(call) {
716
+ if (call.function.arguments === "")
717
+ return call;
718
+ const parsed = this.parseArguments(call);
719
+ if (parsed.error !== undefined)
720
+ return call;
721
+ const name = call.function.name;
722
+ let value = parsed.value;
723
+ const validate = this.validators.get(name) || this.connectors?.validator(name);
724
+ const schema = this.allDefinitions().find(definition => definition.function.name === name)?.function.parameters;
725
+ if (validate && schema) {
726
+ const checked = this.validateArguments(name, value, validate);
727
+ if (checked.validationMessage === undefined)
728
+ value = this.connectors?.has(name)
729
+ ? this.connectors.historyValue(name, checked.value) : schemaOrderedValue(checked.value, schema);
730
+ }
731
+ const argumentsText = JSON.stringify(value);
732
+ return argumentsText === call.function.arguments ? call : { ...call, function: { ...call.function, arguments: argumentsText } };
733
+ }
734
+ takeInteractionBoundary(callId) {
735
+ return this.interactionBoundaries.delete(callId);
736
+ }
691
737
  parseArguments(call) {
692
738
  const rawArguments = typeof call.function?.arguments === "string"
693
739
  ? call.function.arguments
@@ -717,7 +763,13 @@ class ArenaToolExecutor {
717
763
  return { rawArguments, value };
718
764
  }
719
765
  validateArguments(name, value, validate) {
720
- const validatedArguments = structuredClone(value);
766
+ let validatedArguments;
767
+ try {
768
+ validatedArguments = (this.connectors?.has(name) ? this.connectors.normalizeArguments(name, value) : structuredClone(value));
769
+ }
770
+ catch {
771
+ return { validationMessage: connectorFailureText };
772
+ }
721
773
  if (!validate(validatedArguments)) {
722
774
  return {
723
775
  validationMessage: this.connectors?.has(name) ? connectorFailureText : arenaValidationError(name, value, validate.errors),
@@ -873,7 +925,7 @@ class ArenaToolExecutor {
873
925
  return {
874
926
  role: "tool",
875
927
  tool_call_id: callId,
876
- content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt)),
928
+ content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt, name === "generate_image" ? context?.signal : undefined)),
877
929
  };
878
930
  }
879
931
  const content = typeof result === "string" ? result : json(result);
@@ -906,20 +958,20 @@ class ArenaToolExecutor {
906
958
  };
907
959
  }
908
960
  }
909
- runtimeFailureResult(name, values, error, startedAt) {
910
- if (this.signal?.aborted)
961
+ runtimeFailureResult(name, values, error, startedAt, contextSignal) {
962
+ if ((this.signal || contextSignal)?.aborted)
911
963
  return { status: "aborted" };
912
964
  const message = errorMessage(error);
913
965
  const durationMs = Date.now() - startedAt;
914
966
  if (name === "bash") {
915
967
  return {
916
- status: "shell_error",
917
- stderr: message,
918
968
  stdout: "",
969
+ stdout_truncated: false,
970
+ stderr: message,
971
+ stderr_truncated: false,
919
972
  exit_code: null,
973
+ status: "shell_error",
920
974
  duration_ms: durationMs,
921
- stderr_truncated: false,
922
- stdout_truncated: false,
923
975
  };
924
976
  }
925
977
  if (name === "start_process") {
@@ -942,8 +994,8 @@ class ArenaToolExecutor {
942
994
  if (name === "get_process_output") {
943
995
  return {
944
996
  status: "shell_error",
945
- log_tail: message,
946
997
  exit_code: null,
998
+ log_tail: message,
947
999
  listening_ports: [],
948
1000
  };
949
1001
  }
@@ -1020,7 +1072,7 @@ class ArenaToolExecutor {
1020
1072
  case "edit_file":
1021
1073
  return this.editFile(String(values.path), String(values.old_text), String(values.new_text));
1022
1074
  case "read_file":
1023
- return this.readWorkspaceFile(String(values.path), toolCallId);
1075
+ return this.readWorkspaceFile(String(values.path), toolCallId, values.offset, values.limit);
1024
1076
  case "fetch_page":
1025
1077
  return routifyWebFetchEnabled()
1026
1078
  ? this.routifyPageReader.read(String(values.url), Number(values.chunkIndex || 0))
@@ -1041,7 +1093,7 @@ class ArenaToolExecutor {
1041
1093
  ? path.resolve(this.workspaceContext.cwd)
1042
1094
  : "/home/user")), Number(values.startup_wait || 5));
1043
1095
  case "generate_image":
1044
- return this.generateImage(String(values.file_path), String(values.prompt), values.images);
1096
+ return this.generateImage(String(values.file_path), String(values.prompt), values.images, values.offer_options === true, toolCallId, context);
1045
1097
  case "generate_speech":
1046
1098
  return this.generateSpeech(String(values.file_path), String(values.text), String(values.voice_id), values.language === undefined ? undefined : String(values.language));
1047
1099
  case "get_process_output":
@@ -1198,9 +1250,8 @@ class ArenaToolExecutor {
1198
1250
  }
1199
1251
  return new Promise((resolve, reject) => {
1200
1252
  const env = this.sandboxEnvironment();
1201
- // detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
1202
- // 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
1203
- // 必须杀整组,并在宽限期后强制结算兜底。
1253
+ // A process group lets cancellation stop descendants too. A wait timeout
1254
+ // is different: the captured contract permits the command to continue.
1204
1255
  const child = spawn("/bin/bash", ["-lc", command], {
1205
1256
  cwd: workdir,
1206
1257
  env,
@@ -1237,10 +1288,8 @@ class ArenaToolExecutor {
1237
1288
  clearTimeout(timeout);
1238
1289
  if (forceSettle)
1239
1290
  clearTimeout(forceSettle);
1240
- // bash is one-shot in v2. Any descendant left in the process group is
1241
- // cleaned up even when the shell itself exited normally; persistent
1242
- // servers belong in start_process.
1243
- killTree("SIGKILL");
1291
+ if (!timedOut || this.signal?.aborted)
1292
+ killTree("SIGKILL");
1244
1293
  this.signal?.removeEventListener("abort", abort);
1245
1294
  if (this.signal?.aborted) {
1246
1295
  reject(this.signal.reason || new Error("operation cancelled"));
@@ -1249,13 +1298,13 @@ class ArenaToolExecutor {
1249
1298
  const out = stdout.result();
1250
1299
  const err = stderr.result();
1251
1300
  resolve({
1252
- status: timedOut ? "timeout" : signal ? "killed" : "completed",
1253
- stderr: err.text,
1254
1301
  stdout: out.text,
1302
+ stdout_truncated: out.truncated,
1303
+ stderr: err.text,
1304
+ stderr_truncated: err.truncated,
1255
1305
  exit_code: timedOut ? null : code,
1306
+ status: timedOut ? "timeout" : signal ? "killed" : "completed",
1256
1307
  duration_ms: Date.now() - startedAt,
1257
- stderr_truncated: err.truncated,
1258
- stdout_truncated: out.truncated,
1259
1308
  });
1260
1309
  };
1261
1310
  // 宽限期结束时升级为组级 SIGKILL 再结算:abort 只发 SIGTERM,
@@ -1268,8 +1317,12 @@ class ArenaToolExecutor {
1268
1317
  };
1269
1318
  const timeout = setTimeout(() => {
1270
1319
  timedOut = true;
1271
- killTree("SIGKILL");
1272
- armForceSettle();
1320
+ finish(null, null);
1321
+ // Keep draining the pipes so a continuing command cannot block or get
1322
+ // SIGPIPE. Do not keep the host CLI alive just to collect discarded output.
1323
+ child.unref();
1324
+ child.stdout.unref?.();
1325
+ child.stderr.unref?.();
1273
1326
  }, timeoutSeconds * 1000);
1274
1327
  const abort = () => {
1275
1328
  killTree("SIGTERM");
@@ -1277,10 +1330,12 @@ class ArenaToolExecutor {
1277
1330
  };
1278
1331
  this.signal?.addEventListener("abort", abort, { once: true });
1279
1332
  child.stdout.on("data", (chunk) => {
1280
- stdout.append(chunk);
1333
+ if (!settled)
1334
+ stdout.append(chunk);
1281
1335
  });
1282
1336
  child.stderr.on("data", (chunk) => {
1283
- stderr.append(chunk);
1337
+ if (!settled)
1338
+ stderr.append(chunk);
1284
1339
  });
1285
1340
  child.once("error", (error) => {
1286
1341
  if (settled)
@@ -1424,7 +1479,7 @@ class ArenaToolExecutor {
1424
1479
  }
1425
1480
  return { status: "success", message: `Edited ${rawPath}.` };
1426
1481
  }
1427
- async readWorkspaceFile(rawPath, toolCallId) {
1482
+ async readWorkspaceFile(rawPath, toolCallId, offset, limit) {
1428
1483
  let target;
1429
1484
  let fileSize;
1430
1485
  let sample;
@@ -1507,7 +1562,10 @@ class ArenaToolExecutor {
1507
1562
  return result;
1508
1563
  }
1509
1564
  if (kind === "text") {
1510
- const text = readTextResult(data);
1565
+ const start = (offset ?? 1) - 1;
1566
+ const selected = offset === undefined && limit === undefined ? data
1567
+ : Buffer.from(decodeText(data).split("\n").slice(start, limit === undefined ? undefined : start + limit).join("\n"), "utf8");
1568
+ const text = readTextResult(selected);
1511
1569
  return {
1512
1570
  kind: "text",
1513
1571
  lines: data.reduce((count, byte) => count + (byte === 0x0a ? 1 : 0), 1),
@@ -1589,7 +1647,15 @@ class ArenaToolExecutor {
1589
1647
  return { status: "success" };
1590
1648
  }
1591
1649
  async webSearch(query, depth) {
1592
- return executeBraveWebSearch(query, depth, { signal: this.signal });
1650
+ const result = await executeBraveWebSearch(query, depth, { signal: this.signal });
1651
+ // Keep the shared provider/Code Arena envelope unchanged. Agent Arena's
1652
+ // live history emits title before URL; restored history has its own order.
1653
+ if (!Array.isArray(result.results))
1654
+ return result;
1655
+ return { ...result, results: result.results.map(value => {
1656
+ const { id, title, url, ...rest } = record(value);
1657
+ return { id, title, url, ...rest };
1658
+ }) };
1593
1659
  }
1594
1660
  async imageSearch(query, count) {
1595
1661
  const payload = await executeBraveImageSearch(query, count, {
@@ -1637,10 +1703,10 @@ class ArenaToolExecutor {
1637
1703
  mkdirSync(path.dirname(target), { recursive: true });
1638
1704
  await writeFile(target, await responseBytes(imageResponse));
1639
1705
  saved.push({
1640
- title: stringValue(item.title),
1641
1706
  file_path: this.relativePath(target),
1642
- source_url: sourceUrl,
1643
1707
  thumbnail_url: thumbnailUrl,
1708
+ title: stringValue(item.title),
1709
+ source_url: sourceUrl,
1644
1710
  });
1645
1711
  break;
1646
1712
  }
@@ -1665,13 +1731,13 @@ class ArenaToolExecutor {
1665
1731
  const target = await this.confinedPath(rawPath);
1666
1732
  if (!(await stat(target)).isFile())
1667
1733
  throw new Error(rawPath);
1668
- return { path: rawPath, status: "success" };
1734
+ return { status: "success", path: rawPath };
1669
1735
  }
1670
1736
  catch {
1671
1737
  return { status: "error", message: "Error calling present file tool" };
1672
1738
  }
1673
1739
  }
1674
- async generateImage(rawPath, prompt, images) {
1740
+ async generateImage(rawPath, prompt, images, offerOptions = false, toolCallId = "generate_image", context) {
1675
1741
  if (!rawPath) {
1676
1742
  return { status: "error", message: "File path cannot be empty." };
1677
1743
  }
@@ -1683,8 +1749,8 @@ class ArenaToolExecutor {
1683
1749
  message: "File path must end in .jpg, .jpeg, or .png.",
1684
1750
  };
1685
1751
  }
1686
- mkdirSync(path.dirname(target), { recursive: true });
1687
- const model = process.env.ARENA_IMAGE_MODEL || "gemini-3.1-flash-image";
1752
+ if (!offerOptions)
1753
+ mkdirSync(path.dirname(target), { recursive: true });
1688
1754
  const references = [];
1689
1755
  for (const imagePath of images || []) {
1690
1756
  const source = await this.confinedPath(imagePath);
@@ -1709,14 +1775,26 @@ class ArenaToolExecutor {
1709
1775
  mimeType,
1710
1776
  });
1711
1777
  }
1712
- const generated = await generateImageWithModelRouter({
1713
- images: references,
1714
- model,
1715
- prompt,
1716
- signal: this.signal,
1717
- size: process.env.ARENA_IMAGE_SIZE || "1024*1024",
1718
- });
1719
- await writeFile(target, await generatedImageBytesForExtension(generated.bytes, generated.mimeType, extension));
1778
+ const size = process.env.ARENA_IMAGE_SIZE || "1024*1024";
1779
+ const generate = async (imageModel) => {
1780
+ const generated = await generateImageWithModelRouter({
1781
+ images: references, model: imageModel, prompt, signal: this.signal || context?.signal, size,
1782
+ });
1783
+ return generatedImageBytesForExtension(generated.bytes, generated.mimeType, extension);
1784
+ };
1785
+ if (offerOptions) {
1786
+ const models = imageOptionModels();
1787
+ return offerGeneratedImages({
1788
+ workspace: this.workspaceReal, stateRoot: this.config.stateRoot,
1789
+ target, rawPath, callId: toolCallId, context,
1790
+ signal: this.signal || context?.signal, models, generate,
1791
+ fingerprint: JSON.stringify({ target, rawPath, prompt, size, models,
1792
+ images: references.map(image => ({ mimeType: image.mimeType,
1793
+ sha256: createHash("sha256").update(image.bytes).digest("hex") })) }),
1794
+ askUser: this.askUserHandler, confinedPath: filePath => this.confinedPath(filePath),
1795
+ });
1796
+ }
1797
+ await writeFile(target, await generate(arenaImageModel()));
1720
1798
  return {
1721
1799
  status: "success",
1722
1800
  file_path: rawPath,
@@ -1781,6 +1859,12 @@ export class ToolExecutor {
1781
1859
  availableTools() {
1782
1860
  return this.implementation.availableTools();
1783
1861
  }
1862
+ historyCall(call) {
1863
+ return this.implementation instanceof ArenaToolExecutor ? this.implementation.historyCall(call) : call;
1864
+ }
1865
+ takeInteractionBoundary(callId) {
1866
+ return this.implementation instanceof ArenaToolExecutor && this.implementation.takeInteractionBoundary(callId);
1867
+ }
1784
1868
  async beginConnectorTurn(history) {
1785
1869
  if (this.implementation instanceof ArenaToolExecutor)
1786
1870
  await this.implementation.beginConnectorTurn(history);