@zooique/memora 2.0.0 → 3.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 (588) hide show
  1. package/CHANGELOG.md +1028 -0
  2. package/README.en.md +305 -0
  3. package/README.md +303 -148
  4. package/config.example.json +43 -0
  5. package/dist/agent/agent.d.ts +438 -295
  6. package/dist/agent/agent.d.ts.map +1 -1
  7. package/dist/agent/agent.js +1368 -836
  8. package/dist/agent/agent.js.map +1 -1
  9. package/dist/agent/assembler.d.ts +117 -39
  10. package/dist/agent/assembler.d.ts.map +1 -1
  11. package/dist/agent/assembler.js +574 -71
  12. package/dist/agent/assembler.js.map +1 -1
  13. package/dist/agent/budget.d.ts +131 -0
  14. package/dist/agent/budget.d.ts.map +1 -0
  15. package/dist/agent/budget.js +128 -0
  16. package/dist/agent/budget.js.map +1 -0
  17. package/dist/agent/builtinToolHandlers.d.ts +229 -17
  18. package/dist/agent/builtinToolHandlers.d.ts.map +1 -1
  19. package/dist/agent/builtinToolHandlers.js +758 -70
  20. package/dist/agent/builtinToolHandlers.js.map +1 -1
  21. package/dist/agent/builtinTools.d.ts +198 -7
  22. package/dist/agent/builtinTools.d.ts.map +1 -1
  23. package/dist/agent/builtinTools.js +646 -14
  24. package/dist/agent/builtinTools.js.map +1 -1
  25. package/dist/agent/compaction.d.ts +93 -0
  26. package/dist/agent/compaction.d.ts.map +1 -0
  27. package/dist/agent/compaction.js +192 -0
  28. package/dist/agent/compaction.js.map +1 -0
  29. package/dist/agent/constants.d.ts +139 -18
  30. package/dist/agent/constants.d.ts.map +1 -1
  31. package/dist/agent/constants.js +154 -18
  32. package/dist/agent/constants.js.map +1 -1
  33. package/dist/agent/contextManager.d.ts +126 -76
  34. package/dist/agent/contextManager.d.ts.map +1 -1
  35. package/dist/agent/contextManager.js +197 -135
  36. package/dist/agent/contextManager.js.map +1 -1
  37. package/dist/agent/contextPreparer.d.ts +78 -0
  38. package/dist/agent/contextPreparer.d.ts.map +1 -0
  39. package/dist/agent/contextPreparer.js +128 -0
  40. package/dist/agent/contextPreparer.js.map +1 -0
  41. package/dist/agent/duplicateInterceptor.d.ts +67 -0
  42. package/dist/agent/duplicateInterceptor.d.ts.map +1 -0
  43. package/dist/agent/duplicateInterceptor.js +82 -0
  44. package/dist/agent/duplicateInterceptor.js.map +1 -0
  45. package/dist/agent/guardRail.d.ts +166 -0
  46. package/dist/agent/guardRail.d.ts.map +1 -0
  47. package/dist/agent/guardRail.js +333 -0
  48. package/dist/agent/guardRail.js.map +1 -0
  49. package/dist/agent/loop.d.ts +718 -253
  50. package/dist/agent/loop.d.ts.map +1 -1
  51. package/dist/agent/loop.js +2215 -648
  52. package/dist/agent/loop.js.map +1 -1
  53. package/dist/agent/managers/archiveCoordinator.d.ts +21 -74
  54. package/dist/agent/managers/archiveCoordinator.d.ts.map +1 -1
  55. package/dist/agent/managers/archiveCoordinator.js +32 -105
  56. package/dist/agent/managers/archiveCoordinator.js.map +1 -1
  57. package/dist/agent/managers/chatLockManager.d.ts +20 -88
  58. package/dist/agent/managers/chatLockManager.d.ts.map +1 -1
  59. package/dist/agent/managers/chatLockManager.js +31 -103
  60. package/dist/agent/managers/chatLockManager.js.map +1 -1
  61. package/dist/agent/managers/dedupManager.d.ts +85 -0
  62. package/dist/agent/managers/dedupManager.d.ts.map +1 -0
  63. package/dist/agent/managers/dedupManager.js +248 -0
  64. package/dist/agent/managers/dedupManager.js.map +1 -0
  65. package/dist/agent/managers/goalConsistencyChecker.d.ts +42 -0
  66. package/dist/agent/managers/goalConsistencyChecker.d.ts.map +1 -0
  67. package/dist/agent/managers/goalConsistencyChecker.js +178 -0
  68. package/dist/agent/managers/goalConsistencyChecker.js.map +1 -0
  69. package/dist/agent/managers/llmCaller.d.ts +124 -0
  70. package/dist/agent/managers/llmCaller.d.ts.map +1 -0
  71. package/dist/agent/managers/llmCaller.js +417 -0
  72. package/dist/agent/managers/llmCaller.js.map +1 -0
  73. package/dist/agent/managers/llmJudgeHelper.d.ts +21 -0
  74. package/dist/agent/managers/llmJudgeHelper.d.ts.map +1 -0
  75. package/dist/agent/managers/llmJudgeHelper.js +32 -0
  76. package/dist/agent/managers/llmJudgeHelper.js.map +1 -0
  77. package/dist/agent/managers/loopMetrics.d.ts +58 -0
  78. package/dist/agent/managers/loopMetrics.d.ts.map +1 -0
  79. package/dist/agent/managers/loopMetrics.js +68 -0
  80. package/dist/agent/managers/loopMetrics.js.map +1 -0
  81. package/dist/agent/managers/memoryAdvisor.d.ts +27 -120
  82. package/dist/agent/managers/memoryAdvisor.d.ts.map +1 -1
  83. package/dist/agent/managers/memoryAdvisor.js +56 -192
  84. package/dist/agent/managers/memoryAdvisor.js.map +1 -1
  85. package/dist/agent/managers/memoryGovernance.d.ts +24 -0
  86. package/dist/agent/managers/memoryGovernance.d.ts.map +1 -0
  87. package/dist/agent/managers/memoryGovernance.js +46 -0
  88. package/dist/agent/managers/memoryGovernance.js.map +1 -0
  89. package/dist/agent/managers/memoryInspector.d.ts +59 -393
  90. package/dist/agent/managers/memoryInspector.d.ts.map +1 -1
  91. package/dist/agent/managers/memoryInspector.js +92 -736
  92. package/dist/agent/managers/memoryInspector.js.map +1 -1
  93. package/dist/agent/managers/roundSummaryGenerator.d.ts +56 -0
  94. package/dist/agent/managers/roundSummaryGenerator.d.ts.map +1 -0
  95. package/dist/agent/managers/roundSummaryGenerator.js +201 -0
  96. package/dist/agent/managers/roundSummaryGenerator.js.map +1 -0
  97. package/dist/agent/managers/sessionArchiver.d.ts +19 -63
  98. package/dist/agent/managers/sessionArchiver.d.ts.map +1 -1
  99. package/dist/agent/managers/sessionArchiver.js +83 -157
  100. package/dist/agent/managers/sessionArchiver.js.map +1 -1
  101. package/dist/agent/managers/sessionManager.d.ts +286 -57
  102. package/dist/agent/managers/sessionManager.d.ts.map +1 -1
  103. package/dist/agent/managers/sessionManager.js +846 -85
  104. package/dist/agent/managers/sessionManager.js.map +1 -1
  105. package/dist/agent/managers/sessionNamer.d.ts +35 -0
  106. package/dist/agent/managers/sessionNamer.d.ts.map +1 -0
  107. package/dist/agent/managers/sessionNamer.js +104 -0
  108. package/dist/agent/managers/sessionNamer.js.map +1 -0
  109. package/dist/agent/managers/streamAccumulator.d.ts +20 -0
  110. package/dist/agent/managers/streamAccumulator.d.ts.map +1 -0
  111. package/dist/agent/managers/streamAccumulator.js +15 -0
  112. package/dist/agent/managers/streamAccumulator.js.map +1 -0
  113. package/dist/agent/managers/textPolishManager.d.ts +8 -44
  114. package/dist/agent/managers/textPolishManager.d.ts.map +1 -1
  115. package/dist/agent/managers/textPolishManager.js +12 -47
  116. package/dist/agent/managers/textPolishManager.js.map +1 -1
  117. package/dist/agent/managers/toolCallHelpers.d.ts +105 -0
  118. package/dist/agent/managers/toolCallHelpers.d.ts.map +1 -0
  119. package/dist/agent/managers/toolCallHelpers.js +161 -0
  120. package/dist/agent/managers/toolCallHelpers.js.map +1 -0
  121. package/dist/agent/managers/workProjection.d.ts +57 -87
  122. package/dist/agent/managers/workProjection.d.ts.map +1 -1
  123. package/dist/agent/managers/workProjection.js +166 -246
  124. package/dist/agent/managers/workProjection.js.map +1 -1
  125. package/dist/agent/messageHistory.d.ts +134 -84
  126. package/dist/agent/messageHistory.d.ts.map +1 -1
  127. package/dist/agent/messageHistory.js +419 -133
  128. package/dist/agent/messageHistory.js.map +1 -1
  129. package/dist/agent/needsPlanning.d.ts +28 -0
  130. package/dist/agent/needsPlanning.d.ts.map +1 -0
  131. package/dist/agent/needsPlanning.js +68 -0
  132. package/dist/agent/needsPlanning.js.map +1 -0
  133. package/dist/agent/seed/index.d.ts +11 -0
  134. package/dist/agent/seed/index.d.ts.map +1 -0
  135. package/dist/agent/seed/index.js +10 -0
  136. package/dist/agent/seed/index.js.map +1 -0
  137. package/dist/agent/seed/orchestrator.d.ts +83 -0
  138. package/dist/agent/seed/orchestrator.d.ts.map +1 -0
  139. package/dist/agent/seed/orchestrator.js +260 -0
  140. package/dist/agent/seed/orchestrator.js.map +1 -0
  141. package/dist/agent/seed/prepare.d.ts +49 -0
  142. package/dist/agent/seed/prepare.d.ts.map +1 -0
  143. package/dist/agent/seed/prepare.js +107 -0
  144. package/dist/agent/seed/prepare.js.map +1 -0
  145. package/dist/agent/seed/types.d.ts +115 -0
  146. package/dist/agent/seed/types.d.ts.map +1 -0
  147. package/dist/agent/seed/types.js +15 -0
  148. package/dist/agent/seed/types.js.map +1 -0
  149. package/dist/agent/sessionStateMachine.d.ts +78 -0
  150. package/dist/agent/sessionStateMachine.d.ts.map +1 -0
  151. package/dist/agent/sessionStateMachine.js +182 -0
  152. package/dist/agent/sessionStateMachine.js.map +1 -0
  153. package/dist/agent/taskTableRenderer.d.ts +51 -0
  154. package/dist/agent/taskTableRenderer.d.ts.map +1 -0
  155. package/dist/agent/taskTableRenderer.js +115 -0
  156. package/dist/agent/taskTableRenderer.js.map +1 -0
  157. package/dist/agent/toolExecutor.d.ts +255 -9
  158. package/dist/agent/toolExecutor.d.ts.map +1 -1
  159. package/dist/agent/toolExecutor.js +956 -20
  160. package/dist/agent/toolExecutor.js.map +1 -1
  161. package/dist/agent/toolLedger.d.ts +120 -0
  162. package/dist/agent/toolLedger.d.ts.map +1 -0
  163. package/dist/agent/toolLedger.js +149 -0
  164. package/dist/agent/toolLedger.js.map +1 -0
  165. package/dist/agent/toolResultCache.d.ts +134 -0
  166. package/dist/agent/toolResultCache.d.ts.map +1 -0
  167. package/dist/agent/toolResultCache.js +243 -0
  168. package/dist/agent/toolResultCache.js.map +1 -0
  169. package/dist/agent/toolResultOffload.d.ts +59 -0
  170. package/dist/agent/toolResultOffload.d.ts.map +1 -0
  171. package/dist/agent/toolResultOffload.js +70 -0
  172. package/dist/agent/toolResultOffload.js.map +1 -0
  173. package/dist/agent/toolRunner.d.ts +59 -0
  174. package/dist/agent/toolRunner.d.ts.map +1 -0
  175. package/dist/agent/toolRunner.js +147 -0
  176. package/dist/agent/toolRunner.js.map +1 -0
  177. package/dist/agent/tracer.d.ts +99 -107
  178. package/dist/agent/tracer.d.ts.map +1 -1
  179. package/dist/agent/tracer.js +24 -69
  180. package/dist/agent/tracer.js.map +1 -1
  181. package/dist/agent/types.d.ts +512 -162
  182. package/dist/agent/types.d.ts.map +1 -1
  183. package/dist/agent/types.js +2 -51
  184. package/dist/agent/types.js.map +1 -1
  185. package/dist/code-exec/codeExecutionProvider.d.ts +16 -0
  186. package/dist/code-exec/codeExecutionProvider.d.ts.map +1 -0
  187. package/dist/code-exec/codeExecutionProvider.js +57 -0
  188. package/dist/code-exec/codeExecutionProvider.js.map +1 -0
  189. package/dist/code-exec/types.d.ts +37 -0
  190. package/dist/code-exec/types.d.ts.map +1 -0
  191. package/dist/code-exec/types.js +12 -0
  192. package/dist/code-exec/types.js.map +1 -0
  193. package/dist/config/expandEnvVars.d.ts +22 -0
  194. package/dist/config/expandEnvVars.d.ts.map +1 -0
  195. package/dist/config/expandEnvVars.js +38 -0
  196. package/dist/config/expandEnvVars.js.map +1 -0
  197. package/dist/config/loader.d.ts +59 -242
  198. package/dist/config/loader.d.ts.map +1 -1
  199. package/dist/config/loader.js +237 -194
  200. package/dist/config/loader.js.map +1 -1
  201. package/dist/index.d.ts +69 -74
  202. package/dist/index.d.ts.map +1 -1
  203. package/dist/index.js +93 -63
  204. package/dist/index.js.map +1 -1
  205. package/dist/llm/abortSignal.d.ts +2 -3
  206. package/dist/llm/abortSignal.d.ts.map +1 -1
  207. package/dist/llm/abortSignal.js +2 -3
  208. package/dist/llm/abortSignal.js.map +1 -1
  209. package/dist/llm/factory.d.ts +29 -25
  210. package/dist/llm/factory.d.ts.map +1 -1
  211. package/dist/llm/factory.js +34 -69
  212. package/dist/llm/factory.js.map +1 -1
  213. package/dist/llm/openaiCompatible.d.ts +25 -40
  214. package/dist/llm/openaiCompatible.d.ts.map +1 -1
  215. package/dist/llm/openaiCompatible.js +181 -129
  216. package/dist/llm/openaiCompatible.js.map +1 -1
  217. package/dist/llm/provider.d.ts +29 -42
  218. package/dist/llm/provider.d.ts.map +1 -1
  219. package/dist/llm/provider.js +11 -10
  220. package/dist/llm/provider.js.map +1 -1
  221. package/dist/llm/types.d.ts +54 -11
  222. package/dist/llm/types.d.ts.map +1 -1
  223. package/dist/llm/types.js +0 -5
  224. package/dist/llm/types.js.map +1 -1
  225. package/dist/logging/logger.d.ts +8 -29
  226. package/dist/logging/logger.d.ts.map +1 -1
  227. package/dist/logging/logger.js +43 -233
  228. package/dist/logging/logger.js.map +1 -1
  229. package/dist/logging/loggerInterface.d.ts +9 -8
  230. package/dist/logging/loggerInterface.d.ts.map +1 -1
  231. package/dist/memory/gcService.d.ts +164 -0
  232. package/dist/memory/gcService.d.ts.map +1 -0
  233. package/dist/memory/gcService.js +215 -0
  234. package/dist/memory/gcService.js.map +1 -0
  235. package/dist/memory/governance.d.ts +17 -0
  236. package/dist/memory/governance.d.ts.map +1 -0
  237. package/dist/memory/governance.js +17 -0
  238. package/dist/memory/governance.js.map +1 -0
  239. package/dist/memory/inMemoryRoundStore.d.ts +124 -0
  240. package/dist/memory/inMemoryRoundStore.d.ts.map +1 -0
  241. package/dist/memory/inMemoryRoundStore.js +216 -0
  242. package/dist/memory/inMemoryRoundStore.js.map +1 -0
  243. package/dist/memory/inMemorySessionStore.d.ts +92 -0
  244. package/dist/memory/inMemorySessionStore.d.ts.map +1 -0
  245. package/dist/memory/inMemorySessionStore.js +219 -0
  246. package/dist/memory/inMemorySessionStore.js.map +1 -0
  247. package/dist/memory/inMemorySessionViewLoader.d.ts +89 -0
  248. package/dist/memory/inMemorySessionViewLoader.d.ts.map +1 -0
  249. package/dist/memory/inMemorySessionViewLoader.js +183 -0
  250. package/dist/memory/inMemorySessionViewLoader.js.map +1 -0
  251. package/dist/memory/inMemoryStorage.d.ts +26 -141
  252. package/dist/memory/inMemoryStorage.d.ts.map +1 -1
  253. package/dist/memory/inMemoryStorage.js +48 -187
  254. package/dist/memory/inMemoryStorage.js.map +1 -1
  255. package/dist/memory/keywordsTouch.d.ts +20 -0
  256. package/dist/memory/keywordsTouch.d.ts.map +1 -0
  257. package/dist/memory/keywordsTouch.js +25 -0
  258. package/dist/memory/keywordsTouch.js.map +1 -0
  259. package/dist/memory/lockManager.d.ts +7 -2
  260. package/dist/memory/lockManager.d.ts.map +1 -1
  261. package/dist/memory/lockManager.js +35 -11
  262. package/dist/memory/lockManager.js.map +1 -1
  263. package/dist/memory/projectManager.d.ts +30 -128
  264. package/dist/memory/projectManager.d.ts.map +1 -1
  265. package/dist/memory/projectManager.js +39 -171
  266. package/dist/memory/projectManager.js.map +1 -1
  267. package/dist/memory/projectRegistry.d.ts +22 -37
  268. package/dist/memory/projectRegistry.d.ts.map +1 -1
  269. package/dist/memory/projectRegistry.js +78 -85
  270. package/dist/memory/projectRegistry.js.map +1 -1
  271. package/dist/memory/roundStore.d.ts +629 -0
  272. package/dist/memory/roundStore.d.ts.map +1 -0
  273. package/dist/memory/roundStore.js +116 -0
  274. package/dist/memory/roundStore.js.map +1 -0
  275. package/dist/memory/sessionStore.d.ts +70 -61
  276. package/dist/memory/sessionStore.d.ts.map +1 -1
  277. package/dist/memory/sessionStore.js +23 -1
  278. package/dist/memory/sessionStore.js.map +1 -1
  279. package/dist/memory/sessionViewLoader.d.ts +141 -0
  280. package/dist/memory/sessionViewLoader.d.ts.map +1 -0
  281. package/dist/memory/sessionViewLoader.js +65 -0
  282. package/dist/memory/sessionViewLoader.js.map +1 -0
  283. package/dist/memory/sourceValidation.d.ts +18 -9
  284. package/dist/memory/sourceValidation.d.ts.map +1 -1
  285. package/dist/memory/sourceValidation.js +35 -29
  286. package/dist/memory/sourceValidation.js.map +1 -1
  287. package/dist/memory/storageInterface.d.ts +45 -126
  288. package/dist/memory/storageInterface.d.ts.map +1 -1
  289. package/dist/memory/types.d.ts +82 -168
  290. package/dist/memory/types.d.ts.map +1 -1
  291. package/dist/memory/types.js +116 -79
  292. package/dist/memory/types.js.map +1 -1
  293. package/dist/project-search/projectSearchProvider.d.ts +29 -0
  294. package/dist/project-search/projectSearchProvider.d.ts.map +1 -0
  295. package/dist/project-search/projectSearchProvider.js +57 -0
  296. package/dist/project-search/projectSearchProvider.js.map +1 -0
  297. package/dist/project-search/terms.d.ts +14 -0
  298. package/dist/project-search/terms.d.ts.map +1 -0
  299. package/dist/project-search/terms.js +68 -0
  300. package/dist/project-search/terms.js.map +1 -0
  301. package/dist/project-search/types.d.ts +139 -0
  302. package/dist/project-search/types.d.ts.map +1 -0
  303. package/dist/project-search/types.js +8 -0
  304. package/dist/project-search/types.js.map +1 -0
  305. package/dist/role-pack/capabilityMap.d.ts +28 -0
  306. package/dist/role-pack/capabilityMap.d.ts.map +1 -0
  307. package/dist/role-pack/capabilityMap.js +56 -0
  308. package/dist/role-pack/capabilityMap.js.map +1 -0
  309. package/dist/role-pack/constants.d.ts +21 -0
  310. package/dist/role-pack/constants.d.ts.map +1 -0
  311. package/dist/role-pack/constants.js +21 -0
  312. package/dist/role-pack/constants.js.map +1 -0
  313. package/dist/role-pack/rolePackManager.d.ts +269 -0
  314. package/dist/role-pack/rolePackManager.d.ts.map +1 -0
  315. package/dist/role-pack/rolePackManager.js +1082 -0
  316. package/dist/role-pack/rolePackManager.js.map +1 -0
  317. package/dist/role-pack/strategyKeys.d.ts +100 -0
  318. package/dist/role-pack/strategyKeys.d.ts.map +1 -0
  319. package/dist/role-pack/strategyKeys.js +184 -0
  320. package/dist/role-pack/strategyKeys.js.map +1 -0
  321. package/dist/role-pack/strategyResolver.d.ts +109 -0
  322. package/dist/role-pack/strategyResolver.d.ts.map +1 -0
  323. package/dist/role-pack/strategyResolver.js +304 -0
  324. package/dist/role-pack/strategyResolver.js.map +1 -0
  325. package/dist/role-pack/types.d.ts +237 -0
  326. package/dist/role-pack/types.d.ts.map +1 -0
  327. package/dist/role-pack/types.js +8 -0
  328. package/dist/role-pack/types.js.map +1 -0
  329. package/dist/role-pack/validator.d.ts +38 -0
  330. package/dist/role-pack/validator.d.ts.map +1 -0
  331. package/dist/role-pack/validator.js +436 -0
  332. package/dist/role-pack/validator.js.map +1 -0
  333. package/dist/security/pathGuard.d.ts +69 -72
  334. package/dist/security/pathGuard.d.ts.map +1 -1
  335. package/dist/security/pathGuard.js +151 -131
  336. package/dist/security/pathGuard.js.map +1 -1
  337. package/dist/skill/skillLayer3.d.ts +91 -0
  338. package/dist/skill/skillLayer3.d.ts.map +1 -0
  339. package/dist/skill/skillLayer3.js +104 -0
  340. package/dist/skill/skillLayer3.js.map +1 -0
  341. package/dist/skill/skillManager.d.ts +127 -56
  342. package/dist/skill/skillManager.d.ts.map +1 -1
  343. package/dist/skill/skillManager.js +318 -163
  344. package/dist/skill/skillManager.js.map +1 -1
  345. package/dist/skill/skillScriptRunner.d.ts +97 -0
  346. package/dist/skill/skillScriptRunner.d.ts.map +1 -0
  347. package/dist/skill/skillScriptRunner.js +271 -0
  348. package/dist/skill/skillScriptRunner.js.map +1 -0
  349. package/dist/skill/types.d.ts +73 -17
  350. package/dist/skill/types.d.ts.map +1 -1
  351. package/dist/skill/types.js +5 -1
  352. package/dist/skill/types.js.map +1 -1
  353. package/dist/utils/array.d.ts +5 -6
  354. package/dist/utils/array.d.ts.map +1 -1
  355. package/dist/utils/array.js +6 -6
  356. package/dist/utils/array.js.map +1 -1
  357. package/dist/utils/atomicWrite.d.ts +8 -0
  358. package/dist/utils/atomicWrite.d.ts.map +1 -0
  359. package/dist/utils/atomicWrite.js +31 -0
  360. package/dist/utils/atomicWrite.js.map +1 -0
  361. package/dist/utils/backgroundTask.d.ts +38 -0
  362. package/dist/utils/backgroundTask.d.ts.map +1 -0
  363. package/dist/utils/backgroundTask.js +117 -0
  364. package/dist/utils/backgroundTask.js.map +1 -0
  365. package/dist/utils/configResourceManager.d.ts +79 -0
  366. package/dist/utils/configResourceManager.d.ts.map +1 -0
  367. package/dist/utils/configResourceManager.js +150 -0
  368. package/dist/utils/configResourceManager.js.map +1 -0
  369. package/dist/utils/errors.d.ts +48 -56
  370. package/dist/utils/errors.d.ts.map +1 -1
  371. package/dist/utils/errors.js +60 -58
  372. package/dist/utils/errors.js.map +1 -1
  373. package/dist/utils/eventEmitter.d.ts +141 -63
  374. package/dist/utils/eventEmitter.d.ts.map +1 -1
  375. package/dist/utils/eventEmitter.js +50 -35
  376. package/dist/utils/eventEmitter.js.map +1 -1
  377. package/dist/utils/fileSafe.d.ts +20 -0
  378. package/dist/utils/fileSafe.d.ts.map +1 -0
  379. package/dist/utils/fileSafe.js +56 -0
  380. package/dist/utils/fileSafe.js.map +1 -0
  381. package/dist/utils/frontmatter.d.ts +2 -3
  382. package/dist/utils/frontmatter.d.ts.map +1 -1
  383. package/dist/utils/frontmatter.js +6 -5
  384. package/dist/utils/frontmatter.js.map +1 -1
  385. package/dist/utils/hash.d.ts +12 -0
  386. package/dist/utils/hash.d.ts.map +1 -0
  387. package/dist/utils/hash.js +27 -0
  388. package/dist/utils/hash.js.map +1 -0
  389. package/dist/utils/http.d.ts +17 -0
  390. package/dist/utils/http.d.ts.map +1 -0
  391. package/dist/utils/http.js +29 -0
  392. package/dist/utils/http.js.map +1 -0
  393. package/dist/utils/json.d.ts +1 -1
  394. package/dist/utils/json.js +1 -1
  395. package/dist/utils/math.d.ts +34 -1
  396. package/dist/utils/math.d.ts.map +1 -1
  397. package/dist/utils/math.js +47 -14
  398. package/dist/utils/math.js.map +1 -1
  399. package/dist/utils/objects.d.ts +2 -3
  400. package/dist/utils/objects.d.ts.map +1 -1
  401. package/dist/utils/objects.js +2 -3
  402. package/dist/utils/objects.js.map +1 -1
  403. package/dist/utils/recallDefaults.d.ts +16 -0
  404. package/dist/utils/recallDefaults.d.ts.map +1 -0
  405. package/dist/utils/recallDefaults.js +16 -0
  406. package/dist/utils/recallDefaults.js.map +1 -0
  407. package/dist/utils/safeTimer.d.ts +8 -36
  408. package/dist/utils/safeTimer.d.ts.map +1 -1
  409. package/dist/utils/safeTimer.js +9 -37
  410. package/dist/utils/safeTimer.js.map +1 -1
  411. package/dist/utils/scanner.d.ts +97 -17
  412. package/dist/utils/scanner.d.ts.map +1 -1
  413. package/dist/utils/scanner.js +269 -47
  414. package/dist/utils/scanner.js.map +1 -1
  415. package/dist/utils/segmenter.d.ts +20 -29
  416. package/dist/utils/segmenter.d.ts.map +1 -1
  417. package/dist/utils/segmenter.js +230 -88
  418. package/dist/utils/segmenter.js.map +1 -1
  419. package/dist/utils/strings.d.ts +15 -32
  420. package/dist/utils/strings.d.ts.map +1 -1
  421. package/dist/utils/strings.js +25 -44
  422. package/dist/utils/strings.js.map +1 -1
  423. package/dist/utils/time.d.ts +37 -0
  424. package/dist/utils/time.d.ts.map +1 -1
  425. package/dist/utils/time.js +56 -0
  426. package/dist/utils/time.js.map +1 -1
  427. package/dist/utils/toError.d.ts +13 -1
  428. package/dist/utils/toError.d.ts.map +1 -1
  429. package/dist/utils/toError.js +18 -1
  430. package/dist/utils/toError.js.map +1 -1
  431. package/dist/web-fetch/fetchWebFetchProvider.d.ts +14 -0
  432. package/dist/web-fetch/fetchWebFetchProvider.d.ts.map +1 -0
  433. package/dist/web-fetch/fetchWebFetchProvider.js +69 -0
  434. package/dist/web-fetch/fetchWebFetchProvider.js.map +1 -0
  435. package/dist/web-fetch/types.d.ts +30 -0
  436. package/dist/web-fetch/types.d.ts.map +1 -0
  437. package/dist/web-fetch/types.js +7 -0
  438. package/dist/web-fetch/types.js.map +1 -0
  439. package/dist/web-fetch/webFetchProvider.d.ts +10 -0
  440. package/dist/web-fetch/webFetchProvider.d.ts.map +1 -0
  441. package/dist/web-fetch/webFetchProvider.js +28 -0
  442. package/dist/web-fetch/webFetchProvider.js.map +1 -0
  443. package/dist/web-search/fetchWebSearchProvider.d.ts +27 -0
  444. package/dist/web-search/fetchWebSearchProvider.d.ts.map +1 -0
  445. package/dist/web-search/fetchWebSearchProvider.js +199 -0
  446. package/dist/web-search/fetchWebSearchProvider.js.map +1 -0
  447. package/dist/web-search/types.d.ts +46 -0
  448. package/dist/web-search/types.d.ts.map +1 -0
  449. package/dist/web-search/types.js +2 -0
  450. package/dist/{persona → web-search}/types.js.map +1 -1
  451. package/dist/web-search/webSearchProvider.d.ts +10 -0
  452. package/dist/web-search/webSearchProvider.d.ts.map +1 -0
  453. package/dist/web-search/webSearchProvider.js +30 -0
  454. package/dist/web-search/webSearchProvider.js.map +1 -0
  455. package/docs/architecture/role-pack-authoring-guide.md +243 -0
  456. package/docs/architecture/role-pack-spec.md +767 -0
  457. package/docs/memora-api-reference.md +1175 -0
  458. package/docs/memora-/346/216/245/345/205/245/346/214/207/345/215/227.md +843 -0
  459. package/docs/role-pack-/345/274/200/346/224/276/351/224/256/346/214/207/345/215/227.md +148 -0
  460. package/package.json +105 -83
  461. package/role-packs/README.md +41 -0
  462. package/role-packs/memora/345/212/251/346/211/213/manifest.json +12 -0
  463. package/role-packs/memora/345/212/251/346/211/213/persona.md +12 -0
  464. package/role-packs/memora/345/212/251/346/211/213/rules.md +4 -0
  465. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/manifest.json +44 -0
  466. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/persona.md +46 -0
  467. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/rules.md +15 -0
  468. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/character-sheet.md +34 -0
  469. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/core-extractor.md +37 -0
  470. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/craft-review.md +69 -0
  471. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/dialogue-craft.md +45 -0
  472. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/emotion-wave.md +46 -0
  473. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/empathy-craft.md +79 -0
  474. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/foreshadow.md +29 -0
  475. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/hotspot-mapping.md +121 -0
  476. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/idea-lever.md +42 -0
  477. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/logline-craft.md +47 -0
  478. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/longform-guard.md +64 -0
  479. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/need-finder.md +40 -0
  480. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/outline-expand.md +46 -0
  481. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/pacing-hook.md +34 -0
  482. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/polish-method.md +113 -0
  483. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/pov-discipline.md +79 -0
  484. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/project-init/SKILL.md +112 -0
  485. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/project-init/scripts/init-novel-project.mjs +441 -0
  486. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/relationship-web.md +47 -0
  487. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/scene-craft.md +46 -0
  488. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/story-bible.md +60 -0
  489. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/story-structure.md +36 -0
  490. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/style-weaver.md +109 -0
  491. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/three-layer-check.md +27 -0
  492. package/role-packs//345/205/261/351/270/243/345/260/217/350/257/264/345/256/266/skills/world-builder.md +41 -0
  493. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/manifest.json +48 -0
  494. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/persona.md +73 -0
  495. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/rules.md +19 -0
  496. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/skills/api-doc-writer.md +40 -0
  497. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/skills/doc-planner.md +42 -0
  498. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/skills/nutrient-search.md +28 -0
  499. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/skills/seed-finder.md +28 -0
  500. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/skills/ssot-audit.md +34 -0
  501. package/role-packs//347/231/275/350/257/235/346/226/271/346/241/210/350/256/276/350/256/241/345/270/210/skills/style-guide.md +40 -0
  502. package/dist/agent/guardrail.d.ts +0 -47
  503. package/dist/agent/guardrail.d.ts.map +0 -1
  504. package/dist/agent/guardrail.js +0 -51
  505. package/dist/agent/guardrail.js.map +0 -1
  506. package/dist/agent/managers/autoConfigRefiner.d.ts +0 -39
  507. package/dist/agent/managers/autoConfigRefiner.d.ts.map +0 -1
  508. package/dist/agent/managers/autoConfigRefiner.js +0 -150
  509. package/dist/agent/managers/autoConfigRefiner.js.map +0 -1
  510. package/dist/agent/managers/configManager.d.ts +0 -125
  511. package/dist/agent/managers/configManager.d.ts.map +0 -1
  512. package/dist/agent/managers/configManager.js +0 -194
  513. package/dist/agent/managers/configManager.js.map +0 -1
  514. package/dist/agent/managers/insightExtractor.d.ts +0 -139
  515. package/dist/agent/managers/insightExtractor.d.ts.map +0 -1
  516. package/dist/agent/managers/insightExtractor.js +0 -401
  517. package/dist/agent/managers/insightExtractor.js.map +0 -1
  518. package/dist/agent/managers/memoryDecayScheduler.d.ts +0 -198
  519. package/dist/agent/managers/memoryDecayScheduler.d.ts.map +0 -1
  520. package/dist/agent/managers/memoryDecayScheduler.js +0 -349
  521. package/dist/agent/managers/memoryDecayScheduler.js.map +0 -1
  522. package/dist/agent/managers/relationBuilder.d.ts +0 -131
  523. package/dist/agent/managers/relationBuilder.d.ts.map +0 -1
  524. package/dist/agent/managers/relationBuilder.js +0 -192
  525. package/dist/agent/managers/relationBuilder.js.map +0 -1
  526. package/dist/agent/personaMatcher.d.ts +0 -29
  527. package/dist/agent/personaMatcher.d.ts.map +0 -1
  528. package/dist/agent/personaMatcher.js +0 -60
  529. package/dist/agent/personaMatcher.js.map +0 -1
  530. package/dist/agent/userFactExtractor.d.ts +0 -25
  531. package/dist/agent/userFactExtractor.d.ts.map +0 -1
  532. package/dist/agent/userFactExtractor.js +0 -81
  533. package/dist/agent/userFactExtractor.js.map +0 -1
  534. package/dist/eval/evalRunner.d.ts +0 -94
  535. package/dist/eval/evalRunner.d.ts.map +0 -1
  536. package/dist/eval/evalRunner.js +0 -141
  537. package/dist/eval/evalRunner.js.map +0 -1
  538. package/dist/eval/evalTypes.d.ts +0 -148
  539. package/dist/eval/evalTypes.d.ts.map +0 -1
  540. package/dist/eval/evalTypes.js +0 -118
  541. package/dist/eval/evalTypes.js.map +0 -1
  542. package/dist/eval/scenarios.d.ts +0 -33
  543. package/dist/eval/scenarios.d.ts.map +0 -1
  544. package/dist/eval/scenarios.js +0 -109
  545. package/dist/eval/scenarios.js.map +0 -1
  546. package/dist/llm/embedding.d.ts +0 -67
  547. package/dist/llm/embedding.d.ts.map +0 -1
  548. package/dist/llm/embedding.js +0 -184
  549. package/dist/llm/embedding.js.map +0 -1
  550. package/dist/memory/hybridMerge.d.ts +0 -54
  551. package/dist/memory/hybridMerge.d.ts.map +0 -1
  552. package/dist/memory/hybridMerge.js +0 -36
  553. package/dist/memory/hybridMerge.js.map +0 -1
  554. package/dist/memory/inMemoryRelationStore.d.ts +0 -51
  555. package/dist/memory/inMemoryRelationStore.d.ts.map +0 -1
  556. package/dist/memory/inMemoryRelationStore.js +0 -65
  557. package/dist/memory/inMemoryRelationStore.js.map +0 -1
  558. package/dist/memory/loader.d.ts +0 -49
  559. package/dist/memory/loader.d.ts.map +0 -1
  560. package/dist/memory/loader.js +0 -93
  561. package/dist/memory/loader.js.map +0 -1
  562. package/dist/memory/recall.d.ts +0 -66
  563. package/dist/memory/recall.d.ts.map +0 -1
  564. package/dist/memory/recall.js +0 -142
  565. package/dist/memory/recall.js.map +0 -1
  566. package/dist/memory/relationStore.d.ts +0 -78
  567. package/dist/memory/relationStore.d.ts.map +0 -1
  568. package/dist/memory/relationStore.js +0 -2
  569. package/dist/memory/relationStore.js.map +0 -1
  570. package/dist/memory/store.d.ts +0 -50
  571. package/dist/memory/store.d.ts.map +0 -1
  572. package/dist/memory/store.js +0 -161
  573. package/dist/memory/store.js.map +0 -1
  574. package/dist/memory/userProfile.d.ts +0 -156
  575. package/dist/memory/userProfile.d.ts.map +0 -1
  576. package/dist/memory/userProfile.js +0 -320
  577. package/dist/memory/userProfile.js.map +0 -1
  578. package/dist/memory/vectorStore.d.ts +0 -185
  579. package/dist/memory/vectorStore.d.ts.map +0 -1
  580. package/dist/memory/vectorStore.js +0 -257
  581. package/dist/memory/vectorStore.js.map +0 -1
  582. package/dist/persona/personaManager.d.ts +0 -160
  583. package/dist/persona/personaManager.d.ts.map +0 -1
  584. package/dist/persona/personaManager.js +0 -414
  585. package/dist/persona/personaManager.js.map +0 -1
  586. package/dist/persona/types.d.ts +0 -32
  587. package/dist/persona/types.d.ts.map +0 -1
  588. package/dist/persona/types.js +0 -5
