@qwen-code/arena-agent 0.0.9-image-input-fallback.0 → 0.0.10-dev.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 (89) hide show
  1. package/.env.example +34 -13
  2. package/README.md +156 -53
  3. package/dist/agent.d.ts +4 -2
  4. package/dist/agent.js +142 -20
  5. package/dist/agent.js.map +1 -1
  6. package/dist/anthropic.d.ts +1 -0
  7. package/dist/anthropic.js +78 -19
  8. package/dist/anthropic.js.map +1 -1
  9. package/dist/canonical-data.js +2 -2
  10. package/dist/canonical-data.js.map +1 -1
  11. package/dist/cli.js +44 -13
  12. package/dist/cli.js.map +1 -1
  13. package/dist/code-tools.js +18 -161
  14. package/dist/code-tools.js.map +1 -1
  15. package/dist/compaction.js +8 -2
  16. package/dist/compaction.js.map +1 -1
  17. package/dist/config.d.ts +22 -0
  18. package/dist/config.js +53 -1
  19. package/dist/config.js.map +1 -1
  20. package/dist/history.d.ts +3 -2
  21. package/dist/history.js +7 -2
  22. package/dist/history.js.map +1 -1
  23. package/dist/image.d.ts +2 -0
  24. package/dist/image.js +88 -7
  25. package/dist/image.js.map +1 -1
  26. package/dist/model-capabilities.js +12 -2
  27. package/dist/model-capabilities.js.map +1 -1
  28. package/dist/model-diagnostics.js +11 -3
  29. package/dist/model-diagnostics.js.map +1 -1
  30. package/dist/model-router-media.d.ts +26 -0
  31. package/dist/model-router-media.js +274 -0
  32. package/dist/model-router-media.js.map +1 -0
  33. package/dist/openai-responses.d.ts +28 -0
  34. package/dist/openai-responses.js +473 -0
  35. package/dist/openai-responses.js.map +1 -0
  36. package/dist/process-manager.d.ts +38 -0
  37. package/dist/process-manager.js +486 -0
  38. package/dist/process-manager.js.map +1 -0
  39. package/dist/prompts.js +3 -1
  40. package/dist/prompts.js.map +1 -1
  41. package/dist/provider.d.ts +7 -0
  42. package/dist/provider.js +259 -14
  43. package/dist/provider.js.map +1 -1
  44. package/dist/server.d.ts +2 -0
  45. package/dist/server.js +37 -18
  46. package/dist/server.js.map +1 -1
  47. package/dist/spec.js +15 -1
  48. package/dist/spec.js.map +1 -1
  49. package/dist/tools.js +160 -237
  50. package/dist/tools.js.map +1 -1
  51. package/dist/tui.d.ts +2 -0
  52. package/dist/tui.js +15 -11
  53. package/dist/tui.js.map +1 -1
  54. package/dist/types.d.ts +7 -0
  55. package/dist/voice-registry.d.ts +30 -0
  56. package/dist/voice-registry.js +82 -0
  57. package/dist/voice-registry.js.map +1 -0
  58. package/dist/web/app.js +8 -0
  59. package/dist/web/app.js.map +1 -1
  60. package/docs/SDK.md +4 -3
  61. package/package.json +7 -2
  62. package/web/styles.css +6 -0
  63. package/dist/audit.d.ts +0 -14
  64. package/dist/audit.js +0 -31
  65. package/dist/audit.js.map +0 -1
  66. package/dist/dashscope.d.ts +0 -5
  67. package/dist/dashscope.js +0 -55
  68. package/dist/dashscope.js.map +0 -1
  69. package/dist/fetch-page/aparat.d.ts +0 -30
  70. package/dist/fetch-page/aparat.js +0 -209
  71. package/dist/fetch-page/aparat.js.map +0 -1
  72. package/dist/fetch-page/json-path.d.ts +0 -12
  73. package/dist/fetch-page/json-path.js +0 -111
  74. package/dist/fetch-page/json-path.js.map +0 -1
  75. package/dist/fetch-page/sites.d.ts +0 -26
  76. package/dist/fetch-page/sites.js +0 -39
  77. package/dist/fetch-page/sites.js.map +0 -1
  78. package/dist/fetch-page/text.d.ts +0 -5
  79. package/dist/fetch-page/text.js +0 -38
  80. package/dist/fetch-page/text.js.map +0 -1
  81. package/dist/fetch-page/types.d.ts +0 -17
  82. package/dist/fetch-page/types.js +0 -3
  83. package/dist/fetch-page/types.js.map +0 -1
  84. package/dist/fetch-page/youtube.d.ts +0 -35
  85. package/dist/fetch-page/youtube.js +0 -282
  86. package/dist/fetch-page/youtube.js.map +0 -1
  87. package/dist/web-search.d.ts +0 -5
  88. package/dist/web-search.js +0 -465
  89. package/dist/web-search.js.map +0 -1
package/.env.example CHANGED
@@ -7,6 +7,14 @@ OPENAI_MODEL=openai/qwen-latest-series-invite-beta-v92
7
7
  OPENAI_BASE_URL=
8
8
  OPENAI_API_KEY=
9
9
 
10
+ # Auto mode uses Responses for GPT model names and Chat Completions otherwise.
11
+ # Native Responses preserves opaque OpenAI reasoning across tool steps and
12
+ # separate CLI turns by replaying encrypted output items with store=false.
13
+ # Set chat to force Chat Completions.
14
+ # ARENA_OPENAI_API=auto
15
+ # ARENA_OPENAI_API=responses
16
+ # ARENA_OPENAI_API=chat
17
+
10
18
  # Unified runtime mode. Canonical values are shown; full_stack/fullstack and
11
19
  # single_html/single-html remain accepted compatibility aliases.
