@zooique/memora 0.3.0 → 2.0.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 (188) hide show
  1. package/README.md +8 -8
  2. package/dist/agent/agent.d.ts +69 -92
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +267 -197
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/assembler.d.ts +3 -0
  7. package/dist/agent/assembler.d.ts.map +1 -1
  8. package/dist/agent/assembler.js +14 -4
  9. package/dist/agent/assembler.js.map +1 -1
  10. package/dist/agent/builtinToolHandlers.d.ts +1 -1
  11. package/dist/agent/builtinToolHandlers.d.ts.map +1 -1
  12. package/dist/agent/builtinToolHandlers.js +5 -4
  13. package/dist/agent/builtinToolHandlers.js.map +1 -1
  14. package/dist/agent/guardrail.d.ts +47 -0
  15. package/dist/agent/guardrail.d.ts.map +1 -0
  16. package/dist/agent/guardrail.js +51 -0
  17. package/dist/agent/guardrail.js.map +1 -0
  18. package/dist/agent/loop.d.ts +79 -18
  19. package/dist/agent/loop.d.ts.map +1 -1
  20. package/dist/agent/loop.js +317 -231
  21. package/dist/agent/loop.js.map +1 -1
  22. package/dist/agent/managers/chatLockManager.d.ts +116 -0
  23. package/dist/agent/managers/chatLockManager.d.ts.map +1 -0
  24. package/dist/agent/managers/chatLockManager.js +191 -0
  25. package/dist/agent/managers/chatLockManager.js.map +1 -0
  26. package/dist/agent/managers/insightExtractor.d.ts +34 -69
  27. package/dist/agent/managers/insightExtractor.d.ts.map +1 -1
  28. package/dist/agent/managers/insightExtractor.js +133 -189
  29. package/dist/agent/managers/insightExtractor.js.map +1 -1
  30. package/dist/agent/managers/memoryAdvisor.d.ts +102 -2
  31. package/dist/agent/managers/memoryAdvisor.d.ts.map +1 -1
  32. package/dist/agent/managers/memoryAdvisor.js +262 -4
  33. package/dist/agent/managers/memoryAdvisor.js.map +1 -1
  34. package/dist/agent/managers/memoryDecayScheduler.d.ts +85 -0
  35. package/dist/agent/managers/memoryDecayScheduler.d.ts.map +1 -1
  36. package/dist/agent/managers/memoryDecayScheduler.js +215 -0
  37. package/dist/agent/managers/memoryDecayScheduler.js.map +1 -1
  38. package/dist/agent/managers/memoryInspector.d.ts +230 -64
  39. package/dist/agent/managers/memoryInspector.d.ts.map +1 -1
  40. package/dist/agent/managers/memoryInspector.js +439 -81
  41. package/dist/agent/managers/memoryInspector.js.map +1 -1
  42. package/dist/agent/managers/relationBuilder.d.ts +131 -0
  43. package/dist/agent/managers/relationBuilder.d.ts.map +1 -0
  44. package/dist/agent/managers/relationBuilder.js +192 -0
  45. package/dist/agent/managers/relationBuilder.js.map +1 -0
  46. package/dist/agent/managers/sessionArchiver.d.ts +6 -0
  47. package/dist/agent/managers/sessionArchiver.d.ts.map +1 -1
  48. package/dist/agent/managers/sessionArchiver.js +68 -8
  49. package/dist/agent/managers/sessionArchiver.js.map +1 -1
  50. package/dist/agent/managers/sessionManager.d.ts.map +1 -1
  51. package/dist/agent/managers/sessionManager.js +9 -17
  52. package/dist/agent/managers/sessionManager.js.map +1 -1
  53. package/dist/agent/managers/textPolishManager.d.ts +62 -0
  54. package/dist/agent/managers/textPolishManager.d.ts.map +1 -0
  55. package/dist/agent/managers/textPolishManager.js +99 -0
  56. package/dist/agent/managers/textPolishManager.js.map +1 -0
  57. package/dist/agent/personaMatcher.d.ts +29 -0
  58. package/dist/agent/personaMatcher.d.ts.map +1 -0
  59. package/dist/agent/personaMatcher.js +60 -0
  60. package/dist/agent/personaMatcher.js.map +1 -0
  61. package/dist/agent/toolExecutor.d.ts +2 -2
  62. package/dist/agent/toolExecutor.d.ts.map +1 -1
  63. package/dist/agent/toolExecutor.js +2 -1
  64. package/dist/agent/toolExecutor.js.map +1 -1
  65. package/dist/agent/tracer.d.ts +31 -0
  66. package/dist/agent/tracer.d.ts.map +1 -1
  67. package/dist/agent/tracer.js +31 -0
  68. package/dist/agent/tracer.js.map +1 -1
  69. package/dist/agent/types.d.ts +132 -0
  70. package/dist/agent/types.d.ts.map +1 -1
  71. package/dist/agent/types.js +38 -0
  72. package/dist/agent/types.js.map +1 -1
  73. package/dist/config/loader.d.ts +52 -40
  74. package/dist/config/loader.d.ts.map +1 -1
  75. package/dist/config/loader.js +52 -32
  76. package/dist/config/loader.js.map +1 -1
  77. package/dist/eval/evalRunner.d.ts +94 -0
  78. package/dist/eval/evalRunner.d.ts.map +1 -0
  79. package/dist/eval/evalRunner.js +141 -0
  80. package/dist/eval/evalRunner.js.map +1 -0
  81. package/dist/eval/evalTypes.d.ts +33 -3
  82. package/dist/eval/evalTypes.d.ts.map +1 -1
  83. package/dist/eval/evalTypes.js +18 -2
  84. package/dist/eval/evalTypes.js.map +1 -1
  85. package/dist/eval/scenarios.d.ts +33 -0
  86. package/dist/eval/scenarios.d.ts.map +1 -0
  87. package/dist/eval/scenarios.js +109 -0
  88. package/dist/eval/scenarios.js.map +1 -0
  89. package/dist/index.d.ts +51 -9
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +46 -6
  92. package/dist/index.js.map +1 -1
  93. package/dist/llm/abortSignal.d.ts +27 -0
  94. package/dist/llm/abortSignal.d.ts.map +1 -0
  95. package/dist/llm/abortSignal.js +44 -0
  96. package/dist/llm/abortSignal.js.map +1 -0
  97. package/dist/llm/embedding.d.ts +7 -2
  98. package/dist/llm/embedding.d.ts.map +1 -1
  99. package/dist/llm/embedding.js +66 -46
  100. package/dist/llm/embedding.js.map +1 -1
  101. package/dist/llm/factory.js +1 -1
  102. package/dist/llm/factory.js.map +1 -1
  103. package/dist/llm/openaiCompatible.d.ts.map +1 -1
  104. package/dist/llm/openaiCompatible.js +26 -27
  105. package/dist/llm/openaiCompatible.js.map +1 -1
  106. package/dist/llm/provider.d.ts +0 -14
  107. package/dist/llm/provider.d.ts.map +1 -1
  108. package/dist/llm/provider.js.map +1 -1
  109. package/dist/logging/logger.d.ts +1 -0
  110. package/dist/logging/logger.d.ts.map +1 -1
  111. package/dist/logging/logger.js +43 -37
  112. package/dist/logging/logger.js.map +1 -1
  113. package/dist/memory/inMemoryStorage.d.ts.map +1 -1
  114. package/dist/memory/inMemoryStorage.js +8 -7
  115. package/dist/memory/inMemoryStorage.js.map +1 -1
  116. package/dist/memory/lockManager.d.ts +48 -0
  117. package/dist/memory/lockManager.d.ts.map +1 -0
  118. package/dist/memory/lockManager.js +159 -0
  119. package/dist/memory/lockManager.js.map +1 -0
  120. package/dist/memory/projectManager.d.ts +23 -53
  121. package/dist/memory/projectManager.d.ts.map +1 -1
  122. package/dist/memory/projectManager.js +46 -227
  123. package/dist/memory/projectManager.js.map +1 -1
  124. package/dist/memory/projectRegistry.d.ts +62 -0
  125. package/dist/memory/projectRegistry.d.ts.map +1 -0
  126. package/dist/memory/projectRegistry.js +158 -0
  127. package/dist/memory/projectRegistry.js.map +1 -0
  128. package/dist/memory/recall.d.ts +3 -3
  129. package/dist/memory/recall.d.ts.map +1 -1
  130. package/dist/memory/recall.js +3 -3
  131. package/dist/memory/recall.js.map +1 -1
  132. package/dist/memory/sourceValidation.d.ts +49 -0
  133. package/dist/memory/sourceValidation.d.ts.map +1 -0
  134. package/dist/memory/sourceValidation.js +141 -0
  135. package/dist/memory/sourceValidation.js.map +1 -0
  136. package/dist/memory/store.d.ts.map +1 -1
  137. package/dist/memory/store.js +2 -1
  138. package/dist/memory/store.js.map +1 -1
  139. package/dist/memory/types.d.ts +0 -47
  140. package/dist/memory/types.d.ts.map +1 -1
  141. package/dist/memory/types.js +0 -138
  142. package/dist/memory/types.js.map +1 -1
  143. package/dist/memory/vectorStore.d.ts +82 -7
  144. package/dist/memory/vectorStore.d.ts.map +1 -1
  145. package/dist/memory/vectorStore.js +12 -8
  146. package/dist/memory/vectorStore.js.map +1 -1
  147. package/dist/persona/personaManager.d.ts +30 -4
  148. package/dist/persona/personaManager.d.ts.map +1 -1
  149. package/dist/persona/personaManager.js +47 -14
  150. package/dist/persona/personaManager.js.map +1 -1
  151. package/dist/security/pathGuard.d.ts.map +1 -1
  152. package/dist/security/pathGuard.js +3 -1
  153. package/dist/security/pathGuard.js.map +1 -1
  154. package/dist/skill/skillManager.d.ts.map +1 -1
  155. package/dist/skill/skillManager.js +2 -1
  156. package/dist/skill/skillManager.js.map +1 -1
  157. package/dist/utils/array.d.ts +24 -0
  158. package/dist/utils/array.d.ts.map +1 -0
  159. package/dist/utils/array.js +24 -0
  160. package/dist/utils/array.js.map +1 -0
  161. package/dist/utils/errors.d.ts +10 -8
  162. package/dist/utils/errors.d.ts.map +1 -1
  163. package/dist/utils/errors.js +12 -9
  164. package/dist/utils/errors.js.map +1 -1
  165. package/dist/utils/eventEmitter.d.ts +7 -0
  166. package/dist/utils/eventEmitter.d.ts.map +1 -1
  167. package/dist/utils/eventEmitter.js.map +1 -1
  168. package/dist/utils/objects.d.ts +17 -0
  169. package/dist/utils/objects.d.ts.map +1 -0
  170. package/dist/utils/objects.js +19 -0
  171. package/dist/utils/objects.js.map +1 -0
  172. package/dist/utils/segmenter.d.ts +17 -0
  173. package/dist/utils/segmenter.d.ts.map +1 -1
  174. package/dist/utils/segmenter.js +26 -0
  175. package/dist/utils/segmenter.js.map +1 -1
  176. package/dist/utils/strings.d.ts +22 -1
  177. package/dist/utils/strings.d.ts.map +1 -1
  178. package/dist/utils/strings.js +29 -1
  179. package/dist/utils/strings.js.map +1 -1
  180. package/dist/utils/time.d.ts +18 -3
  181. package/dist/utils/time.d.ts.map +1 -1
  182. package/dist/utils/time.js +24 -8
  183. package/dist/utils/time.js.map +1 -1
  184. package/dist/utils/toError.d.ts +8 -4
  185. package/dist/utils/toError.d.ts.map +1 -1
  186. package/dist/utils/toError.js +25 -5
  187. package/dist/utils/toError.js.map +1 -1
  188. package/package.json +11 -1