@@ -1,82 +1,39 @@
1
1
  /**
2
2
  * Agent 门面类 — Memora 宿主项目接入入口
3
3
  *
4
- * 设计文档(ADR-010 · Agent 门面类)要求宿主项目通过 `import { Agent } from '@memora/core'`
5
- * 一行代码接入。本类负责组件组装和核心对话编排,
6
- * 领域专属操作委托给专职 Manager(PersonaManager / ToolExecutor / SkillManager / ConfigManager / InsightExtractor / MemoryInspector)。
7
- *
8
- * 使用方式(最简):
9
- * const agent = new Agent({ projectPath: './my-project', configDir: './agent-config' });
10
- * await agent.init();
11
- * for await (const chunk of agent.chat('你好')) {
12
- * if (chunk.type === 'text') process.stdout.write(chunk.content);
13
- * }
14
- * await agent.close();
15
- *
16
- * 使用方式(高级):
17
- * const agent = new Agent({ projectPath: './my-project', provider: myProvider, configDir: './agent-config' });
18
- *
19
- * 2026-06-12 God Object 拆分:
20
- * - Insight 提取 → InsightExtractor
21
- * - 配置管理 → ConfigManager
22
- * - 记忆查看 → MemoryInspector
23
- * - 薄包装方法移除,调用方改为 agent.<manager>.xxx()
4
+ * 宿主通过 `import { Agent } from '@zooique/memora'` 一行接入。
5
+ * 负责组件组装和核心对话编排,领域专属操作委托给专职 Manager
6
+ * (RolePackManager / ToolExecutor / SkillManager / MemoryInspector)。
24
7
  */
8
+ import { isAbsolute } from 'node:path';
25
9
  import { getBaseName } from '../utils/path.js';
26
10
  import { AGENT_CONSTANTS } from '../agent/constants.js';
27
11
  import { ProjectManager } from '../memory/projectManager.js';
28
12
  import { SecurityGuard } from '../security/pathGuard.js';
29
- import { recall } from '../memory/recall.js';
30
- import { assembleComponents } from '../agent/assembler.js';
31
- import { matchPersonaByLlm } from '../agent/personaMatcher.js';
32
- import { chatBusyError, configError } from '../utils/errors.js';
33
- import { clearSafeInterval } from '../utils/safeTimer.js';
34
- import { SessionManager } from '../agent/managers/sessionManager.js';
13
+ import { DEFAULT_BEHAVIOR_STRATEGY } from '../role-pack/strategyResolver.js';
14
+ import { isTemperature, MAX_OUTPUT_LIMIT } from '../role-pack/strategyKeys.js';
15
+ import { resolveCapabilityTools } from '../role-pack/capabilityMap.js';
16
+ import { SessionNamer } from '../agent/managers/sessionNamer.js';
17
+ import { createDefaultGCService } from '../memory/gcService.js';
18
+ import { assembleComponents, buildSystemPromptPrefix } from '../agent/assembler.js';
19
+ import { estimateTokensMessages } from '../agent/contextManager.js';
20
+ import { SeedOrchestrator } from '../agent/seed/index.js';
21
+ import { chatBusyError, configError, isAbortError, isTimeoutAbortSignal, isTimeoutError, } from '../utils/errors.js';
22
+ import { toError } from '../utils/toError.js';
35
23
  import { ChatLockManager } from '../agent/managers/chatLockManager.js';
36
- import { MemoryDecayScheduler } from '../agent/managers/memoryDecayScheduler.js';
37
- import { ArchiveCoordinator } from '../agent/managers/archiveCoordinator.js';
38
- import { TypedEventEmitter } from '../utils/eventEmitter.js';
24
+ import { MemoryGovernance } from '../agent/managers/memoryGovernance.js';
25
+ import { ArchiveCoordinator, } from '../agent/managers/archiveCoordinator.js';
26
+ import { TypedEventEmitter, AGENT_EVENTS, AGENT_EVENT_SET, } from '../utils/eventEmitter.js';
39
27
  import { logger } from '../logging/logger.js';
40
- import { TRACE_SPANS, NOOP_TRACER } from '../agent/tracer.js';
28
+ import { getBackgroundTaskStats as readBackgroundTaskStats, awaitBackgroundTasks, } from '../utils/backgroundTask.js';
41
29
  // ─── 模块级常量 ─────────────────────────────────────────
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
- ]);
50
30
  // ─── Agent 门面类 ───────────────────────────────────────
