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

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 +21 -0
  7. package/dist/cli.js +353 -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,1204 @@
1
+ /**
2
+ * `ChatRunner` —— 一轮对话的执行者(architecture §4 那张时序)。
3
+ *
4
+ * ## 一轮的骨架
5
+ *
6
+ * ```
7
+ * RunRegistry.start(忙即拒,PRD D14)
8
+ * → 借 Scope(ScopeRegistry.acquire:懒创建或复用,依赖 +1)
9
+ * → 分配两个消息 id 并落库(用户消息 + 助手占位,PRD L6:id 在请求发出前就有)
10
+ * → 装配 agent(可注入的工厂;跑完即弃,PRD D28)
11
+ * → 流式:每片增量推 `message.delta`;同一时刻更新六态并推 `run.state`;同一条消息边流边写(D15)
12
+ * → 内循环结束:写一次最终版(F-C2)→ 释放 agent → RunRegistry.finish → ScopeRegistry.release
13
+ * → 推结算态 `run.state` + `usage`
14
+ * ```
15
+ *
16
+ * ## 三处刻意交给 core 的事
17
+ *
18
+ * 1. **消息与增量不重新定义**:`AgentNS.Message` / `AgentNS.Delta` / `AgentNS.MessageStatus` /
19
+ * `Usage` 全部原样用 core 的导出;攒消息用 core 的 `applyChunk`,求和用 core 的 `sumUsage`,
20
+ * 中止修复用 core 的 `reconcileAbortedToolRounds` / `reconcileAbortedMessages`。
21
+ * 2. **agent 循环不重写**:装配好的 agent 由 core 的 `send()` 驱动(内循环、工具并行、中止、
22
+ * 收尾归一都在 core 里)。本文件只订阅它广播出来的事件,把「发生了什么」变成持久化与推送。
23
+ * 3. **状态不存两处**:运行中的消息以内存(core 手里那份 `agent.messages`)为真相,库里的行是
24
+ * 快照;`isRunning` 只在 `RunRegistry` 一处。
25
+ *
26
+ * ## 为什么要认领 core 追加的消息 id
27
+ *
28
+ * 消息 id 由宿主分配(PRD L6),而 assistant 占位是 core 在 `run()` 里自己 `append` 的
29
+ * (它的 `Message` 构造会自己生成 id)。所以本文件在 `message` 事件里把「这一轮头两条」认领成
30
+ * 宿主已经发给客户端的 id —— 客户端的 `message.delta` 与 `message.list` 才接得上同一条消息。
31
+ * 后续工具轮次里 core 追加的消息同样在这一处被换成宿主分配的 id(`id` 不参与 core 的任何
32
+ * 判断,换掉只会让它对客户端更一致)。
33
+ */
34
+ import { addUsage, AgentNS as AgentNSValue, reconcileAbortedMessages, reconcileAbortedToolRounds, sumUsage, } from "@ai-zen/agents-core";
35
+ import { AgentRepository, AutoMigratePlugin, AutoRefreshToolsPlugin, ContextGuardPlugin, createAgent, SubAgentGuardInstallerPlugin, UnknownToolHintPlugin, } from "@ai-zen/agents-sdk";
36
+ import { DEFAULT_LANGUAGE, render } from "../i18n/index.js";
37
+ import { HostError } from "../wire/errors.js";
38
+ import { systemClock } from "./ScopeRegistry.js";
39
+ /** 边流边写的缺省节流:200ms 一次,兼顾「客户端断了丢得少」与「写盘次数」 */
40
+ export const DEFAULT_STREAM_WRITE_INTERVAL_MS = 200;
41
+ /** 正文 / 思考的字符数(分段内容只数文本段) */
42
+ function contentChars(content) {
43
+ if (typeof content === "string")
44
+ return content.length;
45
+ if (Array.isArray(content)) {
46
+ let total = 0;
47
+ for (const section of content) {
48
+ if ("text" in section && typeof section.text === "string")
49
+ total += section.text.length;
50
+ }
51
+ return total;
52
+ }
53
+ return 0;
54
+ }
55
+ /** 工具调用的定位键:优先用 `id`,没有就退回「名字 + 序号」(`function_call` 形态) */
56
+ function toolKey(toolCall) {
57
+ if (toolCall.id)
58
+ return `id:${toolCall.id}`;
59
+ return `fn:${toolCall.function?.name ?? ""}#${toolCall.index ?? 0}`;
60
+ }
61
+ /** 两位小数(速率是给人看的一个数,不是账目) */
62
+ function round2(value) {
63
+ return Math.round(value * 100) / 100;
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // core 消息 ⇄ 存储行
67
+ // ---------------------------------------------------------------------------
68
+ const ROLES = new Set(Object.values(AgentNSValue.Role));
69
+ const STATUSES = new Set(Object.values(AgentNSValue.MessageStatus));
70
+ function toRole(value) {
71
+ return ROLES.has(value) ? value : AgentNSValue.Role.Unknown;
72
+ }
73
+ function toStatus(value) {
74
+ return STATUSES.has(value) ? value : AgentNSValue.MessageStatus.Unknown;
75
+ }
76
+ function isRecord(value) {
77
+ return typeof value === "object" && value !== null && !Array.isArray(value);
78
+ }
79
+ /** 从 `data` 列里认出 core 的用量(三个数字都在才算) */
80
+ function toUsage(value) {
81
+ if (!isRecord(value))
82
+ return undefined;
83
+ const { prompt_tokens, completion_tokens, total_tokens } = value;
84
+ if (typeof prompt_tokens !== "number" ||
85
+ typeof completion_tokens !== "number" ||
86
+ typeof total_tokens !== "number") {
87
+ return undefined;
88
+ }
89
+ return { prompt_tokens, completion_tokens, total_tokens };
90
+ }
91
+ /**
92
+ * 会话创建时那条 cwd 消息的正文(PRD F-D9:告诉模型「当前目录是哪儿」)。
93
+ *
94
+ * 这条消息在对话落库时写一次,之后不再重写、也不随每轮拼接(说了两遍模型会以为目录变了)。
95
+ * 目录取的是这个对话所属项目的 cwd(不是进程工作目录);句子用的是「这是宿主加的,不是
96
+ * 用户输入的」那条文案(R21:不新造消息类型)。
97
+ */
98
+ export function injectedContextOf(cwd, language) {
99
+ const summary = render(language, "message.injectedContext.cwd", { cwd });
100
+ return render(language, "message.injectedContext", { summary });
101
+ }
102
+ /**
103
+ * 消息 → 边流边写的入参。
104
+ *
105
+ * `content` 走 JSON 列:存储层的 `content` 列就是 core 内容段的 JSON,两边的形状一致,
106
+ * 只差存储层为了「不 import core」而声明的索引签名,故此处做一次显式转换。
107
+ * `parentToolCallId` 非空即「这条消息是某个子 Agent 说的」(R37 / F-I7)。
108
+ */
109
+ function toRow(message, conversationId, createdAt, statusOverride, parentToolCallId) {
110
+ const data = {};
111
+ if (message.tool_calls)
112
+ data.tool_calls = message.tool_calls;
113
+ if (message.function_call)
114
+ data.function_call = message.function_call;
115
+ if (message.tool_call_id !== undefined)
116
+ data.tool_call_id = message.tool_call_id;
117
+ if (message.name !== undefined)
118
+ data.name = message.name;
119
+ if (message.reasoning_content !== undefined)
120
+ data.reasoning_content = message.reasoning_content;
121
+ if (message.usage)
122
+ data.usage = message.usage;
123
+ if (message.subAgentUsage)
124
+ data.subAgentUsage = message.subAgentUsage;
125
+ if (message.finish_reason !== undefined)
126
+ data.finish_reason = message.finish_reason;
127
+ if (message.hidden !== undefined)
128
+ data.hidden = message.hidden;
129
+ return {
130
+ id: message.id,
131
+ conversationId,
132
+ role: message.role,
133
+ status: (statusOverride ?? message.status ?? AgentNSValue.MessageStatus.Unknown),
134
+ omit: message.omit === true,
135
+ content: (message.content ?? null),
136
+ data: Object.keys(data).length > 0 ? data : null,
137
+ parentToolCallId: parentToolCallId ?? null,
138
+ createdAt,
139
+ };
140
+ }
141
+ /** 存储行 → core 消息(缺的字段让它缺,不补空值:core 用 `undefined` 表示「没有」) */
142
+ function fromRow(row) {
143
+ const data = row.data ?? {};
144
+ const message = {
145
+ id: row.id,
146
+ role: toRole(row.role),
147
+ status: toStatus(row.status),
148
+ };
149
+ if (row.content !== null) {
150
+ message.content = row.content;
151
+ }
152
+ if (Array.isArray(data.tool_calls)) {
153
+ message.tool_calls = data.tool_calls;
154
+ }
155
+ if (isRecord(data.function_call)) {
156
+ message.function_call = data.function_call;
157
+ }
158
+ if (typeof data.tool_call_id === "string")
159
+ message.tool_call_id = data.tool_call_id;
160
+ if (typeof data.name === "string")
161
+ message.name = data.name;
162
+ if (typeof data.reasoning_content === "string") {
163
+ message.reasoning_content = data.reasoning_content;
164
+ }
165
+ const usage = toUsage(data.usage);
166
+ if (usage)
167
+ message.usage = usage;
168
+ const subAgentUsage = toUsage(data.subAgentUsage);
169
+ if (subAgentUsage)
170
+ message.subAgentUsage = subAgentUsage;
171
+ if (typeof data.finish_reason === "string") {
172
+ message.finish_reason = data.finish_reason;
173
+ }
174
+ if (typeof data.hidden === "boolean")
175
+ message.hidden = data.hidden;
176
+ message.omit = row.omit;
177
+ return message;
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // 失败事实(ProblemFact:码 + 文案键 + 参数 + 一句已渲染的话)
181
+ // ---------------------------------------------------------------------------
182
+ /**
183
+ * 造一条失败事实。
184
+ *
185
+ * `message` 用缺省语言渲染一份:落库/推送里总要有一句现成的话给日志和排查用;
186
+ * 真正面向某个连接的语言由接线层按 `code` / `messageKey` / `params` 重新渲染(architecture §6)。
187
+ */
188
+ function problem(code, messageKey, params = {}, detail) {
189
+ const fact = {
190
+ code,
191
+ messageKey,
192
+ params,
193
+ message: render(DEFAULT_LANGUAGE, messageKey, params),
194
+ };
195
+ if (detail !== undefined)
196
+ fact.detail = detail;
197
+ return fact;
198
+ }
199
+ /** SDK 的 `ContextOverflowError` 形状(只认字段,避免为一次判断引入 SDK 的运行时依赖面) */
200
+ function asContextOverflow(error) {
201
+ if (!isRecord(error))
202
+ return undefined;
203
+ if (error.name !== "ContextOverflowError")
204
+ return undefined;
205
+ const { promptTokens, guardTokens } = error;
206
+ if (typeof promptTokens !== "number" || typeof guardTokens !== "number")
207
+ return undefined;
208
+ return { promptTokens, guardTokens };
209
+ }
210
+ /** 任意抛出物 → 失败事实:到了硬线就说硬线,其余都归为内部错误 + 原始原因 */
211
+ function toProblem(error) {
212
+ const overflow = asContextOverflow(error);
213
+ if (overflow) {
214
+ return problem("context_guard", "error.contextGuard", { limit: overflow.guardTokens, used: overflow.promptTokens }, overflow);
215
+ }
216
+ const reason = error instanceof Error ? error.message : String(error);
217
+ return problem("internal", "error.internal", {}, { reason });
218
+ }
219
+ /**
220
+ * 一轮的现场:消息真相、六态、用量、写库节流。
221
+ *
222
+ * 它同时是 `ChatTurn`(`conversation.send` 的受理回执):受理与执行是同一件事的两半,
223
+ * 分成两个对象只会让「谁持有内存真相」含糊。
224
+ */
225
+ class TurnSession {
226
+ conversationId;
227
+ userMessageId;
228
+ assistantMessageId;
229
+ attachments;
230
+ acceptedAt;
231
+ done;
232
+ conversation;
233
+ scope;
234
+ createAgent;
235
+ /** 还原后的历史(这一轮实际交给 agent 的那份) */
236
+ history;
237
+ /** 这一轮发给模型的内容(`prepareAttachments` 的产物) */
238
+ content;
239
+ /** 落库的内容(`__asset_id` 形态):它就是用户挂的那几张图 */
240
+ storedContent;
241
+ language;
242
+ handle;
243
+ options;
244
+ clock;
245
+ newId;
246
+ startedAtMs;
247
+ onSettled;
248
+ resolveDone;
249
+ agent;
250
+ aborted = false;
251
+ problem = null;
252
+ settledAtMs = null;
253
+ /** 已经确认过 id 的消息(历史 + 本轮);不在里面的会被认领成宿主 id */
254
+ knownIds = new Set();
255
+ /** 每条消息第一次出现的时间(库里的 `createdAt`) */
256
+ createdAtOf = new Map();
257
+ /** 本轮 core 追加过的消息 id,按先后顺序 */
258
+ producedIds = [];
259
+ /** 中止时在途的消息 id(D15:这些行以 `Aborted` 落库) */
260
+ abortedInFlight = new Set();
261
+ deltaSeq = new Map();
262
+ runningTools = new Map();
263
+ subagents = new Map();
264
+ /** 已经落过库的子 Agent 委派(工具调用 id) */
265
+ persistedSubAgents = new Set();
266
+ userIdUsed = false;
267
+ assistantIdUsed = false;
268
+ streaming;
269
+ /** 结算后 agent 已经被丢掉:把本轮产出留在这里,快照与用量还读得到 */
270
+ detached;
271
+ generation;
272
+ writeTimer;
273
+ lastWriteAt = 0;
274
+ writeChain = Promise.resolve();
275
+ constructor(options, init) {
276
+ this.options = options;
277
+ this.clock = options.clock ?? systemClock;
278
+ this.newId = options.newId ?? defaultNewId;
279
+ this.conversation = init.conversation;
280
+ this.conversationId = init.conversation.id;
281
+ this.scope = init.scope;
282
+ this.createAgent = init.createAgent;
283
+ this.history = init.history;
284
+ this.content = init.content;
285
+ this.storedContent = init.storedContent ?? init.content;
286
+ this.language = init.language;
287
+ this.attachments = init.attachments;
288
+ this.userMessageId = init.userMessageId;
289
+ this.assistantMessageId = init.assistantMessageId;
290
+ this.handle = init.handle;
291
+ this.acceptedAt = init.acceptedAt;
292
+ this.startedAtMs = init.startedAtMs;
293
+ this.onSettled = init.onSettled;
294
+ this.generation = { phase: "connected", since: init.acceptedAt };
295
+ // 受理时刚写过一次占位行:节流从那一次算起
296
+ this.lastWriteAt = init.startedAtMs;
297
+ this.done = new Promise((resolve) => {
298
+ this.resolveDone = resolve;
299
+ });
300
+ for (const message of init.history) {
301
+ this.knownIds.add(message.id);
302
+ this.createdAtOf.set(message.id, this.startedAtMs);
303
+ }
304
+ // 两条由宿主预先分配的消息:id 已经是我们的,不再认领
305
+ this.createdAtOf.set(this.userMessageId, this.startedAtMs);
306
+ this.createdAtOf.set(this.assistantMessageId, this.startedAtMs);
307
+ }
308
+ // ---- 对外的两个动作 ----
309
+ abort(reason) {
310
+ this.handle.abort(reason);
311
+ }
312
+ runState() {
313
+ return this.buildRunState();
314
+ }
315
+ /** 真正跑一轮。`ChatRunner` 在受理之后立刻叫它,不等结果 */
316
+ async execute() {
317
+ try {
318
+ this.pushRunState();
319
+ await this.attemptOnce();
320
+ }
321
+ catch (error) {
322
+ // 一轮就是一次尝试:失败原样记成这一轮的结局。厂商那侧的降级(Files API 失败改内联、
323
+ // 模型看不了图转为文字)在 SDK 的选路里,这里不重发。
324
+ this.recordFailure(error);
325
+ }
326
+ finally {
327
+ await this.settle();
328
+ }
329
+ }
330
+ /** 一次尝试:建 agent → 把历史与用户输入发出去;失败原样上抛,由 `execute` 记成这一轮的结局 */
331
+ async attemptOnce() {
332
+ const agent = await this.createAgent({
333
+ conversationId: this.conversationId,
334
+ workspaceId: this.conversation.workspaceId,
335
+ agentId: this.conversation.agentId ?? "",
336
+ modelId: this.conversation.modelId,
337
+ thinkingLevel: this.conversation.thinkingLevel,
338
+ scope: this.scope,
339
+ history: [...this.history],
340
+ });
341
+ this.attach(agent);
342
+ // 装配 agent 期间用户就按了停:不再发请求,直接按中止结算
343
+ if (this.handle.aborted) {
344
+ this.takeAbortSnapshot();
345
+ agent.abort();
346
+ return;
347
+ }
348
+ await agent.send(this.content);
349
+ }
350
+ // ---- 装配:把 core 的事件接到本文件 ----
351
+ attach(agent) {
352
+ this.agent = agent;
353
+ // 历史就是 agent 的既有消息(内存里只有这一份)
354
+ agent.messages.push(...this.history);
355
+ agent.events.on("message", (message) => this.onMessage(message));
356
+ agent.events.on("open", () => this.enterAwaitingFirstToken());
357
+ agent.events.on("chunk-parsed", (receiver, chunk) => this.onChunk(receiver, chunk));
358
+ agent.events.on("error", (error) => this.recordFailure(error));
359
+ agent.events.on("tool-call-end", (ctx) => this.onToolCallEnd(ctx));
360
+ agent.events.on("sub-agent-start", (ctx) => this.onSubAgentStart(ctx));
361
+ agent.events.on("sub-agent-end", (ctx) => this.onSubAgentEnd(ctx));
362
+ // 中止只有一个入口:RunRegistry 的句柄(服务层的 abort 也走这里)
363
+ this.handle.onAbort(() => {
364
+ this.takeAbortSnapshot();
365
+ this.agent?.abort();
366
+ });
367
+ }
368
+ /** core 追加了一条消息:认领 id、记时间、更新六态里那些跟消息有关的部分 */
369
+ onMessage(message) {
370
+ if (!this.knownIds.has(message.id)) {
371
+ message.id = this.claimId(message);
372
+ this.knownIds.add(message.id);
373
+ this.createdAtOf.set(message.id, this.clock.now());
374
+ if (!this.producedIds.includes(message.id))
375
+ this.producedIds.push(message.id);
376
+ }
377
+ if (message.role === AgentNSValue.Role.Assistant) {
378
+ if (message.status === AgentNSValue.MessageStatus.Pending) {
379
+ // 新一轮内循环的占位:接下来要拼的就是它
380
+ this.streaming = message;
381
+ this.setGeneration({ phase: "connected", since: this.sinceFor("connected") });
382
+ }
383
+ return;
384
+ }
385
+ if (message.role === AgentNSValue.Role.Tool ||
386
+ message.role === AgentNSValue.Role.Function) {
387
+ this.trackRunningTool(message);
388
+ // 工具结果一出现就落一行(状态还是 pending):进程中间没了,装载时能看出
389
+ // 「这个工具参与了、但没定型」,而不是压根不知道它存在
390
+ this.enqueueWrite(message);
391
+ }
392
+ }
393
+ /**
394
+ * 认领 id:这一轮的头两条(用户消息、助手占位)用宿主已经发给客户端的 id,
395
+ * 之后 core 追加的每一条各自要一个新 id(id 由宿主分配,PRD L6)。
396
+ */
397
+ claimId(message) {
398
+ if (message.role === AgentNSValue.Role.User && !this.userIdUsed) {
399
+ this.userIdUsed = true;
400
+ return this.userMessageId;
401
+ }
402
+ if (message.role === AgentNSValue.Role.Assistant &&
403
+ !this.assistantIdUsed &&
404
+ message.status === AgentNSValue.MessageStatus.Pending) {
405
+ this.assistantIdUsed = true;
406
+ return this.assistantMessageId;
407
+ }
408
+ return this.newId();
409
+ }
410
+ /** 一片增量:推事件(core 的 `AgentNS.Delta` 原样)+ 推进六态 + 节流写库 */
411
+ onChunk(receiver, chunk) {
412
+ const delta = chunk.choices?.[0]?.delta;
413
+ // 只有外层字段的收尾片(带 usage / finish_reason)不产生增量事件
414
+ if (!delta)
415
+ return;
416
+ const seq = this.deltaSeq.get(receiver.id) ?? 0;
417
+ this.deltaSeq.set(receiver.id, seq + 1);
418
+ this.options.emit("message.delta", {
419
+ conversationId: this.conversationId,
420
+ messageId: receiver.id,
421
+ seq,
422
+ delta,
423
+ at: this.nowIso(),
424
+ });
425
+ this.advanceGeneration(receiver, delta);
426
+ this.scheduleStreamWrite(receiver);
427
+ }
428
+ /**
429
+ * 一个工具定型了(L8 的「在等哪几个工具」)。
430
+ *
431
+ * 剩下还有工具没定型就继续报「在等工具」,并把列表缩到还在跑的那些(并行时逐个减少)。
432
+ * 都定型了就不该停在「在等工具」:那一态描述的是一件已经不成立的事,此时回到「连上」
433
+ * (接下来要么是下一轮的首字,要么这一轮结束)。
434
+ */
435
+ onToolCallEnd(ctx) {
436
+ this.runningTools.delete(toolKey(ctx.toolCall));
437
+ if (this.runningTools.size === 0) {
438
+ this.setGeneration({ phase: "connected", since: this.sinceFor("connected") });
439
+ return;
440
+ }
441
+ this.setGeneration({
442
+ phase: "awaiting_tools",
443
+ since: this.sinceFor("awaiting_tools"),
444
+ runningTools: [...this.runningTools.values()],
445
+ });
446
+ }
447
+ onSubAgentStart(ctx) {
448
+ const toolCallId = ctx.toolCallContext.tool_call.id ?? "";
449
+ const snapshot = {
450
+ toolCallId,
451
+ name: subAgentName(ctx.subAgent),
452
+ status: "running",
453
+ startedAt: this.nowIso(),
454
+ finishedAt: null,
455
+ messageCount: ctx.subAgent.messages.length,
456
+ usage: null,
457
+ problem: null,
458
+ };
459
+ this.subagents.set(toolCallId, snapshot);
460
+ const running = this.runningTools.get(toolKey(ctx.toolCallContext.tool_call));
461
+ if (running)
462
+ running.subagent = snapshot;
463
+ this.pushRunState();
464
+ }
465
+ onSubAgentEnd(ctx) {
466
+ const toolCallId = ctx.toolCallContext.tool_call.id ?? "";
467
+ const existing = this.subagents.get(toolCallId);
468
+ if (!existing)
469
+ return;
470
+ const usage = sumUsage(ctx.subAgent.messages);
471
+ const failed = ctx.subAgent.messages.some((message) => message.status === AgentNSValue.MessageStatus.Error);
472
+ existing.status = this.aborted ? "aborted" : failed ? "error" : "done";
473
+ existing.finishedAt = this.nowIso();
474
+ existing.usage = usage;
475
+ if (failed) {
476
+ const reason = lastErrorText(ctx.subAgent.messages);
477
+ existing.problem = problem("subagent.failed", "message.subagentFailed", {
478
+ name: existing.name,
479
+ reason,
480
+ });
481
+ }
482
+ // F-I7:子 Agent 说过的话也落库(挂在那次委派的工具调用上),条数就是真落下去的条数
483
+ const rows = this.persistSubAgent(toolCallId, ctx.subAgent.messages);
484
+ existing.messageCount = rows;
485
+ this.pushRunState();
486
+ }
487
+ /**
488
+ * 把子 Agent 的消息落库(F-I7),返回落了多条。
489
+ *
490
+ * 同一次委派只落一遍(`sub-agent-end` 重复到达时不重复写);每条都拿宿主分配的 id(L6),
491
+ * 挂上 `parentToolCallId`,`seq` 由仓储在对话内递增。
492
+ */
493
+ persistSubAgent(toolCallId, messages) {
494
+ if (this.persistedSubAgents.has(toolCallId))
495
+ return messages.length;
496
+ this.persistedSubAgents.add(toolCallId);
497
+ const now = this.clock.now();
498
+ const rows = messages.map((message) => {
499
+ const id = this.newId();
500
+ this.createdAtOf.set(id, now);
501
+ return toRow({ ...message, id }, this.conversationId, now, undefined, toolCallId);
502
+ });
503
+ this.enqueueWrites(rows);
504
+ return rows.length;
505
+ }
506
+ /** 工具结果消息一出现就算「开始等它」;并行时列表里有几个就是几个(PRD L8) */
507
+ trackRunningTool(message) {
508
+ const call = {
509
+ id: message.tool_call_id,
510
+ type: "function",
511
+ function: { name: message.name ?? "" },
512
+ };
513
+ const key = message.tool_call_id ? `id:${message.tool_call_id}` : toolKey(call);
514
+ if (this.runningTools.has(key))
515
+ return;
516
+ this.runningTools.set(key, { toolCall: call, startedAt: this.nowIso() });
517
+ this.setGeneration({
518
+ phase: "awaiting_tools",
519
+ since: this.sinceFor("awaiting_tools"),
520
+ runningTools: [...this.runningTools.values()],
521
+ });
522
+ }
523
+ // ---- 六态 ----
524
+ /** 同一态内沿用原来的 `since`,换态才用此刻(客户端据此算「这一态多久了」) */
525
+ sinceFor(phase) {
526
+ return this.generation.phase === phase ? this.generation.since : this.nowIso();
527
+ }
528
+ enterAwaitingFirstToken() {
529
+ this.setGeneration({
530
+ phase: "awaiting_first_token",
531
+ since: this.sinceFor("awaiting_first_token"),
532
+ });
533
+ }
534
+ /**
535
+ * 按这一片带来的东西推进六态。
536
+ *
537
+ * 只往前推、不回退:思考之后来正文是「在想 → 在输出」;正文之后再来的思考片
538
+ * 不把状态拽回「在想」(同一条消息内部顺序恒定,F-I2)。
539
+ */
540
+ advanceGeneration(receiver, delta) {
541
+ if (delta.tool_calls?.length) {
542
+ this.setGeneration({
543
+ phase: "assembling_tool_calls",
544
+ since: this.sinceFor("assembling_tool_calls"),
545
+ toolCalls: assemblingToolCalls(receiver),
546
+ });
547
+ return;
548
+ }
549
+ if (delta.content !== undefined) {
550
+ this.setGeneration({
551
+ phase: "generating",
552
+ since: this.sinceFor("generating"),
553
+ chars: contentChars(receiver.content),
554
+ });
555
+ return;
556
+ }
557
+ if (delta.reasoning_content !== undefined && this.generation.phase !== "generating") {
558
+ this.setGeneration({
559
+ phase: "thinking",
560
+ since: this.sinceFor("thinking"),
561
+ chars: receiver.reasoning_content?.length ?? 0,
562
+ });
563
+ }
564
+ }
565
+ /**
566
+ * 换态/换列表时推一次 `run.state`。
567
+ *
568
+ * 字符数增长不单独推:增量本来就在 `message.delta` 里,`run.state` 只在「换了一态」或
569
+ * 「还在跑的工具变了」这两种事实变化时发,免得一条流式消息把状态事件刷成两倍流量。
570
+ */
571
+ setGeneration(next) {
572
+ const changed = !sameGeneration(this.generation, next);
573
+ this.generation = next;
574
+ if (changed)
575
+ this.pushRunState();
576
+ }
577
+ // ---- 写库 ----
578
+ /** 节流:到点就写一条,不到点就排一个「到点再写」 */
579
+ scheduleStreamWrite(message) {
580
+ const interval = this.options.streamWriteIntervalMs ?? DEFAULT_STREAM_WRITE_INTERVAL_MS;
581
+ const now = this.clock.now();
582
+ if (interval <= 0 || now - this.lastWriteAt >= interval) {
583
+ this.enqueueWrite(message);
584
+ return;
585
+ }
586
+ if (this.writeTimer)
587
+ return;
588
+ this.writeTimer = this.clock.setTimeout(() => {
589
+ this.writeTimer = undefined;
590
+ if (this.streaming)
591
+ this.enqueueWrite(this.streaming);
592
+ }, interval - (now - this.lastWriteAt));
593
+ }
594
+ enqueueWrite(message) {
595
+ this.lastWriteAt = this.clock.now();
596
+ const row = toRow(message, this.conversationId, this.createdAtOf.get(message.id) ?? this.clock.now(), this.abortedInFlight.has(message.id) ? AgentNSValue.MessageStatus.Aborted : undefined);
597
+ this.writeChain = this.writeChain
598
+ .then(() => this.options.messages.upsert(row))
599
+ .catch((error) => this.options.onError?.(error, "stream-write"));
600
+ }
601
+ /** 一批行按同一份额度写下去(子 Agent 的消息一次写一拍);与边流边写共用一条串行链 */
602
+ enqueueWrites(rows) {
603
+ if (rows.length === 0)
604
+ return;
605
+ this.lastWriteAt = this.clock.now();
606
+ this.writeChain = this.writeChain
607
+ .then(() => this.options.messages.upsertMany(rows))
608
+ .catch((error) => this.options.onError?.(error, "sub-agent-write"));
609
+ }
610
+ /** 把挂着的节流写收干净(结算前必须做:不能让「最终版」被一次迟到的增量写覆盖) */
611
+ async flushWrites() {
612
+ this.writeTimer?.();
613
+ this.writeTimer = undefined;
614
+ await this.writeChain;
615
+ }
616
+ // ---- 结算 ----
617
+ recordFailure(error) {
618
+ // 用户按下中止时 core 也可能抛一次(请求被 abort):那不是失败,不记 problem
619
+ if (this.aborted || this.handle.aborted)
620
+ return;
621
+ this.problem = toProblem(error);
622
+ }
623
+ /** 中止那一刻在途的消息:core 会把它们标成 Aborted,这里的 id 用来保证库里也记 Aborted(D15) */
624
+ takeAbortSnapshot() {
625
+ this.aborted = true;
626
+ for (const message of this.agent?.messages ?? []) {
627
+ if (message.status === AgentNSValue.MessageStatus.Pending ||
628
+ message.status === AgentNSValue.MessageStatus.Writing) {
629
+ this.abortedInFlight.add(message.id);
630
+ }
631
+ }
632
+ if (!this.assistantIdUsed)
633
+ this.abortedInFlight.add(this.assistantMessageId);
634
+ }
635
+ outcome() {
636
+ if (this.aborted || this.handle.aborted)
637
+ return "aborted";
638
+ return this.problem ? "error" : "completed";
639
+ }
640
+ async settle() {
641
+ if (this.settledAtMs !== null)
642
+ return;
643
+ this.settledAtMs = this.clock.now();
644
+ const outcome = this.outcome();
645
+ const produced = this.producedMessages();
646
+ try {
647
+ await this.flushWrites();
648
+ await this.persistFinal(outcome, produced);
649
+ }
650
+ catch (error) {
651
+ this.options.onError?.(error, "final-write");
652
+ }
653
+ // agent 跑完即弃(PRD D28):之后这一轮只剩内存里的消息与库里的快照。
654
+ // 本轮产出先捡出来,否则用量与 `done` 的载荷会跟着 agent 一起消失。
655
+ this.detached = produced;
656
+ this.agent = undefined;
657
+ this.options.runs.finish(this.conversationId);
658
+ this.options.scopes.release(this.conversation.workspaceId);
659
+ const usage = this.usageSnapshot();
660
+ const run = this.buildRunState();
661
+ this.options.emit("run.state", {
662
+ conversationId: this.conversationId,
663
+ run,
664
+ at: this.nowIso(),
665
+ });
666
+ this.options.emit("usage", { conversationId: this.conversationId, usage, at: this.nowIso() });
667
+ this.options.emit("conversation.changed", {
668
+ conversationId: this.conversationId,
669
+ workspaceId: this.conversation.workspaceId,
670
+ change: "messages_persisted",
671
+ messageCount: this.history.length + produced.length,
672
+ at: this.nowIso(),
673
+ });
674
+ this.resolveDone({
675
+ conversationId: this.conversationId,
676
+ assistantMessageId: this.assistantMessageId,
677
+ outcome,
678
+ problem: this.problem,
679
+ messages: [...this.history, ...produced],
680
+ produced,
681
+ usage,
682
+ run,
683
+ });
684
+ this.notifySettled(outcome, usage);
685
+ this.onSettled?.();
686
+ }
687
+ /**
688
+ * 一轮结算之后通知组装层(R59:交接线问题由它触发)。
689
+ *
690
+ * 这一跳只报事实:本轮 `prompt_tokens`、生效模型上那条交接线、这一轮连接的语言。
691
+ * 「到线了没、要不要问用户」不在本文件判断(PRD D22:问,且不在流式中途)。
692
+ */
693
+ notifySettled(outcome, usage) {
694
+ const onSettled = this.options.onTurnSettled;
695
+ if (!onSettled)
696
+ return;
697
+ const model = this.scope.config.models.find((candidate) => candidate.id === this.conversation.modelId);
698
+ try {
699
+ onSettled({
700
+ conversationId: this.conversationId,
701
+ outcome,
702
+ promptTokens: usage.turn.prompt_tokens,
703
+ handoffTokens: model?.tokenThresholds.handoff ?? 0,
704
+ language: this.language,
705
+ });
706
+ }
707
+ catch (error) {
708
+ this.options.onError?.(error, "on-turn-settled");
709
+ }
710
+ }
711
+ /**
712
+ * 内循环结束写一次最终版(PRD F-C2)。
713
+ *
714
+ * 除了本轮产出的消息,还要兜住两种情况:助手占位在 agent 建起来之前就结束了(装配失败、
715
+ * 或被立刻中止),库里那条占位必须有个终态,否则客户端会看到一条永远在写的消息。
716
+ */
717
+ async persistFinal(outcome, produced) {
718
+ const rows = produced.map((message) => {
719
+ const row = toRow(message, this.conversationId, this.createdAtOf.get(message.id) ?? this.settledAtMs ?? this.clock.now(), this.abortedInFlight.has(message.id) ? AgentNSValue.MessageStatus.Aborted : undefined);
720
+ // 用户那条消息在 core 那边带着「这一次发出去的内容」(厂商的 file_id / base64)。
721
+ // 落库要的是 `__asset_id` 那一份:否则库里这条消息就绑死在这次发送所用的端点上了。
722
+ if (message.id === this.userMessageId) {
723
+ row.content = this.storedContent;
724
+ }
725
+ return row;
726
+ });
727
+ if (!this.assistantIdUsed) {
728
+ const status = outcome === "aborted"
729
+ ? AgentNSValue.MessageStatus.Aborted
730
+ : outcome === "error"
731
+ ? AgentNSValue.MessageStatus.Error
732
+ : AgentNSValue.MessageStatus.Completed;
733
+ rows.push({
734
+ id: this.assistantMessageId,
735
+ conversationId: this.conversationId,
736
+ role: AgentNSValue.Role.Assistant,
737
+ status: status,
738
+ omit: false,
739
+ content: null,
740
+ data: null,
741
+ createdAt: this.startedAtMs,
742
+ });
743
+ }
744
+ if (rows.length > 0)
745
+ await this.options.messages.upsertMany(rows);
746
+ }
747
+ /** 本轮 core 追加过的消息(按先后顺序;被 core 整段替换掉的就不在里面了) */
748
+ producedMessages() {
749
+ if (this.detached)
750
+ return this.detached;
751
+ const messages = this.agent?.messages ?? [];
752
+ const byId = new Map(messages.map((message) => [message.id, message]));
753
+ const produced = [];
754
+ for (const id of this.producedIds) {
755
+ const message = byId.get(id);
756
+ if (message)
757
+ produced.push(message);
758
+ }
759
+ return produced;
760
+ }
761
+ // ---- 用量(PRD F-I5:本轮 / 累计(含子 Agent)/ 速率) ----
762
+ usageSnapshot() {
763
+ const produced = this.producedMessages();
764
+ const turn = sumUsage(produced);
765
+ // 累计 = 这一轮之前 + 这一轮;子 Agent 的开销在 `subAgentUsage` 里,`sumUsage` 已并入
766
+ const total = addUsage(sumUsage(this.history), turn);
767
+ const subAgent = sumSubAgentUsage([...this.history, ...produced]);
768
+ const elapsedMs = Math.max(0, (this.settledAtMs ?? this.clock.now()) - this.startedAtMs);
769
+ const rate = elapsedMs > 0 ? round2(turn.completion_tokens / (elapsedMs / 1000)) : 0;
770
+ return {
771
+ turn,
772
+ total,
773
+ subAgent,
774
+ rate: { value: rate, unit: "tokens_per_second", basis: "completion" },
775
+ updatedAt: this.nowIso(),
776
+ };
777
+ }
778
+ // ---- 快照与推送 ----
779
+ buildRunState() {
780
+ const settledAt = this.settledAtMs;
781
+ return {
782
+ conversationId: this.conversationId,
783
+ status: settledAt !== null ? "settled" : "running",
784
+ assistantMessageId: this.assistantMessageId,
785
+ startedAt: new Date(this.startedAtMs).toISOString(),
786
+ settledAt: settledAt !== null ? new Date(settledAt).toISOString() : null,
787
+ generation: settledAt !== null ? null : this.generation,
788
+ outcome: settledAt !== null ? this.outcome() : null,
789
+ problem: this.problem,
790
+ subagents: this.finalizedSubagents(),
791
+ usage: this.usageSnapshot(),
792
+ };
793
+ }
794
+ pushRunState() {
795
+ this.options.emit("run.state", {
796
+ conversationId: this.conversationId,
797
+ run: this.buildRunState(),
798
+ at: this.nowIso(),
799
+ });
800
+ }
801
+ /** 结算时还没跑完的子 Agent:这一轮中止了就记 aborted,否则记 error(没有「悄悄消失」这一档) */
802
+ finalizedSubagents() {
803
+ const settled = this.settledAtMs !== null;
804
+ const items = [...this.subagents.values()];
805
+ if (!settled)
806
+ return items;
807
+ for (const item of items) {
808
+ if (item.status !== "running")
809
+ continue;
810
+ item.status = this.aborted || this.handle.aborted ? "aborted" : "error";
811
+ item.finishedAt = item.finishedAt ?? this.nowIso();
812
+ }
813
+ return items;
814
+ }
815
+ nowIso() {
816
+ return new Date(this.clock.now()).toISOString();
817
+ }
818
+ }
819
+ /** 六态里「正在拼的工具调用」:从 core 正在拼的消息上取,参数长度给出一个可见的进度 */
820
+ function assemblingToolCalls(receiver) {
821
+ return (receiver.tool_calls ?? []).map((toolCall) => ({
822
+ toolCall,
823
+ argumentsChars: toolCall.function?.arguments?.length ?? 0,
824
+ }));
825
+ }
826
+ /** 两个六态快照是不是「同一件事」(只比对外有意义的那些事实) */
827
+ function sameGeneration(a, b) {
828
+ if (a.phase !== b.phase)
829
+ return false;
830
+ // 在想 / 在输出:字符数逐片在长,但那件事本身没变(增量已经逐片推过了)
831
+ if (a.phase === "assembling_tool_calls" && b.phase === "assembling_tool_calls") {
832
+ return toolListKey(a.toolCalls.map((item) => item.toolCall)) ===
833
+ toolListKey(b.toolCalls.map((item) => item.toolCall));
834
+ }
835
+ if (a.phase === "awaiting_tools" && b.phase === "awaiting_tools") {
836
+ return toolListKey(a.runningTools.map((item) => item.toolCall)) ===
837
+ toolListKey(b.runningTools.map((item) => item.toolCall));
838
+ }
839
+ return true;
840
+ }
841
+ function toolListKey(toolCalls) {
842
+ return toolCalls.map((toolCall) => toolKey(toolCall)).join("|");
843
+ }
844
+ /** 子 Agent 的名字:SDK 的 `SdkAgent` 带定义,core 的 `Agent` 没有,认不出来就退回通用名 */
845
+ function subAgentName(agent) {
846
+ const definition = agent.definition;
847
+ return definition?.name ?? "sub-agent";
848
+ }
849
+ /** 子 Agent 失败时给人看的原因:最后一条 error 消息的正文 */
850
+ function lastErrorText(messages) {
851
+ for (let i = messages.length - 1; i >= 0; i--) {
852
+ const message = messages[i];
853
+ if (message.status !== AgentNSValue.MessageStatus.Error)
854
+ continue;
855
+ const content = message.content;
856
+ if (typeof content === "string")
857
+ return content;
858
+ if (Array.isArray(content)) {
859
+ const text = content
860
+ .map((section) => ("text" in section ? section.text : ""))
861
+ .join(" ")
862
+ .trim();
863
+ if (text)
864
+ return text;
865
+ }
866
+ return "error";
867
+ }
868
+ return "error";
869
+ }
870
+ /** 子 Agent 贡献的用量:`subAgentUsage` 一栏(含孙级,core 的委派路径会累加) */
871
+ function sumSubAgentUsage(messages) {
872
+ let total = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
873
+ for (const message of messages) {
874
+ if (message.subAgentUsage)
875
+ total = addUsage(total, message.subAgentUsage);
876
+ }
877
+ return total;
878
+ }
879
+ /** 宿主分配 id 的缺省实现(Node 19+ 有 `crypto.randomUUID`) */
880
+ function defaultNewId() {
881
+ const crypto = globalThis.crypto;
882
+ if (typeof crypto?.randomUUID === "function")
883
+ return crypto.randomUUID();
884
+ return `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
885
+ }
886
+ // ---------------------------------------------------------------------------
887
+ // ChatRunner
888
+ // ---------------------------------------------------------------------------
889
+ export class ChatRunner {
890
+ options;
891
+ clock;
892
+ /**
893
+ * 正在跑的轮次(对话 id → 那一轮的现场)。
894
+ *
895
+ * 存的是**这一轮自己的内存对象**,供 `conversation.state` 拿到流中的六态与用量;
896
+ * 「有没有在跑」仍然只问 `RunRegistry.isRunning`,两张表不由这里合并。
897
+ */
898
+ turns = new Map();
899
+ constructor(options) {
900
+ this.options = options;
901
+ this.clock = options.clock ?? systemClock;
902
+ }
903
+ /** 宿主分配消息 id(PRD L6):缺省 `crypto.randomUUID`,测试可注入递增 id */
904
+ newId() {
905
+ return (this.options.newId ?? defaultNewId)();
906
+ }
907
+ /** 缺省的 agent 工厂(不注入 `createAgent` 时用):按 SDK 口径装配,交接服务来自 `handoff` */
908
+ agentFactory() {
909
+ return this.options.createAgent ?? createSdkAgentFactory({ handoff: this.options.handoff });
910
+ }
911
+ /**
912
+ * 受理一轮:占位、落两条消息、把执行放到后台,然后把两个 id 还给调用方(PRD L6 / F-I12)。
913
+ *
914
+ * 忙即拒(`HostError("busy")`,D14);对话不存在则 `not_found`。任何一种失败都不会
915
+ * 留下一个占着不动的轮次,也不会让 Scope 的依赖计数多算一次。
916
+ */
917
+ async run(params) {
918
+ const { conversationId } = params;
919
+ const handle = this.options.runs.start(conversationId);
920
+ const language = params.language ?? this.options.defaultLanguage ?? DEFAULT_LANGUAGE;
921
+ let workspaceId;
922
+ let session;
923
+ try {
924
+ const conversation = await this.options.conversations.get(conversationId);
925
+ if (!conversation) {
926
+ throw new HostError("not_found", { params: { entity: "conversation" } });
927
+ }
928
+ workspaceId = conversation.workspaceId;
929
+ const scope = await this.options.scopes.acquire(workspaceId);
930
+ // 装载即修复:上一次进程如果是在写的时候没的,这里把在途消息修成可继续的状态
931
+ const stored = await this.reconcile(conversationId);
932
+ const history = await this.resolveHistory(conversationId, stored.messages, conversation.modelId);
933
+ const prepared = await this.prepare(params, conversation);
934
+ const nowMs = this.clock.now();
935
+ const acceptedAt = new Date(nowMs).toISOString();
936
+ const userId = this.newId();
937
+ const assistantId = this.newId();
938
+ // 用户消息与助手占位都在请求返回之前落库(L6:客户端拿着这两个 id 接流)。
939
+ // 用户消息存的是 `__asset_id` 形态(`storedContent`),不是这一次发送的产物
940
+ await this.options.messages.upsertMany([
941
+ {
942
+ id: userId,
943
+ conversationId,
944
+ role: AgentNSValue.Role.User,
945
+ status: AgentNSValue.MessageStatus.Completed,
946
+ omit: false,
947
+ content: (prepared.storedContent ?? prepared.content),
948
+ data: null,
949
+ createdAt: nowMs,
950
+ },
951
+ {
952
+ id: assistantId,
953
+ conversationId,
954
+ role: AgentNSValue.Role.Assistant,
955
+ status: AgentNSValue.MessageStatus.Pending,
956
+ omit: false,
957
+ content: null,
958
+ data: null,
959
+ createdAt: nowMs,
960
+ },
961
+ ]);
962
+ // 用户消息刚落库:把 content 里的 `__asset_id` 记成关系行(同一 message+asset 只一行)
963
+ await this.linkMessageAssets(conversationId, userId, prepared.storedContent ?? prepared.content);
964
+ session = new TurnSession(this.options, {
965
+ conversation,
966
+ scope,
967
+ createAgent: this.agentFactory(),
968
+ history,
969
+ content: prepared.content,
970
+ ...(prepared.storedContent === undefined ? {} : { storedContent: prepared.storedContent }),
971
+ attachments: prepared.attachments,
972
+ userMessageId: userId,
973
+ assistantMessageId: assistantId,
974
+ handle,
975
+ language,
976
+ acceptedAt,
977
+ startedAtMs: nowMs,
978
+ onSettled: () => {
979
+ this.turns.delete(conversationId);
980
+ },
981
+ });
982
+ }
983
+ catch (error) {
984
+ this.options.runs.finish(conversationId);
985
+ if (workspaceId !== undefined)
986
+ this.options.scopes.release(workspaceId);
987
+ throw error;
988
+ }
989
+ // 受理完成:执行交给后台,事件从这一刻起会陆续推给在线客户端
990
+ this.turns.set(session.conversationId, session);
991
+ void session.execute();
992
+ return session;
993
+ }
994
+ /**
995
+ * 历史装载完之后的第一件事情:把里面的 `__asset_id` 还原成当前模型能吃的段(F-H7)。
996
+ *
997
+ * 还原失败不让整轮发不出去:打一条日志、带着原样的历史继续。
998
+ */
999
+ async resolveHistory(conversationId, messages, modelId) {
1000
+ const resolve = this.options.prepareHistory;
1001
+ if (!resolve)
1002
+ return messages;
1003
+ try {
1004
+ return await resolve({ conversationId, modelId, messages });
1005
+ }
1006
+ catch (error) {
1007
+ this.options.onError?.(error, "还原历史里的素材引用失败");
1008
+ return messages;
1009
+ }
1010
+ }
1011
+ /**
1012
+ * 用户消息落库之后登记素材关系行(`conversation_assets`)。
1013
+ *
1014
+ * 记不下来不让这一轮失败(消息已经发出去了):打一条日志继续 —— 后果只是这份素材的
1015
+ * 引用计数少一行,删字节会晚一步。
1016
+ */
1017
+ async linkMessageAssets(conversationId, messageId, content) {
1018
+ const link = this.options.linkMessageAssets;
1019
+ if (!link)
1020
+ return;
1021
+ try {
1022
+ await link({ conversationId, messageId, content });
1023
+ }
1024
+ catch (error) {
1025
+ this.options.onError?.(error, "登记消息的素材引用失败");
1026
+ }
1027
+ }
1028
+ /** 这一段对话正在跑的那一轮(没有就 `undefined`):拉现场用 */
1029
+ turn(conversationId) {
1030
+ return this.turns.get(conversationId);
1031
+ }
1032
+ /** 中止(转发给 `RunRegistry`:中止的唯一入口在那里) */
1033
+ abort(conversationId, reason) {
1034
+ return this.options.runs.abort(conversationId, reason);
1035
+ }
1036
+ /**
1037
+ * 对话(或它所在的项目)被删之后,把这一轮的内存现场丢掉。
1038
+ *
1039
+ * 正在跑的那一轮会先被要求停下,再等它自己结算写库(删对话的人已经先确认过代价)。
1040
+ */
1041
+ forget(conversationId) {
1042
+ const turn = this.turns.get(conversationId);
1043
+ turn?.abort("conversation removed");
1044
+ this.turns.delete(conversationId);
1045
+ }
1046
+ /**
1047
+ * 装载/重启后修复(PRD D15)。
1048
+ *
1049
+ * 两步:
1050
+ * 1. 在途(`pending` / `writing`)消息当作被中断 —— 进程是在写它们的时候没的;
1051
+ * 2. 交 core 的两支归一:工具轮次 `fill`(整轮进出,补上未完成的原因)、
1052
+ * 被中断的 assistant `keep`(已输出的内容留在上下文里,模型下一轮能接着半截说)。
1053
+ *
1054
+ * 只写回状态/`omit` 真的变了的那些行,于是「启动时扫一遍」不会把整段历史重写一次。
1055
+ */
1056
+ async reconcile(conversationId) {
1057
+ // 历史要连 `hidden` 的一起取:那些消息只是不给人看,模型下一轮照样得看到它们
1058
+ // (会话创建时那条 cwd 消息、交接摘要)
1059
+ const rows = await this.options.messages.listByConversation(conversationId, undefined, {
1060
+ includeHidden: true,
1061
+ });
1062
+ const messages = rows.map(fromRow);
1063
+ for (const message of messages) {
1064
+ if (message.status === AgentNSValue.MessageStatus.Pending ||
1065
+ message.status === AgentNSValue.MessageStatus.Writing) {
1066
+ message.status = AgentNSValue.MessageStatus.Aborted;
1067
+ }
1068
+ }
1069
+ reconcileAbortedToolRounds(messages, "fill");
1070
+ reconcileAbortedMessages(messages, "keep");
1071
+ const changed = [];
1072
+ messages.forEach((message, index) => {
1073
+ const row = rows[index];
1074
+ if (!row)
1075
+ return;
1076
+ if (row.status === message.status && row.omit === (message.omit === true))
1077
+ return;
1078
+ changed.push(toRow(message, conversationId, row.createdAt));
1079
+ });
1080
+ if (changed.length > 0)
1081
+ await this.options.messages.upsertMany(changed);
1082
+ return { messages, repairedIds: changed.map((row) => row.id) };
1083
+ }
1084
+ async prepare(params, conversation) {
1085
+ const prepare = this.options.prepareAttachments;
1086
+ if (!prepare) {
1087
+ // 没有注入素材转换:`assetIds` 原样不处理,内容照客户端给的发(事实在 `attachments` 里给空)
1088
+ return { content: params.input.content, attachments: [] };
1089
+ }
1090
+ return prepare({
1091
+ conversationId: conversation.id,
1092
+ agentId: conversation.agentId ?? "",
1093
+ modelId: conversation.modelId,
1094
+ input: params.input,
1095
+ });
1096
+ }
1097
+ }
1098
+ /**
1099
+ * 这一段会话该带哪些模型参数(PRD F-B6)。
1100
+ *
1101
+ * 两件事合在这里:
1102
+ * 1. 助手定义上的 `modelParams`(用户给这个助手配的参数,也是「跟随默认」所指的那份);
1103
+ * 2. 会话行上的思考档位展开出的补丁(档位名 → 参数),盖在 1 上面。
1104
+ *
1105
+ * 只在「会话定的模型就是助手定义那个模型」时才叠上 1:会话把模型换掉之后,定义上那份参数
1106
+ * 是配给旧模型的(可能带着旧模型才认的键),发给新模型只会换来一句 400。换模型时档位会被
1107
+ * 清空,所以那之后这里通常什么都不带,让模型走自己的出厂参数。
1108
+ */
1109
+ export function conversationModelParams(input) {
1110
+ const agentModelId = input.agentModelId ?? input.defaultModelId;
1111
+ const sameModel = input.model !== undefined && input.model.id === agentModelId;
1112
+ const base = sameModel ? input.agentParams : undefined;
1113
+ const patch = thinkingPatchOf(input.model, input.thinkingLevel, input.presets);
1114
+ if (!patch)
1115
+ return base;
1116
+ return { ...(base ?? {}), ...patch };
1117
+ }
1118
+ /** 档位名 → 参数补丁(模型没配策略表、表里没这个档、或没选档位时都是 `undefined`) */
1119
+ function thinkingPatchOf(model, thinkingLevel, presets) {
1120
+ if (!model || thinkingLevel === null || !presets)
1121
+ return undefined;
1122
+ const presetName = model.reasoningPreset;
1123
+ if (!presetName)
1124
+ return undefined;
1125
+ const preset = presets[presetName];
1126
+ if (!preset || !Object.prototype.hasOwnProperty.call(preset.levels, thinkingLevel)) {
1127
+ return undefined;
1128
+ }
1129
+ return preset.levels[thinkingLevel];
1130
+ }
1131
+ /**
1132
+ * 读一次助手定义(只为拿它绑定哪个模型、带哪些参数)。
1133
+ *
1134
+ * SDK 的 `createAgent` 自己也会读一次定义;这里多这一遍是因为会话档位展开的参数要叠在
1135
+ * 定义那份参数上,而只有定义在手才知道该叠什么。读不出来就当作没有(真正的报错由
1136
+ * `createAgent` 那句给出)。
1137
+ */
1138
+ async function readAgentDefinition(context) {
1139
+ try {
1140
+ return await new AgentRepository(context.scope.agentsDir).read(context.agentId);
1141
+ }
1142
+ catch {
1143
+ return null;
1144
+ }
1145
+ }
1146
+ /**
1147
+ * 造一个按 SDK 口径装配的 agent 工厂。
1148
+ *
1149
+ * 装的东西与阈值来源(PRD D27:阈值属 SDK 的 `AppConfig` / 模型配置,不在这里另起一套):
1150
+ * - `ContextGuardPlugin`:硬线,`tokenThresholds.guard`;
1151
+ * - `SubAgentGuardInstallerPlugin`:同一个硬线递归装到子 Agent 与孙级(PRD F-D8);
1152
+ * - `AutoMigratePlugin`:交接线,`tokenThresholds.handoff`(需注入交接服务);
1153
+ * - `AutoRefreshToolsPlugin`:改完能力不用重启(PRD F-E2);
1154
+ * - `UnknownToolHintPlugin`:未知工具时给出 MCP 场景的引导。
1155
+ */
1156
+ export function createSdkAgentFactory(options = {}) {
1157
+ return async (context) => {
1158
+ // 会话行上的模型就是这一轮的模型(`conversation.setModel` 写的就是它),
1159
+ // 再往下的三级解析(助手定义 → 全局兜底)仍由 SDK 那一处算
1160
+ const modelId = context.modelId ?? undefined;
1161
+ const sessionModel = context.scope.config.models.find((candidate) => candidate.id === modelId);
1162
+ const definition = await readAgentDefinition(context);
1163
+ const agent = await createAgent(context.scope, context.agentId, {
1164
+ modelId,
1165
+ modelParams: conversationModelParams({
1166
+ model: sessionModel,
1167
+ agentModelId: definition?.modelId,
1168
+ defaultModelId: context.scope.config.defaultModel,
1169
+ agentParams: definition?.modelParams,
1170
+ thinkingLevel: context.thinkingLevel,
1171
+ presets: options.thinkingPresets?.() ?? null,
1172
+ }),
1173
+ });
1174
+ // 生效的模型写在 `agent.definition.modelId` 上(SDK 三级解析之后的那一份)
1175
+ const model = context.scope.config.models.find((candidate) => candidate.id === agent.definition.modelId);
1176
+ const thresholds = model?.tokenThresholds;
1177
+ const handoff = options.handoff?.(context);
1178
+ // 交接回合本身就是上下文最满的时刻:硬线要对它让行,否则「交接触发拦截 → 拦截又触发交接」
1179
+ const isSuspended = handoff ? () => handoff.busy : undefined;
1180
+ if (thresholds) {
1181
+ agent.use(new ContextGuardPlugin({ guardTokens: thresholds.guard, isSuspended }));
1182
+ agent.use(new SubAgentGuardInstallerPlugin({ guardTokens: thresholds.guard, isSuspended }));
1183
+ if (handoff) {
1184
+ agent.use(new AutoMigratePlugin({
1185
+ service: handoff,
1186
+ handoffTokens: thresholds.handoff,
1187
+ // R59 / PRD §5:宿主不替用户决定「要不要迁移」——问用户的是
1188
+ // `QuestionService.askHandoff`(一轮结算之后),这里一律跳过本次迁移。
1189
+ resolveMode: () => null,
1190
+ }));
1191
+ }
1192
+ }
1193
+ const refreshWhen = options.autoRefreshTools ??
1194
+ context.scope.config.autoRefreshTools ??
1195
+ "inner-loop-start";
1196
+ if (refreshWhen !== "off") {
1197
+ agent.use(new AutoRefreshToolsPlugin({ when: refreshWhen }));
1198
+ }
1199
+ agent.use(new UnknownToolHintPlugin({ scope: context.scope }));
1200
+ // 插件在 send 之前 init(例如工具刷新插件的首次准备)
1201
+ await agent.init();
1202
+ return agent;
1203
+ };
1204
+ }