@qwen-code/arena-agent 0.0.10-dev.9 → 0.0.10-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.
Files changed (144) hide show
  1. package/.env.example +61 -10
  2. package/CHANGELOG.md +140 -11
  3. package/README.md +309 -46
  4. package/dist/agent.d.ts +20 -6
  5. package/dist/agent.js +303 -73
  6. package/dist/agent.js.map +1 -1
  7. package/dist/anthropic.d.ts +1 -1
  8. package/dist/anthropic.js +46 -57
  9. package/dist/anthropic.js.map +1 -1
  10. package/dist/arena-history.d.ts +12 -0
  11. package/dist/arena-history.js +119 -0
  12. package/dist/arena-history.js.map +1 -0
  13. package/dist/ask-user.js +44 -2
  14. package/dist/ask-user.js.map +1 -1
  15. package/dist/attachment-limits.d.ts +4 -0
  16. package/dist/attachment-limits.js +10 -0
  17. package/dist/attachment-limits.js.map +1 -0
  18. package/dist/attachments.d.ts +4 -2
  19. package/dist/attachments.js +9 -11
  20. package/dist/attachments.js.map +1 -1
  21. package/dist/brave-search.js +11 -7
  22. package/dist/brave-search.js.map +1 -1
  23. package/dist/canonical-data.d.ts +9 -0
  24. package/dist/canonical-data.js +12 -3
  25. package/dist/canonical-data.js.map +1 -1
  26. package/dist/cli.js +140 -40
  27. package/dist/cli.js.map +1 -1
  28. package/dist/code-tool-schema.d.ts +3 -0
  29. package/dist/code-tool-schema.js +92 -0
  30. package/dist/code-tool-schema.js.map +1 -0
  31. package/dist/code-tools.js +63 -26
  32. package/dist/code-tools.js.map +1 -1
  33. package/dist/compaction-threshold.d.ts +3 -0
  34. package/dist/compaction-threshold.js +6 -0
  35. package/dist/compaction-threshold.js.map +1 -0
  36. package/dist/compaction.d.ts +24 -7
  37. package/dist/compaction.js +242 -66
  38. package/dist/compaction.js.map +1 -1
  39. package/dist/config.d.ts +17 -0
  40. package/dist/config.js +30 -5
  41. package/dist/config.js.map +1 -1
  42. package/dist/connector-errors.d.ts +16 -0
  43. package/dist/connector-errors.js +69 -0
  44. package/dist/connector-errors.js.map +1 -0
  45. package/dist/connector-files.d.ts +31 -0
  46. package/dist/connector-files.js +249 -0
  47. package/dist/connector-files.js.map +1 -0
  48. package/dist/connector-input.d.ts +10 -0
  49. package/dist/connector-input.js +79 -0
  50. package/dist/connector-input.js.map +1 -0
  51. package/dist/connector-mcp.d.ts +60 -0
  52. package/dist/connector-mcp.js +474 -0
  53. package/dist/connector-mcp.js.map +1 -0
  54. package/dist/connectors.d.ts +13 -0
  55. package/dist/connectors.js +69 -0
  56. package/dist/connectors.js.map +1 -0
  57. package/dist/fetch-page.js +2 -2
  58. package/dist/fetch-page.js.map +1 -1
  59. package/dist/history.d.ts +15 -5
  60. package/dist/history.js +59 -14
  61. package/dist/history.js.map +1 -1
  62. package/dist/image-choice.d.ts +23 -0
  63. package/dist/image-choice.js +190 -0
  64. package/dist/image-choice.js.map +1 -0
  65. package/dist/image-input-fallback.js +5 -0
  66. package/dist/image-input-fallback.js.map +1 -1
  67. package/dist/image-model.d.ts +6 -0
  68. package/dist/image-model.js +32 -0
  69. package/dist/image-model.js.map +1 -0
  70. package/dist/midturn-checkpoint-hook.d.ts +14 -1
  71. package/dist/midturn-checkpoint-hook.js.map +1 -1
  72. package/dist/mode.js +1 -1
  73. package/dist/mode.js.map +1 -1
  74. package/dist/model-capabilities.js +18 -8
  75. package/dist/model-capabilities.js.map +1 -1
  76. package/dist/model-router-media.js +55 -3
  77. package/dist/model-router-media.js.map +1 -1
  78. package/dist/model-stream.d.ts +16 -0
  79. package/dist/model-stream.js +129 -0
  80. package/dist/model-stream.js.map +1 -0
  81. package/dist/openai-responses.d.ts +2 -1
  82. package/dist/openai-responses.js +85 -88
  83. package/dist/openai-responses.js.map +1 -1
  84. package/dist/process-manager.js +9 -9
  85. package/dist/process-manager.js.map +1 -1
  86. package/dist/prompts.d.ts +0 -2
  87. package/dist/prompts.js +10 -60
  88. package/dist/prompts.js.map +1 -1
  89. package/dist/protocol.d.ts +5 -1
  90. package/dist/protocol.js +23 -2
  91. package/dist/protocol.js.map +1 -1
  92. package/dist/provider.d.ts +18 -0
  93. package/dist/provider.js +120 -51
  94. package/dist/provider.js.map +1 -1
  95. package/dist/repo.d.ts +1 -1
  96. package/dist/repo.js +26 -4
  97. package/dist/repo.js.map +1 -1
  98. package/dist/routify-web-fetch.js +2 -2
  99. package/dist/routify-web-fetch.js.map +1 -1
  100. package/dist/server.js +55 -27
  101. package/dist/server.js.map +1 -1
  102. package/dist/session-memory-prompts.d.ts +3 -1
  103. package/dist/session-memory-prompts.js +4 -2
  104. package/dist/session-memory-prompts.js.map +1 -1
  105. package/dist/side-query.js +5 -0
  106. package/dist/side-query.js.map +1 -1
  107. package/dist/spec.d.ts +5 -4
  108. package/dist/spec.js +30 -11
  109. package/dist/spec.js.map +1 -1
  110. package/dist/startup-config.d.ts +39 -0
  111. package/dist/startup-config.js +347 -0
  112. package/dist/startup-config.js.map +1 -0
  113. package/dist/startup-main-config.d.ts +63 -0
  114. package/dist/startup-main-config.js +94 -0
  115. package/dist/startup-main-config.js.map +1 -0
  116. package/dist/startup-tool-config.d.ts +8 -0
  117. package/dist/startup-tool-config.js +116 -0
  118. package/dist/startup-tool-config.js.map +1 -0
  119. package/dist/summary-compaction.d.ts +29 -0
  120. package/dist/{legacy-compaction.js → summary-compaction.js} +79 -14
  121. package/dist/summary-compaction.js.map +1 -0
  122. package/dist/tools.d.ts +5 -1
  123. package/dist/tools.js +164 -54
  124. package/dist/tools.js.map +1 -1
  125. package/dist/tui.js +26 -6
  126. package/dist/tui.js.map +1 -1
  127. package/dist/types.d.ts +21 -1
  128. package/dist/web/app.js +30 -5
  129. package/dist/web/app.js.map +1 -1
  130. package/dist/workspace.js +61 -14
  131. package/dist/workspace.js.map +1 -1
  132. package/docs/BINARY_DISTRIBUTION.md +44 -8
  133. package/docs/MODES_AND_RELEASES.md +79 -6
  134. package/docs/SDK.md +1 -0
  135. package/modes/code-arena/capture-provenance.json +9 -6
  136. package/modes/code-arena/full-stack/dependencies.lock.json +8670 -0
  137. package/modes/code-arena/full-stack/system-prompt.txt +1 -1
  138. package/modes/code-arena/html/system-prompt.txt +1 -1
  139. package/modes/code-arena/react/dependencies.lock.json +2707 -0
  140. package/package.json +7 -2
  141. package/scripts/README.md +23 -4
  142. package/web/styles.css +22 -0
  143. package/dist/legacy-compaction.d.ts +0 -16
  144. package/dist/legacy-compaction.js.map +0 -1
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,12 +11,16 @@ 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";
15
+ import { ConnectorTools, connectorFailureText } from "./connector-mcp.js";
13
16
  import { supportsImageInput } from "./config.js";
