@ai-zen/agents-sdk 1.0.0-alpha.22 → 1.0.0-alpha.23

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 (43) hide show
  1. package/CHANGELOG.md +216 -184
  2. package/README.md +13 -0
  3. package/README.zh.md +13 -0
  4. package/dist/agent-plugins/AutoMigratePlugin.js +1 -1
  5. package/dist/agent-plugins/UnknownToolHintPlugin.js +4 -4
  6. package/dist/config/constants.js +24 -24
  7. package/dist/config/factory-skills.js +56 -56
  8. package/dist/runtime/HandoffGuardPlugin.js +2 -2
  9. package/dist/runtime/HandoffService.js +22 -22
  10. package/dist/runtime/createAgent.js +2 -2
  11. package/dist/runtime/createModel.js +3 -3
  12. package/dist/scope/PermissionEvaluator.js +1 -1
  13. package/dist/scope/Scope.js +1 -1
  14. package/dist/scope/plugins/builtin/BatchEditTool.js +8 -8
  15. package/dist/scope/plugins/builtin/CopyTool.js +4 -4
  16. package/dist/scope/plugins/builtin/CwdTool.js +1 -1
  17. package/dist/scope/plugins/builtin/DownloadFileTool.js +8 -8
  18. package/dist/scope/plugins/builtin/EditTool.js +9 -9
  19. package/dist/scope/plugins/builtin/ExecTool.js +8 -8
  20. package/dist/scope/plugins/builtin/ExistTool.js +2 -2
  21. package/dist/scope/plugins/builtin/FindTextTool.js +10 -10
  22. package/dist/scope/plugins/builtin/GenerateImageTool.js +13 -13
  23. package/dist/scope/plugins/builtin/GlobTool.js +4 -4
  24. package/dist/scope/plugins/builtin/InspectFileTool.js +7 -7
  25. package/dist/scope/plugins/builtin/LsTool.js +5 -5
  26. package/dist/scope/plugins/builtin/MkdirTool.js +3 -3
  27. package/dist/scope/plugins/builtin/ReadTool.js +8 -8
  28. package/dist/scope/plugins/builtin/RenameTool.js +3 -3
  29. package/dist/scope/plugins/builtin/RmTool.js +3 -3
  30. package/dist/scope/plugins/builtin/SleepTool.js +6 -6
  31. package/dist/scope/plugins/builtin/SpawnTool.js +9 -9
  32. package/dist/scope/plugins/builtin/ViewImageTool.js +12 -12
  33. package/dist/scope/plugins/builtin/WriteTool.js +3 -3
  34. package/dist/scope/plugins/mcp/McpConnectionManager.js +7 -7
  35. package/dist/scope/plugins/mcp/discover.js +1 -1
  36. package/dist/scope/plugins/mcp/mcpTools.js +29 -29
  37. package/dist/scope/plugins/skills/discover.js +7 -7
  38. package/dist/scope/plugins/skills/skillTools.js +17 -17
  39. package/dist/scope/plugins/subagents/subAgentTools.js +2 -2
  40. package/dist/scope/plugins/usertools/discover.js +2 -2
  41. package/dist/shared/content-blocks.js +9 -9
  42. package/dist/shared/errors.js +3 -3
  43. package/package.json +10 -1
package/CHANGELOG.md CHANGED
@@ -1,315 +1,347 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-alpha.23] - 2026-10-29
4
+
5
+ > The SDK is now English-only. Every model-facing and user-facing string is a
6
+ > fixed English literal: no i18n layer, no locale field in `AppConfig`, no
7
+ > runtime language switching. This is a performance boundary, not a convenience
8
+ > choice — SDK behaviour and performance must not vary with the user's language.
9
+
10
+ ### 💥 Breaking Changes
11
+
12
+ - **Language is a performance boundary, not a translation gap.** `Model.tokenThresholds.handoff` / `guard` and `AppConfig.maxToolOutput` are absolute token / character counts, so a localized tool schema is a different token count and would make handoff and context-guard trip at different amounts of real content for the same `config.json`. Tool descriptions are also re-sent on every request, so a token delta is a standing cost rather than a one-off, and per-language prompt-cache prefixes would strip benchmarks of the single variable they rely on.
13
+
14
+ - **All tool schema descriptions are now English.** 270 hardcoded Chinese string literals across 39 source files were translated. Tool names, parameter keys, enum values and identifiers were already English and are unchanged.
15
+ - **The factory system prompt no longer forces a reply language.** The hardcoded `reply in Chinese` directive was removed and replaced with a language-neutral instruction — `Always reply in the same language the user writes in.` The model now follows the user's language per message instead of per installation, which is what a locale dimension could never express.
16
+ - **Factory assets are English.** The default Agent and SubAgent prompts, the factory handoff Skill (`skills/handoff/SKILL.md`), the fallback handoff template, and the factory catalog (endpoint / model / MCP server descriptions) are all English. `DEFAULT_AGENT_DEFINITION` and `DEFAULT_SUBAGENT_DEFINITION` keep their shape — only the text changed, so no consumer code needs updating.
17
+ - **Upgrade behaviour.** `bootstrap()` rewrites the factory prompt in `agents/default.json`; Agents marked `custom: true` are untouched. Conversation archives keep their original text: a conversation copies its system message at creation time and never re-derives it from the factory definition, so existing archives stay exactly as they were.
18
+
19
+ ### 🔧 Changed
20
+
21
+ - Runtime-injected guidance (`UnknownToolHintPlugin`), tool results, error messages, and the `[handoff]` / `[autoMigrate]` / `[usertools]` / `[sdk]` log lines are English. Log prefixes are preserved.
22
+ - The `⚠️ Unverified` placeholder marker is now spelled identically in the handoff Skill and in the `HandoffService` summary-mode instruction; the two previously differed in letter case, which let the model emit either form.
23
+ - `CHANGELOG.md` is English-only.
24
+
25
+ ### ✅ Tests
26
+
27
+ - 167 assertions across 43 test files updated. Case count and assertion strictness unchanged — several assertions became more specific rather than looser.
28
+ - Full suite: 57 files / 612 passed / 6 skipped, identical to alpha.22. `tsc --noEmit` clean, build clean.
29
+ - Change verified as text-only: every removed line in the diff contained CJK characters, so no logic, signature or control flow was touched.
30
+
31
+ ### 📄 Docs
32
+
33
+ - Design docs under `docs/` stay Chinese; `README.md` (English) and `README.zh.md` (Chinese) keep their existing split.
34
+
3
35
  ## [1.0.0-alpha.22] - 2026-10-28
4
36
 
5
- > 中断策略(`abortedMessageStrategy` / `abortedToolRoundStrategy`)从「调用方各自传参」
6
- > 升级为**配置项**:写进 `config.json`,`createAgent()` 自动读取。
37
+ > Abort strategy (`abortedMessageStrategy` / `abortedToolRoundStrategy`) moved from "each caller passes its own params"
38
+ > to a **config entry**: written into `config.json`, read automatically by `createAgent()`.
7
39
 
8
- ### ✨ 新增
40
+ ### 🚀 New Features
9
41
 
10
- - **`AppConfig` 新增两个策略字段**(`types/index.ts`):
11
- - `abortedMessageStrategy?: "keep" | "drop"` —— 被中断的 assistant 已输出的思考 / 正文是否保留。`keep` 让模型下一轮能接着半截往下说,不再「失忆重来」。
12
- - `abortedToolRoundStrategy?: "fill" | "drop"` —— 被中断 / 结果不完整的工具轮次如何归一。`fill` 整轮保留并给未完成结果补原因,保证 `tool_calls` 与结果一一配对。
13
- - **出厂默认写 `keep` + `fill`**(`DEFAULT_APP_CONFIG`):`keep` 是产品层结论(core 的库级默认仍是 `drop`),`fill` 与 core 默认一致、无行为变化。
14
- - **`createAgent()` 从 `scope.config` 取策略**:优先级为「显式参数 > `scope.config` > core 默认值」。上游(CLI / Desktop)不必自己读配置再转一道参数,子 Agent 创建时也自然继承同一份策略。
15
- - **配置迁移补齐策略字段**(`migrateConfig`):缺失或非法的 `abortedMessageStrategy` / `abortedToolRoundStrategy` 一律写回出厂默认(`changed` 置真,`ensureDefaultConfig()` 会落盘一次)。于是「配置文件里写着的就是运行时用的」,各端不再需要 `?? "keep"` 式兜底。
42
+ - **`AppConfig` gained two strategy fields** (`types/index.ts`):
43
+ - `abortedMessageStrategy?: "keep" | "drop"` — whether to keep the thinking / body text an interrupted assistant has already emitted. `keep` lets the model continue from the half-finished output in the next round instead of "losing memory and starting over".
44
+ - `abortedToolRoundStrategy?: "fill" | "drop"` — how to normalize an interrupted / incomplete tool round. `fill` keeps the whole round and supplies a reason for unfinished results, guaranteeing that `tool_calls` and results pair up one-to-one.
45
+ - **Factory defaults are `keep` + `fill`** (`DEFAULT_APP_CONFIG`): `keep` is a product-level decision (the library-level default in core is still `drop`); `fill` matches the core default and changes no behavior.
46
+ - **`createAgent()` takes the strategy from `scope.config`**: priority is "explicit parameter > `scope.config` > core default". Upstream consumers (CLI / Desktop) no longer have to read the config themselves and forward another round of params, and child Agents inherit the same strategy naturally when created.
47
+ - **Config migration now fills in the strategy fields** (`migrateConfig`): a missing or invalid `abortedMessageStrategy` / `abortedToolRoundStrategy` is always written back as the factory default (`changed` set to true, `ensureDefaultConfig()` persists it once). So "what the config file says is what runs at runtime", and no client needs a `?? "keep"`-style fallback anymore.
16
48
 
17
- ### ✅ 测试
49
+ ### ✅ Tests
18
50
 
19
- - 全量 **57 文件 / 612 例通过**(6 skipped,alpha.21 时 608 例)。
20
- - `migrate.test.ts` 20 → 22 例:补齐缺失策略、非法值改写、合法值原样保留且 `changed=false`;原「无需迁移返回原对象」用例补上策略字段。
21
- - `createAgent.test.ts` 6 → 8 例:策略取自 `scope.config`、显式参数覆盖配置、config 未写时回落 core 默认(`drop` / `fill`)。
51
+ - Full suite **57 files / 612 cases passing** (6 skipped; 608 cases in alpha.21).
52
+ - `migrate.test.ts` 20 → 22 cases: filling in missing strategy, rewriting invalid values, keeping valid values as is with `changed=false`; the existing "no migration needed, returns the original object" case now also covers the strategy fields.
53
+ - `createAgent.test.ts` 6 → 8 cases: strategy taken from `scope.config`, explicit params overriding the config, falling back to the core default when config has no value (`drop` / `fill`).
22
54
 
23
55
  ## [1.0.0-alpha.21] - 2026-10-28
24
56
 
25
- ### ✨ 新增
57
+ ### 🚀 New Features
26
58
 