12
20
  ARENA_HARNESS_MODE=arena
@@ -72,6 +80,13 @@ ARENA_HARNESS_MODE=arena
72
80
  # false, the optional fallback model converts Arena read_file images to text.
73
81
  # ARENA_MODEL_SUPPORTS_IMAGE_INPUT=false
74
82
  # ARENA_IMAGE_INPUT_FALLBACK_MODEL=qwen3.7-plus
83
+ # Inline image budget before downscaling: max pixels per edge and max 28x28
84
+ # patches. Images within both limits are sent unchanged; larger ones are
85
+ # downscaled to fit and re-encoded as JPEG. Defaults match the captured
86
+ # baseline; raise both for long screenshots (a 1920x5344 full-page capture
87
+ # needs about 8192 edge and 13179 patches to pass through unchanged).
88
+ # ARENA_IMAGE_MAX_EDGE=1568
89
+ # ARENA_IMAGE_MAX_PATCHES=1568
75
90
 
76
91
  # Optional headless ask_user delegation. When enabled, `arena -p` sends the
77
92
  # current questions plus a serialized ATIF trajectory to this JSON endpoint.
@@ -80,10 +95,13 @@ ARENA_HARNESS_MODE=arena
80
95
  # ARENA_ASK_USER_HANDLER_URL=http://127.0.0.1:8080/v1/ask-user
81
96
  # ARENA_ASK_USER_TIMEOUT_MS=60000
82
97
 
83
- # Dynamic session metrics. High-confidence Bash cases use deterministic rules;
84
- # ambiguous Bash attribution/termination plus user praise, complaint, correction,
85
- # repair-attempt, and repair-verdict classification use this tool-free structured
86
- # side query. Disable it to retain those cases as unclassified/ambiguous.
98
+ # Dynamic session metrics. Disabled by default: with the switch off the daemon
99
+ # performs no metric analysis, persists no metrics, and emits no
100
+ # data-session-metrics events. Enable it to compute a snapshot after every
101
+ # user-facing turn; high-confidence Bash cases use deterministic rules, while
102
+ # ambiguous Bash attribution/termination plus user praise, complaint,
103
+ # correction, repair-attempt, and repair-verdict classification use this
104
+ # tool-free structured side query.
87
105
  # ARENA_METRICS_SIDE_QUERY_ENABLED=true
88
106
  # ARENA_METRICS_SIDE_QUERY_MODEL=openai/qwen-latest-series-invite-beta-v92
89
107
  # ARENA_METRICS_SIDE_QUERY_MAX_TOKENS=800
@@ -101,23 +119,26 @@ ARENA_HARNESS_MODE=arena
101
119
  # defaults to ~/.arena so runs never write into the task workspace.
102
120
  # ARENA_HOME=~/.arena
103
121
 
104
- # Brave web and image search through the Routify gateway.
122
+ # Search, page reading, image generation, and speech through ModelRouter/Routify.
105
123
  ROUTIFY_API_KEY=
106
124
  # ROUTIFY_WEB_SEARCH_URL=https://routify-pub.alibaba-inc.com/protocol/compatible-mode/res/v1/llm/context
107
125
  # ROUTIFY_IMAGE_SEARCH_URL=https://routify-pub.alibaba-inc.com/protocol/brave/res/v1/images/search
126
+ # ROUTIFY_VERTEX_BASE_URL=https://routify-pub.alibaba-inc.com/protocol/vertex/v1beta
108
127
  # ARENA_WEB_SEARCH_MODEL=brave.llm-context
109
128
  # CODE_HARNESS_WEB_SEARCH_MODEL=brave.llm-context
110
129
  # Optional fetch_page/web_fetch path: Firecrawl first, then Jina Reader.
111
130
  # CODE_HARNESS_ROUTIFY_WEB_FETCH_ENABLED=false
131
+ # ARENA_IMAGE_MODEL=gemini-2.5-flash-image
132
+ # ARENA_IMAGE_SIZE=1024*1024
133
+ # CODE_HARNESS_IMAGE_MODEL=gemini-2.5-flash-image
134
+ # CODE_HARNESS_IMAGE_SIZE=1024*1024
135
+ # ARENA_SPEECH_MODEL=doubao-tts
136
+ # ARENA_SPEECH_VOICE=zh_female_vv_uranus_bigtts
112
137
 
113
- # DashScope image generation and speech generation.
114
- DASHSCOPE_API_KEY=
115
- # Optional: a workspace-specific host, with or without /api/v1.
116
- # DASHSCOPE_API_BASE=https://your-workspace.cn-beijing.maas.aliyuncs.com
117
- # ARENA_IMAGE_MODEL=qwen-image-2.0-pro
118
- # ARENA_IMAGE_SIZE=2048*2048
119
- # ARENA_SPEECH_MODEL=qwen-audio-3.0-tts-flash
120
- # DASHSCOPE_SPEECH_VOICE=longanhuan_v3.6
138
+ # start_process automatically checks E2B preview hosts when E2B_SANDBOX_ID is
139
+ # present. For another preview proxy, use {port} and optionally {sandboxId}:
140
+ # ARENA_PREVIEW_HOST_TEMPLATE={port}-{sandboxId}.e2b.app
141
+ # ROUTIFY_MEDIA_MAX_RETRIES=5
121
142
 
122
143
  # Code Arena stock photo/video search used by fetch_media.
123
144
  # PEXELS_API_KEY=
package/README.md CHANGED
@@ -1,13 +1,17 @@
1
1
  # Arena CodeAgent Harness
2
2
 
