@zooique/memora 2.0.3 → 3.0.1

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 +1037 -0
  2. package/README.en.md +305 -272
  3. package/README.md +303 -260
  4. package/config.example.json +43 -0
  5. package/dist/agent/agent.d.ts +376 -408
  6. package/dist/agent/agent.d.ts.map +1 -1
  7. package/dist/agent/agent.js +1166 -926
  8. package/dist/agent/agent.js.map +1 -1
  9. package/dist/agent/assembler.d.ts +105 -43
  10. package/dist/agent/assembler.d.ts.map +1 -1
  11. package/dist/agent/assembler.js +543 -75
  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 +225 -38
  18. package/dist/agent/builtinToolHandlers.d.ts.map +1 -1
  19. package/dist/agent/builtinToolHandlers.js +748 -132
  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 +137 -20
  30. package/dist/agent/constants.d.ts.map +1 -1
  31. package/dist/agent/constants.js +152 -20
  32. package/dist/agent/constants.js.map +1 -1
  33. package/dist/agent/contextManager.d.ts +119 -77
  34. package/dist/agent/contextManager.d.ts.map +1 -1
  35. package/dist/agent/contextManager.js +192 -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 +716 -295
  50. package/dist/agent/loop.d.ts.map +1 -1
  51. package/dist/agent/loop.js +2178 -664
  52. package/dist/agent/loop.js.map +1 -1
  53. package/dist/agent/managers/archiveCoordinator.d.ts +14 -171
  54. package/dist/agent/managers/archiveCoordinator.d.ts.map +1 -1
  55. package/dist/agent/managers/archiveCoordinator.js +19 -287
  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 +31 -115
  62. package/dist/agent/managers/dedupManager.d.ts.map +1 -1
  63. package/dist/agent/managers/dedupManager.js +48 -136
  64. package/dist/agent/managers/dedupManager.js.map +1 -1
  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 +8 -25
  74. package/dist/agent/managers/llmJudgeHelper.d.ts.map +1 -1
  75. package/dist/agent/managers/llmJudgeHelper.js +7 -24
  76. package/dist/agent/managers/llmJudgeHelper.js.map +1 -1
  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 +85 -207
  84. package/dist/agent/managers/memoryAdvisor.js.map +1 -1
  85. package/dist/agent/managers/memoryGovernance.d.ts +10 -42
  86. package/dist/agent/managers/memoryGovernance.d.ts.map +1 -1
  87. package/dist/agent/managers/memoryGovernance.js +19 -36
  88. package/dist/agent/managers/memoryGovernance.js.map +1 -1
  89. package/dist/agent/managers/memoryInspector.d.ts +59 -268
  90. package/dist/agent/managers/memoryInspector.d.ts.map +1 -1
  91. package/dist/agent/managers/memoryInspector.js +91 -442
  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 -68
  98. package/dist/agent/managers/sessionArchiver.d.ts.map +1 -1
  99. package/dist/agent/managers/sessionArchiver.js +76 -147
  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 +8 -33
  110. package/dist/agent/managers/streamAccumulator.d.ts.map +1 -1
  111. package/dist/agent/managers/streamAccumulator.js +4 -29
  112. package/dist/agent/managers/streamAccumulator.js.map +1 -1
  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 +9 -41
  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 +55 -100
  122. package/dist/agent/managers/workProjection.d.ts.map +1 -1
  123. package/dist/agent/managers/workProjection.js +164 -268
  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 +236 -10
  158. package/dist/agent/toolExecutor.d.ts.map +1 -1
  159. package/dist/agent/toolExecutor.js +932 -21
  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 +510 -169
  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 +27 -98
  198. package/dist/config/loader.d.ts.map +1 -1
  199. package/dist/config/loader.js +110 -223
  200. package/dist/config/loader.js.map +1 -1
  201. package/dist/index.d.ts +67 -77
  202. package/dist/index.d.ts.map +1 -1
  203. package/dist/index.js +90 -69
  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 +22 -32
  210. package/dist/llm/factory.d.ts.map +1 -1
  211. package/dist/llm/factory.js +29 -46
  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 +179 -120
  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 -31
  226. package/dist/logging/logger.d.ts.map +1 -1
  227. package/dist/logging/logger.js +28 -215
  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 +11 -44
  236. package/dist/memory/governance.d.ts.map +1 -1
  237. package/dist/memory/governance.js +11 -64
  238. package/dist/memory/governance.js.map +1 -1
  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 +25 -175
  252. package/dist/memory/inMemoryStorage.d.ts.map +1 -1
  253. package/dist/memory/inMemoryStorage.js +32 -221
  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 +2 -2
  260. package/dist/memory/lockManager.d.ts.map +1 -1
  261. package/dist/memory/lockManager.js +6 -8
  262. package/dist/memory/lockManager.js.map +1 -1
  263. package/dist/memory/projectManager.d.ts +30 -129
  264. package/dist/memory/projectManager.d.ts.map +1 -1
  265. package/dist/memory/projectManager.js +39 -189
  266. package/dist/memory/projectManager.js.map +1 -1
  267. package/dist/memory/projectRegistry.d.ts +14 -62
  268. package/dist/memory/projectRegistry.d.ts.map +1 -1
  269. package/dist/memory/projectRegistry.js +27 -111
  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 +10 -12
  284. package/dist/memory/sourceValidation.d.ts.map +1 -1
  285. package/dist/memory/sourceValidation.js +16 -32
  286. package/dist/memory/sourceValidation.js.map +1 -1
  287. package/dist/memory/storageInterface.d.ts +44 -150
  288. package/dist/memory/storageInterface.d.ts.map +1 -1
  289. package/dist/memory/types.d.ts +65 -164
  290. package/dist/memory/types.d.ts.map +1 -1
  291. package/dist/memory/types.js +81 -83
  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 +136 -19
  342. package/dist/skill/skillManager.d.ts.map +1 -1
  343. package/dist/skill/skillManager.js +312 -61
  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 -16
  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 +40 -72
  366. package/dist/utils/configResourceManager.d.ts.map +1 -1
  367. package/dist/utils/configResourceManager.js +78 -76
  368. package/dist/utils/configResourceManager.js.map +1 -1
  369. package/dist/utils/errors.d.ts +44 -69
  370. package/dist/utils/errors.d.ts.map +1 -1
  371. package/dist/utils/errors.js +54 -71
  372. package/dist/utils/errors.js.map +1 -1
  373. package/dist/utils/eventEmitter.d.ts +104 -103
  374. package/dist/utils/eventEmitter.d.ts.map +1 -1
  375. package/dist/utils/eventEmitter.js +37 -45
  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 +2 -3
  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 +25 -3
  396. package/dist/utils/math.d.ts.map +1 -1
  397. package/dist/utils/math.js +32 -16
  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 -19
  416. package/dist/utils/segmenter.d.ts.map +1 -1
  417. package/dist/utils/segmenter.js +230 -97
  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 +416 -476
  458. package/docs/memora-/346/216/245/345/205/245/346/214/207/345/215/227.md +841 -806
  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 -100
  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 -49
  503. package/dist/agent/guardrail.d.ts.map +0 -1
  504. package/dist/agent/guardrail.js +0 -59
  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 -146
  509. package/dist/agent/managers/autoConfigRefiner.js.map +0 -1
  510. package/dist/agent/managers/configManager.d.ts +0 -209
  511. package/dist/agent/managers/configManager.d.ts.map +0 -1
  512. package/dist/agent/managers/configManager.js +0 -331
  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 -239
  519. package/dist/agent/managers/memoryDecayScheduler.d.ts.map +0 -1
  520. package/dist/agent/managers/memoryDecayScheduler.js +0 -413
  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 -88
  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 -79
  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 -50
  551. package/dist/memory/hybridMerge.d.ts.map +0 -1
  552. package/dist/memory/hybridMerge.js +0 -51
  553. package/dist/memory/hybridMerge.js.map +0 -1
  554. package/dist/memory/inMemoryRelationStore.d.ts +0 -58
  555. package/dist/memory/inMemoryRelationStore.d.ts.map +0 -1
  556. package/dist/memory/inMemoryRelationStore.js +0 -80
  557. package/dist/memory/inMemoryRelationStore.js.map +0 -1
  558. package/dist/memory/loader.d.ts +0 -51
  559. package/dist/memory/loader.d.ts.map +0 -1
  560. package/dist/memory/loader.js +0 -100
  561. package/dist/memory/loader.js.map +0 -1
  562. package/dist/memory/recall.d.ts +0 -82
  563. package/dist/memory/recall.d.ts.map +0 -1
  564. package/dist/memory/recall.js +0 -157
  565. package/dist/memory/recall.js.map +0 -1
  566. package/dist/memory/relationStore.d.ts +0 -95
  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 -71
  571. package/dist/memory/store.d.ts.map +0 -1
  572. package/dist/memory/store.js +0 -199
  573. package/dist/memory/store.js.map +0 -1
  574. package/dist/memory/userProfile.d.ts +0 -180
  575. package/dist/memory/userProfile.d.ts.map +0 -1
  576. package/dist/memory/userProfile.js +0 -381
  577. package/dist/memory/userProfile.js.map +0 -1
  578. package/dist/memory/vectorStore.d.ts +0 -179
  579. package/dist/memory/vectorStore.d.ts.map +0 -1
  580. package/dist/memory/vectorStore.js +0 -284
  581. package/dist/memory/vectorStore.js.map +0 -1
  582. package/dist/persona/personaManager.d.ts +0 -109
  583. package/dist/persona/personaManager.d.ts.map +0 -1
  584. package/dist/persona/personaManager.js +0 -315
  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,75 +1,39 @@
1
1
  /**
2
2
  * Agent 门面类 — Memora 宿主项目接入入口
3
3
  *
4
- * 设计文档(ADR-010 · Agent 门面类)要求宿主项目通过 `import { Agent } from '@zooique/memora'`
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, boostScores } from '../memory/recall.js';
30
- import { assembleComponents } from '../agent/assembler.js';
31
- import { matchPersonaByLlm } from '../agent/personaMatcher.js';
32
- import { chatBusyError, configError, isAbortError, toError } from '../utils/errors.js';
33
- 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';
34
23
  import { ChatLockManager } from '../agent/managers/chatLockManager.js';
35
- import { MemoryDecayScheduler } from '../agent/managers/memoryDecayScheduler.js';
36
24
  import { MemoryGovernance } from '../agent/managers/memoryGovernance.js';
37
- import { ArchiveCoordinator } from '../agent/managers/archiveCoordinator.js';
38
- import { TypedEventEmitter, AGENT_EVENTS, AGENT_EVENT_SET } from '../utils/eventEmitter.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_EVENTS / AGENT_EVENT_SET 已迁移至 utils/eventEmitter.ts(与 AgentEventMap 同处,单一真理源)
43
30
  // ─── Agent 门面类 ───────────────────────────────────────
44
31
  /**
45
32
  * Memora Agent 门面类
46
33
  *
47
- * **设计哲学**:单 Agent,单配置,单记忆。每个 Agent 实例拥有独立的
48
- * 对话管线(AgentLoop)、独立的消息历史(MessageHistory)和独立的
49
- * 运行时状态(chatBusy)。activeSkill 已移除——技能匹配改为当轮实时注入,无需跨轮状态缓存。
50
- *
51
- * **多实例**:宿主可通过 new Agent({ dataDir: './agent2' }) 创建
52
- * 独立实例。只要 dataDir 不同,它们拥有完全隔离的记忆库和会话。
53
- * 内核不负责多 Agent 编排——那是宿主层的职责。
54
- *
55
- * **fork vs 多实例**:
56
- * - forkSession() 分叉对话历史,但记忆索引全局共享
57
- * - new Agent() + 独立 dataDir 实现完全隔离的记忆空间
58
- *
59
- * @example
60
- * ```ts
61
- * const agent = new Agent({
62
- * projectPath: '/path/to/project',
63
- * provider: new OpenAIProvider({ apiKey: '...' }),
64
- * configDir: '/path/to/config',
65
- * dataDir: '/path/to/data',
66
- * permission: 'owner',
67
- * allowedPaths: ['/path/to/project'],
68
- * });
69
- * await agent.init();
70
- * const iter = agent.chat('你好');
71
- * for await (const chunk of iter) { ... }
72
- * ```
34
+ * 单 Agent,单配置,单记忆:每个实例拥有独立的对话管线、消息历史与运行时状态。
35
+ * 多实例只需 dataDir 不同即可彻底隔离记忆库与会话;forkSession() 分叉对话历史但记忆索引全局共享。
36
+ * skill 为当轮实时注入,不跨轮缓存。
73
37
  */