27
- - **交接/迁移支持取消(`AbortSignal`)** —— 此前 `handoff()` / `migrate()` 一旦开始就没有任何中止手段,宿主按 Ctrl+C 也停不下来:
28
- - `summary` 模式的独立模型请求根本没有任何取消点;
29
- - `self` 模式能被 `agent.abort()` 打断,但中止后被误判成「模型没写出文档」,于是重试一轮、再降级 `summary` 再跑一轮——用户以为取消了,实际多烧两轮全量输入。
59
+ - **Handoff/migration now supports cancellation (`AbortSignal`)** — previously `handoff()` / `migrate()` had no means of aborting once started, and pressing Ctrl+C in the host could not stop them:
60
+ - the standalone model request in `summary` mode had no cancellation point at all;
61
+ - `self` mode could be interrupted by `agent.abort()`, but the abort was misjudged as "the model produced no document", so it retried one round and then degraded to `summary` for another round — the user thought it was cancelled while two extra rounds of full input were burned.
30
62
 
31
- 现在 `HandoffParams` 新增 `signal?: AbortSignal`,两个执行者都认它:
63
+ `HandoffParams` now gains `signal?: AbortSignal`, honored by both executors:
32
64
 
33
- - `summary`:`signal` 作为 **RequestOptions(第二参数)** 传给 `chat.completions.create`,请求会真的被中断;
34
- - `self`:每个 `agent.send()` 之后**主动检查** `aborted`(因为 `abort()` 不抛错),取消即抛 `HandoffAbortedError`,**不重试、不降级**;
35
- - `runHandoff()` 在阶段之间加取消点;`migrate()` 被取消时**不改动会话历史**(不会留下「已迁移」的假象)。
65
+ - `summary`: `signal` is passed as the **RequestOptions (second argument)** to `chat.completions.create`, so the request is genuinely aborted;
66
+ - `self`: after every `agent.send()` it **actively checks** `aborted` (because `abort()` does not throw), and on cancellation throws `HandoffAbortedError` — **no retry, no degradation**;
67
+ - `runHandoff()` adds cancellation points between phases; when `migrate()` is cancelled it **leaves the session history untouched** (no fake "already migrated" state left behind).
36
68
 
37
- - **导出 `HandoffAbortedError`** —— 取消单独成类型,调用方才能把「用户按了 Ctrl+C」与「交接真的失败」分开处理:前者静默收尾,后者才该报错。
69
+ - **Exported `HandoffAbortedError`** — cancellation gets its own type, so callers can handle "the user pressed Ctrl+C" and "the handoff genuinely failed" separately: the former ends quietly, only the latter should raise an error.
38
70
 
39
- ### 🐛 修复
71
+ ### 🔧 Fixed
40
72
 
41
- - **取消后清掉写了一半的交接文档** —— 只删本次新建的(同名旧文件不碰)。半成品比没有更糟:下一轮接手方会把它当成真实交接读进去。
73
+ - **Cleaned up the half-written handoff document on cancellation** — only the file created in this run is deleted (an existing file with the same name is left alone). A half-finished artifact is worse than none: the next round's receiver would read it as a real handoff.
42
74
 
43
- ### ✅ 测试
75
+ ### ✅ Tests
44
76
 
45
- - 全量 **57 文件 / 608 例通过**(6 skipped)。
46
- - `HandoffService.test.ts` **24 → 30 例**:signal 透传 RequestOptions、summary 取消不落盘、self 取消不重试不降级、半成品清理、migrate 取消不改历史、未取消时不误伤正常路径。
47
- - 破坏性验证:删掉 `self` 的取消检查 → 「不重试、不降级」用例红。
77
+ - Full suite **57 files / 608 cases passing** (6 skipped).
78
+ - `HandoffService.test.ts` **24 → 30 cases**: signal forwarded as RequestOptions, cancelled summary writes nothing, cancelled self neither retries nor degrades, half-finished cleanup, cancelled migrate leaves history unchanged, and the normal path is not broken when nothing is cancelled.
79
+ - Destructive verification: removing the cancellation check in `self` turns the "no retry, no degradation" case red.
48
80
 
49
81
  ## [1.0.0-alpha.20] - 2026-10-27
50
82
 
51
- ### ✨ 新增
83
+ ### 🚀 New Features
52
84
 
53
- - **`createAgent(scope, agentId, options?)` 新增第三参,把中断相关策略透传出来** —— 此前 SDK 完全没有暴露 core 的 `abortedToolRoundStrategy` / `abortedToolRoundReason`,调用方只能吃默认值。本轮一并接出三个字段(新增导出类型 `CreateAgentOptions`,`SdkAgent` 构造参数同步支持):
54
- - `abortedToolRoundStrategy?: "drop" | "fill"` —— 工具轮次被中断(或结果不完整)时的归一策略(core 默认 `fill`);
55
- - `abortedToolRoundReason?: string` —— `fill` 策略下未完成工具结果的填充原因;
56
- - `abortedMessageStrategy?: "drop" | "keep"` —— **新增能力**:被中断的 assistant 已输出的内容 / 思考是否保留(core 默认 `drop`)。传 `keep` 后,用户中断时那半句话仍会进入下一轮上下文,模型能接着往下说,而不是「失忆重来」;带工具调用的轮次仍由 `abortedToolRoundStrategy` 决定,不受影响。
85
+ - **`createAgent(scope, agentId, options?)` gained a third parameter that forwards the abort-related strategies** — previously the SDK did not expose core's `abortedToolRoundStrategy` / `abortedToolRoundReason` at all, so callers were stuck with the defaults. This release surfaces three fields (with the new exported type `CreateAgentOptions`, also supported by the `SdkAgent` constructor):
86
+ - `abortedToolRoundStrategy?: "drop" | "fill"` — normalization strategy when a tool round is interrupted (or its results are incomplete) (core default `fill`);
87
+ - `abortedToolRoundReason?: string` — the reason filled in for unfinished tool results under the `fill` strategy;
88
+ - `abortedMessageStrategy?: "drop" | "keep"` — **new capability**: whether to keep the content / thinking an interrupted assistant has already emitted (core default `drop`). With `keep`, the half sentence from a user interruption still enters the next round's context, so the model can continue instead of "losing memory and starting over"; rounds with tool calls are still governed by `abortedToolRoundStrategy` and are unaffected.
57
89
 
58
- ### 🔧 依赖
90
+ ### ⚠️ Dependency
59
91
 
60
- - 升级 `@ai-zen/agents-core` 至 `4.5.3`(新增 `abortedMessageStrategy` 与 `reconcileAbortedMessages()`)。
92
+ - Upgraded `@ai-zen/agents-core` to `4.5.3` (adds `abortedMessageStrategy` and `reconcileAbortedMessages()`).
61
93
 
62
- ### ✅ 测试
94
+ ### ✅ Tests
63
95
 
64
- - 全量 **57 文件 / 602 例通过**(6 skipped)。
96
+ - Full suite **57 files / 602 cases passing** (6 skipped).
65
97
 
66
98
  ## [1.0.0-alpha.19] - 2026-10-27
67
99
 
68
- ### 🐛 修复
100
+ ### 🔧 Fixed
69
101
 
70
- - **日志不再绕过 `getLogger()`:`[load_mcp]` 等日志会直接打到宿主终端** —— 4 个模块此前在**模块顶层**执行 `const log = getLogger()`,而 `getLogger()` 当时返回的是 `{ info: console.log, ... }`。模块求值那一刻,`console.log` 的**函数引用**就被冻进了常量;此后宿主调用 `setLogger()` 换掉的是内部 `currentLogger` 变量,**改不动已经抓走的引用**。后果:宿主怎么接管日志都没用 —— CLI 的 Ink TUI 里加载 MCP 时,`load_mcp` 会把整份 tools / resources 清单直接砸在渲染区外面,把界面撕成乱码。
102
+ - **Logging no longer bypasses `getLogger()`: logs such as `[load_mcp]` went straight to the host terminal** — 4 modules previously ran `const log = getLogger()` at **module top level**, and `getLogger()` at the time returned `{ info: console.log, ... }`. At the moment the module was evaluated, the **function reference** of `console.log` was frozen into the constant; afterwards the host's `setLogger()` swapped the internal `currentLogger` variable, which **could not change the already captured reference**. Consequence: whatever the host did to take over logging, it did not help — when the CLI's Ink TUI loaded MCP, `load_mcp` dumped the entire tools / resources list outside the render area and shredded the interface into garbled output.
71
103
 
72
- 现在 4 处全部改为**运行时取 logger**(`getLogger().info(...)` 等),`setLogger()` 立即生效:
104
+ All 4 sites now **fetch the logger at runtime** (`getLogger().info(...)` etc.), so `setLogger()` takes effect immediately:
73
105
 
74
- - `scope/plugins/mcp/mcpTools.ts` —— `[load_mcp]` 两条 info(摘要 + 完整清单);
75
- - `scope/plugins/skills/discover.ts` —— `[skills]` 一条 warn;
76
- - `agent-plugins/AutoMigratePlugin.ts` —— `[autoMigrate]` 一条 error;
77
- - `scope/plugins/skills/skillTools.ts` —— 顶层快照**从未被调用**(死代码),连同 `getLogger` import 一并删除。
106
+ - `scope/plugins/mcp/mcpTools.ts` — two `[load_mcp]` info logs (summary + full list);
107
+ - `scope/plugins/skills/discover.ts` — one `[skills]` warn;
108
+ - `agent-plugins/AutoMigratePlugin.ts` — one `[autoMigrate]` error;
109
+ - `scope/plugins/skills/skillTools.ts` — the top-level snapshot was **never called** (dead code) and was deleted together with the `getLogger` import.
78
110
 
79
- 至此 SDK 内 10 处日志调用全部经由 `getLogger()` 这一个出口,宿主 `setLogger()` 换什么就走什么,不再存在 console 旁路。
111
+ With this, all 10 log call sites in the SDK go through the single `getLogger()` exit; whatever the host installs via `setLogger()` is what gets used, and no console bypass remains.
80
112
 
81
- ### ✅ 测试
113
+ ### ✅ Tests
82
114
 
83
- - 全量 **57 文件 / 602 例通过**(6 skipped)。
84
- - 端到端对照验证:同一段「先求值模块 → 再 `setLogger` → 调用 `load_mcp`」的代码,旧版日志直写 stdout、日志文件为空;新版 stdout 干净、日志完整落盘。
115
+ - Full suite **57 files / 602 cases passing** (6 skipped).
116
+ - End-to-end comparison: with the same code "evaluate the module first → then `setLogger` → call `load_mcp`", the old version wrote logs straight to stdout and left the log file empty; the new version keeps stdout clean and writes the logs fully to disk.
85
117
 
86
118
  ## [1.0.0-alpha.18] - 2026-10-22
87
119
 
88
- ### 🔧 行为变更(不兼容)
120
+ ### 💥 Breaking Changes
89
121
 
