@qwen-code/arena-agent 0.0.10-dev.8-test.0 → 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 (151) hide show
  1. package/.env.example +61 -10
  2. package/CHANGELOG.md +148 -17
  3. package/README.md +309 -46
  4. package/dist/agent.d.ts +20 -6
  5. package/dist/agent.js +353 -72
  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.d.ts +2 -1
  32. package/dist/code-tools.js +103 -31
  33. package/dist/code-tools.js.map +1 -1
  34. package/dist/compaction-threshold.d.ts +3 -0
  35. package/dist/compaction-threshold.js +6 -0
  36. package/dist/compaction-threshold.js.map +1 -0
  37. package/dist/compaction.d.ts +24 -7
  38. package/dist/compaction.js +242 -66
  39. package/dist/compaction.js.map +1 -1
  40. package/dist/config.d.ts +17 -0
  41. package/dist/config.js +30 -5
  42. package/dist/config.js.map +1 -1
  43. package/dist/connector-errors.d.ts +16 -0
  44. package/dist/connector-errors.js +69 -0
  45. package/dist/connector-errors.js.map +1 -0
  46. package/dist/connector-files.d.ts +31 -0
  47. package/dist/connector-files.js +249 -0
  48. package/dist/connector-files.js.map +1 -0
  49. package/dist/connector-input.d.ts +10 -0
  50. package/dist/connector-input.js +79 -0
  51. package/dist/connector-input.js.map +1 -0
  52. package/dist/connector-mcp.d.ts +60 -0
  53. package/dist/connector-mcp.js +474 -0
  54. package/dist/connector-mcp.js.map +1 -0
  55. package/dist/connectors.d.ts +13 -0
  56. package/dist/connectors.js +69 -0
  57. package/dist/connectors.js.map +1 -0
  58. package/dist/fetch-page.js +2 -2
  59. package/dist/fetch-page.js.map +1 -1
  60. package/dist/history.d.ts +16 -5
  61. package/dist/history.js +73 -14
  62. package/dist/history.js.map +1 -1
  63. package/dist/image-choice.d.ts +23 -0
  64. package/dist/image-choice.js +190 -0
  65. package/dist/image-choice.js.map +1 -0
  66. package/dist/image-input-fallback.js +5 -0
  67. package/dist/image-input-fallback.js.map +1 -1
  68. package/dist/image-model.d.ts +6 -0
  69. package/dist/image-model.js +32 -0
  70. package/dist/image-model.js.map +1 -0
  71. package/dist/midturn-checkpoint-hook.d.ts +14 -1
  72. package/dist/midturn-checkpoint-hook.js.map +1 -1
  73. package/dist/mode.js +1 -1
  74. package/dist/mode.js.map +1 -1
  75. package/dist/model-capabilities.js +18 -8
  76. package/dist/model-capabilities.js.map +1 -1
  77. package/dist/model-router-media.js +55 -3
  78. package/dist/model-router-media.js.map +1 -1
  79. package/dist/model-stream.d.ts +16 -0
  80. package/dist/model-stream.js +129 -0
  81. package/dist/model-stream.js.map +1 -0
  82. package/dist/openai-responses.d.ts +2 -1
  83. package/dist/openai-responses.js +85 -88
  84. package/dist/openai-responses.js.map +1 -1
  85. package/dist/process-manager.js +9 -9
  86. package/dist/process-manager.js.map +1 -1
  87. package/dist/prompts.d.ts +0 -2
  88. package/dist/prompts.js +10 -60
  89. package/dist/prompts.js.map +1 -1
  90. package/dist/protocol.d.ts +5 -1
  91. package/dist/protocol.js +23 -2
  92. package/dist/protocol.js.map +1 -1
  93. package/dist/provider.d.ts +18 -0
  94. package/dist/provider.js +120 -51
  95. package/dist/provider.js.map +1 -1
  96. package/dist/repo.d.ts +1 -1
  97. package/dist/repo.js +26 -4
  98. package/dist/repo.js.map +1 -1
  99. package/dist/routify-web-fetch.js +2 -2
  100. package/dist/routify-web-fetch.js.map +1 -1
  101. package/dist/server.js +55 -27
  102. package/dist/server.js.map +1 -1
  103. package/dist/session-memory-prompts.d.ts +3 -1
  104. package/dist/session-memory-prompts.js +4 -2
  105. package/dist/session-memory-prompts.js.map +1 -1
  106. package/dist/side-query.js +5 -0
  107. package/dist/side-query.js.map +1 -1
  108. package/dist/spec.d.ts +5 -4
  109. package/dist/spec.js +31 -23
  110. package/dist/spec.js.map +1 -1
  111. package/dist/startup-config.d.ts +39 -0
  112. package/dist/startup-config.js +347 -0
  113. package/dist/startup-config.js.map +1 -0
  114. package/dist/startup-main-config.d.ts +63 -0
  115. package/dist/startup-main-config.js +94 -0
  116. package/dist/startup-main-config.js.map +1 -0
  117. package/dist/startup-tool-config.d.ts +8 -0
  118. package/dist/startup-tool-config.js +116 -0
  119. package/dist/startup-tool-config.js.map +1 -0
  120. package/dist/summary-compaction.d.ts +29 -0
  121. package/dist/{legacy-compaction.js → summary-compaction.js} +79 -14
  122. package/dist/summary-compaction.js.map +1 -0
  123. package/dist/tools.d.ts +5 -1
  124. package/dist/tools.js +195 -68
  125. package/dist/tools.js.map +1 -1
  126. package/dist/tui.js +26 -6
  127. package/dist/tui.js.map +1 -1
  128. package/dist/types.d.ts +21 -1
  129. package/dist/web/app.js +30 -5
  130. package/dist/web/app.js.map +1 -1
  131. package/dist/workspace.js +61 -13
  132. package/dist/workspace.js.map +1 -1
  133. package/docs/BINARY_DISTRIBUTION.md +44 -8
  134. package/docs/MODES_AND_RELEASES.md +79 -6
  135. package/docs/SDK.md +1 -0
  136. package/modes/code-arena/capture-provenance.json +57 -0
  137. package/modes/code-arena/full-stack/dependencies.lock.json +8670 -0
  138. package/modes/code-arena/full-stack/system-prompt.txt +1 -1
  139. package/modes/code-arena/full-stack/template/eslint.config.mjs +3 -4
  140. package/modes/code-arena/full-stack/template/postcss.config.mjs +2 -2
  141. package/modes/code-arena/full-stack/tools.json +186 -186
  142. package/modes/code-arena/html/system-prompt.txt +1 -1
  143. package/modes/code-arena/html/tools.json +123 -123
  144. package/modes/code-arena/react/dependencies.lock.json +2707 -0
  145. package/modes/code-arena/react/system-prompt.txt +4 -3
  146. package/modes/code-arena/react/tools.json +178 -184
  147. package/package.json +7 -2
  148. package/scripts/README.md +23 -4
  149. package/web/styles.css +22 -0
  150. package/dist/legacy-compaction.d.ts +0 -16
  151. 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";