74
38
  export class Agent extends TypedEventEmitter {
75
39
  // 构造参数分组
@@ -78,76 +42,62 @@ export class Agent extends TypedEventEmitter {
78
42
  #provider;
79
43
  /** 后台 Provider(独立字段,因 setBackgroundProvider() 可变) */
80
44
  #backgroundProvider;
81
- // 运行时组件(init 后填充)
45
+ /** Provider 路由选择器(多模型路由基础,可选) */
46
+ #providerRouter = null;
47
+ // 运行时组件(init 后填充)——宿主 getter 契约字段(projects/memory/works/polish/sessionManager 等)保持独立
82
48
  projectManager = null;
83
49
  history = null;
84
50
  loop = null;
85
51
  toolExec = null;
86
52
  // 新模块
87
- personaManager = null;
88
- #userProfile = null;
89
53
  skillManager = null;
54
+ /** 角色包管理器(角色+技能+规则的唯一真理源) */
55
+ _rolePackManager = null;
90
56
  // 拆分出的专职 Manager
91
- insightExtractor = null;
92
- configManager = null;
93
57
  memoryInspector = null;
94
- /**
95
- * 语义去重管理器(L1 LLM 记忆治理)
96
- *
97
- * SPLIT-3 闭环(2026-07-21):从 MemoryInspector 拆分出 deduplicateMemories 职责,
98
- * 让 MemoryInspector 回归纯存储读写。Agent.deduplicateMemories() 委托本对象。
99
- */
100
- dedupManager = null;
101
- /**
102
- * 记忆顾问(L3 冲突检测 / sourceHealth / suggest)
103
- *
104
- * v2 PROXY-1 闭环:Agent.detectConflicts 直接调用 advisor,
105
- * 不再经 MemoryInspector 转发,消除 3 层无意义代理。
106
- * sourceHealth/suggest 仍由 inspector 转发以保持 agent.memory 统一入口语义。
107
- */
108
- memoryAdvisor = null;
109
58
  /** 记忆治理统一门面(L0/L1/L2/L3 + 诊断) */
110
59
  _governance = null;
111
- workProjection = null;
112
- /** AutoConfigRefiner(模式 3:Agent 智能总结) */
113
- autoConfigRefiner = null;
114
- /** SessionArchiver(会话内容归档器,content 类记忆) */
115
- sessionArchiver = null;
116
60
  /** TextPolishManager(文本润色管理器,LLM 语法修正 + 表达优化) */
117
61
  textPolisher = null;
118
62
  /** 会话管理器(从 Agent 拆分出的会话管理职责) */
119
63
  _sessionManager = null;
120
- // activeSkill 字段已移除:matchAndInjectSkill 改为当轮实时匹配注入,无需跨轮状态缓存
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
+ };
121
83
  _initialized = false;
122
84
  // 项目上下文(AgentContext 与 ProjectContext 等价,直接使用后者避免重复字段)
123
85
  pctx = null;
124
- /**
125
- * chat() 并发锁管理器(init 时创建,close 时销毁)
126
- *
127
- * 职责:chat() 并发锁 + token 校验(race condition 防护)+ 超时保护 + 外部 signal 合并。
128
- * 详见 ChatLockManager 类注释。
129
- */
130
- chatLockManager = null;
131
86
  /** 最近一次 chat() 调用的时间戳 */
132
87
  _lastInteractionAt = null;
133
88
  /**
134
- * 对话进行中暂存的配置重载请求集合(chatLock 释放后补执行)
135
- *
136
- * 场景:用户在对话中通过 create_persona / create_skill 工具创建配置,
137
- * reloadConfig 因 chatLock 冲突失败时,将 source 暂存于此。
138
- * chat() 的 finally 块释放锁后遍历此集合逐个补执行 reloadConfig,
139
- * 兑现"对话结束后自动加载"的承诺(见 tools.ts createConfigHandler 提示文案)。
140
- *
141
- * 用 Set 而非数组:同一 source 多次请求只需补执行一次(去重)。
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 只需补执行一次。
142
99
  */
143
100
  pendingConfigReload = new Set();
144
- // ─── 衰减职责已拆分至 MemoryDecayScheduler ──────────
145
- // metricDecayRunCount / metricTotalDecayedCount / metricLastDecayAt 字段
146
- // 位于 MemoryDecayScheduler 内部,Agent 通过 memoryDecayScheduler.getMetrics() 读取
147
- /** 记忆衰减调度器(init 时创建,close 时销毁) */
148
- memoryDecayScheduler = null;
149
- /** 归档协调器(归档操作委托给 ArchiveCoordinator) */
150
- archiveCoordinator = null;
151
101
  constructor(opts) {
152
102
  super();
153
103
  this.#config = {
@@ -155,25 +105,45 @@ export class Agent extends TypedEventEmitter {
155
105
  dataDir: opts.dataDir,
156
106
  registryDir: opts.registryDir,
157
107
  maxContextTokens: opts.maxContextTokens ?? AGENT_CONSTANTS.DEFAULT_MAX_CONTEXT_TOKENS,
158
- personaName: opts.persona,
108
+ // per-LLM 输出预算默认值(token):宿主 per-LLM 配置透传;undefined = 不传 max_tokens
109
+ //(回服务端默认)。与角色包策略 act.outputLimit 的最终关系由 buildChatOptions 取更小值。
110
+ maxTokens: opts.maxTokens,
159
111
  permission: opts.permission ?? 'owner',
160
112
  allowedPaths: opts.allowedPaths ?? [],
161
113
  confirmWrites: opts.confirmWrites ?? false,
162
- vectorStore: opts.vectorStore,
163
- recallExcludeSources: opts.recallExcludeSources ?? [
164
- ...AGENT_CONSTANTS.DEFAULT_RECALL_EXCLUDE_SOURCES,
165
- ],
114
+ confirmScripts: opts.confirmScripts ?? false,
166
115
  storage: opts.storage,
167
- relationStore: opts.relationStore,
168
116
  sessionStore: opts.sessionStore,
117
+ roundStore: opts.roundStore,
169
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,
170
126
  tracer: opts.tracer,
171
127
  messages: opts.messages,
172
128
  enableContextSummary: opts.enableContextSummary ?? true,
173
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,
174
143
  };
175
144
  this.#provider = opts.provider;
176
145
  this.#backgroundProvider = opts.backgroundProvider ?? null;
146
+ this.#providerRouter = opts.providerRouter ?? null;
177
147
  // 如需自定义日志,请在创建 Agent 前调用 `import { setLogger } from '@zooique/memora'` 全局设置
178
148
  }
179
149
  // ─── 生命周期 ─────────────────────────────────────────
@@ -190,9 +160,48 @@ export class Agent extends TypedEventEmitter {
190
160
  this.pctx = pctx;
191
161
  this.validateCoreComponents();
192
162
  this.createPostInitComponents(pctx);
163
+ // 标记初始化完成
193
164
  this._initialized = true;
165
+ // 注册暂停超时归档(同进程暂停超时:30min 无心跳则归档清理)
166
+ this.registerPauseTimeoutArchiver();
167
+ this.registerWorkProjectionRefresh();
168
+ // 注:不加载持久化检查点——中止/断电一律走
169
+ // 「中断轮补全为完整 turn 身份 → 参与下一轮」,不存在跨重启恢复;
170
+ // 运行时暂停是同 turn 内续跑(内存态),无需磁盘载体。
194
171
  return pctx;
195
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
+ }
196
205
  /**
197
206
  * 关闭旧实例(如果已初始化),失败不阻塞重建
198
207
  */
@@ -215,7 +224,7 @@ export class Agent extends TypedEventEmitter {
215
224
  storage: this.#config.storage,
216
225
  registryDir: this.#config.registryDir,
217
226
  // SecurityGuard 由 Agent 层创建,解除 memory→security 反向依赖
218
- createSecurityGuard: (projectPath, memoraDir, configDir, agentDataDir) => new SecurityGuard(projectPath, memoraDir, this.#config.allowedPaths, this.#config.confirmWrites, this.#config.permission, configDir ?? this.#config.configDir, agentDataDir ?? this.#config.dataDir),
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),
219
228
  });
220
229
  return this.projectManager.initProject(this.#config.projectPath, undefined, this.#config.configDir);
221
230
  }