51
31
  /**
52
32
  * Memora Agent 门面类
53
33
  *
54
- * **设计哲学**:单 Agent,单配置,单记忆。每个 Agent 实例拥有独立的
55
- * 对话管线(AgentLoop)、独立的消息历史(MessageHistory)和独立的
56
- * 运行时状态(activeSkill、chatBusy)。
57
- *
58
- * **多实例**:宿主可通过 new Agent({ dataDir: './agent2' }) 创建
59
- * 独立实例。只要 dataDir 不同,它们拥有完全隔离的记忆库和会话。
60
- * 内核不负责多 Agent 编排——那是宿主层的职责。
61
- *
62
- * **fork vs 多实例**:
63
- * - forkSession() 分叉对话历史,但记忆索引全局共享
64
- * - new Agent() + 独立 dataDir 实现完全隔离的记忆空间
65
- *
66
- * @example
67
- * ```ts
68
- * const agent = new Agent({
69
- * projectPath: '/path/to/project',
70
- * provider: new OpenAIProvider({ apiKey: '...' }),
71
- * configDir: '/path/to/config',
72
- * dataDir: '/path/to/data',
73
- * permission: 'owner',
74
- * allowedPaths: ['/path/to/project'],
75
- * });
76
- * await agent.init();
77
- * const iter = agent.chat('你好');
78
- * for await (const chunk of iter) { ... }
79
- * ```
34
+ * 单 Agent,单配置,单记忆:每个实例拥有独立的对话管线、消息历史与运行时状态。
35
+ * 多实例只需 dataDir 不同即可彻底隔离记忆库与会话;forkSession() 分叉对话历史但记忆索引全局共享。
36
+ * skill 为当轮实时注入,不跨轮缓存。
80
37
  */
81
38
  export class Agent extends TypedEventEmitter {
82
39
  // 构造参数分组
@@ -85,108 +42,196 @@ export class Agent extends TypedEventEmitter {
85
42
  #provider;
86
43
  /** 后台 Provider(独立字段,因 setBackgroundProvider() 可变) */
87
44
  #backgroundProvider;
88
- // 运行时组件(init 后填充)
45
+ /** Provider 路由选择器(多模型路由基础,可选) */
46
+ #providerRouter = null;
47
+ // 运行时组件(init 后填充)——宿主 getter 契约字段(projects/memory/works/polish/sessionManager 等)保持独立
89
48
  projectManager = null;
90
49
  history = null;
91
50
  loop = null;
92
51
  toolExec = null;
93
52
  // 新模块
94
- personaManager = null;
95
- #userProfile = null;
96
53
  skillManager = null;
54
+ /** 角色包管理器(角色+技能+规则的唯一真理源) */
55
+ _rolePackManager = null;
97
56
  // 拆分出的专职 Manager
98
- insightExtractor = null;
99
- configManager = null;
100
57
  memoryInspector = null;
101
- workProjection = null;
102
- /** AutoConfigRefiner(模式 3:Agent 智能总结) */
103
- autoConfigRefiner = null;
104
- /** SessionArchiver(会话内容归档器,content 类记忆) */
105
- sessionArchiver = null;
58
+ /** 记忆治理统一门面(L0/L1/L2/L3 + 诊断) */
59
+ _governance = null;
106
60
  /** TextPolishManager(文本润色管理器,LLM 语法修正 + 表达优化) */
107
61
  textPolisher = null;
108
62
  /** 会话管理器(从 Agent 拆分出的会话管理职责) */
109
63
  _sessionManager = null;
110
- /** 当前激活的技能名(上一轮匹配,本轮注入) */
111
- activeSkill = null;
64
+ /** WorkProjectionManager(作品投影极简 JSON 索引,宿主经 works getter 消费) */
65
+ workProjection = null;
66
+ // ─── 纯内部组件聚合(无宿主 getter 契约,init 后填充、close 统一清空) ───
67
+ /**
68
+ * 内部组件聚合对象——收敛 12 个「仅门面内部消费、不对宿主暴露」的专职组件,
69
+ * 减少门面扁平字段数;宿主 getter 契约字段(memory/works/polish 等)不在此列。
70
+ */
71
+ internals = {
72
+ dedupManager: null,
73
+ memoryAdvisor: null,
74
+ sessionArchiver: null,
75
+ sessionNamer: null,
76
+ roundSummaryGenerator: null,
77
+ contextPreparer: null,
78
+ chatLockManager: null,
79
+ archiveCoordinator: null,
80
+ seedOrchestrator: null,
81
+ gcService: null,
82
+ };
112
83
  _initialized = false;
113
84
  // 项目上下文(AgentContext 与 ProjectContext 等价,直接使用后者避免重复字段)
114
85
  pctx = null;
115
- /**
116
- * chat() 并发锁管理器(init 时创建,close 时销毁)
117
- *
118
- * 职责:chat() 并发锁 + token 校验(race condition 防护)+ 超时保护 + 外部 signal 合并。
119
- * 详见 ChatLockManager 类注释。
120
- */
121
- chatLockManager = null;
122
- /** 记忆衰减定时器(已迁移至 MemoryDecayScheduler,此字段保留用于 close 时引用判断) */
123
- decayTimer = null;
124
86
  /** 最近一次 chat() 调用的时间戳 */
125
87
  _lastInteractionAt = null;
126
- // ─── 衰减职责已拆分至 MemoryDecayScheduler ──────────
127
- // metricDecayRunCount / metricTotalDecayedCount / metricLastDecayAt 字段
128
- // 位于 MemoryDecayScheduler 内部,Agent 通过 memoryDecayScheduler.getMetrics() 读取
129
- /** 记忆衰减调度器(init 时创建,close 时销毁) */
130
- memoryDecayScheduler = null;
131
- /** 归档协调器(归档操作委托给 ArchiveCoordinator) */
132
- archiveCoordinator = null;
88
+ /**
89
+ * 流活跃标志:chat()/resumeExecution() 的 AsyncGenerator 生命周期内置 true、finally 置 false。
90
+ * 与 chatLock.isBusy(并发锁,180s 超时后自动释放但**不中断生成流**)语义解耦——
91
+ * requestPause 空闲守卫必须用「流是否在产出」而非「锁是否持有」判任务是否结束,
92
+ * 否则长任务运行超锁期后点暂停会被误判为「任务已结束」——锁超时会自动释放但生成流仍在跑,
93
+ * 故判据只能是流活跃,不能是锁持有。
94
+ */
95
+ _flowActive = false;
96
+ /**
97
+ * 对话中因 chatLock 冲突暂存的配置重载请求(锁释放后补执行,兑现"对话后自动加载");
98
+ * 用 Set 去重——同一 source 只需补执行一次。
99
+ */
100
+ pendingConfigReload = new Set();
133
101
  constructor(opts) {
134
102
  super();
135
103
  this.#config = {
136
104
  projectPath: opts.projectPath,
137
- dataDir: opts.dataDir ?? '~/.memora',
105
+ dataDir: opts.dataDir,
138
106
  registryDir: opts.registryDir,
139
107
  maxContextTokens: opts.maxContextTokens ?? AGENT_CONSTANTS.DEFAULT_MAX_CONTEXT_TOKENS,
140
- personaName: opts.persona,
108
+ // per-LLM 输出预算默认值(token):宿主 per-LLM 配置透传;undefined = 不传 max_tokens
109
+ //(回服务端默认)。与角色包策略 act.outputLimit 的最终关系由 buildChatOptions 取更小值。
110
+ maxTokens: opts.maxTokens,
141
111
  permission: opts.permission ?? 'owner',
142
112
  allowedPaths: opts.allowedPaths ?? [],
143
113
  confirmWrites: opts.confirmWrites ?? false,
144
- vectorStore: opts.vectorStore,
145
- recallExcludeSources: opts.recallExcludeSources ?? [
146
- ...AGENT_CONSTANTS.DEFAULT_RECALL_EXCLUDE_SOURCES,
147
- ],
114
+ confirmScripts: opts.confirmScripts ?? false,
148
115
  storage: opts.storage,
149
- relationStore: opts.relationStore,
150
116
  sessionStore: opts.sessionStore,
117
+ roundStore: opts.roundStore,
151
118
  configDir: opts.configDir,
119
+ // 启动时激活的角色包名(宿主注入持久化值,init 时优先激活——§4.1 单链第一层)
120
+ activeRolePack: opts.activeRolePack,
121
+ // 组(宿主装配级):组长角色包 + 组员名单(会议名单容器,非选择对象;组员仅会议参与)
122
+ // 内部运行态归一化必选(外部未配 = 无团队),保证装配链必传
123
+ rolePackTeams: opts.rolePackTeams ?? [],
124
+ // 程序级内置兜底角色(可选):覆盖内核常量 BUILTIN_FALLBACK_PACK;须指向存在的包,否则回退内核常量
125
+ builtinFallbackRole: opts.builtinFallbackRole,
152
126
  tracer: opts.tracer,
153
127
  messages: opts.messages,
154
128
  enableContextSummary: opts.enableContextSummary ?? true,
155
129
  archiveMode: opts.archiveMode ?? 'full',
130
+ webSearchProvider: opts.webSearchProvider,
131
+ fetchProvider: opts.fetchProvider,
132
+ codeExecutionProvider: opts.codeExecutionProvider,
133
+ projectSearchProvider: opts.projectSearchProvider,
134
+ // 脚本执行 node 路径(可选):宿主注入真实 node 路径,避免无独立 node 时 ENOENT
135
+ scriptNodePath: opts.scriptNodePath,
136
+ // 禁用技能清单(配置形态启停):透传装配 → SkillManager 过滤 L1/L2/L3
137
+ disabledSkills: opts.disabledSkills ?? [],
138
+ // 宿主审批/审计/参数改写通道,透传供装配阶段与内部幂等检查组合为一处执行前检查点
139
+ preExecutionCheck: opts.preExecutionCheck,
140
+ // 宿主装配级策略覆盖(能力边界):透传 #config → 装入策略解析链(resolveActiveStrategy),
141
+ // 压过角色包声明(v0.13 后无宿主注入默认值;机制保留供宿主产品能力边界使用)
142
+ strategyOverride: opts.strategyOverride,
156
143
  };
157
144
  this.#provider = opts.provider;
158
145
  this.#backgroundProvider = opts.backgroundProvider ?? null;
159
- // 如需自定义日志,请在创建 Agent 前调用 `import { setLogger } from '@memora/core'` 全局设置
146
+ this.#providerRouter = opts.providerRouter ?? null;
147
+ // 如需自定义日志,请在创建 Agent 前调用 `import { setLogger } from '@zooique/memora'` 全局设置
160
148
  }
161
149
  // ─── 生命周期 ─────────────────────────────────────────
162
150
  /**
163
151
  * 初始化 Agent:加载索引、组装内部组件
164
152
  */
165
153
  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
- }
154
+ await this.disposePreviousInstance();
177
155
  if (projectPathOverride) {
178
156
  this.#config.projectPath = projectPathOverride;
179
157
  }