3
- 这是一个根据 `message.json` 与 `tools.json` 逆向实现的 TypeScript CodeAgent
4
- harness。运行时只依赖 Node.js,不再依赖 Python:
3
+ 这是一个基于 `message.json` 与 `tools.json` 所定义消息和工具契约的 TypeScript
4
+ CodeAgent harness。运行时只依赖 Node.js,不再依赖 Python:
5
5
 
6
6
  - `arena -p ...`:headless 单轮入口,支持文本、完整 JSON 和 NDJSON 流。
7
7
  - `arena tui`:终端多轮对话,实时输出并自动保存、恢复历史。
8
8
  - `arena serve`:HTTP daemon、Web UI 与双通道 SSE 实时接口。
9
9
  - Agent loop:流式 assistant → tool call → tool result → assistant。
10
- - 11 个工具逐字使用 `tools.json` 的 Draft-07 schema。
10
+ - 15 个工具按照 `tools.json` 中的 Draft-07 schema 提供参数校验。
11
+
12
+ 本文提及的第三方产品、模型与服务名称仅用于说明兼容性和配置方式,不表示相关
13
+ 权利人对本项目的认可或背书;相关商标归各自权利人所有。使用者应自行确认并遵守
14
+ 所接入模型、数据源和在线服务的许可、服务条款及适用法律。
11
15
 
12
16
  ## 源码与 SDK 安装
13
17
 
@@ -86,6 +90,39 @@ code-harness -p '做一个单文件网页' --mode html
86
90
  `CODE_HARNESS_MODE`、`ARENA_MODE` 及旧模式别名。详细目录和发包决策见
87
91
  [`docs/MODES_AND_RELEASES.md`](docs/MODES_AND_RELEASES.md)。
88
92
 
93
+ 如需完全替换模型收到的 system prompt,可在任一运行入口传入
94
+ `--system-prompt <text>`:
95
+
96
+ ```bash
97
+ arena -p '分析这个目录' --system-prompt '你是一名精简的代码审查助手。'
98
+ arena tui --system-prompt '始终先给结论,再解释原因。'
99
+ arena serve --system-prompt '只使用中文回复。'
100
+ ```
101
+
102
+ 该参数只在运行时覆盖第一条 system message,不会修改 canonical prompt fixture;
103
+ 续跑已有 session 时,当前传入的值会替换历史中的 system message。未传参数时,
104
+ 日期、workspace 和 mode 对应的现有 prompt 生成逻辑保持不变。
105
+
106
+ 如需保留当前 mode 生成的 system prompt,只在末尾追加任务或部署指令,可传入
107
+ `--append-system-prompt <text>`:
108
+
109
+ ```bash
110
+ arena -p '分析这个目录' --append-system-prompt '回答保持精简。'
111
+ arena tui --append-system-prompt '始终先给结论,再解释原因。'
112
+ arena serve --append-system-prompt '只使用中文回复。'
113
+ ```
114
+
115
+ 追加内容会作为独立段落放在运行时 system message 末尾,因此原有日期、workspace、
116
+ 工具和 mode 指令保持不变,也不会修改 canonical prompt fixture。同一会话内只会追加
117
+ 一次,不会随 turn 重复累积。使用 headless CLI 续跑 session 时,需要再次传入相同的
118
+ 追加参数。`--system-prompt` 与 `--append-system-prompt` 可以同时使用,此时先完整
119
+ 替换,再追加指定文本。
120
+
121
+ `arena serve` 是一个例外:未显式传入 `--system-prompt` 时,它会保留现有系统
122
+ 提示词中的日期、workspace、工具和 mode 指令,但默认将身份声明设为 `Qwen`。
123
+ 显式传入 `--system-prompt` 时仍会完整替换系统提示词;仅传
124
+ `--append-system-prompt` 时,则先应用该 Qwen 身份变换,再追加文本。
125
+
89
126
  Arena 模式下,如需从 system prompt 的身份模型列表中移除 `Qwen`,可增加
90
127
  `--exclude-qwen-from-system-prompt`,或设置
91
128
  `ARENA_EXCLUDE_QWEN_FROM_SYSTEM_PROMPT=true`。CLI 开关优先于环境变量:
@@ -96,15 +133,18 @@ arena tui --exclude-qwen-from-system-prompt
96
133
  arena serve --exclude-qwen-from-system-prompt
97
134
  ```
98
135
 
99
- 三个 Code Arena 模式的 150 个历史 case、工具 replay 和真实模型行为实验入口见
136
+ 三个 Code Arena 模式的兼容性测试集、工具回归测试和模型行为评估入口见
100
137
  [`tasks/code-harness/README.md`](tasks/code-harness/README.md);这些开发数据不会
101
138
  进入 npm 或 SEA 发布包。
102
139
 
103
140
  ## 模型
104
141
 
105
- TypeScript 运行时根据模型前缀选择协议:`openai/` 使用 OpenAI Chat
106
- Completions,`anthropic/` 使用原生 Messages,`gemini/` 使用 Google 的
107
- OpenAI-compatible 接口。LiteLLM 官方提供 Python SDK 或独立 Proxy,因此
142
+ TypeScript 运行时根据模型前缀与名称选择协议:`anthropic/` 使用原生
143
+ Messages,`gemini/` 使用 Google 的 OpenAI-compatible 接口;`openai/` 或
144
+ 无前缀模型使用 OpenAI 传输,其中模型名包含 `gpt`(大小写不敏感)时默认
145
+ 使用 Responses,其余使用 Chat Completions。无前缀且名称包含 `claude` 时
146
+ 使用 Anthropic Messages。显式 provider 前缀与 `ARENA_OPENAI_API` 始终优先。
147
+ LiteLLM 官方提供 Python SDK 或独立 Proxy,因此
108
148
  纯 TypeScript 版本使用
109
149
  [LiteLLM Proxy](https://docs.litellm.ai/) 作为多 provider 网关:
110
150
 
@@ -140,10 +180,19 @@ export OPENAI_API_KEY=...
140
180
  ```