@@ -234,33 +243,80 @@ export class Agent extends TypedEventEmitter {
234
243
  /**
235
244
  * 创建 init() 后期组件(初始化完成后才需要的组件)
236
245
  */
237
- createPostInitComponents(pctx) {
246
+ createPostInitComponents(_pctx) {
238
247
  // chat() 并发锁管理器(生命周期与 Agent 实例一致)
239
- this.chatLockManager = new ChatLockManager();
240
- // 归档操作委托给 ArchiveCoordinator
241
- this.archiveCoordinator = new ArchiveCoordinator({
242
- getUserProfile: () => this.#userProfile,
243
- getInsightExtractor: () => this.insightExtractor,
244
- 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,
245
261
  getArchiveMode: () => this.#config.archiveMode,
246
262
  emit: (event, payload) => this.emit(event, payload),
247
263
  });
248
- // 记忆衰减职责委托给 MemoryDecayScheduler
249
- this.memoryDecayScheduler = new MemoryDecayScheduler({
250
- tracer: this.#config.tracer,
251
- onDecayCompleted: (payload) => {
252
- this.emit(AGENT_EVENTS.decayCompleted, payload);
253
- // MIND2-D4:从 agent.evaluateTimeliness() 迁移到 governance.evaluateTimeliness()
254
- void this._governance?.evaluateTimeliness().catch((err) => {
255
- logger.warn({ err }, 'L2 时效性评估自动触发失败(已降级,不影响衰减循环)');
256
- });
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
+ }
257
318
  },
258
- backgroundProvider: this.#backgroundProvider,
259
- index: pctx.index,
260
319
  });
261
- this.memoryDecayScheduler.start(pctx.index, AGENT_CONSTANTS.DECAY_INTERVAL_MS);
262
- // 记忆治理统一门面(L0/L1/L2/L3 + 诊断)
263
- this._governance = new MemoryGovernance(this.dedupManager, this.memoryDecayScheduler, this.memoryAdvisor);
264
320
  }
265
321
  /**
266
322
  * 流式对话(核心 API)
@@ -271,24 +327,30 @@ export class Agent extends TypedEventEmitter {
271
327
  const lockCtx = this.acquireChatLock(signal);
272
328
  const { myToken, combinedSignal, cleanupExternalSignal } = lockCtx;
273
329
  try {
274
- this._lastInteractionAt = new Date();
275
- // 上下文准备:角色匹配 → 记忆召回 → 技能注入 → 历史追加
276
- const recalledMemories = yield* this.prepareChatContext(input, combinedSignal);
277
- if (combinedSignal.aborted) {
330
+ // 流活跃置位:requestPause 空闲守卫据此判「任务进行中」——锁 180s 超时释放后
331
+ // isBusy=false 但流仍在产出,必须以流生命周期(而非锁持有)为准(09-03 锁语义变更回归修补)
332
+ this._flowActive = true;
333
+ // 状态机翻转是副作用,必须在并发闸门内执行——PAUSED 态收到 chat = 自动恢复 + 继续(作为补充注入);ERROR 态仍拒绝
334
+ if (!this.autoResumeIfPaused()) {
278
335
  yield {
279
- type: 'aborted',
280
- reason: this.#config.messages?.abortedByUser ?? 'User cancelled the conversation',
336
+ type: 'error',
337
+ message: '会话已超时,无法自动恢复,请重新开始新对话',
338
+ category: 'timeout',
281
339
  };
282
340
  return;
283
341
  }
284
- // LLM 流式调用 → 助手消息写历史 → 后处理
285
- yield* this.executeChatLoop(input, recalledMemories, combinedSignal);
342
+ this._lastInteractionAt = new Date();
343
+ // 委托种子编排器:对话路径完整闭环(prepare → act → reflect)
344
+ yield* this.internals.seedOrchestrator.runChat(input, combinedSignal);
286
345
  }
287
346
  finally {
347
+ // 流已退出:清除活跃标志(与置位对称;无论正常/异常退出都复位)
348
+ this._flowActive = false;
288
349
  // 仅当本调用仍是当前锁持有者时才清理资源(token 校验)
289
- this.chatLockManager?.release(myToken);
350
+ this.internals.chatLockManager?.release(myToken);
290
351
  cleanupExternalSignal();
291
- // 补执行对话期间暂存的配置重载请求
352
+ // turn 结束自动收尾:非暂停态无条件清理任务表(turn 内能力,不跨 turn 残留)
353
+ this.clearPlanOnTurnEnd();
292
354
  await this.flushPendingConfigReload();
293
355
  }
294
356
  }
@@ -304,7 +366,7 @@ export class Agent extends TypedEventEmitter {
304
366
  * 获取对话锁 — 返回锁上下文
305
367
  */
306
368
  acquireChatLock(signal) {
307
- const chatLock = this.chatLockManager;
369
+ const chatLock = this.internals.chatLockManager;
308
370
  if (!chatLock)
309
371
  throw configError('ChatLockManager', 'ChatLockManager 未初始化', []);
310
372
  if (chatLock.isBusy) {
@@ -314,79 +376,6 @@ export class Agent extends TypedEventEmitter {
314
376
  const cleanupExternalSignal = chatLock.attachExternalSignal(signal, internalAbort);
315
377
  return { myToken, internalAbort, cleanupExternalSignal, combinedSignal: internalAbort.signal };
316
378
  }
317
- /**
318
- * 准备对话上下文:角色匹配 → 记忆召回 → 技能注入 → 用户消息写历史
319
- * @returns 召回的记忆列表(供 loop.processUserInput 注入为 system 消息)
320
- */
321
- async *prepareChatContext(input, combinedSignal) {
322
- const loop = this.requireLoop;
323
- loop.cleanTemporarySystemMessages();
324
- // 角色自动匹配(回答前执行)
325
- await this.tryAutoMatchPersona(input);
326
- // 基元驱动召回(双通道:语义 + 关键词)
327
- yield { type: 'thinking', phase: 'recalling' };
328
- const recalledMemories = await this.recallAndInject(input);
329
- if (combinedSignal.aborted)
330
- return recalledMemories;
331
- // 技能关键词/正则匹配(当轮立即注入生效)
332
- yield { type: 'thinking', phase: 'processing' };
333
- this.matchAndInjectSkill(input);
334
- const history = this.requireHistory;
335
- await history.appendUser(input);
336
- return recalledMemories;
337
- }
338
- /**
339
- * 执行 LLM 流式循环 → 追加助手消息 → 触发后处理
340
- */
341
- async *executeChatLoop(input, recalledMemories, combinedSignal) {
342
- const loop = this.requireLoop;
343
- const history = this.requireHistory;
344
- let assistantContent = '';
345
- let wasAborted = false;
346
- try {
347
- for await (const chunk of loop.processUserInput(input, recalledMemories, combinedSignal)) {
348
- yield chunk;
349
- if (chunk.type === 'text') {
350
- assistantContent += chunk.content;
351
- }
352
- else if (chunk.type === 'aborted') {
353
- wasAborted = true;
354
- }
355
- }
356
- }
357
- catch (err) {
358
- if (isAbortError(err)) {
359
- yield { type: 'aborted', reason: 'User cancelled the conversation' };
360
- return;
361
- }
362
- else {
363
- yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
364
- return;
365
- }
366
- }
367
- if (wasAborted) {
368
- if (assistantContent.trim()) {
369
- const interruptedMark = this.#config.messages?.interrupted ?? '\n\n[已中断]';
370
- try {
371
- await history.appendAssistant(assistantContent + interruptedMark);
372
- }
373
- catch (err) {
374
- logger.warn({ err }, '中断消息历史写入失败');
375
- }
376
- }
377
- return;
378
- }
379
- // 追加助手消息到历史(best-effort)
380
- try {
381
- await history.appendAssistant(assistantContent);
382
- }
383
- catch (err) {
384
- logger.warn({ err }, '助手消息历史写入失败');
385
- }
386
- // 后处理阶段
387
- yield { type: 'thinking', phase: 'archiving' };
388
- await this.postProcess(input, assistantContent);
389
- }
390
379
  /**
391
380
  * 补执行对话期间暂存的配置重载请求
392
381
  */
@@ -408,231 +397,590 @@ export class Agent extends TypedEventEmitter {
408
397
  /**
409
398
  * 强制释放对话锁
410
399
  *
411
- * 场景:宿主主进程"无进展超时"兜底后,generator 可能仍卡在不可中断的 await 点
412
- * (executeToolCalls 和 generateContextSummary 已覆盖 signal 响应,但其他
413
- * 第三方库或未来新增的 await 点仍可能不响应 signal)。
414
- * 此时 _chatBusy 锁未释放,用户再发消息会被 chat() 的竞态保护拒绝,
415
- * 表现为"UI 能操作但发不出消息",需等到 3 分钟锁超时才能恢复。
416
- *
417
- * 本方法让宿主在确认 generator 挂起后强制释放锁,让用户能立即发起新对话。
418
- *
419
- * 安全机制(委托至 ChatLockManager.forceRelease):
420
- * - 递增 _chatLockToken,让原 chat() 的 finally 块检测到 token 不匹配后
421
- * 跳过资源清理(避免误清新调用者的 _chatBusy/chatAbortController)
422
- * - abort chatAbortController,让响应 signal 的 await 点(如 fetch)退出
423
- * - 原 generator 仍可能在后台运行(无法真正中断不响应 signal 的 await),
424
- * 但其 finally 块的 token 校验会阻止它影响新调用
425
- *
426
- * 幂等性:_chatBusy 已 false 时 no-op(多次调用安全)
427
- *
428
- * 使用约束:
429
- * - 仅在宿主确认 generator 挂起(如无进展超时)后调用
430
- * - 不应在常规 abort 路径调用(常规 abort 走 signal,generator 自然退出)
431
- * - 调用后不应再消费原 chat() generator 的后续 chunk(token 已变,chunk 无意义)
400
+ * 宿主在"无进展超时"确认 generator 挂起后调用,使用户能立即发起新对话(否则需等锁超时)。
401
+ * 安全机制委托 ChatLockManager.forceRelease:递增 token 让原 chat() 的 finally 跳过清理、abort signal 让响应点退出;
402
+ * 原 generator 仍可能后台运行但不影响新调用。幂等(未忙时 no-op)。
432
403
  */
433
404
  forceReleaseChatLock() {
434
- this.chatLockManager?.forceRelease();
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;
435
418
  }
419
+ // ─── 不中断工作模型 v2.0:公开 API ──────────────────────
436
420
  /**
437
- * 召回记忆 + 注入最近对话上下文
421
+ * 软暂停后续跑(不中断工作模型 v2.1)
438
422
  *
439
- * 从 chat() 中提取,职责:
440
- * 1. 双通道召回(语义 + 关键词)
441
- * 2. Layer 5: 最近对话注入
423
+ * 翻状态机为 RUNNING(触发 sessionResumed → 宿主转发 STATUS{running}),
424
+ * 驱动 loop.continueAfterPause 续跑并转发 chunk,完成后追加历史 + 后处理(同 chat 尾处理)。
425
+ * 硬停止(signal.abort)仍是唯一霸道中止路径,与软暂停严格区分。
442
426
  *
443
- * @param input 用户输入
444
- * @returns 召回的记忆列表
445
- */
446
- async recallAndInject(input) {
447
- // 实际 recall() 函数耗时 span(区别于 loop.ts 的 RECALL 注入 span)
448
- const tracer = this.#config.tracer ?? NOOP_TRACER;
449
- const recallSpan = tracer.startSpan(TRACE_SPANS.RECALL_ACTUAL, {
450
- queryLength: input.length,
451
- hasVectorStore: !!this.#config.vectorStore,
452
- });
453
- let recalledMemories = [];
454
- try {
455
- recalledMemories = await recall(this.requirePctx.index, input, {
456
- limit: AGENT_CONSTANTS.DEFAULT_RECALL_LIMIT,
457
- vectorStore: this.#config.vectorStore,
458
- excludeSources: this.#config.recallExcludeSources,
427
+ * 归属:补充输入归属当前问答闭环(复用 prepare 分配的闭环节点 roundId,不分裂新轮),
428
+ * 以 kind 区分交互类型(question-answer=主动提问回答 / supplement=暂停/流中补充)。
429
+ *
430
+ * @param input - 可选补充输入(空=续跑原路径;有=注入修正后续轮)
431
+ * @param signal - 可选 AbortSignal(硬停止仍走此路径)
432
+ * @param kind - 交互输入类型(默认 supplement;对主动提问的回答传 'question-answer')
433
+ */
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: '会话处于错误态,无法续跑',
459
442
  });
443
+ yield {
444
+ type: 'error',
445
+ message: '当前会话处于错误态,无法续跑,请重新开始',
446
+ category: 'unknown',
447
+ };
448
+ return;
460
449
  }
461
- catch (err) {
462
- recallSpan.recordException(err instanceof Error ? err : new Error(String(err)));
463
- throw err;
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);
464
489
  }
465
490
  finally {
466
- recallSpan.setAttribute('resultCount', recalledMemories.length);
467
- recallSpan.end();
468
- }
469
- if (recalledMemories.length > 0) {
470
- this.emit(AGENT_EVENTS.memoryRecalled, { count: recalledMemories.length, query: input });
471
- // FIX-P1-2:boost 持久化拆分为 fire-and-forget,不阻塞 chat 读路径
472
- // boost 是软指标(每次 +0.05,上限 1.0),写入失败仅 log 不影响 chat 流程
473
- const ids = recalledMemories.map((m) => m.id);
474
- void boostScores(this.requirePctx.index, ids).catch((err) => {
475
- logger.warn({ err }, 'boost 持久化失败(不影响 chat 流程)');
476
- this.emit(AGENT_EVENTS.boostPersistFailed, { memoryId: ids.join(','), message: toError(err).message });
477
- });
478
- }
479
- // Layer 5: 最近对话注入
480
- const loop = this.requireLoop;
481
- const recentHistory = loop.getRecentHistory(AGENT_CONSTANTS.DEFAULT_RECENT_HISTORY_ROUNDS);
482
- if (recentHistory.length > 0) {
483
- const msgs = this.#config.messages;
484
- const label = msgs?.recentConversationLabel ?? '[Recent conversation]';
485
- const userLabel = msgs?.userLabel ?? 'User';
486
- const assistantLabel = msgs?.assistantLabel ?? 'Assistant';
487
- const recentPrompt = `${label}\n` +
488
- recentHistory
489
- .map((m) => `${m.role === 'user' ? userLabel : assistantLabel}:${m.content}`)
490
- .join('\n');
491
- loop.injectSystemMessage(recentPrompt);
492
- logger.debug({ turns: recentHistory.length / 2 }, '最近对话已注入');
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();
493
502
  }
494
- return recalledMemories;
495
503
  }
496
504
  /**
497
- * 匹配技能并立即注入本轮对话(当轮生效,不延迟到下一轮)
505
+ * 消费执行流的单一真理源
498
506
  *
499
- * 与 persona 的 tryAutoMatchPersona 同模式:匹配 → 注入 → 本轮 LLM 即生效。
500
- * 原设计为 injectActiveSkill(注入上一轮匹配结果),导致用户说"写代码"的第一轮
501
- * 得不到技能增强,需再发一条消息才生效。已改为实时匹配注入。
507
+ * chat() 与续跑(continueAfterPause)必须共用同一份消费逻辑——两份同构实现会漂移
508
+ * (实证:续跑漏 paused 分支导致三方分叉);收口于此,新增 chunk 类型只需改一处。
509
+ * 暂停幂等锁的释放放 finally——清理是"退出本作用域的不变式"而非某分支动作,新增 return 分支无遗漏。
502
510
  *
503
- * 匹配策略:regex trigger 优先(score=1.0),其次关键词匹配(阈值 0.3)。
504
- * 清理机制:cleanTemporarySystemMessages() 每轮开头清除临时 system 消息,
505
- * 技能 prompt 自然不会累积到下一轮。
506
- *
507
- * @param input 用户输入文本
511
+ * @returns 消费结果;failed 为 true 时调用方应立即 return(错误 chunk 已 yield)
508
512
  */
509
- matchAndInjectSkill(input) {
510
- if (!this.skillManager || !this.loop)
511
- return;
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;
512
519
  try {
513
- const match = this.skillManager.match(input);
514
- if (match) {
515
- const skillPrompt = this.skillManager.buildSystemPrompt(match.skill.name);
516
- if (skillPrompt) {
517
- this.loop.injectSystemMessage(skillPrompt);
518
- logger.debug({ skill: match.skill.name, score: match.score }, '技能 prompt 已注入(当轮生效)');
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;
519
556
  }
520
- this.emit(AGENT_EVENTS.skillMatched, { skill: match.skill.name, score: match.score });
521
557
  }
522
558
  }
523
559
  catch (err) {
524
- logger.warn({ 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 };
525
597
  }
598
+ finally {
599
+ // 释放暂停幂等锁覆盖三路——残留会让 requestPause 的幂等检查永久拒绝后续暂停请求
600
+ this._sessionManager?.cancelPendingPause();
601
+ // 同步清理 loop 的 pause 申请(clearPauseRequest 委托 interruptQueue 过滤 kind='pause' 条目)
602
+ this.requireLoop.clearPauseRequest();
603
+ }
604
+ return { content, aborted, paused, failed: false };
526
605
  }
527
606
  /**
528
- * 对话后处理:用户画像归档、角色匹配、技能匹配、Insight 提取
529
- *
530
- * 所有归档/匹配操作均为 best-effort:任何子步骤失败不应影响用户已收到的回答,
531
- * 失败仅记录日志,不向上抛出异常。
607
+ * 暂停会话(用户/Agent/系统均可触发,暂停前自动创建检查点)
608
+ * @param lowRisk 低风险暂停不计入连续暂停计数(默认 false)
609
+ */
610
+ pause(reason, source = 'user', lowRisk = false) {
611
+ this.assertInitialized('pause');
612
+ return this.requireSessionManager.pause(reason, source, lowRisk);
613
+ }
614
+ /**
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)不走自动恢复,由独立的暂停恢复短路逻辑处理。
532
625
  *
533
- * ADR-015 归档模式控制:
534
- * - 角色匹配 + 技能匹配不受 archiveMode 影响(每轮都执行,非归档行为)
535
- * - `manual` 模式跳过所有自动归档(profile + insight),需用户手动调用
536
- * archiveProfileFacts() / archiveInsight() 触发
537
- * - `full` / `insights-only` 模式下 profile + insight 都自动归档
538
- * (会话归档实现后,`insights-only` 将跳过对话原始内容自动归档)
539
- */
540
- async postProcess(input, assistantContent) {
541
- // postProcess 全流程 span(角色匹配 + 技能匹配 + 归档 + AutoConfigRefiner)
542
- const tracer = this.#config.tracer ?? NOOP_TRACER;
543
- const span = tracer.startSpan(TRACE_SPANS.POST_PROCESS, {
544
- archiveMode: this.#config.archiveMode,
545
- });
546
- try {
547
- await this.doPostProcess(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
+ ]);
548
637
  }
549
- finally {
550
- span.end();
638
+ if (status === 'paused') {
639
+ return this.resume();
551
640
  }
641
+ return true;
552
642
  }
553
643
  /**
554
- * postProcess 内部实现(为 span 埋点提供 try/finally 包裹边界)
644
+ * 请求软暂停(不中断工作模型 v2.1)
555
645
  *
556
- * 原 postProcess 逻辑完整保留于此,由外层 postProcess 负责 span 生命周期管理。
557
- */
558
- async doPostProcess(input, assistantContent) {
559
- // 技能匹配已迁移到 chat() 开头的 matchAndInjectSkill()(当轮实时生效),
560
- // 与 persona 的 tryAutoMatchPersona 同模式,消除"第一轮无技能"的一轮延迟问题。
561
- // ADR-015 + FIX-P1-4: archiveMode 三态控制集中到 ArchiveCoordinator
562
- // 此处统一传 { autoTriggered: true },由 ArchiveCoordinator 内部按 archiveMode 判断是否跳过:
563
- // - manual 模式 → 跳过 profile/insight 自动归档(用户需手动调用)
564
- // - full / insights-only 模式 → 执行
565
- // 角色匹配/技能匹配/AutoConfigRefiner 属"配置学习"行为,非归档,每轮都执行。
566
- const history = this.requireHistory;
567
- // 用户画像实时归档(委托 ArchiveCoordinator,与手动归档路径统一,消除 DRY 违反)
568
- try {
569
- // fire-and-forget 包装:registerPendingArchive 确保 close() 时等待后台归档完成
570
- // 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
571
- const archiveFactsPromise = this.requireArchiveCoordinator.archiveProfileFacts(input, { autoTriggered: true }).then(() => { }, (err) => {
572
- const message = err instanceof Error ? err.message : String(err);
573
- logger.warn({ err, stage: 'profile' }, '归档失败');
574
- this.emit(AGENT_EVENTS.archiveFailed, { stage: 'profile', message: message.slice(0, 200) });
575
- });
576
- history.registerPendingArchive(archiveFactsPromise);
646
+ * 经 sm.requestPause 暂存 pending(SessionStateMachine 是 pending 真理源)+ loop.pauseRequested
647
+ * 入 interruptQueue(在下一 step 边界挂起,不 abort)。
648
+ * 状态机翻 PAUSED 延后到 loop 边界真正挂起时——内核事实驱动而非申请即翻转;与硬停止(abort)严格区分,
649
+ * 软暂停保留历史、可经 resumeExecution 续跑。
650
+ *
651
+ * @returns true=请求已注册(下一 step 边界生效);false=无法暂停
652
+ */
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;
577
663
  }
578
- catch (err) {
579
- logger.warn({ err }, '用户画像归档初始化失败');
664
+ if (status === 'error') {
665
+ logger.debug('requestPause 忽略:会话处于异常状态,无法暂停');
666
+ return false;
580
667
  }
581
- // 输入分类 → Insight 提取(委托 ArchiveCoordinator,与手动归档路径统一)
582
- try {
583
- // fire-and-forget 包装:classify 判断由 ArchiveCoordinator 内部完成
584
- // 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
585
- const archiveInsightPromise = this.requireArchiveCoordinator.archiveInsight(input, assistantContent, { autoTriggered: true }).then(() => { }, (err) => {
586
- const message = err instanceof Error ? err.message : String(err);
587
- logger.warn({ err, stage: 'insight' }, '归档失败');
588
- this.emit(AGENT_EVENTS.archiveFailed, { stage: 'insight', message: message.slice(0, 200) });
589
- });
590
- history.registerPendingArchive(archiveInsightPromise);
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;
591
680
  }
592
- catch (err) {
593
- logger.warn({ err }, 'Insight 提取初始化失败');
681
+ // 委托 SessionStateMachine 管理 pending 暂停状态(含幂等检查)
682
+ if (!sm.requestPause(reason, source)) {
683
+ logger.debug('requestPause 忽略:已有待处理的暂停请求');
684
+ return false;
594
685
  }
595
- // AutoConfigRefiner(模式 3:Agent 智能总结)
596
- if (this.autoConfigRefiner) {
597
- try {
598
- // 注册到 pendingArchives,确保 close() 时等待后台分析完成,避免写入已关闭的存储
599
- const analyzePromise = this.autoConfigRefiner.analyze(input, assistantContent).catch((err) => {
600
- logger.warn({ err }, 'AutoConfigRefiner 分析失败');
601
- });
602
- history.registerPendingArchive(analyzePromise);
603
- }
604
- catch (err) {
605
- logger.warn({ err }, 'AutoConfigRefiner 初始化失败');
606
- }
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');
607
730
  }
731
+ return checkpointCleared;
608
732
  }
609
733
  /**
610
- * 发送用户消息,非流式返回完整回复
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 写入失败,仅记日志),不阻塞插话本身。
611
743
  */
612
- async chatSync(input, signal) {
613
- let result = '';
614
- for await (const chunk of this.chat(input, signal)) {
615
- if (chunk.type === 'text') {
616
- result += chunk.content;
617
- }
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
+ });
618
753
  }
619
- return result;
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();
620
848
  }
621
849
  /**
622
- * 分叉当前会话(委托至 SessionManager)
850
+ * 内核→宿主信号:当前会话是否可"无输入续跑"(决定暂停按钮显隐 + 暂停后继续 UI)
623
851
  *
624
- * 分叉后原会话完整保留,新分支拥有独立消息历史。
625
- * 记忆索引(IMemoryStorage)全局共享,不受分叉影响。
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 态。
626
872
  */
627
- forkSession(targetSession) {
873
+ triggerError(cause) {
874
+ this.assertInitialized('triggerError');
875
+ return this.requireSessionManager.triggerError(cause);
876
+ }
877
+ /**
878
+ * 从异常恢复:校验恢复条件(error.recovered === true 且 cause 已解除)
879
+ */
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) {
628
937
  this.assertInitialized('forkSession');
629
- return this.requireSessionManager.forkSession(targetSession);
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
+ });
945
+ }
946
+ return result;
947
+ }
948
+ /**
949
+ * 分叉会话首轮命名(best-effort,fire-and-forget)
950
+ *
951
+ * 分叉新会话是平等普通会话,自动命名(未提供 targetSession)时应拥有 LLM 生成的语义标题。
952
+ * 取新会话第一个 Round 的用户消息作为命名输入(分叉点即新会话起点,语义=首轮问答);
953
+ * SessionNamer 内部已有 autoName 存在即跳过(幂等),LLM 失败/无价值降级占位标题。
954
+ *
955
+ * @param result - 分叉结果(含新会话名/日期/Round 列表)
956
+ */
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);
630
977
  }