90
- - **模型的唯一真相源改为 `AgentDefinition.modelId`,`AppConfig.defaultModel` 删除** —— 此前模型有两处来源:Agent 定义里的 `modelId` 优先,缺失时回退 `config.defaultModel`。而 CLI 等客户端还会在**会话存档**里另记一份模型名用于展示。三份值一旦分叉,就会出现「界面写着 DeepSeek、请求却发给 GLM」的静默错位:用户看到 401 却怎么切都切不回来(切换命令按显示层判断「已经是当前模型」直接返回,实际模型从未改变)。现在:
91
- - `AppConfig` 删除 `defaultModel` 字段,出厂配置同步删除;`migrateConfig` 会把磁盘上的旧字段**清掉**(一次性,写入后不再出现);
92
- - `createAgent` 只读 `definition.modelId`,**缺失直接报错**,不再有任何隐式回退;
93
- - `ViewImageTool.isAvailable` / `resolveModelAndEndpoint` 同步去掉 `defaultModel` 兜底,反查不到即视为「无端点」,让调用方走降级路径而不是猜一个模型。
94
- - **`DEFAULT_AGENT_DEFINITION.modelId = "deepseek-flash"`(新增导出 `DEFAULT_MODEL_ID`)** —— 出厂默认 Agent 自带模型 ID,装完即用,不再依赖配置层的默认值。
95
- - **升级兼容:`ensureDefaultAgent()` 会为缺 `modelId` 的历史 Agent 补上** —— 优先沿用老的 `config.defaultModel`(若该模型仍在清单里),否则回落到出厂 `deepseek-flash`,因此升级不会改变用户正在用的模型。
122
+ - **The single source of truth for the model became `AgentDefinition.modelId`, and `AppConfig.defaultModel` was removed** — the model previously had two sources: `modelId` in the Agent definition took priority, falling back to `config.defaultModel` when missing. Clients such as the CLI also kept another copy of the model name in the **session archive** for display. Once the three values diverged, a silent mismatch appeared where "the UI says DeepSeek while the request goes to GLM": the user saw a 401 but could not switch back no matter what (the switch command judged "already the current model" from the display layer and returned immediately, while the actual model never changed). Now:
123
+ - the `defaultModel` field was removed from `AppConfig`, and from the factory config as well; `migrateConfig` **clears** the old field on disk (one-time, it never reappears after being written);
124
+ - `createAgent` reads only `definition.modelId` and **throws when it is missing**, with no implicit fallback left;
125
+ - `ViewImageTool.isAvailable` / `resolveModelAndEndpoint` dropped the `defaultModel` fallback as well; a failed lookup now counts as "no endpoint", letting the caller take a degradation path instead of guessing a model.
126
+ - **`DEFAULT_AGENT_DEFINITION.modelId = "deepseek-flash"` (new export `DEFAULT_MODEL_ID`)** — the factory default Agent ships with a model ID, usable right after install, no longer relying on a default from the config layer.
127
+ - **Upgrade compatibility: `ensureDefaultAgent()` fills in `modelId` for legacy Agents that lack it** — it first reuses the old `config.defaultModel` (if that model is still in the list), otherwise falls back to the factory `deepseek-flash`, so upgrading does not change the model the user is currently using.
96
128
 
97
- ### ✅ 测试
129
+ ### ✅ Tests
98
130
 
99
- - 全量 **57 文件 / 602 例通过**(6 skipped)。
100
- - 新增/改写重点:`createAgent` 缺 `modelId` 必须抛错、`ensureDefaultAgent` 补齐并落盘、`migrateConfig` 清除废弃字段且不就地改原对象、`resolveModelAndEndpoint` 未命中返回 `null` 端点、MCP / 图片工具按 `ctx.agent.model` 反查端点(不再靠默认模型兜底)。
131
+ - Full suite **57 files / 602 cases passing** (6 skipped).
132
+ - New/rewritten highlights: `createAgent` must throw when `modelId` is missing, `ensureDefaultAgent` backfills and persists it, `migrateConfig` removes deprecated fields without mutating the original object in place, `resolveModelAndEndpoint` returns a `null` endpoint on a miss, and the MCP / image tools resolve endpoints via `ctx.agent.model` (no longer falling back to a default model).
101
133
 
102
134
  ## [1.0.0-alpha.16] - 2026-10-22
103
135
 
104
- ### 🔧 行为变更(不兼容,向上修复)
136
+ ### 💥 Breaking Changes
105
137
 
106
- - **升级 `@ai-zen/agents-core` 至 `4.5.2`:请求体里的固定字段不再被 `modelParams` 覆盖** —— core 之前把模型参数 spread 在请求体**最后**(`{ model, messages, tools, stream, stream_options, ...modelConfig }`),而 `modelConfig` 来自用户可编辑的 `Model.defaultParams` / `AgentDefinition.modelParams`。一旦其中出现同名键就会静默生效:最惨的是 `stream: false`,它会把流式关掉而调用方仍在按异步流解析,整轮直接失败。现在顺序反过来,固定字段永远胜出,非固定键(`temperature` / `reasoning_effort` / 厂商特有 `thinking` 等)照常透传。
138
+ - **Upgraded `@ai-zen/agents-core` to `4.5.2`: fixed fields in the request body are no longer overridden by `modelParams`** — core used to spread the model params **last** in the request body (`{ model, messages, tools, stream, stream_options, ...modelConfig }`), and `modelConfig` comes from the user-editable `Model.defaultParams` / `AgentDefinition.modelParams`. Any key of the same name then took effect silently: the worst case was `stream: false`, which turned streaming off while the caller was still parsing an async stream, failing the whole round outright. The order is now reversed, so fixed fields always win, while non-fixed keys (`temperature` / `reasoning_effort` / vendor-specific `thinking`, etc.) pass through as before.
107
139
 
108
- 对本 SDK 的实际影响:`AgentDefinition.modelParams` 里写 `model` / `messages` / `tools` / `stream` / `stream_options` 将不再有任何效果。正常配置(温度、思考参数)不受影响。
140
+ Actual impact on this SDK: writing `model` / `messages` / `tools` / `stream` / `stream_options` in `AgentDefinition.modelParams` now has no effect at all. Normal configuration (temperature, thinking params) is unaffected.
109
141
 
110
- ### ✅ 测试
142
+ ### ✅ Tests
111
143
 
112
- - 依赖的 core 侧新增 3 例(非固定键透传、固定键被忽略、无 `modelConfig` 时请求体只含框架字段)。SDK 自身 **57 文件 / 600 例通过**(6 skipped)。
144
+ - 3 new cases on the core side (non-fixed keys pass through, fixed keys are ignored, the request body contains only framework fields when there is no `modelConfig`). The SDK itself: **57 files / 600 cases passing** (6 skipped).
113
145
 
114
146
  ## [1.0.0-alpha.15] - 2026-10-22
115
147
 
116
- ### 🔧 行为变更(不兼容)
148
+ ### 💥 Breaking Changes
117
149
 
118
- - **SDK 不再「消费」思考档位,只保留表本身** —— alpha.14 把「档位 → 请求参数」的整套解析塞进了 SDK 运行时:`applyReasoningEffort` / `modelLevels` / `resolveReasoningEffort` / `resolveModelParams`,还要 `Scope.reasoningPresets` 注入、Agent 上存档位名、运行时查表拼参数。但 `low` / `high` / `max` / `off` 是**产品语义,不是协议事实**,SDK 唯一客观知道的是「请求参数长什么样」——那本来就由 `defaultParams` 承载。现在:
119
- - **删除 `shared/reasoning.ts` 的全部 API**(`applyReasoningEffort` / `modelLevels` / `presetOf` / `readReasoningEffort` / `resolveReasoningEffort` / `resolveModelParams`)以及 `Scope.reasoningPresets`;
120
- - **保留**思考预设的**定义与出厂内容**(`ReasoningPresets` / `ReasoningPreset` / `ReasoningPatch` 类型、`config/reasoning-presets.ts`、`ConfigManager.readReasoningPresets()` / `ensureReasoningPresets()`)—— 它是 CLI / 桌面端等客户端**共享的资产**(「智谱 5.3 只认 low/high/max」这种知识不该被每个客户端各写一遍),但那只是**数据**,SDK 不拿它生成请求参数。
121
- - **`AgentDefinition.reasoningEffort` → `AgentDefinition.modelParams?: ModelParams`** —— Agent 级覆盖从「档位名」变成「具体参数」。客户端读到表、拿档位补丁,在**写入配置时**就把它展开存进 `modelParams` 里。于是**配置即事实**:运行期不查表、不推导。
122
- - **`createModel(scope, modelId, overrides?: ModelParams)`** —— 第三参从 `Pick<AgentDefinition, "reasoningEffort">` 变成裸的参数对象,内部只有一行:
150
+ - **The SDK no longer "consumes" reasoning levels; only the table itself remains** — alpha.14 pushed the entire "level → request params" resolution into the SDK runtime: `applyReasoningEffort` / `modelLevels` / `resolveReasoningEffort` / `resolveModelParams`, plus `Scope.reasoningPresets` injection, storing level names on the Agent, and looking up the table at runtime to assemble params. But `low` / `high` / `max` / `off` are **product semantics, not protocol facts**; the only thing the SDK objectively knows is "what the request params look like" — which `defaultParams` already carries. Now:
151
+ - **the entire API of `shared/reasoning.ts` was removed** (`applyReasoningEffort` / `modelLevels` / `presetOf` / `readReasoningEffort` / `resolveReasoningEffort` / `resolveModelParams`), along with `Scope.reasoningPresets`;
152
+ - **kept**: the **definition and factory content** of the reasoning presets (`ReasoningPresets` / `ReasoningPreset` / `ReasoningPatch` types, `config/reasoning-presets.ts`, `ConfigManager.readReasoningPresets()` / `ensureReasoningPresets()`) — it is an **asset shared by clients** such as the CLI / desktop app (knowledge like "Zhipu 5.3 only accepts low/high/max" should not be written out again by every client), but it is only **data**; the SDK does not use it to generate request params.
153
+ - **`AgentDefinition.reasoningEffort` → `AgentDefinition.modelParams?: ModelParams`** — the Agent-level override changed from "a level name" to "concrete params". The client reads the table, takes the level patch, and expands it into `modelParams` **when writing the config**. So **config is the fact**: no table lookups or inference at runtime.
154
+ - **`createModel(scope, modelId, overrides?: ModelParams)`** — the third parameter changed from `Pick<AgentDefinition, "reasoningEffort">` to a bare params object, and internally it is just one line:
123
155
  ```ts
124
156
  modelConfig = { ...model.defaultParams, ...overrides };
125
157
  ```
126
- 浅合并(嵌套对象整体替换),没有覆盖时原样返回 `defaultParams`。SDK 从此不解释任何键名,也不知道 `thinking` / `reasoning_effort` 是什么。SubAgent 指定独立 `modelId` 时同样应用自己的 `modelParams`。
127
- - **`Model.reasoningPreset` 保留**,语义改为「引用策略表的键」,供客户端判断该模型能选哪几档;SDK 只保证它能随模型配置一起读写与出厂同步,不消费。
158
+ Shallow merge (nested objects are replaced wholesale); without overrides it returns `defaultParams` as is. From now on the SDK interprets no key names and does not even know what `thinking` / `reasoning_effort` are. A SubAgent given its own `modelId` likewise applies its own `modelParams`.
159
+ - **`Model.reasoningPreset` was kept**, with the semantics changed to "a key referencing the preset table", so clients can tell which levels that model offers; the SDK only guarantees that it can be read and written with the model config and synced from the factory — it does not consume it.
128
160
 