14
17
  import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
15
18
  import { FetchPageReader } from "./fetch-page.js";
16
19
  import { replaceFirstText } from "./file-edit.js";
17
20
  import { generatedImageBytesForExtension, prepareImageForModel, } from "./image.js";
18
21
  import { imageTextFromFallbackModel } from "./image-input-fallback.js";
22
+ import { offerGeneratedImages } from "./image-choice.js";
23
+ import { arenaImageModel, imageOptionModels } from "./image-model.js";
19
24
  import { isCodeArenaMode } from "./mode.js";
20
25
  import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
21
26
  import { processManagerForWorkspace, } from "./process-manager.js";
@@ -595,11 +600,14 @@ async function responseBytes(response, limit = 4 * 1024 * 1024) {
595
600
  }
596
601
  class ArenaToolExecutor {
597
602
  config;
603
+ interactionBoundaries = new Set();
604
+ interactionQueue = Promise.resolve();
598
605
  workspace;
599
606
  workspaceReal;
600
607
  tmpReal;
601
608
  varTmpReal;
602
609
  validators = new Map();
610
+ connectors;
603
611
  pendingVisualContent = new Map();
604
612
  askUserHandler;
605
613
  imageInputFallbackClient;
@@ -617,6 +625,7 @@ class ArenaToolExecutor {
617
625
  // The captured runtime uses Zod object parsing: unknown object keys are
618
626
  // stripped before dispatch instead of being emitted as validation issues.
619
627
  removeAdditional: "all",
628
+ useDefaults: true,
620
629
  });
