@zooique/memora 2.0.0 → 2.0.3

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 (212) hide show
  1. package/README.en.md +272 -0
  2. package/README.md +150 -38
  3. package/dist/agent/agent.d.ts +233 -58
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +651 -359
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/agent/assembler.d.ts +29 -13
  8. package/dist/agent/assembler.d.ts.map +1 -1
  9. package/dist/agent/assembler.js +79 -44
  10. package/dist/agent/assembler.js.map +1 -1
  11. package/dist/agent/builtinToolHandlers.d.ts +26 -1
  12. package/dist/agent/builtinToolHandlers.d.ts.map +1 -1
  13. package/dist/agent/builtinToolHandlers.js +73 -1
  14. package/dist/agent/builtinToolHandlers.js.map +1 -1
  15. package/dist/agent/constants.d.ts +4 -0
  16. package/dist/agent/constants.d.ts.map +1 -1
  17. package/dist/agent/constants.js +4 -0
  18. package/dist/agent/constants.js.map +1 -1
  19. package/dist/agent/contextManager.d.ts +8 -0
  20. package/dist/agent/contextManager.d.ts.map +1 -1
  21. package/dist/agent/contextManager.js +7 -2
  22. package/dist/agent/contextManager.js.map +1 -1
  23. package/dist/agent/guardrail.d.ts +4 -2
  24. package/dist/agent/guardrail.d.ts.map +1 -1
  25. package/dist/agent/guardrail.js +13 -5
  26. package/dist/agent/guardrail.js.map +1 -1
  27. package/dist/agent/loop.d.ts +58 -14
  28. package/dist/agent/loop.d.ts.map +1 -1
  29. package/dist/agent/loop.js +184 -131
  30. package/dist/agent/loop.js.map +1 -1
  31. package/dist/agent/managers/archiveCoordinator.d.ts +126 -22
  32. package/dist/agent/managers/archiveCoordinator.d.ts.map +1 -1
  33. package/dist/agent/managers/archiveCoordinator.js +239 -44
  34. package/dist/agent/managers/archiveCoordinator.js.map +1 -1
  35. package/dist/agent/managers/autoConfigRefiner.d.ts.map +1 -1
  36. package/dist/agent/managers/autoConfigRefiner.js +4 -8
  37. package/dist/agent/managers/autoConfigRefiner.js.map +1 -1
  38. package/dist/agent/managers/configManager.d.ts +87 -3
  39. package/dist/agent/managers/configManager.d.ts.map +1 -1
  40. package/dist/agent/managers/configManager.js +139 -2
  41. package/dist/agent/managers/configManager.js.map +1 -1
  42. package/dist/agent/managers/dedupManager.d.ts +169 -0
  43. package/dist/agent/managers/dedupManager.d.ts.map +1 -0
  44. package/dist/agent/managers/dedupManager.js +336 -0
  45. package/dist/agent/managers/dedupManager.js.map +1 -0
  46. package/dist/agent/managers/insightExtractor.d.ts.map +1 -1
  47. package/dist/agent/managers/insightExtractor.js +11 -11
  48. package/dist/agent/managers/insightExtractor.js.map +1 -1
  49. package/dist/agent/managers/llmJudgeHelper.d.ts +38 -0
  50. package/dist/agent/managers/llmJudgeHelper.d.ts.map +1 -0
  51. package/dist/agent/managers/llmJudgeHelper.js +49 -0
  52. package/dist/agent/managers/llmJudgeHelper.js.map +1 -0
  53. package/dist/agent/managers/memoryAdvisor.d.ts +3 -3
  54. package/dist/agent/managers/memoryAdvisor.d.ts.map +1 -1
  55. package/dist/agent/managers/memoryAdvisor.js +53 -67
  56. package/dist/agent/managers/memoryAdvisor.js.map +1 -1
  57. package/dist/agent/managers/memoryDecayScheduler.d.ts +46 -5
  58. package/dist/agent/managers/memoryDecayScheduler.d.ts.map +1 -1
  59. package/dist/agent/managers/memoryDecayScheduler.js +127 -63
  60. package/dist/agent/managers/memoryDecayScheduler.js.map +1 -1
  61. package/dist/agent/managers/memoryGovernance.d.ts +56 -0
  62. package/dist/agent/managers/memoryGovernance.d.ts.map +1 -0
  63. package/dist/agent/managers/memoryGovernance.js +63 -0
  64. package/dist/agent/managers/memoryGovernance.js.map +1 -0
  65. package/dist/agent/managers/memoryInspector.d.ts +32 -157
  66. package/dist/agent/managers/memoryInspector.d.ts.map +1 -1
  67. package/dist/agent/managers/memoryInspector.js +70 -363
  68. package/dist/agent/managers/memoryInspector.js.map +1 -1
  69. package/dist/agent/managers/relationBuilder.js +2 -2
  70. package/dist/agent/managers/relationBuilder.js.map +1 -1
  71. package/dist/agent/managers/sessionArchiver.d.ts +6 -1
  72. package/dist/agent/managers/sessionArchiver.d.ts.map +1 -1
  73. package/dist/agent/managers/sessionArchiver.js +19 -22
  74. package/dist/agent/managers/sessionArchiver.js.map +1 -1
  75. package/dist/agent/managers/streamAccumulator.d.ts +45 -0
  76. package/dist/agent/managers/streamAccumulator.d.ts.map +1 -0
  77. package/dist/agent/managers/streamAccumulator.js +40 -0
  78. package/dist/agent/managers/streamAccumulator.js.map +1 -0
  79. package/dist/agent/managers/textPolishManager.d.ts.map +1 -1
  80. package/dist/agent/managers/textPolishManager.js +4 -7
  81. package/dist/agent/managers/textPolishManager.js.map +1 -1
  82. package/dist/agent/managers/workProjection.d.ts +16 -1
  83. package/dist/agent/managers/workProjection.d.ts.map +1 -1
  84. package/dist/agent/managers/workProjection.js +48 -24
  85. package/dist/agent/managers/workProjection.js.map +1 -1
  86. package/dist/agent/toolExecutor.d.ts +22 -2
  87. package/dist/agent/toolExecutor.d.ts.map +1 -1
  88. package/dist/agent/toolExecutor.js +28 -3
  89. package/dist/agent/toolExecutor.js.map +1 -1
  90. package/dist/agent/types.d.ts +10 -1
  91. package/dist/agent/types.d.ts.map +1 -1
  92. package/dist/agent/userFactExtractor.d.ts.map +1 -1
  93. package/dist/agent/userFactExtractor.js +7 -0
  94. package/dist/agent/userFactExtractor.js.map +1 -1
  95. package/dist/config/loader.d.ts +124 -236
  96. package/dist/config/loader.d.ts.map +1 -1
  97. package/dist/config/loader.js +273 -117
  98. package/dist/config/loader.js.map +1 -1
  99. package/dist/index.d.ts +7 -2
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +9 -0
  102. package/dist/index.js.map +1 -1
  103. package/dist/llm/embedding.d.ts +13 -1
  104. package/dist/llm/embedding.d.ts.map +1 -1
  105. package/dist/llm/embedding.js.map +1 -1
  106. package/dist/llm/factory.d.ts +19 -5
  107. package/dist/llm/factory.d.ts.map +1 -1
  108. package/dist/llm/factory.js +19 -37
  109. package/dist/llm/factory.js.map +1 -1
  110. package/dist/llm/openaiCompatible.d.ts.map +1 -1
  111. package/dist/llm/openaiCompatible.js +2 -9
  112. package/dist/llm/openaiCompatible.js.map +1 -1
  113. package/dist/logging/logger.d.ts +3 -1
  114. package/dist/logging/logger.d.ts.map +1 -1
  115. package/dist/logging/logger.js +47 -50
  116. package/dist/logging/logger.js.map +1 -1
  117. package/dist/memory/governance.d.ts +50 -0
  118. package/dist/memory/governance.d.ts.map +1 -0
  119. package/dist/memory/governance.js +70 -0
  120. package/dist/memory/governance.js.map +1 -0
  121. package/dist/memory/hybridMerge.d.ts +0 -4
  122. package/dist/memory/hybridMerge.d.ts.map +1 -1
  123. package/dist/memory/hybridMerge.js +19 -4
  124. package/dist/memory/hybridMerge.js.map +1 -1
  125. package/dist/memory/inMemoryRelationStore.d.ts +7 -0
  126. package/dist/memory/inMemoryRelationStore.d.ts.map +1 -1
  127. package/dist/memory/inMemoryRelationStore.js +15 -0
  128. package/dist/memory/inMemoryRelationStore.js.map +1 -1
  129. package/dist/memory/inMemoryStorage.d.ts +36 -1
  130. package/dist/memory/inMemoryStorage.d.ts.map +1 -1
  131. package/dist/memory/inMemoryStorage.js +66 -16
  132. package/dist/memory/inMemoryStorage.js.map +1 -1
  133. package/dist/memory/loader.d.ts +2 -0
  134. package/dist/memory/loader.d.ts.map +1 -1
  135. package/dist/memory/loader.js +8 -1
  136. package/dist/memory/loader.js.map +1 -1
  137. package/dist/memory/lockManager.d.ts +7 -2
  138. package/dist/memory/lockManager.d.ts.map +1 -1
  139. package/dist/memory/lockManager.js +31 -5
  140. package/dist/memory/lockManager.js.map +1 -1
  141. package/dist/memory/projectManager.d.ts +2 -1
  142. package/dist/memory/projectManager.d.ts.map +1 -1
  143. package/dist/memory/projectManager.js +21 -3
  144. package/dist/memory/projectManager.js.map +1 -1
  145. package/dist/memory/projectRegistry.d.ts +34 -1
  146. package/dist/memory/projectRegistry.d.ts.map +1 -1
  147. package/dist/memory/projectRegistry.js +99 -22
  148. package/dist/memory/projectRegistry.js.map +1 -1
  149. package/dist/memory/recall.d.ts +16 -0
  150. package/dist/memory/recall.d.ts.map +1 -1
  151. package/dist/memory/recall.js +33 -18
  152. package/dist/memory/recall.js.map +1 -1
  153. package/dist/memory/relationStore.d.ts +20 -3
  154. package/dist/memory/relationStore.d.ts.map +1 -1
  155. package/dist/memory/sourceValidation.d.ts +11 -0
  156. package/dist/memory/sourceValidation.d.ts.map +1 -1
  157. package/dist/memory/sourceValidation.js +24 -2
  158. package/dist/memory/sourceValidation.js.map +1 -1
  159. package/dist/memory/storageInterface.d.ts +26 -1
  160. package/dist/memory/storageInterface.d.ts.map +1 -1
  161. package/dist/memory/store.d.ts +22 -1
  162. package/dist/memory/store.d.ts.map +1 -1
  163. package/dist/memory/store.js +58 -20
  164. package/dist/memory/store.js.map +1 -1
  165. package/dist/memory/types.d.ts +44 -31
  166. package/dist/memory/types.d.ts.map +1 -1
  167. package/dist/memory/types.js +63 -24
  168. package/dist/memory/types.js.map +1 -1
  169. package/dist/memory/userProfile.d.ts +32 -8
  170. package/dist/memory/userProfile.d.ts.map +1 -1
  171. package/dist/memory/userProfile.js +105 -44
  172. package/dist/memory/userProfile.js.map +1 -1
  173. package/dist/memory/vectorStore.d.ts +16 -22
  174. package/dist/memory/vectorStore.d.ts.map +1 -1
  175. package/dist/memory/vectorStore.js +49 -22
  176. package/dist/memory/vectorStore.js.map +1 -1
  177. package/dist/persona/personaManager.d.ts +36 -87
  178. package/dist/persona/personaManager.d.ts.map +1 -1
  179. package/dist/persona/personaManager.js +131 -230
  180. package/dist/persona/personaManager.js.map +1 -1
  181. package/dist/skill/skillManager.d.ts +11 -57
  182. package/dist/skill/skillManager.d.ts.map +1 -1
  183. package/dist/skill/skillManager.js +46 -142
  184. package/dist/skill/skillManager.js.map +1 -1
  185. package/dist/skill/types.d.ts +2 -3
  186. package/dist/skill/types.d.ts.map +1 -1
  187. package/dist/utils/configResourceManager.d.ts +111 -0
  188. package/dist/utils/configResourceManager.d.ts.map +1 -0
  189. package/dist/utils/configResourceManager.js +148 -0
  190. package/dist/utils/configResourceManager.js.map +1 -0
  191. package/dist/utils/errors.d.ts +17 -0
  192. package/dist/utils/errors.d.ts.map +1 -1
  193. package/dist/utils/errors.js +19 -0
  194. package/dist/utils/errors.js.map +1 -1
  195. package/dist/utils/eventEmitter.d.ts +83 -6
  196. package/dist/utils/eventEmitter.d.ts.map +1 -1
  197. package/dist/utils/eventEmitter.js +23 -0
  198. package/dist/utils/eventEmitter.js.map +1 -1
  199. package/dist/utils/frontmatter.d.ts.map +1 -1
  200. package/dist/utils/frontmatter.js +4 -2
  201. package/dist/utils/frontmatter.js.map +1 -1
  202. package/dist/utils/math.d.ts +11 -0
  203. package/dist/utils/math.d.ts.map +1 -1
  204. package/dist/utils/math.js +17 -0
  205. package/dist/utils/math.js.map +1 -1
  206. package/dist/utils/segmenter.d.ts +0 -10
  207. package/dist/utils/segmenter.d.ts.map +1 -1
  208. package/dist/utils/segmenter.js +10 -1
  209. package/dist/utils/segmenter.js.map +1 -1
  210. package/docs/memora-api-reference.md +1235 -0
  211. package/docs/memora-/346/216/245/345/205/245/346/214/207/345/215/227.md +808 -0
  212. package/package.json +39 -22
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent 门面类 — Memora 宿主项目接入入口
3
3
  *