158
+ const pctx = await this.initializeProject();
159
+ await this.assembleComponents(pctx);
160
+ this.pctx = pctx;
161
+ this.validateCoreComponents();
162
+ this.createPostInitComponents(pctx);
163
+ // 标记初始化完成
164
+ this._initialized = true;
165
+ // 注册暂停超时归档(同进程暂停超时:30min 无心跳则归档清理)
166
+ this.registerPauseTimeoutArchiver();
167
+ this.registerWorkProjectionRefresh();
168
+ // 注:不加载持久化检查点——中止/断电一律走
169
+ // 「中断轮补全为完整 turn 身份 → 参与下一轮」,不存在跨重启恢复;
170
+ // 运行时暂停是同 turn 内续跑(内存态),无需磁盘载体。
171
+ return pctx;
172
+ }
173
+ /**
174
+ * 登记作品投影实时刷新处理器:register_work 工具 / 右键登记成功后,
175
+ * 内核 WorkProjectionManager 触发 workProjectionGenerated 事件(携带已写入内存 entries 的投影),
176
+ * 立即重建 AgentLoop 的 systemPromptPrefix,使 AI 在下一轮对话即可感知新索引——
177
+ * 否则需切角色包 / reloadConfig / 开新会话才生效。
178
+ * 与 registerPauseTimeoutArchiver 同生命周期(init 内注册,close 的 removeAllListeners 兜底)。
179
+ */
180
+ registerWorkProjectionRefresh() {
181
+ this.on(AGENT_EVENTS.workProjectionGenerated, () => {
182
+ this.refreshRolePackPrefixOnLoop();
183
+ });
184
+ }
185
+ /**
186
+ * 注册暂停超时归档处理器(SessionManager 两条超时路径——启动加载与运行时定时器——统一在此消费)
187
+ * 注册在 init() 内(close() 的 removeAllListeners + init 起始的 disposePreviousInstance 保证实例周期内恰好一个监听器),
188
+ * 挂在生命周期稳定的 Agent 上,rebuildComponents() 重建管理器后仍有效。
189
+ */
190
+ registerPauseTimeoutArchiver() {
191
+ this.on(AGENT_EVENTS.sessionPauseTimedOut, (payload) => {
192
+ // pendingPauseReason 由 resetToRunning() 内部统一清理
193
+ //(checkPauseTimeout 经 sessionManager resetToRunning 已覆盖,此处无需再补 cancelPendingPause)。
194
+ const { sessionId, date, session } = payload;
195
+ if (!date || !session)
196
+ return;
197
+ // fire-and-forget:归档失败不阻塞主流程,仅记录
198
+ this.internals.archiveCoordinator
199
+ ?.archiveSession(date, session, { autoTriggered: true })
200
+ .catch((err) => {
201
+ logger.warn({ err, sessionId }, '暂停超时会话自动归档失败');
202
+ });
203
+ });
204
+ }
205
+ /**
206
+ * 关闭旧实例(如果已初始化),失败不阻塞重建
207
+ */
208
+ async disposePreviousInstance() {
209
+ if (!this._initialized)
210
+ return;
211
+ try {
212
+ await this.close();
213
+ }
214
+ catch (err) {
215
+ logger.warn({ err }, 'init() 中 close() 旧实例失败,继续重建');
216
+ }
217
+ }
218
+ /**
219
+ * 创建 ProjectManager 并初始化项目上下文
220
+ */
221
+ async initializeProject() {
180
222
  this.projectManager = new ProjectManager({
181
223
  dataDir: this.#config.dataDir,
182
224
  storage: this.#config.storage,
183
225
  registryDir: this.#config.registryDir,
184
226
  // SecurityGuard 由 Agent 层创建,解除 memory→security 反向依赖
185
- 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),
227
+ createSecurityGuard: (projectPath, memoraDir, configDir, agentDataDir) => new SecurityGuard(projectPath, memoraDir, this.#config.allowedPaths, this.#config.confirmWrites, this.#config.permission, this.#config.confirmScripts, configDir ?? this.#config.configDir, agentDataDir ?? this.#config.dataDir),
186
228
  });
187
- const pctx = await this.projectManager.initProject(this.#config.projectPath, undefined, this.#config.configDir);
188
- await this.assembleComponents(pctx);
189
- this.pctx = pctx;
229
+ return this.projectManager.initProject(this.#config.projectPath, undefined, this.#config.configDir);
230
+ }
231
+ /**
232
+ * 校验核心组件(loop / history)非空
233
+ */
234
+ validateCoreComponents() {
190
235
  if (!this.loop || !this.history) {
191
236
  throw configError('Agent 初始化失败', '组件组装后 loop 或 history 为空(可能 assembleComponents 抛异常被静默吞掉)', [
192
237
  '检查 assembleComponents() 是否有未捕获的异常',
@@ -194,406 +239,753 @@ export class Agent extends TypedEventEmitter {
194
239
  '确认 API Key 已配置(环境变量或配置文件)',
195
240
  ]);
196
241
  }
242
+ }
243
+ /**
244
+ * 创建 init() 后期组件(初始化完成后才需要的组件)
245
+ */
246
+ createPostInitComponents(_pctx) {
197
247
  // chat() 并发锁管理器(生命周期与 Agent 实例一致)
198
- this.chatLockManager = new ChatLockManager();
199
- this._initialized = true;
200
- // 归档操作委托给 ArchiveCoordinator
201
- // 使用 getter 回调注入依赖,close 时 null 化字段后 getter 自然返回 null
202
- this.archiveCoordinator = new ArchiveCoordinator({
203
- getUserProfile: () => this.#userProfile,
204
- getInsightExtractor: () => this.insightExtractor,
205
- getSessionArchiver: () => this.sessionArchiver,
248
+ this.internals.chatLockManager = new ChatLockManager();
249
+ // 孤儿 Round 垃圾回收服务:roundStore+storage 均为构造期稳定注入(不随 rebuildComponents 变化),
250
+ // 每日周期清理 refCount=0 且超龄的孤立问答闭环(崩溃残留 pending / 已删会话遗留),防止存储膨胀。
251
+ // shouldSkip:对话进行中跳过本次(minAgeMs 判龄对长任务不可靠,防进行中轮被误清)
252
+ if (this.#config.roundStore && this.#config.storage) {
253
+ this.internals.gcService = createDefaultGCService(this.#config.roundStore, this.#config.storage,
254
+ // 忙碌检查:chatLock busy = 对话/长任务进行中,跳过本次 GC
255
+ () => this.internals.chatLockManager?.isBusy ?? false);
256
+ this.internals.gcService.startPeriodic(AGENT_CONSTANTS.GC_INTERVAL_MS);
257
+ }
258
+ // 归档操作委托给 ArchiveCoordinator(content 会话归档)
259
+ this.internals.archiveCoordinator = new ArchiveCoordinator({
260
+ getSessionArchiver: () => this.internals.sessionArchiver,
261
+ getArchiveMode: () => this.#config.archiveMode,
206
262
  emit: (event, payload) => this.emit(event, payload),
207
263
  });
208
- // 记忆衰减职责委托给 MemoryDecayScheduler
209
- // L2 时效性评估:注入 backgroundProvider + index(可选,未注入时 evaluateTimeliness 静默跳过)
210
- this.memoryDecayScheduler = new MemoryDecayScheduler({
211
- tracer: this.#config.tracer,
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
- });
264
+ // 会话命名器:新建会话首次问答自动命名标题;惰性获取当前 provider(切换后仍命中最新模型)
265
+ // 注入 onTitleUpdated 回调:标题生成后发射 sessionTitleUpdated 事件,宿主据此刷新 UI
266
+ this.internals.sessionNamer = new SessionNamer({
267
+ getProvider: () => this.#provider,
268
+ sessionStore: this.#config.sessionStore,
269
+ onTitleUpdated: (sessionId, title) => {
270
+ this.emit(AGENT_EVENTS.sessionTitleUpdated, { sessionId, title });
271
+ },
272
+ });
273
+ // 记忆治理统一门面(去重 / 冲突 / 诊断)
274
+ this._governance = new MemoryGovernance(this.internals.dedupManager, this.internals.memoryAdvisor);
275
+ // 种子 turn 编排器(turn 唯一编排真理源):依赖 sessionManager/loop/history 等
276
+ // 均已就绪(assembleComponents 已完成 + sessionNamer 本方法前段创建),在此构造一次。
277
+ // getParts() 惰性取当前组件——rebuildComponents(switchProject)更换组件后仍取到最新引用。
278
+ this.internals.seedOrchestrator = this.createSeedOrchestrator();
279
+ }
280
+ /**
281
+ * 构造种子闭环编排器,注入门面稳定能力(getParts 快照 + 工具暴露 + 流收口协议)
282
+ *
283
+ * 构造偏好在 agent.ts 而非 assembler.ts(方案的"assembler 构造"):seed 依赖
284
+ * sessionNamer(post-init 才创建)与 consumeExecutionStream(门面私有流收口协议),
285
+ * 二者均无法在 assembler 阶段就绪,故收敛到 post-init 门面组装处。
286
+ *
287
+ * @returns 已接线门面能力的 SeedOrchestrator
288
+ */
289
+ createSeedOrchestrator() {
290
+ return new SeedOrchestrator({
291
+ // 组件快照经 getter 惰性取当前引用:rebuildComponents 更换组件后仍读到最新(关键)
292
+ getParts: () => ({
293
+ loop: this.loop,
294
+ history: this.history,
295
+ sessionManager: this._sessionManager,
296
+ rolePackManager: this._rolePackManager,
297
+ contextPreparer: this.internals.contextPreparer,
298
+ sessionNamer: this.internals.sessionNamer,
299
+ roundSummaryGenerator: this.internals.roundSummaryGenerator,
300
+ }),
301
+ tracer: this.#config.tracer ?? null,
302
+ archiveMode: this.#config.archiveMode,
303
+ messages: this.#config.messages,
304
+ // 门面私有能力经回调注入 seed(物理实现仍在门面)
305
+ applyRolePackToolExposure: () => this.applyRolePackToolExposure(),
306
+ // 会议机制:按本轮表层装配视角刷新 loop 前缀(roundRole=null 回落 activePack)
307
+ refreshRolePackPrefixForRound: (roundRole) => this.refreshRolePackPrefixForRound(roundRole),
308
+ consumeExecutionStream: (source, signal) => this.consumeExecutionStream(source, signal),
309
+ getBackgroundProvider: () => this.#backgroundProvider,
310
+ // 宿主装配级策略覆盖(能力边界):传给策略解析链,压过角色包声明(单一语义键不变)
311
+ strategyOverride: this.#config.strategyOverride,
312
+ // 事件发射回调:orchestrator.runSummary 完成后 emit roundSummaryGenerated,
313
+ // 让宿主感知后台摘要收口 → 解锁 UI 生命周期(删除/分叉按钮解禁,防孤儿记忆)
314
+ emit: (event, payload) => {
315
+ if (AGENT_EVENT_SET.has(event)) {
316
+ this.emit(event, payload);
317
+ }
222
318
  },
223
- backgroundProvider: this.#backgroundProvider,
224
- index: pctx.index,
225
319
  });
226
- this.memoryDecayScheduler.start(pctx.index, AGENT_CONSTANTS.DECAY_INTERVAL_MS);
227
- // 保留 decayTimer 引用用于 close 时序兼容(实际定时器由 MemoryDecayScheduler 管理)
228
- this.decayTimer = null;
229
- return pctx;
230
320
  }
231
321
  /**
232
322
  * 流式对话(核心 API)
233
323
  */
234
324
  async *chat(input, signal) {
235
325
  this.assertInitialized('chat');
326
+ this.validateChatInput(input);
327
+ const lockCtx = this.acquireChatLock(signal);
328
+ const { myToken, combinedSignal, cleanupExternalSignal } = lockCtx;
329
+ try {
330
+ // 流活跃置位:requestPause 空闲守卫据此判「任务进行中」——锁 180s 超时释放后
331
+ // isBusy=false 但流仍在产出,必须以流生命周期(而非锁持有)为准(09-03 锁语义变更回归修补)
332
+ this._flowActive = true;
333
+ // 状态机翻转是副作用,必须在并发闸门内执行——PAUSED 态收到 chat = 自动恢复 + 继续(作为补充注入);ERROR 态仍拒绝
334
+ if (!this.autoResumeIfPaused()) {
335
+ yield {
336
+ type: 'error',
337
+ message: '会话已超时,无法自动恢复,请重新开始新对话',
338
+ category: 'timeout',
339
+ };
340
+ return;
341
+ }
342
+ this._lastInteractionAt = new Date();
343
+ // 委托种子编排器:对话路径完整闭环(prepare → act → reflect)
344
+ yield* this.internals.seedOrchestrator.runChat(input, combinedSignal);
345
+ }
346
+ finally {
347
+ // 流已退出:清除活跃标志(与置位对称;无论正常/异常退出都复位)
348
+ this._flowActive = false;
349
+ // 仅当本调用仍是当前锁持有者时才清理资源(token 校验)
350
+ this.internals.chatLockManager?.release(myToken);
351
+ cleanupExternalSignal();
352
+ // turn 结束自动收尾:非暂停态无条件清理任务表(turn 内能力,不跨 turn 残留)
353
+ this.clearPlanOnTurnEnd();
354
+ await this.flushPendingConfigReload();
355
+ }
356
+ }
357
+ /**
358
+ * 校验 chat 输入
359
+ */
360
+ validateChatInput(input) {
236
361
  if (input.length > AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH) {
237
362
  throw configError('输入过长', `输入超过最大长度限制(${AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH / 1024}KB)`, ['缩短输入内容', '分多次对话发送']);
238
363
  }
239
- if (this.chatLockManager?.isBusy) {
364
+ }
365
+ /**
366
+ * 获取对话锁 — 返回锁上下文
367
+ */
368
+ acquireChatLock(signal) {
369
+ const chatLock = this.internals.chatLockManager;
370
+ if (!chatLock)
371
+ throw configError('ChatLockManager', 'ChatLockManager 未初始化', []);
372
+ if (chatLock.isBusy) {
240
373
  throw chatBusyError('发起新对话');
241
374
  }
242
- // 获取锁 + 分配 token + 创建内部 AbortController + 启动超时定时器
243
- // token 用于 finally 校验,避免 race condition:超时释放后新调用获取锁,旧 finally 误清新调用者的资源
244
- // assertInitialized 已保证 chatLockManager/loop/history 非 null,此处用 ! 窄化类型
245
- const chatLock = this.chatLockManager;
246
375
  const { token: myToken, internalAbort } = chatLock.acquire(AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS);
247
- // 合并外部 signal:外部 abort 时也触发内部
248
376
  const cleanupExternalSignal = chatLock.attachExternalSignal(signal, internalAbort);
249
- const combinedSignal = internalAbort.signal;
250
- try {
251
- 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);
259
- if (combinedSignal.aborted) {
260
- yield {
261
- type: 'aborted',
262
- reason: this.#config.messages?.abortedByUser ?? 'User cancelled the conversation',
263
- };
264
- return;
265
- }
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;
295
- }
296
- else {
297
- yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
298
- return;
299
- }
300
- }
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
- }
314
- return;
315
- }
316
- // 追加助手消息到历史(best-effort:失败不影响用户已收到的回答)
377
+ return { myToken, internalAbort, cleanupExternalSignal, combinedSignal: internalAbort.signal };
378
+ }
379
+ /**
380
+ * 补执行对话期间暂存的配置重载请求
381
+ */
382
+ async flushPendingConfigReload() {
383
+ if (this.pendingConfigReload.size === 0)
384
+ return;
385
+ const pending = Array.from(this.pendingConfigReload);
386
+ this.pendingConfigReload.clear();
387
+ for (const src of pending) {
317
388
  try {
318
- await history.appendAssistant(assistantContent);
389
+ await this.reloadConfig(src);
390
+ this.emit(AGENT_EVENTS.configReloaded, { source: src });
319
391
  }
320
392
  catch (err) {
321
- logger.warn({ err }, '助手消息历史写入失败');
393
+ logger.warn({ err, source: src }, '补执行配置重载失败');
322
394
  }
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
395
  }
333
396
  }
334
397
  /**
335
398
  * 强制释放对话锁
336
399
  *
337
- * 场景:宿主主进程"无进展超时"兜底后,generator 可能仍卡在不可中断的 await 点
338
- * (executeToolCalls 和 generateContextSummary 已覆盖 signal 响应,但其他
339
- * 第三方库或未来新增的 await 点仍可能不响应 signal)。
340
- * 此时 _chatBusy 锁未释放,用户再发消息会被 chat() 的竞态保护拒绝,
341
- * 表现为"UI 能操作但发不出消息",需等到 3 分钟锁超时才能恢复。
342
- *
343
- * 本方法让宿主在确认 generator 挂起后强制释放锁,让用户能立即发起新对话。
400
+ * 宿主在"无进展超时"确认 generator 挂起后调用,使用户能立即发起新对话(否则需等锁超时)。
401
+ * 安全机制委托 ChatLockManager.forceRelease:递增 token 让原 chat() 的 finally 跳过清理、abort signal 让响应点退出;
402
+ * 原 generator 仍可能后台运行但不影响新调用。幂等(未忙时 no-op)。
403
+ */
404
+ forceReleaseChatLock() {
405
+ this.internals.chatLockManager?.forceRelease();
406
+ }
407
+ /**
408
+ * 发送用户消息,非流式返回完整回复
409
+ */
410
+ async chatSync(input, signal) {
411
+ let result = '';
412
+ for await (const chunk of this.chat(input, signal)) {
413
+ if (chunk.type === 'text') {
414
+ result += chunk.content;
415
+ }
416
+ }
417
+ return result;
418
+ }
419
+ // ─── 不中断工作模型 v2.0:公开 API ──────────────────────
420
+ /**
421
+ * 软暂停后续跑(不中断工作模型 v2.1)
344
422
  *
345
- * 安全机制(委托至 ChatLockManager.forceRelease):
346
- * - 递增 _chatLockToken,让原 chat() 的 finally 块检测到 token 不匹配后
347
- * 跳过资源清理(避免误清新调用者的 _chatBusy/chatAbortController)
348
- * - abort chatAbortController,让响应 signal 的 await 点(如 fetch)退出
349
- * - 原 generator 仍可能在后台运行(无法真正中断不响应 signal 的 await),
350
- * 但其 finally 块的 token 校验会阻止它影响新调用
423
+ * 翻状态机为 RUNNING(触发 sessionResumed → 宿主转发 STATUS{running}),
424
+ * 驱动 loop.continueAfterPause 续跑并转发 chunk,完成后追加历史 + 后处理(同 chat 尾处理)。
425
+ * 硬停止(signal.abort)仍是唯一霸道中止路径,与软暂停严格区分。
351
426
  *
352
- * 幂等性:_chatBusy 已 false 时 no-op(多次调用安全)
427
+ * 归属:补充输入归属当前问答闭环(复用 prepare 分配的闭环节点 roundId,不分裂新轮),
428
+ * 以 kind 区分交互类型(question-answer=主动提问回答 / supplement=暂停/流中补充)。
353
429
  *
354
- * 使用约束:
355
- * - 仅在宿主确认 generator 挂起(如无进展超时)后调用
356
- * - 不应在常规 abort 路径调用(常规 abort 走 signal,generator 自然退出)
357
- * - 调用后不应再消费原 chat() generator 的后续 chunk(token 已变,chunk 无意义)
430
+ * @param input - 可选补充输入(空=续跑原路径;有=注入修正后续轮)
431
+ * @param signal - 可选 AbortSignal(硬停止仍走此路径)
432
+ * @param kind - 交互输入类型(默认 supplement;对主动提问的回答传 'question-answer')
358
433
  */
359
- forceReleaseChatLock() {
360
- this.chatLockManager?.forceRelease();
434
+ async *resumeExecution(input, signal, kind = 'supplement') {
435
+ const resumeStatus = this._sessionManager?.status;
436
+ // 错误态续跑须明确失败而非静默吞没:error 态由检查点恢复回填进入,运行时异常走
437
+ // yield { type: 'error' } 不翻状态机,生产链路不会自然离开,必须显式提示重新开始
438
+ if (resumeStatus === 'error') {
439
+ this.emit(AGENT_EVENTS.sessionResumeFailed, {
440
+ sessionId: this._sessionManager?.getCheckpoint()?.sessionId,
441
+ reason: '会话处于错误态,无法续跑',
442
+ });
443
+ yield {
444
+ type: 'error',
445
+ message: '当前会话处于错误态,无法续跑,请重新开始',
446
+ category: 'unknown',
447
+ };
448
+ return;
449
+ }
450
+ if (resumeStatus !== 'paused')
451
+ return;
452
+ // resumeExecution 与 chat() 同构,必须加并发锁
453
+ const lockCtx = this.acquireChatLock(signal);
454
+ const { myToken, combinedSignal, cleanupExternalSignal } = lockCtx;
455
+ try {
456
+ // 流活跃置位:与 chat() 同构(requestPause 空闲守卫判据——续跑也是活跃流)
457
+ this._flowActive = true;
458
+ // 翻状态机为 RUNNING(触发 sessionResumed)
459
+ if (!this.resume()) {
460
+ // resume() 返回 false 时 yield error + 发事件,让宿主感知失败原因而非静默吞没
461
+ this.emit(AGENT_EVENTS.sessionResumeFailed, {
462
+ sessionId: this._sessionManager?.getCheckpoint()?.sessionId,
463
+ reason: 'resume() 返回 false,可能因暂停超时或状态机拒绝',
464
+ });
465
+ yield {
466
+ type: 'error',
467
+ message: '会话已超时,无法自动恢复,请重新开始新对话',
468
+ category: 'timeout',
469
+ };
470
+ return;
471
+ }
472
+ // 预判短路(收窄):仅「计划全部 blocked」才拦——blocked=真停滞,继续调 LLM
473
+ // 只会复读卡住状态、白烧 token。全 done / 无计划不再拦:用户主动点「继续」就是要 AI 产出
474
+ // (可能收尾总结、补建计划、继续语境),且短问答暂停续跑本就无计划,拦了就没法继续聊。
475
+ // 判定下沉 sessionManager.isPlanAllBlocked()(仅全 blocked 拦;全 done/空计划放行)。
476
+ if (!input &&
477
+ this._sessionManager?.isPlanAllBlocked() &&
478
+ !this.requireLoop.isInAutonomousStep) {
479
+ yield {
480
+ type: 'text',
481
+ content: '计划任务项当前全部处于阻塞状态,无法自动推进。请提供新指令或修改计划',
482
+ };
483
+ yield { type: 'done' };
484
+ return;
485
+ }
486
+ // 委托种子编排器:续跑路径闭环(act(continueAfterPause) → reflect;无回答前),
487
+ // 预判短路与锁/状态机守卫留在门面,执行语义收在编排器内。
488
+ yield* this.internals.seedOrchestrator.runResume(input, combinedSignal, kind);
489
+ }
490
+ finally {
491
+ // 流已退出:清除活跃标志(与置位对称;无论正常/异常退出都复位)
492
+ this._flowActive = false;
493
+ // 与 chat() 同构:释放锁 + 清理外部 signal
494
+ this.internals.chatLockManager?.release(myToken);
495
+ cleanupExternalSignal();
496
+ // 续跑轮(resumeExecution)是同一 turn 的收尾半程:turn 真正结束 → 与 chat() 同构,
497
+ // 无条件清空任务表(除非再次暂停——pauseMeta 由 loop 在边界重新挂起,guard 保留)。
498
+ // 必须无条件清:若仅全 done 才清,resume 以非 done 且未重暂停结束(如某任务项标 blocked
499
+ // 后收尾)时 plan 残留 → 跨 turn 污染下一个 chat()。
500
+ // 两处 turn-end 清理统一为 clearPlanOnTurnEnd(单一收口点),plan 严格 turn 内、不跨 turn 残留。
501
+ this.clearPlanOnTurnEnd();
502
+ }
361
503
  }
362
504
  /**
363
- * 召回记忆 + 注入最近对话上下文
505
+ * 消费执行流的单一真理源
364
506
  *
365
- * 从 chat() 中提取,职责:
366
- * 1. 双通道召回(语义 + 关键词)
367
- * 2. Layer 5: 最近对话注入
507
+ * chat() 与续跑(continueAfterPause)必须共用同一份消费逻辑——两份同构实现会漂移
508
+ * (实证:续跑漏 paused 分支导致三方分叉);收口于此,新增 chunk 类型只需改一处。
509
+ * 暂停幂等锁的释放放 finally——清理是"退出本作用域的不变式"而非某分支动作,新增 return 分支无遗漏。
368
510
  *
369
- * @param input 用户输入
370
- * @returns 召回的记忆列表
371
- */
372
- async recallAndInject(input) {
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,
378
- });
379
- let recalledMemories = [];
511
+ * @returns 消费结果;failed 为 true 时调用方应立即 return(错误 chunk 已 yield)
512
+ */
513
+ async *consumeExecutionStream(source, signal) {
514
+ let content = '';
515
+ let aborted = false;
516
+ // 软暂停标记:loop 在 step 边界挂起(用户 requestPause / ask_user 主动提问)时置真,
517
+ // 供编排器据此推迟摘要——回合未完成不产摘要,保摘要与外部输入 1:1
518
+ let paused = false;
380
519
  try {
381
- recalledMemories = await recall(this.pctx.index, input, {
382
- limit: 5,
383
- vectorStore: this.#config.vectorStore,
384
- excludeSources: this.#config.recallExcludeSources,
385
- });
520
+ for await (const chunk of source) {
521
+ // 内核事实驱动:loop 在 step 边界真正挂起时状态机才翻 PAUSED,非申请即翻转;
522
+ // 与 requestPause 空闲分支同源,须传 lowRisk=true 保持同一暂停事件契约(否则流中暂停计入连续暂停配额)
523
+ if (chunk.type === 'paused') {
524
+ const pendingInfo = this._sessionManager?.consumePendingPause();
525
+ // 暂停收口统一写 pauseMeta:reason/source 取自 pendingPause(与状态机一致)。
526
+ // ask_user 主动提问等"直接暂停路径"不走 loop.onPaused,在此自然补齐,
527
+ // 重启后宿主可展示"为什么暂停 + 问了什么"
528
+ const pauseReason = pendingInfo?.reason ?? '用户主动暂停';
529
+ const pauseSource = pendingInfo?.source ?? 'user';
530
+ this.pause(pauseReason, pauseSource, true);
531
+ this._sessionManager?.setPauseMeta({ reason: pauseReason, source: pauseSource });
532
+ paused = true;
533
+ }
534
+ yield chunk;
535
+ // 累积 assistantContent 用于持久化到 history:只累积正常回答(stage≠self_review)。
536
+ // 自审查文本是内部思考过程——宿主渲染进 round-block § 自审查输出折叠区(不进正文流),
537
+ // 也不应持久化到会话历史(重启后不应在对话记录里看到自审查内容)。
538
+ // 注:loop.messages 里自审查 assistant 仍保留(LLM 下一轮迭代可能需参考),过滤只在持久化出口。
539
+ if (chunk.type === 'text' && chunk.stage !== 'self_review') {
540
+ content += chunk.content;
541
+ }
542
+ else if (chunk.type === 'narrate' && chunk.withdrawn) {
543
+ // 回抽:首轮工具步的叙述文本先作为 text 流式累积进正文(上面分支),
544
+ // 确认工具轮后从**持久化正文**扣除——与宿主正文撤回同源(同一 withdrawn 字段,SSOT)。
545
+ // 契约:该段为最近追加的正文文本(后缀);不符(多 turn 复用流/异常序)则忽略并告警,
546
+ // 宁可正文多一段叙述也不误删真实答案。
547
+ if (content.endsWith(chunk.withdrawn)) {
548
+ content = content.slice(0, content.length - chunk.withdrawn.length);
549
+ }
550
+ else {
551
+ logger.warn({ withdrawnLen: chunk.withdrawn.length, contentLen: content.length }, 'narrate 回抽:持久化正文不含该段(后缀契约不符),已忽略');
552
+ }
553
+ }
554
+ else if (chunk.type === 'aborted') {
555
+ aborted = true;
556
+ }
557
+ }
386
558
  }
387
559
  catch (err) {
388
- recallSpan.recordException(err instanceof Error ? err : new Error(String(err)));
389
- throw err;
560
+ if (isAbortError(err)) {
561
+ // signal 已 abort(宿主/插话合并信号)→ 真用户取消:非失败——置 aborted 供 act() 走
562
+ // 「中断保留已产出文本」分支,半截回答落盘。
563
+ if (signal?.aborted) {
564
+ // 锁超时中断(signal.reason=TimeoutError)≠ 用户取消:stopReason 区分,宿主映射超时文案
565
+ yield {
566
+ type: 'aborted',
567
+ reason: isTimeoutAbortSignal(signal)
568
+ ? 'LLM request timed out (no response)'
569
+ : 'User cancelled the conversation',
570
+ stopReason: isTimeoutAbortSignal(signal) ? 'timeout' : 'user',
571
+ };
572
+ return { content, aborted: true, paused, failed: false };
573
+ }
574
+ // signal 未 abort 却抛 AbortError → provider/网络内部中断(连接被抽断/代理异常):
575
+ // 非用户取消,判为「连接中断」——与普通错误同归 error 分支,用结构化 category
576
+ // 标记连接中断(宿主按 category 映射友好文案),避免谎报为用户取消。
577
+ logger.warn({ err }, 'LLM 请求非用户取消原因中断,判为连接中断');
578
+ const raw = err instanceof Error ? err.message : String(err);
579
+ yield {
580
+ type: 'error',
581
+ // 连接中断分类:语义走 category 字段,不携带裸前缀;message 保留原始细节(调试/报错可追溯)
582
+ category: 'connection',
583
+ message: raw,
584
+ };
585
+ return { content, aborted, paused, failed: true };
586
+ }
587
+ // 超时(典型 = SSE 停摆看门狗 reject 的 DOMException TimeoutError)补结构化分类:
588
+ // 不补 → 宿主 `friendlyByCategory` 无键可映射 → 用户只能读到原始技术文案。
589
+ // 判据 SSOT = isTimeoutError(与上方 isTimeoutAbortSignal 同族,同一 name 判据、不同载体)。
590
+ // 注:非 abort 路径故不入 aborted 分支,它属「失败」并由本 catch 收口。
591
+ yield {
592
+ type: 'error',
593
+ ...(isTimeoutError(err) ? { category: 'timeout' } : {}),
594
+ message: err instanceof Error ? err.message : String(err),
595
+ };
596
+ return { content, aborted, paused, failed: true };
390
597
  }
391
598
  finally {
392
- recallSpan.setAttribute('resultCount', recalledMemories.length);
393
- recallSpan.end();
394
- }
395
- if (recalledMemories.length > 0) {
396
- this.emit('memoryRecalled', { count: recalledMemories.length, query: input });
599
+ // 释放暂停幂等锁覆盖三路——残留会让 requestPause 的幂等检查永久拒绝后续暂停请求
600
+ this._sessionManager?.cancelPendingPause();
601
+ // 同步清理 loop 的 pause 申请(clearPauseRequest 委托 interruptQueue 过滤 kind='pause' 条目)
602
+ this.requireLoop.clearPauseRequest();
397
603
  }
398
- // Layer 5: 最近对话注入
399
- const loop = this.loop;
400
- const recentHistory = loop.getRecentHistory(3);
401
- if (recentHistory.length > 0) {
402
- const msgs = this.#config.messages;
403
- const label = msgs?.recentConversationLabel ?? '[Recent conversation]';
404
- const userLabel = msgs?.userLabel ?? 'User';
405
- const assistantLabel = msgs?.assistantLabel ?? 'Assistant';
406
- const recentPrompt = `${label}\n` +
407
- recentHistory
408
- .map((m) => `${m.role === 'user' ? userLabel : assistantLabel}:${m.content}`)
409
- .join('\n');
410
- loop.injectSystemMessage(recentPrompt);
411
- logger.debug({ turns: recentHistory.length / 2 }, '最近对话已注入');
412
- }
413
- return recalledMemories;
604
+ return { content, aborted, paused, failed: false };
414
605
  }
415
606
  /**
416
- * 注入上一轮匹配的技能 prompt
607
+ * 暂停会话(用户/Agent/系统均可触发,暂停前自动创建检查点)
608
+ * @param lowRisk 低风险暂停不计入连续暂停计数(默认 false)
417
609
  */
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 已注入');
424
- }
425
- this.activeSkill = null;
426
- }
610
+ pause(reason, source = 'user', lowRisk = false) {
611
+ this.assertInitialized('pause');
612
+ return this.requireSessionManager.pause(reason, source, lowRisk);
427
613
  }