621
630
  // ask_user needs a handler: TUI/WebUI collect real user input, while
622
631
  // headless `-p` may delegate to a configured remote User Agent. add_voice
@@ -636,7 +645,18 @@ class ArenaToolExecutor {
636
645
  this.varTmpReal = realpathSync("/var/tmp");
637
646
  this.processManager = processManagerForWorkspace(this.workspaceReal);
638
647
  this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
639
- 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;
640
660
  this.askUserEnabled = Boolean(options.askUserHandler);
641
661
  if (config.imageInputFallbackModel?.trim()) {
642
662
  const connection = imageInputFallbackConnection(config);
@@ -648,6 +668,9 @@ class ArenaToolExecutor {
648
668
  }
649
669
  this.signal = options.signal;
650
670
  this.workspaceContext = options.workspaceContext;
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) });
673
+ }
651
674
  this.pageReader = new FetchPageReader({ signal: options.signal });
652
675
  this.routifyPageReader = new RoutifyFetchPageReader({
653
676
  signal: options.signal,
@@ -660,21 +683,57 @@ class ArenaToolExecutor {
660
683
  type: "string",
661
684
  validate: (value) => URL.canParse(value),
662
685
  });
663
- for (const definition of toolDefinitions(this.workspaceContext)) {
664
- 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));
665
694
  }
666
695
  }
667
696
  // The tool schemas offered to the model for this session. Identical to the
668
697
  // canonical trace set, adjusted for the selected workspace runtime and minus
669
698
  // ask_user when no handler is wired. add_voice stays available headlessly.
670
699
  availableTools() {
671
- let definitions = toolDefinitions(this.workspaceContext);
700
+ let definitions = this.allDefinitions();
672
701
  if (!this.askUserEnabled) {
673
702
  definitions = definitions.filter((definition) => definition.function.name !== "ask_user");
674
703
  }
675
704
  const disallowed = new Set(this.config.disallowedTools || []);
676
705
  return definitions.filter((definition) => !disallowed.has(definition.function.name));
677
706
  }