129
- ### 🐛 修复
161
+ ### 🔧 Fixed
130
162
 
131
- - **迁移 `Model.reasoning` 旧字段(alpha.13 遗留)** —— 未标 `custom` 的模型会被出厂定义整块覆盖,但**标了 `custom: true` 的会原地保留**,于是留下一个无人认识的 `reasoning`,同时因为缺 `reasoningPreset` 而被客户端判定为「不可调档」。`ConfigManager.read()` 现在会删掉该旧字段,并按同 id 的出厂模型补回 `reasoningPreset`;id 不在出厂清单的(真·自建模型)只删不补。只处理「旧字段存在」这一种情况,用户后续主动删掉 `reasoningPreset` 不会被反复补回来。
163
+ - **Migrated the legacy `Model.reasoning` field (left over from alpha.13)** — models without `custom` were overwritten wholesale by the factory definition, but **those marked `custom: true` were kept in place**, leaving behind a `reasoning` nobody recognized, and because `reasoningPreset` was missing the client judged them "not adjustable". `ConfigManager.read()` now deletes that legacy field and restores `reasoningPreset` from the factory model with the same id; ids not in the factory list (genuinely custom models) are only cleaned, not restored. Only the "legacy field exists" case is handled, so a `reasoningPreset` the user removes later is not restored over and over.
132
164
 
133
- ### ✅ 测试
165
+ ### ✅ Tests
134
166
 
135
- - `src/runtime/createModel.test.ts` 新增 6 例锁住合并语义:无覆盖原样返回、空覆盖等同无覆盖、同名键覆盖而其它键保留、模型无 `defaultParams` 时覆盖即全部、**浅合并不做深合并**(换档位不残留旧子键)、两个 Agent 各配一套互不影响。
136
- - `src/config/migrate.test.ts` 新增 6 例(19 例合计)。
137
- - 删除 `src/shared/reasoning.test.ts`(26 例,运行时解析已移除);`src/config/reasoning-presets.test.ts`(11 例)保留 —— 表还在 SDK。
138
- - 全量 **57 文件 / 600 例通过**(6 skipped)。
167
+ - `src/runtime/createModel.test.ts` added 6 cases locking down the merge semantics: no overrides returns as is, empty overrides equal no overrides, same-name keys override while other keys are kept, with no `defaultParams` on the model the overrides are everything, **shallow merge does not deep merge** (switching levels leaves no old sub-keys behind), and two Agents with their own sets do not affect each other.
168
+ - `src/config/migrate.test.ts` added 6 cases (19 in total).
169
+ - Removed `src/shared/reasoning.test.ts` (26 cases; the runtime resolution is gone); `src/config/reasoning-presets.test.ts` (11 cases) is kept — the table is still in the SDK.
170
+ - Full suite **57 files / 600 cases passing** (6 skipped).
139
171
 
140
172
  ## [1.0.0-alpha.14] - 2026-10-22
141
173
 
142
- ### 🔧 行为变更(不兼容)
174
+ ### 💥 Breaking Changes
143
175
 
144
- - **思考能力声明从「模型字段」改为「策略表」** —— alpha.13 那版用 `Model.reasoning: { efforts, disable? }` 表达厂商差异,结果三处结构性缺陷:`disable` 一词双义(既指关闭方式又指能力)、`off` 靠拼接而非声明(`disable ? ["off", ...efforts] : efforts`)、用全局 `ReasoningEffort` 枚举假装各家档位等价。现在改为**档位 → 请求参数补丁**的声明式表:
145
- - 新增 `<basePath>/reasoning-presets.json`(与 `config.json` 同级),出厂内容见 `config/reasoning-presets.ts`;
146
- - `Model.reasoning?` → **`Model.reasoningPreset?: string`**(引用策略名;缺省 = 不可调档);
147
- - `AgentDefinition.reasoningEffort` 类型 `ReasoningEffort` → **`string`**(档位名由策略表定义,不再用假枚举假装统一);
148
- - 删除 `ReasoningEffort` / `ReasoningCapability` 类型与 `capabilityOf` / `modelEfforts` / `modelAllowedEfforts` / `isReasoningEffort` 函数;
149
- - 函数签名统一以 `(model, presets, …)` 开头:`readReasoningEffort(model, presets, params?)`、`applyReasoningEffort(model, presets, level, params?)`、`resolveReasoningEffort(model, presets, definition?)`、`resolveModelParams(model, presets, definition?)`;
150
- - `Scope` 新增 `reasoningPresets`(与 `config` 平级注入,不做全局单例)。
176
+ - **Reasoning capability declaration moved from "model fields" to a "preset table"** — alpha.13 expressed vendor differences with `Model.reasoning: { efforts, disable? }`, which resulted in three structural flaws: `disable` carried two meanings (both the way to turn it off and the capability itself), `off` was assembled rather than declared (`disable ? ["off", ...efforts] : efforts`), and a global `ReasoningEffort` enum pretended every vendor's levels were equivalent. It is now a declarative table of **level → request-param patch**:
177
+ - added `<basePath>/reasoning-presets.json` (sibling of `config.json`); factory content lives in `config/reasoning-presets.ts`;
178
+ - `Model.reasoning?` → **`Model.reasoningPreset?: string`** (references a preset name; absent = not adjustable);
179
+ - the type of `AgentDefinition.reasoningEffort` changed from `ReasoningEffort` to **`string`** (level names are defined by the preset table; no more fake enums pretending uniformity);
180
+ - removed the `ReasoningEffort` / `ReasoningCapability` types and the `capabilityOf` / `modelEfforts` / `modelAllowedEfforts` / `isReasoningEffort` functions;
181
+ - function signatures now all start with `(model, presets, …)`: `readReasoningEffort(model, presets, params?)`, `applyReasoningEffort(model, presets, level, params?)`, `resolveReasoningEffort(model, presets, definition?)`, `resolveModelParams(model, presets, definition?)`;
182
+ - `Scope` gained `reasoningPresets` (injected alongside `config`, not a global singleton).
151
183
 
152
- 换来三个结构性保证,全部来自表的形状、代码零分支:① 可选档位 = `Object.keys(levels)`;② 能否关闭思考 = 表里有没有 `off` 键;③ 「哪些参数键属于思考」= 整张表出现过的键的并集(写入前按此剔除旧值,`max` → `off` 不再残留 `reasoning_effort`,模型换策略也不会反刍上一套参数)。
184
+ In exchange, three structural guarantees, all stemming from the table's shape with zero branches in the code: ① selectable levels = `Object.keys(levels)`; ② whether reasoning can be turned off = whether the table has an `off` key; ③ "which param keys belong to reasoning" = the union of keys appearing anywhere in the table (used to strip stale values before writing, so `max` → `off` no longer leaves `reasoning_effort` behind, and switching a model's preset does not regurgitate the previous param set).
153
185
 
154
- - **`applyReasoningEffort` 的档位校验更严** —— 策略里没有该档位直接抛错(旧版对未声明能力的模型给的是「不支持档位」文案),错误信息带上策略名与可选档位列表。
186
+ - **`applyReasoningEffort` validates levels more strictly** — a level absent from the preset now throws immediately (the old version returned an "unsupported level" message for models without a declared capability), and the error message includes the preset name and the list of available levels.
155
187
 
156
- ### 🚀 新功能
188
+ ### 🚀 New Features
157
189
 
158
- - **`ConfigManager.readReasoningPresets()` / `ensureReasoningPresets()`** —— 策略表的读写与出厂同步。同步粒度**到单个策略**,规则与出厂模型同步同构:文件缺失即写种子;`custom: true` 的策略冻结;未标的用出厂版覆盖;用户自建的非出厂策略永远保留。`bootstrap()` 会自动调用。文件缺失或损坏时 `readReasoningPresets()` 回落到出厂种子,不让上层拿到空表。
159
- - **`presetOf(model, presets)` / `modelLevels(model, presets)`** —— 策略查找与可选档位读取(`modelLevels` 取代旧的 `modelEfforts` + `modelAllowedEfforts` 两个函数:`off` 现在只是表里的一行,没有「档位」与「关闭」的二分)。
160
- - **包入口补导出 `TokenThresholds` / `ReasoningPatch` / `ReasoningPreset` / `ReasoningPresets`** —— alpha.13 漏了这几个类型的导出(原 `7b10196` 补丁并入本版本)。
190
+ - **`ConfigManager.readReasoningPresets()` / `ensureReasoningPresets()`** — read/write of the preset table plus factory sync. Sync granularity is **per individual preset**, with rules isomorphic to factory model sync: a missing file writes the seed; presets marked `custom: true` are frozen; unmarked ones are overwritten by the factory version; user-created non-factory presets are always preserved. `bootstrap()` calls it automatically. When the file is missing or corrupt, `readReasoningPresets()` falls back to the factory seed, so upper layers never get an empty table.
191
+ - **`presetOf(model, presets)` / `modelLevels(model, presets)`** — preset lookup and reading of available levels (`modelLevels` replaces the old `modelEfforts` + `modelAllowedEfforts` pair: `off` is now just another row in the table, with no split between "level" and "off").
192
+ - **The package entry now also exports `TokenThresholds` / `ReasoningPatch` / `ReasoningPreset` / `ReasoningPresets`** — alpha.13 was missing exports for these types (the original `7b10196` patch is merged into this release).
161
193
 
162
- ### ✅ 测试
194
+ ### ✅ Tests
163
195
 
164
- - `src/shared/reasoning.test.ts` 重写(26 例):出厂表协议事实(OpenAI 表无 `thinking` 键、`zhipu-5.3` 表无 `off`、DeepSeek 用 `thinking.type` 关闭)、可调档位 = 表的键、反查取最具体匹配、写入剔除旧键、往返一致、Agent 声明非法回退模型默认、原型链污染名字(`constructor`)不生效。
165
- - `src/config/reasoning-presets.test.ts` 新增(11 例):落盘位置、首次释放、幂等不写盘、未标 `custom` 被覆盖、标了 `custom` 冻结、用户自建策略保留、损坏/数组内容回落种子、`bootstrap()` 顺带释放。
196
+ - `src/shared/reasoning.test.ts` rewritten (26 cases): protocol facts of the factory tables (the OpenAI table has no `thinking` key, `zhipu-5.3` has no `off`, DeepSeek turns reasoning off via `thinking.type`), adjustable levels = the table's keys, lookup picks the most specific match, stale keys are stripped on write, round-trip consistency, an invalid Agent declaration falls back to the model default, and prototype-chain names (`constructor`) have no effect.
197
+ - `src/config/reasoning-presets.test.ts` added (11 cases): file location, first-time release, idempotent runs do not write, entries not marked `custom` are overwritten, entries marked `custom` are frozen, user-created presets are preserved, corrupt / array content falls back to the seed, and `bootstrap()` releases it as a side effect.
166
198
 
167
- ### 📄 文档
199
+ ### 📄 Docs
168
200
 