141
181
 
142
182
  `OPENAI_BASE_URL` / `OPENAI_API_KEY` 是统一的模型连接入口,变量名不决定
143
- wire protocol。`OPENAI_MODEL=anthropic/...` 仍会向 `<base>/messages` 发送
144
- Anthropic Messages payload 并使用 `x-api-key`;`openai/...` 则会向
145
- `<base>/chat/completions` 发送 OpenAI payload 并使用 Bearer token。未设置
146
- 统一入口时,Anthropic 可回退到 `ANTHROPIC_BASE_URL` /
183
+ wire protocol。`OPENAI_MODEL=anthropic/...` 会向 `<base>/messages` 发送
184
+ Anthropic Messages payload 并使用 `x-api-key`;OpenAI 路由根据上述规则向
185
+ `<base>/chat/completions` 或 `<base>/responses` 发送 Bearer 请求。Responses
186
+ 路径固定使用 `store=false`,不会发送 `previous_response_id` 或 conversation。
187
+ 每次响应完成时,Harness 会把 `reasoning`、`function_call` 和 assistant message
188
+ 等原生 output items 原样保存在 assistant 历史中;其中 reasoning 的
189
+ `encrypted_content` 是 OpenAI 返回的不可读加密载荷。后续工具 step 和跨 CLI
190
+ 回合会把这些 output items 连同 function output 一起完整回放到下一次 `input`,
191
+ 从而在不依赖 provider 端存储的情况下保留隐藏 reasoning。请求显式包含
192
+ `reasoning.encrypted_content`;GPT-5.6 还会使用 `reasoning.context=all_turns`。
193
+ 设置 `ARENA_OPENAI_API=chat` 或 `responses` 可覆盖 GPT 的自动选择。
194
+ 该开关不作用于 LiteLLM、Gemini 或 Anthropic 路由。如未设置统一入口,
195
+ Anthropic 可回退到 `ANTHROPIC_BASE_URL` /
147
196
  `ANTHROPIC_API_KEY`。
148
197
 