428
614
  /**
429
- * 对话后处理:用户画像归档、角色匹配、技能匹配、Insight 提取
430
- *
431
- * 所有归档/匹配操作均为 best-effort:任何子步骤失败不应影响用户已收到的回答,
432
- * 失败仅记录日志,不向上抛出异常。
615
+ * 恢复会话(从 PAUSED 恢复到 RUNNING)
616
+ */
617
+ resume() {
618
+ this.assertInitialized('resume');
619
+ return this.requireSessionManager.resume();
620
+ }
621
+ /**
622
+ * 自动恢复暂停状态(chat() 与续跑共用):
623
+ * ERROR 态直接 throw(拒绝,须先 recover 保持状态一致);PAUSED 态尝试自动恢复。
624
+ * 续跑(continueAfterPause)不走自动恢复,由独立的暂停恢复短路逻辑处理。
433
625
  *
434
- * ADR-015 归档模式控制:
435
- * - 角色匹配 + 技能匹配不受 archiveMode 影响(每轮都执行,非归档行为)
436
- * - `manual` 模式跳过所有自动归档(profile + insight),需用户手动调用
437
- * archiveProfileFacts() / archiveInsight() 触发
438
- * - `full` / `insights-only` 模式下 profile + insight 都自动归档
439
- * (会话归档实现后,`insights-only` 将跳过对话原始内容自动归档)
440
- */
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);
626
+ * @returns true=恢复成功或无需恢复;false=恢复失败(暂停超时等)
627
+ */
628
+ autoResumeIfPaused() {
629
+ const sm = this._sessionManager;
630
+ if (!sm)
631
+ return true;
632
+ const status = sm.status;
633
+ if (status === 'error') {
634
+ throw configError('会话异常', '会话处于异常状态,无法接收新消息', [
635
+ '先标记 error.recovered=true 并调用 agent.recover()',
636
+ ]);
449
637
  }
450
- finally {
451
- span.end();
638
+ if (status === 'paused') {
639
+ return this.resume();
452
640
  }
641
+ return true;
453
642
  }
454
643
  /**
455
- * postProcess 内部实现(为 span 埋点提供 try/finally 包裹边界)
644
+ * 请求软暂停(不中断工作模型 v2.1)
645
+ *
646
+ * 经 sm.requestPause 暂存 pending(SessionStateMachine 是 pending 真理源)+ loop.pauseRequested
647
+ * 入 interruptQueue(在下一 step 边界挂起,不 abort)。
648
+ * 状态机翻 PAUSED 延后到 loop 边界真正挂起时——内核事实驱动而非申请即翻转;与硬停止(abort)严格区分,
649
+ * 软暂停保留历史、可经 resumeExecution 续跑。
456
650
  *
457
- * 原 postProcess 逻辑完整保留于此,由外层 postProcess 负责 span 生命周期管理。
651
+ * @returns true=请求已注册(下一 step 边界生效);false=无法暂停
458
652
  */
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
- }
653
+ requestPause(reason, source = 'user') {
654
+ this.assertInitialized('requestPause');
655
+ const sm = this._sessionManager;
656
+ if (!sm)
657
+ return false;
658
+ // 状态机阻止:已暂停/已异常时禁止再申请暂停
659
+ const status = sm.status;
660
+ if (status === 'paused') {
661
+ logger.debug('requestPause 忽略:会话已处于暂停状态');
662
+ return false;
491
663
  }
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
- }
664
+ if (status === 'error') {
665
+ logger.debug('requestPause 忽略:会话处于异常状态,无法暂停');
666
+ return false;
506
667
  }
507
- // ADR-015: manual 模式跳过所有自动归档(profile + insight),
508
- // 需用户手动调用 archiveProfileFacts/archiveInsight 触发。
509
- // 角色匹配/技能匹配/AutoConfigRefiner 属"配置学习"行为,非归档,每轮都执行。
510
- const skipAutoArchive = this.#config.archiveMode === 'manual';
511
- if (skipAutoArchive) {
512
- logger.debug({ mode: 'manual' }, '归档模式为 manual,跳过自动归档');
668
+ // 空闲守卫:无活跃流时暂停无消费方——若此时同步翻状态机并落盘 checkpoint,
669
+ // 「turn 已完成后的暂停」会把会话钉在 paused,后续新输入被宿主路由成
670
+ // supplement(新意图被吞成"上一个回答的补充",毒化闭环节点;loop 无流消费的延迟翻转亦无意义)。
671
+ // 任务已结束 = 暂停申请作废,返回 false 供宿主明确反馈;系统/Agent 触发(ask_user/drift)均在
672
+ // 流中(_flowActive=true)不经过此分支,不受影响。
673
+ // ⚠ 判据必须用 _flowActive(流活跃)而非 chatLock.isBusy(锁持有)——chatLock 锁
674
+ // 180s 超时后自动释放(不中断生成流),长任务运行超锁期后 isBusy=false 但流
675
+ // 仍在产出,用锁判据会把「进行中的暂停申请」误作废成「任务已结束」;_flowActive 由 chat()/resumeExecution()
676
+ // 的流生命周期显式维护,与锁超时无关,是「任务是否仍在运行」的准确判据。
677
+ if (!this._flowActive) {
678
+ logger.debug({ reason, source }, 'requestPause 空闲守卫:任务已结束,暂停申请作废');
679
+ return false;
513
680
  }
514
- // 用户画像实时归档(委托 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
- }
681
+ // 委托 SessionStateMachine 管理 pending 暂停状态(含幂等检查)
682
+ if (!sm.requestPause(reason, source)) {
683
+ logger.debug('requestPause 忽略:已有待处理的暂停请求');
684
+ return false;
531
685
  }
