@qwen-code/arena-agent 0.0.10-dev.10 → 0.0.10-dev.12

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 (56) hide show
  1. package/.env.example +12 -1
  2. package/CHANGELOG.md +37 -1
  3. package/README.md +106 -28
  4. package/dist/agent.d.ts +1 -1
  5. package/dist/agent.js +90 -30
  6. package/dist/agent.js.map +1 -1
  7. package/dist/anthropic.js +17 -15
  8. package/dist/anthropic.js.map +1 -1
  9. package/dist/canonical-data.d.ts +2 -0
  10. package/dist/canonical-data.js +2 -0
  11. package/dist/canonical-data.js.map +1 -1
  12. package/dist/code-tools.js +34 -9
  13. package/dist/code-tools.js.map +1 -1
  14. package/dist/compaction.d.ts +3 -2
  15. package/dist/compaction.js +87 -21
  16. package/dist/compaction.js.map +1 -1
  17. package/dist/config.d.ts +6 -0
  18. package/dist/config.js +5 -0
  19. package/dist/config.js.map +1 -1
  20. package/dist/connector-mcp.d.ts +47 -0
  21. package/dist/connector-mcp.js +314 -0
  22. package/dist/connector-mcp.js.map +1 -0
  23. package/dist/connectors.d.ts +8 -0
  24. package/dist/connectors.js +57 -0
  25. package/dist/connectors.js.map +1 -0
  26. package/dist/midturn-checkpoint-hook.d.ts +14 -1
  27. package/dist/midturn-checkpoint-hook.js.map +1 -1
  28. package/dist/model-router-media.js +50 -0
  29. package/dist/model-router-media.js.map +1 -1
  30. package/dist/prompts.d.ts +0 -1
  31. package/dist/prompts.js +10 -36
  32. package/dist/prompts.js.map +1 -1
  33. package/dist/provider.js +3 -0
  34. package/dist/provider.js.map +1 -1
  35. package/dist/server.js +4 -2
  36. package/dist/server.js.map +1 -1
  37. package/dist/session-memory-prompts.js +1 -1
  38. package/dist/session-memory-prompts.js.map +1 -1
  39. package/dist/spec.d.ts +5 -4
  40. package/dist/spec.js +15 -9
  41. package/dist/spec.js.map +1 -1
  42. package/dist/{legacy-compaction.d.ts → summary-compaction.d.ts} +7 -0
  43. package/dist/{legacy-compaction.js → summary-compaction.js} +54 -10
  44. package/dist/summary-compaction.js.map +1 -0
  45. package/dist/tools.d.ts +3 -1
  46. package/dist/tools.js +35 -9
  47. package/dist/tools.js.map +1 -1
  48. package/dist/types.d.ts +4 -0
  49. package/dist/workspace.js +61 -14
  50. package/dist/workspace.js.map +1 -1
  51. package/docs/MODES_AND_RELEASES.md +55 -1
  52. package/modes/code-arena/full-stack/dependencies.lock.json +8670 -0
  53. package/modes/code-arena/react/dependencies.lock.json +2707 -0
  54. package/package.json +2 -1
  55. package/scripts/README.md +23 -4
  56. package/dist/legacy-compaction.js.map +0 -1
package/.env.example CHANGED
@@ -64,6 +64,16 @@ ARENA_HARNESS_MODE=arena
64
64
  # ARENA_TOOL_CHOICE=auto
65
65
  # JSON array of exact tool names hidden from the model and rejected if called.
66
66
  # ARENA_DISALLOWED_TOOLS=["bash","write_file"]
67
+ # Agent Arena connectors through Merge MCP (unset, blank or [] disables it).
68
+ # Enable the captured catalog in the Tool Pack; connect apps for the Registered
69
+ # User through Merge Link. The key and user must belong to the same environment.
70
+ # One configured Registered User is shared by all sessions of this process.
71
+ # ARENA_CONNECTORS=["notion","google_calendar"]
72
+ # ARENA_MERGE_ACCESS_KEY=
73
+ # ARENA_MERGE_TOOL_PACK_ID=
74
+ # ARENA_MERGE_REGISTERED_USER_ID=
75
+ # ARENA_MERGE_API_BASE=https://ah-api.merge.dev
76
+ # ARENA_CONNECTOR_TIMEOUT_MS=60000
67
77
  # ARENA_THINKING_BUDGET=81920
68
78
  # ARENA_ENABLE_THINKING=true
69
79
  # ARENA_PRESERVE_THINKING=true
@@ -150,6 +160,7 @@ ROUTIFY_API_KEY=
150
160
  # ROUTIFY_WEB_SEARCH_URL=https://routify-pub.alibaba-inc.com/protocol/compatible-mode/res/v1/llm/context
151
161
  # ROUTIFY_IMAGE_SEARCH_URL=https://routify-pub.alibaba-inc.com/protocol/brave/res/v1/images/search
152
162
  # ROUTIFY_VERTEX_BASE_URL=https://routify-pub.alibaba-inc.com/protocol/vertex/v1beta
163
+ # ROUTIFY_OPENAI_BASE_URL=https://routify-pub.alibaba-inc.com/protocol/openai/v1
153
164
  # ARENA_WEB_SEARCH_MODEL=brave.llm-context
154
165
  # CODE_HARNESS_WEB_SEARCH_MODEL=brave.llm-context
155
166
  # Optional fetch_page/web_fetch path: Firecrawl first, then Jina Reader.