@@ -22,27 +22,30 @@
22
22
  * - 记忆查看 → MemoryInspector
23
23
  * - 薄包装方法移除,调用方改为 agent.<manager>.xxx()
24
24
  */
25
- import { basename } from 'node:path';
25
+ import { getBaseName } from '../utils/path.js';
26
26
  import { AGENT_CONSTANTS } from '../agent/constants.js';
27
27
  import { ProjectManager } from '../memory/projectManager.js';
28
28
  import { SecurityGuard } from '../security/pathGuard.js';
29
29
  import { recall } from '../memory/recall.js';
30
- import { extractUserFacts } from '../agent/userFactExtractor.js';
31
30
  import { assembleComponents } from '../agent/assembler.js';
32
- import { configError } from '../utils/errors.js';
33
- import { safeSetTimeout, clearSafeTimeout, clearSafeInterval } from '../utils/safeTimer.js';
31
+ import { matchPersonaByLlm } from '../agent/personaMatcher.js';
32
+ import { chatBusyError, configError } from '../utils/errors.js';
33
+ import { clearSafeInterval } from '../utils/safeTimer.js';
34
34
  import { SessionManager } from '../agent/managers/sessionManager.js';
35
+ import { ChatLockManager } from '../agent/managers/chatLockManager.js';
35
36
  import { MemoryDecayScheduler } from '../agent/managers/memoryDecayScheduler.js';
36
37
  import { ArchiveCoordinator } from '../agent/managers/archiveCoordinator.js';
37
38
  import { TypedEventEmitter } from '../utils/eventEmitter.js';
38
39
  import { logger } from '../logging/logger.js';
40
+ import { TRACE_SPANS, NOOP_TRACER } from '../agent/tracer.js';
39
41
  // ─── 模块级常量 ─────────────────────────────────────────
40
42
  /** Agent 事件名白名单,用于运行时校验 SessionManager 转发的事件类型 */