169
- - `docs/{zh,en}/sdk.md`:「思考强度」章节重写为「思考预设(reasoning presets)」,含策略表结构、出厂策略对照表、三个结构性保证、新 API 表与 `Scope` 注入示例。
170
- - 顺手修掉 alpha.13 之后就过期的示例:`ContextGuardPlugin({ maxTokens, isSuspended })` → `{ guardTokens }`、`AutoMigratePlugin({ maxTokens })` → `{ handoffTokens }`、「交接回合护栏放宽 × 2」的旧描述。
201
+ - `docs/{zh,en}/sdk.md`: the "reasoning effort" section was rewritten as "reasoning presets", covering the preset table structure, a factory preset comparison table, the three structural guarantees, a table of the new APIs, and a `Scope` injection example.
202
+ - Also fixed examples that had been stale since alpha.13: `ContextGuardPlugin({ maxTokens, isSuspended })` → `{ guardTokens }`, `AutoMigratePlugin({ maxTokens })` → `{ handoffTokens }`, and the old "handoff turn guard relaxed × 2" description.
171
203
 
172
204
  ## [1.0.0-alpha.13] - 2026-10-22
173
205
 
174
206
  ### 💥 Breaking Changes
175
207
 
176
- - **`Model.maxContextTokens` / `maxContextChars` → `Model.tokenThresholds: { handoff, guard }`** —— 旧名字一直在误导(它既不是上下文窗口,也不是单一概念)。现在拆成两条**绝对值**:
177
- - `handoff`:软线,响应返回后 `prompt_tokens` 超过它触发任务交接(`AutoMigratePlugin`);
178
- - `guard`:硬线,请求前用量超过它抛 `ContextOverflowError` 中断本轮(`ContextGuardPlugin`)。
179
- - `maxContextChars` 直接淘汰(字符口径早于 token 口径,两者并存只会让「按哪个判」长期含糊)。
180
- - 迁移由 `config/migrate.ts` 完成,`ConfigManager.read()` 读到即迁、`ensureDefaultConfig()` 检测差异才写盘:`maxContextTokens` → `handoff`,`maxContextChars` → `floor(/4)`,缺 `guard` 时按 1.5 倍折算一次(保持迁移前行为),旧键随即删除。
181
- - **`ContextGuardPlugin` 的 `ratio` / `suspendedRatio` / `isSuspended` 全部删除** —— 它们只是「相对阈值」的产物。现在 `new ContextGuardPlugin({ guardTokens })` 一个绝对数就够:想调松就把数字改大。代价是**交接回合不再有 2.0 倍的额外放宽**,若需要该缓冲请把 `guard` 配得更大(出厂取 `handoff × 1.5`,并写成整数)。
182
- - **`ContextOverflowError(promptTokens, guardTokens)`** —— 去掉 `maxTokens` / `ratio` / `threshold` 三个派生字段,改为直接暴露被越过的 `guardTokens`。
183
- - **`AutoMigratePlugin` 的 `maxTokens` → `handoffTokens`**,`AutoMigrateTriggerInfo.maxTokens` → `handoffTokens`,`HandoffService` 的 `HandoffParams.maxTokens` / `HandoffContext.maxTokens` → `handoffTokens`(仅字段改名,语义不变:交接阈值)。
208
+ - **`Model.maxContextTokens` / `maxContextChars` → `Model.tokenThresholds: { handoff, guard }`** — The old names were misleading all along (they were neither a context window nor a single concept). Now split into two **absolute values**:
209
+ - `handoff`: the soft line — once `prompt_tokens` after a response returns exceeds it, task handoff is triggered (`AutoMigratePlugin`);
210
+ - `guard`: the hard line — if usage before a request exceeds it, `ContextOverflowError` is thrown and the turn is aborted (`ContextGuardPlugin`).
211
+ - `maxContextChars` was dropped outright (the character metric predates the token metric; keeping both only leaves "which one decides" ambiguous for good).
212
+ - Migration is performed by `config/migrate.ts`: `ConfigManager.read()` migrates on read, `ensureDefaultConfig()` writes to disk only when a difference is detected: `maxContextTokens` → `handoff`, `maxContextChars` → `floor(/4)`, and when `guard` is missing it is derived once as 1.5x (preserving pre-migration behavior); the old keys are removed right after.
213
+ - **All of `ContextGuardPlugin`'s `ratio` / `suspendedRatio` / `isSuspended` were removed** — they were merely artifacts of "relative thresholds". Now `new ContextGuardPlugin({ guardTokens })` with one absolute number is enough: to loosen it, just make the number larger. The cost is that **handoff turns no longer get the extra 2.0x relaxation**; if that buffer is needed, configure a larger `guard` (the default is `handoff × 1.5`, written as an integer).
214
+ - **`ContextOverflowError(promptTokens, guardTokens)`** — dropped the three derived fields `maxTokens` / `ratio` / `threshold` and now exposes the exceeded `guardTokens` directly.
215
+ - **`AutoMigratePlugin`'s `maxTokens` → `handoffTokens`**, `AutoMigrateTriggerInfo.maxTokens` → `handoffTokens`, and `HandoffService`'s `HandoffParams.maxTokens` / `HandoffContext.maxTokens` → `handoffTokens` (field rename only, semantics unchanged: the handoff threshold).
184
216
 
185
- ### ✨ 新增
217
+ ### 🚀 New Features
186
218
 
