@ai-zen/agents-sdk 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (67) hide show
  1. package/CHANGELOG.md +368 -0
  2. package/README.md +15 -14
  3. package/README.zh.md +15 -14
  4. package/dist/agent-plugins/AutoMigratePlugin.d.ts +35 -9
  5. package/dist/agent-plugins/AutoMigratePlugin.js +23 -10
  6. package/dist/agent-plugins/ContextGuardPlugin.d.ts +14 -16
  7. package/dist/agent-plugins/ContextGuardPlugin.js +13 -17
  8. package/dist/config/ConfigManager.d.ts +57 -8
  9. package/dist/config/ConfigManager.js +166 -25
  10. package/dist/config/constants.d.ts +7 -0
  11. package/dist/config/constants.js +38 -13
  12. package/dist/config/factory-skills.d.ts +33 -0
  13. package/dist/config/factory-skills.js +126 -0
  14. package/dist/config/migrate.d.ts +67 -0
  15. package/dist/config/migrate.js +127 -0
  16. package/dist/config/reasoning-presets.d.ts +37 -0
  17. package/dist/config/reasoning-presets.js +64 -0
  18. package/dist/index.d.ts +11 -5
  19. package/dist/index.js +10 -2
  20. package/dist/runtime/HandoffGuardPlugin.d.ts +35 -0
  21. package/dist/runtime/HandoffGuardPlugin.js +65 -0
  22. package/dist/runtime/HandoffService.d.ts +245 -0
  23. package/dist/runtime/HandoffService.js +472 -0
  24. package/dist/runtime/SdkAgent.d.ts +7 -1
  25. package/dist/runtime/SdkAgent.js +3 -0
  26. package/dist/runtime/createAgent.d.ts +16 -1
  27. package/dist/runtime/createAgent.js +10 -5
  28. package/dist/runtime/createModel.d.ts +8 -4
  29. package/dist/runtime/createModel.js +19 -2
  30. package/dist/scope/plugins/builtin/BatchEditTool.js +5 -2
  31. package/dist/scope/plugins/builtin/DownloadFileTool.js +1 -1
  32. package/dist/scope/plugins/builtin/EditTool.js +6 -1
  33. package/dist/scope/plugins/builtin/ExecTool.js +2 -2
  34. package/dist/scope/plugins/builtin/FindTextTool.js +6 -6
  35. package/dist/scope/plugins/builtin/GenerateImageTool.d.ts +1 -1
  36. package/dist/scope/plugins/builtin/GenerateImageTool.js +7 -7
  37. package/dist/scope/plugins/builtin/GlobTool.js +1 -1
  38. package/dist/scope/plugins/builtin/InspectFileTool.d.ts +2 -2
  39. package/dist/scope/plugins/builtin/InspectFileTool.js +5 -5
  40. package/dist/scope/plugins/builtin/LsTool.js +1 -1
  41. package/dist/scope/plugins/builtin/{ReadFileTool.d.ts → ReadTool.d.ts} +1 -1
  42. package/dist/scope/plugins/builtin/{ReadFileTool.js → ReadTool.js} +3 -3
  43. package/dist/scope/plugins/builtin/{ExecAsyncTool.d.ts → SpawnTool.d.ts} +1 -1
  44. package/dist/scope/plugins/builtin/{ExecAsyncTool.js → SpawnTool.js} +2 -2
  45. package/dist/scope/plugins/builtin/ViewImageTool.d.ts +6 -4
  46. package/dist/scope/plugins/builtin/ViewImageTool.js +43 -59
  47. package/dist/scope/plugins/builtin/{WriteFileTool.d.ts → WriteTool.d.ts} +1 -1
  48. package/dist/scope/plugins/builtin/{WriteFileTool.js → WriteTool.js} +2 -2
  49. package/dist/scope/plugins/builtin/index.d.ts +4 -4
  50. package/dist/scope/plugins/builtin/index.js +7 -7
  51. package/dist/scope/plugins/builtin/outputGuard.d.ts +2 -2
  52. package/dist/scope/plugins/builtin/outputGuard.js +1 -1
  53. package/dist/scope/plugins/mcp/McpScopePlugin.d.ts +1 -1
  54. package/dist/scope/plugins/mcp/McpScopePlugin.js +4 -3
  55. package/dist/scope/plugins/mcp/mcpTools.d.ts +9 -3
  56. package/dist/scope/plugins/mcp/mcpTools.js +196 -18
  57. package/dist/scope/plugins/skills/discover.js +1 -2
  58. package/dist/scope/plugins/skills/skillTools.js +0 -2
  59. package/dist/scope/plugins/subagents/subAgentTools.js +2 -1
  60. package/dist/shared/content-blocks.d.ts +108 -0
  61. package/dist/shared/content-blocks.js +253 -0
  62. package/dist/shared/errors.d.ts +4 -8
  63. package/dist/shared/errors.js +8 -14
  64. package/dist/types/index.d.ts +103 -10
  65. package/package.json +3 -3
  66. package/dist/runtime/TaskMigrationService.d.ts +0 -128
  67. package/dist/runtime/TaskMigrationService.js +0 -211