41
- // 必须与 utils/eventEmitter.ts 的 AgentEventMap 键集保持一致(9 个事件)
43
+ // 必须与 utils/eventEmitter.ts 的 AgentEventMap 键集保持一致(10 个事件)
42
44
  const AGENT_EVENT_NAMES = new Set([
43
45
  'memoryAdded', 'personaSwitched', 'decayCompleted',
44
46
  'memoryRecalled', 'sessionForked', 'insightExtracted',
45
47
  'conflictDetected', 'projectSwitched', 'skillMatched',
48
+ 'archiveFailed',
46
49
  ]);
47
50
  // ─── Agent 门面类 ───────────────────────────────────────
48
51
  /**
@@ -100,6 +103,8 @@ export class Agent extends TypedEventEmitter {
100
103
  autoConfigRefiner = null;
101
104
  /** SessionArchiver(会话内容归档器,content 类记忆) */
102
105
  sessionArchiver = null;
106
+ /** TextPolishManager(文本润色管理器,LLM 语法修正 + 表达优化) */
107
+ textPolisher = null;
103
108
  /** 会话管理器(从 Agent 拆分出的会话管理职责) */
104
109
  _sessionManager = null;
105
110
  /** 当前激活的技能名(上一轮匹配,本轮注入) */
@@ -107,29 +112,13 @@ export class Agent extends TypedEventEmitter {
107
112
  _initialized = false;
108
113
  // 项目上下文(AgentContext 与 ProjectContext 等价,直接使用后者避免重复字段)
109
114
  pctx = null;
110
- /** chat() 并发锁 */
111
- _chatBusy = false;
112
115
  /**
113
- * chat() 锁持有者 token(race condition 防护)
116
+ * chat() 并发锁管理器(init 时创建,close 时销毁)
114
117
  *
115
- * 设计目的:
116
- * 原锁是简单布尔值 `_chatBusy`,无 owner 校验。超时回调与 finally 块无差别清理,
117
- * 导致 race condition:T=0 A 获取锁 → T=180s 超时释放 → T=181s B 获取锁
118
- * → T=182s A 的 finally 误清 B 的锁/计时器/controller。
119
- *
120
- * 防护方案:
121
- * - 获取锁时 token 递增:`const myToken = ++this._chatLockToken`
122
- * - 超时回调校验 `this._chatLockToken === myToken` 后才释放
123
- * - finally 块校验 `this._chatLockToken === myToken` 后才清理
124
- * - 不匹配时跳过清理,避免误清新调用者的资源
125
- *
126
- * 选择 number 而非 Symbol:递增计数器可序列化、零依赖、足够唯一(同一 Agent 实例内)
118
+ * 职责:chat() 并发锁 + token 校验(race condition 防护)+ 超时保护 + 外部 signal 合并。
119
+ * 详见 ChatLockManager 类注释。
127
120
  */
128
- _chatLockToken = 0;
129
- /** 聊天锁超时计时器(防止 LLM 卡死时锁永久持有) */
130
- chatLockTimer = null;
131
- /** chat() 内部 AbortController(超时时中断 generator,防止并发) */
132
- chatAbortController = null;
121
+ chatLockManager = null;
133
122
  /** 记忆衰减定时器(已迁移至 MemoryDecayScheduler,此字段保留用于 close 时引用判断) */
134
123
  decayTimer = null;
135
124
  /** 最近一次 chat() 调用的时间戳 */
@@ -175,7 +164,7 @@ export class Agent extends TypedEventEmitter {
175
164
  */
176
165
  async init(projectPathOverride) {
177
166
  if (this._initialized) {
178
- // 修复 #3:close() 失败不应阻塞 init() 重建
167
+ // close() 失败不应阻塞 init() 重建
179
168
  // 原 close() 异常(如 awaitPendingArchives 超时)会传播到 init() 调用方,
180
169
  // 导致 Agent 处于不可用状态。此处捕获后继续重建。
181
170
  try {
@@ -205,6 +194,8 @@ export class Agent extends TypedEventEmitter {
205
194
  '确认 API Key 已配置(环境变量或配置文件)',
206
195
  ]);
207
196
  }
197
+ // chat() 并发锁管理器(生命周期与 Agent 实例一致)
198
+ this.chatLockManager = new ChatLockManager();
208
199
  this._initialized = true;
209
200
  // 归档操作委托给 ArchiveCoordinator
210
201
  // 使用 getter 回调注入依赖,close 时 null 化字段后 getter 自然返回 null
@@ -215,9 +206,22 @@ export class Agent extends TypedEventEmitter {
215
206
  emit: (event, payload) => this.emit(event, payload),
216
207
  });
217
208
  // 记忆衰减职责委托给 MemoryDecayScheduler
209
+ // L2 时效性评估:注入 backgroundProvider + index(可选,未注入时 evaluateTimeliness 静默跳过)
218
210
  this.memoryDecayScheduler = new MemoryDecayScheduler({
219
211
  tracer: this.#config.tracer,
220
- onDecayCompleted: (payload) => this.emit('decayCompleted', payload),
212
+ onDecayCompleted: (payload) => {
213
+ this.emit('decayCompleted', payload);
214
+ // G2 自动触发 L2 时效性评估:衰减完成后 fire-and-forget 调用 evaluateTimeliness。
215
+ // 不 await:衰减循环不应被 LLM 调用阻塞(LLM 可能 5-15 秒)。
216
+ // 失败静默降级:evaluateTimeliness 内部已捕获 LLM 异常并返回降级报告。
217
+ // 触发频次:每小时 1 次(DECAY_INTERVAL_MS=3_600_000),无频次压力。
218
+ void this.evaluateTimeliness().catch((err) => {
219
+ // 仅记录日志,不抛错(与 onDecayCompleted 回调语义一致:不阻塞衰减循环)
220
+ logger.warn({ err }, 'L2 时效性评估自动触发失败(已降级,不影响衰减循环)');
221
+ });
222
+ },
223
+ backgroundProvider: this.#backgroundProvider,
224
+ index: pctx.index,
221
225
  });
222
226
  this.memoryDecayScheduler.start(pctx.index, AGENT_CONSTANTS.DECAY_INTERVAL_MS);
223
227
  // 保留 decayTimer 引用用于 close 时序兼容(实际定时器由 MemoryDecayScheduler 管理)
@@ -232,46 +236,23 @@ export class Agent extends TypedEventEmitter {
232
236
  if (input.length > AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH) {
233
237
  throw configError('输入过长', `输入超过最大长度限制(${AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH / 1024}KB)`, ['缩短输入内容', '分多次对话发送']);
234
238
  }
235
- if (this._chatBusy) {
236
- throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再发起新对话', [
237
- '等待上一轮 chat() 的 AsyncGenerator 耗尽(收到 done 事件)',
238
- '宿主程序应确保同一时间只有一个 chat() 调用',
239
- ]);
239
+ if (this.chatLockManager?.isBusy) {
240
+ throw chatBusyError('发起新对话');
240
241
  }
241
- this._chatBusy = true;
242
- // 分配本调用的 token,超时回调和 finally 块据此判断是否仍是当前持有者
243
- // 避免 race condition:超时释放后新调用获取锁,旧 finally 误清新调用者的资源
244
- const myToken = ++this._chatLockToken;
245
- // 内部 AbortController,超时时中断 generator 而非仅释放锁
246
- const internalAbort = new AbortController();
247
- this.chatAbortController = internalAbort;
242
+ // 获取锁 + 分配 token + 创建内部 AbortController + 启动超时定时器
243
+ // token 用于 finally 校验,避免 race condition:超时释放后新调用获取锁,旧 finally 误清新调用者的资源
244
+ // assertInitialized 已保证 chatLockManager/loop/history 非 null,此处用 ! 窄化类型
245
+ const chatLock = this.chatLockManager;
246
+ const { token: myToken, internalAbort } = chatLock.acquire(AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS);
248
247
  // 合并外部 signal:外部 abort 时也触发内部
249
- const onExternalAbort = () => internalAbort.abort();
250
- signal?.addEventListener('abort', onExternalAbort, { once: true });
251
- // 修复 #4:addEventListener 对已 aborted 的 signal 不触发回调
252
- // 需手动检查并触发 internalAbort,否则外部已取消的请求仍会进入主流程
253
- if (signal?.aborted) {
254
- internalAbort.abort();
255
- }
248
+ const cleanupExternalSignal = chatLock.attachExternalSignal(signal, internalAbort);
256
249
  const combinedSignal = internalAbort.signal;
257
- // 超时保护:LLM 卡死时中断 generator + 释放锁,防止并发
258
- // 超时回调校验 token 后才清理,避免误清新调用者的资源
259
- this.chatLockTimer = safeSetTimeout(() => {
260
- // 令牌不匹配:锁已被新调用者获取(或本调用已正常退出),跳过清理
261
- if (this._chatLockToken !== myToken) {
262
- return;
263
- }
264
- logger.warn({ timeoutMs: AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS }, 'chat() 锁超时,中断 generator 并释放锁');
265
- internalAbort.abort();
266
- this._chatBusy = false;
267
- this.chatLockTimer = null;
268
- this.chatAbortController = null;
269
- }, AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS);
270
250
  try {
271
251
  this._lastInteractionAt = new Date();
272
252
  // 清理上一轮注入的临时 system 消息(recentConversation/skill/recall/truncation)
273
253
  // 防止多轮累积:每轮 chat() 开始前,只保留 messages[0] 和非 system 消息
274
- this.requireNonNull(this.loop, 'loop').cleanTemporarySystemMessages();
254
+ const loop = this.loop;
255
+ loop.cleanTemporarySystemMessages();
275
256
  // 基元驱动召回(双通道:语义 + 关键词)
276
257
  yield { type: 'thinking', phase: 'recalling' };
277
258
  const recalledMemories = await this.recallAndInject(input);
@@ -285,13 +266,14 @@ export class Agent extends TypedEventEmitter {
285
266
  // 注入上一轮匹配的技能 prompt
286
267
  yield { type: 'thinking', phase: 'processing' };
287
268
  this.injectActiveSkill();
288
- await this.requireNonNull(this.history, 'history').appendUser(input);
269
+ const history = this.history;
270
+ await history.appendUser(input);
289
271
  let assistantContent = '';
290
272
  let wasAborted = false;
291
273
  // 把 provider 异常转为 yield error chunk,让宿主能优雅展示并清理 UI
292
274
  // (裸 throw 会导致未处理 rejection,UI 收不到错误展示机会)
293
275
  try {
294
- for await (const chunk of this.requireNonNull(this.loop, 'loop').processUserInput(input, recalledMemories, combinedSignal)) {
276
+ for await (const chunk of loop.processUserInput(input, recalledMemories, combinedSignal)) {
295
277
  yield chunk;
296
278
  if (chunk.type === 'text') {
297
279
  assistantContent += chunk.content;
@@ -323,7 +305,7 @@ export class Agent extends TypedEventEmitter {
323
305
  if (assistantContent.trim()) {
324
306
  const interruptedMark = this.#config.messages?.interrupted ?? '\n\n[已中断]';
325
307
  try {
326
- await this.requireNonNull(this.history, 'history').appendAssistant(assistantContent + interruptedMark);
308
+ await history.appendAssistant(assistantContent + interruptedMark);
327
309
  }
328
310
  catch (err) {
329
311
  logger.warn({ err }, '中断消息历史写入失败');
@@ -333,7 +315,7 @@ export class Agent extends TypedEventEmitter {
333
315
  }
334
316
  // 追加助手消息到历史(best-effort:失败不影响用户已收到的回答)
335
317
  try {
336
- await this.requireNonNull(this.history, 'history').appendAssistant(assistantContent);
318
+ await history.appendAssistant(assistantContent);
337
319
  }
338
320
  catch (err) {
339
321
  logger.warn({ err }, '助手消息历史写入失败');
@@ -343,17 +325,10 @@ export class Agent extends TypedEventEmitter {
343
325
  await this.postProcess(input, assistantContent);
344
326
  }
345
327
  finally {
346
- // 仅当本调用仍是当前锁持有者时才清理资源
328
+ // 仅当本调用仍是当前锁持有者时才清理资源(token 校验)
347
329
  // 若 token 已变(超时释放后被新调用者获取),跳过清理避免误清新调用者的状态
348
- if (this._chatLockToken === myToken) {
349
- this._chatBusy = false;
350
- this.chatAbortController = null;
351
- if (this.chatLockTimer) {
352
- clearSafeTimeout(this.chatLockTimer);
353
- this.chatLockTimer = null;
354
- }
355
- }
356
- signal?.removeEventListener('abort', onExternalAbort);
330
+ this.chatLockManager?.release(myToken);
331
+ cleanupExternalSignal();
357
332
  }
358
333
  }
359
334
  /**
@@ -367,7 +342,7 @@ export class Agent extends TypedEventEmitter {
367
342
  *
368
343
  * 本方法让宿主在确认 generator 挂起后强制释放锁,让用户能立即发起新对话。
369
344
  *
370
- * 安全机制:
345
+ * 安全机制(委托至 ChatLockManager.forceRelease):
371
346
  * - 递增 _chatLockToken,让原 chat() 的 finally 块检测到 token 不匹配后
372
347
  * 跳过资源清理(避免误清新调用者的 _chatBusy/chatAbortController)
373
348
  * - abort chatAbortController,让响应 signal 的 await 点(如 fetch)退出
@@ -382,22 +357,7 @@ export class Agent extends TypedEventEmitter {
382
357
  * - 调用后不应再消费原 chat() generator 的后续 chunk(token 已变,chunk 无意义)
383
358
  */
384
359
  forceReleaseChatLock() {
385
- if (!this._chatBusy)
386
- return;
387
- // 递增 token 让原 chat() 的 finally 块跳过清理(避免误清新调用者的资源)
388
- this._chatLockToken++;
389
- // abort 当前 generator(响应 signal 的 await 点会 throw AbortError 退出)
390
- if (this.chatAbortController) {
391
- this.chatAbortController.abort();
392
- this.chatAbortController = null;
393
- }
394
- // 清理锁超时定时器(避免后续触发重复清理)
395
- if (this.chatLockTimer) {
396
- clearSafeTimeout(this.chatLockTimer);
397
- this.chatLockTimer = null;
398
- }
399
- this._chatBusy = false;
400
- logger.warn('对话锁被强制释放(宿主无进展超时兜底)');
360
+ this.chatLockManager?.forceRelease();
401
361
  }
402
362
  /**
403
363
  * 召回记忆 + 注入最近对话上下文
@@ -410,16 +370,33 @@ export class Agent extends TypedEventEmitter {
410
370
  * @returns 召回的记忆列表
411
371
  */
412
372
  async recallAndInject(input) {
413
- const recalledMemories = await recall(this.requireNonNull(this.pctx, 'projectContext').index, input, {
414
- limit: 5,
415
- vectorStore: this.#config.vectorStore,
416
- excludeSources: this.#config.recallExcludeSources,
373
+ // 实际 recall() 函数耗时 span(区别于 loop.ts 的 RECALL 注入 span)
374
+ const tracer = this.#config.tracer ?? NOOP_TRACER;
375
+ const recallSpan = tracer.startSpan(TRACE_SPANS.RECALL_ACTUAL, {
376
+ queryLength: input.length,
377
+ hasVectorStore: !!this.#config.vectorStore,
417
378
  });
379
+ let recalledMemories = [];
380
+ try {
381
+ recalledMemories = await recall(this.pctx.index, input, {
382
+ limit: 5,
383
+ vectorStore: this.#config.vectorStore,
384
+ excludeSources: this.#config.recallExcludeSources,
385
+ });
386
+ }
387
+ catch (err) {
388
+ recallSpan.recordException(err instanceof Error ? err : new Error(String(err)));
389
+ throw err;
390
+ }
391
+ finally {
392
+ recallSpan.setAttribute('resultCount', recalledMemories.length);
393
+ recallSpan.end();
394
+ }
418
395
  if (recalledMemories.length > 0) {
419
396
  this.emit('memoryRecalled', { count: recalledMemories.length, query: input });
420
397
  }
421
398
  // Layer 5: 最近对话注入
422
- const loop = this.requireNonNull(this.loop, 'loop');
399
+ const loop = this.loop;
423
400
  const recentHistory = loop.getRecentHistory(3);
424
401
  if (recentHistory.length > 0) {
425
402
  const msgs = this.#config.messages;
@@ -462,10 +439,38 @@ export class Agent extends TypedEventEmitter {
462
439
  * (会话归档实现后,`insights-only` 将跳过对话原始内容自动归档)
463
440
  */
464
441
  async postProcess(input, assistantContent) {
442
+ // postProcess 全流程 span(角色匹配 + 技能匹配 + 归档 + AutoConfigRefiner)
443
+ const tracer = this.#config.tracer ?? NOOP_TRACER;
444
+ const span = tracer.startSpan(TRACE_SPANS.POST_PROCESS, {
445
+ archiveMode: this.#config.archiveMode,
446
+ });
447
+ try {
448
+ await this.postProcessInner(input, assistantContent);
449
+ }
450
+ finally {
451
+ span.end();
452
+ }
453
+ }
454
+ /**
455
+ * postProcess 内部实现(为 span 埋点提供 try/finally 包裹边界)
456
+ *
457
+ * 原 postProcess 逻辑完整保留于此,由外层 postProcess 负责 span 生命周期管理。
458
+ */
459
+ async postProcessInner(input, assistantContent) {
465
460
  // 角色自动匹配(best-effort:失败不阻塞对话结束,非归档行为不受 archiveMode 影响)
461
+ // 两层匹配策略:关键词高置信度 → LLM 辅助(低置信度且 backgroundProvider 已注入时)
462
+ // LLM 辅助匹配在 agent 层执行,遵循 backend_layers_rules §分层职责(persona/ 不直接调 LLM)
466
463
  if (this.personaManager) {
467
464
  try {
468
- const matchedPersona = this.personaManager.autoMatch(input);
465
+ let matchedPersona = null;
466
+ if (this.personaManager.canAutoMatch()) {
467
+ matchedPersona = await this.personaManager.autoMatch(input);
468
+ // 关键词低置信度且 backgroundProvider 已注入 → LLM 辅助语义匹配
469
+ const bgProvider = this.#backgroundProvider;
470
+ if (!matchedPersona && bgProvider) {
471
+ matchedPersona = await matchPersonaByLlm(bgProvider, this.personaManager.list, this.personaManager.activeName, input);
472
+ }
473
+ }
469
474
  if (matchedPersona) {
470
475
  const prevName = this.personaManager.activeName;
471
476
  this.personaManager.switchPersona(matchedPersona);
@@ -506,46 +511,36 @@ export class Agent extends TypedEventEmitter {
506
511
  if (skipAutoArchive) {
507
512
  logger.debug({ mode: 'manual' }, '归档模式为 manual,跳过自动归档');
508
513
  }
509
- // 用户画像实时归档(语义解析在 agent/ 层,存储在 memory/ 层)
514
+ // 用户画像实时归档(委托 ArchiveCoordinator,与手动归档路径统一,消除 DRY 违反)
510
515
  // ADR-015: full / insights-only 模式下 profile facts 自动归档
511
- if (!skipAutoArchive && this.#userProfile) {
516
+ const history = this.history;
517
+ if (!skipAutoArchive) {
512
518
  try {
513
- const turnIndex = `turn-${Date.now()}`;
514
- const facts = extractUserFacts(input, turnIndex);
515
- // 注册到 pendingArchives,确保 close() 时等待后台归档完成,避免写入已关闭的存储
516
- const archiveFactsPromise = this.#userProfile.archiveFacts(facts).then((entries) => {
517
- // 发射 memoryAdded 事件:仅对已确认且写入存储的条目(confirmed=true)
518
- for (const entry of entries) {
519
- if (entry.confirmed) {
520
- this.emit('memoryAdded', { id: entry.id, source: 'profile', name: entry.value });
521
- }
522
- }
523
- }).catch((err) => {
524
- logger.warn({ err }, '用户画像实时归档失败');
519
+ // fire-and-forget 包装:registerPendingArchive 确保 close() 时等待后台归档完成
520
+ // 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
521
+ const archiveFactsPromise = this.archiveCoordinator.archiveProfileFacts(input).then(() => { }, (err) => {
522
+ const message = err instanceof Error ? err.message : String(err);
523
+ logger.warn({ err, stage: 'profile' }, '归档失败');
524
+ this.emit('archiveFailed', { stage: 'profile', message: message.slice(0, 200) });
525
525
  });
526
- this.requireNonNull(this.history, 'history').registerPendingArchive(archiveFactsPromise);
526
+ history.registerPendingArchive(archiveFactsPromise);
527
527
  }
528
528
  catch (err) {
529
529
  logger.warn({ err }, '用户画像归档初始化失败');
530
530
  }
531
531
  }
532
- // 输入分类 → Insight 提取(委托给 InsightExtractor)
532
+ // 输入分类 → Insight 提取(委托 ArchiveCoordinator,与手动归档路径统一)
533
533
  // ADR-015: full / insights-only 模式下 insight 自动归档
534
- if (!skipAutoArchive && this.insightExtractor) {
534
+ if (!skipAutoArchive) {
535
535
  try {
536
- const shouldExtract = this.insightExtractor.classify(input);
537
- if (shouldExtract === 'extract') {
538
- const p = this.insightExtractor.extract(input, assistantContent).then((memories) => {
539
- // 发射 memoryAdded + insightExtracted 事件:每条写入/更新的 insight 均通知宿主
540
- for (const memory of memories) {
541
- this.emit('memoryAdded', { id: memory.id, source: memory.source, name: memory.name });
542
- this.emit('insightExtracted', { source: memory.source, insight: memory.content });
543
- }
544
- }).catch((err) => {
545
- logger.warn({ err }, 'Insight 提取失败');
546
- });
547
- this.requireNonNull(this.history, 'history').registerPendingArchive(p);
548
- }
536
+ // fire-and-forget 包装:classify 判断由 ArchiveCoordinator 内部完成
537
+ // 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
538
+ const archiveInsightPromise = this.archiveCoordinator.archiveInsight(input, assistantContent).then(() => { }, (err) => {
539
+ const message = err instanceof Error ? err.message : String(err);
540
+ logger.warn({ err, stage: 'insight' }, '归档失败');
541
+ this.emit('archiveFailed', { stage: 'insight', message: message.slice(0, 200) });
542
+ });
543
+ history.registerPendingArchive(archiveInsightPromise);
549
544
  }
550
545
  catch (err) {
551
546
  logger.warn({ err }, 'Insight 提取初始化失败');
@@ -558,7 +553,7 @@ export class Agent extends TypedEventEmitter {
558
553
  const analyzePromise = this.autoConfigRefiner.analyze(input, assistantContent).catch((err) => {
559
554
  logger.warn({ err }, 'AutoConfigRefiner 分析失败');
560
555
  });
561
- this.requireNonNull(this.history, 'history').registerPendingArchive(analyzePromise);
556
+ history.registerPendingArchive(analyzePromise);
562
557
  }
563
558
  catch (err) {
564
559
  logger.warn({ err }, 'AutoConfigRefiner 初始化失败');
@@ -585,7 +580,7 @@ export class Agent extends TypedEventEmitter {
585
580
  */
586
581
  forkSession(targetSession) {
587
582
  this.assertInitialized('forkSession');
588
- return this.requireNonNull(this._sessionManager, 'sessionManager').forkSession(targetSession);
583
+ return this._sessionManager.forkSession(targetSession);
589
584
  }
590
585
  /**
591
586
  * 切换到指定项目
@@ -596,12 +591,10 @@ export class Agent extends TypedEventEmitter {
596
591
  async switchProject(nameOrPath) {
597
592
  this.assertInitialized('switchProject', ['projectManager', 'provider']);
598
593
  // 对话进行中切换项目会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
599
- if (this._chatBusy) {
600
- throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换项目', [
601
- '等待上一轮 chat() 的 AsyncGenerator 耗尽',
602
- ]);
594
+ if (this.chatLockManager?.isBusy) {
595
+ throw chatBusyError('切换项目');
603
596
  }
604
- const pm = this.requireNonNull(this.projectManager, 'projectManager');
597
+ const pm = this.projectManager;
605
598
  const projects = pm.list;
606
599
  let target = projects.find((p) => p.name === nameOrPath || p.path === nameOrPath);
607
600
  if (!target) {
@@ -609,7 +602,7 @@ export class Agent extends TypedEventEmitter {
609
602
  target = projects.find((p) => p.name.toLowerCase() === nameOrPathLower || p.path.toLowerCase() === nameOrPathLower);
610
603
  }
611
604
  const projectPath = target ? target.path : nameOrPath;
612
- const projectName = target ? target.name : basename(nameOrPath);
605
+ const projectName = target ? target.name : getBaseName(nameOrPath);
613
606
  const newPctx = await pm.initProject(projectPath, projectName, this.#config.configDir);
614
607
  // 记录源项目路径(用于事件),切换前 pctx 可能不存在(首次初始化)
615
608
  const fromProjectPath = this.pctx?.projectPath ?? null;
@@ -654,6 +647,7 @@ export class Agent extends TypedEventEmitter {
654
647
  this.autoConfigRefiner = result.autoConfigRefiner;
655
648
  this.workProjection = result.workProjection;
656
649
  this.sessionArchiver = result.sessionArchiver;
650
+ this.textPolisher = result.textPolisher;
657
651
  // 绑定冲突检测回调,InsightExtractor 检测到 contradicts 时 emit('conflictDetected')
658
652
  // 与 bindGetRecentHistory 同模式:解决 Agent 晚于 InsightExtractor 创建的时序循环依赖
659
653
  this.insightExtractor.bindOnConflict((info) => {
@@ -693,16 +687,16 @@ export class Agent extends TypedEventEmitter {
693
687
  this.emit(event, data);
694
688
  }
695
689
  };
696
- return new SessionManager(() => this.requireNonNull(this.history, 'history'), () => this.requireNonNull(this.loop, 'loop'), this.#config.sessionStore, () => this._chatBusy, forwardEvent);
690
+ return new SessionManager(
691
+ // 惰性 getter:SessionManager 内部调用时 Agent 已 init,用 ! 窄化
692
+ () => this.history, () => this.loop, this.#config.sessionStore, () => this.chatLockManager?.isBusy ?? false, forwardEvent);
697
693
  }
698
694
  // ─── Provider 管理 ────────────────────────────────────
699
695
  setProvider(provider) {
700
696
  // 对话进行中切换 Provider 会导致同一 processUserInput 循环内前后两次 LLM 调用命中不同 Provider
701
697
  // (模型上下文窗口假设不一致 → 可能导致上下文截断逻辑误判或 tool_call 格式不兼容)
702
- if (this._chatBusy) {
703
- throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换 Provider', [
704
- '等待上一轮 chat() 的 AsyncGenerator 耗尽',
705
- ]);
698
+ if (this.chatLockManager?.isBusy) {
699
+ throw chatBusyError('切换 Provider');
706
700
  }
707
701
  this.#provider = provider;
708
702
  if (this.loop) {
@@ -712,10 +706,8 @@ export class Agent extends TypedEventEmitter {
712
706
  }
713
707
  setBackgroundProvider(provider) {
714
708
  // 与 setProvider 一致,对话进行中禁止切换后台 Provider
715
- if (this._chatBusy) {
716
- throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换后台 Provider', [
717
- '等待上一轮 chat() 的 AsyncGenerator 耗尽',
718
- ]);
709
+ if (this.chatLockManager?.isBusy) {
710
+ throw chatBusyError('切换后台 Provider');
719
711
  }
720
712
  this.#backgroundProvider = provider;
721
713
  // 同步更新 AutoConfigRefiner 的后台 Provider
@@ -733,10 +725,8 @@ export class Agent extends TypedEventEmitter {
733
725
  * @param mode 目标模式
734
726
  */
735
727
  setArchiveMode(mode) {
736
- if (this._chatBusy) {
737
- throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换归档模式', [
738
- '等待上一轮 chat() 的 AsyncGenerator 耗尽',
739
- ]);
728
+ if (this.chatLockManager?.isBusy) {
729
+ throw chatBusyError('切换归档模式');
740
730
  }
741
731
  const prev = this.#config.archiveMode;
742
732
  if (prev === mode)
@@ -818,10 +808,8 @@ export class Agent extends TypedEventEmitter {
818
808
  */
819
809
  async reloadConfig(source) {
820
810
  this.assertInitialized('reloadConfig');
821
- if (this._chatBusy) {
822
- throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再重载配置', [
823
- '等待上一轮 chat() 的 AsyncGenerator 耗尽',
824
- ]);
811
+ if (this.chatLockManager?.isBusy) {
812
+ throw chatBusyError('重载配置');
825
813
  }
826
814
  // guardrail 需重建 AgentLoop,不属于热重载范畴
827
815
  if (source === 'guardrail') {
@@ -859,10 +847,8 @@ export class Agent extends TypedEventEmitter {
859
847
  */
860
848
  async rebuildComponents() {
861
849
  // 对话进行中重建组件会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
862
- if (this._chatBusy) {
863
- throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再重建组件', [
864
- '等待上一轮 chat() 的 AsyncGenerator 耗尽',
865
- ]);
850
+ if (this.chatLockManager?.isBusy) {
851
+ throw chatBusyError('重建组件');
866
852
  }
867
853
  await this.rebuildComponentsWithCurrentCtx();
868
854
  }
@@ -893,19 +879,6 @@ export class Agent extends TypedEventEmitter {
893
879
  }
894
880
  }
895
881
  }
896
- /**
897
- * 非空断言守卫——对 null/undefined 值抛出清晰错误的运行时检查
898
- *
899
- * @param value 可能为 null/undefined 的值
900
- * @param name 组件名称(用于错误消息)
901
- * @throws MemoraError 如果 value 为 null/undefined
902
- */
903
- requireNonNull(value, name) {
904
- if (value === null || value === undefined) {
905
- throw configError('Agent 未初始化', `${name} 组件不可用`, ['请先调用 await agent.init()']);
906
- }
907
- return value;
908
- }
909
882
  // ─── 记忆生命周期 ───────────────────────────────────────
910
883
  // ─── runMemoryDecay 已迁移至 MemoryDecayScheduler.runOnce ─────
911
884
  // ─── 关闭 ─────────────────────────────────────────────
@@ -913,9 +886,10 @@ export class Agent extends TypedEventEmitter {
913
886
  * 关闭 Agent,释放 SQLite 连接等资源
914
887
  */
915
888
  async close() {
916
- // 递增 token,使任何进行中的 chat() generator 的 finally 块
917
- // 检测到 token 变化后跳过资源清理(close 已接管清理职责)
918
- this._chatLockToken++;
889
+ // 清理 chat 锁管理器(递增 token 使进行中的 chat() generator 的 finally 块
890
+ // 检测到 token 变化后跳过资源清理,close 已接管清理职责)
891
+ this.chatLockManager?.dispose();
892
+ this.chatLockManager = null;
919
893
  // 清理 MemoryDecayScheduler(含定时器和 storage 引用)
920
894
  if (this.memoryDecayScheduler) {
921
895
  this.memoryDecayScheduler.stop();
@@ -928,49 +902,55 @@ export class Agent extends TypedEventEmitter {
928
902
  clearSafeInterval(this.decayTimer);
929
903
  this.decayTimer = null;
930
904
  }
931
- if (this.chatLockTimer) {
932
- clearSafeTimeout(this.chatLockTimer);
933
- this.chatLockTimer = null;
934
- }
935
- if (this.chatAbortController) {
936
- this.chatAbortController.abort();
937
- this.chatAbortController = null;
938
- }
939
905
  // 清理 PersonaManager 的角色切换防抖锁计时器,防止关闭后回调触发
940
906
  if (this.personaManager) {
941
907
  this.personaManager.close();
942
908
  }
943
- this.removeAllListeners();
909
+ // 先等待后台归档完成,再移除事件监听器
910
+ // 顺序敏感:若先 removeAllListeners,fire-and-forget 的归档 Promise 在 await 期间
911
+ // reject 时 emit 会变成 no-op(listeners 已清空),archiveFailed 事件丢失
944
912
  if (this.history) {
945
913
  await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
946
914
  }
915
+ // pending archives 完成后再移除监听器,确保归档失败的 emit 能送达
916
+ this.removeAllListeners();
947
917
  if (this.projectManager) {
948
918
  await this.projectManager.shutdown();
949
919
  }
950
920
  this._initialized = false;
921
+ this.nullifyAllComponents();
922
+ // 清理次要状态字段,防止 re-init 后残留上一会话状态
923
+ this.activeSkill = null;
924
+ this._lastInteractionAt = null;
925
+ }
926
+ /**
927
+ * 统一 null 化所有组件字段(新增 Manager 时在此处追加一行)
928
+ *
929
+ * 仅处理"纯 null 化"字段,带副作用的清理(dispose/stop/close/shutdown)
930
+ * 仍由 close() 显式调用,顺序敏感不可合并。
931
+ */
932
+ nullifyAllComponents() {
933
+ // Provider
951
934
  this.#backgroundProvider = null;
952
- this._chatBusy = false;
935
+ // 核心组件
953
936
  this.history = null;
954
937
  this.loop = null;
955
- this._sessionManager = null;
956
- this.projectManager = null;
938
+ this.toolExec = null;
939
+ // 专职 Manager
940
+ this.personaManager = null;
941
+ this.#userProfile = null;
942
+ this.skillManager = null;
957
943
  this.insightExtractor = null;
958
944
  this.configManager = null;
959
945
  this.memoryInspector = null;
946
+ this.workProjection = null;
960
947
  this.autoConfigRefiner = null;
961
948
  this.sessionArchiver = null;
949
+ this.textPolisher = null;
950
+ this._sessionManager = null;
951
+ // 项目管理
952
+ this.projectManager = null;
962
953
  this.pctx = null;
963
- // 补全剩余 manager 字段 null 化,与上述字段处理方式一致
964
- // (原实现仅 null 化部分 manager,toolExec/personaManager/#userProfile/skillManager/workProjection 遗漏)
965
- this.toolExec = null;
966
- this.personaManager = null;
967
- this.#userProfile = null;
968
- this.skillManager = null;
969
- this.workProjection = null;
970
- // 清理次要状态字段,防止 re-init 后残留上一会话状态
971
- this.activeSkill = null;
972
- this._lastInteractionAt = null;
973
- // 衰减指标已迁移至 MemoryDecayScheduler,close 时通过 stop() 销毁实例
974
954
  }
975
955
  // ─── 只读访问器 ───────────────────────────────────────
976
956
  get initialized() {
@@ -1000,7 +980,7 @@ export class Agent extends TypedEventEmitter {
1000
980
  return this.#provider;
1001
981
  }
1002
982
  get isBusy() {
1003
- return this._chatBusy;
983
+ return this.chatLockManager?.isBusy ?? false;
1004
984
  }
1005
985
  get lastInteractionAt() {
1006
986
  return this._lastInteractionAt;
@@ -1051,7 +1031,7 @@ export class Agent extends TypedEventEmitter {
1051
1031
  // - 访问器(getter):返回 Manager | null,供宿主项目链式调用和优雅降级
1052
1032
  // (如 `agent.persona?.activeName`、`if (!agent.memory) return []`)
1053
1033
  // - 门面方法(snapshot/inspect/stats/searchMemories 等):通过
1054
- // assertInitialized + requireNonNull 抛 MemoraError,提供明确错误信息
1034
+ // assertInitialized 抛 MemoraError,提供明确错误信息
1055
1035
  // 宿主项目使用访问器时需自行判空,或使用门面方法获得自动错误处理。
1056
1036
  /**
1057
1037
  * 角色管理器(可能为 null)
@@ -1103,6 +1083,92 @@ export class Agent extends TypedEventEmitter {
1103
1083
  get memory() {
1104
1084
  return this.memoryInspector;
1105
1085
  }
1086
+ // ─── LLM 记忆治理委托方法(G1:暴露能力但不暴露 manager 实例) ──
1087
+ // 这三个方法是 sprite 层接入 L1~L3 治理能力的唯一入口。
1088
+ // 委托内部 manager(memoryInspector / memoryDecayScheduler / memoryAdvisor),
1089
+ // 不返回 manager 实例本身,保持封装(sprite 层不感知 manager 内部结构)。
1090
+ // manager 未初始化时返回 skippedReason 报告,与 manager 内部"backgroundProvider
1091
+ // 未注入时静默跳过"语义一致(向后兼容)。
1092
+ /**
1093
+ * L1 语义去重(委托 MemoryInspector.deduplicateMemories)
1094
+ *
1095
+ * 扫描名称相似的记忆对,调用 LLM 判断语义等价,降级低分记忆(score→0.1,不物理删除)。
1096
+ * backgroundProvider 未注入或 Agent 未初始化时返回 skippedReason 报告,不抛错。
1097
+ *
1098
+ * @param signal 可选的 AbortSignal(取消进行中的 LLM 判断)
1099
+ * @returns 去重报告(扫描数 / 降级 ID 列表 / 跳过原因)
1100
+ */
1101
+ async deduplicateMemories(signal) {
1102
+ const inspector = this.memoryInspector;
1103
+ if (!inspector) {
1104
+ return {
1105
+ scannedCount: 0,
1106
+ pairCount: 0,
1107
+ deduplicatedCount: 0,
1108
+ demotedIds: [],
1109
+ skippedReason: 'Agent 未初始化',
1110
+ };
1111
+ }
1112
+ return inspector.deduplicateMemories(signal);
1113
+ }
1114
+ /**
1115
+ * L2 时效性评估(委托 MemoryDecayScheduler.evaluateTimeliness)
1116
+ *
1117
+ * 扫描低分记忆(score<0.3),调用 LLM 判断是否过时,降级过时记忆(score→0.05)。
1118
+ * backgroundProvider 未注入或 Agent 未初始化时返回 skippedReason 报告,不抛错。
1119
+ *
1120
+ * 使用场景:
1121
+ * - G2 自动触发:onDecayCompleted 钩子内 fire-and-forget 调用
1122
+ * - 手动触发:宿主 UI 健康度面板"时效性评估"按钮
1123
+ *
1124
+ * @param signal 可选的 AbortSignal
1125
+ * @returns 评估报告(扫描数 / 过时数 / 降级 ID 列表 / 跳过原因)
1126
+ */
1127
+ async evaluateTimeliness(signal) {
1128
+ const scheduler = this.memoryDecayScheduler;
1129
+ if (!scheduler) {
1130
+ return {
1131
+ scannedCount: 0,
1132
+ outdatedCount: 0,
1133
+ demotedIds: [],
1134
+ skippedReason: 'Agent 未初始化',
1135
+ };
1136
+ }
1137
+ return scheduler.evaluateTimeliness(signal);
1138
+ }
1139
+ /**
1140
+ * 手动触发一次 L0 记忆衰减(委托 MemoryDecayScheduler.runOnce)
1141
+ *
1142
+ * 执行纯 score 递减(无 LLM 调用),衰减完成后触发 decayCompleted 事件。
1143
+ * 供宿主 UI 手动触发(如仪表盘"立即衰减"按钮),与定时器自动触发路径一致。
1144
+ */
1145
+ runMemoryDecayOnce() {
1146
+ this.memoryDecayScheduler?.runOnce();
1147
+ }
1148
+ /**
1149
+ * L3 冲突检测(委托 MemoryAdvisor.detectConflicts)
1150
+ *
1151
+ * 同 source 内配对,调用 LLM 判断语义冲突,仅检测不修复(需用户决策)。
1152
+ * backgroundProvider 未注入或 Agent 未初始化时返回 skippedReason 报告,不抛错。
1153
+ *
1154
+ * @param signal 可选的 AbortSignal
1155
+ * @returns 冲突报告(扫描数 / 冲突数 / 冲突详情列表 / 跳过原因)
1156
+ */
1157
+ async detectConflicts(signal) {
1158
+ // MemoryAdvisor 实例由 MemoryInspector 持有(组合根装配时注入),
1159
+ // 通过 inspector.detectConflicts 转发,避免 agent 直接持有 advisor 引用
1160
+ const inspector = this.memoryInspector;
1161
+ if (!inspector) {
1162
+ return {
1163
+ scannedCount: 0,
1164
+ pairCount: 0,
1165
+ conflictCount: 0,
1166
+ conflicts: [],
1167
+ skippedReason: 'Agent 未初始化',
1168
+ };
1169
+ }
1170
+ return inspector.detectConflicts(signal);
1171
+ }
1106
1172
  /**
1107
1173
  * 项目管理器(可能为 null)
1108
1174
  *
@@ -1151,5 +1217,9 @@ export class Agent extends TypedEventEmitter {
1151
1217
  get sessionManager() {
1152
1218
  return this._sessionManager;
1153
1219
  }
1220
+ /** 文本润色管理器(宿主可通过此 getter 调用 polish 方法) */
1221
+ get polish() {
1222
+ return this.textPolisher;
1223
+ }
1154
1224
  }
1155
1225
  //# sourceMappingURL=agent.js.map