187
- - **思考强度(reasoning effort)能力位** —— 档位此前完全没有建模,CLI 只能手改 `defaultParams`,而各家端点接受的值域并不相同(智谱 GLM-5.3 系对 `medium` 直接报错、对 `thinking.type = "disabled"` 也报错)。现在分三层:
188
- - `Model.reasoning: { efforts: ReasoningEffort[], disable?: "thinking" | "effort-none" }` —— **物理能力上限**,按厂商官方文档声明(防 400);
189
- - `AgentDefinition.reasoningEffort?: ReasoningEffort` —— 该 Agent 的实际档位,在 `createModel` 阶段覆盖模型 `defaultParams`。于是同一模型可以给不同 Agent 配不同强度(编程 Agent `max`、闲聊 Agent `low`、闲聊小号 `off`)。
190
- - 刻意**没有**做 Agent 侧的档位白名单(`reasoningEfforts`):它解决的是伪需求(真正会 400 的是端点能力,那已由模型声明兜住),却引入「差一个 s」的命名陷阱与一层交集规则。
191
- - **`shared/reasoning.ts`**(公共 API,均从包入口导出):`resolveReasoningEffort()`、`resolveModelParams()`、`applyReasoningEffort()`(按声明写入,非法档位抛错)、`modelEfforts()` / `modelAllowedEfforts()` / `readReasoningEffort()` / `isReasoningEffort()` / `capabilityOf()`,以及 `ReasoningEffort` / `ReasoningCapability` 类型。
192
- - **写入形态由声明决定**:`disable: "thinking"` → 同时维护 `thinking.type` 与 `reasoning_effort`(DeepSeek);`disable: "effort-none"` → 只写 `reasoning_effort`(OpenAI 没有 `thinking` 参数,写它会被当未知参数);未声明 `disable` → 只写 `reasoning_effort`,`off` 直接抛错(智谱 5.3 系)。
193
- - **出厂模型按官方文档声明能力**(依据已写入 `constants.ts` 注释):
194
- - `deepseek-flash` → `low/high/max`,可关闭(`api-docs.deepseek.com/guides/thinking_mode`)
195
- - `glm-5.3` / `glm-5.3-flash` / `glm-5.3-flashx` → `low/high/max`,**不可关闭**(`docs.bigmodel.cn/cn/guide/capabilities/thinking`)
196
- - `glm-4.7-flash` → 不可调(强制思考,`reasoning_effort` 仅 5.2+ 支持)
197
- - `gpt-6-astra` / `gpt-6.1-sol` / `gpt-6-luna` → `minimal/low/medium/high`,可用 `reasoning_effort: "none"` 关闭
198
- - 未声明的模型一律视为不可调档,不推测
219
+ - **Reasoning effort capability bit** — effort levels were previously not modeled at all; the CLI could only hand-edit `defaultParams`, while each endpoint accepts a different value range (Zhipu's GLM-5.3 family errors out directly on `medium`, and also errors on `thinking.type = "disabled"`). Now there are three layers:
220
+ - `Model.reasoning: { efforts: ReasoningEffort[], disable?: "thinking" | "effort-none" }` — the **physical capability ceiling**, declared per the vendor's official docs (to avoid 400s);
221
+ - `AgentDefinition.reasoningEffort?: ReasoningEffort` — the actual effort level for that Agent, overriding the model's `defaultParams` at the `createModel` stage. So the same model can be given different strengths for different Agents (coding Agent `max`, chit-chat Agent `low`, chit-chat mini `off`).
222
+ - Deliberately **did not** add an Agent-side effort whitelist (`reasoningEfforts`): it solves a non-problem (what actually causes 400s is endpoint capability, and that is already covered by the model declaration), yet introduces a "one s apart" naming trap and an extra layer of intersection rules.
223
+ - **`shared/reasoning.ts`** (public API, all exported from the package entry): `resolveReasoningEffort()`, `resolveModelParams()`, `applyReasoningEffort()` (writes per the declaration, throws on invalid levels), `modelEfforts()` / `modelAllowedEfforts()` / `readReasoningEffort()` / `isReasoningEffort()` / `capabilityOf()`, plus the `ReasoningEffort` / `ReasoningCapability` types.
224
+ - **Write shape is decided by the declaration**: `disable: "thinking"` → maintains both `thinking.type` and `reasoning_effort` (DeepSeek); `disable: "effort-none"` → writes only `reasoning_effort` (OpenAI has no `thinking` parameter, and writing it would be treated as an unknown parameter); no `disable` declared → writes only `reasoning_effort`, and `off` throws directly (Zhipu 5.3 family).
225
+ - **Factory models declare capabilities per the official docs** (the basis is recorded in the `constants.ts` comments):
226
+ - `deepseek-flash` → `low/high/max`, can be disabled (`api-docs.deepseek.com/guides/thinking_mode`)
227
+ - `glm-5.3` / `glm-5.3-flash` / `glm-5.3-flashx` → `low/high/max`, **cannot be disabled** (`docs.bigmodel.cn/cn/guide/capabilities/thinking`)
228
+ - `glm-4.7-flash` → not adjustable (thinking is forced; `reasoning_effort` is supported only in 5.2+)
229
+ - `gpt-6-astra` / `gpt-6.1-sol` / `gpt-6-luna` → `minimal/low/medium/high`, can be disabled with `reasoning_effort: "none"`
230
+ - Models with no declaration are all treated as non-adjustable; no guessing
199
231
 
200
- ### 🔧 变更
232
+ ### 🔧 Changed
201
233
 
202
- - **`createModel(scope, modelId)` → `createModel(scope, modelId, overrides?: Pick<AgentDefinition, "reasoningEffort">)`** —— 新增的可选第三参由 `createAgent` 传入 `definition.reasoningEffort`。不传时行为与以前完全一致(原样返回模型 `defaultParams`)。
234
+ - **`createModel(scope, modelId)` → `createModel(scope, modelId, overrides?: Pick<AgentDefinition, "reasoningEffort">)`** — the new optional third parameter is passed in by `createAgent` as `definition.reasoningEffort`. When omitted, behavior is exactly as before (returns the model's `defaultParams` unchanged).
203
235
 
204
- ### ✅ 测试
236
+ ### ✅ Tests
205
237
 
206
- - 新增 `shared/reasoning.test.ts` 22 例:能力声明与出厂清单一致性、`off` 只对有 `disable` 的模型开放、GLM-5.3 设 `medium` 与 `off` 均抛错、OpenAI 永不写 `thinking` 键、往返一致性、Agent 覆盖与回退。
207
- - 新增 `config/migrate.test.ts` 13 例:旧字段迁移(tokens / chars / 两者 / 都无 / 脏值)、`guard` 折算、已是新形状时不迁移、不改入参、部分迁移时未变条目保持同一引用。
208
- - 全量 **57 文件 / 599 用例** 通过(6 skipped),`tsc --noEmit` 与 `build` 零错误。
238
+ - Added 22 cases in `shared/reasoning.test.ts`: capability declarations match the factory list, `off` is available only for models with `disable`, GLM-5.3 throws for both `medium` and `off`, OpenAI never writes a `thinking` key, round-trip consistency, Agent override and fallback.
239
+ - Added 13 cases in `config/migrate.test.ts`: old-field migration (tokens / chars / both / neither / dirty values), `guard` derivation, no migration when already in the new shape, input arguments are not mutated, and untouched entries keep the same reference during partial migration.
240
+ - Full suite **57 files / 599 cases** passing (6 skipped), `tsc --noEmit` and `build` with zero errors.
209
241
 
210
242
  ### 📄 Docs
211
243
 
212
- - `docs/{zh,en}/sdk.md` 新增「思考强度(reasoning effort)」小节(三层模型、出厂声明表、写入形态、公共 API),核心实体表补充 `Model.reasoning` 与 Agent 的两个档位字段。
244
+ - `docs/{zh,en}/sdk.md` added a "Reasoning effort" section (the three-layer model, the factory declaration table, write shapes, public API), and the core entity table now includes `Model.reasoning` and the Agent's two effort fields.
213
245
 
214
246
  ## [1.0.0-alpha.12] - 2026-10-22
215
247
 
216
248
  ### 💥 Breaking Changes
217
249
 
218
- - **`createReadMcpResourceTool(mcpManager)` → `createReadMcpResourceTool(mcpManager, env: ToolEnv)`** —— 与 alpha.11 的 `createCallMcpTool` 对齐:`env` 用于读取 `maxToolOutput` 并解析当前模型绑定的端点。直接调用该工厂的下游需补第二参。
219
- - **`view_image` 不再依赖 `deepseek` 端点** —— 此前本地图片固定上传到 `id === "deepseek"` 的端点,未配置时直接报「未配置 deepseek 端点,无法上传本地图片」。现在按能力位决定:声明 `capabilities.filesApi` 的端点走上传,其余走 base64 内联,没有 deepseek 也能查看本地图片。返回文案随之变化(含传输方式与降级说明)。
220
- - **`Endpoint` 新增可选字段 `capabilities`** —— 类型层面非破坏性(可选),但语义重要:缺省视为「无特殊能力」,即不做上传。
250
+ - **`createReadMcpResourceTool(mcpManager)` → `createReadMcpResourceTool(mcpManager, env: ToolEnv)`** — aligned with `createCallMcpTool` from alpha.11: `env` is used to read `maxToolOutput` and to resolve the endpoint bound to the current model. Downstream code calling this factory directly must add the second argument.
251
+ - **`view_image` no longer depends on the `deepseek` endpoint** — previously local images were always uploaded to the endpoint with `id === "deepseek"`, and when it was not configured it failed with "no deepseek endpoint configured, cannot upload local images". Now it is decided by capability bit: endpoints declaring `capabilities.filesApi` go through upload, all others go through base64 inlining, so local images can be viewed without deepseek. The returned message changed accordingly (including the transport and the fallback notes).
252
+ - **`Endpoint` gained the optional field `capabilities`** — non-breaking at the type level (optional), but semantically important: absent means "no special capabilities", i.e. no upload.
221
253
 
222
- ### ✨ 新增
254
+ ### 🚀 New Features
223
255
 
224
- - **「本地文件 → 内容块」统一入口 `shared/content-blocks.ts`**。这段逻辑此前散落三处各自为政(`ViewImageTool` 硬编码端点 id、MCP 媒体块归一、CLI 侧再实现一遍),现在收敛为一处:
225
- - `fileToContentSection({ source, endpoint, model, signal, maxInlineBytes, deps })` → `{ section, transport, bytes, warning? }`;`source` 支持 `path` / `url` / `data`(`Uint8Array`)/ `base64` 四种来源。
226
- - 决策链:http(s) URL → `image_url`;端点有 Files API → 上传得 `file_id`(失败**自动降级**为内联);其余端点 + 图片 → `image_url` + base64 data URL;其余端点 + 非图片 → `file` 块 + `file_data`;非视觉模型 + 图片 → 文本提示;内联超限且无法上传 → 抛错(不静默塞进请求体)。
227
- - 配套:`resolveModelAndEndpoint()`(按发给 API 的模型名反查模型与端点)、`supportsFilesApi()`、`mimeOf()` / `toDataUrl()` / `base64ByteLength()` / `contentHash()`、`clearUploadCache()`、`DEFAULT_MAX_INLINE_BYTES`(8 MiB)。
228
- - **已从包入口 `src/index.ts` 导出为公共 API**(`fileToContentSection` / `resolveModelAndEndpoint` / `supportsFilesApi` / `mimeOf` / `toDataUrl` / `base64ByteLength` / `contentHash` / `clearUploadCache` / `MIME_BY_EXT` / `DEFAULT_MAX_INLINE_BYTES`,以及 `FileSource` / `FileToSectionInput` / `FileToSectionResult` / `ContentTransport` / `UploadFn` / `EndpointCapabilities` 类型)。上层应用构造媒体消息时直接复用,不要各自再实现一遍「上传 or 内联」。
229
- - 上传缓存:路径来源用「端点 id + 绝对路径 + mtime + size」,内存来源用内容 sha256(同一张图不重复上传),上限 200 条。
230
- - `deps.upload` 可注入,便于测试与替换上传实现。
231
- - **`Endpoint.capabilities.filesApi` 能力位** —— 出厂仅 `deepseek` 标记为 `true`(description 同步说明)。判据从「端点 id 是不是 deepseek」改为能力位,用户自建端点只要声明即可享受上传。
256
+ - **Single unified entry point for "local file → content block": `shared/content-blocks.ts`**. This logic was previously scattered across three places each doing its own thing (`ViewImageTool` hardcoding the endpoint id, MCP media block normalization, and yet another implementation on the CLI side); it is now consolidated into one place:
257
+ - `fileToContentSection({ source, endpoint, model, signal, maxInlineBytes, deps })` → `{ section, transport, bytes, warning? }`; `source` supports four kinds of sources: `path` / `url` / `data` (`Uint8Array`) / `base64`.
258
+ - Decision chain: http(s) URL → `image_url`; endpoint has Files API → upload to get a `file_id` (**falls back automatically** to inlining on failure); other endpoints + image → `image_url` + base64 data URL; other endpoints + non-image → `file` block + `file_data`; non-vision model + image → text hint; inline over the limit and upload impossible → throw (never silently stuffed into the request body).
259
+ - Companion helpers: `resolveModelAndEndpoint()` (looks up the model and endpoint by the model name sent to the API), `supportsFilesApi()`, `mimeOf()` / `toDataUrl()` / `base64ByteLength()` / `contentHash()`, `clearUploadCache()`, `DEFAULT_MAX_INLINE_BYTES` (8 MiB).
260
+ - **Exported from the package entry `src/index.ts` as public API** (`fileToContentSection` / `resolveModelAndEndpoint` / `supportsFilesApi` / `mimeOf` / `toDataUrl` / `base64ByteLength` / `contentHash` / `clearUploadCache` / `MIME_BY_EXT` / `DEFAULT_MAX_INLINE_BYTES`, plus the `FileSource` / `FileToSectionInput` / `FileToSectionResult` / `ContentTransport` / `UploadFn` / `EndpointCapabilities` types). Upper-layer applications should reuse this directly when building media messages instead of each reimplementing "upload or inline".
261
+ - Upload cache: path sources key on "endpoint id + absolute path + mtime + size", in-memory sources key on the content sha256 (the same image is never uploaded twice), capped at 200 entries.
262
+ - `deps.upload` is injectable, which makes testing easier and lets the upload implementation be swapped.
263
+ - **`Endpoint.capabilities.filesApi` capability bit** — only `deepseek` is marked `true` out of the box (the description says so as well). The criterion changed from "is the endpoint id deepseek" to the capability bit, so user-built endpoints get upload just by declaring it.
232
264
 
233
- ### 🔧 修复
265
+ ### 🔧 Fixed
234
266
 
235
- - **MCP 媒体不再一律 base64 内联**:`call_mcp_tool` 的 `image` / `audio` / `resource.blob` 改走统一入口,端点有 Files API 能力时上传后以 `file_id` 发送,请求体不再随历史反复膨胀;转换失败**不丢块**,退化为一条文本说明,避免整个工具调用因媒体问题失败。
236
- - **`read_mcp_resource` 的二进制资源被静默丢弃**:此前只拼 `contents[].text`,图片 / PDF 等 `blob` 内容直接消失。新增 `resourceToMessageSection()`——注意 `readResource` 的 contents 形如 `{ uri, mimeType?, text|blob }`,**没有 `type` 判别字段**,不能复用 `toMessageSection`(否则文本资源会被 JSON 化);`blob` 交给统一入口。
237
- - **`read_mcp_resource` 接入输出保护**:与 `call_mcp_tool` 共用同一套口径——文本超限落盘 `result.txt` 并返回头尾预览,媒体块原位保留;返回形态同样是「仅文本 → 字符串 / 含媒体 → 内容块数组」。
238
- - **非视觉模型退化**:`Model.vision !== true` 时图片退化为一行文本提示(本地路径来源与 MCP 内存 base64 来源均生效),避免 `image_url` 被服务端 4xx 拒掉。
239
- - **`edit` 并发护栏**:`edit` / `batch_edit` 工具描述明确「同一文件禁止并行 `edit`,多处修改改用 `batch_edit`,只有不同文件才能并行」——描述层劝阻(不引入运行时锁)。
267
+ - **MCP media is no longer always base64-inlined**: `image` / `audio` / `resource.blob` from `call_mcp_tool` now go through the unified entry point; when the endpoint has Files API capability they are uploaded and sent as `file_id`, so the request body no longer keeps ballooning with history; a failed conversion **does not drop the block** but degrades to a text note, so the whole tool call does not fail over a media problem.
268
+ - **Binary resources from `read_mcp_resource` were silently dropped**: previously only `contents[].text` was concatenated, so `blob` content such as images / PDFs simply vanished. Added `resourceToMessageSection()` — note that `readResource`'s contents look like `{ uri, mimeType?, text|blob }` and have **no `type` discriminator field**, so `toMessageSection` cannot be reused (otherwise text resources would be JSON-ified); `blob` is handed to the unified entry point.
269
+ - **`read_mcp_resource` now has output protection**: it shares the same rules as `call_mcp_tool` — text over the limit is written to disk as `result.txt` and a head/tail preview is returned, while media blocks stay in place; the return shape is likewise "text only → string / contains media → content block array".
270
+ - **Non-vision model degradation**: when `Model.vision !== true`, images degrade to a one-line text hint (applies to both local-path sources and MCP in-memory base64 sources), so `image_url` is not rejected by the server with a 4xx.
271
+ - **`edit` concurrency guardrail**: the `edit` / `batch_edit` tool descriptions state explicitly that "parallel `edit` on the same file is forbidden; use `batch_edit` for multiple changes; only different files may be parallelized" — a description-level deterrent (no runtime lock introduced).
240
272
 
241
- ### ✅ 测试
273
+ ### ✅ Tests
242
274
 
243
- - 新增 `shared/content-blocks.test.ts` 16 例;`mcpTools.test.ts` 扩展覆盖媒体统一入口、非视觉退化、`read` 超限落盘与混排保序;`ViewImageTool` / `EditTool` / `BatchEditTool` 断言同步到新语义。
244
- - 全量 **55 文件 / 569 用例**通过(6 skipped),`tsc --noEmit` 与 `build` 零错误。
275
+ - Added 16 cases in `shared/content-blocks.test.ts`; `mcpTools.test.ts` was extended to cover the unified media entry point, non-vision degradation, `read` over-limit disk writes, and order preservation in mixed content; the assertions in `ViewImageTool` / `EditTool` / `BatchEditTool` were synced to the new semantics.
276
+ - Full suite **55 files / 569 cases** passing (6 skipped), `tsc --noEmit` and `build` with zero errors.
245
277
 
246
278
  ### 📄 Docs
247
279
 
248
- - `docs/{zh,en}/sdk.md` 新增「本地文件 → 内容块(统一入口)」小节、`Endpoint` 能力位与 MCP 媒体 / 退化 / 输出保护说明。
249
- - `docs/{zh,en}/mcp.md` 同步 `call_mcp_tool` / `read_mcp_resource` 的媒体传输方式与输出保护口径。
280
+ - `docs/{zh,en}/sdk.md` added a "Local file → content block (unified entry point)" section, plus the `Endpoint` capability bit and notes on MCP media / degradation / output protection.
281
+ - `docs/{zh,en}/mcp.md` synced on the media transport and output protection rules for `call_mcp_tool` / `read_mcp_resource`.
250
282
 
251
283
 
252
284
  ## [1.0.0-alpha.11] - 2026-10-20
253
285
 
254
286
  ### 💥 Breaking Changes
255
287
 
256
- - **`createCallMcpTool(mcpManager)` → `createCallMcpTool(mcpManager, env: ToolEnv)`** —— 新增的 `env` 只用于读取 `maxToolOutput`,由 `McpScopePlugin.instantiate` 传入 `scope.env`。直接调用该工厂的下游需补第二参。
288
+ - **`createCallMcpTool(mcpManager)` → `createCallMcpTool(mcpManager, env: ToolEnv)`** — the new `env` is used only to read `maxToolOutput`, and is passed in by `McpScopePlugin.instantiate` as `scope.env`. Downstream code calling this factory directly must add the second argument.
257
289
 
258
- ### ✨ 新增
290
+ ### 🚀 New Features
259
291
 
260
- - **`call_mcp_tool` 输出保护**:MCP 工具是最容易一次吐出上百 KB 甚至上 MB JSON 的通道,此前完全裸返回。现在文本部分超过 `maxToolOutput`(默认 32768 字符)时,完整文本落盘为 `result.txt`,只返回警告 + 头尾预览(`outputTooLarge` / `chars` / `head` / `tail` / `files`)。复用内置工具同一套 `outputGuard` 骨架与同一配置口径,不另立阈值。
261
- - **媒体内容块原样透传**:`image` → `image_url`(`data:<mimeType>;base64,...`)、`audio` → `file`(`file_data` 内联 data URL)、`resource.blob` 按 mimeType 分流到 `image_url` / `file`。媒体**不参与字符计数、不落盘、不丢弃**——只对文本做保护。
262
- - **返回形态**:仅文本 → 纯字符串(与历史一致);含媒体 → 内容块数组。
292
+ - **`call_mcp_tool` output protection**: MCP tools are the channel most likely to spit out hundreds of KB or even several MB of JSON in one go, and previously it was returned completely raw. Now when the text portion exceeds `maxToolOutput` (default 32768 characters), the full text is written to disk as `result.txt` and only a warning plus a head/tail preview are returned (`outputTooLarge` / `chars` / `head` / `tail` / `files`). It reuses the same `outputGuard` skeleton and the same configuration rules as the built-in tools; no separate threshold.
293
+ - **Media content blocks are passed through as-is**: `image` → `image_url` (`data:<mimeType>;base64,...`), `audio` → `file` (`file_data`, inlined data URL), `resource.blob` routed by mimeType to `image_url` / `file`. Media **does not count toward the character count, is not written to disk, and is never dropped** — only text is protected.
294
+ - **Return shape**: text only → plain string (consistent with the past); contains media → content block array.
263
295
 
264
- ### 🔧 修复
296
+ ### 🔧 Fixed
265
297
 
266
- - **非文本内容块被静默丢弃**:旧实现只取 `type === "text"`,`image` / `audio` / `resource` / `resource_link` 全部被扔掉,模型只知道「有个块没给我」。现在媒体透传、`resource.text` 并入正文、`resource_link` 转为 `[资源链接] name · uri · mimeType` 线索、未知类型(如 `tool_use`)退化为 JSON 文本。
267
- - **内容块顺序**:严格按 MCP `content[]` 原始顺序逐块映射,文本夹在图片中间也保持原位。超限时只保留一个警告块占据原**首个**文本块的位置,其余文本落盘、媒体原位不动——即使文本夹在两图之间,图注与图片的对应关系也不会被打乱。
268
- - **判定口径**:用全部文本的字符**总数**判超限,不逐块判(否则 N 段各 3 万字符都不超限、合起来却能灌爆)。
298
+ - **Non-text content blocks were silently dropped**: the old implementation only took `type === "text"`, so `image` / `audio` / `resource` / `resource_link` were all thrown away, leaving the model knowing only that "a block was not given to me". Now media is passed through, `resource.text` is merged into the body, `resource_link` becomes a `[resource_link] name · uri · mimeType` clue, and unknown types (such as `tool_use`) degrade to JSON text.
299
+ - **Content block order**: blocks are mapped strictly one by one in the original order of MCP `content[]`, so text sandwiched between images also keeps its position. When over the limit, a single warning block takes the place of the original **first** text block, the remaining text goes to disk, and media stays exactly where it was — even if text sits between two images, the correspondence between captions and images is not scrambled.
300
+ - **Threshold rule**: the limit is judged on the **total** character count of all text, not block by block (otherwise N segments of 30k characters each would all pass individually while together they could still flood the context).
269
301
 
270
- ### ✅ 测试
302
+ ### ✅ Tests
271
303
 
272
- - `mcpTools.test.ts` 38 例(新增内容块映射 8 例、输出保护 4 例、保序 4 例)。全量 **54 文件 / 542 用例**通过,`tsc --noEmit` 与 `build` 零错误。
304
+ - `mcpTools.test.ts` 38 cases (8 new for content block mapping, 4 for output protection, 4 for order preservation). Full suite **54 files / 542 cases** passing, `tsc --noEmit` and `build` with zero errors.
273
305
 
274
306
  ### 📄 Docs
275
307
 
276
- - `docs/sdk-design-v1.md` §9 `call_mcp_tool` 补充内容块映射表、输出保护口径与超限保序折叠规则;`../docs/{zh,en}/mcp.md` 及网站镜像同步。
308
+ - `docs/sdk-design-v1.md` §9 on `call_mcp_tool` gained the content block mapping table, the output protection rules, and the folding rule that preserves order when over the limit; `../docs/{zh,en}/mcp.md` and the website mirror were synced.
277
309
 
278
310
 
279
311
  ## [1.0.0-alpha.10] - 2026-10-20
280
312
 
281
313
  ### 💥 Breaking Changes
282
314
 
283
- - **`TaskMigrationService` → `HandoffService`,并拆成 `handoff()` / `migrate()` 两层** —— 「交接」(写交接文档)与「迁移」(交接 + 开新会话)此前混在一个方法里,现在概念与 API 一一对应:
284
- - `handoff(params)`:停工 → 核实 → 写文档 → 校验(失败降级)。**不改动会话**;
285
- - `migrate(params)`:`handoff()` 之后按 `strategy` 清历史并注入断点消息。
286
- - 类型随迁:`MigrationContext` → `HandoffContext`(新增 `mode` / `requestedMode` / `handoffPath` / `migrating`),`TaskMigrationServiceOptions` → `HandoffServiceOptions`(新增 `handoffDir` / `mode` / `skillDirs` / `minHandoffBytes`)。
287
- - 钩子重命名:`onBeforeMigrate` → `onBeforeHandoff`,新增 `onHandoff`(文档写好、清历史之前)。
288
- - **交接文档改由「原 agent 在自己会话里写」(`mode: "self"`,默认)**,而非另起一次独立的摘要调用。原实现把历史序列化成 `[role]: content` 后交给一个无工具、无原会话上下文的调用,工具名与调用参数在序列化时全部丢失,写文档的一方也无法核实分支/改动/测试状态,只能靠推演——这是文档质量差的根因。现在的 `self` 回合:原 agent 保留全部上下文与工具,可 `read` / `exec` / `git status` 核实后落盘。
289
- - **`summary`(旧的独立摘要)保留为逃生模式**,并作为 `self` 失败时的**单向**降级目标(无 `write` 工具 / 文件缺失或过小(重试一次)/ 工具或护栏报错)。反向永不发生——那会在上下文最满的时刻再加一次全量请求。
290
- - **`AutoMigrateOptions` 新增 `resolveMode`**,把「用谁交接」从插件继承转移到回调:返回 `HandoffMode` 或 `null`(跳过本次迁移)。原 CLI 的 `AutoMigrateConfirmPlugin` 子类因此退化为纯回调。
315
+ - **`TaskMigrationService` → `HandoffService`, split into two layers, `handoff()` / `migrate()`** — "handoff" (writing a handoff document) and "migration" (handoff + starting a new session) used to be mixed into one method; now the concepts and the API map one-to-one:
316
+ - `handoff(params)`: stop work → verify → write the document → validate (fall back on failure). **Does not touch the session**;
317
+ - `migrate(params)`: after `handoff()`, clears history and injects the breakpoint message according to `strategy`.
318
+ - Types moved along: `MigrationContext` → `HandoffContext` (added `mode` / `requestedMode` / `handoffPath` / `migrating`), `TaskMigrationServiceOptions` → `HandoffServiceOptions` (added `handoffDir` / `mode` / `skillDirs` / `minHandoffBytes`).
319
+ - Hooks renamed: `onBeforeMigrate` → `onBeforeHandoff`; added `onHandoff` (after the document is written, before history is cleared).
320
+ - **Handoff documents are now written by "the original agent in its own session" (`mode: "self"`, the default)**, rather than by a separate standalone summary call. The old implementation serialized the history into `[role]: content` and handed it to a call with no tools and no context of the original session, so tool names and call arguments were all lost during serialization, and the writing side could not verify branches/changes/test status — it could only infer them — which was the root cause of the poor document quality. The `self` turn today: the original agent keeps its full context and tools and can `read` / `exec` / `git status` to verify before writing to disk.
321
+ - **`summary` (the old standalone summary) is kept as an escape mode**, and serves as the **one-way** fallback target when `self` fails (no `write` tool / file missing or too small (retry once) / tool or guard error). The reverse never happens — that would add one more full request at the moment the context is the fullest.
322
+ - **`AutoMigrateOptions` gained `resolveMode`**, moving "who performs the handoff" from plugin inheritance to a callback: it returns a `HandoffMode` or `null` (skip this migration). The CLI's `AutoMigrateConfirmPlugin` subclass therefore degrades into a pure callback.
291
323
 
292
- ### ✨ 新增
324
+ ### 🚀 New Features
293
325
 
294
- - **`HandoffGuardPlugin`**:交接回合的一次性工具守卫。只放行 `write` 到交接文档(绝对路径归一后比较),拒绝 `edit` / `batch_edit` / `rm` / `rename` / `mkdir` / `copy`。拒绝返回字符串(软拒绝)而非抛错——抛错会被 core 标记为 Error + `isPreventDefault`,整轮交接直接死。边界如实记录:挡不住 `exec`,它降低误操作概率,不是沙箱。
295
- - **`ContextGuardPlugin` 新增 `isSuspended` / `suspendedRatio`**:交接回合(`service.busy`)把硬上限从 `× 1.5` 放宽到 `× 2`,既不被误杀又保留熔断。
296
- - **出厂 Skill 机制**:`ConfigManager.ensureFactorySkills()` 在 `bootstrap()` 中释放 / 同步 `skills/handoff/SKILL.md`。覆盖策略与 `syncManagedModels` 同构——frontmatter 标 `metadata.custom: "true"` 视为用户自有、永不覆盖;否则随版本同步。内容以字符串常量内联在 `config/factory-skills.ts`(`tsc` 直接编译,无需打包非 TS 资源)。
297
- - **交接模板的唯一真相源改为 Skill**:`HANDOFF_SECTIONS` / `createPrompt()` 已删除,代码只保留「读 Skill → 注入 → 校验文件 → 读不到时用 `FALLBACK_HANDOFF_TEMPLATE`」。Skill 只讲「怎么写」,代码只讲「写到哪、有什么能力」(本体系无模板变量引擎,不发明占位符)。
298
- - `HandoffService.createHandoffFileName()`(`<YYYYMMDD-HHmmss>-<会话id前8位>.md`)、`createHandoffInstruction()`、`createPostMessages(handoffPath)`(断点**只给路径**,由接手方 `read`)。
326
+ - **`HandoffGuardPlugin`**: a one-shot tool guard for the handoff turn. It only allows `write` to the handoff document (compared after normalizing to an absolute path) and rejects `edit` / `batch_edit` / `rm` / `rename` / `mkdir` / `copy`. A rejection returns a string (soft rejection) rather than throwing — a thrown error would be marked by core as Error + `isPreventDefault`, killing the whole handoff turn outright. The boundary is recorded honestly: it cannot stop `exec`; it lowers the probability of mistakes, it is not a sandbox.
327
+ - **`ContextGuardPlugin` gained `isSuspended` / `suspendedRatio`**: during the handoff turn (`service.busy`) the hard limit is relaxed from `× 1.5` to `× 2`, so the turn is neither killed by mistake nor left without the circuit breaker.
328
+ - **Factory Skill mechanism**: `ConfigManager.ensureFactorySkills()` releases / syncs `skills/handoff/SKILL.md` in `bootstrap()`. The overwrite policy mirrors `syncManagedModels` — frontmatter marked `metadata.custom: "true"` is treated as user-owned and never overwritten; otherwise it syncs with the version. The content is inlined as a string constant in `config/factory-skills.ts` (`tsc` compiles it directly, no need to bundle non-TS assets).
329
+ - **The single source of truth for the handoff template is now the Skill**: `HANDOFF_SECTIONS` / `createPrompt()` are removed, and the code keeps only "read the Skill → inject → validate the file → fall back to `FALLBACK_HANDOFF_TEMPLATE` when it cannot be read". The Skill only covers "how to write"; the code only covers "where to write and what capabilities are available" (this system has no template variable engine, and placeholders are not invented).
330
+ - `HandoffService.createHandoffFileName()` (`<YYYYMMDD-HHmmss>-<first 8 chars of session id>.md`), `createHandoffInstruction()`, `createPostMessages(handoffPath)` (the breakpoint **gives only the path**, which the receiving side `read`s).
299
331
 
300
- ### 🔧 修复
332
+ ### 🔧 Fixed
301
333
 
302
- - **交接触发的无限递归**:交接回合本身是一次 `send()`,其 `onAfterSend` 会再次进入 `AutoMigratePlugin`。现以 `service.busy` 让行(`handoff()` / `migrate()` 共用一把锁,顺带拒绝重入)。
303
- - **交接回合被自己触发/被护栏击杀**:交接恰好发生在上下文最满的时刻。除 `busy` 让行外,交接回合开跑前清空 `agent.lastUsage`(纵深防御),并由调用方在迁移后清空(否则护栏拿交接回合用量误杀新会话第一条消息)。
334
+ - **Infinite recursion triggered by handoff**: the handoff turn is itself a `send()`, and its `onAfterSend` re-enters `AutoMigratePlugin`. It now yields on `service.busy` (`handoff()` / `migrate()` share one lock, which also rejects re-entry).
335
+ - **The handoff turn being triggered by itself / killed by the guard**: handoff happens exactly at the moment the context is the fullest. Besides yielding on `busy`, `agent.lastUsage` is cleared before the handoff turn starts (defense in depth) and cleared by the caller after migration (otherwise the guard would use the handoff turn's usage to kill the first message of the new session by mistake).
304
336
 
305
- ### ✅ 测试
337
+ ### ✅ Tests
306
338
 
307
- - 新增 `HandoffService.test.ts`(24 例)、`HandoffGuardPlugin.test.ts`(7 例)、`factory-skills.test.ts`(7 例);重写 `AutoMigratePlugin.test.ts`、`ContextGuardPlugin.test.ts`(新增 `isSuspended` 4 例)。全量 **54 文件 / 525 用例**通过,`tsc --noEmit` 与 `build` 零错误。
339
+ - Added `HandoffService.test.ts` (24 cases), `HandoffGuardPlugin.test.ts` (7 cases), `factory-skills.test.ts` (7 cases); rewrote `AutoMigratePlugin.test.ts`, `ContextGuardPlugin.test.ts` (added 4 `isSuspended` cases). The full suite of **54 files / 525 cases** passes, with zero errors from `tsc --noEmit` and `build`.
308
340
 
309
341
  ### 📄 Docs
310
342
 
311
- - `docs/sdk-design-v1.md` §18 重写为「任务交接(HandoffService)」(两层语义、Template 单一真相源、两种执行者、自交接三道保险、接口、断点只给路径);§17 插件章节、目录树与消费示例同步。
312
- - `README.md` / `README.zh.md`、`../docs/{en,zh}/sdk.md`、`architecture.md`、`getting-started.md` 及网站镜像同步。
343
+ - `docs/sdk-design-v1.md` §18 rewritten as "Task handoff (HandoffService)" (two-layer semantics, Template as the single source of truth, two kinds of executor, three safeguards for self-handoff, the interface, breakpoint gives only the path); the §17 plugin chapter, the directory tree and the consumption examples were synced.
344
+ - `README.md` / `README.zh.md`, `../docs/{en,zh}/sdk.md`, `architecture.md`, `getting-started.md` and the website mirror synced.
313
345
 
314
346
 
315
347
  ## [1.0.0-alpha.9] - 2026-10-14
@@ -382,7 +414,7 @@
382
414
 
383
415
  ### 🔧 Fixed
384
416
 
385
- - **`generateImage` `model` parameter description no longer points at a host-CLI command** — the description used to read "配置中的图片模型可通过 'aiz config show' 查看" (the configured image models can be viewed via `aiz config show`). Since tool descriptions ship inside every request's `tools` payload, that hint propagated into **every layer** of the agent stack: the model would `exec("aiz config show")`, but the CLI resolves a bare `aiz config show` invocation as stdio mode with the instruction `config show` — i.e. a **brand-new agent** — whose own `generateImage` description repeats the very same hint, producing a self-referential recursion that keeps spawning nested agents. The parameter now reads "若指定的模型不存在,本工具会返回当前可用的模型列表" (if the specified model does not exist, this tool returns the list of currently available models), which matches the actual `!imageModel` branch in `call()` and stops the model from shelling out to the host CLI.
417
+ - **`generateImage` `model` parameter description no longer points at a host-CLI command** — the description used to read "the configured image models can be viewed via `aiz config show`". Since tool descriptions ship inside every request's `tools` payload, that hint propagated into **every layer** of the agent stack: the model would `exec("aiz config show")`, but the CLI resolves a bare `aiz config show` invocation as stdio mode with the instruction `config show` — i.e. a **brand-new agent** — whose own `generateImage` description repeats the very same hint, producing a self-referential recursion that keeps spawning nested agents. The parameter now reads "if the specified model does not exist, this tool returns the list of currently available models", which matches the actual `!imageModel` branch in `call()` and stops the model from shelling out to the host CLI.
386
418
 
387
419
  ### 📄 Docs
388
420
 
@@ -412,7 +444,7 @@
412
444
  - **Zhipu**: `glm-5.1` / `glm-5v-turbo` → `glm-5.3` / `glm-5.3-flash` / `glm-5.3-flashx`
413
445
  - **DeepSeek**: `deepseek-v4-pro` / `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` → `deepseek-flash`. The two V4-Flash variants were retired by the vendor, and V4-Pro requests are routed to V4.1-Flash.
414
446
  - Defaults: `defaultModel` / `defaultMigrationModel` move from `deepseek-v4-flash` to `deepseek-flash`.
415
- - **`Model.maxContextTokens` semantics clarified — it is the task-migration (handoff) trigger threshold, not the model context window** — `AutoMigratePlugin` does not estimate or learn: it only compares `usage.prompt_tokens` from the API response against this value. The previous comment read "上下文窗口 token 上限" ("context window token limit"), which is why values had been filled in with raw window sizes. Values are now ~25% of the real window (250K for ≥1M-window models), except `glm-4.7-flash` (200K window) which uses 100K.
447
+ - **`Model.maxContextTokens` semantics clarified — it is the task-migration (handoff) trigger threshold, not the model context window** — `AutoMigratePlugin` does not estimate or learn: it only compares `usage.prompt_tokens` from the API response against this value. The previous comment read "context window token limit", which is why values had been filled in with raw window sizes. Values are now ~25% of the real window (250K for ≥1M-window models), except `glm-4.7-flash` (200K window) which uses 100K.
416
448
 
417
449
  ### 🚀 New Features
418
450