@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.
- package/CHANGELOG.md +216 -184
- package/README.md +13 -0
- package/README.zh.md +13 -0
- package/dist/agent-plugins/AutoMigratePlugin.js +1 -1
- package/dist/agent-plugins/UnknownToolHintPlugin.js +4 -4
- package/dist/config/constants.js +24 -24
- package/dist/config/factory-skills.js +56 -56
- package/dist/runtime/HandoffGuardPlugin.js +2 -2
- package/dist/runtime/HandoffService.js +22 -22
- package/dist/runtime/createAgent.js +2 -2
- package/dist/runtime/createModel.js +3 -3
- package/dist/scope/PermissionEvaluator.js +1 -1
- package/dist/scope/Scope.js +1 -1
- package/dist/scope/plugins/builtin/BatchEditTool.js +8 -8
- package/dist/scope/plugins/builtin/CopyTool.js +4 -4
- package/dist/scope/plugins/builtin/CwdTool.js +1 -1
- package/dist/scope/plugins/builtin/DownloadFileTool.js +8 -8
- package/dist/scope/plugins/builtin/EditTool.js +9 -9
- package/dist/scope/plugins/builtin/ExecTool.js +8 -8
- package/dist/scope/plugins/builtin/ExistTool.js +2 -2
- package/dist/scope/plugins/builtin/FindTextTool.js +10 -10
- package/dist/scope/plugins/builtin/GenerateImageTool.js +13 -13
- package/dist/scope/plugins/builtin/GlobTool.js +4 -4
- package/dist/scope/plugins/builtin/InspectFileTool.js +7 -7
- package/dist/scope/plugins/builtin/LsTool.js +5 -5
- package/dist/scope/plugins/builtin/MkdirTool.js +3 -3
- package/dist/scope/plugins/builtin/ReadTool.js +8 -8
- package/dist/scope/plugins/builtin/RenameTool.js +3 -3
- package/dist/scope/plugins/builtin/RmTool.js +3 -3
- package/dist/scope/plugins/builtin/SleepTool.js +6 -6
- package/dist/scope/plugins/builtin/SpawnTool.js +9 -9
- package/dist/scope/plugins/builtin/ViewImageTool.js +12 -12
- package/dist/scope/plugins/builtin/WriteTool.js +3 -3
- package/dist/scope/plugins/mcp/McpConnectionManager.js +7 -7
- package/dist/scope/plugins/mcp/discover.js +1 -1
- package/dist/scope/plugins/mcp/mcpTools.js +29 -29
- package/dist/scope/plugins/skills/discover.js +7 -7
- package/dist/scope/plugins/skills/skillTools.js +17 -17
- package/dist/scope/plugins/subagents/subAgentTools.js +2 -2
- package/dist/scope/plugins/usertools/discover.js +2 -2
- package/dist/shared/content-blocks.js +9 -9
- package/dist/shared/errors.js +3 -3
- 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
|
-
>
|
|
6
|
-
>
|
|
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`
|
|
11
|
-
- `abortedMessageStrategy?: "keep" | "drop"`
|
|
12
|
-
- `abortedToolRoundStrategy?: "fill" | "drop"`
|
|
13
|
-
-
|
|
14
|
-
- **`createAgent()`
|
|
15
|
-
-
|
|
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
|
-
-
|
|
20
|
-
- `migrate.test.ts` 20 → 22
|
|
21
|
-
- `createAgent.test.ts` 6 → 8
|
|
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
|
-
-
|
|
28
|
-
- `summary`
|
|
29
|
-
- `self`
|
|
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
|
-
|
|
63
|
+
`HandoffParams` now gains `signal?: AbortSignal`, honored by both executors:
|
|
32
64
|
|
|
33
|
-
- `summary
|
|
34
|
-
- `self
|
|
35
|
-
- `runHandoff()`
|
|
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
|
-
-
|
|
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
|
-
-
|
|
46
|
-
- `HandoffService.test.ts` **24 → 30
|
|
47
|
-
-
|
|
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?)`
|
|
54
|
-
- `abortedToolRoundStrategy?: "drop" | "fill"`
|
|
55
|
-
- `abortedToolRoundReason?: string`
|
|
56
|
-
- `abortedMessageStrategy?: "drop" | "keep"`
|
|
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
|
-
-
|
|
92
|
+
- Upgraded `@ai-zen/agents-core` to `4.5.3` (adds `abortedMessageStrategy` and `reconcileAbortedMessages()`).
|
|
61
93
|
|
|
62
|
-
### ✅
|
|
94
|
+
### ✅ Tests
|
|
63
95
|
|
|
64
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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`
|
|
75
|
-
- `scope/plugins/skills/discover.ts`
|
|
76
|
-
- `agent-plugins/AutoMigratePlugin.ts`
|
|
77
|
-
- `scope/plugins/skills/skillTools.ts`
|
|
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
|
-
|
|
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
|
-
-
|
|
84
|
-
-
|
|
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
|
-
-
|
|
91
|
-
- `
|
|
92
|
-
- `createAgent`
|
|
93
|
-
- `ViewImageTool.isAvailable` / `resolveModelAndEndpoint`
|
|
94
|
-
- **`DEFAULT_AGENT_DEFINITION.modelId = "deepseek-flash"
|
|
95
|
-
-
|
|
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
|
-
-
|
|
100
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
- **`AgentDefinition.reasoningEffort` → `AgentDefinition.modelParams?: ModelParams`**
|
|
122
|
-
- **`createModel(scope, modelId, overrides?: ModelParams)`**
|
|
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
|
-
|
|
127
|
-
- **`Model.reasoningPreset`
|
|
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
|
-
-
|
|
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`
|
|
136
|
-
- `src/config/migrate.test.ts`
|
|
137
|
-
-
|
|
138
|
-
-
|
|
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
|
-
-
|
|
145
|
-
-
|
|
146
|
-
- `Model.reasoning?` → **`Model.reasoningPreset?: string
|
|
147
|
-
- `AgentDefinition.reasoningEffort`
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
- `Scope`
|
|
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
|
-
|
|
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()`**
|
|
159
|
-
- **`presetOf(model, presets)` / `modelLevels(model, presets)`**
|
|
160
|
-
-
|
|
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`
|
|
165
|
-
- `src/config/reasoning-presets.test.ts`
|
|
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
|
|
170
|
-
-
|
|
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
|
|
178
|
-
- `guard
|
|
179
|
-
- `maxContextChars`
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
- **`ContextOverflowError(promptTokens, guardTokens)`**
|
|
183
|
-
- **`AutoMigratePlugin`
|
|
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
|
-
-
|
|
188
|
-
- `Model.reasoning: { efforts: ReasoningEffort[], disable?: "thinking" | "effort-none" }`
|
|
189
|
-
- `AgentDefinition.reasoningEffort?: ReasoningEffort`
|
|
190
|
-
-
|
|
191
|
-
- **`shared/reasoning.ts
|
|
192
|
-
-
|
|
193
|
-
-
|
|
194
|
-
- `deepseek-flash` → `low/high/max
|
|
195
|
-
- `glm-5.3` / `glm-5.3-flash` / `glm-5.3-flashx` → `low/high/max
|
|
196
|
-
- `glm-4.7-flash` →
|
|
197
|
-
- `gpt-6-astra` / `gpt-6.1-sol` / `gpt-6-luna` → `minimal/low/medium/high
|
|
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">)`**
|
|
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
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
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`
|
|
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)`**
|
|
219
|
-
- **`view_image`
|
|
220
|
-
- **`Endpoint`
|
|
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
|
-
-
|
|
225
|
-
- `fileToContentSection({ source, endpoint, model, signal, maxInlineBytes, deps })` → `{ section, transport, bytes, warning? }
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
229
|
-
-
|
|
230
|
-
- `deps.upload`
|
|
231
|
-
- **`Endpoint.capabilities.filesApi`
|
|
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
|
|
236
|
-
-
|
|
237
|
-
- **`read_mcp_resource`
|
|
238
|
-
-
|
|
239
|
-
- **`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
|
-
-
|
|
244
|
-
-
|
|
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`
|
|
249
|
-
- `docs/{zh,en}/mcp.md`
|
|
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)`**
|
|
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`
|
|
261
|
-
-
|
|
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
|
-
-
|
|
267
|
-
-
|
|
268
|
-
-
|
|
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
|
|
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`
|
|
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
|
|
284
|
-
- `handoff(params)
|
|
285
|
-
- `migrate(params)
|
|
286
|
-
-
|
|
287
|
-
-
|
|
288
|
-
-
|
|
289
|
-
- **`summary
|
|
290
|
-
- **`AutoMigrateOptions`
|
|
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
|
|
295
|
-
- **`ContextGuardPlugin`
|
|
296
|
-
-
|
|
297
|
-
-
|
|
298
|
-
- `HandoffService.createHandoffFileName()
|
|
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
|
-
-
|
|
303
|
-
-
|
|
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
|
-
-
|
|
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
|
|
312
|
-
- `README.md` / `README.zh.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 "
|
|
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 "
|
|
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
|
|