149
198
  Anthropic 直连默认只用 `x-api-key` 鉴权(同时携带 Authorization 会让部分
@@ -160,11 +209,11 @@ Anthropic 直连在原生协议上完整支持 thinking。旧版 Claude 和非 C
160
209
  budget。Anthropic 要求启用 thinking 时不调整 temperature,此时不发送该
161
210
  参数。带签名的 thinking/redacted_thinking block 会原样保存在对应 assistant
162
211
  消息的 `reasoning_details` 字段——
163
- Anthropic 要求 tool-use 续跑时回传签名 thinking block,重放历史时会按
212
+ Anthropic 要求 tool-use 续跑时回传签名 thinking block,恢复历史上下文时会按
164
213
  原顺序还原;无签名的 thinking block(部分网关返回 `signature: ""`)
165
- 不参与重放,因为协议不需要它们,而且某些网关收到会挂起。
214
+ 不会再次发送,因为协议不需要它们,而且某些网关收到会挂起。
166
215
  `reasoning_content` 仍是纯文本推理,历史契约不变。向
167
- OpenAI-compatible 服务重放同一份历史时,`reasoning_details` 与 `usage`
216
+ OpenAI-compatible 服务发送同一份历史时,`reasoning_details` 与 `usage`
168
217
  一样在 wire 边界剥离。usage 会映射为 OpenAI 风格字段(`prompt_tokens`
169
218
  含 cache 创建/读取部分),同时保留 `input_tokens`、
170
219
  `cache_read_input_tokens` 等原生字段。`OPENAI_BASE_URL` 对 anthropic
@@ -191,8 +240,9 @@ ARENA_COMPACTION_SUMMARY_MAX_TOKENS=8192
191
240
  ```
192
241
 
193
242
  可选的 `ARENA_REASONING_EFFORT` 支持 `low`、`medium`、`high`、`xhigh`
194
- 和 `max` 五档;OpenAI-compatible 请求使用 `reasoning_effort`,原生
195
- Anthropic Messages 请求映射为 `output_config.effort`。不会按模型静默
243
+ 和 `max` 五档;Chat Completions 请求使用 `reasoning_effort`,Responses
244
+ 请求使用 `reasoning.effort`,原生 Anthropic Messages 请求映射为
245
+ `output_config.effort`。不会按模型静默
196
246
  降档;目标模型或兼容网关不支持所选档位时,由服务端返回明确错误。
197
247
  `ARENA_DISALLOWED_TOOLS` 是 JSON 字符串数组,工具名精确匹配:
198
248
 
@@ -207,17 +257,17 @@ ARENA_DISALLOWED_TOOLS=["bash","write_file"]
207
257
  参数覆盖,也会原样写入 `history.json` 的
208
258
  `metadata.parameters.extra_body`。
209
259
 
210
- `ROUTIFY_API_KEY` 供 `web_search` 和 `image_search` 使用;开启
260
+ `ROUTIFY_API_KEY` 供 `web_search`、`image_search`、`generate_image` 和
261
+ `generate_speech` 使用;开启
211
262
  `CODE_HARNESS_ROUTIFY_WEB_FETCH_ENABLED` 后,Arena 的 `fetch_page` 和 Code Arena 的
212
263
  `web_fetch` 也复用该密钥,通过 Routify 先调用 Firecrawl,失败时回退到
213
264
  Jina Reader。
214
- `DASHSCOPE_API_KEY` 供图片生成和语音工具使用。这些工具密钥都不会
215
- 作为主模型连接的密钥。
265
+ 该密钥不会作为主模型连接的密钥。
216
266
 
217
267
  Harness 会在每次模型调用前估算消息、reasoning、工具 schema 和图片占用;达到
218
268
  `ARENA_CONTEXT_WINDOW × ARENA_CONTEXT_COMPACTION_RATIO`(默认 80%)时,调用
219
269
  当前模型把已完成的旧回合生成结构化续作摘要。只有发给模型的内存上下文会把
220
- 旧前缀替换为与 Arena trace 一致的 leading `<arena-system-message>`;持久化的
270
+ 旧前缀替换为符合既有会话格式的 leading `<arena-system-message>`;持久化的
221
271
  `history.json.messages` 保留原始消息,不会因压缩删除或替换前缀。当前用户回合及
222
272
  其后的 tool-call / result 链在模型上下文中保持原样。摘要重点保留目标、硬约束、
223
273
  发现、已完成/未完成事项、下一步和相关文件路径;摘要调用默认关闭 thinking,
@@ -225,14 +275,20 @@ Harness 会在每次模型调用前估算消息、reasoning、工具 schema 和
225
275
  `ARENA_COMPACTION_SUMMARY_MAX_TOKENS` 控制(默认 8192)。
226
276
 
227
277
  `ARENA_CONTEXT_WINDOW` 应设置为所用模型的真实上下文窗口;默认 `1000000` 是
228
- 当前 trace 模型的暂定值。token 数目前是本地保守估算,并非 provider tokenizer
278
+ 当前默认模型的暂定值。token 数目前是本地保守估算,并非 provider tokenizer
229
279
  的精确计数。如果摘要请求失败,Harness 会保留原历史并继续正常模型请求,避免
230
280
  摘要服务的瞬时故障直接中断任务。
231
281
 
282
+ Responses 与其他协议一样,会在每个工具 step 前检查是否需要压缩。由于所有
283
+ reasoning/function-call 状态都由本地 history 携带,压缩不再需要切断或重建 provider
284
+ 端 response 链。压缩会把已完成的旧回合替换成续作摘要,同时保留当前 user turn 里的
285
+ 加密 reasoning、function call 和 function output;下一次 Responses 请求直接回放这份
286
+ 压缩后的本地上下文。持久化的原始 `history.json` 仍保持 append-only。
287
+
232
288
  每次模型请求都会通过 OpenAI-compatible 的 `stream_options.include_usage`
233
289
  请求 token 用量,并把服务端在最终 chunk 上报的 `usage` 对象原样记录到该轮
234
290
  assistant 消息的 `usage` 字段(`history.json` 与事件流中均可见),供训练
235
- 和成本统计使用。向模型重放历史时,`usage` 会在 wire 边界剥离,不会发回
291
+ 和成本统计使用。再次发送历史上下文时,`usage` 会在 wire 边界剥离,不会发回
236
292
  服务端。`model.complete` 事件同样携带该轮 `usage`;上下文压缩的摘要调用
237
293
  不会在历史中留下消息,其用量通过 `compaction` 事件上报,只在事件流中可
238
294
  见。不支持 `stream_options` 的服务可通过 `ARENA_EXTRA_BODY` 覆盖该参数。
@@ -250,6 +306,14 @@ assistant 消息的 `usage` 字段(`history.json` 与事件流中均可见)
250
306
  `model.retry` 事件(TUI 会显示「reconnecting after a network error」提示)。
251
307
  用户主动取消(abort)不会被当作可重试的网络故障。
252
308
 
309
+ OpenAI Responses 还会检查 HTTP 200 SSE 中的 `error` / `response.failed`
310
+ 事件。带 408/409/429/5xx 或明确瞬时错误类型的事件会整轮重试;上游只给文本时,
311
+ 仅明确的 `experiencing high demand`、`maximum usage size allowed during peak
312
+ load`、`Provisioned Throughput` 容量错误会重试。容量错误最多重试 5 次
313
+ (`ARENA_MODEL_MAX_RETRIES` 可进一步降低或关闭),每次均重新发送完全相同的
314
+ 模型输入,并沿用 full-jitter 指数退避。上下文超限、参数、鉴权、媒体和安全错误
315
+ 等永久错误不会因正文中出现 `error` 一词而重试。
316
+
253
317
  Agent turn 默认没有模型 step 数上限;设置 `ARENA_MAX_TURNS` 后,达到指定上限
254
318
  会返回 error,已经完成的 assistant/tool step 仍会保留。整轮没有内建墙钟时长
255
319
  上限,会持续到模型正常结束、请求用户输入、发生不可恢复错误、达到显式配置的
@@ -319,12 +383,17 @@ Arena 还维护一份有顺序的模型输入模态表,在第一次 HTTP 调
319
383
  转换为文字;主模型支持图片、模型未知或 Code Arena 模式均不触发此 fallback。
320
384
  fallback 不继承 `ARENA_EXTRA_BODY` 或主模型生成参数;原生 Anthropic Messages
321
385
  因协议要求会单独使用 `max_tokens: 16384`。
386
+ 同一条件下,用户直接附加的图片不会发送给主模型;已有附件文件列表会保留,并提示
387
+ 模型调用 `read_file` 获取 fallback 生成的文字版本。
322
388
 
323
389
  输出事件及 JSON 结果中的 `session_id` 就是路径里的 `<sessionId>`。
324
390
  `history.json` 的 `messages` 是只追加的原始轨迹;上下文压缩只影响发送给模型的
325
391
  临时副本。`metadata` 记录 session、模型以及本次实际生效的 `max_turns`、
326
- `context_window`、生成和压缩参数。daemon 完成一次 turn 后还会写入
327
- `version: 3` 和 `metrics`,后者是截至该 turn 的 praise vs. complaint、
392
+ `context_window`、生成和压缩参数。`tools` 记录最近一次保存时实际提供给模型的
393
+ 完整工具定义,包括针对 workspace 的描述改写和不可用工具(未接 `ask_user`
394
+ handler、`ARENA_DISALLOWED_TOOLS`)的过滤结果。daemon 完成一次 turn 后还会写入
395
+ `version: 4`;开启动态指标统计(`ARENA_METRICS_SIDE_QUERY_ENABLED`,默认关闭)
396
+ 时还会写入 `metrics`,后者是截至该 turn 的 praise vs. complaint、
328
397
  steerability、Bash recovery 与 tool hallucination 增量快照。即使结果为
329
398
  `needs_input` 或 `error` 也会保存;旧的 `{"messages":[...]}` 历史仍可直接加载。
330
399
  需要跨命令续写同一份显式历史时,可另外传入
@@ -358,17 +427,17 @@ arena -p 'redraw this photo with the reference face' \
358
427
  每个附件会被保存到工作区 `uploads/`(模型内可通过
359
428
  `/home/user/uploads/<name>` 访问;重名自动加 `-2`、`-3` 后缀,URL 无扩展名
360
429
  时按图片字节特征补全扩展名)。本轮 user message 与 daemon 附件上传共用同
361
- 一构造逻辑,并使用与 Arena trace 相同的原生部件格式:图片部件
430
+ 一构造逻辑,并使用统一的会话部件格式:图片部件
362
431
  `{"image": ...}` 在前,随后是 prompt 文本 `{"text": ...}`,最后追加一条
363
432
  `<arena-system-message>`,列出保存到 `/home/user/uploads/` 的文件名。单条
364
433
  消息最多 10 个附件,单个附件上限 20 MB、总量上限 50 MB,与 daemon 一致。
365
434
 
366
435
  多模态 user 消息(附件、图片 `read_file` 回灌、中断通知)在历史与事件流中
367
- 一律使用 Arena 原生部件 `{"image": ...}` / `{"text": ...}`,与 baseline
368
- trace 逐字段同构;仅在向模型发起 chat-completions 请求的边界处转换为
436
+ 一律使用 `{"image": ...}` / `{"text": ...}` 原生部件并保持字段格式一致;仅在
437
+ 向模型发起 chat-completions 请求的边界处转换为
369
438
  OpenAI 风格的 `image_url` 部件。`--attach` 传 URL 时 `image` 直接记录该
370
- URL(与 baseline 引用附件的方式一致,模型服务端会自行下载;重放历史要求
371
- 该 URL 仍可访问);本地文件没有可引用的 URL,`image` 存自包含的 base64
439
+ URL(模型服务端会自行下载;恢复历史上下文时要求该 URL 仍可访问);本地文件
440
+ 没有可引用的 URL,`image` 存自包含的 base64
372
441
  data-URI。
373
442
 
374
443
  `stream-json` 每行一个 JSON 对象。`start`、`message`、`result` 事件中的
@@ -382,7 +451,12 @@ data-URI。
382
451
 
383
452
  `assistant.delta` 实时提供 content、reasoning 与 tool-call 参数碎片;
384
453
  `model.start`、`model.complete`、`model.retry`、`tool.start`、`tool.complete`
385
- 标记阶段。最后的 `result` 带完整可重放消息。
454
+ 标记阶段。最后的 `result` 带完整消息。
455
+
456
+ Headless 会向模型提供 `present_file`,以保持生成轨迹与 Arena 工具契约一致。
457
+ 该工具在 headless 中只校验目标是 workspace 内已有文件,并返回
458
+ `{"path":"<原始路径>","status":"success"}`;不会连接前端或打开 viewer。
459
+ 自动打开 viewer 仅由 daemon/WebUI 消费同一工具结果后触发。
386
460
 
387
461
  `ask_user` 只有在注入 handler 时才会提供给模型。TUI 和 WebUI 使用真人交互
388
462
  handler;headless(`-p`)默认仍不开放该工具,但可配置远程 User Agent handler:
@@ -426,8 +500,9 @@ arena tui --workspace . --model mock/ask-user --session-id my-session
426
500
  workspace 默认是启动命令时的当前目录(可用 `--workspace` 覆盖)。TUI 会把
427
501
  实际 workspace root 和 cwd 动态注入 system prompt 与工具 schema;因此本机
428
502
  运行时使用本机路径,部署进沙箱后会自然使用沙箱内的实际路径,不需要切换模式。
429
- TUI 没有内置文件 viewer,因此不会向模型提供 `present_file`;模型会直接报告
430
- 生成文件的实际路径。
503
+ TUI 会向模型提供 `present_file`,以保持生成轨迹与 Arena 工具契约一致。由于
504
+ TUI 没有内置文件 viewer,该工具只校验 workspace 内的目标文件并返回 canonical
505
+ 成功结果,不会打开文件;模型仍会在正文中报告生成文件的实际路径。
431
506
 
432
507
  assistant 的 reasoning、正文和工具调用状态会边生成边输出。每轮结束都会把
433
508
  完整消息原子写入与 headless、daemon 相同的 `history.json`;再次传入相同的
@@ -673,7 +748,7 @@ curl -N -X POST \
673
748
 
674
749
  ## 工具
675
750
 
676
- 原始规范在源码仓库中逐字保存在:
751
+ 消息与工具契约配置保存在:
677
752
 
678
753
  - `spec/message.json`
679
754
  - `spec/tools.json`
@@ -682,40 +757,56 @@ curl -N -X POST \
682
757
  `spec/` 目录,运行时也不依赖仓库文件。执行工具前仍使用 AJV 校验原始
683
758
  Draft-07 schema。repo mode 的动态提示词模板保存在 `src/prompts.ts`:
684
759
 
685
- `bash`、`ask_user`、`edit_file`、`read_file`、`fetch_page`、`web_search`、
686
- `write_file`、`image_search`、`present_file`、`generate_image`、
687
- `generate_speech`。
760
+ `bash`、`ask_user`、`add_voice`、`edit_file`、`read_file`、`fetch_page`、
761
+ `web_search`、`write_file`、`image_search`、`present_file`、`stop_process`、
762
+ `start_process`、`generate_image`、`generate_speech`、`get_process_output`。
763
+
764
+ 新版生产导出的前 50 条脱敏请求单独保存在
765
+ [`tasks/v2`](tasks/v2/README.md),没有与原有 replay 数据混放。该目录记录了
766
+ 2 GB 源文件的 SHA-256、全量解析统计、tool schema 变体和可重复执行的拆分命令;
767
+ `npm run refresh:arena-v2-contract` 用它刷新 canonical prompt/tool fixtures 并重新
768
+ 生成 `src/canonical-data.ts`。
769
+
770
+ `bash` 只用于一次性命令:超时结果为 `status=timeout`,被信号终止为
771
+ `status=killed`,并会清理命令遗留的后台进程。需要跨 tool call 存活的 server、
772
+ watcher 或任务应使用 `start_process`;随后可用 `get_process_output` 按日志、端口
773
+ 或退出状态阻塞等待,并用 `stop_process` 停止整个进程组。完整合并日志保存在
774
+ 工作区的 `.arena/processes/`,工具返回对应的 `log_tail`;`stop_process` 对齐生产
775
+ 只返回最后 100 行。assistant tool call 和这些 JSON tool result 会原样写入
776
+ `history.json`,供后续 turn replay。`start_process` 在 E2B 环境中还会用
777
+ `E2B_SANDBOX_ID` 组成 `{port}-{sandboxId}.e2b.app`,分别探测本地 Host 和用户
778
+ 预览 Host;如果新开的公网端口仅拒绝后者,结果会按需增加 `warnings`。其他预览
779
+ 代理可用 `ARENA_PREVIEW_HOST_TEMPLATE` 配置 Host 模板。没有告警时不返回该字段,
780
+ 保持普通捕获的结果 shape 不变。
688
781
 
689
782
  `web_search` 使用 Routify 网关提供的 Brave LLM Context,从工作区 `.env`
690
- 读取 `ROUTIFY_API_KEY`,默认调用
691
- `https://routify-pub.alibaba-inc.com/protocol/compatible-mode/res/v1/llm/context`
692
- 并使用 `brave.llm-context` 路由。实现按 depth 请求最多 3/5/8 条结果,并将
783
+ 读取 `ROUTIFY_API_KEY`,默认通过内置的 ModelRouter 兼容接口调用
784
+ `brave.llm-context` 路由。实现按 depth 请求最多 3/5/8 条结果,并将
693
785
  `grounding.generic` 的内容片段及 `sources` 的来源信息转换成 Arena 的既有工具
694
786
  结果。请求固定考虑最多 20 个搜索候选,再通过 `maximum_number_of_urls` 将
695
787
  最终来源数限制为 3/5/8。depth 还分别对应 1024/3072/8192 的总 token 预算和
696
788
  512/552/2048 的单 URL token 预算;description 使用预算内的全部 grounding
697
789
  snippets,仅在没有 grounding 内容时回退到来源摘要。三个 Code Arena 模式
698
- 使用独立的复刻参数:来源数 3/5/10、总 token 预算 1280/4096/6144、单 URL
790
+ 使用独立的模式参数:来源数 3/5/10、总 token 预算 1280/4096/6144、单 URL
699
791
  token 预算 512/1280/1536。
700
792
  可用 `ROUTIFY_WEB_SEARCH_URL` 和 `ARENA_WEB_SEARCH_MODEL` 覆盖默认值。
701
793
 
702
794
  `image_search` 使用 Routify 网关提供的 Brave Images Search,同样读取
703
- `ROUTIFY_API_KEY`,通过 `X-Subscription-Token` 发送 GET 请求。默认接口为
704
- `https://routify-pub.alibaba-inc.com/protocol/brave/res/v1/images/search`,可用
705
- `ROUTIFY_IMAGE_SEARCH_URL` 覆盖。实现优先下载 Brave 返回的原图地址,原图不可用
795
+ `ROUTIFY_API_KEY`,通过 `X-Subscription-Token` 发送 GET 请求。默认使用内置的
796
+ ModelRouter 兼容接口,可用 `ROUTIFY_IMAGE_SEARCH_URL` 覆盖。实现优先下载 Brave
797
+ 返回的原图地址,原图不可用
706
798
  时回退到 Brave 代理缩略图,并保持既有工具结果格式和工作区文件命名不变。
707
799
 
708
800
  Arena 的 `fetch_page` 和 Code Arena 的 `web_fetch` 默认仍使用本地抓取与正文
709
- 解析。设置 `CODE_HARNESS_ROUTIFY_WEB_FETCH_ENABLED=true` 后,两者改走 Routify 的
710
- `POST /protocol/search/tools`:先使用 `readpage-firecrawl-inner`,调用失败、HTTP
711
- 非 200 或响应不可用时回退到 `readpage-jina-inner`;两者都失败则返回普通工具
801
+ 解析。设置 `CODE_HARNESS_ROUTIFY_WEB_FETCH_ENABLED=true` 后,两者改走 Routify
802
+ 页面读取接口,并在首选读取服务不可用时使用备用服务;两者都失败则返回普通工具
712
803
  错误。`fetch_page` 仍按 8000 字符分块并缓存同一 URL 的后续 chunk。认证复用
713
804
  `ROUTIFY_API_KEY`。
714
805
 
715
- `generate_image`、`generate_speech` 继续使用阿里云百炼 DashScope,并从工作区
716
- `.env` 读取 `DASHSCOPE_API_KEY`。默认调用公共北京地域
717
- 地址;若使用业务空间专属域名或新加坡地域,可设置 `DASHSCOPE_API_BASE`
718
- (可带或不带 `/api/v1`)。
806
+ `generate_image`、`generate_speech` 使用 ModelRouter/Routify,并从工作区
807
+ `.env` 读取同一个 `ROUTIFY_API_KEY`。图片使用 Google 兼容协议调用;可用
808
+ `ROUTIFY_VERTEX_BASE_URL` 覆盖默认根地址。语音使用 ModelRouter 的火山引擎
809
+ `/protocol/volcengine/api/v1/tts` 路由,不需要新增服务密钥。
719
810
 
720
811
  `fetch_page` 使用 Defuddle 将 HTML 正文转换为 Markdown,并使用
721
812
  `pdf-parse` 提取 PDF 的前 30 页。响应按 Markdown 边界稳定分块,同一轮内
@@ -730,10 +821,19 @@ Arena 的 `fetch_page` 和 Code Arena 的 `web_fetch` 默认仍使用本地抓
730
821
  内容继续发送给模型。所有工具结果另有 1,000,000 字符的最终兜底。当前不做
731
822
  OCR,扫描版 PDF 只会返回无可提取文字的提示。
732
823
 
733
- 可用 `ARENA_IMAGE_MODEL`、`ARENA_SPEECH_MODEL` 覆盖 DashScope 默认模型。图片生成结果和
824
+ 图片和语音的默认 ModelRouter 路由分别为 `gemini-2.5-flash-image` 和
825
+ `doubao-tts`,可用 `ARENA_IMAGE_MODEL`、`ARENA_SPEECH_MODEL` 覆盖。图片生成结果和
734
826
  搜索到的图片会立即下载到工作区;每次图片搜索的结果按生产命名保存为
735
827
  `image-search/<query slug>-<序号>.<扩展名>`,并作为可预览文件显示在 Web UI。
736
- 语音生成目前按 DashScope HTTP API 支持 `.mp3`、`.wav` 和 `.opus` 输出。
828
+ 语音生成先由 `add_voice` 生成两个试听候选并让用户选择,选择结果注册为稳定的
829
+ `voice_id`;后续 `generate_speech` 必须传入该 id。
830
+ 无交互 handler 的 headless 模式仍会开放 `add_voice`,但不会生成无法播放的试听;
831
+ 它会按语言和 `voice_identity` 确定性注册一个默认音色,并在结果中以
832
+ `virtual: true` 标记。返回的 `voice_id` 可直接用于后续 `generate_speech`。
833
+ 也可用
834
+ `ARENA_SPEECH_VOICE` 强制覆盖底层音色。豆包响应中的 Base64 数据已经是 MP3、RIFF/WAV
835
+ 或 Ogg Opus;`.ogg` 与 `.opus` 都保存 Ogg Opus 数据,所有支持的输出均不依赖
836
+ 本地 `ffmpeg` 或 `lame`。
737
837
 
738
838
  所有路径工具都限制在 `--workspace` 内并检查符号链接逃逸。`bash` 是本机
739
839
  子进程,不是操作系统安全沙箱;多租户或不可信模型部署应使用容器或 VM。
@@ -749,4 +849,7 @@ npm test
749
849
  ```
750
850
 
751
851
  测试覆盖消息契约、工具校验、headless、`/query`、实时双通道、
752
- `ask_user`、`present_file` 和 daemon 优雅退出。
852
+ `ask_user`、`add_voice`、后台进程工具、`present_file` 和 daemon 优雅退出。
853
+ 配置 `.env` 中的 `ROUTIFY_API_KEY` 后,可运行
854
+ `npm run verify:live-speech-voices` 对 headless `add_voice` 到真实
855
+ `generate_speech` 的多音色链路做烟测。
package/dist/agent.d.ts CHANGED
@@ -2,16 +2,17 @@ import { type ArenaConfig } from "./config.js";
2
2
  import { EventSink } from "./protocol.js";
3
3
  import { type HarnessMode } from "./mode.js";
4
4
  import { ToolExecutor } from "./tools.js";
5
- import type { AgentResult, AskUserHandler, ClientContext, Message, ModelClient, RepoContext, UserTurnMessage, WorkspaceContext } from "./types.js";
5
+ import type { AgentResult, AskUserHandler, ClientContext, Message, ModelClient, RepoContext, ToolDefinition, UserTurnMessage, WorkspaceContext } from "./types.js";
6
6
  interface AgentOptions {
7
7
  client?: ModelClient;
8
8
  tools?: ToolExecutor;
9
9
  askUserHandler?: AskUserHandler;
10
10
  sessionId?: string;
11
+ sessionAffinityId?: string;
11
12
  signal?: AbortSignal;
12
13
  clientContext?: ClientContext;
13
14
  workspaceContext?: WorkspaceContext;
14
- onCheckpoint?: (messages: readonly Message[]) => Promise<void>;
15
+ onCheckpoint?: (messages: readonly Message[], tools: readonly ToolDefinition[]) => Promise<void>;
15
16
  onModelFailure?: (messages: readonly Message[], error: unknown) => void;
16
17
  }
17
18
  interface AgentRunOptions {
@@ -27,6 +28,7 @@ export declare class Agent {
27
28
  private readonly client;
28
29
  private readonly tools;
29
30
  private readonly workspaceContext?;
31
+ private readonly sessionAffinityId;
30
32
  readonly sessionId: string;
31
33
  constructor(config: ArenaConfig, sink: EventSink, options?: AgentOptions);
32
34
  run(prompt: string, history?: Message[], runOptions?: AgentRunOptions): Promise<AgentResult>;