@ai-zen/agents-host 0.0.0-stage → 0.1.0-alpha.1

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 (146) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +759 -2
  4. package/dist/app.d.ts +384 -0
  5. package/dist/app.js +1148 -0
  6. package/dist/cli.d.ts +22 -0
  7. package/dist/cli.js +354 -0
  8. package/dist/config.d.ts +97 -0
  9. package/dist/config.js +171 -0
  10. package/dist/handlers/agentHandlers.d.ts +18 -0
  11. package/dist/handlers/agentHandlers.js +20 -0
  12. package/dist/handlers/assetHandlers.d.ts +29 -0
  13. package/dist/handlers/assetHandlers.js +35 -0
  14. package/dist/handlers/capabilities.d.ts +26 -0
  15. package/dist/handlers/capabilities.js +138 -0
  16. package/dist/handlers/completionHandlers.d.ts +18 -0
  17. package/dist/handlers/completionHandlers.js +13 -0
  18. package/dist/handlers/configHandlers.d.ts +31 -0
  19. package/dist/handlers/configHandlers.js +59 -0
  20. package/dist/handlers/conversationHandlers.d.ts +118 -0
  21. package/dist/handlers/conversationHandlers.js +314 -0
  22. package/dist/handlers/handoffHandlers.d.ts +17 -0
  23. package/dist/handlers/handoffHandlers.js +14 -0
  24. package/dist/handlers/hostHandlers.d.ts +39 -0
  25. package/dist/handlers/hostHandlers.js +56 -0
  26. package/dist/handlers/index.d.ts +128 -0
  27. package/dist/handlers/index.js +92 -0
  28. package/dist/handlers/messageHandlers.d.ts +11 -0
  29. package/dist/handlers/messageHandlers.js +10 -0
  30. package/dist/handlers/modelHandlers.d.ts +17 -0
  31. package/dist/handlers/modelHandlers.js +19 -0
  32. package/dist/handlers/noticeHandlers.d.ts +24 -0
  33. package/dist/handlers/noticeHandlers.js +24 -0
  34. package/dist/handlers/questionHandlers.d.ts +16 -0
  35. package/dist/handlers/questionHandlers.js +16 -0
  36. package/dist/handlers/searchHandlers.d.ts +12 -0
  37. package/dist/handlers/searchHandlers.js +12 -0
  38. package/dist/handlers/workspaceHandlers.d.ts +29 -0
  39. package/dist/handlers/workspaceHandlers.js +47 -0
  40. package/dist/i18n/en.d.ts +243 -0
  41. package/dist/i18n/en.js +270 -0
  42. package/dist/i18n/index.d.ts +43 -0
  43. package/dist/i18n/index.js +72 -0
  44. package/dist/i18n/zh.d.ts +240 -0
  45. package/dist/i18n/zh.js +257 -0
  46. package/dist/index.d.ts +17 -0
  47. package/dist/index.js +15 -0
  48. package/dist/net/UploadReceiver.d.ts +186 -0
  49. package/dist/net/UploadReceiver.js +494 -0
  50. package/dist/net/WsHub.d.ts +231 -0
  51. package/dist/net/WsHub.js +689 -0
  52. package/dist/net/auth.d.ts +73 -0
  53. package/dist/net/auth.js +74 -0
  54. package/dist/net/discovery.d.ts +72 -0
  55. package/dist/net/discovery.js +171 -0
  56. package/dist/net/index.d.ts +14 -0
  57. package/dist/net/index.js +10 -0
  58. package/dist/runtime/ChatRunner.d.ts +374 -0
  59. package/dist/runtime/ChatRunner.js +1204 -0
  60. package/dist/runtime/ConversationDrafts.d.ts +43 -0
  61. package/dist/runtime/ConversationDrafts.js +47 -0
  62. package/dist/runtime/RunRegistry.d.ts +79 -0
  63. package/dist/runtime/RunRegistry.js +108 -0
  64. package/dist/runtime/ScopeRegistry.d.ts +153 -0
  65. package/dist/runtime/ScopeRegistry.js +259 -0
  66. package/dist/runtime/TransientRunner.d.ts +90 -0
  67. package/dist/runtime/TransientRunner.js +179 -0
  68. package/dist/runtime/index.d.ts +14 -0
  69. package/dist/runtime/index.js +10 -0
  70. package/dist/services/AgentService.d.ts +150 -0
  71. package/dist/services/AgentService.js +718 -0
  72. package/dist/services/AssetService.d.ts +338 -0
  73. package/dist/services/AssetService.js +739 -0
  74. package/dist/services/AutoNaming.d.ts +92 -0
  75. package/dist/services/AutoNaming.js +235 -0
  76. package/dist/services/CompletionService.d.ts +122 -0
  77. package/dist/services/CompletionService.js +442 -0
  78. package/dist/services/ConfigService.d.ts +171 -0
  79. package/dist/services/ConfigService.js +695 -0
  80. package/dist/services/ConversationService.d.ts +274 -0
  81. package/dist/services/ConversationService.js +902 -0
  82. package/dist/services/CredentialService.d.ts +33 -0
  83. package/dist/services/CredentialService.js +86 -0
  84. package/dist/services/DiscoveryService.d.ts +182 -0
  85. package/dist/services/DiscoveryService.js +998 -0
  86. package/dist/services/HandoffService.d.ts +189 -0
  87. package/dist/services/HandoffService.js +414 -0
  88. package/dist/services/Logger.d.ts +130 -0
  89. package/dist/services/Logger.js +256 -0
  90. package/dist/services/MessageService.d.ts +88 -0
  91. package/dist/services/MessageService.js +280 -0
  92. package/dist/services/ModelService.d.ts +89 -0
  93. package/dist/services/ModelService.js +489 -0
  94. package/dist/services/NoticeService.d.ts +93 -0
  95. package/dist/services/NoticeService.js +217 -0
  96. package/dist/services/QuestionService.d.ts +184 -0
  97. package/dist/services/QuestionService.js +457 -0
  98. package/dist/services/SearchService.d.ts +54 -0
  99. package/dist/services/SearchService.js +117 -0
  100. package/dist/services/WorkspaceService.d.ts +96 -0
  101. package/dist/services/WorkspaceService.js +213 -0
  102. package/dist/services/imageSize.d.ts +41 -0
  103. package/dist/services/imageSize.js +146 -0
  104. package/dist/services/index.d.ts +20 -0
  105. package/dist/services/index.js +15 -0
  106. package/dist/services/secretMask.d.ts +24 -0
  107. package/dist/services/secretMask.js +34 -0
  108. package/dist/storage/AssetRepository.d.ts +87 -0
  109. package/dist/storage/AssetRepository.js +180 -0
  110. package/dist/storage/ConversationRepository.d.ts +55 -0
  111. package/dist/storage/ConversationRepository.js +128 -0
  112. package/dist/storage/CredentialRepository.d.ts +28 -0
  113. package/dist/storage/CredentialRepository.js +47 -0
  114. package/dist/storage/Database.d.ts +95 -0
  115. package/dist/storage/Database.js +180 -0
  116. package/dist/storage/MessageRepository.d.ts +104 -0
  117. package/dist/storage/MessageRepository.js +323 -0
  118. package/dist/storage/MetaRepository.d.ts +21 -0
  119. package/dist/storage/MetaRepository.js +33 -0
  120. package/dist/storage/NoticeRepository.d.ts +31 -0
  121. package/dist/storage/NoticeRepository.js +89 -0
  122. package/dist/storage/QuestionRepository.d.ts +34 -0
  123. package/dist/storage/QuestionRepository.js +96 -0
  124. package/dist/storage/WorkspaceRepository.d.ts +25 -0
  125. package/dist/storage/WorkspaceRepository.js +44 -0
  126. package/dist/storage/dbWorker.d.ts +10 -0
  127. package/dist/storage/dbWorker.js +132 -0
  128. package/dist/storage/index.d.ts +20 -0
  129. package/dist/storage/index.js +16 -0
  130. package/dist/storage/migrations.d.ts +26 -0
  131. package/dist/storage/migrations.js +207 -0
  132. package/dist/storage/types.d.ts +298 -0
  133. package/dist/storage/types.js +14 -0
  134. package/dist/wire/errors.d.ts +105 -0
  135. package/dist/wire/errors.js +143 -0
  136. package/dist/wire/events.d.ts +102 -0
  137. package/dist/wire/events.js +13 -0
  138. package/dist/wire/frames.d.ts +106 -0
  139. package/dist/wire/frames.js +156 -0
  140. package/dist/wire/index.d.ts +31 -0
  141. package/dist/wire/index.js +31 -0
  142. package/dist/wire/methods.d.ts +1896 -0
  143. package/dist/wire/methods.js +99 -0
  144. package/dist/wire/states.d.ts +487 -0
  145. package/dist/wire/states.js +61 -0
  146. package/package.json +63 -3