@@ -159,7 +170,7 @@ ROUTIFY_API_KEY=
159
170
  # CODE_HARNESS_WEB_FETCH_MAX_BYTES=40000
160
171
  # ARENA_IMAGE_MODEL=gemini-3.1-flash-image
161
172
  # ARENA_IMAGE_SIZE=1024*1024
162
- # CODE_HARNESS_IMAGE_MODEL=gemini-3.1-flash-image
173
+ # CODE_HARNESS_IMAGE_MODEL=gpt-image-2.5-sunburst
163
174
  # CODE_HARNESS_IMAGE_SIZE=1024*1024
164
175
  # ARENA_SPEECH_MODEL=gemini-3.1-flash-tts-preview
165
176
  # ARENA_SPEECH_VOICE=Kore
package/CHANGELOG.md CHANGED
@@ -12,6 +12,40 @@
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.0.10-dev.12] - 2026-10-15
16
+
17
+ ### Agent Arena
18
+
19
+ - 摘要压缩接入服务端 token 用量,并支持长单轮工具任务。
20
+
21
+ ### Code Arena
22
+
23
+ - 摘要压缩接入服务端 token 用量,支持长单轮工具任务,并在续跑时恢复已应用的摘要。
24
+ - 固定 React 和 Full Stack 模板的完整安装依赖,避免上游新版本发布导致初始化失败,并保留已有项目的依赖锁。
25
+ - React 与 Full Stack 的命令执行支持毫秒级 `timeout`,默认 180 秒,并拒绝空命令;React 可执行 Codex 的 `shell_command` 调用。
26
+
27
+ ### 通用
28
+
29
+ - 压缩计数改善 SVG 等大工具输出的估算,避免重复计入已压缩内容。
30
+ - 模型因长度限制截断时保留部分输出并报告失败,避免将未完成的任务标为成功。
31
+ - 修复 Responses 多工具调用的事件错配,确保工具结果、文件展示和用户输入请求对应正确调用。
32
+
33
+ ## [0.0.10-dev.11] - 2026-10-11
34
+
35
+ ### Agent Arena
36
+
37
+ - 新增实验性 Notion、Google Calendar 接入,使用 `ARENA_CONNECTORS` 选择应用,并通过 `ARENA_MERGE_*` 配置已授权的 Merge 账号。
38
+ - Headless Hook 新增 `before_compaction` 事件,可按压缩次数在摘要式或 rolling session memory 压缩前快照暂停,续跑时延续计数。
39
+
40
+ ### Code Arena
41
+
42
+ - `generate_image` 默认模型改为 `gpt-image-2.5-sunburst`,继续通过 ModelRouter 调用。
43
+
44
+ ### 通用
45
+
46
+ - 修复 Claude Haiku 5.5 开启 thinking 时因旧参数格式报 400 的问题。
47
+ - 修复 Gemini 通过 Chat Completions 调用工具后因签名丢失而无法继续的问题。
48
+
15
49
  ## [0.0.10-dev.10] - 2026-10-07
16
50
 
17
51
  ### Agent Arena
@@ -214,7 +248,9 @@
214
248
  - `bash` 用于一次性命令,区分超时与信号终止,并清理遗留后台进程;需要持续运行
215
249
  的任务改用进程工具。预览端口支持 E2B Host 探测及按需返回告警。
216
250
 
217
- [Unreleased]: https://gitlab.alibaba-inc.com/know-how/arena-harness/-/compare/v0.0.10-dev.10...master
251
+ [Unreleased]: https://gitlab.alibaba-inc.com/know-how/arena-harness/-/compare/v0.0.10-dev.12...master
252
+ [0.0.10-dev.12]: https://gitlab.alibaba-inc.com/know-how/arena-harness/-/compare/v0.0.10-dev.11...v0.0.10-dev.12
253
+ [0.0.10-dev.11]: https://gitlab.alibaba-inc.com/know-how/arena-harness/-/compare/v0.0.10-dev.10...v0.0.10-dev.11
218
254
  [0.0.10-dev.10]: https://gitlab.alibaba-inc.com/know-how/arena-harness/-/compare/v0.0.10-dev.9...v0.0.10-dev.10
219
255
  [0.0.10-dev.9]: https://gitlab.alibaba-inc.com/know-how/arena-harness/-/compare/v0.0.10-dev.8...v0.0.10-dev.9
220
256
  [0.0.10-dev.8]: https://gitlab.alibaba-inc.com/know-how/arena-harness/-/compare/v0.0.10-dev.7...v0.0.10-dev.8