631
978
  /**
632
- * 切换到指定项目
979
+ * 切换到指定项目:切换后自动 rebuildComponents();Agent 级记忆存储(宿主注入,实例不随项目切换重建)保留,项目级配置(.memora/)重新加载
633
980
  *
634
- * 切换后自动 rebuildComponents(),无需手动调用。
635
- * Agent 级记忆(memora.db)保留,项目级配置(.memora/)重新加载。
981
+ * `nameOrPath` 只接受两种输入:① 项目注册表中的项目名(依赖注册表跨项目共享,见 dataDir/registryDir);
982
+ * ② 项目根目录的**绝对路径**。二者都不满足时抛 configError——否则相对串会以 process.cwd()
983
+ * 为基准解析,静默造出非预期的项目目录并占用其锁。
636
984
  */
637
985
  async switchProject(nameOrPath) {
638
986
  this.assertInitialized('switchProject', ['projectManager', 'provider']);
@@ -645,6 +993,14 @@ export class Agent extends TypedEventEmitter {
645
993
  const nameOrPathLower = nameOrPath.toLowerCase();
646
994
  target = projects.find((p) => p.name.toLowerCase() === nameOrPathLower || p.path.toLowerCase() === nameOrPathLower);
647
995
  }
996
+ // 注册表未命中时按路径处理。相对路径会以 process.cwd() 为基准解析——宿主进程的 cwd
997
+ // 不具项目语义,会静默创建出非预期的项目目录并占用其锁,故只接受绝对路径。
998
+ if (!target && !isAbsolute(nameOrPath)) {
999
+ throw configError('项目切换失败', `「${nameOrPath}」既不是已注册的项目名,也不是绝对路径`, [
1000
+ '按名切换:先经 agent.projects.registerProject(项目路径, 项目名) 注册,并保证注册表跨项目共享(多项目共用同一 dataDir 或 registryDir)',
1001
+ '按路径切换:传入项目根目录的绝对路径',
1002
+ ]);
1003
+ }
648
1004
  const projectPath = target ? target.path : nameOrPath;
649
1005
  const projectName = target ? target.name : getBaseName(nameOrPath);
650
1006
  const newPctx = await pm.initProject(projectPath, projectName, this.#config.configDir);
@@ -653,7 +1009,7 @@ export class Agent extends TypedEventEmitter {
653
1009
  // 先尝试重建组件,失败时回滚 pctx 防止状态不一致
654
1010
  try {
655
1011
  this.pctx = newPctx;
656
- await this.rebuildComponentsWithCurrentCtx();
1012
+ await this.rebuildComponents();
657
1013
  }
658
1014
  catch (err) {
659
1015
  // 回滚:恢复旧 pctx(若存在)
@@ -673,121 +1029,162 @@ export class Agent extends TypedEventEmitter {
673
1029
  }
674
1030
  // ─── 组件组装 ─────────────────────────────────────────
675
1031
  /**
676
- * 组装所有运行时组件(委托给 assembler 工厂)
1032
+ * 组装所有运行时组件(一行委托给 assembler 工厂)
1033
+ *
1034
+ * 接线回调(工具幂等 outbox / 任务表 / loop 回调 / createSessionManager)与装配私有状态
1035
+ * (暂存队列 / 停滞计数器)全部由 assembler 组装收口——Agent 只注入稳定能力(hooks)。
677
1036
  */
678
1037
  async assembleComponents(pctx) {
679
1038
  const result = await assembleComponents(pctx, {
680
1039
  provider: this.provider,
681
1040
  backgroundProvider: this.#backgroundProvider,
1041
+ providerRouter: this.#providerRouter,
682
1042
  projectPath: this.#config.projectPath,
683
1043
  configDir: this.#config.configDir,
684
- personaName: this.#config.personaName,
1044
+ activeRolePack: this.#config.activeRolePack,
1045
+ // 组数据(宿主装配级):组长角色包 + 组员名单(会议名单容器)
1046
+ rolePackTeams: this.#config.rolePackTeams,
685
1047
  maxContextTokens: this.#config.maxContextTokens,
1048
+ // per-LLM 输出预算默认值:透传组装器 → AgentLoop.defaultMaxTokens(buildChatOptions 填底座)
1049
+ maxTokens: this.#config.maxTokens,
686
1050
  sessionStore: this.#config.sessionStore,
687
- relationStore: this.#config.relationStore,
1051
+ roundStore: this.#config.roundStore,
688
1052
  tracer: this.#config.tracer,
689
1053
  messages: this.#config.messages,
690
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,
691
1063
  existingSkillManager: this.skillManager,
692
- onWorkProjectionGenerated: (sourcePath, summary) => {
693
- this.emit(AGENT_EVENTS.workProjectionGenerated, { sourcePath, summary });
694
- },
695
- onContextTruncated: (skippedCount, keptCount) => {
696
- this.emit(AGENT_EVENTS.contextTruncated, { skippedCount, keptCount });
697
- },
698
- onDedupCompleted: (report) => {
699
- this.emit(AGENT_EVENTS.dedupCompleted, { deduplicatedCount: report.deduplicatedCount, demotedIds: report.demotedIds });
700
- },
701
- onGuardrailError: (rule, message) => {
702
- this.emit(AGENT_EVENTS.guardrailError, { rule, message });
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(),
703
1085
  },
704
1086
  });
705
1087
  this.history = result.history;
706
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
+ };
707
1095
  this.toolExec = result.toolExec;
708
- this.personaManager = result.personaManager;
709
- this.#userProfile = result.userProfile;
710
1096
  this.skillManager = result.skillManager;
711
- this.insightExtractor = result.insightExtractor;
712
- 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
+ });
713
1102
  this.memoryInspector = result.memoryInspector;
714
- this.dedupManager = result.dedupManager;
715
- this.memoryAdvisor = result.memoryAdvisor;
716
- this.autoConfigRefiner = result.autoConfigRefiner;
1103
+ this.internals.dedupManager = result.dedupManager;
1104
+ this.internals.memoryAdvisor = result.memoryAdvisor;
717
1105
  this.workProjection = result.workProjection;
718
- this.sessionArchiver = result.sessionArchiver;
1106
+ this.internals.sessionArchiver = result.sessionArchiver;
719
1107
  this.textPolisher = result.textPolisher;
720
- // 绑定冲突检测回调,InsightExtractor 检测到 contradicts 时 emit('conflictDetected')
721
- // 与 bindGetRecentHistory 同模式:解决 Agent 晚于 InsightExtractor 创建的时序循环依赖
722
- this.insightExtractor.bindOnConflict((info) => {
723
- this.emit(AGENT_EVENTS.conflictDetected, info);
724
- });
725
- // 注入 VectorStore 到 MemoryInspector,启用混合搜索
726
- if (this.memoryInspector && this.#config.vectorStore) {
727
- 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);
728
1121
  }
729
- // 创建会话管理器(提取 createSessionManager 辅助方法,消除重复)
730
- this._sessionManager = this.createSessionManager();
1122
+ logger.info({ provider: this.#provider.name }, 'Provider 已切换');
731
1123
  }
732
1124
  /**
733
- * 用当前 pctx 重建 history / loop
1125
+ * 运行时更新上下文窗口上限(token)
1126
+ *
1127
+ * 模型热切换配套:同步三处装配期值拷贝(loop 截断/软上限、ContextManager 阈值、
1128
+ * ContextPreparer 预算与占用快照),全部对新窗口即刻生效。对话进行中禁止更新
1129
+ * (与 setProvider 同守卫)。
1130
+ *
1131
+ * @param tokens 新窗口 token 数
734
1132
  */
735
- async rebuildComponentsWithCurrentCtx() {
736
- if (!this.pctx)
737
- return;
738
- await this.assembleComponents(this.pctx);
739
- // 重建会话管理器:assembleComponents 创建了新的 history/loop 实例(复用 createSessionManager)
740
- 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 上下文窗口已更新');
741
1146
  }
742
1147
  /**
743
- * 创建会话管理器(提取重复的 forwardEvent + SessionManager 构造逻辑)
1148
+ * 运行时更新 per-LLM 输出预算默认值(token)
744
1149
  *
745
- * assembleComponents 和 rebuildComponentsWithCurrentCtx 共享同一套构造逻辑:
746
- * - 通过回调访问当前组件(支持 rebuild 后自动获取最新引用)
747
- * - 事件转发桥接:SessionManager 使用宽类型 (string, Record<string,unknown>),
748
- * Agent 内部桥接到 TypedEventEmitter 的强类型 emit
749
- * - 运行时校验事件名是否在 AgentEventMap 中,避免不安全的类型断言
1150
+ * Provider 热切换配套(与 setContextWindow 同链路):同步 #config 拷贝并即时生效于 loop——
1151
+ * 切换 Provider 后新配置的 maxTokens 无需重装 Agent。undefined = 清除(回服务端默认)。
1152
+ * 不设 assertNotBusy:maxTokens 是请求参数(非上下文结构),轮内下一迭代即用新值,无一致性风险。
750
1153
  *
751
- * @returns 新的 SessionManager 实例
1154
+ * @param tokens 新输出预算(token);undefined 清除
752
1155
  */
753
- createSessionManager() {
754
- const forwardEvent = (event, data) => {
755
- if (AGENT_EVENT_SET.has(event)) {
756
- this.emit(event, data);
757
- }
758
- };
759
- return new SessionManager(
760
- // 惰性 getter:SessionManager 内部调用时 Agent 已 init,用 ! 窄化
761
- () => this.history, () => this.loop, this.#config.sessionStore, () => this.chatLockManager?.isBusy ?? false, forwardEvent);
762
- }
763
- // ─── Provider 管理 ────────────────────────────────────
764
- setProvider(provider) {
765
- // 对话进行中切换 Provider 会导致同一 processUserInput 循环内前后两次 LLM 调用命中不同 Provider
766
- // (模型上下文窗口假设不一致 → 可能导致上下文截断逻辑误判或 tool_call 格式不兼容)
767
- this.assertNotBusy('切换 Provider');
768
- this.#provider = provider;
1156
+ setMaxTokens(tokens) {
1157
+ this.#config.maxTokens = tokens;
769
1158
  if (this.loop) {
770
- this.loop.setProvider(provider);
1159
+ this.loop.setDefaultMaxTokens(tokens);
771
1160
  }
772
- 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();
773
1171
  }
774
1172
  setBackgroundProvider(provider) {
775
1173
  // 与 setProvider 一致,对话进行中禁止切换后台 Provider
776
1174
  this.assertNotBusy('切换后台 Provider');
777
1175
  this.#backgroundProvider = provider;
778
- // 同步更新 AutoConfigRefiner 的后台 Provider
779
- if (this.autoConfigRefiner) {
780
- 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);
781
1182
  }
782
1183
  logger.info({ hasBackground: !!provider }, '后台 Provider 已切换');
783
1184
  }
784
- // ─── 归档模式管理(ADR-015) ───────────────────────────
1185
+ // ─── 归档模式管理 ───────────────────────────
785
1186
  /**
786
- * 运行时切换归档模式
787
- *
788
- * 与 setProvider 一致,对话进行中禁止切换(避免本轮 postProcess 行为不一致)。
789
- *
790
- * @param mode 目标模式
1187
+ * 运行时切换归档模式:对话进行中禁止切换(避免本轮 postProcess 行为不一致),无变更幂等返回
791
1188
  */
792
1189
  setArchiveMode(mode) {
793
1190
  this.assertNotBusy('切换归档模式');
@@ -799,284 +1196,172 @@ export class Agent extends TypedEventEmitter {
799
1196
  logger.info({ from: prev, to: mode }, '归档模式已切换');
800
1197
  }
801
1198
  /**
802
- * 查询当前归档模式
803
- *
804
- * 宿主 UI(如设置面板)可据此同步显示当前模式。
1199
+ * 查询当前归档模式(宿主 UI 可据此同步显示)
805
1200
  */
806
1201
  getArchiveMode() {
807
1202
  return this.#config.archiveMode;
808
1203
  }
809
1204
  /**
810
- * 手动切换角色(宿主 UI 角色选择器入口)
811
- *
812
- * 与 doPostProcess 中的自动匹配走同一条事件链路,确保:
813
- * 1. AgentLoop 的 systemPromptPrefix 立即刷新(下一次对话使用新角色 prompt)
814
- * 2. 发射 personaSwitched 事件,触发 spriteLifecycleManager 的完整副作用:
815
- * - emit('personaChanged') 通知宿主 UI
816
- * - proactiveEngine.addNotice('persona', ...) 记录通知
817
- * - perceptionCoordinator.refreshBeforeChat() 基于新角色 traits 重新推导情感基调
818
- *
819
- * 对话进行中切换角色会破坏当前 system prompt,与项目切换一致拒绝。
820
- *
821
- * @param name 目标角色名
822
- * @returns 切换成功返回新角色的 system prompt 段;角色不存在或切换失败返回 null
823
- */
824
- switchPersona(name) {
825
- this.assertInitialized('switchPersona');
826
- this.assertNotBusy('切换角色');
827
- if (!this.personaManager)
828
- return null;
829
- // 同名切换幂等:直接返回当前 prompt,不触发事件链路
830
- if (this.personaManager.activeName === name) {
831
- return this.personaManager.buildSystemPrompt();
832
- }
833
- const prevName = this.personaManager.activeName;
834
- try {
835
- this.personaManager.switchPersona(name);
836
- }
837
- catch (err) {
838
- // 角色不存在时 PersonaManager 抛 MemoraError,降级为 null 返回
839
- logger.warn({ err: toError(err).message, name }, '手动切换角色失败');
840
- return null;
841
- }
842
- // 同步刷新 AgentLoop 的角色前缀(关键:否则下一次对话仍用旧角色 prompt)
843
- // 与 doPostProcess 自动匹配共用同一段逻辑,ADR-017 枝叶层 2 次提取
844
- this.refreshPersonaPrefixOnLoop();
845
- // 发射切换事件,触发宿主 UI 刷新 + 感知重推导 + 通知队列记录
846
- this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: name });
847
- logger.info({ from: prevName, to: name }, '角色手动切换');
848
- return this.personaManager.buildSystemPrompt();
849
- }
850
- /**
851
- * 获取角色切换锁定状态(透传 PersonaManager,P0-2 用户体验打磨)
852
- *
853
- * 与 switchPersona 分离:switchPersona 仍返回 string | null 不变,
854
- * 锁定原因查询走独立路径,避免破坏既有契约(cli.ts、sprite.test.ts 等消费者无感)。
855
- *
856
- * 宿主 IPC 层调用此方法前置判断锁定状态,区分"切换失败"原因
857
- * (locked / busy / not_found / invalid),让用户知道为什么没反应。
858
- *
859
- * @returns locked 是否处于锁定状态;unlockAt 锁定自动恢复时间戳(ms epoch),未锁定时为 null
1205
+ * 获取角色包切换锁定状态(透传 RolePackManager,宿主 IPC 层据此区分"切换失败"原因)
1206
+ * @returns locked 是否锁定;unlockAt 锁定自动恢复时间戳(ms epoch),未锁定时为 null
860
1207
  */
861
- getPersonaSwitchLockStatus() {
862
- this.assertInitialized('getPersonaSwitchLockStatus');
863
- if (!this.personaManager)
1208
+ getRolePackSwitchLockStatus() {
1209
+ this.assertInitialized('getRolePackSwitchLockStatus');
1210
+ if (!this._rolePackManager)
864
1211
  return { locked: false, unlockAt: null };
865
- return this.personaManager.getSwitchLockStatus();
1212
+ return this._rolePackManager.getSwitchLockStatus();
866
1213
  }
867
1214
  /**
868
- * 刷新 AgentLoop 的 systemPromptPrefix(角色 prompt + profile prompt)
1215
+ * 获取当前激活角色包的 traits(宿主情感计算 API)
869
1216
  *
870
- * 提取自 doPostProcess 自动匹配 + switchPersona 手动切换两处共用逻辑(ADR-017 枝叶层 2 次提取)。
871
- * 组装规则:[personaPrompt, profilePrompt].filter(Boolean).join('\n\n') + 末尾分隔符 '---'
872
- *
873
- * 调用时机:
874
- * - tryAutoMatchPersona 中角色自动匹配成功后(chat() 回答前)
875
- * - switchPersona 手动切换成功后
876
- * - loop 为 null 时静默跳过(init 前或 close 后的边界场景)
1217
+ * @returns traits 键值对,无激活角色包或无 traits 时返回 undefined
877
1218
  */
878
- refreshPersonaPrefixOnLoop() {
879
- if (!this.loop)
880
- return;
881
- const profilePrompt = this.userProfile?.buildSystemPrompt() ?? '';
882
- const personaPrompt = this.personaManager?.buildSystemPrompt() ?? '';
883
- const newPrefix = [personaPrompt, profilePrompt].filter(Boolean).join('\n\n') +
884
- ([personaPrompt, profilePrompt].some(Boolean) ? '\n\n---\n\n' : '');
885
- this.loop.refreshPersonaPrefix(newPrefix);
1219
+ getActiveTraits() {
1220
+ return this._rolePackManager?.getActiveTraits();
886
1221
  }
887
1222
  /**
888
- * 角色自动匹配(best-effort:失败不阻塞对话流程)
889
- *
890
- * 在 chat() 回答前执行,确保本轮 LLM 调用就用匹配到的角色 system prompt。
891
- * 两层匹配策略:关键词高置信度 → LLM 辅助(低置信度且 backgroundProvider 已注入时)。
892
- * LLM 辅助匹配在 agent 层执行,遵循 backend_layers_rules §分层职责(persona/ 不直接调 LLM)。
893
- *
894
- * 匹配成功时:切换角色 + 发射 personaSwitched 事件 + 刷新 AgentLoop 前缀。
895
- * 匹配失败/异常时:静默降级,保持当前角色。
1223
+ * 刷新 AgentLoop 的 systemPromptPrefix(SSOT:buildSystemPromptPrefix)
896
1224
  *
897
- * @param input 用户输入文本
1225
+ * 角色包是 system prompt 的唯一注入源。调用时机:手动切换成功后、reloadConfig 重载后;
1226
+ * loop 为 null 时静默跳过(init 前或 close 后边界)。统一走 buildSystemPromptPrefix 真理源,
1227
+ * 全局技能清单也在该前缀内(不能只拼 rolePackPrompt)。等价于「按 activePack 视角刷新」(roundRole=null)。
898
1228
  */
899
- async tryAutoMatchPersona(input) {
900
- if (!this.personaManager)
901
- return;
902
- try {
903
- let matchedPersona = null;
904
- if (this.personaManager.canAutoMatch()) {
905
- matchedPersona = this.personaManager.autoMatch(input);
906
- // 关键词低置信度且 backgroundProvider 已注入 → LLM 辅助语义匹配
907
- const bgProvider = this.#backgroundProvider;
908
- if (!matchedPersona && bgProvider) {
909
- matchedPersona = await matchPersonaByLlm(bgProvider, this.personaManager.list, this.personaManager.activeName, input);
910
- }
911
- }
912
- if (matchedPersona) {
913
- const prevName = this.personaManager.activeName;
914
- this.personaManager.switchPersona(matchedPersona);
915
- this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: matchedPersona });
916
- // 刷新 AgentLoop 的角色前缀(与 switchPersona 共用同一段逻辑,ADR-017 枝叶层 2 次提取)
917
- this.refreshPersonaPrefixOnLoop();
918
- logger.info({ persona: matchedPersona }, '角色自动切换');
919
- }
920
- else {
921
- // 关键词 + LLM 均未命中,且当前角色不是列表首个角色 → 回退到首个角色
922
- // "切过去回不来":用户从默认切到散文作者后,输入无关话题应回到默认角色
923
- // 使用 list[0] 而非硬编码 'default':防止 default 角色被删除后 fallback 抛异常
924
- const shouldFallback = this.shouldFallbackToDefault();
925
- if (shouldFallback) {
926
- const fallbackTarget = this.personaManager.list[0]?.name ?? 'default';
927
- const prevName = this.personaManager.activeName;
928
- this.personaManager.switchPersona(fallbackTarget);
929
- this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: fallbackTarget });
930
- this.refreshPersonaPrefixOnLoop();
931
- logger.info({ persona: fallbackTarget }, '角色回退默认');
932
- }
933
- }
934
- }
935
- catch (err) {
936
- logger.warn({ err }, '角色自动匹配失败');
937
- }
1229
+ refreshRolePackPrefixOnLoop() {
1230
+ this.refreshRolePackPrefixForRound(null);
938
1231
  }
939
1232
  /**
940
- * 判断是否应回退到默认角色
941
- *
942
- * 回退条件(全部满足):
943
- * 1. 当前激活角色不是默认角色(已在默认角色则无需回退)
944
- * 2. 角色管理器存在且非锁定状态(锁定时 switchPersona 也会被拦截,回退无意义)
1233
+ * 会议阶梯推进:按当前 active 任务项的 rolePack 派生本轮装配视角。
945
1234
  *
946
- * 注意:bgProvider 未配置时也会触发回退——因为关键词 + LLM 均未命中说明当前角色不匹配本轮对话,
947
- * 回退默认角色比卡在错误角色上更合理。
948
- *
949
- * @returns 是否应回退到默认角色
1235
+ * 由 assembler.getTaskTable 每轮注入时驱动(hooks.applyActivePlanItemAssembly),与任务表渲染同源,
1236
+ * 保证后继 step 换角色即时生效(装配视角不能只在 prepare.run 设一次,否则展示层正确/装配层冻结)。
1237
+ * roundRole=candidate(可能为 null,无有效覆盖时回落 activePack 组长),调 refreshRolePackPrefixForRound 重建前缀。
1238
+ * 防重:candidate 与 rolePackManager 当前 roundAssemblyPerspective 一致则跳过,避免每轮迭代重复重建。
950
1239
  */
951
- shouldFallbackToDefault() {
952
- if (!this.personaManager)
953
- return false;
954
- // manual 模式下不回退:用户明确固定了角色,回退会破坏其意图
955
- if (this.personaManager.currentMode !== 'auto')
956
- return false;
957
- const status = this.personaManager.getSwitchLockStatus();
958
- if (status.locked)
959
- return false; // 锁定中不回退,避免无意义调用 switchPersona
960
- // 当前角色已是���表首个角色("默认"角色)→ 无需回退
961
- const defaultName = this.personaManager.list[0]?.name;
962
- return defaultName !== undefined && this.personaManager.activeName !== defaultName;
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 任务项切换');
963
1249
  }
964
1250
  /**
965
- * 手动触发 profile facts 归档(manual 模式下使用)
966
- *
967
- * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
968
- * full / insights-only 模式下也可调用(会重复归档,但不推荐)。
969
- *
970
- * FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
971
- * 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
972
- * 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
973
- *
974
- * 归档逻辑已委托给 ArchiveCoordinator
1251
+ * 按本轮表层装配视角刷新 AgentLoop 前缀(会议机制)
975
1252
  *
976
- * @param input 本轮用户输入
977
- * @param options 触发选项(autoTriggered 默认 false,即手动触发)
978
- * @returns 写入/更新的 UserProfileEntry 列表
1253
+ * roundRole=null(日常态)→ 回落 activePack 前缀;roundRole 为任务项角色 → 该角色
1254
+ * persona/rules/skills 全量表层装配(buildSystemPrompt(name))。策略键恒为 activePack:
1255
+ * ChatOptions(temperature/outputLimit)由 activePack 策略构建,不随视角变(键不换防抖动)。
1256
+ * 由 seed prepare 每轮调用(`refreshRolePackPrefixForRound` 契约),保证前缀始终匹配本轮装配角色。
979
1257
  */
980
- async archiveProfileFacts(input, options) {
981
- this.assertInitialized('archiveProfileFacts');
982
- return this.requireArchiveCoordinator.archiveProfileFacts(input, options);
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());
983
1273
  }