@@ -1,17 +1,22 @@
1
1
  import { getLogger } from "../shared/logger.js";
2
- const log = getLogger();
3
2
  /**
4
- * 自动迁移插件 — 只负责「触发」,实际迁移逻辑统一委托给持有的 `TaskMigrationService` 实例。
3
+ * 自动迁移插件 —— 只负责「何时触发」,实际迁移统一委托给持有的 `HandoffService`。
5
4
  *
6
- * 当 token 使用量超过 maxTokens 时触发迁移。`service.migrate()` 需要 `agent`(当前对话)作为
7
- * 唯一运行时参数,其余(模型、迁移前后钩子)已由服务在构造时持有。
5
+ * 当 `usage.prompt_tokens` 超过 handoffTokens 时触发迁移:
8
6
  *
9
7
  * ```ts
10
8
  * agent.use(new AutoMigratePlugin({
11
- * service: new TaskMigrationService({ onMigrated }),
12
- * maxTokens: 250_000,
9
+ * service: handoffService,
10
+ * handoffTokens: 250_000,
11
+ * resolveMode: async ({ promptTokens, handoffTokens }) =>
12
+ * (await askUser(promptTokens, handoffTokens)) ? "self" : null,
13
13
  * }));
14
14
  * ```
15
+ *
16
+ * 两层让行(缺一不可):
17
+ * - `service.busy`:交接回合本身就是一次 `send()`,其 `onAfterSend` 会再次到这里;
18
+ * 不跳过就会「交接触发迁移 → 迁移再交接」无限递归;
19
+ * - 交接回合结束后的用量也仍可能超阈值,故 busy 判断必须覆盖整个交接 + 迁移过程。
15
20
  */
16
21
  export class AutoMigratePlugin {
17
22
  options;
@@ -20,17 +25,25 @@ export class AutoMigratePlugin {
20
25
  }
21
26
  async onAfterSend(ctx) {
22
27
  const agent = ctx.agent;
23
- const { service, maxTokens } = this.options;
28
+ const { service, handoffTokens, resolveMode } = this.options;
29
+ // 交接/迁移进行中:让行(防递归)
30
+ if (service.busy)
31
+ return;
24
32
  const promptTokens = agent.lastUsage?.prompt_tokens;
25
33
  if (promptTokens == null)
26
34
  return;
27
- if (promptTokens <= maxTokens)
35
+ if (promptTokens <= handoffTokens)
28
36
  return;
29
37
  try {
30
- await service.migrate({ agent, promptTokens, maxTokens });
38
+ const mode = resolveMode
39
+ ? await resolveMode({ promptTokens, handoffTokens })
40
+ : undefined;
41
+ if (mode === null)
42
+ return; // 用户选择跳过本次迁移
43
+ await service.migrate({ agent, promptTokens, handoffTokens, mode: mode ?? undefined });
31
44
  }
32
45
  catch (err) {
33
- log.error(`[autoMigrate] 迁移失败: ${err?.message ?? err}`);
46
+ getLogger().error(`[autoMigrate] 迁移失败: ${err?.message ?? err}`);
34
47
  }
35
48
  }
36
49
  }
@@ -1,34 +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
- * 超出比例。用量 > maxTokens × ratio 时判定为「严重超限」(如读入超大文件),
7
- * 在请求前抛出 ContextOverflowError 中断对话。默认 1.5(即 +50%)。
4
+ * 强行阻断阈值(tokens,绝对值)。
5
+ *
6
+ * 语义只有一条:请求前上一轮用量超过它 → 中断本轮。没有倍数、没有放宽档——
7
+ * 想调松就把它调大,「安全边界写多少就是多少」比「隐式乘以某个系数」可预测得多。
8
8
  */
9
- ratio?: number;
9
+ guardTokens: number;
10
10
  }