4
- * 设计文档(ADR-010 · Agent 门面类)要求宿主项目通过 `import { Agent } from '@memora/core'`
4
+ * 设计文档(ADR-010 · Agent 门面类)要求宿主项目通过 `import { Agent } from '@zooique/memora'`
5
5
  * 一行代码接入。本类负责组件组装和核心对话编排,
6
6
  * 领域专属操作委托给专职 Manager(PersonaManager / ToolExecutor / SkillManager / ConfigManager / InsightExtractor / MemoryInspector)。
7
7
  *
@@ -26,34 +26,27 @@ 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
- import { recall } from '../memory/recall.js';
29
+ import { recall, boostScores } from '../memory/recall.js';
30
30
  import { assembleComponents } from '../agent/assembler.js';
31
31
  import { matchPersonaByLlm } from '../agent/personaMatcher.js';
32
- import { chatBusyError, configError } from '../utils/errors.js';
33
- import { clearSafeInterval } from '../utils/safeTimer.js';
32
+ import { chatBusyError, configError, isAbortError, toError } from '../utils/errors.js';
34
33
  import { SessionManager } from '../agent/managers/sessionManager.js';
35
34
  import { ChatLockManager } from '../agent/managers/chatLockManager.js';
36
35
  import { MemoryDecayScheduler } from '../agent/managers/memoryDecayScheduler.js';
36
+ import { MemoryGovernance } from '../agent/managers/memoryGovernance.js';
37
37
  import { ArchiveCoordinator } from '../agent/managers/archiveCoordinator.js';
38
- import { TypedEventEmitter } from '../utils/eventEmitter.js';
38
+ import { TypedEventEmitter, AGENT_EVENTS, AGENT_EVENT_SET } from '../utils/eventEmitter.js';
39
39
  import { logger } from '../logging/logger.js';
40
40
  import { TRACE_SPANS, NOOP_TRACER } from '../agent/tracer.js';
41
41
  // ─── 模块级常量 ─────────────────────────────────────────
42
- /** Agent 事件名白名单,用于运行时校验 SessionManager 转发的事件类型 */
43
- // 必须与 utils/eventEmitter.ts 的 AgentEventMap 键集保持一致(10 个事件)
44
- const AGENT_EVENT_NAMES = new Set([
45
- 'memoryAdded', 'personaSwitched', 'decayCompleted',
46
- 'memoryRecalled', 'sessionForked', 'insightExtracted',
47
- 'conflictDetected', 'projectSwitched', 'skillMatched',
48
- 'archiveFailed',
49
- ]);
42
+ // AGENT_EVENTS / AGENT_EVENT_SET 已迁移至 utils/eventEmitter.ts(与 AgentEventMap 同处,单一真理源)
50
43
  // ─── Agent 门面类 ───────────────────────────────────────
51
44
  /**
52
45
  * Memora Agent 门面类
53
46
  *
54
47
  * **设计哲学**:单 Agent,单配置,单记忆。每个 Agent 实例拥有独立的
55
48
  * 对话管线(AgentLoop)、独立的消息历史(MessageHistory)和独立的
56
- * 运行时状态(activeSkill、chatBusy)。
49
+ * 运行时状态(chatBusy)。activeSkill 已移除——技能匹配改为当轮实时注入,无需跨轮状态缓存。
57
50
  *
58
51
  * **多实例**:宿主可通过 new Agent({ dataDir: './agent2' }) 创建
59
52
  * 独立实例。只要 dataDir 不同,它们拥有完全隔离的记忆库和会话。
@@ -98,6 +91,23 @@ export class Agent extends TypedEventEmitter {
98
91
  insightExtractor = null;
99
92
  configManager = null;
100
93
  memoryInspector = null;
94
+ /**
95
+ * 语义去重管理器(L1 LLM 记忆治理)
96
+ *
97
+ * SPLIT-3 闭环(2026-07-21):从 MemoryInspector 拆分出 deduplicateMemories 职责,
98
+ * 让 MemoryInspector 回归纯存储读写。Agent.deduplicateMemories() 委托本对象。
99
+ */
100
+ dedupManager = null;
101
+ /**
102
+ * 记忆顾问(L3 冲突检测 / sourceHealth / suggest)
103
+ *
104
+ * v2 PROXY-1 闭环:Agent.detectConflicts 直接调用 advisor,
105
+ * 不再经 MemoryInspector 转发,消除 3 层无意义代理。
106
+ * sourceHealth/suggest 仍由 inspector 转发以保持 agent.memory 统一入口语义。
107
+ */
108
+ memoryAdvisor = null;
109
+ /** 记忆治理统一门面(L0/L1/L2/L3 + 诊断) */
110
+ _governance = null;
101
111
  workProjection = null;
102
112
  /** AutoConfigRefiner(模式 3:Agent 智能总结) */
103
113
  autoConfigRefiner = null;