984
1274
  /**
985
- * 手动触发 insight 提取(manual 模式下使用)
986
- *
987
- * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
988
- * 内部仍走 classify 判断(避免无价值输入浪费 LLM 调用)。
989
- *
990
- * FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
991
- * 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
992
- * 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
993
- *
994
- * 归档逻辑已委托给 ArchiveCoordinator
995
- *
996
- * @param input 本轮用户输入
997
- * @param assistantContent 本轮助手回复内容
998
- * @param options 触发选项(autoTriggered 默认 false,即手动触发)
999
- * @returns 写入/更新的 Memory 列表
1275
+ * 从当前激活的角色包策略构建 ChatOptions 覆盖项
1276
+ * 将 act.temperature / act.outputLimit 映射到 ChatOptions 字段;输出上限最终与 per-LLM 默认值
1277
+ * 取**更小值**(两者皆为「上限」,须同时满足)——收敛在 `buildChatOptions`,此处不裁决大小。
1000
1278
  */
1001
- async archiveInsight(input, assistantContent, options) {
1002
- this.assertInitialized('archiveInsight');
1003
- return this.requireArchiveCoordinator.archiveInsight(input, assistantContent, options);
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;
1004
1296
  }
1005
1297
  /**
1006
- * 手动归档会话内容(content 类记忆)
1007
- *
1008
- * 适用于 `insights-only` / `manual` 模式下用户手动触发会话内容归档。
1009
- * `full` 模式下由宿主在会话切换前自动调用,无需用户干预。
1010
- *
1011
- * FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
1012
- * 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
1013
- * 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
1014
- *
1015
- * 归档逻辑已委托给 ArchiveCoordinator
1016
- *
1017
- * @param date 会话日期 YYYY-MM-DD
1018
- * @param session 会话标识(不含日期前缀)
1019
- * @param options 触发选项(autoTriggered 默认 false,即手动触发)
1020
- * @returns 归档结果(memories 可能为空,表示无归档价值或 LLM 失败)
1298
+ * 角色包系统的唯一切换入口(自动匹配 + 手动切换共用):activate 更新激活态 → 发射 rolePackSwitched 事件
1299
+ * → 刷新 AgentLoop 前缀 → 同步工具白名单。
1300
+ * @returns 是否切换成功。false 有两种成因(**均不发射切换事件**):角色包不存在、
1301
+ * 切换锁已激活(30s 内超 5 次限流);调用方须以 getRolePackSwitchLockStatus() 区分,
1302
+ * 不可由「返回 false + 未收到 rolePackSwitchLocked」推断为"不存在"(被锁时该事件不发射,
1303
+ * 仅在**触发锁定**的那一次切换中发射,而那一次返回 true)。同名切换幂等返回 true。
1021
1304
  */
