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