707
+ allDefinitions() {
708
+ return [...toolDefinitions(this.workspaceContext, "arena", this.config.connectors, this.config.connectedConnectors),
709
+ ...(this.connectors?.definitions() || [])];
710
+ }
711
+ async beginConnectorTurn(history) {
712
+ await this.connectors?.beginTurn(history);
713
+ }
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
+ }
678
737
  parseArguments(call) {
679
738
  const rawArguments = typeof call.function?.arguments === "string"
680
739
  ? call.function.arguments
@@ -704,10 +763,16 @@ class ArenaToolExecutor {
704
763
  return { rawArguments, value };
705
764
  }
706
765
  validateArguments(name, value, validate) {
707
- 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
+ }
708
773
  if (!validate(validatedArguments)) {
709
774
  return {
710
- validationMessage: arenaValidationError(name, value, validate.errors),
775
+ validationMessage: this.connectors?.has(name) ? connectorFailureText : arenaValidationError(name, value, validate.errors),
711
776
  };
712
777
  }
713
778
  if (name === "ask_user") {
@@ -730,7 +795,7 @@ class ArenaToolExecutor {
730
795
  ? call.function.arguments
731
796
  : "{}";
732
797
  const parsed = this.parseArguments(call);
733
- const validate = this.validators.get(name);
798
+ const validate = this.validators.get(name) || this.connectors?.validator(name);
734
799
  if (this.config.disallowedTools?.includes(name) || !validate) {
735
800
  return {
736
801
  id,
@@ -740,7 +805,7 @@ class ArenaToolExecutor {
740
805
  validation: "unknown_tool",
741
806
  validationMessage: this.config.disallowedTools?.includes(name)
742
807
  ? `tool is disallowed: ${name}`
743
- : unavailableToolError(name, toolDefinitions(this.workspaceContext).map((definition) => definition.function.name)),
808
+ : this.connectors?.recognizes(name) ? connectorFailureText : unavailableToolError(name, this.allDefinitions().map((definition) => definition.function.name)),
744
809
  };
745
810
  }
746
811
  if (parsed.error !== undefined) {
@@ -749,7 +814,7 @@ class ArenaToolExecutor {
749
814
  name,
750
815
  rawArguments: parsed.rawArguments,
751
816
  validation: "invalid_json",
752
- validationMessage: arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
817
+ validationMessage: this.connectors?.has(name) ? connectorFailureText : arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
753
818
  };
754
819
  }
755
820
  const validated = this.validateArguments(name, parsed.value, validate);
@@ -788,12 +853,12 @@ class ArenaToolExecutor {
788
853
  if (this.config.disallowedTools?.includes(name)) {
789
854
  throw new Error(`tool is disallowed: ${name}`);
790
855
  }
791
- const validate = this.validators.get(name);
856
+ const validate = this.validators.get(name) || this.connectors?.validator(name);
792
857
  if (!validate) {
793
858
  return {
794
859
  role: "tool",
795
860
  tool_call_id: callId,
796
- content: unavailableToolError(name, toolDefinitions(this.workspaceContext).map((definition) => definition.function.name)),
861
+ content: this.connectors?.recognizes(name) ? connectorFailureText : unavailableToolError(name, this.allDefinitions().map((definition) => definition.function.name)),
797
862
  };
798
863
  }
799
864
  const parsed = this.parseArguments(call);
@@ -801,7 +866,7 @@ class ArenaToolExecutor {
801
866
  return {
802
867
  role: "tool",
803
868
  tool_call_id: callId,
804
- content: arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
869
+ content: this.connectors?.has(name) ? connectorFailureText : arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
805
870
  };
806
871
  }
807
872
  const argumentsValue = parsed.value;
@@ -860,7 +925,7 @@ class ArenaToolExecutor {
860
925
  return {
861
926
  role: "tool",
862
927
  tool_call_id: callId,
863
- content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt)),
928
+ content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt, name === "generate_image" ? context?.signal : undefined)),
864
929
  };
865
930
  }
866
931
  const content = typeof result === "string" ? result : json(result);
@@ -893,20 +958,20 @@ class ArenaToolExecutor {
893
958
  };
894
959
  }
895
960
  }