532
- // 输入分类 → 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
- }
686
+ this.requireLoop.requestPause();
687
+ return true;
688
+ }
689
+ /**
690
+ * 取消待处理的软暂停请求:清空 loop 的 pauseRequested 标志(与 requestPause 对称),
691
+ * 并同步清理 SessionStateMachine 的 pending 暂停状态。
692
+ */
693
+ cancelPauseRequest() {
694
+ this.assertInitialized('cancelPauseRequest');
695
+ this.requireLoop.clearPauseRequest();
696
+ this._sessionManager?.cancelPendingPause();
697
+ }
698
+ /**
699
+ * 是否存在待处理的暂停申请(申请已发、尚未在 loop 边界挂起)
700
+ *
701
+ * 供宿主 UI 区分三态:无申请(暂停)/ 申请在途(取消暂停)/ 已暂停(继续)。
702
+ * 状态真理源 SessionStateMachine.isPausePending():流中 requestPause 置位、边界挂起后 consumePendingPause 消费、
703
+ * cancelPauseRequest 主动清理;空闲态由 requestPause 空闲守卫作废(见上,return false 不触达状态机)——
704
+ * isPausePending 仅在流中「申请在途」时为 true(空闲态不翻 PAUSED)。
705
+ */
706
+ isPausePending() {
707
+ return this._sessionManager?.isPausePending() ?? false;
708
+ }
709
+ /**
710
+ * 放弃当前暂停检查点(宿主 stop 语义的内核出口)
711
+ *
712
+ * 与 pause() 对称:pause 创建检查点;discardCurrentCheckpoint 销毁检查点。
713
+ * 宿主 handleStop 在 paused 态调用——用户决定彻底放弃暂停执行,不再 resume。
714
+ *
715
+ * 除转发 SessionManager.discardCheckpoint(清内存 + 状态机 resetToRunning——检查点不落盘,无存储清理动作),
716
+ * 还须协同 loop.clearPendingInterjections() 清排队插话——pause 生效时 step 边界优先返回不消费 queue,
717
+ * 只清 checkpoint 会留孤儿 queue,下次 chat() 首个 step 误消费残留(坑)。
718
+ * 返回值以 checkpoint 清理为主(queue 有残留不判失败)。
719
+ * @returns true=成功清理;false=无暂停检查点(空闲态调了个空)
720
+ */
721
+ discardCurrentCheckpoint() {
722
+ this.assertInitialized('discardCurrentCheckpoint');
723
+ // 协同清理暂停上下文的两个组成部分:checkpoint(内存态暂停点,不落盘)+ pendingInterjections(排队插话)
724
+ // 暂停上下文 = checkpoint + queue,stop 语义下两者必须同清,不留孤儿
725
+ const checkpointCleared = this._sessionManager?.discardCheckpoint() ?? false;
726
+ // queue 清理独立:即使 checkpoint 已不存在(空闲态调),残留 queue 也应顺手清掉
727
+ const queueCleared = this.loop?.clearPendingInterjections() ?? 0;
728
+ if (queueCleared > 0) {
729
+ logger.info({ count: queueCleared }, 'discardCheckpoint 协同清理残留 pendingInterjections');
548
730
  }
549
- // AutoConfigRefiner(模式 3:Agent 智能总结)
550
- if (this.autoConfigRefiner) {
551
- try {
552
- // 注册到 pendingArchives,确保 close() 时等待后台分析完成,避免写入已关闭的存储
553
- const analyzePromise = this.autoConfigRefiner.analyze(input, assistantContent).catch((err) => {
554
- logger.warn({ err }, 'AutoConfigRefiner 分析失败');
555
- });
556
- history.registerPendingArchive(analyzePromise);
557
- }
558
- catch (err) {
559
- logger.warn({ err }, 'AutoConfigRefiner 初始化失败');
560
- }
731
+ return checkpointCleared;
732
+ }
733
+ /**
734
+ * 执行中插话(申请):把用户补充输入排队,当前 step 完成后在 step 边界统一注入为 user 消息。
735
+ *
736
+ * 与 requestPause(step 边界挂起待续跑)同为「申请 → 气口生效」——不中断当前 LLM/工具执行,
737
+ * 只在边界拿到补充输入后开始下一轮 step。插话是唯一写入口(interject → pendingInterjections 排队),
738
+ * 无「立即中断」模式:只有单一申请模式(无 inputInterrupt 立即中断通道)。
739
+ *
740
+ * 归属:插话同时以「补充」交互输入持久化到当前闭环节点(interactiveInputs),
741
+ * 保证跨重启重放时插话内容不丢失、不分裂新轮。持久化 fire-and-forget(appendUser 内部
742
+ * 已 catch 写入失败,仅记日志),不阻塞插话本身。
743
+ */
744
+ interject(content) {
745
+ this.assertInitialized('interject');
746
+ // 持久化插话为闭环节点交互补充输入(roundId 非空时才写;空=无在途闭环,跳过)
747
+ const closureRoundId = this.loop?.getCurrentRoundId();
748
+ if (closureRoundId) {
749
+ void this.history?.appendUser(content, closureRoundId, {
750
+ interactive: true,
751
+ kind: 'supplement',
752
+ });
561
753
  }
754
+ this.requireLoop.interject(content);
755
+ }
756
+ /**
757
+ * 删除待注入的插话(宿主 UI 层用户后悔)。与 interject 对称。
758
+ * 注意:interject 时已持久化到 history(交互输入归属),此处只从内核队列移除,不删除已落盘记录——
759
+ * 未来若需完整"撤回"需额外方案(如软删标记),当前先保证 step 边界不会注入已删除的补充。
760
+ * @param index 要删除的插话在队列中的位置(宿主镜像数组与内核队列同序同长度)
761
+ * @returns true=成功删除;false=index 越界或队列为空
762
+ */
763
+ removePendingInterject(index) {
764
+ this.assertInitialized('removePendingInterject');
765
+ return this.requireLoop.removePendingInterject(index);
766
+ }
767
+ /**
768
+ * 清空全部待注入插话(宿主「全部清空」按钮触发,或 stop→discard 协同清理)。
769
+ *
770
+ * 逻辑下沉 loop.clearPendingInterjections 原子方法——清队列唯一入口在内核,
771
+ * 宿主不维护队列镜像,避免镜像与内核双写不一致;discardCurrentCheckpoint 经同一方法协同清理。
772
+ * @returns 被清除的条目数(宿主可用于 notice 反馈;无队列时返回 0)
773
+ */
774
+ clearPendingInterjections() {
775
+ this.assertInitialized('clearPendingInterjections');
776
+ return this.requireLoop.clearPendingInterjections();
777
+ }
778
+ /**
779
+ * 读取当前待注入插话队列快照(宿主渲染层只读镜像,不修改内核状态)。
780
+ * 返回副本,宿主无法暗改内核 queue;宿主渲染直接读本快照,不另维护队列镜像。
781
+ */
782
+ getPendingInterjections() {
783
+ this.assertInitialized('getPendingInterjections');
784
+ return this.requireLoop.getPendingInterjections();
785
+ }
786
+ /**
787
+ * 放弃在途主动提问(ask_user 工具挂起后,宿主「跳过/取消提问」时调用):
788
+ * 委托 loop.cancelAsk 补占位 tool 结果(防 assistant.tool_calls 无配对 → 400),
789
+ * 随后 resumeExecution() 续跑。与 answerQuestion 二选一消费在途提问。
790
+ */
791
+ cancelAsk() {
792
+ this.assertInitialized('cancelAsk');
793
+ this.requireLoop.cancelAsk();
794
+ }
795
+ /**
796
+ * 回答在途主动提问(ask_user 工具):
797
+ * 答案以 ask_user 的 tool result 回填(与 assistant.tool_calls 配对,结构合法)。
798
+ * 调用后宿主以 resumeExecution(回答文本, undefined, 'question-answer') 续跑——
799
+ * 回答文本同时作为新 user 输入注入并记录为闭环节点交互输入(round 不分裂),
800
+ * 与既有续跑主流程一致(本方法只负责结构化回填,不做历史记录,避免双写)。
801
+ * 返回 false 表示无在途提问(提供方未调用 / 已消费)。
802
+ */
803
+ answerQuestion(answers) {
804
+ this.assertInitialized('answerQuestion');
805
+ return this.requireLoop.answerQuestion(answers);
806
+ }
807
+ /**
808
+ * 追加计划任务项(SESSION_APPEND_TASK 落点):在现有 plan 末尾追加新任务项
809
+ */
810
+ appendPlanItem(description) {
811
+ this.assertInitialized('appendPlanItem');
812
+ const sm = this.requireSessionManager;
813
+ return sm.appendPlanItem(description);
814
+ }
815
+ /**
816
+ * 卸载运行态挂载物:清空检查点计划与任务项推进日志(宿主任务流结束/停止/异常广播 idle 前调用),
817
+ * 回到"空闲 = 无挂载物"的资源层常态;会话历史与记忆等资源层内容不受影响。
818
+ */
819
+ clearPlan() {
820
+ this.assertInitialized('clearPlan');
821
+ this.requireSessionManager.clearPlan();
822
+ }
823
+ /**
824
+ * turn 结束自动收尾:非暂停态 → 无条件 clearPlan(任务表收紧为 turn 内能力,不跨 turn 残留)。
825
+ *
826
+ * 任务表生命周期 = 单个 turn:LLM 在 turn 内编排并完成任务表;chat() 流退出(turn 真正结束)
827
+ * 时,无论任务项是否全部标记完成,都清空运行时挂载物(checkpoint.plan 与 planItemLog)。这样:
828
+ * ① 宏任务一个 turn 完不成 → 本 turn 结束兜底丢弃,下个 turn 由 LLM 重新规划全新任务表;
829
+ * ② 续跑轮(resumeExecution = ask_user 问答/暂停补充)是同一 turn 的闭环收尾,与 chat() 同构
830
+ * 走本方法——turn 真正结束即无条件清空,chat() 与续跑轮共用本收口点。
831
+ *
832
+ * 唯一例外是暂停态(pauseMeta):turn 尚未真正结束,暂停恢复后要继续用 plan 推进,
833
+ * 故保留不清;恢复完成后该 turn 最终退出时仍会走本方法无条件清空。
834
+ *
835
+ * 运行时状态(plan/planItemLog)清空,但对话记录里的 round-block 折叠块已沉淀为历史(不落盘删除)。
836
+ * 挂点:chat() 与 resumeExecution 的 finally 块(generator close 时触发,确保所有 yield 已被宿主消费)
837
+ */
838
+ clearPlanOnTurnEnd() {
839
+ const sm = this._sessionManager;
840
+ if (!sm)
841
+ return;
842
+ const cp = sm.getCheckpoint();
843
+ if (!cp?.plan || cp.plan.length === 0)
844
+ return;
845
+ if (cp.pauseMeta)
846
+ return; // 暂停态:turn 未真正结束,恢复时需继续用 plan,保留不清
847
+ sm.clearPlan();
848
+ }
849
+ /**
850
+ * 内核→宿主信号:当前会话是否可"无输入续跑"(决定暂停按钮显隐 + 暂停后继续 UI)
851
+ *
852
+ * 真值条件:① 状态机已 paused(已软暂停必可续跑,最高优先级——pauseRequested 在下一 step 边界才挂起,
853
+ * 彼时 inAutonomousStep 已重置为 false,仅看它会误报);② loop 在自主工具步(可暴露暂停按钮在边界挂起);
854
+ * ③ 存在未完成的计划任务项(多轮任务可续跑下一轮)。三者皆否(纯单轮、无待续目标)→ 隐藏暂停按钮(仅停止)。
855
+ */
856
+ canContinueWithoutInput() {
857
+ if (this._sessionManager?.status === 'paused')
858
+ return true;
859
+ // 错误态不展示"继续":error 态由检查点恢复回填进入,须显式处理(重新开始或 recover),避免点了静默无反应
860
+ if (this._sessionManager?.status === 'error')
861
+ return false;
862
+ // 仅 pending/active(可推进)任务项计入"可续跑"——blocked 不续(全 blocked 由 isPlanAllBlocked 拦,
863
+ // 避免全 blocked 计划按钮可点但 resumeExecution 早退)。判定走 SessionManager.hasInflightPlan
864
+ // 单一真理源(禁内联谓词,SSOT 见 sessionManager.ts hasInflightPlan 注释),与 prepare/loop 同源。
865
+ const hasPendingPlan = this._sessionManager?.hasInflightPlan() ?? false;
866
+ return this.requireLoop.isInAutonomousStep || hasPendingPlan;
867
+ }
868
+ /**
869
+ * 触发会话异常(公开 API,宿主显式调用):仅 RUNNING 状态可触发,异常时自动创建检查点保存状态。
870
+ * 生产内部无调用者——运行时异常(LLM 超时/工具失败)走 yield {type:'error'} 事件流不翻状态机;
871
+ * 本 API 供宿主在自定义异常(如外部服务故障)显式触发,或经检查点恢复回填进入 ERROR 态。
872
+ */
873
+ triggerError(cause) {
874
+ this.assertInitialized('triggerError');
875
+ return this.requireSessionManager.triggerError(cause);
562
876
  }
563
877
  /**
564
- * 发送用户消息,非流式返回完整回复
878
+ * 从异常恢复:校验恢复条件(error.recovered === true 且 cause 已解除)
565
879
  */
566
- async chatSync(input, signal) {
567
- let result = '';
568
- for await (const chunk of this.chat(input, signal)) {
569
- if (chunk.type === 'text') {
570
- result += chunk.content;
571
- }
880
+ recover() {
881
+ this.assertInitialized('recover');
882
+ return this.requireSessionManager.recover();
883
+ }
884
+ /**
885
+ * 创建会话检查点:快照当前运行时状态(热记忆等),生成纯内存态快照
886
+ * (不落盘、无序列化/恢复路径——中止/断电走「中断轮补全为完整 turn」)。
887
+ * 无 role/standard 会话能力位,createCheckpoint 仅收 mainGoal。
888
+ */
889
+ createCheckpoint(mainGoal) {
890
+ this.assertInitialized('createCheckpoint');
891
+ return this.requireSessionManager.createCheckpoint(mainGoal);
892
+ }
893
+ /**
894
+ * 获取当前检查点
895
+ */
896
+ getCheckpoint() {
897
+ this.assertInitialized('getCheckpoint');
898
+ return this.requireSessionManager.getCheckpoint();
899
+ }
900
+ /**
901
+ * 获取最近一次处理线程的工具执行历史(loop 域聚合出口,补待办 #3)
902
+ *
903
+ * 数据源自 sessionManager 检查点的 completedToolCalls(已由
904
+ * AGENT_CONSTANTS.COMPLETED_TOOL_CALLS_MAX 做 FIFO 封顶)。本方法剥离「恢复/幂等」内部
905
+ * 语义,提供稳定的 execute 域只读契约——宿主可在透明面板/观测面消费本轮及历史的工具调用
906
+ * 明细(工具名 / 成败 / 结果摘要),与累计 AgentMetrics.tools 互补。
907
+ *
908
+ * @param limit 返回条数上限(默认取封顶上限,超界由调用方控制,取最近 limit 条)
909
+ * @returns 工具执行记录(记录顺序:时间正序即旧→新;无记录返回空数组;返回浅拷贝防宿主改动检查点)
910
+ */
911
+ getRecentToolExecutions(limit = AGENT_CONSTANTS.COMPLETED_TOOL_CALLS_MAX) {
912
+ this.assertInitialized('getRecentToolExecutions');
913
+ const calls = this.requireSessionManager.getCheckpoint()?.completedToolCalls;
914
+ if (!calls || calls.length === 0)
915
+ return [];
916
+ // 取最近 limit 条并浅拷贝,避免宿主引用改动污染检查点(FIFO 封顶后 slice 安全)
917
+ return calls.slice(-limit).map((r) => ({ ...r }));
918
+ }
919
+ /**
920
+ * 向 loop 注入 system 消息
921
+ */
922
+ injectSystemMessage(message) {
923
+ this.requireLoop.injectSystemMessage(message);
924
+ }
925
+ /**
926
+ * 从指定 Round 位置分叉当前会话(委托至 SessionManager):
927
+ * round-based 模式唯一分叉方式——创建新会话,复制 Round ID 列表(指针复制),
928
+ * 新会话是完全平等的普通会话。记忆索引全局共享不受影响。
929
+ *
930
+ * 自动命名场景(未传 targetSession):分叉后异步触发 SessionNamer 为新会话生成标题
931
+ * (best-effort,不阻塞分叉返回);用户显式指定会话名时尊重用户命名,不触发 LLM 覆盖。
932
+ *
933
+ * @param roundId - 分叉点的 Round ID(可选;不传默认使用最后一个 Round)
934
+ * @param targetSession - 可选,自定义新会话名
935
+ */
936
+ forkSession(roundId, targetSession) {
937
+ this.assertInitialized('forkSession');
938
+ const result = this.requireSessionManager.forkSession(roundId, targetSession);
939
+ // 分叉会话首轮命名:仅自动命名(未传 targetSession)时触发,
940
+ // 以分叉点首轮用户消息为标题依据(与新会话首次问答命名同源语义)
941
+ if (!targetSession) {
942
+ void this.ensureForkSessionTitle(result).catch((err) => {
943
+ logger.debug({ err, session: result.newSession }, '分叉会话首轮命名失败(best-effort,不影响分叉结果)');
944
+ });
572
945
  }
573
946
  return result;
574
947
  }
575
948
  /**
576
- * 分叉当前会话(委托至 SessionManager)
949
+ * 分叉会话首轮命名(best-effort,fire-and-forget)
577
950
  *
578
- * 分叉后原会话完整保留,新分支拥有独立消息历史。
579
- * 记忆索引(IMemoryStorage)全局共享,不受分叉影响。
951
+ * 分叉新会话是平等普通会话,自动命名(未提供 targetSession)时应拥有 LLM 生成的语义标题。
952
+ * 取新会话第一个 Round 的用户消息作为命名输入(分叉点即新会话起点,语义=首轮问答);
953
+ * SessionNamer 内部已有 autoName 存在即跳过(幂等),LLM 失败/无价值降级占位标题。
954
+ *
955
+ * @param result - 分叉结果(含新会话名/日期/Round 列表)
580
956
  */
581
- forkSession(targetSession) {
582
- this.assertInitialized('forkSession');
583
- return this._sessionManager.forkSession(targetSession);
957
+ async ensureForkSessionTitle(result) {
958
+ const namer = this.internals.sessionNamer;
959
+ if (!namer) {
960
+ logger.debug('ensureForkSessionTitle: sessionNamer 未创建,跳过分叉会话命名');
961
+ return;
962
+ }
963
+ const roundStore = this.#config.roundStore;
964
+ const firstRoundId = result.roundIds[0];
965
+ if (!roundStore || !firstRoundId) {
966
+ logger.debug({ hasRoundStore: !!roundStore }, 'ensureForkSessionTitle: 无 RoundStore/新会话为空,跳过');
967
+ return;
968
+ }
969
+ // 分叉点首轮用户消息 = 新会话的"首条消息",作为标题生成输入
970
+ const firstRound = roundStore.getById(firstRoundId);
971
+ const firstUserContent = firstRound?.userMessage?.content;
972
+ if (!firstUserContent) {
973
+ logger.debug({ roundId: firstRoundId }, 'ensureForkSessionTitle: 首轮无用户消息,跳过');
974
+ return;
975
+ }
976
+ await namer.ensureSessionTitle(result.date, result.newSession, firstUserContent);
584
977
  }
585
978
  /**
586
- * 切换到指定项目
979
+ * 切换到指定项目:切换后自动 rebuildComponents();Agent 级记忆存储(宿主注入,实例不随项目切换重建)保留,项目级配置(.memora/)重新加载
587
980
  *
588
- * 切换后自动 rebuildComponents(),无需手动调用。
589
- * Agent 级记忆(memora.db)保留,项目级配置(.memora/)重新加载。
981
+ * `nameOrPath` 只接受两种输入:① 项目注册表中的项目名(依赖注册表跨项目共享,见 dataDir/registryDir);
982
+ * ② 项目根目录的**绝对路径**。二者都不满足时抛 configError——否则相对串会以 process.cwd()
983
+ * 为基准解析,静默造出非预期的项目目录并占用其锁。
590
984
  */
591
985
  async switchProject(nameOrPath) {
592
986
  this.assertInitialized('switchProject', ['projectManager', 'provider']);
593
987
  // 对话进行中切换项目会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
594
- if (this.chatLockManager?.isBusy) {
595
- throw chatBusyError('切换项目');
596
- }
988
+ this.assertNotBusy('切换项目');
597
989
  const pm = this.projectManager;
598
990
  const projects = pm.list;
599
991
  let target = projects.find((p) => p.name === nameOrPath || p.path === nameOrPath);
@@ -601,15 +993,34 @@ export class Agent extends TypedEventEmitter {
601
993
  const nameOrPathLower = nameOrPath.toLowerCase();
602
994
  target = projects.find((p) => p.name.toLowerCase() === nameOrPathLower || p.path.toLowerCase() === nameOrPathLower);
603
995
  }
996
+ // 注册表未命中时按路径处理。相对路径会以 process.cwd() 为基准解析——宿主进程的 cwd
997
+ // 不具项目语义,会静默创建出非预期的项目目录并占用其锁,故只接受绝对路径。
998
+ if (!target && !isAbsolute(nameOrPath)) {
999
+ throw configError('项目切换失败', `「${nameOrPath}」既不是已注册的项目名,也不是绝对路径`, [
1000
+ '按名切换:先经 agent.projects.registerProject(项目路径, 项目名) 注册,并保证注册表跨项目共享(多项目共用同一 dataDir 或 registryDir)',
1001
+ '按路径切换:传入项目根目录的绝对路径',
1002
+ ]);
1003
+ }
604
1004
  const projectPath = target ? target.path : nameOrPath;
605
1005
  const projectName = target ? target.name : getBaseName(nameOrPath);
606
1006
  const newPctx = await pm.initProject(projectPath, projectName, this.#config.configDir);
607
1007
  // 记录源项目路径(用于事件),切换前 pctx 可能不存在(首次初始化)
608
1008
  const fromProjectPath = this.pctx?.projectPath ?? null;
609
- this.pctx = newPctx;
610
- await this.rebuildComponentsWithCurrentCtx();
1009
+ // 先尝试重建组件,失败时回滚 pctx 防止状态不一致
1010
+ try {
1011
+ this.pctx = newPctx;
1012
+ await this.rebuildComponents();
1013
+ }
1014
+ catch (err) {
1015
+ // 回滚:恢复旧 pctx(若存在)
1016
+ if (fromProjectPath) {
1017
+ logger.warn({ err: toError(err) }, 'switchProject: 组件重建失败,回滚项目上下文');
1018
+ this.pctx = { projectPath: fromProjectPath };
1019
+ }
1020
+ throw err;
1021
+ }
611
1022
  // 发射项目切换事件(供宿主 UI 刷新项目相关界面)
612
- this.emit('projectSwitched', {
1023
+ this.emit(AGENT_EVENTS.projectSwitched, {
613
1024
  from: fromProjectPath,
614
1025
  to: projectPath,
615
1026
  projectName,
@@ -618,246 +1029,384 @@ export class Agent extends TypedEventEmitter {
618
1029
  }
619
1030
  // ─── 组件组装 ─────────────────────────────────────────
620
1031
  /**
621
- * 组装所有运行时组件(委托给 assembler 工厂)
1032
+ * 组装所有运行时组件(一行委托给 assembler 工厂)
1033
+ *
1034
+ * 接线回调(工具幂等 outbox / 任务表 / loop 回调 / createSessionManager)与装配私有状态
1035
+ * (暂存队列 / 停滞计数器)全部由 assembler 组装收口——Agent 只注入稳定能力(hooks)。
622
1036
  */
623
1037
  async assembleComponents(pctx) {
624
1038
  const result = await assembleComponents(pctx, {
625
1039
  provider: this.provider,
626
1040
  backgroundProvider: this.#backgroundProvider,
1041
+ providerRouter: this.#providerRouter,
627
1042
  projectPath: this.#config.projectPath,
628
1043
  configDir: this.#config.configDir,
629
- personaName: this.#config.personaName,
1044
+ activeRolePack: this.#config.activeRolePack,
1045
+ // 组数据(宿主装配级):组长角色包 + 组员名单(会议名单容器)
1046
+ rolePackTeams: this.#config.rolePackTeams,
630
1047
  maxContextTokens: this.#config.maxContextTokens,
1048
+ // per-LLM 输出预算默认值:透传组装器 → AgentLoop.defaultMaxTokens(buildChatOptions 填底座)
1049
+ maxTokens: this.#config.maxTokens,
631
1050
  sessionStore: this.#config.sessionStore,
632
- relationStore: this.#config.relationStore,
1051
+ roundStore: this.#config.roundStore,
633
1052
  tracer: this.#config.tracer,
634
1053
  messages: this.#config.messages,
635
1054
  enableContextSummary: this.#config.enableContextSummary,
1055
+ webSearchProvider: this.#config.webSearchProvider,
1056
+ fetchProvider: this.#config.fetchProvider,
1057
+ codeExecutionProvider: this.#config.codeExecutionProvider,
1058
+ projectSearchProvider: this.#config.projectSearchProvider,
1059
+ // 脚本执行 node 路径(可选):透传装配 → toolExecutor/assembler 脚本执行回调
1060
+ scriptNodePath: this.#config.scriptNodePath,
1061
+ // 禁用技能清单:透传装配 → SkillManager.setDisabledSkills
1062
+ disabledSkills: this.#config.disabledSkills,
636
1063
  existingSkillManager: this.skillManager,
1064
+ // 策略覆盖不经组装器:AssembleInput 零消费,真实消费链 = 本类
1065
+ // 构造 SeedOrchestrator deps 时直传(见 #createSeedOrchestrator 的 strategyOverride),
1066
+ // 经 resolveActiveStrategy 压过角色包声明——策略解析唯一链在 seed 侧。
1067
+ // Agent 稳定能力(emit/守卫/暂停/角色切换):接线下沉后仅传能力,接线语义在组装器唯一实现
1068
+ hooks: {
1069
+ // 桥接 SessionManager 宽类型事件到 Agent 强类型 emit(校验事件名在 AgentEventMap 内,避免不安全断言)
1070
+ emit: (event, data) => {
1071
+ if (AGENT_EVENT_SET.has(event)) {
1072
+ this.emit(event, data);
1073
+ }
1074
+ },
1075
+ isChatBusy: () => this.internals.chatLockManager?.isBusy ?? false,
1076
+ requestPause: (reason, source) => {
1077
+ this.requestPause(reason, source);
1078
+ },
1079
+ preExecutionCheck: this.#config.preExecutionCheck,
1080
+ // 检查点恢复协议角色契约重注入 → Agent 生命周期(工具暴露面 / loop 前缀刷新)
1081
+ applyRolePackToolExposure: () => this.applyRolePackToolExposure(),
1082
+ refreshRolePackPrefixOnLoop: () => this.refreshRolePackPrefixOnLoop(),
1083
+ // 会议逐项切换:随 active 任务项刷新本轮装配视角
1084
+ applyActivePlanItemAssembly: () => this.applyActivePlanItemAssemblyIfChanged(),
1085
+ },
637
1086
  });
638
1087
  this.history = result.history;
639
1088
  this.loop = result.loop;
1089
+ // 有效窗口变更 → 同步 ContextPreparer。有效窗口的**唯一计算点在 loop**
1090
+ // (min(provider 窗口, 角色包 contextLimit)),此处只做分发;用闭包读最新
1091
+ // internals.contextPreparer,避免与装配顺序耦合。
1092
+ this.loop.onContextWindowChanged = (effectiveTokens) => {
1093
+ this.internals.contextPreparer?.setMaxContextTokens(effectiveTokens);
1094
+ };
640
1095
  this.toolExec = result.toolExec;
641
- this.personaManager = result.personaManager;
642
- this.#userProfile = result.userProfile;
643
1096
  this.skillManager = result.skillManager;
644
- this.insightExtractor = result.insightExtractor;
645
- this.configManager = result.configManager;
1097
+ this._rolePackManager = result.rolePackManager;
1098
+ // 角色包切换锁定事件:rolePackManager 触发锁定时,Agent 向宿主发射 rolePackSwitchLocked 事件
1099
+ this._rolePackManager.onRolePackSwitchLocked((reason, lockedSeconds) => {
1100
+ this.emit(AGENT_EVENTS.rolePackSwitchLocked, { reason, lockedSeconds });
1101
+ });
646
1102
  this.memoryInspector = result.memoryInspector;
647
- this.autoConfigRefiner = result.autoConfigRefiner;
1103
+ this.internals.dedupManager = result.dedupManager;
1104
+ this.internals.memoryAdvisor = result.memoryAdvisor;
648
1105
  this.workProjection = result.workProjection;
649
- this.sessionArchiver = result.sessionArchiver;
1106
+ this.internals.sessionArchiver = result.sessionArchiver;
650
1107
  this.textPolisher = result.textPolisher;
651
- // 绑定冲突检测回调,InsightExtractor 检测到 contradicts 时 emit('conflictDetected')
652
- // 与 bindGetRecentHistory 同模式:解决 Agent 晚于 InsightExtractor 创建的时序循环依赖
653
- this.insightExtractor.bindOnConflict((info) => {
654
- this.emit('conflictDetected', info);
655
- });
656
- // 注入 VectorStore 到 MemoryInspector,启用混合搜索
657
- if (this.memoryInspector && this.#config.vectorStore) {
658
- this.memoryInspector.setVectorStore(this.#config.vectorStore);
1108
+ this.internals.roundSummaryGenerator = result.roundSummaryGenerator;
1109
+ // 会话管理器由组装器创建(先于 loop),Agent 直接持有
1110
+ this._sessionManager = result.sessionManager;
1111
+ // 输入增强管线由组装器创建,Agent 只保留编排调用点
1112
+ this.internals.contextPreparer = result.contextPreparer;
1113
+ }
1114
+ // ─── Provider 管理 ────────────────────────────────────
1115
+ setProvider(provider) {
1116
+ // 对话中切换会导致同循环内前后两次 LLM 调用命中不同 Provider(上下文窗口假设/工具格式不一致)
1117
+ this.assertNotBusy('切换 Provider');
1118
+ this.#provider = provider;
1119
+ if (this.loop) {
1120
+ this.loop.setProvider(provider);
659
1121
  }
660
- // 创建会话管理器(提取 createSessionManager 辅助方法,消除重复)
661
- this._sessionManager = this.createSessionManager();
1122
+ logger.info({ provider: this.#provider.name }, 'Provider 已切换');
662
1123
  }
663
1124
  /**
664
- * 用当前 pctx 重建 history / loop
1125
+ * 运行时更新上下文窗口上限(token)
1126
+ *
1127
+ * 模型热切换配套:同步三处装配期值拷贝(loop 截断/软上限、ContextManager 阈值、
1128
+ * ContextPreparer 预算与占用快照),全部对新窗口即刻生效。对话进行中禁止更新
1129
+ * (与 setProvider 同守卫)。
1130
+ *
1131
+ * @param tokens 新窗口 token 数
665
1132
  */
666
- async rebuildComponentsWithCurrentCtx() {
667
- if (!this.pctx)
668
- return;
669
- await this.assembleComponents(this.pctx);
670
- // 重建会话管理器:assembleComponents 创建了新的 history/loop 实例(复用 createSessionManager)
671
- this._sessionManager = this.createSessionManager();
1133
+ setContextWindow(tokens) {
1134
+ this.assertNotBusy('切换上下文窗口');
1135
+ this.#config.maxContextTokens = tokens;
1136
+ if (this.loop) {
1137
+ // loop 内部按 min(provider 窗口, 角色包 contextLimit) 重算**有效窗口**,并经
1138
+ // onContextWindowChanged 回调把有效值同步到 contextPreparer —— 计算点唯一(在 loop)。
1139
+ this.loop.setContextWindow(tokens);
1140
+ }
1141
+ else if (this.internals.contextPreparer) {
1142
+ // loop 尚未装配(早期阶段):无角色策略可叠加,直接用 provider 窗口
1143
+ this.internals.contextPreparer.setMaxContextTokens(tokens);
1144
+ }
1145
+ logger.info({ providerWindow: tokens }, 'provider 上下文窗口已更新');
672
1146
  }
673
1147
  /**
674
- * 创建会话管理器(提取重复的 forwardEvent + SessionManager 构造逻辑)
1148
+ * 运行时更新 per-LLM 输出预算默认值(token)
675
1149
  *
676
- * assembleComponents 和 rebuildComponentsWithCurrentCtx 共享同一套构造逻辑:
677
- * - 通过回调访问当前组件(支持 rebuild 后自动获取最新引用)
678
- * - 事件转发桥接:SessionManager 使用宽类型 (string, Record<string,unknown>),
679
- * Agent 内部桥接到 TypedEventEmitter 的强类型 emit
680
- * - 运行时校验事件名是否在 AgentEventMap 中,避免不安全的类型断言
1150
+ * Provider 热切换配套(与 setContextWindow 同链路):同步 #config 拷贝并即时生效于 loop——
1151
+ * 切换 Provider 后新配置的 maxTokens 无需重装 Agent。undefined = 清除(回服务端默认)。
1152
+ * 不设 assertNotBusy:maxTokens 是请求参数(非上下文结构),轮内下一迭代即用新值,无一致性风险。
681
1153
  *
682
- * @returns 新的 SessionManager 实例
1154
+ * @param tokens 新输出预算(token);undefined 清除
683
1155
  */
684
- createSessionManager() {
685
- const forwardEvent = (event, data) => {
686
- if (AGENT_EVENT_NAMES.has(event)) {
687
- this.emit(event, data);
688
- }
689
- };
690
- return new SessionManager(
691
- // 惰性 getter:SessionManager 内部调用时 Agent 已 init,用 ! 窄化
692
- () => this.history, () => this.loop, this.#config.sessionStore, () => this.chatLockManager?.isBusy ?? false, forwardEvent);
693
- }
694
- // ─── Provider 管理 ────────────────────────────────────
695
- setProvider(provider) {
696
- // 对话进行中切换 Provider 会导致同一 processUserInput 循环内前后两次 LLM 调用命中不同 Provider
697
- // (模型上下文窗口假设不一致 → 可能导致上下文截断逻辑误判或 tool_call 格式不兼容)
698
- if (this.chatLockManager?.isBusy) {
699
- throw chatBusyError('切换 Provider');
700
- }
701
- this.#provider = provider;
1156
+ setMaxTokens(tokens) {
1157
+ this.#config.maxTokens = tokens;
702
1158
  if (this.loop) {
703
- this.loop.setProvider(provider);
1159
+ this.loop.setDefaultMaxTokens(tokens);
704
1160
  }
705
- logger.info({ provider: this.#provider.name }, 'Provider 已切换');
1161
+ logger.info({ maxTokens: tokens }, 'per-LLM 输出预算已更新');
1162
+ }
1163
+ /**
1164
+ * 读取本轮请求体**实际**携带的 max_tokens(生效值),供宿主取证面写 meta。
1165
+ *
1166
+ * 薄委托:优先级裁决收敛在 AgentLoop.buildChatOptions(见其注释),此处不复制规则——
1167
+ * 否则「谁压过谁」会在宿主侧长出第二真理源。loop 未装配时返回 undefined。
1168
+ */
1169
+ getEffectiveMaxTokens() {
1170
+ return this.loop?.getEffectiveMaxTokens();
706
1171
  }
707
1172
  setBackgroundProvider(provider) {
708
1173
  // 与 setProvider 一致,对话进行中禁止切换后台 Provider
709
- if (this.chatLockManager?.isBusy) {
710
- throw chatBusyError('切换后台 Provider');
711
- }
1174
+ this.assertNotBusy('切换后台 Provider');
712
1175
  this.#backgroundProvider = provider;
713
- // 同步更新 AutoConfigRefiner 的后台 Provider
714
- if (this.autoConfigRefiner) {
715
- this.autoConfigRefiner.setBackgroundProvider(provider);
1176
+ // 同步更新所有后台组件(统一消费 backgroundProvider)
1177
+ if (this.internals.roundSummaryGenerator) {
1178
+ this.internals.roundSummaryGenerator.setBackgroundProvider(provider);
1179
+ }
1180
+ if (this.internals.sessionArchiver) {
1181
+ this.internals.sessionArchiver.setBackgroundProvider(provider);
716
1182
  }
717
1183
  logger.info({ hasBackground: !!provider }, '后台 Provider 已切换');
718
1184
  }
719
- // ─── 归档模式管理(ADR-015) ───────────────────────────
1185
+ // ─── 归档模式管理 ───────────────────────────
720
1186
  /**
721
- * 运行时切换归档模式
722
- *
723
- * 与 setProvider 一致,对话进行中禁止切换(避免本轮 postProcess 行为不一致)。
724
- *
725
- * @param mode 目标模式
1187
+ * 运行时切换归档模式:对话进行中禁止切换(避免本轮 postProcess 行为不一致),无变更幂等返回
726
1188
  */
727
1189
  setArchiveMode(mode) {
728
- if (this.chatLockManager?.isBusy) {
729
- throw chatBusyError('切换归档模式');
730
- }
1190
+ this.assertNotBusy('切换归档模式');
731
1191
  const prev = this.#config.archiveMode;
732
1192
  if (prev === mode)
733
1193
  return; // 幂等:无变更直接返回
734
1194
  this.#config.archiveMode = mode;
1195
+ this.emit(AGENT_EVENTS.archiveModeChanged, { from: prev, to: mode });
735
1196
  logger.info({ from: prev, to: mode }, '归档模式已切换');
736
1197
  }
737
1198
  /**
738
- * 查询当前归档模式
739
- *
740
- * 宿主 UI(如设置面板)可据此同步显示当前模式。
1199
+ * 查询当前归档模式(宿主 UI 可据此同步显示)
741
1200
  */
742
1201
  getArchiveMode() {
743
1202
  return this.#config.archiveMode;
744
1203
  }
745
1204
  /**
746
- * 手动触发 profile facts 归档(manual 模式下使用)
747
- *
748
- * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
749
- * full / insights-only 模式下也可调用(会重复归档,但不推荐)。
750
- *
751
- * 归档逻辑已委托给 ArchiveCoordinator
752
- *
753
- * @param input 本轮用户输入
754
- * @returns 写入/更新的 UserProfileEntry 列表
1205
+ * 获取角色包切换锁定状态(透传 RolePackManager,宿主 IPC 层据此区分"切换失败"原因)
1206
+ * @returns locked 是否锁定;unlockAt 锁定自动恢复时间戳(ms epoch),未锁定时为 null
755
1207
  */
756
- async archiveProfileFacts(input) {
757
- this.assertInitialized('archiveProfileFacts');
758
- return this.archiveCoordinator.archiveProfileFacts(input);
1208
+ getRolePackSwitchLockStatus() {
1209
+ this.assertInitialized('getRolePackSwitchLockStatus');
1210
+ if (!this._rolePackManager)
1211
+ return { locked: false, unlockAt: null };
1212
+ return this._rolePackManager.getSwitchLockStatus();
759
1213
  }
760
1214
  /**
761
- * 手动触发 insight 提取(manual 模式下使用)
1215
+ * 获取当前激活角色包的 traits(宿主情感计算 API)
762
1216
  *
763
- * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
764
- * 内部仍走 classify 判断(避免无价值输入浪费 LLM 调用)。
1217
+ * @returns traits 键值对,无激活角色包或无 traits 时返回 undefined
1218
+ */
1219
+ getActiveTraits() {
1220
+ return this._rolePackManager?.getActiveTraits();
1221
+ }
1222
+ /**
1223
+ * 刷新 AgentLoop 的 systemPromptPrefix(SSOT:buildSystemPromptPrefix)
765
1224
  *
766
- * 归档逻辑已委托给 ArchiveCoordinator
1225
+ * 角色包是 system prompt 的唯一注入源。调用时机:手动切换成功后、reloadConfig 重载后;
1226
+ * loop 为 null 时静默跳过(init 前或 close 后边界)。统一走 buildSystemPromptPrefix 真理源,
1227
+ * 全局技能清单也在该前缀内(不能只拼 rolePackPrompt)。等价于「按 activePack 视角刷新」(roundRole=null)。
1228
+ */
1229
+ refreshRolePackPrefixOnLoop() {
1230
+ this.refreshRolePackPrefixForRound(null);
1231
+ }
1232
+ /**
1233
+ * 会议阶梯推进:按当前 active 任务项的 rolePack 派生本轮装配视角。
767
1234
  *
768
- * @param input 本轮用户输入
769
- * @param assistantContent 本轮助手回复内容
770
- * @returns 写入/更新的 Memory 列表
1235
+ * 由 assembler.getTaskTable 每轮注入时驱动(hooks.applyActivePlanItemAssembly),与任务表渲染同源,
1236
+ * 保证后继 step 换角色即时生效(装配视角不能只在 prepare.run 设一次,否则展示层正确/装配层冻结)。
1237
+ * roundRole=candidate(可能为 null,无有效覆盖时回落 activePack 组长),调 refreshRolePackPrefixForRound 重建前缀。
1238
+ * 防重:candidate 与 rolePackManager 当前 roundAssemblyPerspective 一致则跳过,避免每轮迭代重复重建。
771
1239
  */
772
- async archiveInsight(input, assistantContent) {
773
- this.assertInitialized('archiveInsight');
774
- return this.archiveCoordinator.archiveInsight(input, assistantContent);
1240
+ applyActivePlanItemAssemblyIfChanged() {
1241
+ const plan = this._sessionManager?.getCheckpoint()?.plan ?? [];
1242
+ const active = plan.find((s) => s.status === 'active');
1243
+ const candidate = this._rolePackManager?.resolveRoundAssemblyRole(active?.rolePack) ?? null;
1244
+ if (candidate === this._rolePackManager?.roundAssemblyPerspective)
1245
+ return;
1246
+ this._rolePackManager?.setRoundAssemblyRole(candidate);
1247
+ this.refreshRolePackPrefixForRound(candidate);
1248
+ logger.debug({ roundAssemblyRole: candidate }, '会议装配视角随 active 任务项切换');
775
1249
  }
776
1250
  /**
777
- * 手动归档会话内容(content 类记忆)
1251
+ * 按本轮表层装配视角刷新 AgentLoop 前缀(会议机制)
778
1252
  *
779
- * 适用于 `insights-only` / `manual` 模式下用户手动触发会话内容归档。
780
- * `full` 模式下由宿主在会话切换前自动调用,无需用户干预。
1253
+ * roundRole=null(日常态)→ 回落 activePack 前缀;roundRole 为任务项角色 → 该角色
1254
+ * persona/rules/skills 全量表层装配(buildSystemPrompt(name))。策略键恒为 activePack:
1255
+ * ChatOptions(temperature/outputLimit)由 activePack 策略构建,不随视角变(键不换防抖动)。
1256
+ * 由 seed prepare 每轮调用(`refreshRolePackPrefixForRound` 契约),保证前缀始终匹配本轮装配角色。
1257
+ */
1258
+ refreshRolePackPrefixForRound(roundRole) {
1259
+ if (!this.loop)
1260
+ return;
1261
+ const assemblyName = roundRole ?? this._rolePackManager?.activeName ?? undefined;
1262
+ const rolePackPrompt = this._rolePackManager?.buildSystemPrompt(assemblyName) ?? '';
1263
+ const globalSkillList = this.skillManager?.buildSkillList() ?? '';
1264
+ // 作品投影装配块:缓存于 manager(装配时刷新 + register_work 更新),同步读取即可
1265
+ const workProjectionContext = this.workProjection?.contextBlock() ?? '';
1266
+ const newPrefix = buildSystemPromptPrefix(rolePackPrompt, globalSkillList, undefined, workProjectionContext);
1267
+ this.loop.refreshRolePackPrefix(newPrefix);
1268
+ // 角色包底盘占用随切换实时更新(不依赖跑 prepare):装配此刻即确定的真值
1269
+ this.loop.setRolePackBaseTokens(estimateTokensMessages([{ content: newPrefix }]));
1270
+ // 同步注入角色包策略的 ChatOptions 覆盖项(temperature / outputLimit)
1271
+ // 键恒为 activePack:buildChatOptionsFromStrategy 读激活包策略,不随本轮装配视角变
1272
+ this.loop.setChatOptions(this.buildChatOptionsFromStrategy());
1273
+ }
1274
+ /**
1275
+ * 从当前激活的角色包策略构建 ChatOptions 覆盖项
1276
+ * 将 act.temperature / act.outputLimit 映射到 ChatOptions 字段;输出上限最终与 per-LLM 默认值
1277
+ * 取**更小值**(两者皆为「上限」,须同时满足)——收敛在 `buildChatOptions`,此处不裁决大小。
1278
+ */
1279
+ buildChatOptionsFromStrategy() {
1280
+ const strategy = this.getActiveStrategy();
1281
+ if (!strategy)
1282
+ return undefined;
1283
+ const chatOptions = {};
1284
+ const act = strategy.act;
1285
+ // act.temperature → ChatOptions.temperature(限 0-2;边界 SSOT = strategyKeys.isTemperature,不重复字面量)
1286
+ const temperature = act?.temperature;
1287
+ if (isTemperature(temperature)) {
1288
+ chatOptions.temperature = temperature;
1289
+ }
1290
+ // act.outputLimit → ChatOptions.maxTokens(需 ∈ [1, MAX_OUTPUT_LIMIT],越界忽略防资源失控)
1291
+ const outputLimit = act?.outputLimit;
1292
+ if (typeof outputLimit === 'number' && outputLimit > 0 && outputLimit <= MAX_OUTPUT_LIMIT) {
1293
+ chatOptions.maxTokens = outputLimit;
1294
+ }
1295
+ return Object.keys(chatOptions).length > 0 ? chatOptions : undefined;
1296
+ }
1297
+ /**
1298
+ * 角色包系统的唯一切换入口(自动匹配 + 手动切换共用):activate 更新激活态 → 发射 rolePackSwitched 事件
1299
+ * → 刷新 AgentLoop 前缀 → 同步工具白名单。
1300
+ * @returns 是否切换成功。false 有两种成因(**均不发射切换事件**):角色包不存在、
1301
+ * 切换锁已激活(30s 内超 5 次限流);调用方须以 getRolePackSwitchLockStatus() 区分,
1302
+ * 不可由「返回 false + 未收到 rolePackSwitchLocked」推断为"不存在"(被锁时该事件不发射,
1303
+ * 仅在**触发锁定**的那一次切换中发射,而那一次返回 true)。同名切换幂等返回 true。
1304
+ */
1305
+ switchRolePack(name) {
1306
+ this.assertInitialized('switchRolePack');
1307
+ const rpm = this._rolePackManager;
1308
+ if (!rpm)
1309
+ return false;
1310
+ const prevName = rpm.activeName;
1311
+ if (prevName === name)
1312
+ return true;
1313
+ const ok = rpm.activate(name);
1314
+ if (!ok) {
1315
+ logger.warn({ name }, '切换角色包失败(不存在或切换锁已激活)');
1316
+ return false;
1317
+ }
1318
+ this.emit(AGENT_EVENTS.rolePackSwitched, { from: prevName ?? '', to: name });
1319
+ this.refreshRolePackPrefixOnLoop();
1320
+ this.applyRolePackToolExposure();
1321
+ logger.info({ rolePack: name }, '角色包切换');
1322
+ return true;
1323
+ }
1324
+ /**
1325
+ * 手动归档会话(更新 SessionMeta,委托 ArchiveCoordinator)
781
1326
  *
782
- * 归档逻辑已委托给 ArchiveCoordinator
1327
+ * 适用于 manual 模式(用户手动触发;full 模式由宿主切换前自动调用)。
1328
+ * options 透传给 ArchiveCoordinator:宿主自动触发传 { autoTriggered: true }(按其模式判断),
1329
+ * 用户手动触发省略 options(默认 autoTriggered=false,无条件执行)。
783
1330
  *
784
- * @param date 会话日期 YYYY-MM-DD
785
- * @param session 会话标识(不含日期前缀)
786
- * @returns 归档结果(memories 可能为空,表示无归档价值或 LLM 失败)
1331
+ * @returns 归档结果(updatedFields 可能为空,表示无归档价值或 LLM 失败)
787
1332
  */
788
- async archiveSessionContent(date, session) {
789
- this.assertInitialized('archiveSessionContent');
790
- return this.archiveCoordinator.archiveSessionContent(date, session);
1333
+ async archiveSession(date, session, options) {
1334
+ this.assertInitialized('archiveSession');
1335
+ return this.requireArchiveCoordinator.archiveSession(date, session, options);
791
1336
  }
792
1337
  // ─── 配置重载(事件驱动) ───────────────────────
793
1338
  /**
794
- * 重载配置类记忆:从 configDir 重新扫描指定 source 的配置文件并更新内存缓存 + SQLite 索引
795
- *
796
- * 解决方案:installSkill 写入文件后或 confirmConfigSuggestion 写入配置文件后,
797
- * 调用此方法使当前会话立即生效,无需重启 Agent。
798
- *
799
- * 支持的 source:
800
- * - 'skill' → SkillManager.reload() 清空缓存重新扫描 skills/ 目录
801
- * - 'persona' → PersonaManager.reload() 清空缓存重新扫描 personas/ 目录(保持激活角色)
802
- * - 'rule' → 无操作(rule 类型已由 ConfigManager.addRule() 即时注入 system prompt)
803
- * - 'guardrail' → 抛错(guardrail 是 AgentLoop 的 readonly 数组,需 rebuildComponents 才能重载)
804
- * - undefined → 重载 skill + persona(全量重载,不含 guardrail)
805
- *
806
- * @param source 配置类型,缺省时重载全部可热更新的配置
807
- * @returns 重载结果统计
1339
+ * 重载配置类记忆:从 configDir 重新扫描指定 source 并更新内存缓存(设定记忆纯文件装载,不写记忆库),使当前会话立即生效(无需重启)。
1340
+ * - 'skill' → SkillManager.reload() 重扫 skills/
1341
+ * - 'rolePack' → RolePackManager.reload() 重扫 role-packs/(保持激活角色,刷新 AgentLoop 前缀)
1342
+ * - 'rule' → 经角色包管理机制热更新(即 rolePack)
1343
+ * - undefined → 全量重载 skill + rolePack
808
1344
  */
809
1345
  async reloadConfig(source) {
810
1346
  this.assertInitialized('reloadConfig');
811
- if (this.chatLockManager?.isBusy) {
1347
+ if (this.internals.chatLockManager?.isBusy) {
1348
+ // 对话中无法热重载:暂存 source 待锁释放后补执行;undefined(全量)不暂存——无具体来源,补执行语义不明
1349
+ if (source) {
1350
+ this.pendingConfigReload.add(source);
1351
+ logger.info({ source }, '对话进行中,配置重载已暂存,将在对话结束后补执行');
1352
+ }
812
1353
  throw chatBusyError('重载配置');
813
1354
  }
814
- // guardrail 需重建 AgentLoop,不属于热重载范畴
815
- if (source === 'guardrail') {
816
- throw configError('guardrail 不支持热重载', 'guardrail 规则是 AgentLoop 的 readonly 数组,需调用 rebuildComponents() 重建', ['使用 rebuildComponents() 重建组件(代价较高)']);
817
- }
818
- // rule 类型已由 ConfigManager.addRule() 即时注入,无需重载
1355
+ // rule 是角色包的一部分,重载走 rolePack 路径
819
1356
  if (source === 'rule') {
820
- logger.info('rule 类型已由 addRule() 即时注入,reloadConfig 跳过');
821
- return { skill: 0, persona: 0 };
1357
+ logger.info('rule 类型重载:触发角色包重新扫描');
1358
+ return this.reloadConfig('rolePack');
822
1359
  }
823
- const result = { skill: 0, persona: 0 };
1360
+ const result = { skill: 0, rolePack: 0 };
1361
+ const errors = [];
824
1362
  // 按需重载:source 缺省时全量重载,否则只重载指定类型
825
1363
  const shouldReloadSkill = !source || source === 'skill';
826
- const shouldReloadPersona = !source || source === 'persona';
1364
+ const shouldReloadRolePack = !source || source === 'rolePack';
827
1365
  if (shouldReloadSkill && this.skillManager) {
828
- result.skill = await this.skillManager.reload();
1366
+ try {
1367
+ result.skill = await this.skillManager.reload();
1368
+ }
1369
+ catch (err) {
1370
+ errors.push(toError(err));
1371
+ logger.warn({ err: toError(err) }, 'reloadConfig: skillManager.reload 失败');
1372
+ }
829
1373
  }
830
- 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);
1374
+ if (shouldReloadRolePack && this._rolePackManager) {
1375
+ try {
1376
+ result.rolePack = await this._rolePackManager.reload();
1377
+ // 角色重载后刷新 AgentLoop 前缀
1378
+ if (this.loop) {
1379
+ this.refreshRolePackPrefixOnLoop();
1380
+ }
1381
+ }
1382
+ catch (err) {
1383
+ errors.push(toError(err));
1384
+ logger.warn({ err: toError(err) }, 'reloadConfig: rolePackManager.reload 失败');
836
1385
  }
837
1386
  }
838
- logger.info({ source, ...result }, '配置已热重载');
1387
+ if (errors.length > 0) {
1388
+ logger.warn({ source, ...result, errorCount: errors.length }, '配置热重载部分失败');
1389
+ }
1390
+ else {
1391
+ logger.info({ source, ...result }, '配置已热重载');
1392
+ }
839
1393
  return result;
840
1394
  }
841
1395
  // ─── 组件访问 ─────────────────────────────────────────
842
1396
  /**
843
- * 重建内部组件(history / loop / managers)
844
- *
845
- * 通常不需要手动调用——switchProject() 已自动执行 rebuild。
846
- * 仅在宿主项目需要强制刷新组件时使用(如热更新配置后)。
1397
+ * 重建内部组件(history / loop / managers):通常由 switchProject() 自动触发,仅强制刷新时手动调用
847
1398
  */
848
1399
  async rebuildComponents() {
849
- // 对话进行中重建组件会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
850
- if (this.chatLockManager?.isBusy) {
851
- throw chatBusyError('重建组件');
852
- }
853
- await this.rebuildComponentsWithCurrentCtx();
1400
+ // 对话进行中重建会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
1401
+ this.assertNotBusy('重建组件');
1402
+ if (!this.pctx)
1403
+ return;
1404
+ // assembleComponents 内部重建 history/loop/managers,并返回新创建的 sessionManager
1405
+ await this.assembleComponents(this.pctx);
854
1406
  }
855
1407
  // ─── 守卫方法 ───────────────────────────────────────────
856
1408
  /**
857
- * 检查 Agent 是否已初始化
858
- *
859
- * @param methodName 调用方法名(用于错误消息)
860
- * @param requires 需要检查的组件列表
1409
+ * 检查 Agent 是否已初始化(methodName 用于错误消息,requires 为需检查的组件列表)
861
1410
  */
862
1411
  assertInitialized(methodName, requires = ['history', 'loop']) {
863
1412
  if (!this._initialized) {
@@ -879,78 +1428,186 @@ export class Agent extends TypedEventEmitter {
879
1428
  }
880
1429
  }
881
1430
  }
1431
+ /**
1432
+ * 断言对话未进行中(统一守卫):破坏状态一致性的入口统一在此做 isBusy 检查。
1433
+ * reloadConfig 不使用本方法——它在 isBusy 时需暂存 source 而非直接抛错。
1434
+ */
1435
+ assertNotBusy(operation) {
1436
+ if (this.internals.chatLockManager?.isBusy) {
1437
+ throw chatBusyError(operation);
1438
+ }
1439
+ }
1440
+ // 断言 getter:非空或抛明确错误,替代 this.loop!/history!/pctx! 非空断言
1441
+ get requireLoop() {
1442
+ if (!this.loop)
1443
+ throw configError('AgentLoop 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1444
+ return this.loop;
1445
+ }
1446
+ get requireArchiveCoordinator() {
1447
+ if (!this.internals.archiveCoordinator)
1448
+ throw configError('ArchiveCoordinator 未初始化', undefined, [
1449
+ '在调用此方法前执行 await agent.init()',
1450
+ ]);
1451
+ return this.internals.archiveCoordinator;
1452
+ }
1453
+ get requireSessionManager() {
1454
+ if (!this._sessionManager)
1455
+ throw configError('SessionManager 未初始化', undefined, [
1456
+ '在调用此方法前执行 await agent.init()',
1457
+ ]);
1458
+ return this._sessionManager;
1459
+ }
1460
+ /**
1461
+ * 获取角色包管理器(system prompt 的唯一注入源,返回 null 表示 Agent 未初始化)
1462
+ */
1463
+ get rolePackManager() {
1464
+ return this._rolePackManager;
1465
+ }
1466
+ /**
1467
+ * 获取当前激活的 L2 行为策略:从激活角色包读取,未激活时用全局默认值;供各阶段按策略调整行为
1468
+ */
1469
+ getActiveStrategy() {
1470
+ return this._rolePackManager?.getActive()?.strategy ?? DEFAULT_BEHAVIOR_STRATEGY;
1471
+ }
1472
+ /**
1473
+ * 应用当前角色包的工具暴露面(换角色 → 工具集切换):声明 capabilities 时解析为工具白名单,未声明放行全部。
1474
+ * 空数组(parseCapabilities 对 undefined 返回 [])同样视为"未声明能力"放行全部,避免误解释为"禁止所有"。
1475
+ * toolMode=block 全禁与此正交;变更经 setToolWhitelist → onToolsChanged 链路同步 loop 快照与 system prompt。
1476
+ */
1477
+ applyRolePackToolExposure() {
1478
+ if (!this.toolExec)
1479
+ return;
1480
+ const active = this._rolePackManager?.getActive();
1481
+ const capabilities = active?.capabilities;
1482
+ const whitelist = capabilities && capabilities.length > 0 ? resolveCapabilityTools(capabilities) : null;
1483
+ this.toolExec.setToolWhitelist(whitelist);
1484
+ }
882
1485
  // ─── 记忆生命周期 ───────────────────────────────────────
883
- // ─── runMemoryDecay 已迁移至 MemoryDecayScheduler.runOnce ─────
1486
+ /**
1487
+ * 立即执行一次孤儿 Round 垃圾回收(宿主手动触发入口)。
1488
+ *
1489
+ * roundStore+storage 未注入时返回空统计(降级不报错);
1490
+ * 对话进行中(chatLock busy)经 shouldSkip 跳过并返回空统计。
1491
+ *
1492
+ * @returns GC 结果统计(scanned/deleted/memoryCleaned 等)
1493
+ */
1494
+ gcNow() {
1495
+ const gc = this.internals.gcService;
1496
+ if (!gc) {
1497
+ logger.debug('gcNow: roundStore/storage 未注入,GCService 未创建(降级为空转)');
1498
+ return {
1499
+ scanned: 0,
1500
+ orphaned: 0,
1501
+ deleted: 0,
1502
+ failedDueToRefCount: 0,
1503
+ memoryCleaned: 0,
1504
+ elapsedMs: 0,
1505
+ };
1506
+ }
1507
+ return gc.run();
1508
+ }
884
1509
  // ─── 关闭 ─────────────────────────────────────────────
885
1510
  /**
886
- * 关闭 Agent,释放 SQLite 连接等资源
1511
+ * 关闭 Agent,释放存储连接等资源
887
1512
  */
888
1513
  async close() {
889
- // 清理 chat 锁管理器(递增 token 使进行中的 chat() generator 的 finally 块
890
- // 检测到 token 变化后跳过资源清理,close 已接管清理职责)
891
- this.chatLockManager?.dispose();
892
- this.chatLockManager = null;
893
- // 清理 MemoryDecayScheduler(含定时器和 storage 引用)
894
- if (this.memoryDecayScheduler) {
895
- this.memoryDecayScheduler.stop();
896
- this.memoryDecayScheduler = null;
1514
+ // 清理 chat 锁管理器(递增 token 使进行中 chat() 的 finally 跳过清理,close 已接管)
1515
+ this.internals.chatLockManager?.dispose();
1516
+ this.internals.chatLockManager = null;
1517
+ // 孤儿 Round GC 停止定时任务(须先于下方 projectManager.shutdown——其会关闭 storage)
1518
+ this.internals.gcService?.stopPeriodic();
1519
+ this.internals.gcService = null;
1520
+ // 等待 WorkProjection 无 inflight 概念(作品投影改为用户主动触发同步写,无后台 LLM 任务)
1521
+ // 背景任务统一由下方 awaitBackgroundTasks 兜底
1522
+ // 关键约束:等待 backgroundTask 全局在途任务完成(如 touchScores)。
1523
+ // 否则 close 后后台任务仍可能 upsert 已关闭的 storage,触发写失效或异常。
1524
+ try {
1525
+ const awaited = await awaitBackgroundTasks(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
1526
+ if (awaited > 0) {
1527
+ logger.debug({ awaited }, 'close: 背景任务等待结束(awaited = 初始在途数,超时放弃时非完成数)');
1528
+ }
897
1529
  }
898
- // 清理 ArchiveCoordinator(无定时器,只需释放引用)
899
- this.archiveCoordinator = null;
900
- // 清理定时器
901
- if (this.decayTimer) {
902
- clearSafeInterval(this.decayTimer);
903
- this.decayTimer = null;
1530
+ catch (err) {
1531
+ logger.warn({ err: toError(err) }, 'close: awaitBackgroundTasks 失败');
1532
+ }
1533
+ // 处理 pending 暂停残留:关闭前确保状态机与检查点一致
1534
+ const sm = this._sessionManager;
1535
+ if (sm?.isPausePending()) {
1536
+ const pendingInfo = sm.pendingPauseInfo;
1537
+ logger.warn({ reason: pendingInfo?.reason, source: pendingInfo?.source }, 'close() 时存在未消费的暂停请求,已自动清理');
1538
+ // 若状态机仍 running,同步翻 paused 使检查点内存态一致(无需触发挂起,仅修正一致性);
1539
+ // 系统清理不消耗连续暂停配额 → lowRisk=true
1540
+ if (sm.status === 'running') {
1541
+ sm.pause('close 清理残留暂停', 'system', true);
1542
+ }
1543
+ sm.cancelPendingPause();
1544
+ }
1545
+ // 清理 SessionManager(须先于 nullifyAllComponents,destroy 需访问其内部状态)
1546
+ if (this._sessionManager) {
1547
+ // 清脏后再 destroy(纯内存,无落盘;检查点不落盘,本调用为保留语义边界的 no-op)
1548
+ this._sessionManager.flushOnShutdown();
1549
+ this._sessionManager.destroy();
904
1550
  }
905
- // 清理 PersonaManager 的角色切换防抖锁计时器,防止关闭后回调触发
906
- if (this.personaManager) {
907
- this.personaManager.close();
1551
+ // ArchiveCoordinator 无定时器,仅释放引用
1552
+ this.internals.archiveCoordinator = null;
1553
+ // 清理 RolePackManager 切换防抖锁计时器,防止关闭后回调触发
1554
+ if (this._rolePackManager) {
1555
+ this._rolePackManager.close();
908
1556
  }
909
- // 先等待后台归档完成,再移除事件监听器
910
- // 顺序敏感:若先 removeAllListeners,fire-and-forget 的归档 Promise 在 await 期间
911
- // reject 时 emit 会变成 no-op(listeners 已清空),archiveFailed 事件丢失
1557
+ // 顺序敏感:先等后台归档完成再移除监听器,否则 await 期间归档 reject 的 emit 变 no-op、事件丢失
912
1558
  if (this.history) {
913
- await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
1559
+ try {
1560
+ await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
1561
+ }
1562
+ catch (err) {
1563
+ logger.warn({ err: toError(err) }, 'close: awaitPendingArchives 失败');
1564
+ }
914
1565
  }
915
- // pending archives 完成后再移除监听器,确保归档失败的 emit 能送达
916
1566
  this.removeAllListeners();
917
1567
  if (this.projectManager) {
918
- await this.projectManager.shutdown();
1568
+ try {
1569
+ await this.projectManager.shutdown();
1570
+ }
1571
+ catch (err) {
1572
+ logger.warn({ err: toError(err) }, 'close: projectManager.shutdown 失败');
1573
+ }
919
1574
  }
920
1575
  this._initialized = false;
921
1576
  this.nullifyAllComponents();
922
1577
  // 清理次要状态字段,防止 re-init 后残留上一会话状态
923
- this.activeSkill = null;
924
1578
  this._lastInteractionAt = null;
1579
+ this.pendingConfigReload.clear();
925
1580
  }
926
1581
  /**
927
- * 统一 null 化所有组件字段(新增 Manager 时在此处追加一行)
928
- *
929
- * 仅处理"纯 null 化"字段,带副作用的清理(dispose/stop/close/shutdown)
930
- * 仍由 close() 显式调用,顺序敏感不可合并。
1582
+ * 统一 null 化所有组件字段(新增 Manager 时在此追加一行);带副作用的清理仍由 close() 显式调用,顺序敏感不可合并
931
1583
  */
932
1584
  nullifyAllComponents() {
933
- // Provider
934
1585
  this.#backgroundProvider = null;
935
- // 核心组件
936
1586
  this.history = null;
937
1587
  this.loop = null;
938
1588
  this.toolExec = null;
939
- // 专职 Manager
940
- this.personaManager = null;
941
- this.#userProfile = null;
942
1589
  this.skillManager = null;
943
- this.insightExtractor = null;
944
- this.configManager = null;
1590
+ this._rolePackManager = null;
945
1591
  this.memoryInspector = null;
1592
+ this._governance = null;
946
1593
  this.workProjection = null;
947
- this.autoConfigRefiner = null;
948
- this.sessionArchiver = null;
949
1594
  this.textPolisher = null;
950
1595
  this._sessionManager = null;
951
- // 项目管理
952
1596
  this.projectManager = null;
953
1597
  this.pctx = null;
1598
+ // 纯内部组件聚合整体置 null(新增内部组件只需在字段声明与 createPostInitComponents 登记)
1599
+ this.internals = {
1600
+ dedupManager: null,
1601
+ memoryAdvisor: null,
1602
+ sessionArchiver: null,
1603
+ sessionNamer: null,
1604
+ roundSummaryGenerator: null,
1605
+ contextPreparer: null,
1606
+ chatLockManager: null,
1607
+ archiveCoordinator: null,
1608
+ seedOrchestrator: null,
1609
+ gcService: null,
1610
+ };
954
1611
  }
955
1612
  // ─── 只读访问器 ───────────────────────────────────────
956
1613
  get initialized() {
@@ -959,265 +1616,140 @@ export class Agent extends TypedEventEmitter {
959
1616
  get context() {
960
1617
  return this.pctx;
961
1618
  }
1619
+ /**
1620
+ * 获取 AgentLoop 实例(宿主集成面,用于 restoreHistory 等底层操作;getMessages/getMessageCount 无法替代)
1621
+ */
962
1622
  get agentLoop() {
963
1623
  return this.loop;
964
1624
  }
965
1625
  /**
966
- * 注入情感基调到 system prompt(Phase 2.1:AffectController)
967
- *
968
- * 委托至 AgentLoop.injectAffect(),在角色前缀和 bootstrap 记忆之间插入情感描述。
969
- * 与角色切换独立——切换角色不会清除情感注入。
970
- *
971
- * @param affectString 情感描述文本,传空字符串清除注入
1626
+ * 获取当前消息列表(只读副本,不可绕过编排链路直接修改)
972
1627
  */
973
- injectAffect(affectString) {
974
- this.loop?.injectAffect(affectString);
1628
+ getMessages() {
1629
+ return this.loop?.getMessages() ?? [];
1630
+ }
1631
+ /** 获取当前消息数量 */
1632
+ getMessageCount() {
1633
+ return this.loop?.getMessages().length ?? 0;
975
1634
  }
1635
+ /**
1636
+ * 获取 MessageHistory 实例(宿主集成面,用于日期/会话判断等底层操作;
1637
+ * currentDateValue/currentSessionValue 等只读属性无替代接口)
1638
+ */
976
1639
  get agentHistory() {
977
1640
  return this.history;
978
1641
  }
1642
+ /**
1643
+ * 注入情感基调到 system prompt:委托 AgentLoop.injectAffect(),在角色前缀与 bootstrap 记忆间插入情感描述;
1644
+ * 与角色切换独立(切换角色不清除情感注入),传空串清除。
1645
+ */
1646
+ injectAffect(affectString) {
1647
+ if (!this.loop)
1648
+ throw configError('AgentLoop', 'AgentLoop 未初始化,无法注入 affect', []);
1649
+ this.loop.injectAffect(affectString);
1650
+ }
979
1651
  get provider() {
980
1652
  return this.#provider;
981
1653
  }
982
1654
  get isBusy() {
983
- return this.chatLockManager?.isBusy ?? false;
1655
+ return this.internals.chatLockManager?.isBusy ?? false;
984
1656
  }
985
1657
  get lastInteractionAt() {
986
1658
  return this._lastInteractionAt;
987
1659
  }
1660
+ /** 记忆治理统一门面(语义去重 / 来源健康诊断 / 冲突检测 / 建议推荐),Agent 上的记忆管理方法统一收口于此(无显式衰减层面) */
1661
+ get governance() {
1662
+ return this._governance;
1663
+ }
988
1664
  // ─── 运行时指标 ────────────────────────────────
989
1665
  /**
990
- * 获取 Agent 运行时指标快照(可观测性增强)
991
- *
992
- * 聚合 AgentLoop 指标(LLM 调用、记忆召回、工具调用、上下文管理)
993
- * 与 Agent 层指标(记忆衰减),返回完整的 AgentMetrics 快照。
994
- *
995
- * 未初始化时返回全零指标(合理的默认值,不抛异常)。
996
- * 纯只读、同步、零副作用——适合宿主项目定期轮询构建监控面板。
997
- *
998
- * 使用方式:
999
- * const metrics = agent.getMetrics();
1000
- * console.log(`LLM 调用 ${metrics.llm.callCount} 次,命中率 ${metrics.recall.hitRate}`);
1001
- *
1002
- * @returns AgentMetrics 完整快照(含衰减指标)
1666
+ * 后台任务统计快照(宿主观测后台副作用健康度,纯只读转发 utils 单例)。
1667
+ * @returns 在途 / 成功 / 失败计数
1668
+ */
1669
+ getBackgroundTaskStats() {
1670
+ return readBackgroundTaskStats();
1671
+ }
1672
+ /**
1673
+ * 获取 Agent 运行时指标快照(可观测性):聚合 AgentLoop 指标与 Agent 层治理指标;
1674
+ * 未初始化返回全零默认值(不抛异常),纯只读同步零副作用,适合宿主定期轮询监控面板。
1003
1675
  */
1004
1676
  getMetrics() {
1005
- // 衰减指标从 MemoryDecayScheduler 读取
1006
- const decayMetrics = this.memoryDecayScheduler?.getMetrics() ?? {
1007
- runCount: 0,
1008
- totalDecayedCount: 0,
1009
- lastRunAt: null,
1010
- };
1011
- // 未初始化时返回全零指标,避免调用方需要判空
1677
+ // 未初始化时返回全零指标,避免调用方判空
1012
1678
  if (!this.loop) {
1013
1679
  return {
1014
- llm: { callCount: 0, totalInputTokens: 0, totalOutputTokens: 0 },
1015
- recall: { totalCount: 0, hitCount: 0, hitRate: 0 },
1016
- tools: { callCount: 0, failureCount: 0 },
1680
+ llm: {
1681
+ callCount: 0,
1682
+ totalInputTokens: 0,
1683
+ totalOutputTokens: 0,
1684
+ actualInputTokens: 0,
1685
+ actualOutputTokens: 0,
1686
+ emptyResponseCount: 0,
1687
+ truncationRecoveryCount: 0,
1688
+ pairingGuardFires: 0,
1689
+ },
1690
+ tools: {
1691
+ callCount: 0,
1692
+ failureCount: 0,
1693
+ unparsedToolIntentCount: 0,
1694
+ ledgerStubEchoCount: 0,
1695
+ readDedupBlockCount: 0,
1696
+ },
1017
1697
  context: { truncationCount: 0, messageCount: 0, estimatedTokens: 0 },
1018
- decay: decayMetrics,
1698
+ plan: { taskTableWriteCount: 0, planItemBoundaryCount: 0 },
1699
+ tasks: {
1700
+ totalCount: 0,
1701
+ successCount: 0,
1702
+ failureCount: 0,
1703
+ successRate: 0,
1704
+ avgDurationMs: 0,
1705
+ },
1019
1706
  };
1020
1707
  }
1021
- // 获取 AgentLoop 指标快照,填充衰减字段
1022
1708
  const loopMetrics = this.loop.getMetrics();
1023
1709
  return {
1024
1710
  ...loopMetrics,
1025
- decay: decayMetrics,
1026
1711
  };
1027
1712
  }
1028
- // ─── Manager 暴露(激进拆分:调用方直接操作 Manager)──
1029
- //
1030
- // 设计策略(ADR-010 §Manager 访问器):访问器返回 null,门面方法抛错。
1031
- // - 访问器(getter):返回 Manager | null,供宿主项目链式调用和优雅降级
1032
- // (如 `agent.persona?.activeName`、`if (!agent.memory) return []`)
1033
- // - 门面方法(snapshot/inspect/stats/searchMemories 等):通过
1034
- // assertInitialized 抛 MemoraError,提供明确错误信息
1035
- // 宿主项目使用访问器时需自行判空,或使用门面方法获得自动错误处理。
1036
- /**
1037
- * 角色管理器(可能为 null)
1038
- *
1039
- * 返回 null 时表示 Agent 未初始化或角色系统未加载。
1040
- * 链式调用建议使用可选链:`agent.persona?.activeName`
1041
- */
1042
- get persona() {
1043
- return this.personaManager;
1713
+ // ─── Manager 暴露(调用方直接操作 Manager)──
1714
+ // 设计策略:访问器(getter)返回 Manager | null 供链式调用和优雅降级(如 `if (!agent.memory) return []`);
1715
+ // 门面方法经 assertInitialized 抛明确错误;宿主用访问器需自行判空,或用门面方法获得自动错误处理。
1716
+ /** 角色包管理器(角色+技能+规则的唯一真理源,system prompt 的唯一注入源) */
1717
+ get rolePack() {
1718
+ return this._rolePackManager;
1044
1719
  }
1045
- /**
1046
- * 工具执行器(可能为 null)
1047
- *
1048
- * 返回 null 时表示 Agent 未初始化。
1049
- */
1720
+ /** 工具执行器(返回 null 表示 Agent 未初始化) */
1050
1721
  get tools() {
1051
1722
  return this.toolExec;
1052
1723
  }
1053
- /**
1054
- * 技能管理器(可能为 null)
1055
- *
1056
- * 返回 null 时表示 Agent 未初始化或技能系统未加载。
1057
- */
1724
+ /** 技能管理器(返回 null 表示未初始化或技能系统未加载) */
1058
1725
  get skills() {
1059
1726
  return this.skillManager;
1060
1727
  }
1061
- /**
1062
- * 配置管理器(可能为 null)—— 规则/技能注入 + 配置建议
1063
- *
1064
- * 返回 null 时表示 Agent 未初始化。
1065
- */
1066
- get config() {
1067
- return this.configManager;
1068
- }
1069
- /**
1070
- * Insight 提取器(可能为 null)—— 输入分类 + 记忆提取
1071
- *
1072
- * 返回 null 时表示 Agent 未初始化或 LLM Provider 未配置。
1073
- */
1074
- get insight() {
1075
- return this.insightExtractor;
1728
+ /** 角色包管理器(返回 null 表示未初始化或角色包系统未加载) */
1729
+ get rolePacks() {
1730
+ return this._rolePackManager;
1076
1731
  }
1077
- /**
1078
- * 记忆查看器(可能为 null)—— 快照 + 搜索 + 统计
1079
- *
1080
- * 返回 null 时表示 Agent 未初始化或存储层未就绪。
1081
- * 宿主项目常用模式:`const mem = agent.memory; if (!mem) return [];`
1082
- */
1732
+ /** 记忆查看器(快照 + 搜索 + 统计;宿主常用 `const mem = agent.memory; if (!mem) return []`) */
1083
1733
  get memory() {
1084
1734
  return this.memoryInspector;
1085
1735
  }
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
- /**
1173
- * 项目管理器(可能为 null)
1174
- *
1175
- * 返回 null 时表示 Agent 未初始化。
1176
- */
1736
+ /** 项目管理器(返回 null 表示 Agent 未初始化) */
1177
1737
  get projects() {
1178
1738
  return this.projectManager;
1179
1739
  }
1180
- /**
1181
- * 用户画像管理器(可能为 null)—— 实时归档 + 确认/拒绝
1182
- *
1183
- * 返回 null 时表示 Agent 未初始化。
1184
- * 宿主常用模式:
1185
- * - `agent.userProfile?.getPending()` 查询待确认条目
1186
- * - `await agent.userProfile?.confirm(id)` 确认条目
1187
- * - `await agent.userProfile?.reject(id)` 拒绝条目
1188
- */
1189
- get userProfile() {
1190
- return this.#userProfile;
1191
- }
1192
- /**
1193
- * 安全守卫(可能为 null)—— 写入确认回调注册
1194
- *
1195
- * 返回 null 时表示 Agent 未初始化或 projectPath 未设置。
1196
- * 宿主常用模式:
1197
- * - `agent.security?.onWriteConfirmation(callback)` 注册写入确认回调
1198
- * - 回调返回 true 允许写入,返回 false 拒绝写入
1199
- *
1200
- * 详见 M1 写入确认 UI 闭环
1201
- */
1740
+ /** 安全守卫(写入确认回调注册:`onWriteConfirmation` 返回 true 放行、false 拒绝;未初始化/projectPath 未设为 null) */
1202
1741
  get security() {
1203
1742
  return this.pctx?.security ?? null;
1204
1743
  }
1205
- /**
1206
- * 作品投影管理器(可能为 null)—— 文件内容 → 概要+结构+决策
1207
- *
1208
- * 返回 null 时表示 Agent 未初始化。
1209
- * 宿主常用模式:
1210
- * - `agent.works?.ensureProjection(filePath, content)` 读取文件后触发生成
1211
- * - `agent.works?.loadAll()` 获取所有投影
1212
- */
1744
+ /** 作品投影管理器(用户主动登记的 JSON 极简索引;`registerWork`/`listWorks`/`loadAndGetContextBlock`/`isFileMalformed`) */
1213
1745
  get works() {
1214
1746
  return this.workProjection;
1215
1747
  }
1216
- /** 会话管理器(宿主可通过此 getter 访问会话恢复/切换/分叉功能) */
1748
+ /** 会话管理器(会话恢复/切换/分叉功能) */
1217
1749
  get sessionManager() {
1218
1750
  return this._sessionManager;
1219
1751
  }
1220
- /** 文本润色管理器(宿主可通过此 getter 调用 polish 方法) */
1752
+ /** 文本润色管理器(polish 方法) */
1221
1753
  get polish() {
1222
1754
  return this.textPolisher;
1223
1755
  }