@@ -107,8 +117,7 @@ export class Agent extends TypedEventEmitter {
107
117
  textPolisher = null;
108
118
  /** 会话管理器(从 Agent 拆分出的会话管理职责) */
109
119
  _sessionManager = null;
110
- /** 当前激活的技能名(上一轮匹配,本轮注入) */
111
- activeSkill = null;
120
+ // activeSkill 字段已移除:matchAndInjectSkill 改为当轮实时匹配注入,无需跨轮状态缓存
112
121
  _initialized = false;
113
122
  // 项目上下文(AgentContext 与 ProjectContext 等价,直接使用后者避免重复字段)
114
123
  pctx = null;
@@ -119,10 +128,19 @@ export class Agent extends TypedEventEmitter {
119
128
  * 详见 ChatLockManager 类注释。
120
129
  */
121
130
  chatLockManager = null;
122
- /** 记忆衰减定时器(已迁移至 MemoryDecayScheduler,此字段保留用于 close 时引用判断) */
123
- decayTimer = null;
124
131
  /** 最近一次 chat() 调用的时间戳 */
125
132
  _lastInteractionAt = null;
133
+ /**
134
+ * 对话进行中暂存的配置重载请求集合(chatLock 释放后补执行)
135
+ *
136
+ * 场景:用户在对话中通过 create_persona / create_skill 工具创建配置,
137
+ * reloadConfig 因 chatLock 冲突失败时,将 source 暂存于此。
138
+ * chat() 的 finally 块释放锁后遍历此集合逐个补执行 reloadConfig,
139
+ * 兑现"对话结束后自动加载"的承诺(见 tools.ts createConfigHandler 提示文案)。
140
+ *
141
+ * 用 Set 而非数组:同一 source 多次请求只需补执行一次(去重)。
142
+ */
143
+ pendingConfigReload = new Set();
126
144
  // ─── 衰减职责已拆分至 MemoryDecayScheduler ──────────
127
145
  // metricDecayRunCount / metricTotalDecayedCount / metricLastDecayAt 字段
128
146
  // 位于 MemoryDecayScheduler 内部,Agent 通过 memoryDecayScheduler.getMetrics() 读取
@@ -134,7 +152,7 @@ export class Agent extends TypedEventEmitter {
134
152
  super();
135
153
  this.#config = {
136
154
  projectPath: opts.projectPath,
137
- dataDir: opts.dataDir ?? '~/.memora',
155
+ dataDir: opts.dataDir,
138
156
  registryDir: opts.registryDir,
139
157
  maxContextTokens: opts.maxContextTokens ?? AGENT_CONSTANTS.DEFAULT_MAX_CONTEXT_TOKENS,
140
158
  personaName: opts.persona,
@@ -156,27 +174,42 @@ export class Agent extends TypedEventEmitter {
156
174
  };
157
175
  this.#provider = opts.provider;
158
176
  this.#backgroundProvider = opts.backgroundProvider ?? null;
159
- // 如需自定义日志,请在创建 Agent 前调用 `import { setLogger } from '@memora/core'` 全局设置
177
+ // 如需自定义日志,请在创建 Agent 前调用 `import { setLogger } from '@zooique/memora'` 全局设置
160
178
  }
161
179
  // ─── 生命周期 ─────────────────────────────────────────
162
180
  /**
163
181
  * 初始化 Agent:加载索引、组装内部组件
164
182
  */
165
183
  async init(projectPathOverride) {
166
- if (this._initialized) {
167
- // close() 失败不应阻塞 init() 重建
168
- // 原 close() 异常(如 awaitPendingArchives 超时)会传播到 init() 调用方,
169
- // 导致 Agent 处于不可用状态。此处捕获后继续重建。
170
- try {
171
- await this.close();
172
- }
173
- catch (err) {
174
- logger.warn({ err }, 'init() 中 close() 旧实例失败,继续重建');
175
- }
176
- }
184
+ await this.disposePreviousInstance();
177
185
  if (projectPathOverride) {
178
186
  this.#config.projectPath = projectPathOverride;
179
187
  }
188
+ const pctx = await this.initializeProject();
189
+ await this.assembleComponents(pctx);
190
+ this.pctx = pctx;
191
+ this.validateCoreComponents();
192
+ this.createPostInitComponents(pctx);
193
+ this._initialized = true;
194
+ return pctx;
195
+ }
196
+ /**
197
+ * 关闭旧实例(如果已初始化),失败不阻塞重建
198
+ */
199
+ async disposePreviousInstance() {
200
+ if (!this._initialized)
201
+ return;
202
+ try {
203
+ await this.close();
204
+ }
205
+ catch (err) {
206
+ logger.warn({ err }, 'init() 中 close() 旧实例失败,继续重建');
207
+ }
208
+ }
209
+ /**
210
+ * 创建 ProjectManager 并初始化项目上下文
211
+ */
212
+ async initializeProject() {
180
213
  this.projectManager = new ProjectManager({
181
214
  dataDir: this.#config.dataDir,
182
215
  storage: this.#config.storage,
@@ -184,9 +217,12 @@ export class Agent extends TypedEventEmitter {
184
217
  // SecurityGuard 由 Agent 层创建,解除 memory→security 反向依赖
185
218
  createSecurityGuard: (projectPath, memoraDir, configDir, agentDataDir) => new SecurityGuard(projectPath, memoraDir, this.#config.allowedPaths, this.#config.confirmWrites, this.#config.permission, configDir ?? this.#config.configDir, agentDataDir ?? this.#config.dataDir),
186
219
  });
187
- const pctx = await this.projectManager.initProject(this.#config.projectPath, undefined, this.#config.configDir);
188
- await this.assembleComponents(pctx);
189
- this.pctx = pctx;
220
+ return this.projectManager.initProject(this.#config.projectPath, undefined, this.#config.configDir);
221
+ }
222
+ /**
223
+ * 校验核心组件(loop / history)非空
224
+ */
225
+ validateCoreComponents() {
190
226
  if (!this.loop || !this.history) {
191
227
  throw configError('Agent 初始化失败', '组件组装后 loop 或 history 为空(可能 assembleComponents 抛异常被静默吞掉)', [
192
228
  '检查 assembleComponents() 是否有未捕获的异常',
@@ -194,29 +230,28 @@ export class Agent extends TypedEventEmitter {
194
230
  '确认 API Key 已配置(环境变量或配置文件)',
195
231
  ]);
196
232
  }
233
+ }
234
+ /**
235
+ * 创建 init() 后期组件(初始化完成后才需要的组件)
236
+ */
237
+ createPostInitComponents(pctx) {
197
238
  // chat() 并发锁管理器(生命周期与 Agent 实例一致)
198
239
  this.chatLockManager = new ChatLockManager();
199
- this._initialized = true;
200
240
  // 归档操作委托给 ArchiveCoordinator
201
- // 使用 getter 回调注入依赖,close 时 null 化字段后 getter 自然返回 null
202
241
  this.archiveCoordinator = new ArchiveCoordinator({
203
242
  getUserProfile: () => this.#userProfile,
204
243
  getInsightExtractor: () => this.insightExtractor,
205
244
  getSessionArchiver: () => this.sessionArchiver,
245
+ getArchiveMode: () => this.#config.archiveMode,
206
246
  emit: (event, payload) => this.emit(event, payload),
207
247
  });
208
248
  // 记忆衰减职责委托给 MemoryDecayScheduler
209
- // L2 时效性评估:注入 backgroundProvider + index(可选,未注入时 evaluateTimeliness 静默跳过)
210
249
  this.memoryDecayScheduler = new MemoryDecayScheduler({
211
250
  tracer: this.#config.tracer,
212
251
  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 回调语义一致:不阻塞衰减循环)
252
+ this.emit(AGENT_EVENTS.decayCompleted, payload);
253
+ // MIND2-D4:从 agent.evaluateTimeliness() 迁移到 governance.evaluateTimeliness()
254
+ void this._governance?.evaluateTimeliness().catch((err) => {
220
255
  logger.warn({ err }, 'L2 时效性评估自动触发失败(已降级,不影响衰减循环)');
221
256
  });
222
257
  },
@@ -224,38 +259,21 @@ export class Agent extends TypedEventEmitter {
224
259
  index: pctx.index,
225
260
  });
226
261
  this.memoryDecayScheduler.start(pctx.index, AGENT_CONSTANTS.DECAY_INTERVAL_MS);
227
- // 保留 decayTimer 引用用于 close 时序兼容(实际定时器由 MemoryDecayScheduler 管理)
228
- this.decayTimer = null;
229
- return pctx;
262
+ // 记忆治理统一门面(L0/L1/L2/L3 + 诊断)
263
+ this._governance = new MemoryGovernance(this.dedupManager, this.memoryDecayScheduler, this.memoryAdvisor);
230
264
  }
231
265
  /**
232
266
  * 流式对话(核心 API)
233
267
  */
234
268
  async *chat(input, signal) {
235
269
  this.assertInitialized('chat');
236
- if (input.length > AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH) {
237
- throw configError('输入过长', `输入超过最大长度限制(${AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH / 1024}KB)`, ['缩短输入内容', '分多次对话发送']);
238
- }
239
- if (this.chatLockManager?.isBusy) {
240
- throw chatBusyError('发起新对话');
241
- }
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);
247
- // 合并外部 signal:外部 abort 时也触发内部
248
- const cleanupExternalSignal = chatLock.attachExternalSignal(signal, internalAbort);
249
- const combinedSignal = internalAbort.signal;
270
+ this.validateChatInput(input);
271
+ const lockCtx = this.acquireChatLock(signal);
272
+ const { myToken, combinedSignal, cleanupExternalSignal } = lockCtx;
250
273
  try {
251
274
  this._lastInteractionAt = new Date();
252
- // 清理上一轮注入的临时 system 消息(recentConversation/skill/recall/truncation)
253
- // 防止多轮累积:每轮 chat() 开始前,只保留 messages[0] 和非 system 消息
254
- const loop = this.loop;
255
- loop.cleanTemporarySystemMessages();
256
- // 基元驱动召回(双通道:语义 + 关键词)
257
- yield { type: 'thinking', phase: 'recalling' };
258
- const recalledMemories = await this.recallAndInject(input);
275
+ // 上下文准备:角色匹配 → 记忆召回 → 技能注入 → 历史追加
276
+ const recalledMemories = yield* this.prepareChatContext(input, combinedSignal);
259
277
  if (combinedSignal.aborted) {
260
278
  yield {
261
279
  type: 'aborted',
@@ -263,72 +281,128 @@ export class Agent extends TypedEventEmitter {
263
281
  };
264
282
  return;
265
283
  }
266
- // 注入上一轮匹配的技能 prompt
267
- yield { type: 'thinking', phase: 'processing' };
268
- this.injectActiveSkill();
269
- const history = this.history;
270
- await history.appendUser(input);
271
- let assistantContent = '';
272
- let wasAborted = false;
273
- // 把 provider 异常转为 yield error chunk,让宿主能优雅展示并清理 UI
274
- // (裸 throw 会导致未处理 rejection,UI 收不到错误展示机会)
275
- try {
276
- for await (const chunk of loop.processUserInput(input, recalledMemories, combinedSignal)) {
277
- yield chunk;
278
- if (chunk.type === 'text') {
279
- assistantContent += chunk.content;
280
- }
281
- else if (chunk.type === 'aborted') {
282
- wasAborted = true;
283
- }
284
- }
285
- }
286
- catch (err) {
287
- // aborted 已由 loop.ts 内部 yield chunk 处理,此处只捕获真正的异常
288
- // (如 LLM 超时、连接断开、AbortError 未被 loop 拦截等)
289
- // 转为 error chunk 通知宿主,避免裸 throw 导致 UI 卡死
290
- if (err instanceof DOMException && err.name === 'AbortError') {
291
- // 必须 yield aborted chunk 让宿主能展示中断标记
292
- // (仅设置 wasAborted 会导致 chatHandlers catch 不触发,渲染层收不到 ABORTED)
293
- yield { type: 'aborted', reason: 'User cancelled the conversation' };
294
- return;
284
+ // LLM 流式调用 → 助手消息写历史 → 后处理
285
+ yield* this.executeChatLoop(input, recalledMemories, combinedSignal);
286
+ }
287
+ finally {
288
+ // 仅当本调用仍是当前锁持有者时才清理资源(token 校验)
289
+ this.chatLockManager?.release(myToken);
290
+ cleanupExternalSignal();
291
+ // 补执行对话期间暂存的配置重载请求
292
+ await this.flushPendingConfigReload();
293
+ }
294
+ }
295
+ /**
296
+ * 校验 chat 输入
297
+ */
298
+ validateChatInput(input) {
299
+ if (input.length > AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH) {
300
+ throw configError('输入过长', `输入超过最大长度限制(${AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH / 1024}KB)`, ['缩短输入内容', '分多次对话发送']);
301
+ }
302
+ }
303
+ /**
304
+ * 获取对话锁 — 返回锁上下文
305
+ */
306
+ acquireChatLock(signal) {
307
+ const chatLock = this.chatLockManager;
308
+ if (!chatLock)
309
+ throw configError('ChatLockManager', 'ChatLockManager 未初始化', []);
310
+ if (chatLock.isBusy) {
311
+ throw chatBusyError('发起新对话');
312
+ }
313
+ const { token: myToken, internalAbort } = chatLock.acquire(AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS);
314
+ const cleanupExternalSignal = chatLock.attachExternalSignal(signal, internalAbort);
315
+ return { myToken, internalAbort, cleanupExternalSignal, combinedSignal: internalAbort.signal };
316
+ }
317
+ /**
318
+ * 准备对话上下文:角色匹配 → 记忆召回 → 技能注入 → 用户消息写历史
319
+ * @returns 召回的记忆列表(供 loop.processUserInput 注入为 system 消息)
320
+ */
321
+ async *prepareChatContext(input, combinedSignal) {
322
+ const loop = this.requireLoop;
323
+ loop.cleanTemporarySystemMessages();
324
+ // 角色自动匹配(回答前执行)
325
+ await this.tryAutoMatchPersona(input);
326
+ // 基元驱动召回(双通道:语义 + 关键词)
327
+ yield { type: 'thinking', phase: 'recalling' };
328
+ const recalledMemories = await this.recallAndInject(input);
329
+ if (combinedSignal.aborted)
330
+ return recalledMemories;
331
+ // 技能关键词/正则匹配(当轮立即注入生效)
332
+ yield { type: 'thinking', phase: 'processing' };
333
+ this.matchAndInjectSkill(input);
334
+ const history = this.requireHistory;
335
+ await history.appendUser(input);
336
+ return recalledMemories;
337
+ }
338
+ /**
339
+ * 执行 LLM 流式循环 → 追加助手消息 → 触发后处理
340
+ */
341
+ async *executeChatLoop(input, recalledMemories, combinedSignal) {
342
+ const loop = this.requireLoop;
343
+ const history = this.requireHistory;
344
+ let assistantContent = '';
345
+ let wasAborted = false;
346
+ try {
347
+ for await (const chunk of loop.processUserInput(input, recalledMemories, combinedSignal)) {
348
+ yield chunk;
349
+ if (chunk.type === 'text') {
350
+ assistantContent += chunk.content;
295
351
  }
296
- else {
297
- yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
298
- return;
352
+ else if (chunk.type === 'aborted') {
353
+ wasAborted = true;
299
354
  }
300
355
  }
301
- if (wasAborted) {
302
- // 流式中断时仍保留已生成的部分文本到历史,避免下一轮上下文丢失
303
- // 追加 interrupted 标记让下一轮 LLM 和历史归档能识别这是中断响应(非完整回复)
304
- // 与下方正常路径一致采用 best-effort 写入(失败不影响中断流程)
305
- if (assistantContent.trim()) {
306
- const interruptedMark = this.#config.messages?.interrupted ?? '\n\n[已中断]';
307
- try {
308
- await history.appendAssistant(assistantContent + interruptedMark);
309
- }
310
- catch (err) {
311
- logger.warn({ err }, '中断消息历史写入失败');
312
- }
313
- }
356
+ }
357
+ catch (err) {
358
+ if (isAbortError(err)) {
359
+ yield { type: 'aborted', reason: 'User cancelled the conversation' };
314
360
  return;
315
361
  }
316
- // 追加助手消息到历史(best-effort:失败不影响用户已收到的回答)
362
+ else {
363
+ yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
364
+ return;
365
+ }
366
+ }
367
+ if (wasAborted) {
368
+ if (assistantContent.trim()) {
369
+ const interruptedMark = this.#config.messages?.interrupted ?? '\n\n[已中断]';
370
+ try {
371
+ await history.appendAssistant(assistantContent + interruptedMark);
372
+ }
373
+ catch (err) {
374
+ logger.warn({ err }, '中断消息历史写入失败');
375
+ }
376
+ }
377
+ return;
378
+ }
379
+ // 追加助手消息到历史(best-effort)
380
+ try {
381
+ await history.appendAssistant(assistantContent);
382
+ }
383
+ catch (err) {
384
+ logger.warn({ err }, '助手消息历史写入失败');
385
+ }
386
+ // 后处理阶段
387
+ yield { type: 'thinking', phase: 'archiving' };
388
+ await this.postProcess(input, assistantContent);
389
+ }
390
+ /**
391
+ * 补执行对话期间暂存的配置重载请求
392
+ */
393
+ async flushPendingConfigReload() {
394
+ if (this.pendingConfigReload.size === 0)
395
+ return;
396
+ const pending = Array.from(this.pendingConfigReload);
397
+ this.pendingConfigReload.clear();
398
+ for (const src of pending) {
317
399
  try {
318
- await history.appendAssistant(assistantContent);
400
+ await this.reloadConfig(src);
401
+ this.emit(AGENT_EVENTS.configReloaded, { source: src });
319
402
  }
320
403
  catch (err) {
321
- logger.warn({ err }, '助手消息历史写入失败');
404
+ logger.warn({ err, source: src }, '补执行配置重载失败');
322
405
  }
323
- // 后处理阶段
324
- yield { type: 'thinking', phase: 'archiving' };
325
- await this.postProcess(input, assistantContent);
326
- }
327
- finally {
328
- // 仅当本调用仍是当前锁持有者时才清理资源(token 校验)
329
- // 若 token 已变(超时释放后被新调用者获取),跳过清理避免误清新调用者的状态
330
- this.chatLockManager?.release(myToken);
331
- cleanupExternalSignal();
332
406
  }
333
407
  }
334
408
  /**
@@ -378,8 +452,8 @@ export class Agent extends TypedEventEmitter {
378
452
  });
379
453
  let recalledMemories = [];
380
454
  try {
381
- recalledMemories = await recall(this.pctx.index, input, {
382
- limit: 5,
455
+ recalledMemories = await recall(this.requirePctx.index, input, {
456
+ limit: AGENT_CONSTANTS.DEFAULT_RECALL_LIMIT,
383
457
  vectorStore: this.#config.vectorStore,
384
458
  excludeSources: this.#config.recallExcludeSources,
385
459
  });
@@ -393,11 +467,18 @@ export class Agent extends TypedEventEmitter {
393
467
  recallSpan.end();
394
468
  }
395
469
  if (recalledMemories.length > 0) {
396
- this.emit('memoryRecalled', { count: recalledMemories.length, query: input });
470
+ this.emit(AGENT_EVENTS.memoryRecalled, { count: recalledMemories.length, query: input });
471
+ // FIX-P1-2:boost 持久化拆分为 fire-and-forget,不阻塞 chat 读路径
472
+ // boost 是软指标(每次 +0.05,上限 1.0),写入失败仅 log 不影响 chat 流程
473
+ const ids = recalledMemories.map((m) => m.id);
474
+ void boostScores(this.requirePctx.index, ids).catch((err) => {
475
+ logger.warn({ err }, 'boost 持久化失败(不影响 chat 流程)');
476
+ this.emit(AGENT_EVENTS.boostPersistFailed, { memoryId: ids.join(','), message: toError(err).message });
477
+ });
397
478
  }
398
479
  // Layer 5: 最近对话注入
399
- const loop = this.loop;
400
- const recentHistory = loop.getRecentHistory(3);
480
+ const loop = this.requireLoop;
481
+ const recentHistory = loop.getRecentHistory(AGENT_CONSTANTS.DEFAULT_RECENT_HISTORY_ROUNDS);
401
482
  if (recentHistory.length > 0) {
402
483
  const msgs = this.#config.messages;
403
484
  const label = msgs?.recentConversationLabel ?? '[Recent conversation]';
@@ -413,16 +494,34 @@ export class Agent extends TypedEventEmitter {
413
494
  return recalledMemories;
414
495
  }
415
496
  /**
416
- * 注入上一轮匹配的技能 prompt
497
+ * 匹配技能并立即注入本轮对话(当轮生效,不延迟到下一轮)
498
+ *
499
+ * 与 persona 的 tryAutoMatchPersona 同模式:匹配 → 注入 → 本轮 LLM 即生效。
500
+ * 原设计为 injectActiveSkill(注入上一轮匹配结果),导致用户说"写代码"的第一轮
501
+ * 得不到技能增强,需再发一条消息才生效。已改为实时匹配注入。
502
+ *
503
+ * 匹配策略:regex trigger 优先(score=1.0),其次关键词匹配(阈值 0.3)。
504
+ * 清理机制:cleanTemporarySystemMessages() 每轮开头清除临时 system 消息,
505
+ * 技能 prompt 自然不会累积到下一轮。
506
+ *
507
+ * @param input 用户输入文本
417
508
  */
418
- injectActiveSkill() {
419
- if (this.activeSkill && this.skillManager && this.loop) {
420
- const skillPrompt = this.skillManager.buildSystemPrompt(this.activeSkill);
421
- if (skillPrompt) {
422
- this.loop.injectSystemMessage(skillPrompt);
423
- logger.debug({ skill: this.activeSkill }, '技能 prompt 已注入');
509
+ matchAndInjectSkill(input) {
510
+ if (!this.skillManager || !this.loop)
511
+ return;
512
+ try {
513
+ const match = this.skillManager.match(input);
514
+ if (match) {
515
+ const skillPrompt = this.skillManager.buildSystemPrompt(match.skill.name);
516
+ if (skillPrompt) {
517
+ this.loop.injectSystemMessage(skillPrompt);
518
+ logger.debug({ skill: match.skill.name, score: match.score }, '技能 prompt 已注入(当轮生效)');
519
+ }
520
+ this.emit(AGENT_EVENTS.skillMatched, { skill: match.skill.name, score: match.score });
424
521
  }
425
- this.activeSkill = null;
522
+ }
523
+ catch (err) {
524
+ logger.warn({ err }, '技能匹配失败');
426
525
  }
427
526
  }
428
527
  /**
@@ -445,7 +544,7 @@ export class Agent extends TypedEventEmitter {
445
544
  archiveMode: this.#config.archiveMode,
446
545
  });
447
546
  try {
448
- await this.postProcessInner(input, assistantContent);
547
+ await this.doPostProcess(input, assistantContent);
449
548
  }
450
549
  finally {
451
550
  span.end();
@@ -456,95 +555,42 @@ export class Agent extends TypedEventEmitter {
456
555
  *
457
556
  * 原 postProcess 逻辑完整保留于此,由外层 postProcess 负责 span 生命周期管理。
458
557
  */
459
- async postProcessInner(input, assistantContent) {
460
- // 角色自动匹配(best-effort:失败不阻塞对话结束,非归档行为不受 archiveMode 影响)
461
- // 两层匹配策略:关键词高置信度 → LLM 辅助(低置信度且 backgroundProvider 已注入时)
462
- // LLM 辅助匹配在 agent 层执行,遵循 backend_layers_rules §分层职责(persona/ 不直接调 LLM)
463
- if (this.personaManager) {
464
- try {
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
- }
474
- if (matchedPersona) {
475
- const prevName = this.personaManager.activeName;
476
- this.personaManager.switchPersona(matchedPersona);
477
- this.emit('personaSwitched', { from: prevName, to: matchedPersona });
478
- if (this.loop) {
479
- const profilePrompt = this.userProfile?.buildSystemPrompt() ?? '';
480
- const personaPrompt = this.personaManager.buildSystemPrompt();
481
- const newPrefix = [personaPrompt, profilePrompt].filter(Boolean).join('\n\n') +
482
- ([personaPrompt, profilePrompt].some(Boolean) ? '\n\n---\n\n' : '');
483
- this.loop.refreshPersonaPrefix(newPrefix);
484
- }
485
- logger.info({ persona: matchedPersona }, '角色自动切换');
486
- }
487
- }
488
- catch (err) {
489
- logger.warn({ err }, '角色自动匹配失败');
490
- }
491
- }
492
- // 技能关键词匹配(best-effort:失败不阻塞对话结束,非归档行为不受 archiveMode 影响)
493
- if (this.skillManager) {
494
- try {
495
- const match = this.skillManager.match(input);
496
- if (match) {
497
- this.activeSkill = match.skill.name;
498
- // 发射 skillMatched 事件:宿主 UI 可据此展示当前激活技能
499
- this.emit('skillMatched', { skill: match.skill.name, score: match.score });
500
- logger.debug({ skill: match.skill.name, score: match.score }, '技能匹配,下一轮注入');
501
- }
502
- }
503
- catch (err) {
504
- logger.warn({ err }, '技能匹配失败');
505
- }
506
- }
507
- // ADR-015: manual 模式跳过所有自动归档(profile + insight),
508
- // 需用户手动调用 archiveProfileFacts/archiveInsight 触发。
558
+ async doPostProcess(input, assistantContent) {
559
+ // 技能匹配已迁移到 chat() 开头的 matchAndInjectSkill()(当轮实时生效),
560
+ // 与 persona 的 tryAutoMatchPersona 同模式,消除"第一轮无技能"的一轮延迟问题。
561
+ // ADR-015 + FIX-P1-4: archiveMode 三态控制集中到 ArchiveCoordinator
562
+ // 此处统一传 { autoTriggered: true },由 ArchiveCoordinator 内部按 archiveMode 判断是否跳过:
563
+ // - manual 模式 → 跳过 profile/insight 自动归档(用户需手动调用)
564
+ // - full / insights-only 模式 → 执行
509
565
  // 角色匹配/技能匹配/AutoConfigRefiner 属"配置学习"行为,非归档,每轮都执行。
510
- const skipAutoArchive = this.#config.archiveMode === 'manual';
511
- if (skipAutoArchive) {
512
- logger.debug({ mode: 'manual' }, '归档模式为 manual,跳过自动归档');
513
- }
566
+ const history = this.requireHistory;
514
567
  // 用户画像实时归档(委托 ArchiveCoordinator,与手动归档路径统一,消除 DRY 违反)
515
- // ADR-015: full / insights-only 模式下 profile facts 自动归档
516
- const history = this.history;
517
- if (!skipAutoArchive) {
518
- try {
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
- });
526
- history.registerPendingArchive(archiveFactsPromise);
527
- }
528
- catch (err) {
529
- logger.warn({ err }, '用户画像归档初始化失败');
530
- }
568
+ try {
569
+ // fire-and-forget 包装:registerPendingArchive 确保 close() 时等待后台归档完成
570
+ // 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
571
+ const archiveFactsPromise = this.requireArchiveCoordinator.archiveProfileFacts(input, { autoTriggered: true }).then(() => { }, (err) => {
572
+ const message = err instanceof Error ? err.message : String(err);
573
+ logger.warn({ err, stage: 'profile' }, '归档失败');
574
+ this.emit(AGENT_EVENTS.archiveFailed, { stage: 'profile', message: message.slice(0, 200) });
575
+ });
576
+ history.registerPendingArchive(archiveFactsPromise);
577
+ }
578
+ catch (err) {
579
+ logger.warn({ err }, '用户画像归档初始化失败');
531
580
  }
532
581
  // 输入分类 → Insight 提取(委托 ArchiveCoordinator,与手动归档路径统一)
533
- // ADR-015: full / insights-only 模式下 insight 自动归档
534
- if (!skipAutoArchive) {
535
- try {
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);
544
- }
545
- catch (err) {
546
- logger.warn({ err }, 'Insight 提取初始化失败');
547
- }
582
+ try {
583
+ // fire-and-forget 包装:classify 判断由 ArchiveCoordinator 内部完成
584
+ // 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
585
+ const archiveInsightPromise = this.requireArchiveCoordinator.archiveInsight(input, assistantContent, { autoTriggered: true }).then(() => { }, (err) => {
586
+ const message = err instanceof Error ? err.message : String(err);
587
+ logger.warn({ err, stage: 'insight' }, '归档失败');
588
+ this.emit(AGENT_EVENTS.archiveFailed, { stage: 'insight', message: message.slice(0, 200) });
589
+ });
590
+ history.registerPendingArchive(archiveInsightPromise);
591
+ }
592
+ catch (err) {
593
+ logger.warn({ err }, 'Insight 提取初始化失败');
548
594
  }
549
595
  // AutoConfigRefiner(模式 3:Agent 智能总结)
550
596
  if (this.autoConfigRefiner) {
@@ -580,7 +626,7 @@ export class Agent extends TypedEventEmitter {
580
626
  */
581
627
  forkSession(targetSession) {
582
628
  this.assertInitialized('forkSession');
583
- return this._sessionManager.forkSession(targetSession);
629
+ return this.requireSessionManager.forkSession(targetSession);
584
630
  }
585
631
  /**
586
632
  * 切换到指定项目
@@ -591,9 +637,7 @@ export class Agent extends TypedEventEmitter {
591
637
  async switchProject(nameOrPath) {
592
638
  this.assertInitialized('switchProject', ['projectManager', 'provider']);
593
639
  // 对话进行中切换项目会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
594
- if (this.chatLockManager?.isBusy) {
595
- throw chatBusyError('切换项目');
596
- }
640
+ this.assertNotBusy('切换项目');
597
641
  const pm = this.projectManager;
598
642
  const projects = pm.list;
599
643
  let target = projects.find((p) => p.name === nameOrPath || p.path === nameOrPath);
@@ -606,10 +650,21 @@ export class Agent extends TypedEventEmitter {
606
650
  const newPctx = await pm.initProject(projectPath, projectName, this.#config.configDir);
607
651
  // 记录源项目路径(用于事件),切换前 pctx 可能不存在(首次初始化)
608
652
  const fromProjectPath = this.pctx?.projectPath ?? null;
609
- this.pctx = newPctx;
610
- await this.rebuildComponentsWithCurrentCtx();
653
+ // 先尝试重建组件,失败时回滚 pctx 防止状态不一致
654
+ try {
655
+ this.pctx = newPctx;
656
+ await this.rebuildComponentsWithCurrentCtx();
657
+ }
658
+ catch (err) {
659
+ // 回滚:恢复旧 pctx(若存在)
660
+ if (fromProjectPath) {
661
+ logger.warn({ err: toError(err) }, 'switchProject: 组件重建失败,回滚项目上下文');
662
+ this.pctx = { projectPath: fromProjectPath };
663
+ }
664
+ throw err;
665
+ }
611
666
  // 发射项目切换事件(供宿主 UI 刷新项目相关界面)
612
- this.emit('projectSwitched', {
667
+ this.emit(AGENT_EVENTS.projectSwitched, {
613
668
  from: fromProjectPath,
614
669
  to: projectPath,
615
670
  projectName,
@@ -634,6 +689,18 @@ export class Agent extends TypedEventEmitter {
634
689
  messages: this.#config.messages,
635
690
  enableContextSummary: this.#config.enableContextSummary,
636
691
  existingSkillManager: this.skillManager,
692
+ onWorkProjectionGenerated: (sourcePath, summary) => {
693
+ this.emit(AGENT_EVENTS.workProjectionGenerated, { sourcePath, summary });
694
+ },
695
+ onContextTruncated: (skippedCount, keptCount) => {
696
+ this.emit(AGENT_EVENTS.contextTruncated, { skippedCount, keptCount });
697
+ },
698
+ onDedupCompleted: (report) => {
699
+ this.emit(AGENT_EVENTS.dedupCompleted, { deduplicatedCount: report.deduplicatedCount, demotedIds: report.demotedIds });
700
+ },
701
+ onGuardrailError: (rule, message) => {
702
+ this.emit(AGENT_EVENTS.guardrailError, { rule, message });
703
+ },
637
704
  });
638
705
  this.history = result.history;
639
706
  this.loop = result.loop;
@@ -644,6 +711,8 @@ export class Agent extends TypedEventEmitter {
644
711
  this.insightExtractor = result.insightExtractor;
645
712
  this.configManager = result.configManager;
646
713
  this.memoryInspector = result.memoryInspector;
714
+ this.dedupManager = result.dedupManager;
715
+ this.memoryAdvisor = result.memoryAdvisor;
647
716
  this.autoConfigRefiner = result.autoConfigRefiner;
648
717
  this.workProjection = result.workProjection;
649
718
  this.sessionArchiver = result.sessionArchiver;
@@ -651,7 +720,7 @@ export class Agent extends TypedEventEmitter {
651
720
  // 绑定冲突检测回调,InsightExtractor 检测到 contradicts 时 emit('conflictDetected')
652
721
  // 与 bindGetRecentHistory 同模式:解决 Agent 晚于 InsightExtractor 创建的时序循环依赖
653
722
  this.insightExtractor.bindOnConflict((info) => {
654
- this.emit('conflictDetected', info);
723
+ this.emit(AGENT_EVENTS.conflictDetected, info);
655
724
  });
656
725
  // 注入 VectorStore 到 MemoryInspector,启用混合搜索
657
726
  if (this.memoryInspector && this.#config.vectorStore) {
@@ -683,7 +752,7 @@ export class Agent extends TypedEventEmitter {
683
752
  */
684
753
  createSessionManager() {
685
754
  const forwardEvent = (event, data) => {
686
- if (AGENT_EVENT_NAMES.has(event)) {
755
+ if (AGENT_EVENT_SET.has(event)) {
687
756
  this.emit(event, data);
688
757
  }
689
758
  };
@@ -695,9 +764,7 @@ export class Agent extends TypedEventEmitter {
695
764
  setProvider(provider) {
696
765
  // 对话进行中切换 Provider 会导致同一 processUserInput 循环内前后两次 LLM 调用命中不同 Provider
697
766
  // (模型上下文窗口假设不一致 → 可能导致上下文截断逻辑误判或 tool_call 格式不兼容)
698
- if (this.chatLockManager?.isBusy) {
699
- throw chatBusyError('切换 Provider');
700
- }
767
+ this.assertNotBusy('切换 Provider');
701
768
  this.#provider = provider;
702
769
  if (this.loop) {
703
770
  this.loop.setProvider(provider);
@@ -706,9 +773,7 @@ export class Agent extends TypedEventEmitter {
706
773
  }
707
774
  setBackgroundProvider(provider) {
708
775
  // 与 setProvider 一致,对话进行中禁止切换后台 Provider
709
- if (this.chatLockManager?.isBusy) {
710
- throw chatBusyError('切换后台 Provider');
711
- }
776
+ this.assertNotBusy('切换后台 Provider');
712
777
  this.#backgroundProvider = provider;
713
778
  // 同步更新 AutoConfigRefiner 的后台 Provider
714
779
  if (this.autoConfigRefiner) {
@@ -725,13 +790,12 @@ export class Agent extends TypedEventEmitter {
725
790
  * @param mode 目标模式
726
791
  */
727
792
  setArchiveMode(mode) {
728
- if (this.chatLockManager?.isBusy) {
729
- throw chatBusyError('切换归档模式');
730
- }
793
+ this.assertNotBusy('切换归档模式');
731
794
  const prev = this.#config.archiveMode;
732
795
  if (prev === mode)
733
796
  return; // 幂等:无变更直接返回
734
797
  this.#config.archiveMode = mode;
798
+ this.emit(AGENT_EVENTS.archiveModeChanged, { from: prev, to: mode });
735
799
  logger.info({ from: prev, to: mode }, '归档模式已切换');
736
800
  }
737
801
  /**
@@ -742,20 +806,180 @@ export class Agent extends TypedEventEmitter {
742
806
  getArchiveMode() {
743
807
  return this.#config.archiveMode;
744
808
  }
809
+ /**
810
+ * 手动切换角色(宿主 UI 角色选择器入口)
811
+ *
812
+ * 与 doPostProcess 中的自动匹配走同一条事件链路,确保:
813
+ * 1. AgentLoop 的 systemPromptPrefix 立即刷新(下一次对话使用新角色 prompt)
814
+ * 2. 发射 personaSwitched 事件,触发 spriteLifecycleManager 的完整副作用:
815
+ * - emit('personaChanged') 通知宿主 UI
816
+ * - proactiveEngine.addNotice('persona', ...) 记录通知
817
+ * - perceptionCoordinator.refreshBeforeChat() 基于新角色 traits 重新推导情感基调
818
+ *
819
+ * 对话进行中切换角色会破坏当前 system prompt,与项目切换一致拒绝。
820
+ *
821
+ * @param name 目标角色名
822
+ * @returns 切换成功返回新角色的 system prompt 段;角色不存在或切换失败返回 null
823
+ */
824
+ switchPersona(name) {
825
+ this.assertInitialized('switchPersona');
826
+ this.assertNotBusy('切换角色');
827
+ if (!this.personaManager)
828
+ return null;
829
+ // 同名切换幂等:直接返回当前 prompt,不触发事件链路
830
+ if (this.personaManager.activeName === name) {
831
+ return this.personaManager.buildSystemPrompt();
832
+ }
833
+ const prevName = this.personaManager.activeName;
834
+ try {
835
+ this.personaManager.switchPersona(name);
836
+ }
837
+ catch (err) {
838
+ // 角色不存在时 PersonaManager 抛 MemoraError,降级为 null 返回
839
+ logger.warn({ err: toError(err).message, name }, '手动切换角色失败');
840
+ return null;
841
+ }
842
+ // 同步刷新 AgentLoop 的角色前缀(关键:否则下一次对话仍用旧角色 prompt)
843
+ // 与 doPostProcess 自动匹配共用同一段逻辑,ADR-017 枝叶层 2 次提取
844
+ this.refreshPersonaPrefixOnLoop();
845
+ // 发射切换事件,触发宿主 UI 刷新 + 感知重推导 + 通知队列记录
846
+ this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: name });
847
+ logger.info({ from: prevName, to: name }, '角色手动切换');
848
+ return this.personaManager.buildSystemPrompt();
849
+ }
850
+ /**
851
+ * 获取角色切换锁定状态(透传 PersonaManager,P0-2 用户体验打磨)
852
+ *
853
+ * 与 switchPersona 分离:switchPersona 仍返回 string | null 不变,
854
+ * 锁定原因查询走独立路径,避免破坏既有契约(cli.ts、sprite.test.ts 等消费者无感)。
855
+ *
856
+ * 宿主 IPC 层调用此方法前置判断锁定状态,区分"切换失败"原因
857
+ * (locked / busy / not_found / invalid),让用户知道为什么没反应。
858
+ *
859
+ * @returns locked 是否处于锁定状态;unlockAt 锁定自动恢复时间戳(ms epoch),未锁定时为 null
860
+ */
861
+ getPersonaSwitchLockStatus() {
862
+ this.assertInitialized('getPersonaSwitchLockStatus');
863
+ if (!this.personaManager)
864
+ return { locked: false, unlockAt: null };
865
+ return this.personaManager.getSwitchLockStatus();
866
+ }
867
+ /**
868
+ * 刷新 AgentLoop 的 systemPromptPrefix(角色 prompt + profile prompt)
869
+ *
870
+ * 提取自 doPostProcess 自动匹配 + switchPersona 手动切换两处共用逻辑(ADR-017 枝叶层 2 次提取)。
871
+ * 组装规则:[personaPrompt, profilePrompt].filter(Boolean).join('\n\n') + 末尾分隔符 '---'
872
+ *
873
+ * 调用时机:
874
+ * - tryAutoMatchPersona 中角色自动匹配成功后(chat() 回答前)
875
+ * - switchPersona 手动切换成功后
876
+ * - loop 为 null 时静默跳过(init 前或 close 后的边界场景)
877
+ */
878
+ refreshPersonaPrefixOnLoop() {
879
+ if (!this.loop)
880
+ return;
881
+ const profilePrompt = this.userProfile?.buildSystemPrompt() ?? '';
882
+ const personaPrompt = this.personaManager?.buildSystemPrompt() ?? '';
883
+ const newPrefix = [personaPrompt, profilePrompt].filter(Boolean).join('\n\n') +
884
+ ([personaPrompt, profilePrompt].some(Boolean) ? '\n\n---\n\n' : '');
885
+ this.loop.refreshPersonaPrefix(newPrefix);
886
+ }
887
+ /**
888
+ * 角色自动匹配(best-effort:失败不阻塞对话流程)
889
+ *
890
+ * 在 chat() 回答前执行,确保本轮 LLM 调用就用匹配到的角色 system prompt。
891
+ * 两层匹配策略:关键词高置信度 → LLM 辅助(低置信度且 backgroundProvider 已注入时)。
892
+ * LLM 辅助匹配在 agent 层执行,遵循 backend_layers_rules §分层职责(persona/ 不直接调 LLM)。
893
+ *
894
+ * 匹配成功时:切换角色 + 发射 personaSwitched 事件 + 刷新 AgentLoop 前缀。
895
+ * 匹配失败/异常时:静默降级,保持当前角色。
896
+ *
897
+ * @param input 用户输入文本
898
+ */
899
+ async tryAutoMatchPersona(input) {
900
+ if (!this.personaManager)
901
+ return;
902
+ try {
903
+ let matchedPersona = null;
904
+ if (this.personaManager.canAutoMatch()) {
905
+ matchedPersona = this.personaManager.autoMatch(input);
906
+ // 关键词低置信度且 backgroundProvider 已注入 → LLM 辅助语义匹配
907
+ const bgProvider = this.#backgroundProvider;
908
+ if (!matchedPersona && bgProvider) {
909
+ matchedPersona = await matchPersonaByLlm(bgProvider, this.personaManager.list, this.personaManager.activeName, input);
910
+ }
911
+ }
912
+ if (matchedPersona) {
913
+ const prevName = this.personaManager.activeName;
914
+ this.personaManager.switchPersona(matchedPersona);
915
+ this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: matchedPersona });
916
+ // 刷新 AgentLoop 的角色前缀(与 switchPersona 共用同一段逻辑,ADR-017 枝叶层 2 次提取)
917
+ this.refreshPersonaPrefixOnLoop();
918
+ logger.info({ persona: matchedPersona }, '角色自动切换');
919
+ }
920
+ else {
921
+ // 关键词 + LLM 均未命中,且当前角色不是列表首个角色 → 回退到首个角色
922
+ // "切过去回不来":用户从默认切到散文作者后,输入无关话题应回到默认角色
923
+ // 使用 list[0] 而非硬编码 'default':防止 default 角色被删除后 fallback 抛异常
924
+ const shouldFallback = this.shouldFallbackToDefault();
925
+ if (shouldFallback) {
926
+ const fallbackTarget = this.personaManager.list[0]?.name ?? 'default';
927
+ const prevName = this.personaManager.activeName;
928
+ this.personaManager.switchPersona(fallbackTarget);
929
+ this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: fallbackTarget });
930
+ this.refreshPersonaPrefixOnLoop();
931
+ logger.info({ persona: fallbackTarget }, '角色回退默认');
932
+ }
933
+ }
934
+ }
935
+ catch (err) {
936
+ logger.warn({ err }, '角色自动匹配失败');
937
+ }
938
+ }
939
+ /**
940
+ * 判断是否应回退到默认角色
941
+ *
942
+ * 回退条件(全部满足):
943
+ * 1. 当前激活角色不是默认角色(已在默认角色则无需回退)
944
+ * 2. 角色管理器存在且非锁定状态(锁定时 switchPersona 也会被拦截,回退无意义)
945
+ *
946
+ * 注意:bgProvider 未配置时也会触发回退——因为关键词 + LLM 均未命中说明当前角色不匹配本轮对话,
947
+ * 回退默认角色比卡在错误角色上更合理。
948
+ *
949
+ * @returns 是否应回退到默认角色
950
+ */
951
+ shouldFallbackToDefault() {
952
+ if (!this.personaManager)
953
+ return false;
954
+ // manual 模式下不回退:用户明确固定了角色,回退会破坏其意图
955
+ if (this.personaManager.currentMode !== 'auto')
956
+ return false;
957
+ const status = this.personaManager.getSwitchLockStatus();
958
+ if (status.locked)
959
+ return false; // 锁定中不回退,避免无意义调用 switchPersona
960
+ // 当前角色已是���表首个角色("默认"角色)→ 无需回退
961
+ const defaultName = this.personaManager.list[0]?.name;
962
+ return defaultName !== undefined && this.personaManager.activeName !== defaultName;
963
+ }
745
964
  /**
746
965
  * 手动触发 profile facts 归档(manual 模式下使用)
747
966
  *
748
967
  * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
749
968
  * full / insights-only 模式下也可调用(会重复归档,但不推荐)。
750
969
  *
970
+ * FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
971
+ * 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
972
+ * 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
973
+ *
751
974
  * 归档逻辑已委托给 ArchiveCoordinator
752
975
  *
753
976
  * @param input 本轮用户输入
977
+ * @param options 触发选项(autoTriggered 默认 false,即手动触发)
754
978
  * @returns 写入/更新的 UserProfileEntry 列表
755
979
  */
756
- async archiveProfileFacts(input) {
980
+ async archiveProfileFacts(input, options) {
757
981
  this.assertInitialized('archiveProfileFacts');
758
- return this.archiveCoordinator.archiveProfileFacts(input);
982
+ return this.requireArchiveCoordinator.archiveProfileFacts(input, options);
759
983
  }
760
984
  /**
761
985
  * 手动触发 insight 提取(manual 模式下使用)
@@ -763,15 +987,20 @@ export class Agent extends TypedEventEmitter {
763
987
  * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
764
988
  * 内部仍走 classify 判断(避免无价值输入浪费 LLM 调用)。
765
989
  *
990
+ * FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
991
+ * 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
992
+ * 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
993
+ *
766
994
  * 归档逻辑已委托给 ArchiveCoordinator
767
995
  *
768
996
  * @param input 本轮用户输入
769
997
  * @param assistantContent 本轮助手回复内容
998
+ * @param options 触发选项(autoTriggered 默认 false,即手动触发)
770
999
  * @returns 写入/更新的 Memory 列表
771
1000
  */
772
- async archiveInsight(input, assistantContent) {
1001
+ async archiveInsight(input, assistantContent, options) {
773
1002
  this.assertInitialized('archiveInsight');
774
- return this.archiveCoordinator.archiveInsight(input, assistantContent);
1003
+ return this.requireArchiveCoordinator.archiveInsight(input, assistantContent, options);
775
1004
  }
776
1005
  /**
777
1006
  * 手动归档会话内容(content 类记忆)
@@ -779,15 +1008,32 @@ export class Agent extends TypedEventEmitter {
779
1008
  * 适用于 `insights-only` / `manual` 模式下用户手动触发会话内容归档。
780
1009
  * `full` 模式下由宿主在会话切换前自动调用,无需用户干预。
781
1010
  *
1011
+ * FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
1012
+ * 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
1013
+ * 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
1014
+ *
782
1015
  * 归档逻辑已委托给 ArchiveCoordinator
783
1016
  *
784
1017
  * @param date 会话日期 YYYY-MM-DD
785
1018
  * @param session 会话标识(不含日期前缀)
1019
+ * @param options 触发选项(autoTriggered 默认 false,即手动触发)
786
1020
  * @returns 归档结果(memories 可能为空,表示无归档价值或 LLM 失败)
787
1021
  */
788
- async archiveSessionContent(date, session) {
1022
+ async archiveSessionContent(date, session, options) {
789
1023
  this.assertInitialized('archiveSessionContent');
790
- return this.archiveCoordinator.archiveSessionContent(date, session);
1024
+ return this.requireArchiveCoordinator.archiveSessionContent(date, session, options);
1025
+ }
1026
+ /**
1027
+ * 获取 pending 归档队列长度(MIND2-C3)
1028
+ *
1029
+ * 返回待重试的失败归档数。归档失败时输入入队,下次同阶段归档调用时自动重试。
1030
+ * 供宿主 UI 展示"N 条待补归档"提示,或判断是否需要手动触发重试。
1031
+ *
1032
+ * @returns pending 队列长度(0 表示无待补)
1033
+ */
1034
+ getPendingArchiveCount() {
1035
+ this.assertInitialized('getPendingArchiveCount');
1036
+ return this.requireArchiveCoordinator.getPendingArchiveCount();
791
1037
  }
792
1038
  // ─── 配置重载(事件驱动) ───────────────────────
793
1039
  /**
@@ -799,7 +1045,7 @@ export class Agent extends TypedEventEmitter {
799
1045
  * 支持的 source:
800
1046
  * - 'skill' → SkillManager.reload() 清空缓存重新扫描 skills/ 目录
801
1047
  * - 'persona' → PersonaManager.reload() 清空缓存重新扫描 personas/ 目录(保持激活角色)
802
- * - 'rule' → 无操作(rule 类型已由 ConfigManager.addRule() 即时注入 system prompt)
1048
+ * - 'rule' → 无操作(rule 类型由 ConfigManager CRUD 即时同步 bootstrap 段,详见 deleteRule/updateRule)
803
1049
  * - 'guardrail' → 抛错(guardrail 是 AgentLoop 的 readonly 数组,需 rebuildComponents 才能重载)
804
1050
  * - undefined → 重载 skill + persona(全量重载,不含 guardrail)
805
1051
  *
@@ -809,33 +1055,57 @@ export class Agent extends TypedEventEmitter {
809
1055
  async reloadConfig(source) {
810
1056
  this.assertInitialized('reloadConfig');
811
1057
  if (this.chatLockManager?.isBusy) {
1058
+ // 对话进行中无法热重载:将 source 暂存,待 chat() finally 块释放锁后补执行
1059
+ // source 为 undefined(全量重载)时不暂存——全量重载无具体来源,补执行语义不明
1060
+ if (source) {
1061
+ this.pendingConfigReload.add(source);
1062
+ logger.info({ source }, '对话进行中,配置重载已暂存,将在对话结束后补执行');
1063
+ }
812
1064
  throw chatBusyError('重载配置');
813
1065
  }
814
1066
  // guardrail 需重建 AgentLoop,不属于热重载范畴
815
1067
  if (source === 'guardrail') {
816
1068
  throw configError('guardrail 不支持热重载', 'guardrail 规则是 AgentLoop 的 readonly 数组,需调用 rebuildComponents() 重建', ['使用 rebuildComponents() 重建组件(代价较高)']);
817
1069
  }
818
- // rule 类型已由 ConfigManager.addRule() 即时注入,无需重载
1070
+ // rule 类型由 ConfigManager.deleteRule/updateRule CRUD 即时同步 bootstrap 段(refreshBootstrapMemories 回调),无需 reloadConfig
819
1071
  if (source === 'rule') {
820
- logger.info('rule 类型已由 addRule() 即时注入,reloadConfig 跳过');
1072
+ logger.info('rule 类型由 ConfigManager CRUD 即时同步 bootstrap 段,reloadConfig 跳过');
821
1073
  return { skill: 0, persona: 0 };
822
1074
  }
823
1075
  const result = { skill: 0, persona: 0 };
1076
+ const errors = [];
824
1077
  // 按需重载:source 缺省时全量重载,否则只重载指定类型
825
1078
  const shouldReloadSkill = !source || source === 'skill';
826
1079
  const shouldReloadPersona = !source || source === 'persona';
827
1080
  if (shouldReloadSkill && this.skillManager) {
828
- result.skill = await this.skillManager.reload();
1081
+ try {
1082
+ result.skill = await this.skillManager.reload();
1083
+ }
1084
+ catch (err) {
1085
+ errors.push(toError(err));
1086
+ logger.warn({ err: toError(err) }, 'reloadConfig: skillManager.reload 失败');
1087
+ }
829
1088
  }
830
1089
  if (shouldReloadPersona && this.personaManager) {
831
- result.persona = await this.personaManager.reload();
832
- // 角色重载后,刷新 AgentLoop 的角色前缀(使新角色内容立即注入 system prompt)
833
- if (this.loop) {
834
- const newPrefix = this.personaManager.buildSystemPrompt();
835
- this.loop.refreshPersonaPrefix(newPrefix);
1090
+ try {
1091
+ result.persona = await this.personaManager.reload();
1092
+ // 角色重载后,刷新 AgentLoop 的角色前缀(使新角色内容立即注入 system prompt)
1093
+ if (this.loop) {
1094
+ const newPrefix = this.personaManager.buildSystemPrompt();
1095
+ this.loop.refreshPersonaPrefix(newPrefix);
1096
+ }
836
1097
  }
1098
+ catch (err) {
1099
+ errors.push(toError(err));
1100
+ logger.warn({ err: toError(err) }, 'reloadConfig: personaManager.reload 失败');
1101
+ }
1102
+ }
1103
+ if (errors.length > 0) {
1104
+ logger.warn({ source, ...result, errorCount: errors.length }, '配置热重载部分失败');
1105
+ }
1106
+ else {
1107
+ logger.info({ source, ...result }, '配置已热重载');
837
1108
  }
838
- logger.info({ source, ...result }, '配置已热重载');
839
1109
  return result;
840
1110
  }
841
1111
  // ─── 组件访问 ─────────────────────────────────────────
@@ -847,9 +1117,7 @@ export class Agent extends TypedEventEmitter {
847
1117
  */
848
1118
  async rebuildComponents() {
849
1119
  // 对话进行中重建组件会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
850
- if (this.chatLockManager?.isBusy) {
851
- throw chatBusyError('重建组件');
852
- }
1120
+ this.assertNotBusy('重建组件');
853
1121
  await this.rebuildComponentsWithCurrentCtx();
854
1122
  }
855
1123
  // ─── 守卫方法 ───────────────────────────────────────────
@@ -879,6 +1147,57 @@ export class Agent extends TypedEventEmitter {
879
1147
  }
880
1148
  }
881
1149
  }
1150
+ /**
1151
+ * 断言对话未进行中 — 统一守卫,避免在 chat() 进行中执行会破坏状态一致性的操作
1152
+ *
1153
+ * 提取原因:switchProject / setProvider / setBackgroundProvider / setArchiveMode /
1154
+ * switchPersona / rebuildComponents 等 6 处方法均有相同的 `if (isBusy) throw chatBusyError('XXX')` 守卫,
1155
+ * 违反 DRY 原则(v2 神木回天 REPEAT-4 闭环)。
1156
+ *
1157
+ * 注意:reloadConfig 不使用本方法——它在 isBusy 时需暂存 source 而非直接抛错。
1158
+ *
1159
+ * @param operation 操作名称(用于错误消息,如 "切换项目")
1160
+ */
1161
+ assertNotBusy(operation) {
1162
+ if (this.chatLockManager?.isBusy) {
1163
+ throw chatBusyError(operation);
1164
+ }
1165
+ }
1166
+ /**
1167
+ * 断言 getter:获取 loop,若 null 则抛出明确错误
1168
+ * 替代 `this.loop!` 非空断言,提供更好的重构安全性
1169
+ */
1170
+ get requireLoop() {
1171
+ if (!this.loop)
1172
+ throw configError('AgentLoop 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1173
+ return this.loop;
1174
+ }
1175
+ /**
1176
+ * 断言 getter:获取 history,若 null 则抛出明确错误
1177
+ */
1178
+ get requireHistory() {
1179
+ if (!this.history)
1180
+ throw configError('MessageHistory 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1181
+ return this.history;
1182
+ }
1183
+ /**
1184
+ * 断言 getter:获取 pctx,若 null 则抛出明确错误
1185
+ */
1186
+ get requirePctx() {
1187
+ if (!this.pctx)
1188
+ throw configError('ProjectContext 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1189
+ return this.pctx;
1190
+ }
1191
+ get requireArchiveCoordinator() {
1192
+ if (!this.archiveCoordinator)
1193
+ throw configError('ArchiveCoordinator 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1194
+ return this.archiveCoordinator;
1195
+ }
1196
+ get requireSessionManager() {
1197
+ if (!this._sessionManager)
1198
+ throw configError('SessionManager 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1199
+ return this._sessionManager;
1200
+ }
882
1201
  // ─── 记忆生命周期 ───────────────────────────────────────
883
1202
  // ─── runMemoryDecay 已迁移至 MemoryDecayScheduler.runOnce ─────
884
1203
  // ─── 关闭 ─────────────────────────────────────────────
@@ -891,17 +1210,29 @@ export class Agent extends TypedEventEmitter {
891
1210
  this.chatLockManager?.dispose();
892
1211
  this.chatLockManager = null;
893
1212
  // 清理 MemoryDecayScheduler(含定时器和 storage 引用)
1213
+ // FIX-P0-1:先 stop() abort L2 评估的 LLM 调用,再 awaitInflight() 等待 Promise 完成,
1214
+ // 防止 close 后 LLM 回调 upsert 已关闭的 storage
894
1215
  if (this.memoryDecayScheduler) {
895
1216
  this.memoryDecayScheduler.stop();
1217
+ try {
1218
+ await this.memoryDecayScheduler.awaitInflight();
1219
+ }
1220
+ catch (err) {
1221
+ logger.warn({ err: toError(err) }, 'close: memoryDecayScheduler.awaitInflight 失败');
1222
+ }
896
1223
  this.memoryDecayScheduler = null;
897
1224
  }
1225
+ // FIX-P0-1:等待 WorkProjection 的 inflight LLM 生成完成,防止 close 后 upsert 已关闭的 storage
1226
+ if (this.workProjection) {
1227
+ try {
1228
+ await this.workProjection.awaitInflight();
1229
+ }
1230
+ catch (err) {
1231
+ logger.warn({ err: toError(err) }, 'close: workProjection.awaitInflight 失败');
1232
+ }
1233
+ }
898
1234
  // 清理 ArchiveCoordinator(无定时器,只需释放引用)
899
1235
  this.archiveCoordinator = null;
900
- // 清理定时器
901
- if (this.decayTimer) {
902
- clearSafeInterval(this.decayTimer);
903
- this.decayTimer = null;
904
- }
905
1236
  // 清理 PersonaManager 的角色切换防抖锁计时器,防止关闭后回调触发
906
1237
  if (this.personaManager) {
907
1238
  this.personaManager.close();
@@ -910,18 +1241,28 @@ export class Agent extends TypedEventEmitter {
910
1241
  // 顺序敏感:若先 removeAllListeners,fire-and-forget 的归档 Promise 在 await 期间
911
1242
  // reject 时 emit 会变成 no-op(listeners 已清空),archiveFailed 事件丢失
912
1243
  if (this.history) {
913
- await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
1244
+ try {
1245
+ await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
1246
+ }
1247
+ catch (err) {
1248
+ logger.warn({ err: toError(err) }, 'close: awaitPendingArchives 失败');
1249
+ }
914
1250
  }
915
1251
  // pending archives 完成后再移除监听器,确保归档失败的 emit 能送达
916
1252
  this.removeAllListeners();
917
1253
  if (this.projectManager) {
918
- await this.projectManager.shutdown();
1254
+ try {
1255
+ await this.projectManager.shutdown();
1256
+ }
1257
+ catch (err) {
1258
+ logger.warn({ err: toError(err) }, 'close: projectManager.shutdown 失败');
1259
+ }
919
1260
  }
920
1261
  this._initialized = false;
921
1262
  this.nullifyAllComponents();
922
1263
  // 清理次要状态字段,防止 re-init 后残留上一会话状态
923
- this.activeSkill = null;
924
1264
  this._lastInteractionAt = null;
1265
+ this.pendingConfigReload.clear();
925
1266
  }
926
1267
  /**
927
1268
  * 统一 null 化所有组件字段(新增 Manager 时在此处追加一行)
@@ -943,6 +1284,9 @@ export class Agent extends TypedEventEmitter {
943
1284
  this.insightExtractor = null;
944
1285
  this.configManager = null;
945
1286
  this.memoryInspector = null;
1287
+ this.dedupManager = null;
1288
+ this.memoryAdvisor = null;
1289
+ this._governance = null;
946
1290
  this.workProjection = null;
947
1291
  this.autoConfigRefiner = null;
948
1292
  this.sessionArchiver = null;
@@ -959,9 +1303,30 @@ export class Agent extends TypedEventEmitter {
959
1303
  get context() {
960
1304
  return this.pctx;
961
1305
  }
1306
+ /**
1307
+ * @deprecated 直接暴露 AgentLoop 破坏封装,将在下一主版本移除。
1308
+ * 使用 getMessages() / getMessageCount() 等有界接口替代。
1309
+ */
962
1310
  get agentLoop() {
963
1311
  return this.loop;
964
1312
  }
1313
+ /**
1314
+ * 获取当前消息列表(只读副本,不可绕过 Agent 编排链路直接修改)
1315
+ */
1316
+ getMessages() {
1317
+ return this.loop?.getMessages() ?? [];
1318
+ }
1319
+ /** 获取当前消息数量 */
1320
+ getMessageCount() {
1321
+ return this.loop?.getMessages().length ?? 0;
1322
+ }
1323
+ /**
1324
+ * @deprecated 直接暴露 MessageHistory 破坏封装,将在下一主版本移除。
1325
+ * 使用 Agent 的对话管理方法(chat/chatSync/forkSession)替代直接操作。
1326
+ */
1327
+ get agentHistory() {
1328
+ return this.history;
1329
+ }
965
1330
  /**
966
1331
  * 注入情感基调到 system prompt(Phase 2.1:AffectController)
967
1332
  *
@@ -971,10 +1336,9 @@ export class Agent extends TypedEventEmitter {
971
1336
  * @param affectString 情感描述文本,传空字符串清除注入
972
1337
  */
973
1338
  injectAffect(affectString) {
974
- this.loop?.injectAffect(affectString);
975
- }
976
- get agentHistory() {
977
- return this.history;
1339
+ if (!this.loop)
1340
+ throw configError('AgentLoop', 'AgentLoop 未初始化,无法注入 affect', []);
1341
+ this.loop.injectAffect(affectString);
978
1342
  }
979
1343
  get provider() {
980
1344
  return this.#provider;
@@ -985,6 +1349,20 @@ export class Agent extends TypedEventEmitter {
985
1349
  get lastInteractionAt() {
986
1350
  return this._lastInteractionAt;
987
1351
  }
1352
+ /**
1353
+ * 记忆治理统一门面(L0 衰减 / L1 去重 / L2 时效性 / L3 冲突 / 诊断 / 推荐)
1354
+ *
1355
+ * 统一入口替代散落在 Agent 上的 6 个独立方法:
1356
+ * agent.governance.decay() → 原 runMemoryDecayOnce()
1357
+ * agent.governance.deduplicate() → 原 deduplicateMemories()
1358
+ * agent.governance.evaluateTimeliness() → 原 evaluateTimeliness()
1359
+ * agent.governance.detectConflicts() → 原 detectConflicts()
1360
+ * agent.governance.sourceHealth() → 原 sourceHealth()
1361
+ * agent.governance.suggest() → 原 suggest()
1362
+ */
1363
+ get governance() {
1364
+ return this._governance;
1365
+ }
988
1366
  // ─── 运行时指标 ────────────────────────────────
989
1367
  /**
990
1368
  * 获取 Agent 运行时指标快照(可观测性增强)
@@ -1083,92 +1461,6 @@ export class Agent extends TypedEventEmitter {
1083
1461
  get memory() {
1084
1462
  return this.memoryInspector;
1085
1463
  }
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
- }
1172
1464
  /**
1173
1465
  * 项目管理器(可能为 null)
1174
1466
  *