@@ -424,12 +429,22 @@ function receivedType(value) {
424
429
  return "array";
425
430
  return typeof value;
426
431
  }
432
+ function arenaEnumIssue(values, pathParts) {
433
+ return {
434
+ code: "invalid_value", values, path: pathParts,
435
+ message: `Invalid option: expected one of ${values.map((item) => JSON.stringify(item)).join("|")}`,
436
+ };
437
+ }
427
438
  function arenaValidationIssue(error, value) {
428
439
  const pathParts = jsonPointerPath(error.instancePath);
429
440
  if (error.keyword === "required") {
430
441
  const missingProperty = String(error.params.missingProperty);
431
442
  const parentSchema = error.parentSchema;
432
- const expected = zodTypeName(parentSchema?.properties?.[missingProperty]?.type || "unknown");
443
+ const missingSchema = parentSchema?.properties?.[missingProperty];
444
+ if (missingSchema?.enum) {
445
+ return arenaEnumIssue(missingSchema.enum, [...pathParts, missingProperty]);
446
+ }
447
+ const expected = zodTypeName(missingSchema?.type || "unknown");
433
448
  return {
434
449
  expected,
435
450
  code: "invalid_type",
@@ -490,19 +505,11 @@ function arenaValidationIssue(error, value) {
490
505
  };
491
506
  }
492
507
  if (error.keyword === "enum") {
493
- const values = error.params.allowedValues;
494
- return {
495
- code: "invalid_value",
496
- values,
497
- path: pathParts,
498
- message: `Invalid option: expected one of ${values
499
- .map((item) => JSON.stringify(item))
500
- .join("|")}`,
501
- };
508
+ return arenaEnumIssue(error.params.allowedValues, pathParts);
502
509
  }
503
510
  if (error.keyword === "format") {
504
511
  return {
505
- origin: "string",
512
+ ...(error.params.format === "uri" ? {} : { origin: "string" }),
506
513
  code: "invalid_format",
507
514
  format: error.params.format === "uri" ? "url" : error.params.format,
508
515
  path: pathParts,
@@ -548,8 +555,8 @@ function arenaValidationIssues(error, value) {
548
555
  }
549
556
  return issues;
550
557
  }
551
- function arenaValidationError(toolName, value, errors) {
552
- const issues = (errors || []).flatMap((error) => arenaValidationIssues(error, value));
558
+ function arenaValidationError(toolName, value, errors, customIssues = []) {
559
+ const issues = (errors || []).flatMap((error) => arenaValidationIssues(error, value)).concat(customIssues);
553
560
  return (`Invalid input for tool ${toolName}: Type validation failed: Value: ` +
554
561
  `${JSON.stringify(value)}.\nError message: ${JSON.stringify(issues, null, 2)}`);
555
562
  }
@@ -593,11 +600,14 @@ async function responseBytes(response, limit = 4 * 1024 * 1024) {
593
600
  }
594
601
  class ArenaToolExecutor {
595
602
  config;
603
+ interactionBoundaries = new Set();
604
+ interactionQueue = Promise.resolve();
596
605
  workspace;
597
606
  workspaceReal;
598
607
  tmpReal;
599
608
  varTmpReal;
600
609
  validators = new Map();
610
+ connectors;
601
611
  pendingVisualContent = new Map();
602
612
  askUserHandler;
603
613
  imageInputFallbackClient;
@@ -615,6 +625,7 @@ class ArenaToolExecutor {
615
625
  // The captured runtime uses Zod object parsing: unknown object keys are
616
626
  // stripped before dispatch instead of being emitted as validation issues.
617
627
  removeAdditional: "all",
628
+ useDefaults: true,
618
629
  });
619
630
  // ask_user needs a handler: TUI/WebUI collect real user input, while
620
631
  // headless `-p` may delegate to a configured remote User Agent. add_voice
@@ -634,7 +645,18 @@ class ArenaToolExecutor {
634
645
  this.varTmpReal = realpathSync("/var/tmp");
635
646
  this.processManager = processManagerForWorkspace(this.workspaceReal);
636
647
  this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
637
- 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;
638
660
  this.askUserEnabled = Boolean(options.askUserHandler);
639
661
  if (config.imageInputFallbackModel?.trim()) {
640
662
  const connection = imageInputFallbackConnection(config);
@@ -646,6 +668,9 @@ class ArenaToolExecutor {
646
668
  }
647
669
  this.signal = options.signal;
648
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
+ }
649
674
  this.pageReader = new FetchPageReader({ signal: options.signal });
650
675
  this.routifyPageReader = new RoutifyFetchPageReader({
651
676
  signal: options.signal,
@@ -658,28 +683,66 @@ class ArenaToolExecutor {
658
683
  type: "string",
659
684
  validate: (value) => URL.canParse(value),
660
685
  });
661
- for (const definition of toolDefinitions(this.workspaceContext)) {
662
- 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));
663
694
  }
664
695
  }
665
696
  // The tool schemas offered to the model for this session. Identical to the
666
697
  // canonical trace set, adjusted for the selected workspace runtime and minus
667
698
  // ask_user when no handler is wired. add_voice stays available headlessly.
668
699
  availableTools() {
669
- let definitions = toolDefinitions(this.workspaceContext);
700
+ let definitions = this.allDefinitions();
670
701
  if (!this.askUserEnabled) {
671
702
  definitions = definitions.filter((definition) => definition.function.name !== "ask_user");
672
703
  }
673
704
  const disallowed = new Set(this.config.disallowedTools || []);
674
705
  return definitions.filter((definition) => !disallowed.has(definition.function.name));
675
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
+ }
676
737
  parseArguments(call) {
677
738
  const rawArguments = typeof call.function?.arguments === "string"
678
739
  ? call.function.arguments
679
740
  : "{}";
680
741
  let value;
681
742
  try {
682
- value = JSON.parse(rawArguments);
743
+ // Captured empty arguments validate as an empty object, while whitespace
744
+ // and other incomplete JSON still produce parsing errors.
745
+ value = JSON.parse(rawArguments === "" ? "{}" : rawArguments);
683
746
  }
684
747
  catch (error) {
685
748
  return { rawArguments, invalidJsonText: rawArguments, error };
@@ -688,6 +751,8 @@ class ArenaToolExecutor {
688
751
  ? value.__malformedToolArguments
689
752
  : undefined;
690
753
  if (typeof malformedArguments === "string") {
754
+ if (malformedArguments === "")
755
+ return { rawArguments, value: {} };
691
756
  try {
692
757
  JSON.parse(malformedArguments);
693
758
  }
@@ -698,12 +763,29 @@ class ArenaToolExecutor {
698
763
  return { rawArguments, value };
699
764
  }
700
765
  validateArguments(name, value, validate) {
701
- 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
+ }
702
773
  if (!validate(validatedArguments)) {
703
774
  return {
704
- validationMessage: arenaValidationError(name, value, validate.errors),
775
+ validationMessage: this.connectors?.has(name) ? connectorFailureText : arenaValidationError(name, value, validate.errors),
705
776
  };
706
777
  }
778
+ if (name === "ask_user") {
779
+ const issues = validatedArguments.questions.flatMap((question, index) => {
780
+ const ids = question.options.map((option) => option.id);
781
+ return new Set(ids).size === ids.length ? [] : [{
782
+ code: "custom", path: ["questions", index, "options"],
783
+ message: "Option IDs must be unique within a question",
784
+ }];
785
+ });
786
+ if (issues.length)
787
+ return { validationMessage: arenaValidationError(name, value, [], issues) };
788
+ }
707
789
  return { value: validatedArguments };
708
790
  }
709
791
  inspect(call) {
@@ -713,7 +795,7 @@ class ArenaToolExecutor {
713
795
  ? call.function.arguments
714
796
  : "{}";
715
797
  const parsed = this.parseArguments(call);
716
- const validate = this.validators.get(name);
798
+ const validate = this.validators.get(name) || this.connectors?.validator(name);
717
799
  if (this.config.disallowedTools?.includes(name) || !validate) {
718
800
  return {
719
801
  id,
@@ -723,7 +805,7 @@ class ArenaToolExecutor {
723
805
  validation: "unknown_tool",
724
806
  validationMessage: this.config.disallowedTools?.includes(name)
725
807
  ? `tool is disallowed: ${name}`
726
- : 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)),
727
809
  };
728
810
  }
729
811
  if (parsed.error !== undefined) {
@@ -732,7 +814,7 @@ class ArenaToolExecutor {
732
814
  name,
733
815
  rawArguments: parsed.rawArguments,
734
816
  validation: "invalid_json",
735
- validationMessage: arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
817
+ validationMessage: this.connectors?.has(name) ? connectorFailureText : arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
736
818
  };
737
819
  }
738
820
  const validated = this.validateArguments(name, parsed.value, validate);
@@ -771,12 +853,12 @@ class ArenaToolExecutor {
771
853
  if (this.config.disallowedTools?.includes(name)) {
772
854
  throw new Error(`tool is disallowed: ${name}`);
773
855
  }
774
- const validate = this.validators.get(name);
856
+ const validate = this.validators.get(name) || this.connectors?.validator(name);
775
857
  if (!validate) {
776
858
  return {
777
859
  role: "tool",
778
860
  tool_call_id: callId,
779
- 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)),
780
862
  };
781
863
  }
782
864
  const parsed = this.parseArguments(call);
@@ -784,7 +866,7 @@ class ArenaToolExecutor {
784
866
  return {
785
867
  role: "tool",
786
868
  tool_call_id: callId,
787
- content: arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
869
+ content: this.connectors?.has(name) ? connectorFailureText : arenaJsonParsingError(name, parsed.invalidJsonText, parsed.error),
788
870
  };
789
871
  }
790
872
  const argumentsValue = parsed.value;
@@ -843,7 +925,7 @@ class ArenaToolExecutor {
843
925
  return {
844
926
  role: "tool",
845
927
  tool_call_id: callId,
846
- content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt)),
928
+ content: json(this.runtimeFailureResult(name, validatedArguments, error, startedAt, name === "generate_image" ? context?.signal : undefined)),
847
929
  };
848
930
  }
849
931
  const content = typeof result === "string" ? result : json(result);
@@ -876,20 +958,20 @@ class ArenaToolExecutor {
876
958
  };
877
959
  }
878
960
  }
879
- runtimeFailureResult(name, values, error, startedAt) {
880
- if (this.signal?.aborted)
961
+ runtimeFailureResult(name, values, error, startedAt, contextSignal) {
962
+ if ((this.signal || contextSignal)?.aborted)
881
963
  return { status: "aborted" };
882
964
  const message = errorMessage(error);
883
965
  const durationMs = Date.now() - startedAt;
884
966
  if (name === "bash") {
885
967
  return {
886
- status: "shell_error",
887
- stderr: message,
888
968
  stdout: "",
969
+ stdout_truncated: false,
970
+ stderr: message,
971
+ stderr_truncated: false,
889
972
  exit_code: null,
973
+ status: "shell_error",
890
974
  duration_ms: durationMs,
891
- stderr_truncated: false,
892
- stdout_truncated: false,
893
975
  };
894
976
  }
895
977
  if (name === "start_process") {
@@ -912,8 +994,8 @@ class ArenaToolExecutor {
912
994
  if (name === "get_process_output") {
913
995
  return {
914
996
  status: "shell_error",
915
- log_tail: message,
916
997
  exit_code: null,
998
+ log_tail: message,
917
999
  listening_ports: [],
918
1000
  };
919
1001
  }
@@ -970,6 +1052,11 @@ class ArenaToolExecutor {
970
1052
  };
971
1053
  }
972
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);
973
1060
  // Canned replay responses are served in execute() before schema
974
1061
  // validation (the lookup consumes entries — single serving point).
975
1062
  switch (name) {
@@ -985,7 +1072,7 @@ class ArenaToolExecutor {
985
1072
  case "edit_file":
986
1073
  return this.editFile(String(values.path), String(values.old_text), String(values.new_text));
987
1074
  case "read_file":
988
- return this.readWorkspaceFile(String(values.path), toolCallId);
1075
+ return this.readWorkspaceFile(String(values.path), toolCallId, values.offset, values.limit);
989
1076
  case "fetch_page":
990
1077
  return routifyWebFetchEnabled()
991
1078
  ? this.routifyPageReader.read(String(values.url), Number(values.chunkIndex || 0))
@@ -1006,7 +1093,7 @@ class ArenaToolExecutor {
1006
1093
  ? path.resolve(this.workspaceContext.cwd)
1007
1094
  : "/home/user")), Number(values.startup_wait || 5));
1008
1095
  case "generate_image":
1009
- 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);
1010
1097
  case "generate_speech":
1011
1098
  return this.generateSpeech(String(values.file_path), String(values.text), String(values.voice_id), values.language === undefined ? undefined : String(values.language));
1012
1099
  case "get_process_output":
@@ -1163,9 +1250,8 @@ class ArenaToolExecutor {
1163
1250
  }
1164
1251
  return new Promise((resolve, reject) => {
1165
1252
  const env = this.sandboxEnvironment();
1166
- // detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
1167
- // 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
1168
- // 必须杀整组,并在宽限期后强制结算兜底。
1253
+ // A process group lets cancellation stop descendants too. A wait timeout
1254
+ // is different: the captured contract permits the command to continue.
1169
1255
  const child = spawn("/bin/bash", ["-lc", command], {
1170
1256
  cwd: workdir,
1171
1257
  env,
@@ -1202,10 +1288,8 @@ class ArenaToolExecutor {
1202
1288
  clearTimeout(timeout);
1203
1289
  if (forceSettle)
1204
1290
  clearTimeout(forceSettle);
1205
- // bash is one-shot in v2. Any descendant left in the process group is
1206
- // cleaned up even when the shell itself exited normally; persistent
1207
- // servers belong in start_process.
1208
- killTree("SIGKILL");
1291
+ if (!timedOut || this.signal?.aborted)
1292
+ killTree("SIGKILL");
1209
1293
  this.signal?.removeEventListener("abort", abort);
1210
1294
  if (this.signal?.aborted) {
1211
1295
  reject(this.signal.reason || new Error("operation cancelled"));
@@ -1214,13 +1298,13 @@ class ArenaToolExecutor {
1214
1298
  const out = stdout.result();
1215
1299
  const err = stderr.result();
1216
1300
  resolve({
1217
- status: timedOut ? "timeout" : signal ? "killed" : "completed",
1218
- stderr: err.text,
1219
1301
  stdout: out.text,
1302
+ stdout_truncated: out.truncated,
1303
+ stderr: err.text,
1304
+ stderr_truncated: err.truncated,
1220
1305
  exit_code: timedOut ? null : code,
1306
+ status: timedOut ? "timeout" : signal ? "killed" : "completed",
1221
1307
  duration_ms: Date.now() - startedAt,
1222
- stderr_truncated: err.truncated,
1223
- stdout_truncated: out.truncated,
1224
1308
  });
1225
1309
  };
1226
1310
  // 宽限期结束时升级为组级 SIGKILL 再结算:abort 只发 SIGTERM,
@@ -1233,8 +1317,12 @@ class ArenaToolExecutor {
1233
1317
  };
1234
1318
  const timeout = setTimeout(() => {
1235
1319
  timedOut = true;
1236
- killTree("SIGKILL");
1237
- 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?.();
1238
1326
  }, timeoutSeconds * 1000);
1239
1327
  const abort = () => {
1240
1328
  killTree("SIGTERM");
@@ -1242,10 +1330,12 @@ class ArenaToolExecutor {
1242
1330
  };
1243
1331
  this.signal?.addEventListener("abort", abort, { once: true });
1244
1332
  child.stdout.on("data", (chunk) => {
1245
- stdout.append(chunk);
1333
+ if (!settled)
1334
+ stdout.append(chunk);
1246
1335
  });
1247
1336
  child.stderr.on("data", (chunk) => {
1248
- stderr.append(chunk);
1337
+ if (!settled)
1338
+ stderr.append(chunk);
1249
1339
  });
1250
1340
  child.once("error", (error) => {
1251
1341
  if (settled)
@@ -1389,7 +1479,7 @@ class ArenaToolExecutor {
1389
1479
  }
1390
1480
  return { status: "success", message: `Edited ${rawPath}.` };
1391
1481
  }
1392
- async readWorkspaceFile(rawPath, toolCallId) {
1482
+ async readWorkspaceFile(rawPath, toolCallId, offset, limit) {
1393
1483
  let target;
1394
1484
  let fileSize;
1395
1485
  let sample;
@@ -1472,7 +1562,10 @@ class ArenaToolExecutor {
1472
1562
  return result;
1473
1563
  }
1474
1564
  if (kind === "text") {
1475
- 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);
1476
1569
  return {
1477
1570
  kind: "text",
1478
1571
  lines: data.reduce((count, byte) => count + (byte === 0x0a ? 1 : 0), 1),
@@ -1554,7 +1647,15 @@ class ArenaToolExecutor {
1554
1647
  return { status: "success" };
1555
1648
  }
1556
1649
  async webSearch(query, depth) {
1557
- 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
+ }) };
1558
1659
  }
1559
1660
  async imageSearch(query, count) {
1560
1661
  const payload = await executeBraveImageSearch(query, count, {
@@ -1602,10 +1703,10 @@ class ArenaToolExecutor {
1602
1703
  mkdirSync(path.dirname(target), { recursive: true });
1603
1704
  await writeFile(target, await responseBytes(imageResponse));
1604
1705
  saved.push({
1605
- title: stringValue(item.title),
1606
1706
  file_path: this.relativePath(target),
1607
- source_url: sourceUrl,
1608
1707
  thumbnail_url: thumbnailUrl,
1708
+ title: stringValue(item.title),
1709
+ source_url: sourceUrl,
1609
1710
  });
1610
1711
  break;
1611
1712
  }
@@ -1630,13 +1731,13 @@ class ArenaToolExecutor {
1630
1731
  const target = await this.confinedPath(rawPath);
1631
1732
  if (!(await stat(target)).isFile())
1632
1733
  throw new Error(rawPath);
1633
- return { path: rawPath, status: "success" };
1734
+ return { status: "success", path: rawPath };
1634
1735
  }
1635
1736
  catch {
1636
1737
  return { status: "error", message: "Error calling present file tool" };
1637
1738
  }
1638
1739
  }
1639
- async generateImage(rawPath, prompt, images) {
1740
+ async generateImage(rawPath, prompt, images, offerOptions = false, toolCallId = "generate_image", context) {
1640
1741
  if (!rawPath) {
1641
1742
  return { status: "error", message: "File path cannot be empty." };
1642
1743
  }
@@ -1648,8 +1749,8 @@ class ArenaToolExecutor {
1648
1749
  message: "File path must end in .jpg, .jpeg, or .png.",
1649
1750
  };
1650
1751
  }
1651
- mkdirSync(path.dirname(target), { recursive: true });
1652
- const model = process.env.ARENA_IMAGE_MODEL || "gemini-3.1-flash-image";
1752
+ if (!offerOptions)
1753
+ mkdirSync(path.dirname(target), { recursive: true });
1653
1754
  const references = [];
1654
1755
  for (const imagePath of images || []) {
1655
1756
  const source = await this.confinedPath(imagePath);
@@ -1674,14 +1775,26 @@ class ArenaToolExecutor {
1674
1775
  mimeType,
1675
1776
  });
1676
1777
  }
1677
- const generated = await generateImageWithModelRouter({
1678
- images: references,
1679
- model,
1680
- prompt,
1681
- signal: this.signal,
1682
- size: process.env.ARENA_IMAGE_SIZE || "1024*1024",
1683
- });
1684
- 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()));
1685
1798
  return {
1686
1799
  status: "success",
1687
1800
  file_path: rawPath,
@@ -1746,6 +1859,20 @@ export class ToolExecutor {
1746
1859
  availableTools() {
1747
1860
  return this.implementation.availableTools();
1748
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
+ }
1749
1876
  inspect(call) {
1750
1877
  return this.implementation.inspect(call);
1751
1878
  }