package/README.md CHANGED
@@ -3,14 +3,17 @@
3
3
  版本变化见 [Changelog](CHANGELOG.md),发布前请按[发版流程](docs/MODES_AND_RELEASES.md#发布节奏)
4
4
  更新变更记录并运行 `npm run check:changelog`。
5
5
 
6
- 这是一个基于 `message.json` 与 `tools.json` 所定义消息和工具契约的 TypeScript
6
+ 这是一个基于各模式正式提示词与工具契约的 TypeScript
7
7
  CodeAgent harness。运行时只依赖 Node.js,不再依赖 Python:
8
8
 
9
9
  - `arena -p ...` / `arena --file prompt.txt`:headless 单轮入口,支持文本、完整 JSON 和 NDJSON 流。
10
10
  - `arena tui`:终端多轮对话,实时输出并自动保存、恢复历史。
11
11
  - `arena serve`:HTTP daemon、Web UI 与双通道 SSE 实时接口。
12
12
  - Agent loop:流式 assistant → tool call → tool result → assistant。
13
- - 15 个工具按照 `tools.json` 中的 Draft-07 schema 提供参数校验。
13
+ - Agent Arena 的 15 个工具按照 `spec/tools.json` 中的 Draft-07 schema 提供参数校验;Code Arena 使用各模式的工具契约。
14
+
15
+ 查看代码或修改配置前,可先看[源码入口与历史资料](docs/MODES_AND_RELEASES.md#源码入口与历史资料),
16
+ 其中区分了当前提示词、生成文件、测试样本和历史审计脚本。
14
17
 
15
18
  本文提及的第三方产品、模型与服务名称仅用于说明兼容性和配置方式,不表示相关
16
19
  权利人对本项目的认可或背书;相关商标归各自权利人所有。使用者应自行确认并遵守
@@ -111,7 +114,8 @@ code-harness --file prompt.txt --mode react
111
114
  ### 中途快照与单步执行
112
115
 
113
116
  Headless CLI 可通过 JavaScript Hook 决定何时继续或创建 Harbor 快照。工具校验在
114
- 整批工具执行前触发;输出重复在主模型生成正文或 reasoning 时触发。
117
+ 整批工具执行前触发;输出重复在主模型生成正文或 reasoning 时触发;Agent Arena
118
+ 的压缩事件在达到阈值且存在可压缩历史时、执行压缩前触发。
115
119
 
116
120
  | 类型 | 含义 |
117
121
  | --- | --- |
@@ -120,12 +124,13 @@ Headless CLI 可通过 JavaScript Hook 决定何时继续或创建 Harbor 快照
120
124
  | `invalid_json` | 工具参数不是合法 JSON |
121
125
  | `invalid_arguments` | 参数不符合工具 schema |
122
126
  | `repetitive_output` | 模型持续重复同一字符或片段 |
127
+ | `before_compaction` | Agent Arena 即将执行摘要式或 rolling session memory 压缩 |
123
128
 
124
129
  | Hook 返回值 | 后续行为 |
125
130
  | --- | --- |
126
131
  | `continue` 或 `false` | 正常继续 |
127
- | `snapshot_continue` 或 `true` | 快照后继续当前生成与工具执行 |
128
- | `snapshot_exit` | 停止当前生成,保存快照后退出,本批工具不执行 |
132
+ | `snapshot_continue` 或 `true` | 快照后继续当前生成、工具执行或压缩 |
133
+ | `snapshot_exit` | 保存快照后暂停退出;触发处的生成、工具批次或压缩不再继续 |
129
134
 
130
135
  例如,将以下内容保存为 `policy.mjs`:
131
136
 
@@ -145,14 +150,34 @@ export HARBOR_REMOTE_COMMAND_BASE_URL="http://<harbor-host>:<port>"
145
150
  export HARBOR_REMOTE_COMMAND_API_KEY="<api-key>"
146
151
  ```
147
152
 
153
+ 要在第 `k` 次压缩执行前停止,可将 `policy.mjs` 写成:
154
+
155
+ ```js
156
+ const k = 3; // 正整数;1 表示第一次压缩前停止
157
+ export default event =>
158
+ event.type === "before_compaction" && event.compaction.count >= k
159
+ ? "snapshot_exit"
160
+ : "continue";
161
+ ```
162
+
163
+ 两种压缩模式共用 `event.compaction.count`:历史中已完成且未撤销的压缩次数 + 1,
164
+ 因此第 `k` 次暂停时只完成了 `k - 1` 次压缩。续跑或切换 `ARENA_COMPACTION_MODE`
165
+ 会从历史恢复计数;后台记忆更新、失败的压缩和已撤销的记录不增加次数。
166
+ 事件还提供 `compaction.mode`(`summary` / `rolling`)、`source`(`outer` / `inner`)、
167
+ `estimatedTokensBefore` 和 `thresholdTokens`,以及空的 `toolCalls` 数组。
168
+ 暂停不会发起本次摘要或强制记忆更新,也不会应用已缓存的记忆;在途后台更新会收束后再快照。
169
+ 结果为 `status: "paused"`、`stop_reason: "checkpoint"`,CLI 退出码为 0。
170
+ 续跑前需提高 `k` 或调整 Hook,否则会在同一次压缩前再次暂停。
171
+
148
172
  配置 Hook 后自动检测输出重复,命中时由 Hook 决定继续、快照或退出;未配置 Hook
149
173
  时按默认流程执行。检测连续重复至少 512 个 Unicode 字符、至少 4 次的片段,
150
174
  片段最长 1024 字符,忽略纯空白和标点填充。
151
175
  正文与 reasoning 分开检测,工具参数和工具结果不参与;每次生成最多触发一次重复事件。
152
176
  事件的 `repetition.channel` 和 `repetition.sample` 提供通道与最多 256 字符的诊断片段。
153
177
 
154
- 快照保存当前回复生成前的 `history.json`。恢复快照对应环境后,使用原 session ID
155
- 续跑同一个 user turn,不追加用户消息;被中止的那一步会重新生成:
178
+ 快照保存触发点之前的 `history.json`;压缩事件保留完整的压缩前历史。
179
+ 恢复快照对应环境后,使用原 session ID 续跑同一个 user turn,不追加用户消息;
180
+ 被中止的生成或压缩会重新执行:
156
181
 
157
182
  ```bash
158
183
  arena --continue --session-id demo
@@ -291,7 +316,7 @@ claude-code-proxy),收到 401 后会自动切换鉴权方式并记住。流
291
316
  Anthropic 直连在原生协议上完整支持 thinking。旧版 Claude 和非 Claude
292
317
  兼容网关把 `ARENA_ENABLE_THINKING` / `ARENA_THINKING_BUDGET` 映射为
293
318
  `thinking.type=enabled` / `thinking.budget_tokens`;Claude Opus/Sonnet 4.6+
294
- 以及 Claude 5、Fable、Mythos 使用 `thinking.type=adaptive`,不再发送固定
319
+ 以及 Haiku 5.5+、Fable、Mythos 使用 `thinking.type=adaptive`,不再发送固定
295
320
  budget。Anthropic 要求启用 thinking 时不调整 temperature,此时不发送该
296
321
  参数。带签名的 thinking/redacted_thinking block 会原样保存在对应 assistant
297
322
  消息的 `reasoning_details` 字段——
@@ -339,6 +364,46 @@ ARENA_DISALLOWED_TOOLS=["bash","write_file"]
339
364
 
340
365
  被禁用的工具不会发送给模型;即使模型仍构造了该工具调用,执行层也会
341
366
  在产生副作用前拒绝。
367
+
368
+ Agent Arena 可通过 Merge MCP 调用 Notion 和 Google Calendar,应用范围由
369
+ `ARENA_CONNECTORS` JSON 数组控制。先在 Merge 创建 Tool Pack 和 Registered User,
370
+ 让该用户通过 Merge Link 连接目标应用,再配置同一环境的 Access Key:
371
+
372
+ ```sh
373
+ # 写入本地 .env;三个 Merge 值从自己的 Merge 环境取得。
374
+ ARENA_CONNECTORS='["notion","google_calendar"]'
375
+ ARENA_MERGE_ACCESS_KEY=...
376
+ ARENA_MERGE_TOOL_PACK_ID=...
377
+ ARENA_MERGE_REGISTERED_USER_ID=...
378
+ ```
379
+
380
+ 首次可运行 `arena -p '加载 Google Calendar 工具并读取我的时区,不要修改日历'`。
381
+ Tool Pack 需启用所选应用的完整捕获目录:Notion 22 个、Calendar 42 个,见
382
+ [目录源文件](spec/connectors/catalog.json)。缺少工具时会明确报错;上游新增的工具不会自动暴露给模型。
383
+
384
+ 未设置、空字符串或 `[]` 时,提示词和工具集合保持原样。非空时在基础工具后注册
385
+ `list_connector_tools`,并在普通/Repo 系统提示词末尾加入捕获的 `Connected apps`
386
+ 段落,列出数组中的应用。目前只接受 `notion`、`google_calendar`;重复项去重,
387
+ 保留输入顺序,未知名称或非法 JSON 会在启动时报错。Code Arena 忽略此配置。
388
+
389
+ 模型调用 `list_connector_tools` 后,harness 才连接 Merge、加载该应用的参数定义,
390
+ 并注册业务工具。每条新用户消息都需要重新发现;暂停续跑只恢复目录,不重放已经执行的操作。
391
+ 数组表示本次会话允许哪些应用,加载目录成功也不保证账号具有全部读写权限。
392
+ 缺少配置、授权失败或目录不完整时返回明确错误;业务调用按历史日志包装结果。
393
+ `ARENA_DISALLOWED_TOOLS` 禁用发现工具时也会省略应用提示词;显式 `--system-prompt`
394
+ 仍然完整覆盖系统提示词。
395
+
396
+ 默认连接 `https://ah-api.merge.dev`,可用 `ARENA_MERGE_API_BASE` 配置区域地址;
397
+ `ARENA_CONNECTOR_TIMEOUT_MS` 默认 60000。超时或取消不会自动重试写操作,但远端可能
398
+ 已经完成写入,需先查询确认。当前一个进程使用一个 Registered User,Web 会话共用该账号;
399
+ 尚未提供各 Web 用户独立登录和账号绑定界面。
400
+
401
+ 可选应用提示词的唯一维护源是 `spec/connectors/system-prompt.txt`,由构建脚本嵌入
402
+ 生成数据;关闭 connector 时继续使用 `spec/message.json` 的原始提示词。
403
+ 工具名称、描述和顺序来自日志;完整参数使用 Merge 实时 schema,发现入口仍是根据
404
+ `service` 参数实现的最小结构。已通过 40 次历史调用的本地回放比对,尚未用真实账号验证。
405
+ 配置步骤、已验证范围和剩余差异见 [Connector 接入分析](docs/research/AGENT_ARENA_CONNECTOR_BACKEND.md)。
406
+
342
407
  `ARENA_EXTRA_BODY` 会在配置加载时解析为 JSON 对象,既作为最后一层请求
343
408
  参数覆盖,也会原样写入 `history.json` 的
344
409
  `metadata.parameters.extra_body`。
@@ -357,21 +422,24 @@ Agent Arena 通过 `ARENA_COMPACTION_MODE` 选择压缩方式,默认 `rolling`
357
422
  ARENA_COMPACTION_MODE=summary arena -p "继续任务" --session-id demo
358
423
  ```
359
424
 
360
- `summary` 达到阈值时同步生成完整摘要,保留当前用户回合及其工具调用,不启动或恢复
361
- 后台记忆缓存。它复用 `src/legacy-compaction.ts` 中的原实现:在引入 rolling 的
362
- 提交 `c56456e9` 中,这份代码由旧 `src/compaction.ts` 完整保留;原 Agent Arena
363
- 与当前 Code Arena 使用相同的摘要系统提示词、请求提示词和 continuation preamble。
425
+ `summary` 达到阈值时同步生成完整摘要,优先保留当前用户回合,不启动或恢复
426
+ 后台记忆缓存。当前实现位于 `src/summary-compaction.ts`,与 Code Arena 共用
427
+ 摘要系统提示词、请求提示词和 continuation preamble。
364
428
  摘要按 `Goal`、`Instructions`、`Discoveries`、`Accomplished` 等章节生成,使用
365
429
  主模型、temperature=0、关闭 thinking,默认输出上限为 8192。
366
430
  `ARENA_COMPACTION_SUMMARY_MAX_TOKENS` 可覆盖上限;`rolling` 的默认上限仍为 131072。
367
431
  两种模式默认阈值均为 250k;只有窗口和比例两个环境变量都配置有效时才使用乘积。
368
- 摘要模式沿用旧的
369
- 本地 token 估算。旧实现只归档当前用户回合之前的内容,因此单个长用户回合没有
370
- 可归档前缀时不会强制裁切。Code Arena 保持摘要式压缩,不受此环境变量影响。
371
-
372
- Agent Arena 在两种模式下都保存已应用的 `compaction` 历史记录;resume 或切换模式
373
- 后继续使用这些记录,不丢失已压缩内容。历史 metadata 记录本次压缩方式,但下一次
374
- 运行仍以当前环境变量为准,未设置时回到 `rolling`。
432
+ 两种模式及 Code Arena 都优先使用最近一次主请求的服务端 usage,仅估算此后新增的内容。
433
+ 大段文本取字符估算与带 10% 余量的 `o200k_base` 分词估算的较大值,减少 SVG 等
434
+ 密集文本的低估;本地分词不保证与所选模型完全一致。长用户回合没有更早回合可归档,
435
+ 或当前回合本身达到阈值时,摘要模式也可归档回合内较早的步骤,保留最近完整步骤的
436
+ 工具调用、全部结果及图片;Code Arena 的摘要压缩也支持此行为。
437
+ Code Arena 始终使用摘要模式,阈值仍为窗口与比例的乘积,默认比例 70%。
438
+
439
+ Agent Arena 两种模式和 Code Arena 都保存已应用的 `compaction` 历史记录,续跑时恢复
440
+ 压缩后的上下文。原始历史保留服务端 usage,但已压缩上下文中的旧 usage 不再作为计数基线。
441
+ Agent Arena 历史 metadata 记录本次压缩方式;下一次运行仍以当前环境变量为准,
442
+ 未设置时回到 `rolling`,切换模式也会恢复已应用的压缩记录。
375
443
 
376
444
  以下后台记忆机制适用于 `rolling` 模式。
377
445
 
@@ -434,7 +502,9 @@ Agent Arena 记忆更新使用主模型、temperature=1、thinking
434
502
 
435
503
  `ARENA_CONTEXT_WINDOW` 应设置为所用模型的真实上下文窗口;默认 `1000000` 是
436
504
  当前默认模型的暂定值。Agent Arena 的 usage 基线来自 provider;最近 assistant
437
- 之后的新增上下文以及缺少 usage 时的完整上下文仍使用本地保守估算。如果摘要请求失败,Harness 会保留原历史并继续正常模型请求,避免
505
+ 之后的新增上下文以及缺少 usage 时的完整上下文仍使用本地保守估算。压缩后,保留消息
506
+ 中的旧 usage 仅用于原始历史的用量统计;下一次主请求完成后重新建立计数基线。
507
+ 如果摘要请求失败,Harness 会保留原历史并继续正常模型请求,避免
438
508
  摘要服务的瞬时故障直接中断任务。
439
509
 
440
510
  Responses 与其他协议一样,会在每个工具 step 前检查是否需要压缩。由于所有
@@ -935,7 +1005,10 @@ curl -N -X POST \
935
1005
 
936
1006
  构建时会将两份规范生成为 `dist` 内的 JS 数据模块;npm 包不包含原始
937
1007
  `spec/` 目录,运行时也不依赖仓库文件。执行工具前仍使用 AJV 校验原始
938
- Draft-07 schema。repo mode 的动态提示词模板保存在 `src/prompts.ts`:
1008
+ Draft-07 schema。普通模式的审计模板也从同一份生成数据取得;`src/prompts.ts`
1009
+ 中直接维护的正文用于 Repo 模式。
1010
+
1011
+ Agent Arena 提供以下工具:
939
1012
 
940
1013
  `bash`、`ask_user`、`add_voice`、`edit_file`、`read_file`、`fetch_page`、
941
1014
  `web_search`、`write_file`、`image_search`、`present_file`、`stop_process`、
@@ -944,8 +1017,10 @@ Draft-07 schema。repo mode 的动态提示词模板保存在 `src/prompts.ts`
944
1017
  新版生产导出的前 50 条脱敏请求单独保存在
945
1018
  [`tasks/v2`](tasks/v2/README.md),没有与原有 replay 数据混放。该目录记录了
946
1019
  2 GB 源文件的 SHA-256、全量解析统计、tool schema 变体和可重复执行的拆分命令;
947
- `npm run refresh:arena-v2-contract` 用它刷新 canonical prompt/tool fixtures 并重新
948
- 生成 `src/canonical-data.ts`。
1020
+ 每条历史样本在 `request.messages` 中保留当时的系统提示词,不另存一份提示词 TXT。
1021
+ `npm run refresh:arena-v2-contract` 是从这批旧数据重新导入契约的历史维护命令,
1022
+ 会用首条样本覆盖 `spec/message.json` 并重新生成派生数据;它不用于日常构建或
1023
+ 获取最新官方提示词。
949
1024
 
950
1025
  `bash` 只用于一次性命令:超时结果为 `status=timeout`,被信号终止为
951
1026
  `status=killed`,并会清理命令遗留的后台进程。需要跨 tool call 存活的 server、
@@ -984,9 +1059,10 @@ Arena 的 `fetch_page` 和 Code Arena 的 `web_fetch` 默认仍使用本地抓
984
1059
  `ROUTIFY_API_KEY`。
985
1060
 
986
1061
  `generate_image`、`generate_speech` 使用 ModelRouter/Routify,并从工作区
987
- `.env` 读取同一个 `ROUTIFY_API_KEY`。图片使用 Google 兼容协议调用;可用
988
- `ROUTIFY_VERTEX_BASE_URL` 覆盖默认根地址。语音默认也通过该 Google 协议
989
- 调用 Gemini TTS,不需要新增服务密钥。
1062
+ `.env` 读取同一个 `ROUTIFY_API_KEY`。GPT Image 图片生成与编辑使用 OpenAI 图片
1063
+ 协议,可用 `ROUTIFY_OPENAI_BASE_URL` 覆盖 ModelRouter 根地址。显式选择 Gemini
1064
+ 图片模型及默认 Gemini TTS 时使用 Google 协议,可用 `ROUTIFY_VERTEX_BASE_URL`
1065
+ 覆盖对应根地址,不需要新增服务密钥。
990
1066
 
991
1067
  `fetch_page` 使用 Defuddle 将 HTML 正文转换为 Markdown,并使用
992
1068
  `pdf-parse` 提取 PDF 的前 30 页。响应按 Markdown 边界稳定分块,同一轮内
@@ -1001,8 +1077,10 @@ XLSX 仍可提取文字和表格内容,旧版 DOC、PPT、XLS 会先通过 Lib
1001
1077
  内容继续发送给模型。所有工具结果另有 1,000,000 字符的最终兜底。当前不做
1002
1078
  OCR,扫描版 PDF 只会返回无可提取文字的提示。
1003
1079
 
1004
- 图片和语音的默认 ModelRouter 路由分别为 `gemini-3.1-flash-image` 和
1005
- `gemini-3.1-flash-tts-preview`,可用 `ARENA_IMAGE_MODEL`、`ARENA_SPEECH_MODEL` 覆盖。图片生成结果和
1080
+ Agent Arena 图片和语音的默认 ModelRouter 路由分别为 `gemini-3.1-flash-image` 和
1081
+ `gemini-3.1-flash-tts-preview`,可用 `ARENA_IMAGE_MODEL`、`ARENA_SPEECH_MODEL` 覆盖。
1082
+ Code Arena 图片默认使用 `gpt-image-2.5-sunburst`;配置时优先使用
1083
+ `CODE_HARNESS_IMAGE_MODEL`,其次使用显式设置的 `ARENA_IMAGE_MODEL`。图片生成结果和
1006
1084
  搜索到的图片会立即下载到工作区;每次图片搜索的结果按生产命名保存为
1007
1085
  `image-search/<query slug>-<序号>.<扩展名>`,并作为可预览文件显示在 Web UI。
1008
1086
  语音生成先由 `add_voice` 生成两个试听候选并让用户选择,选择结果注册为稳定的
package/dist/agent.d.ts CHANGED
@@ -16,7 +16,7 @@ interface AgentOptions {
16
16
  clientContext?: ClientContext;
17
17
  workspaceContext?: WorkspaceContext;
18
18
  onCheckpoint?: (messages: readonly Message[], tools: readonly ToolDefinition[], sessionMemory?: StoredSessionMemory | null) => Promise<void>;
19
- /** Inspect output repetition and complete tool-call batches before recording them. */
19
+ /** Inspect repetition, tool-call batches and pending Agent Arena compactions. */
20
20
  midturnCheckpointHook?: MidturnCheckpointHook;
21
21
  /** Materialize the current history and create the external snapshot. */
22
22
  onMidturnCheckpoint?: (messages: readonly Message[], tools: readonly ToolDefinition[], event: MidturnCheckpointHookEvent, action: Exclude<CheckpointAction, "continue">) => Promise<void>;
package/dist/agent.js CHANGED
@@ -389,6 +389,7 @@ export class Agent {
389
389
  return await this.runTurn(prompt, history, runOptions);
390
390
  }
391
391
  finally {
392
+ await this.tools.closeConnectors?.();
392
393
  await settleSessionMemory(this.memoryCache);
393
394
  await this.checkpointQueue;
394
395
  }
@@ -412,9 +413,11 @@ export class Agent {
412
413
  // carrying the old runtime configuration into the next request.
413
414
  const dialogue = withoutLeadingSystemMessage(history || []).map((message) => normalizeToolCallHistory(message, this.config.mode));
414
415
  const trajectory = completeInterruptedToolCalls(this.options.reuseHistoryMessages ? [...dialogue] : structuredClone(dialogue));
416
+ const connectors = this.config.disallowedTools?.includes("list_connector_tools")
417
+ ? undefined : this.config.connectors;
415
418
  const runtimeSystemMessage = repoContext
416
- ? repoSystemMessage(clientContext, repoContext, this.config.mode, this.workspaceContext, this.config.maxTurns)
417
- : systemMessage(clientContext, this.workspaceContext, this.config.mode, this.config.maxTurns);
419
+ ? repoSystemMessage(clientContext, repoContext, this.config.mode, this.workspaceContext, this.config.maxTurns, connectors)
420
+ : systemMessage(clientContext, this.workspaceContext, this.config.mode, this.config.maxTurns, connectors);
418
421
  applySystemPromptConfig(runtimeSystemMessage, this.config);
419
422
  if (runOptions.resumeFromHistory) {
420
423
  if (!isContinuableHistory(dialogue)) {
@@ -424,6 +427,9 @@ export class Agent {
424
427
  else {
425
428
  trajectory.push(userMessage);
426
429
  }
430
+ // A new human reply requires discovery again. Same-turn continuation only
431
+ // reloads schemas from paired discovery results; it never repeats writes.
432
+ await this.tools.beginConnectorTurn?.(runOptions.resumeFromHistory ? dialogue : undefined);
427
433
  // Compaction projects model input with a runtime system prefix; the lossless
428
434
  // trajectory and its checkpoints remain in the official dialogue format.
429
435
  const messages = modelContextFromTrajectory([runtimeSystemMessage, ...trajectory]);
@@ -466,9 +472,11 @@ export class Agent {
466
472
  this.sink.message(this.sessionId, recorded, trajectory);
467
473
  await this.checkpoint(trajectory);
468
474
  };
469
- await this.maybeCompact(messages, trajectory);
470
- this.sink.start(this.sessionId, trajectory);
471
475
  try {
476
+ const compaction = await this.maybeCompact(messages, trajectory);
477
+ this.sink.start(this.sessionId, trajectory);
478
+ if (compaction === "checkpoint")
479
+ return await this.pause(trajectory, "checkpoint");
472
480
  await this.checkpoint(trajectory);
473
481
  const maxTurns = this.config.maxTurns
474
482
  ? Math.max(1, Math.floor(this.config.maxTurns))
@@ -483,13 +491,15 @@ export class Agent {
483
491
  // Every transport is replayable from local history. Native Responses
484
492
  // stores its encrypted reasoning output items there, so compaction is
485
493
  // safe at the same tool-step boundaries as the other protocols.
486
- if (step > 1)
487
- await this.maybeCompact(messages, trajectory, "inner");
494
+ if (step > 1 && await this.maybeCompact(messages, trajectory, "inner") === "checkpoint") {
495
+ return await this.pause(trajectory, "checkpoint");
496
+ }
488
497
  const offeredTools = this.tools.availableTools();
489
498
  this.sink.modelStart(this.sessionId, step, offeredTools.map((tool) => tool.function.name));
490
499
  let assistant;
500
+ let finishReason;
491
501
  try {
492
- assistant = await this.assistantTurn(messages, offeredTools, trajectory);
502
+ ({ assistant, finishReason } = await this.assistantTurn(messages, offeredTools, trajectory));
493
503
  }
494
504
  catch (error) {
495
505
  if (error instanceof CheckpointExit) {
@@ -509,8 +519,14 @@ export class Agent {
509
519
  this.options.onModelFailure?.(trajectory, error);
510
520
  throw error;
511
521
  }
512
- const finishReason = assistant.tool_calls?.length ? "tool_calls" : "stop";
513
522
  this.sink.modelComplete(this.sessionId, step, finishReason, assistant.usage);
523
+ if (finishReason === "length") {
524
+ // A completed but truncated request is not a transport failure. Keep
525
+ // its evidence without retrying it or executing unfinished tool calls.
526
+ await recordMessage(assistant);
527
+ throw new Error("Model response was truncated (finish_reason=length). " +
528
+ "The turn did not complete; any partial output has been preserved.");
529
+ }
514
530
  if (assistant.tool_calls?.length &&
515
531
  this.options.midturnCheckpointHook) {
516
532
  const event = {
@@ -609,7 +625,7 @@ export class Agent {
609
625
  return this.sink.result(this.sessionId, status, messages, error, undefined, this.memoryCache.update);
610
626
  }
611
627
  async makeCheckpoint(messages, tools, event, action) {
612
- await settleSessionMemory(this.memoryCache);
628
+ await settleSessionMemory(this.memoryCache, event.type !== "before_compaction" || action !== "snapshot_continue");
613
629
  await this.checkpoint(messages);
614
630
  if (!this.options.onMidturnCheckpoint) {
615
631
  throw new Error("mid-turn checkpoint hook triggered without a snapshot handler");
@@ -636,6 +652,30 @@ export class Agent {
636
652
  compactionThresholdTokens: isCodeArenaMode(this.config.mode) ? undefined
637
653
  : this.options.compactionThresholdTokens ?? this.config.compactionThresholdTokens,
638
654
  legacy: isCodeArenaMode(this.config.mode) || this.config.compactionMode === "summary",
655
+ onBeforeCompaction: !isCodeArenaMode(this.config.mode) && this.options.midturnCheckpointHook
656
+ ? async (estimatedTokensBefore, thresholdTokens) => {
657
+ const event = {
658
+ type: "before_compaction", sessionId: this.sessionId, toolCalls: [],
659
+ compaction: {
660
+ count: trajectory.filter(message => message.role === "compaction" && !message.undone).length + 1,
661
+ mode: this.config.compactionMode === "summary" ? "summary" : "rolling",
662
+ source, estimatedTokensBefore, thresholdTokens,
663
+ },
664
+ };
665
+ let action;
666
+ try {
667
+ action = checkpointAction(await this.options.midturnCheckpointHook(event));
668
+ if (action !== "continue") {
669
+ await this.makeCheckpoint(trajectory, this.tools.availableTools(), event, action);
670
+ }
671
+ }
672
+ catch (error) {
673
+ throw new CheckpointFailure(error);
674
+ }
675
+ if (action === "snapshot_exit")
676
+ throw new CheckpointExit(event);
677
+ }
678
+ : undefined,
639
679
  };
640
680
  const contextLength = messages.length;
641
681
  const result = settings.legacy
@@ -647,26 +687,27 @@ export class Agent {
647
687
  return this.checkpoint(trajectory).catch(() => undefined);
648
688
  });
649
689
  if (result.compacted) {
690
+ const covered = settings.legacy ? result.coveredMessages : this.memoryCache.state.coveredMessages;
691
+ const retained = contextLength - covered;
692
+ // Insert at the covered boundary, before the retained original tail.
693
+ // Marker position is the resume boundary; no separate index is stored.
694
+ let insertion = trajectory.length;
695
+ let remaining = retained;
696
+ while (remaining > 0 && insertion > 0) {
697
+ if (trajectory[--insertion].role !== "compaction")
698
+ remaining--;
699
+ }
700
+ trajectory.splice(insertion, 0, {
701
+ role: "compaction", content: [{ type: "text", text: result.memory }],
702
+ undone: false,
703
+ compaction: {
704
+ ratio: result.estimatedTokensBefore / this.config.contextWindow,
705
+ tokens_at_compaction: result.estimatedTokensBefore,
706
+ source,
707
+ retained_messages: retained,
708
+ },
709
+ });
650
710
  if (!isCodeArenaMode(this.config.mode)) {
651
- const covered = settings.legacy ? result.coveredMessages : this.memoryCache.state.coveredMessages;
652
- const retained = contextLength - covered;
653
- // Insert at the covered boundary, before the retained original tail.
654
- // Marker position is the resume boundary; no separate index is stored.
655
- let insertion = trajectory.length;
656
- let remaining = retained;
657
- while (remaining > 0 && insertion > 0) {
658
- if (trajectory[--insertion].role !== "compaction")
659
- remaining--;
660
- }
661
- trajectory.splice(insertion, 0, {
662
- role: "compaction", content: [{ type: "text", text: result.memory }],
663
- undone: false,
664
- compaction: {
665
- ratio: result.estimatedTokensBefore / this.config.contextWindow,
666
- tokens_at_compaction: result.estimatedTokensBefore,
667
- source,
668
- },
669
- });
670
711
  // Coverage indices referred to the pre-compaction history. The next
671
712
  // update reads its prior memory from the newly embedded block.
672
713
  this.memoryCache.state = undefined;
@@ -680,7 +721,11 @@ export class Agent {
680
721
  }
681
722
  return result.compacted;
682
723
  }
683
- catch {
724
+ catch (error) {
725
+ if (error instanceof CheckpointExit)
726
+ return "checkpoint";
727
+ if (error instanceof CheckpointFailure)
728
+ throw error.cause;
684
729
  // Compaction is protective rather than task-critical. If its dedicated
685
730
  // summary request fails, keep the original history and let the normal
686
731
  // model request (and its retry policy) decide whether it still fits.
@@ -707,6 +752,7 @@ export class Agent {
707
752
  const calls = new Map();
708
753
  const responseItems = new Map();
709
754
  let usage;
755
+ let finishReason;
710
756
  let reasoningDetails;
711
757
  const detectors = this.options.midturnCheckpointHook ? {
712
758
  content: new RepetitiveOutputDetector(),
@@ -775,6 +821,9 @@ export class Agent {
775
821
  // letting the last non-empty value win.
776
822
  if (chunk.usage)
777
823
  usage = chunk.usage;
824
+ if (chunk.choices?.[0]?.finish_reason) {
825
+ finishReason = chunk.choices[0].finish_reason;
826
+ }
778
827
  if (chunk.reasoning_details?.length) {
779
828
  reasoningDetails = chunk.reasoning_details;
780
829
  }
@@ -813,6 +862,11 @@ export class Agent {
813
862
  if (fragment.function?.arguments) {
814
863
  call.function.arguments += String(fragment.function.arguments);
815
864
  }
865
+ // Metadata is an opaque value for this index, not a text delta.
866
+ // Later fragments that omit it must not erase an earlier signature.
867
+ if (fragment.extra_content !== undefined) {
868
+ call.extra_content = structuredClone(fragment.extra_content);
869
+ }
816
870
  calls.set(index, call);
817
871
  this.sink.delta(this.sessionId, {
818
872
  tool_calls: [
@@ -824,6 +878,9 @@ export class Agent {
824
878
  name: String(fragment.function?.name || ""),
825
879
  arguments: String(fragment.function?.arguments || ""),
826
880
  },
881
+ ...(fragment.extra_content !== undefined
882
+ ? { extra_content: structuredClone(fragment.extra_content) }
883
+ : {}),
827
884
  },
828
885
  ],
829
886
  });
@@ -896,7 +953,10 @@ export class Agent {
896
953
  .sort(([left], [right]) => left - right)
897
954
  .map(([, call]) => call);
898
955
  }
899
- return assistant;
956
+ return {
957
+ assistant,
958
+ finishReason: finishReason ?? (assistant.tool_calls?.length ? "tool_calls" : "stop"),
959
+ };
900
960
  }
901
961
  }
902
962
  async retryBackoff(attempt) {