@ai-zen/agents-sdk 1.0.0-alpha.12 → 1.0.0-alpha.14
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 +74 -0
- package/dist/agent-plugins/AutoMigratePlugin.d.ts +7 -7
- package/dist/agent-plugins/AutoMigratePlugin.js +8 -8
- package/dist/agent-plugins/ContextGuardPlugin.d.ts +13 -25
- package/dist/agent-plugins/ContextGuardPlugin.js +13 -20
- package/dist/config/ConfigManager.d.ts +28 -1
- package/dist/config/ConfigManager.js +84 -9
- package/dist/config/constants.js +24 -11
- package/dist/config/migrate.d.ts +47 -0
- package/dist/config/migrate.js +89 -0
- package/dist/config/reasoning-presets.d.ts +29 -0
- package/dist/config/reasoning-presets.js +56 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -0
- package/dist/runtime/HandoffService.d.ts +2 -2
- package/dist/runtime/HandoffService.js +3 -3
- package/dist/runtime/createAgent.js +4 -1
- package/dist/runtime/createModel.d.ts +7 -1
- package/dist/runtime/createModel.js +9 -2
- package/dist/scope/Scope.d.ts +9 -1
- package/dist/scope/Scope.js +8 -0
- package/dist/shared/errors.d.ts +4 -8
- package/dist/shared/errors.js +8 -14
- package/dist/shared/reasoning.d.ts +62 -0
- package/dist/shared/reasoning.js +159 -0
- package/dist/types/index.d.ts +69 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-alpha.14] - 2026-10-22
|
|
4
|
+
|
|
5
|
+
### 🔧 行为变更(不兼容)
|
|
6
|
+
|
|
7
|
+
- **思考能力声明从「模型字段」改为「策略表」** —— alpha.13 那版用 `Model.reasoning: { efforts, disable? }` 表达厂商差异,结果三处结构性缺陷:`disable` 一词双义(既指关闭方式又指能力)、`off` 靠拼接而非声明(`disable ? ["off", ...efforts] : efforts`)、用全局 `ReasoningEffort` 枚举假装各家档位等价。现在改为**档位 → 请求参数补丁**的声明式表:
|
|
8
|
+
- 新增 `<basePath>/reasoning-presets.json`(与 `config.json` 同级),出厂内容见 `config/reasoning-presets.ts`;
|
|
9
|
+
- `Model.reasoning?` → **`Model.reasoningPreset?: string`**(引用策略名;缺省 = 不可调档);
|
|
10
|
+
- `AgentDefinition.reasoningEffort` 类型 `ReasoningEffort` → **`string`**(档位名由策略表定义,不再用假枚举假装统一);
|
|
11
|
+
- 删除 `ReasoningEffort` / `ReasoningCapability` 类型与 `capabilityOf` / `modelEfforts` / `modelAllowedEfforts` / `isReasoningEffort` 函数;
|
|
12
|
+
- 函数签名统一以 `(model, presets, …)` 开头:`readReasoningEffort(model, presets, params?)`、`applyReasoningEffort(model, presets, level, params?)`、`resolveReasoningEffort(model, presets, definition?)`、`resolveModelParams(model, presets, definition?)`;
|
|
13
|
+
- `Scope` 新增 `reasoningPresets`(与 `config` 平级注入,不做全局单例)。
|
|
14
|
+
|
|
15
|
+
换来三个结构性保证,全部来自表的形状、代码零分支:① 可选档位 = `Object.keys(levels)`;② 能否关闭思考 = 表里有没有 `off` 键;③ 「哪些参数键属于思考」= 整张表出现过的键的并集(写入前按此剔除旧值,`max` → `off` 不再残留 `reasoning_effort`,模型换策略也不会反刍上一套参数)。
|
|
16
|
+
|
|
17
|
+
- **`applyReasoningEffort` 的档位校验更严** —— 策略里没有该档位直接抛错(旧版对未声明能力的模型给的是「不支持档位」文案),错误信息带上策略名与可选档位列表。
|
|
18
|
+
|
|
19
|
+
### 🚀 新功能
|
|
20
|
+
|
|
21
|
+
- **`ConfigManager.readReasoningPresets()` / `ensureReasoningPresets()`** —— 策略表的读写与出厂同步。同步粒度**到单个策略**,规则与出厂模型同步同构:文件缺失即写种子;`custom: true` 的策略冻结;未标的用出厂版覆盖;用户自建的非出厂策略永远保留。`bootstrap()` 会自动调用。文件缺失或损坏时 `readReasoningPresets()` 回落到出厂种子,不让上层拿到空表。
|
|
22
|
+
- **`presetOf(model, presets)` / `modelLevels(model, presets)`** —— 策略查找与可选档位读取(`modelLevels` 取代旧的 `modelEfforts` + `modelAllowedEfforts` 两个函数:`off` 现在只是表里的一行,没有「档位」与「关闭」的二分)。
|
|
23
|
+
- **包入口补导出 `TokenThresholds` / `ReasoningPatch` / `ReasoningPreset` / `ReasoningPresets`** —— alpha.13 漏了这几个类型的导出(原 `7b10196` 补丁并入本版本)。
|
|
24
|
+
|
|
25
|
+
### ✅ 测试
|
|
26
|
+
|
|
27
|
+
- `src/shared/reasoning.test.ts` 重写(26 例):出厂表协议事实(OpenAI 表无 `thinking` 键、`zhipu-5.3` 表无 `off`、DeepSeek 用 `thinking.type` 关闭)、可调档位 = 表的键、反查取最具体匹配、写入剔除旧键、往返一致、Agent 声明非法回退模型默认、原型链污染名字(`constructor`)不生效。
|
|
28
|
+
- `src/config/reasoning-presets.test.ts` 新增(11 例):落盘位置、首次释放、幂等不写盘、未标 `custom` 被覆盖、标了 `custom` 冻结、用户自建策略保留、损坏/数组内容回落种子、`bootstrap()` 顺带释放。
|
|
29
|
+
|
|
30
|
+
### 📄 文档
|
|
31
|
+
|
|
32
|
+
- `docs/{zh,en}/sdk.md`:「思考强度」章节重写为「思考预设(reasoning presets)」,含策略表结构、出厂策略对照表、三个结构性保证、新 API 表与 `Scope` 注入示例。
|
|
33
|
+
- 顺手修掉 alpha.13 之后就过期的示例:`ContextGuardPlugin({ maxTokens, isSuspended })` → `{ guardTokens }`、`AutoMigratePlugin({ maxTokens })` → `{ handoffTokens }`、「交接回合护栏放宽 × 2」的旧描述。
|
|
34
|
+
|
|
35
|
+
## [1.0.0-alpha.13] - 2026-10-22
|
|
36
|
+
|
|
37
|
+
### 💥 Breaking Changes
|
|
38
|
+
|
|
39
|
+
- **`Model.maxContextTokens` / `maxContextChars` → `Model.tokenThresholds: { handoff, guard }`** —— 旧名字一直在误导(它既不是上下文窗口,也不是单一概念)。现在拆成两条**绝对值**:
|
|
40
|
+
- `handoff`:软线,响应返回后 `prompt_tokens` 超过它触发任务交接(`AutoMigratePlugin`);
|
|
41
|
+
- `guard`:硬线,请求前用量超过它抛 `ContextOverflowError` 中断本轮(`ContextGuardPlugin`)。
|
|
42
|
+
- `maxContextChars` 直接淘汰(字符口径早于 token 口径,两者并存只会让「按哪个判」长期含糊)。
|
|
43
|
+
- 迁移由 `config/migrate.ts` 完成,`ConfigManager.read()` 读到即迁、`ensureDefaultConfig()` 检测差异才写盘:`maxContextTokens` → `handoff`,`maxContextChars` → `floor(/4)`,缺 `guard` 时按 1.5 倍折算一次(保持迁移前行为),旧键随即删除。
|
|
44
|
+
- **`ContextGuardPlugin` 的 `ratio` / `suspendedRatio` / `isSuspended` 全部删除** —— 它们只是「相对阈值」的产物。现在 `new ContextGuardPlugin({ guardTokens })` 一个绝对数就够:想调松就把数字改大。代价是**交接回合不再有 2.0 倍的额外放宽**,若需要该缓冲请把 `guard` 配得更大(出厂取 `handoff × 1.5`,并写成整数)。
|
|
45
|
+
- **`ContextOverflowError(promptTokens, guardTokens)`** —— 去掉 `maxTokens` / `ratio` / `threshold` 三个派生字段,改为直接暴露被越过的 `guardTokens`。
|
|
46
|
+
- **`AutoMigratePlugin` 的 `maxTokens` → `handoffTokens`**,`AutoMigrateTriggerInfo.maxTokens` → `handoffTokens`,`HandoffService` 的 `HandoffParams.maxTokens` / `HandoffContext.maxTokens` → `handoffTokens`(仅字段改名,语义不变:交接阈值)。
|
|
47
|
+
|
|
48
|
+
### ✨ 新增
|
|
49
|
+
|
|
50
|
+
- **思考强度(reasoning effort)能力位** —— 档位此前完全没有建模,CLI 只能手改 `defaultParams`,而各家端点接受的值域并不相同(智谱 GLM-5.3 系对 `medium` 直接报错、对 `thinking.type = "disabled"` 也报错)。现在分三层:
|
|
51
|
+
- `Model.reasoning: { efforts: ReasoningEffort[], disable?: "thinking" | "effort-none" }` —— **物理能力上限**,按厂商官方文档声明(防 400);
|
|
52
|
+
- `AgentDefinition.reasoningEffort?: ReasoningEffort` —— 该 Agent 的实际档位,在 `createModel` 阶段覆盖模型 `defaultParams`。于是同一模型可以给不同 Agent 配不同强度(编程 Agent `max`、闲聊 Agent `low`、闲聊小号 `off`)。
|
|
53
|
+
- 刻意**没有**做 Agent 侧的档位白名单(`reasoningEfforts`):它解决的是伪需求(真正会 400 的是端点能力,那已由模型声明兜住),却引入「差一个 s」的命名陷阱与一层交集规则。
|
|
54
|
+
- **`shared/reasoning.ts`**(公共 API,均从包入口导出):`resolveReasoningEffort()`、`resolveModelParams()`、`applyReasoningEffort()`(按声明写入,非法档位抛错)、`modelEfforts()` / `modelAllowedEfforts()` / `readReasoningEffort()` / `isReasoningEffort()` / `capabilityOf()`,以及 `ReasoningEffort` / `ReasoningCapability` 类型。
|
|
55
|
+
- **写入形态由声明决定**:`disable: "thinking"` → 同时维护 `thinking.type` 与 `reasoning_effort`(DeepSeek);`disable: "effort-none"` → 只写 `reasoning_effort`(OpenAI 没有 `thinking` 参数,写它会被当未知参数);未声明 `disable` → 只写 `reasoning_effort`,`off` 直接抛错(智谱 5.3 系)。
|
|
56
|
+
- **出厂模型按官方文档声明能力**(依据已写入 `constants.ts` 注释):
|
|
57
|
+
- `deepseek-flash` → `low/high/max`,可关闭(`api-docs.deepseek.com/guides/thinking_mode`)
|
|
58
|
+
- `glm-5.3` / `glm-5.3-flash` / `glm-5.3-flashx` → `low/high/max`,**不可关闭**(`docs.bigmodel.cn/cn/guide/capabilities/thinking`)
|
|
59
|
+
- `glm-4.7-flash` → 不可调(强制思考,`reasoning_effort` 仅 5.2+ 支持)
|
|
60
|
+
- `gpt-6-astra` / `gpt-6.1-sol` / `gpt-6-luna` → `minimal/low/medium/high`,可用 `reasoning_effort: "none"` 关闭
|
|
61
|
+
- 未声明的模型一律视为不可调档,不推测
|
|
62
|
+
|
|
63
|
+
### 🔧 变更
|
|
64
|
+
|
|
65
|
+
- **`createModel(scope, modelId)` → `createModel(scope, modelId, overrides?: Pick<AgentDefinition, "reasoningEffort">)`** —— 新增的可选第三参由 `createAgent` 传入 `definition.reasoningEffort`。不传时行为与以前完全一致(原样返回模型 `defaultParams`)。
|
|
66
|
+
|
|
67
|
+
### ✅ 测试
|
|
68
|
+
|
|
69
|
+
- 新增 `shared/reasoning.test.ts` 22 例:能力声明与出厂清单一致性、`off` 只对有 `disable` 的模型开放、GLM-5.3 设 `medium` 与 `off` 均抛错、OpenAI 永不写 `thinking` 键、往返一致性、Agent 覆盖与回退。
|
|
70
|
+
- 新增 `config/migrate.test.ts` 13 例:旧字段迁移(tokens / chars / 两者 / 都无 / 脏值)、`guard` 折算、已是新形状时不迁移、不改入参、部分迁移时未变条目保持同一引用。
|
|
71
|
+
- 全量 **57 文件 / 599 用例** 通过(6 skipped),`tsc --noEmit` 与 `build` 零错误。
|
|
72
|
+
|
|
73
|
+
### 📄 Docs
|
|
74
|
+
|
|
75
|
+
- `docs/{zh,en}/sdk.md` 新增「思考强度(reasoning effort)」小节(三层模型、出厂声明表、写入形态、公共 API),核心实体表补充 `Model.reasoning` 与 Agent 的两个档位字段。
|
|
76
|
+
|
|
3
77
|
## [1.0.0-alpha.12] - 2026-10-22
|
|
4
78
|
|
|
5
79
|
### 💥 Breaking Changes
|
|
@@ -4,8 +4,8 @@ import type { HandoffMode, HandoffService } from "../runtime/HandoffService.js";
|
|
|
4
4
|
export interface AutoMigrateTriggerInfo {
|
|
5
5
|
/** 当前 prompt token 用量 */
|
|
6
6
|
promptTokens: number;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/** 交接阈值(当前模型的 tokenThresholds.handoff) */
|
|
8
|
+
handoffTokens: number;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* 迁移执行者解析器:只负责「用什么方式交接」,不负责「何时迁移」。
|
|
@@ -19,7 +19,7 @@ export interface AutoMigrateOptions {
|
|
|
19
19
|
/** 交接服务实例(持有交接前后钩子;编排 handoff / migrate 两条链路) */
|
|
20
20
|
service: HandoffService;
|
|
21
21
|
/** 触发迁移的上下文阈值 */
|
|
22
|
-
|
|
22
|
+
handoffTokens: number;
|
|
23
23
|
/**
|
|
24
24
|
* 执行者解析(可选)。上层可在此弹确认框、读用户偏好,
|
|
25
25
|
* 或直接返回 `null` 表达「本次不迁移」——**无需**再继承本插件。
|
|
@@ -29,14 +29,14 @@ export interface AutoMigrateOptions {
|
|
|
29
29
|
/**
|
|
30
30
|
* 自动迁移插件 —— 只负责「何时触发」,实际迁移统一委托给持有的 `HandoffService`。
|
|
31
31
|
*
|
|
32
|
-
* 当 `usage.prompt_tokens` 超过
|
|
32
|
+
* 当 `usage.prompt_tokens` 超过 handoffTokens 时触发迁移:
|
|
33
33
|
*
|
|
34
34
|
* ```ts
|
|
35
35
|
* agent.use(new AutoMigratePlugin({
|
|
36
36
|
* service: handoffService,
|
|
37
|
-
*
|
|
38
|
-
* resolveMode: async ({ promptTokens,
|
|
39
|
-
* (await askUser(promptTokens,
|
|
37
|
+
* handoffTokens: 250_000,
|
|
38
|
+
* resolveMode: async ({ promptTokens, handoffTokens }) =>
|
|
39
|
+
* (await askUser(promptTokens, handoffTokens)) ? "self" : null,
|
|
40
40
|
* }));
|
|
41
41
|
* ```
|
|
42
42
|
*
|
|
@@ -3,14 +3,14 @@ const log = getLogger();
|
|
|
3
3
|
/**
|
|
4
4
|
* 自动迁移插件 —— 只负责「何时触发」,实际迁移统一委托给持有的 `HandoffService`。
|
|
5
5
|
*
|
|
6
|
-
* 当 `usage.prompt_tokens` 超过
|
|
6
|
+
* 当 `usage.prompt_tokens` 超过 handoffTokens 时触发迁移:
|
|
7
7
|
*
|
|
8
8
|
* ```ts
|
|
9
9
|
* agent.use(new AutoMigratePlugin({
|
|
10
10
|
* service: handoffService,
|
|
11
|
-
*
|
|
12
|
-
* resolveMode: async ({ promptTokens,
|
|
13
|
-
* (await askUser(promptTokens,
|
|
11
|
+
* handoffTokens: 250_000,
|
|
12
|
+
* resolveMode: async ({ promptTokens, handoffTokens }) =>
|
|
13
|
+
* (await askUser(promptTokens, handoffTokens)) ? "self" : null,
|
|
14
14
|
* }));
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
@@ -26,22 +26,22 @@ export class AutoMigratePlugin {
|
|
|
26
26
|
}
|
|
27
27
|
async onAfterSend(ctx) {
|
|
28
28
|
const agent = ctx.agent;
|
|
29
|
-
const { service,
|
|
29
|
+
const { service, handoffTokens, resolveMode } = this.options;
|
|
30
30
|
// 交接/迁移进行中:让行(防递归)
|
|
31
31
|
if (service.busy)
|
|
32
32
|
return;
|
|
33
33
|
const promptTokens = agent.lastUsage?.prompt_tokens;
|
|
34
34
|
if (promptTokens == null)
|
|
35
35
|
return;
|
|
36
|
-
if (promptTokens <=
|
|
36
|
+
if (promptTokens <= handoffTokens)
|
|
37
37
|
return;
|
|
38
38
|
try {
|
|
39
39
|
const mode = resolveMode
|
|
40
|
-
? await resolveMode({ promptTokens,
|
|
40
|
+
? await resolveMode({ promptTokens, handoffTokens })
|
|
41
41
|
: undefined;
|
|
42
42
|
if (mode === null)
|
|
43
43
|
return; // 用户选择跳过本次迁移
|
|
44
|
-
await service.migrate({ agent, promptTokens,
|
|
44
|
+
await service.migrate({ agent, promptTokens, handoffTokens, mode: mode ?? undefined });
|
|
45
45
|
}
|
|
46
46
|
catch (err) {
|
|
47
47
|
log.error(`[autoMigrate] 迁移失败: ${err?.message ?? err}`);
|
|
@@ -1,44 +1,32 @@
|
|
|
1
1
|
import type { AgentPlugin, SendContext } from "@ai-zen/agents-core";
|
|
2
2
|
export interface ContextGuardOptions {
|
|
3
|
-
/** 上下文告警阈值(token)。用量超过 maxTokens × ratio 即中断。 */
|
|
4
|
-
maxTokens: number;
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
7
|
-
* 在请求前抛出 ContextOverflowError 中断对话。默认 1.5(即 +50%)。
|
|
8
|
-
*/
|
|
9
|
-
ratio?: number;
|
|
10
|
-
/**
|
|
11
|
-
* 是否处于「需要放宽」的阶段(例如交接回合)。返回 true 时改用 `suspendedRatio`。
|
|
4
|
+
* 强行阻断阈值(tokens,绝对值)。
|
|
12
5
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
6
|
+
* 语义只有一条:请求前上一轮用量超过它 → 中断本轮。没有倍数、没有放宽档——
|
|
7
|
+
* 想调松就把它调大,「安全边界写多少就是多少」比「隐式乘以某个系数」可预测得多。
|
|
15
8
|
*/
|
|
16
|
-
|
|
17
|
-
/** 放宽后的比例,默认 2.0(即 maxTokens × 2) */
|
|
18
|
-
suspendedRatio?: number;
|
|
9
|
+
guardTokens: number;
|
|
19
10
|
}
|
|
20
11
|
/**
|
|
21
12
|
* 上下文安全护栏插件。
|
|
22
13
|
*
|
|
23
|
-
* 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens
|
|
24
|
-
*
|
|
25
|
-
* 在请求前抛出 ContextOverflowError,**中断对话**,交由上层向用户报错。
|
|
14
|
+
* 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens):超过 `guardTokens`
|
|
15
|
+
* 即抛 `ContextOverflowError` 中断对话,交由上层向用户报错。
|
|
26
16
|
*
|
|
27
17
|
* 与迁移插件职责分离:
|
|
28
|
-
* - ContextGuardPlugin
|
|
29
|
-
* - AutoMigratePlugin
|
|
30
|
-
*
|
|
18
|
+
* - `ContextGuardPlugin`:硬线,只做「严重超限即中断」的安全兜底,绝不继续对话/迁移;
|
|
19
|
+
* - `AutoMigratePlugin`:软线(`tokenThresholds.handoff`),在正常范围内做交接迁移。
|
|
20
|
+
* 两条线都是绝对值,`guard` 应当明显大于 `handoff`,交接回合的额外请求才有余量。
|
|
31
21
|
*
|
|
32
22
|
* ```ts
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* }));
|
|
37
|
-
* agent.use(new AutoMigratePlugin({ service: handoffService, maxTokens: 250_000 }));
|
|
23
|
+
* const { handoff, guard } = config.models[i].tokenThresholds;
|
|
24
|
+
* agent.use(new ContextGuardPlugin({ guardTokens: guard }));
|
|
25
|
+
* agent.use(new AutoMigratePlugin({ service: handoffService, handoffTokens: handoff }));
|
|
38
26
|
* ```
|
|
39
27
|
*/
|
|
40
28
|
export declare class ContextGuardPlugin implements AgentPlugin {
|
|
41
|
-
private
|
|
29
|
+
private readonly guardTokens;
|
|
42
30
|
constructor(options: ContextGuardOptions);
|
|
43
31
|
onInnerLoopStart(ctx: SendContext): Promise<void>;
|
|
44
32
|
}
|
|
@@ -2,39 +2,32 @@ import { ContextOverflowError } from "../shared/errors.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* 上下文安全护栏插件。
|
|
4
4
|
*
|
|
5
|
-
* 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens
|
|
6
|
-
*
|
|
7
|
-
* 在请求前抛出 ContextOverflowError,**中断对话**,交由上层向用户报错。
|
|
5
|
+
* 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens):超过 `guardTokens`
|
|
6
|
+
* 即抛 `ContextOverflowError` 中断对话,交由上层向用户报错。
|
|
8
7
|
*
|
|
9
8
|
* 与迁移插件职责分离:
|
|
10
|
-
* - ContextGuardPlugin
|
|
11
|
-
* - AutoMigratePlugin
|
|
12
|
-
*
|
|
9
|
+
* - `ContextGuardPlugin`:硬线,只做「严重超限即中断」的安全兜底,绝不继续对话/迁移;
|
|
10
|
+
* - `AutoMigratePlugin`:软线(`tokenThresholds.handoff`),在正常范围内做交接迁移。
|
|
11
|
+
* 两条线都是绝对值,`guard` 应当明显大于 `handoff`,交接回合的额外请求才有余量。
|
|
13
12
|
*
|
|
14
13
|
* ```ts
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* }));
|
|
19
|
-
* agent.use(new AutoMigratePlugin({ service: handoffService, maxTokens: 250_000 }));
|
|
14
|
+
* const { handoff, guard } = config.models[i].tokenThresholds;
|
|
15
|
+
* agent.use(new ContextGuardPlugin({ guardTokens: guard }));
|
|
16
|
+
* agent.use(new AutoMigratePlugin({ service: handoffService, handoffTokens: handoff }));
|
|
20
17
|
* ```
|
|
21
18
|
*/
|
|
22
19
|
export class ContextGuardPlugin {
|
|
23
|
-
|
|
20
|
+
guardTokens;
|
|
24
21
|
constructor(options) {
|
|
25
|
-
this.
|
|
22
|
+
this.guardTokens = options.guardTokens;
|
|
26
23
|
}
|
|
27
24
|
async onInnerLoopStart(ctx) {
|
|
28
|
-
const
|
|
29
|
-
const { maxTokens, ratio = 1.5, isSuspended, suspendedRatio = 2 } = this.options;
|
|
25
|
+
const promptTokens = ctx.agent.lastUsage?.prompt_tokens;
|
|
30
26
|
// 首轮请求前尚无用量数据,跳过(只能在发出首轮请求、拿到 usage 后才有据可查)
|
|
31
|
-
const promptTokens = agent.lastUsage?.prompt_tokens;
|
|
32
27
|
if (promptTokens == null)
|
|
33
28
|
return;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (promptTokens > maxTokens * effectiveRatio) {
|
|
37
|
-
throw new ContextOverflowError(promptTokens, maxTokens, effectiveRatio);
|
|
29
|
+
if (promptTokens > this.guardTokens) {
|
|
30
|
+
throw new ContextOverflowError(promptTokens, this.guardTokens);
|
|
38
31
|
}
|
|
39
32
|
}
|
|
40
33
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppConfig, AgentDefinition } from "../types/index.js";
|
|
1
|
+
import type { AppConfig, AgentDefinition, ReasoningPresets } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* 配置管理器 — 负责配置文件的读写、目录初始化、默认实体创建。
|
|
4
4
|
*/
|
|
@@ -8,8 +8,14 @@ export declare class ConfigManager {
|
|
|
8
8
|
constructor(configPath: string);
|
|
9
9
|
/**
|
|
10
10
|
* 读取配置。文件不存在时返回出厂默认配置。
|
|
11
|
+
*
|
|
12
|
+
* 读到即**迁移**(`config/migrate.ts`):上层拿到的永远是当前字段形状,
|
|
13
|
+
* 旧字段(`maxContextTokens` / `maxContextChars`)不会漏到调用方。
|
|
14
|
+
* 迁移结果不在这里落盘——落盘只在 `ensureDefaultConfig()` 做一次。
|
|
11
15
|
*/
|
|
12
16
|
read(): Promise<AppConfig>;
|
|
17
|
+
/** 读磁盘原文(不做任何迁移/同步),供 `ensureDefaultConfig()` 对比写盘用 */
|
|
18
|
+
private readRaw;
|
|
13
19
|
write(config: AppConfig): Promise<void>;
|
|
14
20
|
/**
|
|
15
21
|
* 确保基础目录结构存在。
|
|
@@ -52,6 +58,27 @@ export declare class ConfigManager {
|
|
|
52
58
|
agent: AgentDefinition | null;
|
|
53
59
|
subAgent: AgentDefinition | null;
|
|
54
60
|
}>;
|
|
61
|
+
/** 策略表文件路径(与 config.json 同级) */
|
|
62
|
+
get reasoningPresetsPath(): string;
|
|
63
|
+
/**
|
|
64
|
+
* 读取思考策略表。
|
|
65
|
+
*
|
|
66
|
+
* 文件缺失或无法解析时返回出厂种子(内存值,不写盘)——保证调用方永远拿到可用的表:
|
|
67
|
+
* 用户误删文件后仍能正常选档位,下次 `ensureReasoningPresets()` 会重新释放。
|
|
68
|
+
*/
|
|
69
|
+
readReasoningPresets(): Promise<ReasoningPresets>;
|
|
70
|
+
/**
|
|
71
|
+
* 释放 / 同步出厂思考策略。
|
|
72
|
+
*
|
|
73
|
+
* 覆盖策略与 `syncManagedModels` 同构,**按策略逐个判断**(粒度到单个策略):
|
|
74
|
+
* - 文件不存在 → 写入出厂种子;
|
|
75
|
+
* - 某策略标了 `custom: true` → 用户自有,原样保留;
|
|
76
|
+
* - 某策略未标 → 用出厂版覆盖该策略(内容相同则不写盘);
|
|
77
|
+
* - 用户自建的非出厂策略 → 始终保留。
|
|
78
|
+
*
|
|
79
|
+
* @returns 实际写入的策略名列表(未变化 / 用户自有 均不计入)
|
|
80
|
+
*/
|
|
81
|
+
ensureReasoningPresets(): Promise<string[]>;
|
|
55
82
|
/**
|
|
56
83
|
* 释放 / 同步出厂 Skill。
|
|
57
84
|
*
|
|
@@ -2,6 +2,8 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { DEFAULT_AGENT_ID, DEFAULT_AGENT_DEFINITION, DEFAULT_SUBAGENT_ID, DEFAULT_SUBAGENT_DEFINITION, DEFAULT_APP_CONFIG, DEFAULT_MCP_CONFIG, CONFIG_SUB_DIRS, } from "./constants.js";
|
|
4
4
|
import { FACTORY_SKILLS, isCustomSkill } from "./factory-skills.js";
|
|
5
|
+
import { FACTORY_REASONING_PRESETS, REASONING_PRESETS_FILE, } from "./reasoning-presets.js";
|
|
6
|
+
import { migrateConfig } from "./migrate.js";
|
|
5
7
|
/**
|
|
6
8
|
* 配置管理器 — 负责配置文件的读写、目录初始化、默认实体创建。
|
|
7
9
|
*/
|
|
@@ -17,13 +19,22 @@ export class ConfigManager {
|
|
|
17
19
|
// -----------------------------------------------------------------------
|
|
18
20
|
/**
|
|
19
21
|
* 读取配置。文件不存在时返回出厂默认配置。
|
|
22
|
+
*
|
|
23
|
+
* 读到即**迁移**(`config/migrate.ts`):上层拿到的永远是当前字段形状,
|
|
24
|
+
* 旧字段(`maxContextTokens` / `maxContextChars`)不会漏到调用方。
|
|
25
|
+
* 迁移结果不在这里落盘——落盘只在 `ensureDefaultConfig()` 做一次。
|
|
20
26
|
*/
|
|
21
27
|
async read() {
|
|
28
|
+
const raw = await this.readRaw();
|
|
29
|
+
return raw ? migrateConfig(raw).config : { ...DEFAULT_APP_CONFIG };
|
|
30
|
+
}
|
|
31
|
+
/** 读磁盘原文(不做任何迁移/同步),供 `ensureDefaultConfig()` 对比写盘用 */
|
|
32
|
+
async readRaw() {
|
|
22
33
|
try {
|
|
23
34
|
await fs.access(this.configPath);
|
|
24
35
|
}
|
|
25
36
|
catch {
|
|
26
|
-
return
|
|
37
|
+
return null;
|
|
27
38
|
}
|
|
28
39
|
const raw = await fs.readFile(this.configPath, "utf-8");
|
|
29
40
|
return JSON.parse(raw);
|
|
@@ -62,18 +73,16 @@ export class ConfigManager {
|
|
|
62
73
|
* 条目视为用户自有,原样保留。同步结果与磁盘内容一致时不写盘。
|
|
63
74
|
*/
|
|
64
75
|
async ensureDefaultConfig() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await fs.access(this.configPath);
|
|
68
|
-
existing = await this.read();
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
76
|
+
const onDisk = await this.readRaw();
|
|
77
|
+
if (!onDisk) {
|
|
71
78
|
await this.ensureDirs();
|
|
72
79
|
await this.write(DEFAULT_APP_CONFIG);
|
|
73
80
|
return { ...DEFAULT_APP_CONFIG };
|
|
74
81
|
}
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
// 先迁移历史字段,再同步出厂模型清单;两者结果与磁盘原文不同才写盘
|
|
83
|
+
const migrated = migrateConfig(onDisk).config;
|
|
84
|
+
const synced = syncManagedModels(migrated);
|
|
85
|
+
if (JSON.stringify(synced) !== JSON.stringify(onDisk)) {
|
|
77
86
|
await this.write(synced);
|
|
78
87
|
}
|
|
79
88
|
return synced;
|
|
@@ -180,9 +189,75 @@ export class ConfigManager {
|
|
|
180
189
|
const subAgent = await this.ensureDefaultSubAgent();
|
|
181
190
|
await this.ensureDefaultMcpConfig();
|
|
182
191
|
await this.ensureFactorySkills();
|
|
192
|
+
await this.ensureReasoningPresets();
|
|
183
193
|
return { config, agent, subAgent };
|
|
184
194
|
}
|
|
185
195
|
// -----------------------------------------------------------------------
|
|
196
|
+
// 思考策略表({basePath}/reasoning-presets.json)
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
/** 策略表文件路径(与 config.json 同级) */
|
|
199
|
+
get reasoningPresetsPath() {
|
|
200
|
+
return join(this.basePath, REASONING_PRESETS_FILE);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 读取思考策略表。
|
|
204
|
+
*
|
|
205
|
+
* 文件缺失或无法解析时返回出厂种子(内存值,不写盘)——保证调用方永远拿到可用的表:
|
|
206
|
+
* 用户误删文件后仍能正常选档位,下次 `ensureReasoningPresets()` 会重新释放。
|
|
207
|
+
*/
|
|
208
|
+
async readReasoningPresets() {
|
|
209
|
+
try {
|
|
210
|
+
const raw = await fs.readFile(this.reasoningPresetsPath, "utf-8");
|
|
211
|
+
const parsed = JSON.parse(raw);
|
|
212
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
213
|
+
return parsed;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// 缺失或损坏 → 回落出厂种子
|
|
217
|
+
}
|
|
218
|
+
return { ...FACTORY_REASONING_PRESETS };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 释放 / 同步出厂思考策略。
|
|
222
|
+
*
|
|
223
|
+
* 覆盖策略与 `syncManagedModels` 同构,**按策略逐个判断**(粒度到单个策略):
|
|
224
|
+
* - 文件不存在 → 写入出厂种子;
|
|
225
|
+
* - 某策略标了 `custom: true` → 用户自有,原样保留;
|
|
226
|
+
* - 某策略未标 → 用出厂版覆盖该策略(内容相同则不写盘);
|
|
227
|
+
* - 用户自建的非出厂策略 → 始终保留。
|
|
228
|
+
*
|
|
229
|
+
* @returns 实际写入的策略名列表(未变化 / 用户自有 均不计入)
|
|
230
|
+
*/
|
|
231
|
+
async ensureReasoningPresets() {
|
|
232
|
+
let onDisk = null;
|
|
233
|
+
try {
|
|
234
|
+
onDisk = JSON.parse(await fs.readFile(this.reasoningPresetsPath, "utf-8"));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
onDisk = null;
|
|
238
|
+
}
|
|
239
|
+
if (!onDisk || typeof onDisk !== "object" || Array.isArray(onDisk)) {
|
|
240
|
+
await fs.mkdir(this.basePath, { recursive: true });
|
|
241
|
+
await fs.writeFile(this.reasoningPresetsPath, JSON.stringify(FACTORY_REASONING_PRESETS, null, 2), "utf-8");
|
|
242
|
+
return Object.keys(FACTORY_REASONING_PRESETS);
|
|
243
|
+
}
|
|
244
|
+
const next = { ...onDisk };
|
|
245
|
+
const written = [];
|
|
246
|
+
for (const [name, preset] of Object.entries(FACTORY_REASONING_PRESETS)) {
|
|
247
|
+
const existing = onDisk[name];
|
|
248
|
+
if (existing?.custom === true)
|
|
249
|
+
continue; // 用户改过 → 冻结
|
|
250
|
+
if (JSON.stringify(existing) === JSON.stringify(preset))
|
|
251
|
+
continue; // 已是最新
|
|
252
|
+
next[name] = preset;
|
|
253
|
+
written.push(name);
|
|
254
|
+
}
|
|
255
|
+
if (written.length > 0) {
|
|
256
|
+
await fs.writeFile(this.reasoningPresetsPath, JSON.stringify(next, null, 2), "utf-8");
|
|
257
|
+
}
|
|
258
|
+
return written;
|
|
259
|
+
}
|
|
260
|
+
// -----------------------------------------------------------------------
|
|
186
261
|
// 出厂 Skill({basePath}/skills/<id>/SKILL.md)
|
|
187
262
|
// -----------------------------------------------------------------------
|
|
188
263
|
/**
|
package/dist/config/constants.js
CHANGED
|
@@ -98,9 +98,15 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
98
98
|
// 标了 `custom: true` 的条目视为用户自有,原样保留。实现见
|
|
99
99
|
// ConfigManager.syncManagedModels。
|
|
100
100
|
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
101
|
+
// tokenThresholds 的两条线都与模型的真实上下文窗口无关:
|
|
102
|
+
// handoff(软线,交接触发)与 guard(硬线,请求前强行阻断)。
|
|
103
|
+
// 出厂取值:窗口 ≥1M 的模型统一 handoff=250K / guard=375K(约窗口的 25% / 37.5%),
|
|
104
|
+
// glm-4.7-flash 窗口仅 200K,取 handoff=100K / guard=150K。
|
|
105
|
+
//
|
|
106
|
+
// reasoningPreset:思考策略名,引用 `<basePath>/reasoning-presets.json` 里的键
|
|
107
|
+
// (出厂内容见 `config/reasoning-presets.ts`,档位的协议差异全部写在表里)。
|
|
108
|
+
// 不写这个字段 = 不可调档(如 GLM-4.7 强制思考且无 reasoning_effort 参数)。
|
|
109
|
+
// 新接入端点时先在策略表里加一张表,再在这里引用它的名字。
|
|
104
110
|
models: [
|
|
105
111
|
// ---------- OpenAI ----------
|
|
106
112
|
{
|
|
@@ -108,9 +114,10 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
108
114
|
name: "GPT-6 Astra",
|
|
109
115
|
endpointId: "openai",
|
|
110
116
|
modelName: "gpt-6-astra",
|
|
111
|
-
|
|
117
|
+
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
112
118
|
defaultParams: {},
|
|
113
119
|
vision: true,
|
|
120
|
+
reasoningPreset: "openai",
|
|
114
121
|
description: "OpenAI 最强旗舰(2026-09):高级分析、软件工程、深度研究与科学工作",
|
|
115
122
|
},
|
|
116
123
|
{
|
|
@@ -118,9 +125,10 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
118
125
|
name: "GPT-6.1 Sol",
|
|
119
126
|
endpointId: "openai",
|
|
120
127
|
modelName: "gpt-6.1-sol",
|
|
121
|
-
|
|
128
|
+
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
122
129
|
defaultParams: {},
|
|
123
130
|
vision: true,
|
|
131
|
+
reasoningPreset: "openai",
|
|
124
132
|
description: "OpenAI 最新旗舰(2026-09-29):Agentic Coding、计算机操作与文档密集型专业任务",
|
|
125
133
|
},
|
|
126
134
|
{
|
|
@@ -128,9 +136,10 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
128
136
|
name: "GPT-6 Luna",
|
|
129
137
|
endpointId: "openai",
|
|
130
138
|
modelName: "gpt-6-luna",
|
|
131
|
-
|
|
139
|
+
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
132
140
|
defaultParams: {},
|
|
133
141
|
vision: true,
|
|
142
|
+
reasoningPreset: "openai",
|
|
134
143
|
description: "OpenAI GPT-6 系列快速低成本模型:适合高吞吐、低延迟场景",
|
|
135
144
|
},
|
|
136
145
|
// ---------- 智谱 BigModel ----------
|
|
@@ -139,8 +148,9 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
139
148
|
name: "GLM-5.3",
|
|
140
149
|
endpointId: "bigmodelcn",
|
|
141
150
|
modelName: "glm-5.3",
|
|
142
|
-
|
|
151
|
+
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
143
152
|
defaultParams: { thinking: { type: "enabled" }, reasoning_effort: "max" },
|
|
153
|
+
reasoningPreset: "zhipu-5.3",
|
|
144
154
|
description: "智谱最新旗舰:复杂软件工程与长程 Agent 任务(强制开启思考,1M 上下文)",
|
|
145
155
|
},
|
|
146
156
|
{
|
|
@@ -148,9 +158,10 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
148
158
|
name: "GLM-5.3-Flash",
|
|
149
159
|
endpointId: "bigmodelcn",
|
|
150
160
|
modelName: "glm-5.3-flash",
|
|
151
|
-
|
|
161
|
+
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
152
162
|
defaultParams: { thinking: { type: "enabled" }, reasoning_effort: "max" },
|
|
153
163
|
vision: true,
|
|
164
|
+
reasoningPreset: "zhipu-5.3",
|
|
154
165
|
description: "智谱原生多模态模型(图片/视频/文件):视觉 Coding,1M 上下文",
|
|
155
166
|
},
|
|
156
167
|
{
|
|
@@ -158,9 +169,10 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
158
169
|
name: "GLM-5.3-FlashX",
|
|
159
170
|
endpointId: "bigmodelcn",
|
|
160
171
|
modelName: "glm-5.3-flashx",
|
|
161
|
-
|
|
172
|
+
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
162
173
|
defaultParams: { thinking: { type: "enabled" }, reasoning_effort: "max" },
|
|
163
174
|
vision: true,
|
|
175
|
+
reasoningPreset: "zhipu-5.3",
|
|
164
176
|
description: "GLM-5.3-Flash 高速版(200 tokens/s),原生多模态",
|
|
165
177
|
},
|
|
166
178
|
{
|
|
@@ -169,7 +181,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
169
181
|
endpointId: "bigmodelcn",
|
|
170
182
|
modelName: "glm-4.7-flash",
|
|
171
183
|
// 该模型真实窗口仅 200K,阈值下调至 100K(约窗口的 50%),留足 response 空间
|
|
172
|
-
|
|
184
|
+
tokenThresholds: { handoff: 100_000, guard: 150_000 },
|
|
173
185
|
defaultParams: {},
|
|
174
186
|
description: "智谱免费文本模型:200K 上下文,适合轻量任务",
|
|
175
187
|
},
|
|
@@ -179,9 +191,10 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
179
191
|
name: "DeepSeek-V4.1-Flash",
|
|
180
192
|
endpointId: "deepseek",
|
|
181
193
|
modelName: "deepseek-flash",
|
|
182
|
-
|
|
194
|
+
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
183
195
|
defaultParams: { thinking: { type: "enabled" } },
|
|
184
196
|
vision: true,
|
|
197
|
+
reasoningPreset: "deepseek",
|
|
185
198
|
description: "DeepSeek 旗舰(2026-09-10):552B MoE 非对称架构,原生多模态,1M 上下文",
|
|
186
199
|
},
|
|
187
200
|
],
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置字段迁移 —— 历史形状升级到当前形状
|
|
3
|
+
*
|
|
4
|
+
* 三条遗留字段被淘汰:
|
|
5
|
+
*
|
|
6
|
+
* 1. `Model.maxContextTokens` / `Model.maxContextChars` → **`Model.tokenThresholds`**
|
|
7
|
+
* 旧名字一直在误导:它既不是「模型的真实上下文窗口」,也不是单一概念。现在拆成两条线:
|
|
8
|
+
* `handoff`(交接阈值,软线)与 `guard`(强行阻断阈值,硬线)。
|
|
9
|
+
* `maxContextChars` 是 token 口径之前的历史遗留,按 `floor(chars / 4)` 折成 tokens
|
|
10
|
+
* (与旧代码的回退算法一致)后并入 `handoff`。
|
|
11
|
+
*
|
|
12
|
+
* 迁移时机:`ConfigManager.read()` 读出即迁(内存里永远是当前形状);
|
|
13
|
+
* 落盘由 `ensureDefaultConfig()` 负责 —— 它比较迁移结果与磁盘原文,不同才写回,
|
|
14
|
+
* 因此迁移只发生一次,且不会在每次启动时无谓地改写文件。
|
|
15
|
+
*/
|
|
16
|
+
import type { AppConfig, Model } from "../types/index.js";
|
|
17
|
+
/** 当前字段名 */
|
|
18
|
+
export declare const TOKEN_THRESHOLDS_KEY = "tokenThresholds";
|
|
19
|
+
/** 已淘汰的旧字段名(仅迁移代码认识它们) */
|
|
20
|
+
export declare const LEGACY_TOKEN_FIELD = "maxContextTokens";
|
|
21
|
+
export declare const LEGACY_CHARS_FIELD = "maxContextChars";
|
|
22
|
+
/**
|
|
23
|
+
* 旧数据缺失阈值时的交接阈值兜底。
|
|
24
|
+
*
|
|
25
|
+
* 与 CLI 新建模型的默认值一致;宁可给一个保守值,也不要让模型缺阈值——
|
|
26
|
+
* 缺阈值意味着既没有交接触发、也没有阻断保护。
|
|
27
|
+
*/
|
|
28
|
+
export declare const DEFAULT_HANDOFF_THRESHOLD = 128000;
|
|
29
|
+
/**
|
|
30
|
+
* 迁移单个模型条目。
|
|
31
|
+
*
|
|
32
|
+
* 优先级:已有 `tokenThresholds` 的成员 > 旧 tokens 字段 > 旧 chars 字段折算 > 兜底默认。
|
|
33
|
+
* 旧字段一律删除(连同一份数据里残留的脏值),保证迁移后的形状唯一。
|
|
34
|
+
* 已有 `handoff` 但缺 `guard` 时按 1.5 倍补齐。
|
|
35
|
+
*/
|
|
36
|
+
export declare function migrateModelThresholds(model: Model): {
|
|
37
|
+
model: Model;
|
|
38
|
+
changed: boolean;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* 迁移整个配置。只处理对话模型(`models`)——
|
|
42
|
+
* 图片模型(`imageModels`)从来没有上下文阈值概念。
|
|
43
|
+
*/
|
|
44
|
+
export declare function migrateConfig(config: AppConfig): {
|
|
45
|
+
config: AppConfig;
|
|
46
|
+
changed: boolean;
|
|
47
|
+
};
|