@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
@@ -8,17 +8,13 @@ export declare class SdkError extends Error {
8
8
  }
9
9
  /**
10
10
  * 上下文超限错误。由 ContextGuardPlugin 在请求前抛出,用于中断对话。
11
- * 触发条件通常是上下文 token 用量远超阈值(如读入超大文件),
11
+ * 触发条件通常是上下文 token 用量超过硬线(如读入超大文件),
12
12
  * 继续对话已无意义且会持续放大费用/延迟,故由上层捕获后向用户显式报错。
13
13
  */
14
14
  export declare class ContextOverflowError extends Error {
15
15
  /** 触发中断时的刚用量(usage.prompt_tokens) */
16
16
  readonly promptTokens: number;
17
- /** 配置的告警阈值(maxTokens) */
18
- readonly maxTokens: number;
19
- /** 实际生效的硬上限(maxTokens × ratio,越界即中断) */
20
- readonly threshold: number;
21
- /** 超出比例(如 1.2 表示 20%) */
22
- readonly ratio: number;
23
- constructor(promptTokens: number, maxTokens: number, ratio: number);
17
+ /** 被越过的强行阻断阈值(`tokenThresholds.guard`,绝对值) */
18
+ readonly guardTokens: number;
19
+ constructor(promptTokens: number, guardTokens: number);
24
20
  }
@@ -15,26 +15,20 @@ export class SdkError extends Error {
15
15
  }
16
16
  /**
17
17
  * 上下文超限错误。由 ContextGuardPlugin 在请求前抛出,用于中断对话。
18
- * 触发条件通常是上下文 token 用量远超阈值(如读入超大文件),
18
+ * 触发条件通常是上下文 token 用量超过硬线(如读入超大文件),
19
19
  * 继续对话已无意义且会持续放大费用/延迟,故由上层捕获后向用户显式报错。
20
20
  */