896
- runtimeFailureResult(name, values, error, startedAt) {
897
- if (this.signal?.aborted)
961
+ runtimeFailureResult(name, values, error, startedAt, contextSignal) {
962
+ if ((this.signal || contextSignal)?.aborted)
898
963
  return { status: "aborted" };
899
964
  const message = errorMessage(error);
900
965
  const durationMs = Date.now() - startedAt;
901
966
  if (name === "bash") {
902
967
  return {
903
- status: "shell_error",
904
- stderr: message,
905
968
  stdout: "",
969
+ stdout_truncated: false,
970
+ stderr: message,
971
+ stderr_truncated: false,
906
972
  exit_code: null,
973
+ status: "shell_error",
907
974
  duration_ms: durationMs,
908
- stderr_truncated: false,
909
- stdout_truncated: false,
910
975
  };
911
976
  }
912
977
  if (name === "start_process") {
@@ -929,8 +994,8 @@ class ArenaToolExecutor {
929
994
  if (name === "get_process_output") {
930
995
  return {
931
996
  status: "shell_error",
932
- log_tail: message,
933
997
  exit_code: null,
998
+ log_tail: message,
934
999
  listening_ports: [],
935
1000
  };
936
1001
  }
@@ -987,6 +1052,11 @@ class ArenaToolExecutor {
987
1052
  };
988
1053
  }
989
1054
  async dispatch(name, values, toolCallId, context) {
1055
+ if (name === "list_connector_tools" && this.connectors) {
1056
+ return this.connectors.discover(String(values.service));
1057
+ }
1058
+ if (this.connectors?.has(name))
1059
+ return this.connectors.call(name, values);
990
1060
  // Canned replay responses are served in execute() before schema
991
1061
  // validation (the lookup consumes entries — single serving point).
992
1062
  switch (name) {
@@ -1002,7 +1072,7 @@ class ArenaToolExecutor {
1002
1072
  case "edit_file":
1003
1073
  return this.editFile(String(values.path), String(values.old_text), String(values.new_text));
1004
1074
  case "read_file":
1005
- return this.readWorkspaceFile(String(values.path), toolCallId);
1075
+ return this.readWorkspaceFile(String(values.path), toolCallId, values.offset, values.limit);
1006
1076
  case "fetch_page":
1007
1077
  return routifyWebFetchEnabled()
1008
1078
  ? this.routifyPageReader.read(String(values.url), Number(values.chunkIndex || 0))
@@ -1023,7 +1093,7 @@ class ArenaToolExecutor {
1023
1093
  ? path.resolve(this.workspaceContext.cwd)
1024
1094
  : "/home/user")), Number(values.startup_wait || 5));
1025
1095
  case "generate_image":
1026
- 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);
1027
1097
  case "generate_speech":
1028
1098
  return this.generateSpeech(String(values.file_path), String(values.text), String(values.voice_id), values.language === undefined ? undefined : String(values.language));
1029
1099
  case "get_process_output":
@@ -1180,9 +1250,8 @@ class ArenaToolExecutor {
1180
1250
  }
1181
1251
  return new Promise((resolve, reject) => {
1182
1252
  const env = this.sandboxEnvironment();
1183
- // detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
1184
- // 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
1185
- // 必须杀整组,并在宽限期后强制结算兜底。
1253
+ // A process group lets cancellation stop descendants too. A wait timeout
1254
+ // is different: the captured contract permits the command to continue.
1186
1255
  const child = spawn("/bin/bash", ["-lc", command], {
1187
1256
  cwd: workdir,
1188
1257
  env,
@@ -1219,10 +1288,8 @@ class ArenaToolExecutor {
1219
1288
  clearTimeout(timeout);
1220
1289
  if (forceSettle)
1221
1290
  clearTimeout(forceSettle);
1222
- // bash is one-shot in v2. Any descendant left in the process group is
1223
- // cleaned up even when the shell itself exited normally; persistent
1224
- // servers belong in start_process.
1225
- killTree("SIGKILL");
1291
+ if (!timedOut || this.signal?.aborted)
1292
+ killTree("SIGKILL");
1226
1293
  this.signal?.removeEventListener("abort", abort);
1227
1294
  if (this.signal?.aborted) {
1228
1295
  reject(this.signal.reason || new Error("operation cancelled"));
@@ -1231,13 +1298,13 @@ class ArenaToolExecutor {
1231
1298
  const out = stdout.result();
1232
1299
  const err = stderr.result();
1233
1300
  resolve({
1234
- status: timedOut ? "timeout" : signal ? "killed" : "completed",
1235
- stderr: err.text,
1236
1301
  stdout: out.text,
1302
+ stdout_truncated: out.truncated,
1303
+ stderr: err.text,
1304
+ stderr_truncated: err.truncated,
1237
1305
  exit_code: timedOut ? null : code,
1306
+ status: timedOut ? "timeout" : signal ? "killed" : "completed",
1238
1307
  duration_ms: Date.now() - startedAt,
1239
- stderr_truncated: err.truncated,
1240
- stdout_truncated: out.truncated,
1241
1308
  });
1242
1309
  };
1243
1310
  // 宽限期结束时升级为组级 SIGKILL 再结算:abort 只发 SIGTERM,
@@ -1250,8 +1317,12 @@ class ArenaToolExecutor {
1250
1317
  };
1251
1318
  const timeout = setTimeout(() => {
1252
1319
  timedOut = true;
1253
- killTree("SIGKILL");
1254
- 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?.();
1255
1326
  }, timeoutSeconds * 1000);
1256
1327
  const abort = () => {
1257
1328
  killTree("SIGTERM");
@@ -1259,10 +1330,12 @@ class ArenaToolExecutor {
1259
1330
  };
1260
1331
  this.signal?.addEventListener("abort", abort, { once: true });
1261
1332
  child.stdout.on("data", (chunk) => {
1262
- stdout.append(chunk);
1333
+ if (!settled)
1334
+ stdout.append(chunk);
1263
1335
  });
1264
1336
  child.stderr.on("data", (chunk) => {
1265
- stderr.append(chunk);
1337
+ if (!settled)
1338
+ stderr.append(chunk);
1266
1339
  });
1267
1340
  child.once("error", (error) => {
1268
1341
  if (settled)
@@ -1406,7 +1479,7 @@ class ArenaToolExecutor {
1406
1479
  }
1407
1480
  return { status: "success", message: `Edited ${rawPath}.` };
1408
1481
  }
1409
- async readWorkspaceFile(rawPath, toolCallId) {
1482
+ async readWorkspaceFile(rawPath, toolCallId, offset, limit) {
1410
1483
  let target;
1411
1484
  let fileSize;
1412
1485
  let sample;
@@ -1489,7 +1562,10 @@ class ArenaToolExecutor {
1489
1562
  return result;
1490
1563
  }
1491
1564
  if (kind === "text") {
1492
- 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);
1493
1569
  return {
1494
1570
  kind: "text",
1495
1571
  lines: data.reduce((count, byte) => count + (byte === 0x0a ? 1 : 0), 1),
@@ -1571,7 +1647,15 @@ class ArenaToolExecutor {
1571
1647
  return { status: "success" };
1572
1648
  }
1573
1649
  async webSearch(query, depth) {
1574
- 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
+ }) };
1575
1659
  }
1576
1660
  async imageSearch(query, count) {
1577
1661
  const payload = await executeBraveImageSearch(query, count, {
@@ -1619,10 +1703,10 @@ class ArenaToolExecutor {
1619
1703
  mkdirSync(path.dirname(target), { recursive: true });
1620
1704
  await writeFile(target, await responseBytes(imageResponse));
1621
1705
  saved.push({
1622
- title: stringValue(item.title),
1623
1706
  file_path: this.relativePath(target),
1624
- source_url: sourceUrl,
1625
1707
  thumbnail_url: thumbnailUrl,
1708
+ title: stringValue(item.title),
1709
+ source_url: sourceUrl,
1626
1710
  });
1627
1711
  break;
1628
1712
  }
@@ -1647,13 +1731,13 @@ class ArenaToolExecutor {
1647
1731
  const target = await this.confinedPath(rawPath);
1648
1732
  if (!(await stat(target)).isFile())
1649
1733
  throw new Error(rawPath);
1650
- return { path: rawPath, status: "success" };
1734
+ return { status: "success", path: rawPath };
1651
1735
  }
1652
1736
  catch {
1653
1737
  return { status: "error", message: "Error calling present file tool" };
1654
1738
  }
1655
1739
  }
1656
- async generateImage(rawPath, prompt, images) {
1740
+ async generateImage(rawPath, prompt, images, offerOptions = false, toolCallId = "generate_image", context) {
1657
1741
  if (!rawPath) {
1658
1742
  return { status: "error", message: "File path cannot be empty." };
1659
1743
  }
@@ -1665,8 +1749,8 @@ class ArenaToolExecutor {
1665
1749
  message: "File path must end in .jpg, .jpeg, or .png.",
1666
1750
  };
1667
1751
  }
1668
- mkdirSync(path.dirname(target), { recursive: true });
1669
- const model = process.env.ARENA_IMAGE_MODEL || "gemini-3.1-flash-image";
1752
+ if (!offerOptions)
1753
+ mkdirSync(path.dirname(target), { recursive: true });
1670
1754
  const references = [];
1671
1755
  for (const imagePath of images || []) {
1672
1756
  const source = await this.confinedPath(imagePath);
@@ -1691,14 +1775,26 @@ class ArenaToolExecutor {
1691
1775
  mimeType,
1692
1776
  });
1693
1777
  }
1694
- const generated = await generateImageWithModelRouter({
1695
- images: references,
1696
- model,
1697
- prompt,
1698
- signal: this.signal,
1699
- size: process.env.ARENA_IMAGE_SIZE || "1024*1024",
1700
- });
1701
- 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()));
1702
1798
  return {
1703
1799
  status: "success",
1704
1800
  file_path: rawPath,
@@ -1763,6 +1859,20 @@ export class ToolExecutor {
1763
1859
  availableTools() {
1764
1860
  return this.implementation.availableTools();
1765
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
+ }
1868
+ async beginConnectorTurn(history) {
1869
+ if (this.implementation instanceof ArenaToolExecutor)
1870
+ await this.implementation.beginConnectorTurn(history);
1871
+ }
1872
+ async closeConnectors() {
1873
+ if (this.implementation instanceof ArenaToolExecutor)
1874
+ await this.implementation.closeConnectors();
1875
+ }
1766
1876
  inspect(call) {
1767
1877
  return this.implementation.inspect(call);
1768
1878
  }