11
11
  /**
12
12
  * 上下文安全护栏插件。
13
13
  *
14
- * 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens):
15
- * - 若用量超过 maxTokens × ratio,说明上下文已严重超限(通常因读入超大文件),
16
- * 在请求前抛出 ContextOverflowError,**中断对话**,交由上层向用户报错。
14
+ * 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens):超过 `guardTokens`
15
+ * 即抛 `ContextOverflowError` 中断对话,交由上层向用户报错。
17
16
  *
18
17
  * 与迁移插件职责分离:
19
- * - ContextGuardPlugin 只做「严重超限即中断」的安全兜底,绝不继续对话/迁移;
20
- * - AutoMigratePlugin 在正常范围内(超 maxTokens 但未达硬上限)做交接迁移。
18
+ * - `ContextGuardPlugin`:硬线,只做「严重超限即中断」的安全兜底,绝不继续对话/迁移;
19
+ * - `AutoMigratePlugin`:软线(`tokenThresholds.handoff`),在正常范围内做交接迁移。
20
+ * 两条线都是绝对值,`guard` 应当明显大于 `handoff`,交接回合的额外请求才有余量。
21
21
  *
22
22
  * ```ts
23
- * agent.use(new ContextGuardPlugin({ maxTokens: 250_000 }));
24
- * agent.use(new AutoMigratePlugin({
25
- * service: new TaskMigrationService({ onMigrated }),
26
- * maxTokens: 250_000,
27
- * }));
23
+ * const { handoff, guard } = config.models[i].tokenThresholds;
24
+ * agent.use(new ContextGuardPlugin({ guardTokens: guard }));
25
+ * agent.use(new AutoMigratePlugin({ service: handoffService, handoffTokens: handoff }));
28
26
  * ```
29
27
  */
30
28
  export declare class ContextGuardPlugin implements AgentPlugin {
31
- private options;
29
+ private readonly guardTokens;
32
30
  constructor(options: ContextGuardOptions);
33
31
  onInnerLoopStart(ctx: SendContext): Promise<void>;
34
32
  }
@@ -2,36 +2,32 @@ import { ContextOverflowError } from "../shared/errors.js";
2
2
  /**
3
3
  * 上下文安全护栏插件。
4
4
  *
5
- * 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens):
6
- * - 若用量超过 maxTokens × ratio,说明上下文已严重超限(通常因读入超大文件),
7
- * 在请求前抛出 ContextOverflowError,**中断对话**,交由上层向用户报错。
5
+ * 在每次内循环**发请求前**检测上一轮用量(usage.prompt_tokens):超过 `guardTokens`
6
+ * 即抛 `ContextOverflowError` 中断对话,交由上层向用户报错。
8
7
  *
9
8
  * 与迁移插件职责分离:
10
- * - ContextGuardPlugin 只做「严重超限即中断」的安全兜底,绝不继续对话/迁移;
11
- * - AutoMigratePlugin 在正常范围内(超 maxTokens 但未达硬上限)做交接迁移。
9
+ * - `ContextGuardPlugin`:硬线,只做「严重超限即中断」的安全兜底,绝不继续对话/迁移;
10
+ * - `AutoMigratePlugin`:软线(`tokenThresholds.handoff`),在正常范围内做交接迁移。
11
+ * 两条线都是绝对值,`guard` 应当明显大于 `handoff`,交接回合的额外请求才有余量。
12
12
  *
13
13
  * ```ts
14
- * agent.use(new ContextGuardPlugin({ maxTokens: 250_000 }));
15
- * agent.use(new AutoMigratePlugin({
16
- * service: new TaskMigrationService({ onMigrated }),
17
- * maxTokens: 250_000,
18
- * }));
14
+ * const { handoff, guard } = config.models[i].tokenThresholds;
15
+ * agent.use(new ContextGuardPlugin({ guardTokens: guard }));
16
+ * agent.use(new AutoMigratePlugin({ service: handoffService, handoffTokens: handoff }));
19
17
  * ```
20
18
  */