1022
- async archiveSessionContent(date, session, options) {
1023
- this.assertInitialized('archiveSessionContent');
1024
- return this.requireArchiveCoordinator.archiveSessionContent(date, session, options);
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;
1025
1323
  }
1026
1324
  /**
1027
- * 获取 pending 归档队列长度(MIND2-C3)
1325
+ * 手动归档会话(更新 SessionMeta,委托 ArchiveCoordinator)
1028
1326
  *
1029
- * 返回待重试的失败归档数。归档失败时输入入队,下次同阶段归档调用时自动重试。
1030
- * 供宿主 UI 展示"N 条待补归档"提示,或判断是否需要手动触发重试。
1327
+ * 适用于 manual 模式(用户手动触发;full 模式由宿主切换前自动调用)。
1328
+ * options 透传给 ArchiveCoordinator:宿主自动触发传 { autoTriggered: true }(按其模式判断),
1329
+ * 用户手动触发省略 options(默认 autoTriggered=false,无条件执行)。
1031
1330
  *
1032
- * @returns pending 队列长度(0 表示无待补)
1331
+ * @returns 归档结果(updatedFields 可能为空,表示无归档价值或 LLM 失败)
1033
1332
  */
1034
- getPendingArchiveCount() {
1035
- this.assertInitialized('getPendingArchiveCount');
1036
- return this.requireArchiveCoordinator.getPendingArchiveCount();
1333
+ async archiveSession(date, session, options) {
1334
+ this.assertInitialized('archiveSession');
1335
+ return this.requireArchiveCoordinator.archiveSession(date, session, options);
1037
1336
  }
1038
1337
  // ─── 配置重载(事件驱动) ───────────────────────
1039
1338
  /**
1040
- * 重载配置类记忆:从 configDir 重新扫描指定 source 的配置文件并更新内存缓存 + SQLite 索引
1041
- *
1042
- * 解决方案:installSkill 写入文件后或 confirmConfigSuggestion 写入配置文件后,
1043
- * 调用此方法使当前会话立即生效,无需重启 Agent。
1044
- *
1045
- * 支持的 source:
1046
- * - 'skill' → SkillManager.reload() 清空缓存重新扫描 skills/ 目录
1047
- * - 'persona' → PersonaManager.reload() 清空缓存重新扫描 personas/ 目录(保持激活角色)
1048
- * - 'rule' → 无操作(rule 类型由 ConfigManager CRUD 即时同步 bootstrap 段,详见 deleteRule/updateRule)
1049
- * - 'guardrail' → 抛错(guardrail 是 AgentLoop 的 readonly 数组,需 rebuildComponents 才能重载)
1050
- * - undefined → 重载 skill + persona(全量重载,不含 guardrail)
1051
- *
1052
- * @param source 配置类型,缺省时重载全部可热更新的配置
1053
- * @returns 重载结果统计
1339
+ * 重载配置类记忆:从 configDir 重新扫描指定 source 并更新内存缓存(设定记忆纯文件装载,不写记忆库),使当前会话立即生效(无需重启)。
1340
+ * - 'skill' → SkillManager.reload() 重扫 skills/
1341
+ * - 'rolePack' → RolePackManager.reload() 重扫 role-packs/(保持激活角色,刷新 AgentLoop 前缀)
1342
+ * - 'rule' → 经角色包管理机制热更新(即 rolePack)
1343
+ * - undefined → 全量重载 skill + rolePack
1054
1344
  */
1055
1345
  async reloadConfig(source) {
1056
1346
  this.assertInitialized('reloadConfig');
1057
- if (this.chatLockManager?.isBusy) {
1058
- // 对话进行中无法热重载:将 source 暂存,待 chat() finally 块释放锁后补执行
1059
- // source 为 undefined(全量重载)时不暂存——全量重载无具体来源,补执行语义不明
1347
+ if (this.internals.chatLockManager?.isBusy) {
1348
+ // 对话中无法热重载:暂存 source 待锁释放后补执行;undefined(全量)不暂存——无具体来源,补执行语义不明
1060
1349
  if (source) {
1061
1350
  this.pendingConfigReload.add(source);
1062
1351
  logger.info({ source }, '对话进行中,配置重载已暂存,将在对话结束后补执行');
1063
1352
  }
1064
1353
  throw chatBusyError('重载配置');
1065
1354
  }
1066
- // guardrail 需重建 AgentLoop,不属于热重载范畴
1067
- if (source === 'guardrail') {
1068
- throw configError('guardrail 不支持热重载', 'guardrail 规则是 AgentLoop 的 readonly 数组,需调用 rebuildComponents() 重建', ['使用 rebuildComponents() 重建组件(代价较高)']);
1069
- }
1070
- // rule 类型由 ConfigManager.deleteRule/updateRule CRUD 即时同步 bootstrap 段(refreshBootstrapMemories 回调),无需 reloadConfig
1355
+ // rule 是角色包的一部分,重载走 rolePack 路径
1071
1356
  if (source === 'rule') {
1072
- logger.info('rule 类型由 ConfigManager CRUD 即时同步 bootstrap 段,reloadConfig 跳过');
1073
- return { skill: 0, persona: 0 };
1357
+ logger.info('rule 类型重载:触发角色包重新扫描');
1358
+ return this.reloadConfig('rolePack');
1074
1359
  }
1075
- const result = { skill: 0, persona: 0 };
1360
+ const result = { skill: 0, rolePack: 0 };
1076
1361
  const errors = [];
1077
1362
  // 按需重载:source 缺省时全量重载,否则只重载指定类型
1078
1363
  const shouldReloadSkill = !source || source === 'skill';
1079
- const shouldReloadPersona = !source || source === 'persona';
1364
+ const shouldReloadRolePack = !source || source === 'rolePack';
1080
1365
  if (shouldReloadSkill && this.skillManager) {
1081
1366
  try {
1082
1367
  result.skill = await this.skillManager.reload();
@@ -1086,18 +1371,17 @@ export class Agent extends TypedEventEmitter {
1086
1371
  logger.warn({ err: toError(err) }, 'reloadConfig: skillManager.reload 失败');
1087
1372
  }
1088
1373
  }
1089
- if (shouldReloadPersona && this.personaManager) {
1374
+ if (shouldReloadRolePack && this._rolePackManager) {
1090
1375
  try {
1091
- result.persona = await this.personaManager.reload();
1092
- // 角色重载后,刷新 AgentLoop 的角色前缀(使新角色内容立即注入 system prompt)
1376
+ result.rolePack = await this._rolePackManager.reload();
1377
+ // 角色重载后刷新 AgentLoop 前缀
1093
1378
  if (this.loop) {
1094
- const newPrefix = this.personaManager.buildSystemPrompt();
1095
- this.loop.refreshPersonaPrefix(newPrefix);
1379
+ this.refreshRolePackPrefixOnLoop();
1096
1380
  }
1097
1381
  }
1098
1382
  catch (err) {
1099
1383
  errors.push(toError(err));
1100
- logger.warn({ err: toError(err) }, 'reloadConfig: personaManager.reload 失败');
1384
+ logger.warn({ err: toError(err) }, 'reloadConfig: rolePackManager.reload 失败');
1101
1385
  }
1102
1386
  }
1103
1387
  if (errors.length > 0) {
@@ -1110,22 +1394,19 @@ export class Agent extends TypedEventEmitter {
1110
1394
  }
1111
1395
  // ─── 组件访问 ─────────────────────────────────────────
1112
1396
  /**
1113
- * 重建内部组件(history / loop / managers)
1114
- *
1115
- * 通常不需要手动调用——switchProject() 已自动执行 rebuild。
1116
- * 仅在宿主项目需要强制刷新组件时使用(如热更新配置后)。
1397
+ * 重建内部组件(history / loop / managers):通常由 switchProject() 自动触发,仅强制刷新时手动调用
1117
1398
  */
1118
1399
  async rebuildComponents() {
1119
- // 对话进行中重建组件会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
1400
+ // 对话进行中重建会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
1120
1401
  this.assertNotBusy('重建组件');
1121
- await this.rebuildComponentsWithCurrentCtx();
1402
+ if (!this.pctx)
1403
+ return;
1404
+ // assembleComponents 内部重建 history/loop/managers,并返回新创建的 sessionManager
1405
+ await this.assembleComponents(this.pctx);
1122
1406
  }
1123
1407
  // ─── 守卫方法 ───────────────────────────────────────────
1124
1408
  /**
1125
- * 检查 Agent 是否已初始化
1126
- *
1127
- * @param methodName 调用方法名(用于错误消息)
1128
- * @param requires 需要检查的组件列表
1409
+ * 检查 Agent 是否已初始化(methodName 用于错误消息,requires 为需检查的组件列表)
1129
1410
  */
1130
1411
  assertInitialized(methodName, requires = ['history', 'loop']) {
1131
1412
  if (!this._initialized) {
@@ -1148,98 +1429,132 @@ export class Agent extends TypedEventEmitter {
1148
1429
  }
1149
1430
  }
1150
1431
  /**
1151
- * 断言对话未进行中 — 统一守卫,避免在 chat() 进行中执行会破坏状态一致性的操作
1152
- *
1153
- * 提取原因:switchProject / setProvider / setBackgroundProvider / setArchiveMode /
1154
- * switchPersona / rebuildComponents 等 6 处方法均有相同的 `if (isBusy) throw chatBusyError('XXX')` 守卫,
1155
- * 违反 DRY 原则(v2 神木回天 REPEAT-4 闭环)。
1156
- *
1157
- * 注意:reloadConfig 不使用本方法——它在 isBusy 时需暂存 source 而非直接抛错。
1158
- *
1159
- * @param operation 操作名称(用于错误消息,如 "切换项目")
1432
+ * 断言对话未进行中(统一守卫):破坏状态一致性的入口统一在此做 isBusy 检查。
1433
+ * reloadConfig 不使用本方法——它在 isBusy 时需暂存 source 而非直接抛错。
1160
1434
  */
1161
1435
  assertNotBusy(operation) {
1162
- if (this.chatLockManager?.isBusy) {
1436
+ if (this.internals.chatLockManager?.isBusy) {
1163
1437
  throw chatBusyError(operation);
1164
1438
  }
1165
1439
  }
1166
- /**
1167
- * 断言 getter:获取 loop,若 null 则抛出明确错误
1168
- * 替代 `this.loop!` 非空断言,提供更好的重构安全性
1169
- */
1440
+ // 断言 getter:非空或抛明确错误,替代 this.loop!/history!/pctx! 非空断言
1170
1441
  get requireLoop() {
1171
1442
  if (!this.loop)
1172
1443
  throw configError('AgentLoop 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1173
1444
  return this.loop;
1174
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
+ }
1175
1460
  /**
1176
- * 断言 getter:获取 history,若 null 则抛出明确错误
1461
+ * 获取角色包管理器(system prompt 的唯一注入源,返回 null 表示 Agent 未初始化)
1177
1462
  */
1178
- get requireHistory() {
1179
- if (!this.history)
1180
- throw configError('MessageHistory 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1181
- return this.history;
1463
+ get rolePackManager() {
1464
+ return this._rolePackManager;
1182
1465
  }
1183
1466
  /**
1184
- * 断言 getter:获取 pctx,若 null 则抛出明确错误
1467
+ * 获取当前激活的 L2 行为策略:从激活角色包读取,未激活时用全局默认值;供各阶段按策略调整行为
1185
1468
  */
1186
- get requirePctx() {
1187
- if (!this.pctx)
1188
- throw configError('ProjectContext 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1189
- return this.pctx;
1190
- }
1191
- get requireArchiveCoordinator() {
1192
- if (!this.archiveCoordinator)
1193
- throw configError('ArchiveCoordinator 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1194
- return this.archiveCoordinator;
1469
+ getActiveStrategy() {
1470
+ return this._rolePackManager?.getActive()?.strategy ?? DEFAULT_BEHAVIOR_STRATEGY;
1195
1471
  }
1196
- get requireSessionManager() {
1197
- if (!this._sessionManager)
1198
- throw configError('SessionManager 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
1199
- return this._sessionManager;
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);
1200
1484
  }
1201
1485
  // ─── 记忆生命周期 ───────────────────────────────────────
1202
- // ─── 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
+ }
1203
1509
  // ─── 关闭 ─────────────────────────────────────────────
1204
1510
  /**
1205
- * 关闭 Agent,释放 SQLite 连接等资源
1511
+ * 关闭 Agent,释放存储连接等资源
1206
1512
  */
1207
1513
  async close() {
1208
- // 清理 chat 锁管理器(递增 token 使进行中的 chat() generator 的 finally 块
1209
- // 检测到 token 变化后跳过资源清理,close 已接管清理职责)
1210
- this.chatLockManager?.dispose();
1211
- this.chatLockManager = null;
1212
- // 清理 MemoryDecayScheduler(含定时器和 storage 引用)
1213
- // FIX-P0-1:先 stop() abort L2 评估的 LLM 调用,再 awaitInflight() 等待 Promise 完成,
1214
- // 防止 close 后 LLM 回调 upsert 已关闭的 storage
1215
- if (this.memoryDecayScheduler) {
1216
- this.memoryDecayScheduler.stop();
1217
- try {
1218
- await this.memoryDecayScheduler.awaitInflight();
1219
- }
1220
- catch (err) {
1221
- logger.warn({ err: toError(err) }, 'close: memoryDecayScheduler.awaitInflight 失败');
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 = 初始在途数,超时放弃时非完成数)');
1222
1528
  }
1223
- this.memoryDecayScheduler = null;
1224
1529
  }
1225
- // FIX-P0-1:等待 WorkProjection 的 inflight LLM 生成完成,防止 close 后 upsert 已关闭的 storage
1226
- if (this.workProjection) {
1227
- try {
1228
- await this.workProjection.awaitInflight();
1229
- }
1230
- catch (err) {
1231
- logger.warn({ err: toError(err) }, 'close: workProjection.awaitInflight 失败');
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);
1232
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();
1233
1550
  }
1234
- // 清理 ArchiveCoordinator(无定时器,只需释放引用)
1235
- this.archiveCoordinator = null;
1236
- // 清理 PersonaManager 的角色切换防抖锁计时器,防止关闭后回调触发
1237
- if (this.personaManager) {
1238
- this.personaManager.close();
1551
+ // ArchiveCoordinator 无定时器,仅释放引用
1552
+ this.internals.archiveCoordinator = null;
1553
+ // 清理 RolePackManager 切换防抖锁计时器,防止关闭后回调触发
1554
+ if (this._rolePackManager) {
1555
+ this._rolePackManager.close();
1239
1556
  }
1240
- // 先等待后台归档完成,再移除事件监听器
1241
- // 顺序敏感:若先 removeAllListeners,fire-and-forget 的归档 Promise 在 await 期间
1242
- // reject 时 emit 会变成 no-op(listeners 已清空),archiveFailed 事件丢失
1557
+ // 顺序敏感:先等后台归档完成再移除监听器,否则 await 期间归档 reject 的 emit 变 no-op、事件丢失
1243
1558
  if (this.history) {
1244
1559
  try {
1245
1560
  await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
@@ -1248,7 +1563,6 @@ export class Agent extends TypedEventEmitter {
1248
1563
  logger.warn({ err: toError(err) }, 'close: awaitPendingArchives 失败');
1249
1564
  }
1250
1565
  }
1251
- // pending archives 完成后再移除监听器,确保归档失败的 emit 能送达
1252
1566
  this.removeAllListeners();
1253
1567
  if (this.projectManager) {
1254
1568
  try {
@@ -1265,36 +1579,35 @@ export class Agent extends TypedEventEmitter {
1265
1579
  this.pendingConfigReload.clear();
1266
1580
  }
1267
1581
  /**
1268
- * 统一 null 化所有组件字段(新增 Manager 时在此处追加一行)
1269
- *
1270
- * 仅处理"纯 null 化"字段,带副作用的清理(dispose/stop/close/shutdown)
1271
- * 仍由 close() 显式调用,顺序敏感不可合并。
1582
+ * 统一 null 化所有组件字段(新增 Manager 时在此追加一行);带副作用的清理仍由 close() 显式调用,顺序敏感不可合并
1272
1583
  */
1273
1584
  nullifyAllComponents() {
1274
- // Provider
1275
1585
  this.#backgroundProvider = null;
1276
- // 核心组件
1277
1586
  this.history = null;
1278
1587
  this.loop = null;
1279
1588
  this.toolExec = null;
1280
- // 专职 Manager
1281
- this.personaManager = null;
1282
- this.#userProfile = null;
1283
1589
  this.skillManager = null;
1284
- this.insightExtractor = null;
1285
- this.configManager = null;
1590
+ this._rolePackManager = null;
1286
1591
  this.memoryInspector = null;
1287
- this.dedupManager = null;
1288
- this.memoryAdvisor = null;
1289
1592
  this._governance = null;
1290
1593
  this.workProjection = null;
1291
- this.autoConfigRefiner = null;
1292
- this.sessionArchiver = null;
1293
1594
  this.textPolisher = null;
1294
1595
  this._sessionManager = null;
1295
- // 项目管理
1296
1596
  this.projectManager = null;
1297
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
+ };
1298
1611
  }
1299
1612
  // ─── 只读访问器 ───────────────────────────────────────
1300
1613
  get initialized() {
@@ -1304,14 +1617,13 @@ export class Agent extends TypedEventEmitter {
1304
1617
  return this.pctx;
1305
1618
  }
1306
1619
  /**
1307
- * @deprecated 直接暴露 AgentLoop 破坏封装,将在下一主版本移除。
1308
- * 使用 getMessages() / getMessageCount() 等有界接口替代。
1620
+ * 获取 AgentLoop 实例(宿主集成面,用于 restoreHistory 等底层操作;getMessages/getMessageCount 无法替代)
1309
1621
  */
1310
1622
  get agentLoop() {
1311
1623
  return this.loop;
1312
1624
  }
1313
1625
  /**
1314
- * 获取当前消息列表(只读副本,不可绕过 Agent 编排链路直接修改)
1626
+ * 获取当前消息列表(只读副本,不可绕过编排链路直接修改)
1315
1627
  */
1316
1628
  getMessages() {
1317
1629
  return this.loop?.getMessages() ?? [];
@@ -1321,19 +1633,15 @@ export class Agent extends TypedEventEmitter {
1321
1633
  return this.loop?.getMessages().length ?? 0;
1322
1634
  }
1323
1635
  /**
1324
- * @deprecated 直接暴露 MessageHistory 破坏封装,将在下一主版本移除。
1325
- * 使用 Agent 的对话管理方法(chat/chatSync/forkSession)替代直接操作。
1636
+ * 获取 MessageHistory 实例(宿主集成面,用于日期/会话判断等底层操作;
1637
+ * currentDateValue/currentSessionValue 等只读属性无替代接口)
1326
1638
  */
1327
1639
  get agentHistory() {
1328
1640
  return this.history;
1329
1641
  }
1330
1642
  /**
1331
- * 注入情感基调到 system prompt(Phase 2.1:AffectController)
1332
- *
1333
- * 委托至 AgentLoop.injectAffect(),在角色前缀和 bootstrap 记忆之间插入情感描述。
1334
- * 与角色切换独立——切换角色不会清除情感注入。
1335
- *
1336
- * @param affectString 情感描述文本,传空字符串清除注入
1643
+ * 注入情感基调到 system prompt:委托 AgentLoop.injectAffect(),在角色前缀与 bootstrap 记忆间插入情感描述;
1644
+ * 与角色切换独立(切换角色不清除情感注入),传空串清除。
1337
1645
  */
1338
1646
  injectAffect(affectString) {
1339
1647
  if (!this.loop)
@@ -1344,172 +1652,104 @@ export class Agent extends TypedEventEmitter {
1344
1652
  return this.#provider;
1345
1653
  }
1346
1654
  get isBusy() {
1347
- return this.chatLockManager?.isBusy ?? false;
1655
+ return this.internals.chatLockManager?.isBusy ?? false;
1348
1656
  }
1349
1657
  get lastInteractionAt() {
1350
1658
  return this._lastInteractionAt;
1351
1659
  }
1352
- /**
1353
- * 记忆治理统一门面(L0 衰减 / L1 去重 / L2 时效性 / L3 冲突 / 诊断 / 推荐)
1354
- *
1355
- * 统一入口替代散落在 Agent 上的 6 个独立方法:
1356
- * agent.governance.decay() → 原 runMemoryDecayOnce()
1357
- * agent.governance.deduplicate() → 原 deduplicateMemories()
1358
- * agent.governance.evaluateTimeliness() → 原 evaluateTimeliness()
1359
- * agent.governance.detectConflicts() → 原 detectConflicts()
1360
- * agent.governance.sourceHealth() → 原 sourceHealth()
1361
- * agent.governance.suggest() → 原 suggest()
1362
- */
1660
+ /** 记忆治理统一门面(语义去重 / 来源健康诊断 / 冲突检测 / 建议推荐),Agent 上的记忆管理方法统一收口于此(无显式衰减层面) */
1363
1661
  get governance() {
1364
1662
  return this._governance;
1365
1663
  }
1366
1664
  // ─── 运行时指标 ────────────────────────────────
1367
1665
  /**
1368
- * 获取 Agent 运行时指标快照(可观测性增强)
1369
- *
1370
- * 聚合 AgentLoop 指标(LLM 调用、记忆召回、工具调用、上下文管理)
1371
- * 与 Agent 层指标(记忆衰减),返回完整的 AgentMetrics 快照。
1372
- *
1373
- * 未初始化时返回全零指标(合理的默认值,不抛异常)。
1374
- * 纯只读、同步、零副作用——适合宿主项目定期轮询构建监控面板。
1375
- *
1376
- * 使用方式:
1377
- * const metrics = agent.getMetrics();
1378
- * console.log(`LLM 调用 ${metrics.llm.callCount} 次,命中率 ${metrics.recall.hitRate}`);
1379
- *
1380
- * @returns AgentMetrics 完整快照(含衰减指标)
1666
+ * 后台任务统计快照(宿主观测后台副作用健康度,纯只读转发 utils 单例)。
1667
+ * @returns 在途 / 成功 / 失败计数
1668
+ */
1669
+ getBackgroundTaskStats() {
1670
+ return readBackgroundTaskStats();
1671
+ }
1672
+ /**
1673
+ * 获取 Agent 运行时指标快照(可观测性):聚合 AgentLoop 指标与 Agent 层治理指标;
1674
+ * 未初始化返回全零默认值(不抛异常),纯只读同步零副作用,适合宿主定期轮询监控面板。
1381
1675
  */
1382
1676
  getMetrics() {
1383
- // 衰减指标从 MemoryDecayScheduler 读取
1384
- const decayMetrics = this.memoryDecayScheduler?.getMetrics() ?? {
1385
- runCount: 0,
1386
- totalDecayedCount: 0,
1387
- lastRunAt: null,
1388
- };
1389
- // 未初始化时返回全零指标,避免调用方需要判空
1677
+ // 未初始化时返回全零指标,避免调用方判空
1390
1678
  if (!this.loop) {
1391
1679
  return {
1392
- llm: { callCount: 0, totalInputTokens: 0, totalOutputTokens: 0 },
1393
- recall: { totalCount: 0, hitCount: 0, hitRate: 0 },
1394
- 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
+ },
1395
1697
  context: { truncationCount: 0, messageCount: 0, estimatedTokens: 0 },
1396
- 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
+ },
1397
1706
  };
1398
1707
  }
1399
- // 获取 AgentLoop 指标快照,填充衰减字段
1400
1708
  const loopMetrics = this.loop.getMetrics();
1401
1709
  return {
1402
1710
  ...loopMetrics,
1403
- decay: decayMetrics,
1404
1711
  };
1405
1712
  }
1406
- // ─── Manager 暴露(激进拆分:调用方直接操作 Manager)──
1407
- //
1408
- // 设计策略(ADR-010 §Manager 访问器):访问器返回 null,门面方法抛错。
1409
- // - 访问器(getter):返回 Manager | null,供宿主项目链式调用和优雅降级
1410
- // (如 `agent.persona?.activeName`、`if (!agent.memory) return []`)
1411
- // - 门面方法(snapshot/inspect/stats/searchMemories 等):通过
1412
- // assertInitialized 抛 MemoraError,提供明确错误信息
1413
- // 宿主项目使用访问器时需自行判空,或使用门面方法获得自动错误处理。
1414
- /**
1415
- * 角色管理器(可能为 null)
1416
- *
1417
- * 返回 null 时表示 Agent 未初始化或角色系统未加载。
1418
- * 链式调用建议使用可选链:`agent.persona?.activeName`
1419
- */
1420
- get persona() {
1421
- 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;
1422
1719
  }
1423
- /**
1424
- * 工具执行器(可能为 null)
1425
- *
1426
- * 返回 null 时表示 Agent 未初始化。
1427
- */
1720
+ /** 工具执行器(返回 null 表示 Agent 未初始化) */
1428
1721
  get tools() {
1429
1722
  return this.toolExec;
1430
1723
  }
1431
- /**
1432
- * 技能管理器(可能为 null)
1433
- *
1434
- * 返回 null 时表示 Agent 未初始化或技能系统未加载。
1435
- */
1724
+ /** 技能管理器(返回 null 表示未初始化或技能系统未加载) */
1436
1725
  get skills() {
1437
1726
  return this.skillManager;
1438
1727
  }
1439
- /**
1440
- * 配置管理器(可能为 null)—— 规则/技能注入 + 配置建议
1441
- *
1442
- * 返回 null 时表示 Agent 未初始化。
1443
- */
1444
- get config() {
1445
- return this.configManager;
1446
- }
1447
- /**
1448
- * Insight 提取器(可能为 null)—— 输入分类 + 记忆提取
1449
- *
1450
- * 返回 null 时表示 Agent 未初始化或 LLM Provider 未配置。
1451
- */
1452
- get insight() {
1453
- return this.insightExtractor;
1728
+ /** 角色包管理器(返回 null 表示未初始化或角色包系统未加载) */
1729
+ get rolePacks() {
1730
+ return this._rolePackManager;
1454
1731
  }
1455
- /**
1456
- * 记忆查看器(可能为 null)—— 快照 + 搜索 + 统计
1457
- *
1458
- * 返回 null 时表示 Agent 未初始化或存储层未就绪。
1459
- * 宿主项目常用模式:`const mem = agent.memory; if (!mem) return [];`
1460
- */
1732
+ /** 记忆查看器(快照 + 搜索 + 统计;宿主常用 `const mem = agent.memory; if (!mem) return []`) */
1461
1733
  get memory() {
1462
1734
  return this.memoryInspector;
1463
1735
  }
1464
- /**
1465
- * 项目管理器(可能为 null)
1466
- *
1467
- * 返回 null 时表示 Agent 未初始化。
1468
- */
1736
+ /** 项目管理器(返回 null 表示 Agent 未初始化) */
1469
1737
  get projects() {
1470
1738
  return this.projectManager;
1471
1739
  }
1472
- /**
1473
- * 用户画像管理器(可能为 null)—— 实时归档 + 确认/拒绝
1474
- *
1475
- * 返回 null 时表示 Agent 未初始化。
1476
- * 宿主常用模式:
1477
- * - `agent.userProfile?.getPending()` 查询待确认条目
1478
- * - `await agent.userProfile?.confirm(id)` 确认条目
1479
- * - `await agent.userProfile?.reject(id)` 拒绝条目
1480
- */
1481
- get userProfile() {
1482
- return this.#userProfile;
1483
- }
1484
- /**
1485
- * 安全守卫(可能为 null)—— 写入确认回调注册
1486
- *
1487
- * 返回 null 时表示 Agent 未初始化或 projectPath 未设置。
1488
- * 宿主常用模式:
1489
- * - `agent.security?.onWriteConfirmation(callback)` 注册写入确认回调
1490
- * - 回调返回 true 允许写入,返回 false 拒绝写入
1491
- *
1492
- * 详见 M1 写入确认 UI 闭环
1493
- */
1740
+ /** 安全守卫(写入确认回调注册:`onWriteConfirmation` 返回 true 放行、false 拒绝;未初始化/projectPath 未设为 null) */
1494
1741
  get security() {
1495
1742
  return this.pctx?.security ?? null;
1496
1743
  }
1497
- /**
1498
- * 作品投影管理器(可能为 null)—— 文件内容 → 概要+结构+决策
1499
- *
1500
- * 返回 null 时表示 Agent 未初始化。
1501
- * 宿主常用模式:
1502
- * - `agent.works?.ensureProjection(filePath, content)` 读取文件后触发生成
1503
- * - `agent.works?.loadAll()` 获取所有投影
1504
- */
1744
+ /** 作品投影管理器(用户主动登记的 JSON 极简索引;`registerWork`/`listWorks`/`loadAndGetContextBlock`/`isFileMalformed`) */
1505
1745
  get works() {
1506
1746
  return this.workProjection;
1507
1747
  }
1508
- /** 会话管理器(宿主可通过此 getter 访问会话恢复/切换/分叉功能) */
1748
+ /** 会话管理器(会话恢复/切换/分叉功能) */
1509
1749
  get sessionManager() {
1510
1750
  return this._sessionManager;
1511
1751
  }
1512
- /** 文本润色管理器(宿主可通过此 getter 调用 polish 方法) */
1752
+ /** 文本润色管理器(polish 方法) */
1513
1753
  get polish() {
1514
1754
  return this.textPolisher;
1515
1755
  }