@@ -0,0 +1,718 @@
1
+ /**
2
+ * 助手服务:助手与子 Agent 定义的增删改查、改默认模型、改思考档位(F-B1…F-B7 / F-F5 / F-G3)。
3
+ *
4
+ * 助手定义是**配置**而不是用户数据(F-G3):一实体一文件,存在数据根的 `agents/` 与
5
+ * `sub-agents/` 下,能进 git、能手改。所以这里的读写全走 SDK 的 `AgentRepository`,
6
+ * 不碰数据库。有 `function` 字段的那些是子 Agent(SDK 的发现口径),落在 `sub-agents/`。
7
+ *
8
+ * 产品的两条硬要求:
9
+ * - **不可用的也要列出并说明原因**(F-B5):文件读不出来、模型没了、端点没配密钥,都算
10
+ * 不可用,但都照样出现在清单里,带着一句为什么。
11
+ * - **删默认助手要连带清默认**(F-F5):删完还留着一个指向空处的默认助手,下一次建对话
12
+ * 才会炸出来。
13
+ */
14
+ import { promises as fs } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { AgentNS } from "@ai-zen/agents-core";
17
+ import { AgentRepository, DEFAULT_AGENT_DEFINITION, DEFAULT_AGENT_ID, DEFAULT_SUBAGENT_DEFINITION, } from "@ai-zen/agents-sdk";
18
+ import { DEFAULT_LANGUAGE, render } from "../i18n/index.js";
19
+ import { HostError } from "../wire/errors.js";
20
+ import { findModel, modelUnavailableProblem, reasoningPresetOf } from "./ModelService.js";
21
+ /** 数据根下的两个定义目录(与 SDK 的 `CONFIG_SUB_DIRS` 同口径) */
22
+ const DIR_AGENTS = "agents";
23
+ const DIR_SUB_AGENTS = "sub-agents";
24
+ /** 定义文件名的后缀:文件名 = `<id>.json` */
25
+ const JSON_SUFFIX = ".json";
26
+ /**
27
+ * 出厂清单:只有 SDK 的出厂定义才有「出厂版本」可恢复,用户自建的助手没有。
28
+ *
29
+ * 与模型那边(`DEFAULT_APP_CONFIG.models`)同一个口径:出厂那一份由 SDK 持有,宿主不另存一份。
30
+ */
31
+ const FACTORY_AGENTS = [
32
+ DEFAULT_AGENT_DEFINITION,
33
+ DEFAULT_SUBAGENT_DEFINITION,
34
+ ];
35
+ // ---------------------------------------------------------------------------
36
+ // 文案
37
+ // ---------------------------------------------------------------------------
38
+ /** 一条给人看的事实(码 = 文案键,与素材层同口径:存码不存句子,D8) */
39
+ function text(messageKey, params = {}) {
40
+ return {
41
+ code: messageKey,
42
+ messageKey,
43
+ params,
44
+ message: render(DEFAULT_LANGUAGE, messageKey, params),
45
+ };
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // 纯函数
49
+ // ---------------------------------------------------------------------------
50
+ /** 必填的标识:只允许字母数字与 `-` `_` `.`,它同时是文件名(F-G3) */
51
+ function requireId(value, field) {
52
+ const raw = typeof value === "string" ? value.trim() : "";
53
+ if (raw === "") {
54
+ throw new HostError("invalid_params", {
55
+ params: { field },
56
+ detail: { field, reason: "empty" },
57
+ });
58
+ }
59
+ if (!/^[A-Za-z0-9._-]+$/.test(raw) || raw === "." || raw === "..") {
60
+ throw new HostError("invalid_params", {
61
+ params: { field },
62
+ detail: { field, reason: "not_a_file_name", value: raw },
63
+ });
64
+ }
65
+ return raw;
66
+ }
67
+ /** 必填的显示名 */
68
+ function requireName(value) {
69
+ const raw = typeof value === "string" ? value.trim() : "";
70
+ if (raw === "") {
71
+ throw new HostError("invalid_params", {
72
+ params: { field: "agent.name" },
73
+ detail: { field: "agent.name", reason: "empty" },
74
+ });
75
+ }
76
+ return raw;
77
+ }
78
+ /** 结构相等(思考档位的补丁与定义上的参数要能对得上) */
79
+ function deepEqual(left, right) {
80
+ if (left === right)
81
+ return true;
82
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) {
83
+ return false;
84
+ }
85
+ const leftEntries = Object.entries(left);
86
+ const rightEntries = Object.entries(right);
87
+ if (leftEntries.length !== rightEntries.length)
88
+ return false;
89
+ return leftEntries.every(([key, value]) => deepEqual(value, right[key]));
90
+ }
91
+ /**
92
+ * 定义上的参数是否「就是这一档」:档位补丁的每个键都在,且值相等。
93
+ *
94
+ * 写进去的是参数而不是档位名(F-B6),所以反推档位时按补丁逐键比对;
95
+ * 补丁允许是定义参数的子集(定义里可能还有与思考无关的参数)。
96
+ */
97
+ function matchesPatch(params, patch) {
98
+ if (!params)
99
+ return false;
100
+ const entries = Object.entries(patch);
101
+ if (entries.length === 0)
102
+ return false;
103
+ return entries.every(([key, value]) => deepEqual(params[key], value));
104
+ }
105
+ /**
106
+ * 去掉「属于某个思考策略档位」的那些参数键,回剩下的参数(没有剩下时 `undefined`)。
107
+ *
108
+ * 判断一个键是不是思考参数只看一件事:它在策略表里出现过(SDK 那套表就是这么定的,
109
+ * 不硬编码任何键名)。换模型之后旧档位的键留在定义上,下一轮就会把旧模型才认的参数
110
+ * 发给新模型。
111
+ */
112
+ function withoutThinkingParams(params, preset) {
113
+ if (!params)
114
+ return undefined;
115
+ if (!preset)
116
+ return params;
117
+ const levelKeys = new Set(Object.values(preset.levels).flatMap((patch) => Object.keys(patch ?? {})));
118
+ if (levelKeys.size === 0)
119
+ return params;
120
+ const kept = {};
121
+ for (const [key, value] of Object.entries(params)) {
122
+ if (!levelKeys.has(key))
123
+ kept[key] = value;
124
+ }
125
+ return Object.keys(kept).length === 0 ? undefined : kept;
126
+ }
127
+ /** 只保留「入参真的给了」的字段(`undefined` 表示这次不改它) */
128
+ function definedOnly(input) {
129
+ const result = {};
130
+ for (const [key, value] of Object.entries(input)) {
131
+ if (value !== undefined)
132
+ result[key] = value;
133
+ }
134
+ return result;
135
+ }
136
+ /** 预设对话的校验:至少要有一条(没有 system 的助手定义跑起来没有行为约束) */
137
+ function requireMessages(value) {
138
+ if (!Array.isArray(value) || value.length === 0) {
139
+ throw new HostError("invalid_params", {
140
+ params: { field: "agent.messages" },
141
+ detail: { field: "agent.messages", reason: "empty" },
142
+ });
143
+ }
144
+ const messages = value;
145
+ const hasSystem = messages.some((message) => message.role === AgentNS.Role.System);
146
+ if (!hasSystem) {
147
+ throw new HostError("invalid_params", {
148
+ params: { field: "agent.messages" },
149
+ detail: { field: "agent.messages", reason: "no_system_message" },
150
+ });
151
+ }
152
+ return messages;
153
+ }
154
+ /**
155
+ * 两份提示词是不是同一份。
156
+ *
157
+ * 只比角色与内容:`Message.System()` 每次都生成新 id,落盘再读回来的 id 不会与出厂常量相等,
158
+ * 拿 id 比永远比不中。SDK 判断「出厂提示词是否已被改过」用的也是这个口径。
159
+ */
160
+ function samePrompts(left, right) {
161
+ if (left.length !== right.length)
162
+ return false;
163
+ return left.every((message, index) => message.role === right[index]?.role &&
164
+ JSON.stringify(message.content) === JSON.stringify(right[index]?.content));
165
+ }
166
+ /**
167
+ * 这条定义是不是已经就是出厂那一份(时间戳不比,`custom` 只看有没有被声明为自定义)。
168
+ *
169
+ * 用来回答「还需要恢复吗」:本来就一样时恢复是空动作,不该报成一次恢复成功。
170
+ */
171
+ function isFactoryState(current, factory) {
172
+ if (current.custom === true)
173
+ return false;
174
+ return (current.name === factory.name &&
175
+ (current.description ?? null) === (factory.description ?? null) &&
176
+ (current.modelId ?? null) === (factory.modelId ?? null) &&
177
+ deepEqual(current.modelParams ?? {}, factory.modelParams ?? {}) &&
178
+ deepEqual(current.permissions ?? null, factory.permissions ?? null) &&
179
+ deepEqual(current.function ?? null, factory.function ?? null) &&
180
+ samePrompts(current.messages ?? [], factory.messages));
181
+ }
182
+ // ---------------------------------------------------------------------------
183
+ // 服务
184
+ // ---------------------------------------------------------------------------
185
+ export class AgentService {
186
+ configManager;
187
+ repos;
188
+ now;
189
+ constructor(options) {
190
+ this.configManager = options.configManager;
191
+ this.repos = {
192
+ agents: options.agents ?? new AgentRepository(join(options.configManager.basePath, DIR_AGENTS)),
193
+ "sub-agents": options.subAgents ??
194
+ new AgentRepository(join(options.configManager.basePath, DIR_SUB_AGENTS)),
195
+ };
196
+ this.now = options.now ?? Date.now;
197
+ }
198
+ // -------------------------------------------------------------------------
199
+ // 读
200
+ // -------------------------------------------------------------------------
201
+ /**
202
+ * 助手清单(F-B2 / F-B3 / F-B5)。
203
+ *
204
+ * 主助手在前、子 Agent 在后,各自按 id 排 —— 同一份磁盘状态每次给同一个顺序。
205
+ * 读不出来的定义文件也列出来(`available: false` + 原因):藏起来会让人以为它没了。
206
+ *
207
+ * `workspaceId` 只做存在性校验:助手定义的可用性(模型、端点、密钥)是全局配置的事实,
208
+ * 与项目无关;「这个项目里能不能用」要等可用的 Skill / MCP 清单定下来才有答案。
209
+ */
210
+ async list(_params = {}) {
211
+ const config = await this.configManager.read();
212
+ const presets = await this.configManager.readReasoningPresets();
213
+ const loaded = await this.loadAll();
214
+ const agents = loaded.map((item) => this.toView(config, presets, item));
215
+ return { agents, defaultAgentId: this.resolveDefaultAgentId(config, loaded) };
216
+ }
217
+ /** 按 id 取一个助手 */
218
+ async get(params) {
219
+ const id = requireId(params.agentId, "agentId");
220
+ const found = await this.load(id);
221
+ if (!found) {
222
+ throw new HostError("not_found", { params: { entity: "agent" }, detail: { agentId: id } });
223
+ }
224
+ const config = await this.configManager.read();
225
+ const presets = await this.configManager.readReasoningPresets();
226
+ return { agent: this.toView(config, presets, found) };
227
+ }
228
+ /**
229
+ * 一个助手现在叫什么、能不能用(最小只读导出,PRD F-I6 的状态栏助手块用)。
230
+ *
231
+ * 与 `get()` 走同一份 `load()` / `toView()` 口径,只是只回这两条事实:调用方(会话现场快照)
232
+ * 要的是「这个 id 现在叫什么、还能不能跑」,不是整份定义。
233
+ *
234
+ * 拿不到就回 `null`:定义文件已经没了(助手被删,而对话仍记着那个 id,L7),或者文件在但
235
+ * 读不出来(坏 JSON)。这两种情况下「名字」与「可用性」都没有可报的事实。
236
+ */
237
+ async describe(agentId) {
238
+ const id = requireId(agentId, "agentId");
239
+ const found = await this.load(id);
240
+ if (!found || found.definition === null)
241
+ return null;
242
+ const config = await this.configManager.read();
243
+ const presets = await this.configManager.readReasoningPresets();
244
+ const view = this.toView(config, presets, found);
245
+ return { agentId: view.id, name: view.name, available: view.available };
246
+ }
247
+ // -------------------------------------------------------------------------
248
+ // 写
249
+ // -------------------------------------------------------------------------
250
+ /** 新建助手定义(一实体一文件,F-G3) */
251
+ async create(params) {
252
+ const input = params.agent;
253
+ const id = requireId(input?.id, "agent.id");
254
+ const existing = await this.load(id);
255
+ if (existing) {
256
+ throw new HostError("invalid_params", {
257
+ params: { field: "agent.id" },
258
+ detail: { field: "agent.id", reason: "already_exists", agentId: id },
259
+ });
260
+ }
261
+ const config = await this.configManager.read();
262
+ this.assertModelReference(config, input.modelId);
263
+ const timestamp = new Date(this.now()).toISOString();
264
+ const definition = {
265
+ id,
266
+ name: requireName(input.name),
267
+ ...definedOnly({
268
+ description: input.description,
269
+ modelId: input.modelId,
270
+ modelParams: input.modelParams,
271
+ permissions: input.permissions,
272
+ function: input.function,
273
+ }),
274
+ messages: requireMessages(input.messages),
275
+ // 通过 API 写下的定义算用户自有:SDK 初始化时不会再拿出厂提示词覆盖它
276
+ custom: true,
277
+ createdAt: timestamp,
278
+ updatedAt: timestamp,
279
+ };
280
+ const home = this.homeOf(definition);
281
+ await this.repos[home].write(definition);
282
+ const presets = await this.configManager.readReasoningPresets();
283
+ return { agent: this.toView(config, presets, { id, home, definition, brokenReason: null }) };
284
+ }
285
+ /** 编辑助手定义(局部更新;`modelId: ""` 表示把绑定清掉,交给配置里的兜底模型) */
286
+ async update(params) {
287
+ const id = requireId(params.agentId, "agentId");
288
+ const found = await this.requireEditable(id);
289
+ const patch = params.patch ?? {};
290
+ const config = await this.configManager.read();
291
+ this.assertModelReference(config, patch.modelId);
292
+ const base = found.definition;
293
+ const merged = {
294
+ ...base,
295
+ ...definedOnly({
296
+ name: patch.name === undefined ? undefined : requireName(patch.name),
297
+ description: patch.description,
298
+ modelParams: patch.modelParams,
299
+ permissions: patch.permissions,
300
+ function: patch.function,
301
+ }),
302
+ messages: patch.messages === undefined ? base.messages : requireMessages(patch.messages),
303
+ // 空串表示「清掉绑定」(三级解析会落到配置里的兜底模型)
304
+ modelId: patch.modelId === undefined
305
+ ? base.modelId
306
+ : patch.modelId === ""
307
+ ? undefined
308
+ : patch.modelId,
309
+ custom: true,
310
+ updatedAt: new Date(this.now()).toISOString(),
311
+ };
312
+ const home = this.homeOf(merged);
313
+ await this.repos[home].write(merged);
314
+ if (home !== found.home)
315
+ await this.repos[found.home].delete(id);
316
+ const presets = await this.configManager.readReasoningPresets();
317
+ return { agent: this.toView(config, presets, { id, home, definition: merged, brokenReason: null }) };
318
+ }
319
+ /**
320
+ * 删除助手定义(F-F5)。
321
+ *
322
+ * 删的正好是默认助手时连带把默认清掉(`defaultsCleared`)。其它指向关系(别的助手在
323
+ * 权限里点名它的 function)只报在 `references` 里 —— 那些是权限白名单里的一个名字,
324
+ * 删掉定义之后那条名字什么都不指,不是「不能删」。
325
+ */
326
+ async delete(params) {
327
+ const id = requireId(params.agentId, "agentId");
328
+ const found = await this.load(id);
329
+ if (!found) {
330
+ throw new HostError("not_found", { params: { entity: "agent" }, detail: { agentId: id } });
331
+ }
332
+ const config = await this.configManager.read();
333
+ const references = await this.referencesOf(config, id, found.definition);
334
+ const wasDefault = config.defaultAgent === id || (config.defaultAgent === undefined && id === DEFAULT_AGENT_ID);
335
+ if (params.dryRun === true) {
336
+ return { removed: false, references, defaultsCleared: false };
337
+ }
338
+ await this.repos[found.home].delete(id);
339
+ if (wasDefault) {
340
+ const next = { ...config };
341
+ delete next.defaultAgent;
342
+ await this.configManager.write(next);
343
+ }
344
+ return { removed: true, references, defaultsCleared: wasDefault };
345
+ }
346
+ /**
347
+ * 改助手的默认模型(F-B4 / F-B1:影响以后的对话,与「改这段会话的模型」是两个动作)。
348
+ *
349
+ * 换了模型就同时把定义上「旧模型档位带来的那些参数」去掉:定义上的参数是给旧模型配的,
350
+ * 旧模型才认的键(例如某个厂商的 `thinking` 形状)发给新模型只会换来一句 400。
351
+ * 定义上与其无关的参数(`temperature` 之类)不动。
352
+ */
353
+ async setModel(params) {
354
+ const id = requireId(params.agentId, "agentId");
355
+ const found = await this.requireEditable(id);
356
+ const modelId = requireId(params.modelId, "modelId");
357
+ const config = await this.configManager.read();
358
+ if (!findModel(config, modelId)) {
359
+ throw new HostError("not_found", { params: { entity: "model" }, detail: { modelId } });
360
+ }
361
+ const presets = await this.configManager.readReasoningPresets();
362
+ const previousModelId = found.definition.modelId ?? config.defaultModel;
363
+ const modelParams = previousModelId === modelId
364
+ ? found.definition.modelParams
365
+ : withoutThinkingParams(found.definition.modelParams, reasoningPresetOf(previousModelId ? findModel(config, previousModelId) : undefined, presets));
366
+ const updated = {
367
+ ...found.definition,
368
+ modelId,
369
+ custom: true,
370
+ updatedAt: new Date(this.now()).toISOString(),
371
+ };
372
+ if (modelParams === undefined)
373
+ delete updated.modelParams;
374
+ else
375
+ updated.modelParams = modelParams;
376
+ await this.repos[found.home].write(updated);
377
+ return {
378
+ agent: this.toView(config, presets, {
379
+ id,
380
+ home: found.home,
381
+ definition: updated,
382
+ brokenReason: null,
383
+ }),
384
+ };
385
+ }
386
+ /**
387
+ * 把思考档位展开成参数写进助手定义(F-B6:写进去的是参数,档位名只是标签)。
388
+ *
389
+ * 档位必须在这个模型的思考策略表里:表里没有的档发出去只会换来一句 400。
390
+ * 合并口径是「在现有参数上叠加这一档的补丁」—— 与思考无关的参数(temperature 之类)不动。
391
+ *
392
+ * `level: null` 是撤回设值:把定义里属于思考策略的那些参数删掉,其余参数照旧留着;
393
+ * 删完不剩任何参数时 `modelParams` 整个从定义里去掉,让「参数即真相」回到模型的默认参数。
394
+ * 与会话那一侧的 `conversation.setThinking(null)`(改成会话行上的档位名)是两件事。
395
+ */
396
+ async setThinking(params) {
397
+ const id = requireId(params.agentId, "agentId");
398
+ const found = await this.requireEditable(id);
399
+ const config = await this.configManager.read();
400
+ const presets = await this.configManager.readReasoningPresets();
401
+ const agent = found.definition;
402
+ const modelId = agent.modelId ?? config.defaultModel;
403
+ const model = modelId ? findModel(config, modelId) : undefined;
404
+ if (!model) {
405
+ throw new HostError("not_found", {
406
+ params: { entity: "model" },
407
+ detail: { agentId: id, modelId: modelId ?? null, reason: "no_model_for_agent" },
408
+ });
409
+ }
410
+ const preset = reasoningPresetOf(model, presets);
411
+ if (!preset) {
412
+ throw new HostError("unsupported", {
413
+ params: { what: "thinking levels for this model" },
414
+ detail: { modelId: model.id, preset: model.reasoningPreset ?? null },
415
+ });
416
+ }
417
+ const level = params.level === null ? null : requireId(params.level, "level");
418
+ if (level !== null && !Object.prototype.hasOwnProperty.call(preset.levels, level)) {
419
+ throw new HostError("invalid_params", {
420
+ params: { field: "level" },
421
+ detail: { field: "level", level, levels: Object.keys(preset.levels) },
422
+ });
423
+ }
424
+ // `null` 走「删掉思考那些键」,否则走「叠加这一档的补丁」
425
+ const modelParams = level === null
426
+ ? withoutThinkingParams(agent.modelParams, preset)
427
+ : { ...(agent.modelParams ?? {}), ...(preset.levels[level] ?? {}) };
428
+ const updated = {
429
+ ...agent,
430
+ custom: true,
431
+ updatedAt: new Date(this.now()).toISOString(),
432
+ };
433
+ if (modelParams === undefined)
434
+ delete updated.modelParams;
435
+ else
436
+ updated.modelParams = modelParams;
437
+ await this.repos[found.home].write(updated);
438
+ const view = this.toView(config, presets, {
439
+ id,
440
+ home: found.home,
441
+ definition: updated,
442
+ brokenReason: null,
443
+ });
444
+ return { agent: view, thinking: view.thinking };
445
+ }
446
+ /**
447
+ * 恢复出厂(F-F5:整条替换,与 `model.restoreFactory` 同一个语义)。
448
+ *
449
+ * 出厂那一份来自 SDK 的出厂定义(`DEFAULT_AGENT_DEFINITION` / `DEFAULT_SUBAGENT_DEFINITION`),
450
+ * 用户自建的助手不在出厂清单里,没有可恢复的版本。恢复之后这一条重新归 SDK 托管
451
+ * (`custom: false`,下次初始化会跟着出厂定义同步提示词),提示词 / 模型 / 权限 / 思考参数
452
+ * 全回到出厂那份;`createdAt` 是这个实体诞生的时刻,留着,`updatedAt` 是这次写入。
453
+ *
454
+ * `dryRun` 只算出「恢复之后会是什么样」,不落盘;本来就和出厂一致时回 `restored: false`
455
+ * (没有可恢复的改动,不是失败)。
456
+ */
457
+ async restoreFactory(params) {
458
+ const id = requireId(params.agentId, "agentId");
459
+ const found = await this.requireEditable(id);
460
+ const factory = FACTORY_AGENTS.find((item) => item.id === id);
461
+ if (!factory) {
462
+ throw new HostError("invalid_params", {
463
+ messageKey: "agent.notFactory",
464
+ params: { id },
465
+ detail: { agentId: id, reason: "not_a_factory_agent" },
466
+ });
467
+ }
468
+ const config = await this.configManager.read();
469
+ const presets = await this.configManager.readReasoningPresets();
470
+ if (isFactoryState(found.definition, factory)) {
471
+ return { agent: this.toView(config, presets, found), restored: false };
472
+ }
473
+ const restored = {
474
+ ...structuredClone(factory),
475
+ createdAt: found.definition.createdAt,
476
+ updatedAt: new Date(this.now()).toISOString(),
477
+ };
478
+ const home = this.homeOf(restored);
479
+ const after = { id, home, definition: restored, brokenReason: null };
480
+ if (params.dryRun === true) {
481
+ return { agent: this.toView(config, presets, after), restored: true };
482
+ }
483
+ await this.repos[home].write(restored);
484
+ // 出厂那份放在另一个目录时(比如被手改到 agents/ 里的子 Agent),旧文件跟着删掉
485
+ if (home !== found.home)
486
+ await this.repos[found.home].delete(id);
487
+ return { agent: this.toView(config, presets, after), restored: true };
488
+ }
489
+ // -------------------------------------------------------------------------
490
+ // 视图
491
+ // -------------------------------------------------------------------------
492
+ /** 一条定义(或一个读不出来的文件)→ 对外形状 */
493
+ toView(config, presets, item) {
494
+ const definition = item.definition;
495
+ if (definition === null) {
496
+ // 文件在但读不出来:照样占一行,说明为什么用不了(F-B5)
497
+ return {
498
+ id: item.id,
499
+ name: item.id,
500
+ description: null,
501
+ modelId: null,
502
+ modelParams: {},
503
+ permissions: null,
504
+ // 提示词与子 Agent 定义都读不出来:说「不知道」比编一个空数组诚实
505
+ messages: null,
506
+ function: null,
507
+ isSubAgent: item.home === "sub-agents",
508
+ custom: false,
509
+ isDefault: config.defaultAgent === item.id,
510
+ available: false,
511
+ unavailableReason: text("agent.fileBroken", {
512
+ name: item.id,
513
+ reason: item.brokenReason ?? "unreadable",
514
+ }),
515
+ thinking: {
516
+ preset: null,
517
+ levels: [],
518
+ selected: null,
519
+ source: "not_adjustable",
520
+ params: {},
521
+ },
522
+ createdAt: new Date(0).toISOString(),
523
+ updatedAt: new Date(0).toISOString(),
524
+ };
525
+ }
526
+ const modelId = definition.modelId ?? config.defaultModel ?? null;
527
+ const model = modelId ? findModel(config, modelId) : undefined;
528
+ const unavailable = this.unavailableProblem(config, modelId, model);
529
+ return {
530
+ id: definition.id,
531
+ name: definition.name,
532
+ description: definition.description ?? null,
533
+ modelId: definition.modelId ?? null,
534
+ modelParams: definition.modelParams ?? {},
535
+ permissions: definition.permissions ?? null,
536
+ // 定义本身的形状:提示词与子 Agent 定义原样给出(手改的文件可能没写 `messages`,那也归到 null)
537
+ messages: definition.messages ?? null,
538
+ function: definition.function ?? null,
539
+ isSubAgent: definition.function !== undefined,
540
+ custom: definition.custom === true,
541
+ isDefault: config.defaultAgent === definition.id,
542
+ available: unavailable === null,
543
+ unavailableReason: unavailable,
544
+ thinking: this.thinkingOf(model, presets, definition.modelParams),
545
+ createdAt: definition.createdAt,
546
+ updatedAt: definition.updatedAt,
547
+ };
548
+ }
549
+ // -------------------------------------------------------------------------
550
+ // 内部
551
+ // -------------------------------------------------------------------------
552
+ /** 这个助手现在能不能跑:没模型、模型没了、端点没了/没密钥,各有各的说法 */
553
+ unavailableProblem(config, modelId, model) {
554
+ if (modelId === null)
555
+ return text("error.modelUnresolved");
556
+ if (!model)
557
+ return text("agent.modelMissing", { id: modelId });
558
+ return modelUnavailableProblem(config, model);
559
+ }
560
+ /**
561
+ * 思考强度的状态(F-B6 / F-B7)。
562
+ *
563
+ * `manual` = 定义上的参数正好是某一档;`following_default` = 定义没指定,跟着模型默认走;
564
+ * `not_adjustable` = 这个模型引用的策略表里一条档位都没有。
565
+ */
566
+ thinkingOf(model, presets, params) {
567
+ const presetName = model?.reasoningPreset ?? null;
568
+ const preset = reasoningPresetOf(model, presets);
569
+ if (!preset) {
570
+ return {
571
+ preset: presetName,
572
+ levels: [],
573
+ selected: null,
574
+ source: "not_adjustable",
575
+ params: {},
576
+ };
577
+ }
578
+ const levels = Object.keys(preset.levels);
579
+ const detected = levels.find((level) => matchesPatch(params, preset.levels[level] ?? {}));
580
+ return {
581
+ preset: presetName,
582
+ levels,
583
+ selected: detected ?? null,
584
+ source: detected === undefined ? "following_default" : "manual",
585
+ params: detected === undefined ? {} : preset.levels[detected] ?? {},
586
+ };
587
+ }
588
+ /** 默认助手 id:配置里那个还在就用它,否则回落 `default`,再否则取清单第一个 */
589
+ resolveDefaultAgentId(config, loaded) {
590
+ const usable = loaded.filter((item) => item.definition !== null);
591
+ const byId = (id) => id !== undefined && usable.some((item) => item.id === id);
592
+ if (byId(config.defaultAgent))
593
+ return config.defaultAgent;
594
+ if (byId(DEFAULT_AGENT_ID))
595
+ return DEFAULT_AGENT_ID;
596
+ return usable[0]?.id ?? null;
597
+ }
598
+ /** 哪个目录该放这条定义:有 `function` 的是子 Agent(SDK 的发现口径) */
599
+ homeOf(definition) {
600
+ return definition.function === undefined ? "agents" : "sub-agents";
601
+ }
602
+ /** 读一个定义(两个目录都找);不存在回 `null` */
603
+ async load(id) {
604
+ for (const home of ["agents", "sub-agents"]) {
605
+ const path = join(this.repoDir(home), `${id}${JSON_SUFFIX}`);
606
+ const exists = await fileExists(path);
607
+ if (!exists)
608
+ continue;
609
+ const definition = await this.repos[home].read(id);
610
+ return {
611
+ id,
612
+ home,
613
+ definition,
614
+ brokenReason: definition === null ? "not readable as JSON" : null,
615
+ };
616
+ }
617
+ return null;
618
+ }
619
+ /** 读全部定义(两个目录),读不出来的也占一行 */
620
+ async loadAll() {
621
+ const result = [];
622
+ for (const home of ["agents", "sub-agents"]) {
623
+ const ids = await listJsonIds(this.repoDir(home));
624
+ for (const id of ids) {
625
+ const definition = await this.repos[home].read(id);
626
+ result.push({
627
+ id,
628
+ home,
629
+ definition,
630
+ brokenReason: definition === null ? "not readable as JSON" : null,
631
+ });
632
+ }
633
+ }
634
+ return result;
635
+ }
636
+ /** 仓储对应的目录 */
637
+ repoDir(home) {
638
+ return join(this.configManager.basePath, home === "agents" ? DIR_AGENTS : DIR_SUB_AGENTS);
639
+ }
640
+ /** 要改定义时先把它读出来;读不出来(坏文件)就没有可改的内容 */
641
+ async requireEditable(id) {
642
+ const found = await this.load(id);
643
+ if (!found) {
644
+ throw new HostError("not_found", { params: { entity: "agent" }, detail: { agentId: id } });
645
+ }
646
+ if (found.definition === null) {
647
+ throw new HostError("config_invalid", {
648
+ messageKey: "agent.fileBroken",
649
+ params: { name: id, reason: found.brokenReason ?? "unreadable" },
650
+ detail: { agentId: id, path: join(this.repoDir(found.home), `${id}${JSON_SUFFIX}`) },
651
+ });
652
+ }
653
+ return { ...found, definition: found.definition };
654
+ }
655
+ /** 引用:默认助手 + 别的助手在权限里点名的子 Agent function */
656
+ async referencesOf(config, agentId, definition) {
657
+ const items = [];
658
+ if (config.defaultAgent === agentId) {
659
+ items.push({ kind: "default", id: "defaultAgent", name: agentId });
660
+ }
661
+ const functionName = definition?.function?.name;
662
+ if (functionName !== undefined) {
663
+ const loaded = await this.loadAll();
664
+ for (const item of loaded) {
665
+ if (item.id === agentId || item.definition === null)
666
+ continue;
667
+ if (mentionsSubagent(item.definition, functionName)) {
668
+ items.push({ kind: "agent", id: item.id, name: item.definition.name });
669
+ }
670
+ }
671
+ }
672
+ return { referenced: items.length > 0, items };
673
+ }
674
+ /** `modelId` 给了就必须指向一个存在的模型(写进定义的 id 不能是悬空的) */
675
+ assertModelReference(config, modelId) {
676
+ if (modelId === undefined || modelId === "")
677
+ return;
678
+ if (!findModel(config, modelId)) {
679
+ throw new HostError("invalid_params", {
680
+ params: { field: "agent.modelId" },
681
+ detail: { field: "agent.modelId", reason: "model_not_found", modelId },
682
+ });
683
+ }
684
+ }
685
+ }
686
+ // ---------------------------------------------------------------------------
687
+ // 纯函数
688
+ // ---------------------------------------------------------------------------
689
+ /** 别的助手的权限四维里有没有点名这个子 Agent(`allow` / `deny` 都算) */
690
+ function mentionsSubagent(definition, functionName) {
691
+ const policy = definition.permissions?.subagents;
692
+ if (!policy)
693
+ return false;
694
+ const list = "allow" in policy ? policy.allow : policy.deny;
695
+ return list.includes(functionName);
696
+ }
697
+ /** 目录下的 `.json` 文件名(去掉后缀)→ id 列表;目录不存在就是空 */
698
+ async function listJsonIds(dir) {
699
+ try {
700
+ const entries = await fs.readdir(dir);
701
+ return entries
702
+ .filter((file) => file.endsWith(JSON_SUFFIX))
703
+ .map((file) => file.slice(0, -JSON_SUFFIX.length))
704
+ .sort();
705
+ }
706
+ catch {
707
+ return [];
708
+ }
709
+ }
710
+ async function fileExists(path) {
711
+ try {
712
+ const info = await fs.stat(path);
713
+ return info.isFile();
714
+ }
715
+ catch {
716
+ return false;
717
+ }
718
+ }