21
21
  export class ContextOverflowError extends Error {
22
22
  /** 触发中断时的刚用量(usage.prompt_tokens) */
23
23
  promptTokens;
24
- /** 配置的告警阈值(maxTokens) */
25
- maxTokens;
26
- /** 实际生效的硬上限(maxTokens × ratio,越界即中断) */
27
- threshold;
28
- /** 超出比例(如 1.2 表示 20%) */
29
- ratio;
30
- constructor(promptTokens, maxTokens, ratio) {
31
- const threshold = maxTokens * ratio;
32
- super(`🛑 上下文严重超限:已用 ${promptTokens} token,超过硬上限 ${threshold}(${Math.round((ratio - 1) * 100)}%,配置上限 ${maxTokens})。` +
33
- `当前对话可能因读入超大文件而失控,已中断,请检查并重新开始。`);
24
+ /** 被越过的强行阻断阈值(`tokenThresholds.guard`,绝对值) */
25
+ guardTokens;
26
+ constructor(promptTokens, guardTokens) {
27
+ super(`🛑 上下文严重超限:已用 ${promptTokens} token,超过阻断阈值 ${guardTokens}。` +
28
+ `当前对话可能因读入超大文件而失控,已中断,请检查并重新开始。` +
29
+ `(如确需更长上下文,请调高 config.json 中该模型的 tokenThresholds.guard。)`);
34
30
  this.name = "ContextOverflowError";
35
31
  this.promptTokens = promptTokens;
36
- this.maxTokens = maxTokens;
37
- this.threshold = threshold;
38
- this.ratio = ratio;
32
+ this.guardTokens = guardTokens;
39
33
  }
40
34
  }
@@ -12,6 +12,58 @@ export interface AgentPermissions {
12
12
  mcps?: PermissionPolicy;
13
13
  subagents?: PermissionPolicy;
14
14
  }
15
+ /**
16
+ * 端点能力声明 —— 决定「本地文件 → 内容块」时走哪条传输路径。
17
+ *
18
+ * 之前这段判断硬编码在 ViewImageTool 里(`endpoint.id === "deepseek"`),
19
+ * 用户改一下端点 id 就失效;改为显式能力位后,新端点只要声明能力即可享受上传。
20
+ */
21
+ export interface EndpointCapabilities {
22
+ /**
23
+ * 是否支持 OpenAI 兼容的 Files API(`POST {baseUrl}/files`,`purpose=user_data`)。
24
+ * 为 true 时本地文件优先上传并以 `file` 块(file_id)发送;否则走 base64 内联。
25
+ */
26
+ filesApi?: boolean;
27
+ }
28
+ /**
29
+ * 模型请求参数(`defaultParams` / `modelParams` 的形状)。
30
+ *
31
+ * 含厂商特有字段(`thinking` / `reasoning_effort` 等),SDK **原样透传**、不解释。
32
+ */
33
+ export type ModelParams = Record<string, unknown>;
34
+ /**
35
+ * 单个档位的请求参数补丁。
36
+ *
37
+ * 各家协议差异全部关在补丁里,不硬编码任何键名:
38
+ * OpenAI → `{ reasoning_effort: "high" }`(没有 `thinking` 参数)
39
+ * DeepSeek → `{ thinking: { type: "enabled" }, reasoning_effort: "high" }`
40
+ * 关闭 → `{ reasoning_effort: "none" }` 或 `{ thinking: { type: "disabled" } }`
41
+ */
42
+ export type ReasoningPatch = ModelParams;
43
+ /**
44
+ * 一个具名思考策略:档位名 → 参数补丁。
45
+ *
46
+ * `levels` 的键集合**就是**该策略的全部可选档位 —— 没有单独的「能否关闭思考」字段:
47
+ * 表里有 `off` 键就能关,没有就不能(如智谱 GLM-5.3 系,官方原文「其余输入将报错」)。
48
+ */
49
+ export interface ReasoningPreset {
50
+ /** 用户改过 → SDK 初始化不再覆盖该策略(与 `Model.custom` 同构) */
51
+ custom?: boolean;
52
+ /** 档位名 → 参数补丁 */
53
+ levels: Record<string, ReasoningPatch>;
54
+ }
55
+ /**
56
+ * 思考策略表 —— `<basePath>/reasoning-presets.json` 的结构,出厂内容见
57
+ * `config/reasoning-presets.ts`。
58
+ *
59
+ * 这份表是 **CLI / 桌面端等客户端共享的资产**:它定义「有哪些档位、每档写什么参数」,
60
+ * 由 `Model.reasoningPreset` 引用。客户端据此限制可选强度、把选中的档位展开成参数
61
+ * 写进 `AgentDefinition.modelParams`。
62
+ *
63
+ * **SDK 自身不消费它生成请求参数**:参数合并只是 `{...defaultParams, ...modelParams}`
64
+ * 一次浅合并(见 `createModel`)。表在这里只负责「定义 + 出厂默认 + 读写」。
65
+ */
66
+ export type ReasoningPresets = Record<string, ReasoningPreset>;
15
67
  /** API 端点 */
16
68
  export interface Endpoint {
17
69
  id: string;
@@ -19,6 +71,25 @@ export interface Endpoint {
19
71
  baseUrl: string;
20
72
  apiKey: string;
21
73
  description?: string;
74
+ capabilities?: EndpointCapabilities;
75
+ }
76
+ /**
77
+ * 上下文用量阈值 —— 与模型的真实窗口无关,只描述「用量到了这条线该怎么办」。
78
+ *
79
+ * 两个阈值语义不同,消费方也不同:
80
+ * - `handoff`:软线。`AutoMigratePlugin` 在响应返回后拿 `usage.prompt_tokens` 比它,
81
+ * 超过即触发任务交接(交接本身还要再发 1~2 次请求,所以这条线必须留足余量)。
82
+ * - `guard`:硬线。`ContextGuardPlugin` 在**每次请求前**比它,超过即抛
83
+ * `ContextOverflowError` 中断本轮——这是最后一道保护,宁可报错也不要发出必然失败的请求。
84
+ *
85
+ * 建议 `guard ≈ handoff × 1.5`,给交接留出缓冲;两者都是绝对值,配置里写多少就是多少,
86
+ * 不做任何倍数换算(阈值就该是能直接读懂的数字)。
87
+ */
88
+ export interface TokenThresholds {
89
+ /** 交接阈值(软线):prompt_tokens 超过它 → 触发任务交接 */
90
+ handoff: number;
91
+ /** 强行阻断阈值(硬线):请求前用量超过它 → 中断本轮并报错 */
92
+ guard: number;
22
93
  }
23
94
  /** 模型配置,绑定一个 Endpoint */
24
95
  export interface Model {
@@ -27,16 +98,22 @@ export interface Model {
27
98
  endpointId: string;
28
99
  modelName?: string;
29
100
  /**
30
- * 任务迁移(交接)触发阈值 —— **不是**模型的真实上下文窗口。
101
+ * 上下文用量阈值(交接软线 + 阻断硬线)。**不是**模型的真实上下文窗口。
31
102
  *
32
- * AutoMigratePlugin 不估算、不学习:只在 API 响应返回后,用 `usage.prompt_tokens`
33
- * 与该值比较,超过即触发交接迁移。建议取模型真实窗口的约 25%,为 response 留出空间。
103
+ * 历史字段 `maxContextTokens` / `maxContextChars` 已由 `config/migrate.ts`
104
+ * 迁移成这个结构体并删除。
34
105
  */
35
- maxContextTokens: number;
36
- /** 旧版字符数阈值(兼容迁移) */
37
- maxContextChars?: number;
106
+ tokenThresholds: TokenThresholds;
38
107
  defaultParams?: Record<string, unknown>;
39
- /** 是否支持图片输入(视觉模型)。看图工具(viewImage)仅对视觉模型启用;generateImage 是否返回图片内容块也据此判断 */
108
+ /**
109
+ * 思考策略名,引用 `reasoning-presets.json` 里的键。
110
+ *
111
+ * 它是**跨客户端共享的元数据**:CLI / 桌面端据此判断这个模型能选哪几档。
112
+ * SDK 不拿它去生成请求参数(那条路只有 `defaultParams` + `modelParams` 浅合并),
113
+ * 只保证该字段能随模型配置一起读写与出厂同步。
114
+ */
115
+ reasoningPreset?: string;
116
+ /** 是否支持图片输入(视觉模型)。看图工具(view_image)仅对视觉模型启用;generate_image 是否返回图片内容块也据此判断 */
40
117
  vision?: boolean;
41
118
  description?: string;
42
119
  version?: number;
@@ -54,7 +131,24 @@ export interface AgentDefinition {
54
131
  name: string;
55
132
  description?: string;
56
133
  messages: AgentNS.Message[];
134
+ /**
135
+ * 绑定模型 id —— **运行期唯一真相源**。
136
+ *
137
+ * 缺省即「无模型可用」,`createAgent` 会直接报错;不存在配置层的 `defaultModel`
138
+ * 兜底,也不存在会话级的模型记录。要换模型就改这里(`/model` 写的就是它)。
139
+ */
57
140
  modelId?: string;
141
+ /**
142
+ * Agent 级模型参数覆盖,浅合并进模型的 `defaultParams`。
143
+ *
144
+ * SDK 只做 `{ ...model.defaultParams, ...modelParams }`,不解释任何键名、不做
145
+ * 档位推导、不知道厂商差异。於是「配置即事实」:应用(如 CLI)在**写入配置时**就把
146
+ * 档位展开成具体参数存在这里,运行时不再查表。
147
+ *
148
+ * 于是同一个模型可以给不同 Agent 配不同强度(编程 Agent `reasoning_effort: "max"`、
149
+ * 闲聊 Agent `{ thinking: { type: "disabled" } }`),而模型自身的 `defaultParams` 不动。
150
+ */
151
+ modelParams?: ModelParams;
58
152
  permissions?: AgentPermissions;
59
153
  function?: AgentNS.FunctionDefine;
60
154
  createdAt: string;
@@ -88,7 +182,6 @@ export interface ImageModel {
88
182
  }
89
183
  /** config.json 结构 */
90
184
  export interface AppConfig {
91
- defaultModel?: string;
92
185
  endpoints: Endpoint[];
93
186
  models: Model[];
94
187
  /** 图片生成模型列表 */
@@ -104,7 +197,7 @@ export interface AppConfig {
104
197
  *
105
198
  * 工具本次输出超过该值时,由工具自身的保护策略处置(落盘或仅警告),
106
199
  * 避免超长输出撑爆上下文。仅作用于"输出",与工具的读取阈值无关
107
- * (如 readFile/findText 对 300KB 文件的读取上限)。
200
+ * (如 read/find_text 对 300KB 文件的读取上限)。
108
201
  */
109
202
  maxToolOutput?: number;
110
203
  }
@@ -112,7 +205,7 @@ export interface AppConfig {
112
205
  * 工具环境 — Scope 的能力插件在实例化内置工具时注入的环境信息。
113
206
  *
114
207
  * cwd 是相对路径解析的基准(每个 Scope 对应一个工作目录);
115
- * config 供需要模型/端点配置的工具(如 generateImage)使用。
208
+ * config 供需要模型/端点配置的工具(如 generate_image)使用。
116
209
  */
117
210
  export interface ToolEnv {
118
211
  /** 当前工作目录 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-zen/agents-sdk",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.21",
4
4
  "description": "AI-Zen Agents SDK — shared business logic for CLI and Desktop",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,10 +16,10 @@
16
16
  "author": "LZQCN",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
+ "@ai-zen/agents-core": "^4.5.3",
19
20
  "@modelcontextprotocol/sdk": "^1.29.0",
20
21
  "dayjs": "^1.11.21",
21
- "openai": "^7.5.0",
22
- "@ai-zen/agents-core": "^4.3.0"
22
+ "openai": "^7.5.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^24.12.2",
@@ -1,128 +0,0 @@
1
- import { AgentNS } from "@ai-zen/agents-core";
2
- import type OpenAI from "openai";
3
- import { type Logger } from "../shared/logger.js";
4
- import type { SdkAgent } from "./SdkAgent.js";
5
- /**
6
- * 迁移上下文 — 贯穿「迁移前 → 生成交接文档 → 迁移后」的结构化数据。
7
- *
8
- * 供 `TaskMigrationService.migrate` 与钩子(`onBeforeMigrate` / `onMigrated`)使用。
9
- * 手动迁移与自动迁移共用同一上下文结构,保证上层(CLI)行为一致。
10
- */
11
- export interface MigrationContext {
12
- /** 当前对话 Agent(引用不变,仅替换消息列表) */
13
- agent: SdkAgent;
14
- /** 生成交接文档所用模型名 */
15
- model: string;
16
- /** 当前 prompt token 用量(自动迁移时由 AutoMigratePlugin 提供;手动迁移可省略) */
17
- promptTokens?: number;
18
- /** 上下文阈值(自动迁移时由 AutoMigratePlugin 提供;手动迁移可省略) */
19
- maxTokens?: number;
20
- /** 序列化后的完整对话历史(迁移输入) */
21
- historyText: string;
22
- /** 迁移前消息总数 */
23
- messageCountBefore: number;
24
- /** 生成的交接文档(迁移完成后填充) */
25
- handoffDoc?: string;
26
- }
27
- /**
28
- * 迁移后的历史处理策略。
29
- *
30
- * - `omit`(默认):将历史消息标记 `omit: true`,仍保留在 `agent.messages` 可审计/回放,但不再发送给模型;
31
- * - `prune`:物理剔除历史,只保留 `definition.messages`(系统提示等模板)+ 追加的对话断点。
32
- */
33
- export type MigrationStrategy = "omit" | "prune";
34
- /**
35
- * 任务迁移服务构造参数。
36
- */
37
- export interface TaskMigrationServiceOptions {
38
- /** 生成交接文档所用客户端(可选;未传则 migrate 时回退到 agent.client) */
39
- client?: OpenAI;
40
- /** 生成交接文档所用模型名(可选;未传则 migrate 时回退到 agent.model) */
41
- model?: string;
42
- /** 生成交接文档所用模型参数(可选;未传则 migrate 时回退到 agent.modelConfig) */
43
- modelConfig?: Record<string, unknown>;
44
- /** 迁移后历史处理策略,默认 `omit`(标记历史保留可审计)。可选 `prune`(物理剔除)。 */
45
- strategy?: MigrationStrategy;
46
- /** 迁移前钩子(服务持有)。迁移开始前触发,此时 agent.messages 仍为完整旧历史 */
47
- onBeforeMigrate?: (ctx: MigrationContext) => void | Promise<void>;
48
- /** 迁移后钩子(服务持有)。迁移完成后触发,交接文档已注入 agent.messages */
49
- onMigrated?: (ctx: MigrationContext) => void | Promise<void>;
50
- /** 可注入日志(可选,默认使用全局单例) */
51
- logger?: Logger;
52
- }
53
- /**
54
- * 任务迁移服务。
55
- *
56
- * 一个服务实例对应一次迁移上下文。它**不持有**任何模型调用——生成交接文档时
57
- * 默认复用传入 `agent`(`SdkAgent`)自身的 `client` / `model` / `modelConfig`,
58
- * 因此无需 Scope、无需独立「迁移 Agent」对象,也无需自建 OpenAI 客户端。
59
- * 如需用不同模型生成交接文档,可在构造时显式传入 `client` / `model` / `modelConfig`,
60
- * 未传则 migrate 时回退到 agent 自带的配置。
61
- *
62
- * 职责:
63
- * 1. 生成交接文档的提示词 / 注入格式(静态纯工具,供上层使用)
64
- * 2. 执行完整迁移(串行化 → 模型生成 → 按策略处理历史 + 追加对话断点),并暴露迁移前后钩子(实例方法)
65
- *
66
- * 设计上,`AutoMigratePlugin` 只负责「何时触发」(检测 token 超限),
67
- * 实际迁移逻辑统一收敛到本服务,从而让手动迁移与自动迁移复用同一条链路。
68
- */
69
- export declare class TaskMigrationService {
70
- static readonly HANDOFF_SECTIONS: {
71
- readonly breakpoint: "## 💬 对话断点";
72
- readonly completed: "## ✅ 已完成的任务";
73
- readonly pending: "## 📋 未完成的任务";
74
- readonly memory: "## 🧠 重要记忆";
75
- readonly files: "## 📁 文件索引";
76
- readonly instructions: "## 🔔 接手指令";
77
- };
78
- private onBeforeMigrate?;
79
- private onMigrated?;
80
- private client?;
81
- private model?;
82
- private modelConfig?;
83
- private strategy;
84
- private logger;
85
- constructor(options?: TaskMigrationServiceOptions);
86
- static createPrompt(): string;
87
- /**
88
- * 生成迁移后追加的「对话断点」消息(role=user):交接文档 + 接手指令。
89
- * 该消息是 omit 迁移方案中新上下文的起点,模型看到的序列为
90
- * `[definition.messages, 断点消息]`(历史消息已被标记 omit,不再发送)。
91
- */
92
- static createPostMessages(handoffDoc: string): AgentNS.Message[];
93
- /**
94
- * 序列化消息为 `[role]: content` 文本,供模型理解完整对话历史。
95
- */
96
- static serializeMessages(messages: AgentNS.Message[]): string;
97
- /**
98
- * 生成交接文档:使用指定的 client / model / modelConfig 带交接 prompt 生成结构化文档。
99
- */
100
- private generateHandoff;
101
- /**
102
- * 执行完整迁移:序列化历史 → 模型生成交接文档 → 按策略处理历史并追加对话断点 → 触发钩子。
103
- *
104
- * 策略(`strategy`,构造函数默认 / 本参数可覆盖):
105
- * - `omit`(默认):`agent.definition.messages`(模板)保留,其余既有历史标记 `omit: true`(仍存于
106
- * `agent.messages` 可审计/回放但不再发送给模型),末尾追加「对话断点」消息。因此 `formatHistory()`
107
- * 发送给模型的是 `[definition.messages, 断点消息]` —— 历史被压缩但并不删除。
108
- * - `prune`:物理剔除历史,只保留 `definition.messages` + 断点消息(即原先的替换行为)。
109
- *
110
- * 设计约束:
111
- * - 不重建 Agent,保留所有引用与插件绑定(仅重建消息列表)
112
- * - 钩子(onBeforeMigrate / onMigrated)抛错会被捕获并记录,**不中断**迁移流程
113
- * - 生成失败 / 未取到交接文档时抛错,此时 `agent.messages` 保持原样(不会被部分修改)
114
- *
115
- * @param params.agent 当前对话 Agent
116
- * @param params.promptTokens 当前 prompt token 用量(可选)
117
- * @param params.maxTokens 上下文阈值(可选)
118
- * @param params.strategy 本次迁移的历史处理策略(可选,覆盖构造时的默认值)
119
- * @returns 完整的迁移上下文(含生成的交接文档)
120
- */
121
- migrate(params: {
122
- agent: SdkAgent;
123
- promptTokens?: number;
124
- maxTokens?: number;
125
- /** 本次迁移的历史处理策略(可选,覆盖构造时的默认值) */
126
- strategy?: MigrationStrategy;
127
- }): Promise<MigrationContext>;
128
- }
@@ -1,211 +0,0 @@
1
- import { Message } from "@ai-zen/agents-core";
2
- import { getLogger } from "../shared/logger.js";
3
- /**
4
- * 任务迁移服务。
5
- *
6
- * 一个服务实例对应一次迁移上下文。它**不持有**任何模型调用——生成交接文档时
7
- * 默认复用传入 `agent`(`SdkAgent`)自身的 `client` / `model` / `modelConfig`,
8
- * 因此无需 Scope、无需独立「迁移 Agent」对象,也无需自建 OpenAI 客户端。
9
- * 如需用不同模型生成交接文档,可在构造时显式传入 `client` / `model` / `modelConfig`,
10
- * 未传则 migrate 时回退到 agent 自带的配置。
11
- *
12
- * 职责:
13
- * 1. 生成交接文档的提示词 / 注入格式(静态纯工具,供上层使用)
14
- * 2. 执行完整迁移(串行化 → 模型生成 → 按策略处理历史 + 追加对话断点),并暴露迁移前后钩子(实例方法)
15
- *
16
- * 设计上,`AutoMigratePlugin` 只负责「何时触发」(检测 token 超限),
17
- * 实际迁移逻辑统一收敛到本服务,从而让手动迁移与自动迁移复用同一条链路。
18
- */
19
- export class TaskMigrationService {
20
- static HANDOFF_SECTIONS = {
21
- breakpoint: "## 💬 对话断点",
22
- completed: "## ✅ 已完成的任务",
23
- pending: "## 📋 未完成的任务",
24
- memory: "## 🧠 重要记忆",
25
- files: "## 📁 文件索引",
26
- instructions: "## 🔔 接手指令",
27
- };
28
- onBeforeMigrate;
29
- onMigrated;
30
- client;
31
- model;
32
- modelConfig;
33
- strategy;
34
- logger;
35
- constructor(options = {}) {
36
- const { onBeforeMigrate, onMigrated, logger, client, model, modelConfig, strategy } = options;
37
- this.onBeforeMigrate = onBeforeMigrate;
38
- this.onMigrated = onMigrated;
39
- this.client = client;
40
- this.model = model;
41
- this.modelConfig = modelConfig;
42
- this.strategy = strategy ?? "omit";
43
- this.logger = logger ?? getLogger();
44
- }
45
- static createPrompt() {
46
- return [
47
- "根据以下对话历史,生成一篇结构化交接文档。不要做任何解释,只输出交接文档。",
48
- "",
49
- `文档必须包含以下章节:`,
50
- `${TaskMigrationService.HANDOFF_SECTIONS.breakpoint}`,
51
- `- 用户最后说了什么,AI 最后回复了什么`,
52
- "",
53
- `${TaskMigrationService.HANDOFF_SECTIONS.completed}`,
54
- `- 列出已完成的每项任务及其关键产出`,
55
- "",
56
- `${TaskMigrationService.HANDOFF_SECTIONS.pending}`,
57
- `- 按优先级列出未完成的任务、当前进度、下一步`,
58
- "",
59
- `${TaskMigrationService.HANDOFF_SECTIONS.memory}`,
60
- `- 需要新 Agent 记住的关键信息(设计决策、偏好、约定等)`,
61
- "",
62
- `${TaskMigrationService.HANDOFF_SECTIONS.files}`,
63
- `- 涉及的文件及其用途`,
64
- "",
65
- `${TaskMigrationService.HANDOFF_SECTIONS.instructions}`,
66
- `- 接手后建议的操作步骤`,
67
- ].join("\n");
68
- }
69
- /**
70
- * 生成迁移后追加的「对话断点」消息(role=user):交接文档 + 接手指令。
71
- * 该消息是 omit 迁移方案中新上下文的起点,模型看到的序列为
72
- * `[definition.messages, 断点消息]`(历史消息已被标记 omit,不再发送)。
73
- */
74
- static createPostMessages(handoffDoc) {
75
- const content = [
76
- "这是上一轮对话的任务交接文档。请先阅读交接文档,理解上下文后再继续协助用户完成任务。",
77
- "",
78
- "---",
79
- "",
80
- handoffDoc,
81
- "",
82
- "---",
83
- "",
84
- "请确认你已理解以上内容,然后询问用户接下来需要什么帮助。",
85
- ].join("\n");
86
- return [Message.User(content)];
87
- }
88
- /**
89
- * 序列化消息为 `[role]: content` 文本,供模型理解完整对话历史。
90
- */
91
- static serializeMessages(messages) {
92
- return messages
93
- .map((m) => {
94
- const role = m.role ?? "unknown";
95
- const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
96
- return `[${role}]: ${content}`;
97
- })
98
- .join("\n\n");
99
- }
100
- /**
101
- * 生成交接文档:使用指定的 client / model / modelConfig 带交接 prompt 生成结构化文档。
102
- */
103
- async generateHandoff(client, model, modelConfig, historyText) {
104
- const response = await client.chat.completions.create({
105
- model,
106
- ...(modelConfig ?? {}),
107
- messages: [
108
- { role: "system", content: TaskMigrationService.createPrompt() },
109
- { role: "user", content: historyText },
110
- ],
111
- });
112
- const handoffDoc = response.choices[0]?.message?.content;
113
- if (!handoffDoc) {
114
- throw new Error("未能生成任务交接文档");
115
- }
116
- return handoffDoc;
117
- }
118
- /**
119
- * 执行完整迁移:序列化历史 → 模型生成交接文档 → 按策略处理历史并追加对话断点 → 触发钩子。
120
- *
121
- * 策略(`strategy`,构造函数默认 / 本参数可覆盖):
122
- * - `omit`(默认):`agent.definition.messages`(模板)保留,其余既有历史标记 `omit: true`(仍存于
123
- * `agent.messages` 可审计/回放但不再发送给模型),末尾追加「对话断点」消息。因此 `formatHistory()`
124
- * 发送给模型的是 `[definition.messages, 断点消息]` —— 历史被压缩但并不删除。
125
- * - `prune`:物理剔除历史,只保留 `definition.messages` + 断点消息(即原先的替换行为)。
126
- *
127
- * 设计约束:
128
- * - 不重建 Agent,保留所有引用与插件绑定(仅重建消息列表)
129
- * - 钩子(onBeforeMigrate / onMigrated)抛错会被捕获并记录,**不中断**迁移流程
130
- * - 生成失败 / 未取到交接文档时抛错,此时 `agent.messages` 保持原样(不会被部分修改)
131
- *
132
- * @param params.agent 当前对话 Agent
133
- * @param params.promptTokens 当前 prompt token 用量(可选)
134
- * @param params.maxTokens 上下文阈值(可选)
135
- * @param params.strategy 本次迁移的历史处理策略(可选,覆盖构造时的默认值)
136
- * @returns 完整的迁移上下文(含生成的交接文档)
137
- */
138
- async migrate(params) {
139
- const { agent, promptTokens, maxTokens, strategy: overrideStrategy } = params;
140
- const strategy = overrideStrategy ?? this.strategy;
141
- // 序列化有效上下文:过滤已标记 omit 的历史,避免重复迁移时把旧省略历史再次喂给模型。
142
- // historyText 供模型生成交接文档,而 messageCountBefore 仍记录本次迁移前的消息总数。
143
- const historyText = TaskMigrationService.serializeMessages(agent.messages.filter((m) => !m.omit));
144
- const messageCountBefore = agent.messages.length;
145
- // 生成交接文档所用模型调用:优先使用构造时传入的 client/model/modelConfig,
146
- // 未传则回退到 agent 自带的(无需 Scope / 独立迁移 Agent)。
147
- const client = this.client ?? agent.client;
148
- const model = this.model ?? agent.model;
149
- const modelConfig = this.modelConfig ?? agent.modelConfig;
150
- // 迁移前上下文(不含 handoffDoc,独立对象,语义固定)
151
- const beforeCtx = {
152
- agent,
153
- model,
154
- maxTokens,
155
- promptTokens,
156
- historyText,
157
- messageCountBefore,
158
- };
159
- // 迁移前钩子(容错,不中断)
160
- if (this.onBeforeMigrate) {
161
- try {
162
- await this.onBeforeMigrate(beforeCtx);
163
- }
164
- catch (err) {
165
- this.logger.error(`[migrate] onBeforeMigrate 回调失败: ${err?.message ?? err}`);
166
- }
167
- }
168
- // 生成交接文档
169
- const handoffDoc = await this.generateHandoff(client, model, modelConfig, historyText);
170
- // 重建消息列表:按策略处理历史(omit 标记保留 / prune 物理剔除),并追加「对话断点」消息。
171
- // 均用浅拷贝/新数组,避免直接修改 definition.messages 引用的对象(防止污染模板)。
172
- const breakpointMessages = TaskMigrationService.createPostMessages(handoffDoc);
173
- let nextMessages;
174
- if (strategy === "prune") {
175
- // 物理剔除:只保留模板消息 + 断点
176
- nextMessages = [...agent.definition.messages, ...breakpointMessages];
177
- }
178
- else {
179
- // omit(默认):以 definition.messages 长度为界保留模板消息,其余历史统一标记 omit
180
- const preserveCount = Math.min(agent.definition.messages.length, agent.messages.length);
181
- nextMessages = [
182
- ...agent.messages.map((m, i) => ({
183
- ...m,
184
- omit: i >= preserveCount ? true : m.omit,
185
- })),
186
- ...breakpointMessages,
187
- ];
188
- }
189
- agent.messages = nextMessages;
190
- // 迁移后上下文(含 handoffDoc,与迁移前上下文独立)
191
- const afterCtx = {
192
- agent,
193
- model,
194
- maxTokens,
195
- promptTokens,
196
- historyText,
197
- messageCountBefore,
198
- handoffDoc,
199
- };
200
- // 迁移后钩子(容错,不中断;此时交接文档已注入)
201
- if (this.onMigrated) {
202
- try {
203
- await this.onMigrated(afterCtx);
204
- }
205
- catch (err) {
206
- this.logger.error(`[migrate] onMigrated 回调失败: ${err?.message ?? err}`);
207
- }
208
- }
209
- return afterCtx;
210
- }
211
- }