21
19
  export class ContextGuardPlugin {
22
- options;
20
+ guardTokens;
23
21
  constructor(options) {
24
- this.options = options;
22
+ this.guardTokens = options.guardTokens;
25
23
  }
26
24
  async onInnerLoopStart(ctx) {
27
- const { agent } = ctx;
28
- const { maxTokens, ratio = 1.5 } = this.options;
25
+ const promptTokens = ctx.agent.lastUsage?.prompt_tokens;
29
26
  // 首轮请求前尚无用量数据,跳过(只能在发出首轮请求、拿到 usage 后才有据可查)
30
- const promptTokens = agent.lastUsage?.prompt_tokens;
31
27
  if (promptTokens == null)
32
28
  return;
33
- if (promptTokens > maxTokens * ratio) {
34
- throw new ContextOverflowError(promptTokens, maxTokens, ratio);
29
+ if (promptTokens > this.guardTokens) {
30
+ throw new ContextOverflowError(promptTokens, this.guardTokens);
35
31
  }
36
32
  }
37
33
  }
@@ -1,15 +1,23 @@
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
  */
5
5
  export declare class ConfigManager {
6
6
  readonly configPath: string;
7
7
  readonly basePath: string;
8
+ /** 当前配置里的模型 id 集合(bootstrap 时填充,供默认 Agent 校验 modelId) */
9
+ private knownModelIds;
8
10
  constructor(configPath: string);
9
11
  /**
10
12
  * 读取配置。文件不存在时返回出厂默认配置。
13
+ *
14
+ * 读到即**迁移**(`config/migrate.ts`):上层拿到的永远是当前字段形状,
15
+ * 旧字段(`maxContextTokens` / `maxContextChars`)不会漏到调用方。
16
+ * 迁移结果不在这里落盘——落盘只在 `ensureDefaultConfig()` 做一次。
11
17
  */
12
18
  read(): Promise<AppConfig>;
19
+ /** 读磁盘原文(不做任何迁移/同步),供 `ensureDefaultConfig()` 对比写盘用 */
20
+ private readRaw;
13
21
  write(config: AppConfig): Promise<void>;
14
22
  /**
15
23
  * 确保基础目录结构存在。
@@ -28,13 +36,20 @@ export declare class ConfigManager {
28
36
  /**
29
37
  * 确保 basePath/agents/default.json 存在。
30
38
  *
31
- * - default.json 已存在且 custom === true → 用户声明为自定义,原样返回,不做任何改动
32
- * - default.json 已存在且 custom !== true → 仅同步出厂提示词(messages),
33
- * 其余字段(name/permissions/modelId 等)保留;提示词一致时不写盘
34
- * - agents/ 为空 → 写入默认 Agent
39
+ * - default.json 已存在且 custom === true → 用户声明为自定义,不替换提示词(缺 modelId 仍补)
40
+ * - default.json 已存在且 custom !== true → 同步出厂提示词(messages),
41
+ * 其余字段(name/permissions 等)保留;提示词与 modelId 都一致时不写盘
42
+ * - agents/ 为空 → 写入默认 Agent(含 modelId)
35
43
  * - 已有其他 Agent → 返回 null
44
+ *
45
+ * 模型真相源只有 `AgentDefinition.modelId`:这里保证默认 Agent **一定有** modelId,
46
+ * 运行期不再有任何 `config.defaultModel` 兜底。
47
+ *
48
+ * @param legacyDefaultModel 老配置里的 `config.defaultModel`。仅在 Agent **既无 modelId**
49
+ * 且该模型仍在模型清单里时用它补齐,让升级不改变用户实际在用的模型;之后
50
+ * 这个字段就彻底不存在了。
36
51
  */
37
- ensureDefaultAgent(): Promise<AgentDefinition | null>;
52
+ ensureDefaultAgent(legacyDefaultModel?: string): Promise<AgentDefinition | null>;
38
53
  /**
39
54
  * 确保 basePath/sub-agents/{DEFAULT_SUBAGENT_ID}.json 存在。
40
55
  *
@@ -52,6 +67,38 @@ export declare class ConfigManager {
52
67
  agent: AgentDefinition | null;
53
68
  subAgent: AgentDefinition | null;
54
69
  }>;
70
+ /** 策略表文件路径(与 config.json 同级) */
71
+ get reasoningPresetsPath(): string;
72
+ /**
73
+ * 读取思考策略表。
74
+ *
75
+ * 文件缺失或无法解析时返回出厂种子(内存值,不写盘)——保证调用方永远拿到可用的表:
76
+ * 用户误删文件后客户端仍能正常选档位,下次 `ensureReasoningPresets()` 会重新释放。
77
+ */
78
+ readReasoningPresets(): Promise<ReasoningPresets>;
79
+ /**
80
+ * 释放 / 同步出厂思考策略。
81
+ *
82
+ * 覆盖策略与 `syncManagedModels` 同构,**按策略逐个判断**(粒度到单个策略):
83
+ * - 文件不存在 → 写入出厂种子;
84
+ * - 某策略标了 `custom: true` → 用户自有,原样保留;
85
+ * - 某策略未标 → 用出厂版覆盖该策略(内容相同则不写盘);
86
+ * - 用户自建的非出厂策略 → 始终保留。
87
+ *
88
+ * @returns 实际写入的策略名列表(未变化 / 用户自有 均不计入)
89
+ */
90
+ ensureReasoningPresets(): Promise<string[]>;
91
+ /**
92
+ * 释放 / 同步出厂 Skill。
93
+ *
94
+ * 覆盖策略与 `syncManagedModels` 同构:
95
+ * - 磁盘上不存在 → 写入出厂内容;
96
+ * - 存在且 frontmatter 标了 `custom: "true"` → 用户自有,原样保留;
97
+ * - 存在但未标 → 视为出厂托管,同步为当前版本内容(内容相同则不写盘)。
98
+ *
99
+ * @returns 实际写入的 Skill id 列表(内容未变化 / 用户自有 均不计入)
100
+ */
101
+ ensureFactorySkills(): Promise<string[]>;
55
102
  /** mcp.json 路径:{basePath}/mcp.json */
56
103
  get mcpPath(): string;
57
104
  /**
@@ -81,10 +128,12 @@ export declare class ConfigManager {
81
128
  * 规则:
82
129
  * - 未标 `custom: true` 的条目 → 由 SDK 托管,从结果中移除,改用 defaults 中的最新定义;
83
130
  * - 标了 `custom: true` 的条目 → 用户自有,原样保留;与出厂定义同 id 时用户版本优先;
84
- * - 默认模型 ID(defaultModel / defaultImageModel / defaultMigrationModel)
131
+ * - 默认模型 ID(defaultImageModel / defaultMigrationModel)
85
132
  * 在合并后若已不存在 → 回退到出厂默认,避免悬空引用。
86
133
  *
134
+ * 注意 `defaultModel` 已不再存在:模型真相源是 `AgentDefinition.modelId`。
135
+ *
87
136
  * 端点(endpoints)不参与托管,始终保持用户配置。
88
137
  */
89
138
  export declare function syncManagedModels(config: AppConfig): AppConfig;
90
- export { DEFAULT_AGENT_ID, DEFAULT_AGENT_DEFINITION, DEFAULT_SUBAGENT_ID, DEFAULT_SUBAGENT_DEFINITION, DEFAULT_APP_CONFIG, DEFAULT_MCP_CONFIG, CONFIG_SUB_DIRS, } from "./constants.js";
139
+ export { DEFAULT_AGENT_ID, DEFAULT_AGENT_DEFINITION, DEFAULT_MODEL_ID, DEFAULT_SUBAGENT_ID, DEFAULT_SUBAGENT_DEFINITION, DEFAULT_APP_CONFIG, DEFAULT_MCP_CONFIG, CONFIG_SUB_DIRS, } from "./constants.js";
@@ -1,12 +1,17 @@
1
1
  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
+ 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";
4
7
  /**
5
8
  * 配置管理器 — 负责配置文件的读写、目录初始化、默认实体创建。
6
9
  */
7
10
  export class ConfigManager {
8
11
  configPath;
9
12
  basePath;
13
+ /** 当前配置里的模型 id 集合(bootstrap 时填充,供默认 Agent 校验 modelId) */
14
+ knownModelIds = new Set();
10
15
  constructor(configPath) {
11
16
  this.configPath = configPath;
12
17
  this.basePath = dirname(configPath);
@@ -16,13 +21,22 @@ export class ConfigManager {
16
21
  // -----------------------------------------------------------------------
17
22
  /**
18
23
  * 读取配置。文件不存在时返回出厂默认配置。
24
+ *
25
+ * 读到即**迁移**(`config/migrate.ts`):上层拿到的永远是当前字段形状,
26
+ * 旧字段(`maxContextTokens` / `maxContextChars`)不会漏到调用方。
27
+ * 迁移结果不在这里落盘——落盘只在 `ensureDefaultConfig()` 做一次。
19
28
  */
20
29
  async read() {
30
+ const raw = await this.readRaw();
31
+ return raw ? migrateConfig(raw).config : { ...DEFAULT_APP_CONFIG };
32
+ }
33
+ /** 读磁盘原文(不做任何迁移/同步),供 `ensureDefaultConfig()` 对比写盘用 */
34
+ async readRaw() {
21
35
  try {
22
36
  await fs.access(this.configPath);
23
37
  }
24
38
  catch {
25
- return { ...DEFAULT_APP_CONFIG };
39
+ return null;
26
40
  }
27
41
  const raw = await fs.readFile(this.configPath, "utf-8");
28
42
  return JSON.parse(raw);
@@ -61,18 +75,16 @@ export class ConfigManager {
61
75
  * 条目视为用户自有,原样保留。同步结果与磁盘内容一致时不写盘。
62
76
  */
63
77
  async ensureDefaultConfig() {
64
- let existing;
65
- try {
66
- await fs.access(this.configPath);
67
- existing = await this.read();
68
- }
69
- catch {
78
+ const onDisk = await this.readRaw();
79
+ if (!onDisk) {
70
80
  await this.ensureDirs();
71
81
  await this.write(DEFAULT_APP_CONFIG);
72
82
  return { ...DEFAULT_APP_CONFIG };
73
83
  }
74
- const synced = syncManagedModels(existing);
75
- if (JSON.stringify(synced) !== JSON.stringify(existing)) {
84
+ // 先迁移历史字段,再同步出厂模型清单;两者结果与磁盘原文不同才写盘
85
+ const migrated = migrateConfig(onDisk).config;
86
+ const synced = syncManagedModels(migrated);
87
+ if (JSON.stringify(synced) !== JSON.stringify(onDisk)) {
76
88
  await this.write(synced);
77
89
  }
78
90
  return synced;
@@ -80,28 +92,48 @@ export class ConfigManager {
80
92
  /**
81
93
  * 确保 basePath/agents/default.json 存在。
82
94
  *
83
- * - default.json 已存在且 custom === true → 用户声明为自定义,原样返回,不做任何改动
84
- * - default.json 已存在且 custom !== true → 仅同步出厂提示词(messages),
85
- * 其余字段(name/permissions/modelId 等)保留;提示词一致时不写盘
86
- * - agents/ 为空 → 写入默认 Agent
95
+ * - default.json 已存在且 custom === true → 用户声明为自定义,不替换提示词(缺 modelId 仍补)
96
+ * - default.json 已存在且 custom !== true → 同步出厂提示词(messages),
97
+ * 其余字段(name/permissions 等)保留;提示词与 modelId 都一致时不写盘
98
+ * - agents/ 为空 → 写入默认 Agent(含 modelId)
87
99
  * - 已有其他 Agent → 返回 null
100
+ *
101
+ * 模型真相源只有 `AgentDefinition.modelId`:这里保证默认 Agent **一定有** modelId,
102
+ * 运行期不再有任何 `config.defaultModel` 兜底。
103
+ *
104
+ * @param legacyDefaultModel 老配置里的 `config.defaultModel`。仅在 Agent **既无 modelId**
105
+ * 且该模型仍在模型清单里时用它补齐,让升级不改变用户实际在用的模型;之后
106
+ * 这个字段就彻底不存在了。
88
107
  */
89
- async ensureDefaultAgent() {
108
+ async ensureDefaultAgent(legacyDefaultModel) {
90
109
  const agentsDir = join(this.basePath, "agents");
91
110
  const defaultPath = join(agentsDir, `${DEFAULT_AGENT_ID}.json`);
111
+ const fallbackModelId = legacyDefaultModel && this.knownModelIds.has(legacyDefaultModel)
112
+ ? legacyDefaultModel
113
+ : DEFAULT_AGENT_DEFINITION.modelId;
92
114
  try {
93
115
  const existing = JSON.parse(await fs.readFile(defaultPath, "utf-8"));
94
- // 用户声明为自定义 → 完全跳过替换
95
- if (existing.custom === true)
96
- return existing;
116
+ // 缺 modelId → 补上(历史配置升级路径)
117
+ const withModel = existing.modelId
118
+ ? existing
119
+ : { ...existing, modelId: fallbackModelId };
120
+ const modelPatched = !existing.modelId;
121
+ // 用户声明为自定义 → 不替换提示词,但补上的 modelId 要落盘
122
+ if (withModel.custom === true) {
123
+ if (modelPatched) {
124
+ await fs.writeFile(defaultPath, JSON.stringify(withModel, null, 2), "utf-8");
125
+ }
126
+ return withModel;
127
+ }
97
128
  // 仅同步提示词;内容一致则不写盘、不动时间戳
98
129
  const messages = DEFAULT_AGENT_DEFINITION.messages;
99
- if (sameMessages(existing.messages ?? [], messages))
100
- return existing;
130
+ const sameMsgs = sameMessages(withModel.messages ?? [], messages);
131
+ if (sameMsgs && !modelPatched)
132
+ return withModel;
101
133
  const updated = {
102
- ...existing,
134
+ ...withModel,
103
135
  messages,
104
- updatedAt: new Date().toISOString(),
136
+ updatedAt: sameMsgs ? withModel.updatedAt : new Date().toISOString(),
105
137
  };
106
138
  await fs.writeFile(defaultPath, JSON.stringify(updated, null, 2), "utf-8");
107
139
  return updated;
@@ -174,13 +206,121 @@ export class ConfigManager {
174
206
  */
175
207
  async bootstrap() {
176
208
  await this.ensureDirs();
209
+ // 迁移前读一次磁盘原文:只为了拿旧的 `defaultModel`(字段已从类型里移除),
210
+ // 让升级不改变用户原本在用的模型
211
+ const raw = (await this.readRaw());
177
212
  const config = await this.ensureDefaultConfig();
178
- const agent = await this.ensureDefaultAgent();
213
+ this.knownModelIds = new Set((config.models ?? []).map((m) => m.id));
214
+ const agent = await this.ensureDefaultAgent(raw?.defaultModel);
179
215
  const subAgent = await this.ensureDefaultSubAgent();
180
216
  await this.ensureDefaultMcpConfig();
217
+ await this.ensureFactorySkills();
218
+ await this.ensureReasoningPresets();
181
219
  return { config, agent, subAgent };
182
220
  }
183
221
  // -----------------------------------------------------------------------
222
+ // 思考策略表({basePath}/reasoning-presets.json)
223
+ // -----------------------------------------------------------------------
224
+ /** 策略表文件路径(与 config.json 同级) */
225
+ get reasoningPresetsPath() {
226
+ return join(this.basePath, REASONING_PRESETS_FILE);
227
+ }
228
+ /**
229
+ * 读取思考策略表。
230
+ *
231
+ * 文件缺失或无法解析时返回出厂种子(内存值,不写盘)——保证调用方永远拿到可用的表:
232
+ * 用户误删文件后客户端仍能正常选档位,下次 `ensureReasoningPresets()` 会重新释放。
233
+ */
234
+ async readReasoningPresets() {
235
+ try {
236
+ const raw = await fs.readFile(this.reasoningPresetsPath, "utf-8");
237
+ const parsed = JSON.parse(raw);
238
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
239
+ return parsed;
240
+ }
241
+ catch {
242
+ // 缺失或损坏 → 回落出厂种子
243
+ }
244
+ return { ...FACTORY_REASONING_PRESETS };
245
+ }
246
+ /**
247
+ * 释放 / 同步出厂思考策略。
248
+ *
249
+ * 覆盖策略与 `syncManagedModels` 同构,**按策略逐个判断**(粒度到单个策略):
250
+ * - 文件不存在 → 写入出厂种子;
251
+ * - 某策略标了 `custom: true` → 用户自有,原样保留;
252
+ * - 某策略未标 → 用出厂版覆盖该策略(内容相同则不写盘);
253
+ * - 用户自建的非出厂策略 → 始终保留。
254
+ *
255
+ * @returns 实际写入的策略名列表(未变化 / 用户自有 均不计入)
256
+ */
257
+ async ensureReasoningPresets() {
258
+ let onDisk = null;
259
+ try {
260
+ onDisk = JSON.parse(await fs.readFile(this.reasoningPresetsPath, "utf-8"));
261
+ }
262
+ catch {
263
+ onDisk = null;
264
+ }
265
+ if (!onDisk || typeof onDisk !== "object" || Array.isArray(onDisk)) {
266
+ await fs.mkdir(this.basePath, { recursive: true });
267
+ await fs.writeFile(this.reasoningPresetsPath, JSON.stringify(FACTORY_REASONING_PRESETS, null, 2), "utf-8");
268
+ return Object.keys(FACTORY_REASONING_PRESETS);
269
+ }
270
+ const next = { ...onDisk };
271
+ const written = [];
272
+ for (const [name, preset] of Object.entries(FACTORY_REASONING_PRESETS)) {
273
+ const existing = onDisk[name];
274
+ if (existing?.custom === true)
275
+ continue; // 用户改过 → 冻结
276
+ if (JSON.stringify(existing) === JSON.stringify(preset))
277
+ continue; // 已是最新
278
+ next[name] = preset;
279
+ written.push(name);
280
+ }
281
+ if (written.length > 0) {
282
+ await fs.writeFile(this.reasoningPresetsPath, JSON.stringify(next, null, 2), "utf-8");
283
+ }
284
+ return written;
285
+ }
286
+ // -----------------------------------------------------------------------
287
+ // 出厂 Skill({basePath}/skills/<id>/SKILL.md)
288
+ // -----------------------------------------------------------------------
289
+ /**
290
+ * 释放 / 同步出厂 Skill。
291
+ *
292
+ * 覆盖策略与 `syncManagedModels` 同构:
293
+ * - 磁盘上不存在 → 写入出厂内容;
294
+ * - 存在且 frontmatter 标了 `custom: "true"` → 用户自有,原样保留;
295
+ * - 存在但未标 → 视为出厂托管,同步为当前版本内容(内容相同则不写盘)。
296
+ *
297
+ * @returns 实际写入的 Skill id 列表(内容未变化 / 用户自有 均不计入)
298
+ */
299
+ async ensureFactorySkills() {
300
+ const written = [];
301
+ for (const skill of FACTORY_SKILLS) {
302
+ const dir = join(this.basePath, "skills", skill.id);
303
+ const file = join(dir, "SKILL.md");
304
+ let existing = null;
305
+ try {
306
+ existing = await fs.readFile(file, "utf-8");
307
+ }
308
+ catch {
309
+ existing = null;
310
+ }
311
+ // 用户声明自有 → 永不覆盖
312
+ if (existing != null && isCustomSkill(existing))
313
+ continue;
314
+ // 内容已一致 → 不重复写盘
315
+ if (existing === skill.content)
316
+ continue;
317
+ await fs.mkdir(dir, { recursive: true });
318
+ await fs.writeFile(file, skill.content, "utf-8");
319
+ written.push(skill.id);
320
+ }
321
+ return written;
322
+ }
323
+ // -----------------------------------------------------------------------
184
324
  // MCP 配置(~/.ai-zen/mcp.json)
185
325
  // -----------------------------------------------------------------------
186
326
  /** mcp.json 路径:{basePath}/mcp.json */
@@ -251,9 +391,11 @@ export class ConfigManager {
251
391
  * 规则:
252
392
  * - 未标 `custom: true` 的条目 → 由 SDK 托管,从结果中移除,改用 defaults 中的最新定义;
253
393
  * - 标了 `custom: true` 的条目 → 用户自有,原样保留;与出厂定义同 id 时用户版本优先;
254
- * - 默认模型 ID(defaultModel / defaultImageModel / defaultMigrationModel)
394
+ * - 默认模型 ID(defaultImageModel / defaultMigrationModel)
255
395
  * 在合并后若已不存在 → 回退到出厂默认,避免悬空引用。
256
396
  *
397
+ * 注意 `defaultModel` 已不再存在:模型真相源是 `AgentDefinition.modelId`。
398
+ *
257
399
  * 端点(endpoints)不参与托管,始终保持用户配置。
258
400
  */
259
401
  export function syncManagedModels(config) {
@@ -272,7 +414,6 @@ export function syncManagedModels(config) {
272
414
  ...config,
273
415
  models,
274
416
  imageModels,
275
- defaultModel: resolve(config.defaultModel, modelIds, DEFAULT_APP_CONFIG.defaultModel),
276
417
  defaultImageModel: resolve(config.defaultImageModel, imageIds, DEFAULT_APP_CONFIG.defaultImageModel),
277
418
  defaultMigrationModel: resolve(config.defaultMigrationModel, modelIds, DEFAULT_APP_CONFIG.defaultMigrationModel),
278
419
  };
@@ -289,4 +430,4 @@ function sameMessages(a, b) {
289
430
  return a.every((message, index) => message.role === b[index].role &&
290
431
  JSON.stringify(message.content) === JSON.stringify(b[index].content));
291
432
  }
292
- export { DEFAULT_AGENT_ID, DEFAULT_AGENT_DEFINITION, DEFAULT_SUBAGENT_ID, DEFAULT_SUBAGENT_DEFINITION, DEFAULT_APP_CONFIG, DEFAULT_MCP_CONFIG, CONFIG_SUB_DIRS, } from "./constants.js";
433
+ export { DEFAULT_AGENT_ID, DEFAULT_AGENT_DEFINITION, DEFAULT_MODEL_ID, DEFAULT_SUBAGENT_ID, DEFAULT_SUBAGENT_DEFINITION, DEFAULT_APP_CONFIG, DEFAULT_MCP_CONFIG, CONFIG_SUB_DIRS, } from "./constants.js";
@@ -1,6 +1,13 @@
1
1
  import type { AgentDefinition, AppConfig } from "../types/index.js";
2
2
  /** 默认 Agent ID */
3
3
  export declare const DEFAULT_AGENT_ID = "default";
4
+ /**
5
+ * 出厂默认模型 id。
6
+ *
7
+ * 模型的**唯一真相源是 `AgentDefinition.modelId`**:配置里不再有 `defaultModel`,
8
+ * 会话存档里也不记模型。这个常量只用于给出厂默认 Agent 填一个初始值。
9
+ */
10
+ export declare const DEFAULT_MODEL_ID = "deepseek-flash";
4
11
  /** 默认 Agent 定义(不含时间戳) */
5
12
  export declare const DEFAULT_AGENT_DEFINITION: Omit<AgentDefinition, "createdAt" | "updatedAt">;
6
13
  /** 默认 SubAgent ID */