@zooique/memora 2.0.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (588) hide show
  1. package/CHANGELOG.md +1028 -0
  2. package/README.en.md +305 -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
@@ -7,37 +7,114 @@
7
7
  * 3. 文件写入内容计算(computeWriteContent)
8
8
  * 4. 目录递归遍历(walkDir + shouldIgnore)
9
9
  *
10
- * 设计理由:ToolExecutor 802 行超阈值,内置工具实现(~440 行)
10
+ * 设计理由:ToolExecutor 作为编排层(注册/分发/校验)体量偏大,内置工具实现(~440 行)
11
11
  * 是独立职责——实际文件系统/记忆索引操作,与 ToolExecutor 的注册/分发/校验职责分离。
12
12
  *
13
13
  * 自然生长原则:BuiltinToolHandlers 不持有 customTools 注册表(避免与 ToolExecutor 状态耦合),
14
14
  * 所有方法接收参数,是无状态的纯计算 + I/O 操作。
15
15
  */
16
- import { readFile, writeFile, mkdir, readdir, stat, access } from 'node:fs/promises';
16
+ import { readFile, writeFile, mkdir, readdir, stat, access, unlink } from 'node:fs/promises';
17
17
  import { constants } from 'node:fs';
18
18
  import { resolve, isAbsolute, join, relative, dirname, basename } from 'node:path';
19
- import { toolError, MemoraError, ToolErrorCode, toError } from '../utils/errors.js';
19
+ import { toolError, MemoraError, ToolErrorCode } from '../utils/errors.js';
20
+ import { toError, isNodeErrorCode } from '../utils/toError.js';
20
21
  import { logger } from '../logging/logger.js';
21
22
  import { segmentLower } from '../utils/segmenter.js';
22
23
  import { truncate } from '../utils/strings.js';
23
- import { parseFrontmatter } from '../utils/frontmatter.js';
24
+ import { SOURCE_LABELS, roundSummarySessionPrefix } from '../memory/types.js';
25
+ // 会话显示名回退单一真理源(displayName→autoName),sessionId 兜底留在调用端
26
+ import { getSessionDisplayName } from '../memory/sessionStore.js';
27
+ import { splitSessionId } from '../utils/time.js';
28
+ // read_file 分段脚注格式单一真理源(生成侧在此用作「截断诚实化」文案)
29
+ import { formatSegmentationFooter } from '../agent/toolLedger.js';
30
+ // write_file 写入模式单一真理源(schema 描述与 handler 校验共用,避免双源漂移)
31
+ import { WRITE_FILE_MODES, isWriteFileMode } from '../agent/builtinTools.js';
32
+ import { sanitizeExternalText, stripControlChars } from '../agent/toolExecutor.js';
33
+ import { backgroundTask } from '../utils/backgroundTask.js';
34
+ import { touchScores } from '../memory/keywordsTouch.js';
35
+ import { AGENT_CONSTANTS, LOOP_CONSTANTS } from '../agent/constants.js';
36
+ // token 估算唯一真理源(CJK 感知)——read_file 的分段预算与上下文占用/截断同口径
37
+ import { estimateTokensText } from '../agent/contextManager.js';
38
+ // 正整数解析唯一真理源(read_file 分段 offset/limit 与 toolResultCache 去重主体共用)
39
+ import { positiveInt, parseLimit } from '../utils/math.js';
40
+ /** trace_summary 溯源原始对话的最大消息数(规模控制) */
41
+ const TRACE_MESSAGE_LIMIT = 5;
42
+ /** trace_summary 单条消息的最大字符数(防长上下文注入) */
43
+ const TRACE_MESSAGE_CHAR_LIMIT = 2000;
44
+ /** list_sessions 默认返回条数(会话路标,token 可控优先) */
45
+ const LIST_SESSIONS_DEFAULT = 10;
46
+ /** list_sessions 最大返回条数(防历史会话过多撑爆上下文) */
47
+ const LIST_SESSIONS_MAX = 30;
48
+ /** list_sessions 单条路标摘要的最大字符数(LLM 生成,过 sanitize 防注入) */
49
+ const LIST_SESSIONS_SUMMARY_CHARS = 200;
50
+ /** run_team_meeting 返回的评审文本最大字符数(多角色 persona 拼入 + 产出长文,防超长注入撑爆上下文) */
51
+ const MEETING_RESULT_MAX_LEN = 20_000;
24
52
  /**
25
- * 检测写入内容是否为配置文件(persona/skill/rule)
53
+ * 目录树返回的最大字符数(防超大目录整段进上下文)
26
54
  *
27
- * 解析 frontmatter 中的 source 字段,若匹配 persona/skill/rule 则返回对应的工具名后缀。
28
- * 这是内容特征检测——路径拦截易被绕过,但 frontmatter 中的 source 字段
29
- * 是 FileStore.write 写入的结构特征,LLM 无法绕过。
55
+ * read_file 走 **token 预算分段**(`LOOP_CONSTANTS.SINGLE_TOOL_RESULT_MAX_TOKENS`),
56
+ * 不使用字符上限;本常量专职 list_dir——名字须与它实际约束的东西一致。
57
+ */
58
+ const DIR_LIST_MAX_LEN = 50_000;
59
+ /**
60
+ * 按 token 预算反推字符数截断单行(仅用于「单行本身即超预算」的病态退化分支)
30
61
  *
31
- * @param content 写入内容(可能含 frontmatter)
32
- * @returns 匹配的 source 标签('persona' | 'skill' | 'rule'),未匹配返回 null
62
+ * 先按其他字符比例(`CHARS_PER_TOKEN`)取字符上限,再按实测比值迭代收敛——
63
+ * 不用固定比例是因为中文密度近英文 2 倍(CJK 1.5 vs 其他 3),固定比例必有一侧失准。
64
+ * 每次迭代都严格缩小 chars(tokens > budget ⇒ 新 chars < 旧 chars),故必收敛。
33
65
  */
34
- function checkForConfigSource(content) {
35
- const { frontmatter } = parseFrontmatter(content);
36
- const source = frontmatter.source;
37
- if (source && BuiltinToolHandlers.CONFIG_SOURCES.has(source)) {
38
- return source;
66
+ function clampCharsToTokenBudget(line, budget) {
67
+ let chars = Math.max(1, Math.floor(budget * LOOP_CONSTANTS.CHARS_PER_TOKEN));
68
+ let slice = line.slice(0, chars);
69
+ let tokens = estimateTokensText(slice);
70
+ while (tokens > budget && chars > 1) {
71
+ chars = Math.max(1, Math.floor((chars * budget) / tokens));
72
+ slice = line.slice(0, chars);
73
+ tokens = estimateTokensText(slice);
39
74
  }
40
- return null;
75
+ return slice;
76
+ }
77
+ /**
78
+ * 脚本文件读取最大长度(run_code script_path 模式)
79
+ *
80
+ * 脚本代码要原样交给执行器,截断会破坏语法——故超限**直接报错**(不静默截断)。
81
+ * 与 read_file 不同:read_file 走 token 预算分段
82
+ * (LOOP_CONSTANTS.SINGLE_TOOL_RESULT_MAX_TOKENS),不使用字符上限,
83
+ * 故本常量与 read_file 不在同一量纲,无「谁更宽松」的可比关系。
84
+ *
85
+ * 与 utils/fileSafe.DEFAULT_MAX_CONTENT_LEN 同为 200_000 属**异义同值**,勿误合并:
86
+ * - 本常量:脚本原样读取上限(超限报错,内容交执行器);
87
+ * - DEFAULT_MAX_CONTENT_LEN:内容文件读取上限(超限截断,内容入上下文)。
88
+ * 两者各自独立演进,改一侧不联动另一侧。
89
+ */
90
+ const SCRIPT_READ_MAX_LEN = 200_000;
91
+ /**
92
+ * 内置文件工具忽略的目录/文件名(模块级单一真理源)
93
+ *
94
+ * list_dir 忽略规则与宿主项目内容搜索(search_project content 模式)共享;
95
+ * 宿主通过主入口 import 此常量对齐忽略目录,避免数值/规则漂移。
96
+ */
97
+ export const IGNORED_DIR_NAMES = [
98
+ '.git',
99
+ 'node_modules',
100
+ '.memora',
101
+ 'dist',
102
+ 'coverage',
103
+ '.next',
104
+ ];
105
+ /**
106
+ * 任务表保留文件名判定(write_file 守卫唯一消费)
107
+ *
108
+ * 任务表是内核工具数据(task_table_write/update 管理),不落盘为 markdown 文件——`task-table.md` /
109
+ * `任务表.md` 等文件名保留给任务表机制,禁止 write_file 写入。命中规则 = 文件名(去扩展名)即
110
+ * `task-table` / `task_table` / `任务表`,或以其带分隔符/后缀开头(如 `task-table-进度.md`)。
111
+ *
112
+ * @param absolutePath 目标文件绝对路径
113
+ * @returns 是否命中保留名(任务表语义文件)
114
+ */
115
+ export function isReservedTaskTableFile(absolutePath) {
116
+ const base = basename(absolutePath).replace(/\.(md|markdown|txt)$/i, '');
117
+ return /^task[-_ ]?table(?:[-_ ].+)?$/i.test(base) || /^任务表(?:[-_ ].+)?$/.test(base);
41
118
  }
42
119
  /**
43
120
  * 内置工具处理器
@@ -49,60 +126,53 @@ export class BuiltinToolHandlers {
49
126
  projectPath;
50
127
  security;
51
128
  memoryIndex;
52
- workProjection;
53
- configDir;
54
- /** list_dir 默认忽略的目录/文件名 */
55
- static IGNORED_DIR_NAMES = [
56
- '.git',
57
- 'node_modules',
58
- '.memora',
59
- 'dist',
60
- 'coverage',
61
- '.next',
62
- ];
63
- /**
64
- * 配置文件目录名集合——这些目录的内容应通过专用工具创建
65
- *
66
- * personas/skills/rules 是 SOURCE_LABELS 的映射目录(store.ts SOURCE_TO_DIR),
67
- * 通过 create_persona/create_skill/create_rule 工具创建会正确写入 configDir 并触发热重载。
68
- * write_file 写入 projectPath/personas/ 只会创建"孤儿文件"——不在配置目录中,不会被加载。
69
- */
70
- /** 配置目录名(路径拦截用,第一级目录匹配) */
71
- static CONFIG_DIRS = new Set(['personas', 'skills', 'rules']);
129
+ sessionStore;
130
+ /** list_dir 默认忽略的目录/文件名(引用模块级单一真理源,保持类静态 API 兼容) */
131
+ static IGNORED_DIR_NAMES = IGNORED_DIR_NAMES;
72
132
  /**
73
- * 配置文件 source 标签(内容特征检测用,frontmatter 中 source 字段值)
74
- *
75
- * 路径拦截易被绕过(去掉 personas/ 前缀即可),但配置文件 frontmatter 中必然包含
76
- * source: persona/skill/rule —— 这是 FileStore.write 写入的结构特征,LLM 无法绕过。
77
- */
78
- static CONFIG_SOURCES = new Set(['persona', 'skill', 'rule']);
79
- /**
80
- * @param projectPath 项目根路径(用于相对路径解析)
133
+ * @param projectPath 项目根路径(用于相对路径解析;公开只读,供 run_code script_path 模式取 cwd)
81
134
  * @param security 安全守卫(路径白名单 + 写入确认)
82
135
  * @param memoryIndex 记忆索引(用于 search_memories 工具)
83
- * @param workProjection 作品投影管理器(可选,读取文件时自动生成投影)
84
- * @param configDir 配置目录路径(可选,拦截提示中告知 LLM 正确的写入位置)
136
+ * @param sessionStore 会话存储(可选,trace_summary 溯源原始对话用;未注入时回退为摘要文本)
85
137
  */
86
- constructor(projectPath, security, memoryIndex, workProjection, configDir) {
138
+ constructor(projectPath, security, memoryIndex, sessionStore) {
87
139
  this.projectPath = projectPath;
88
140
  this.security = security;
89
141
  this.memoryIndex = memoryIndex;
90
- this.workProjection = workProjection;
91
- this.configDir = configDir;
142
+ this.sessionStore = sessionStore;
92
143
  }
144
+ /** 记忆搜索器(search_memories 语义后端,装配期注入;未注入时回退关键词 memoryIndex.search) */
145
+ memoryInspector = null;
146
+ /** 当前会话「正文或摘要已在眼前」的轮次提供者(装配期注入;search_memories 用它与装配期内容互斥) */
147
+ exclusionRoundIdsProvider = null;
148
+ /** memoryRecalled 事件发射回调(宿主感知「LLM 查询记忆命中 N 条」;装配期注入,缺省不发射) */
149
+ onMemoryRecalled = null;
93
150
  /**
94
- * 生成配置目录路径提示
95
- *
96
- * 当 configDir 已知时,告知 LLM 正确写入位置(如 <configDir>/personas/);
97
- * 未知时回退到通用描述。用于拦截错误消息中,引导 LLM 使用专用工具而非 write_file。
151
+ * 注入 MemoryInspector,启用 search_memories 的纯关键词搜索后端(memory-tool-recall-design §3.3)。
152
+ * 装配依赖 loop/history,toolExec 在 assembler 中先于它构造,故用「构造后注入」而非构造参数。
153
+ * 注入后 search_memories 走 searchByKeyword(纯关键词 + superseded 过滤 + 溯源揭示),未注入保持旧关键词行为。
98
154
  */
99
- configPathHint(toolSuffix) {
100
- if (this.configDir) {
101
- // 计算目标子目录:persona → personas, skill → skills, rule → rules
102
- const subDir = `${toolSuffix}s`;
103
- return `${this.configDir}/${subDir}/`;
104
- }
105
- return '配置目录';
155
+ setMemoryInspector(inspector) {
156
+ this.memoryInspector = inspector;
157
+ }
158
+ /**
159
+ * 注入 memoryRecalled 事件发射回调(宿主消费,§2.4「保留改语义」定案)。
160
+ * 搜索工具命中记忆时触发,文案语义 =「LLM 查询记忆命中 N 条」。
161
+ * **唯一触发位**:全库仅本类 emit memoryRecalled(无其他发射点)。
162
+ * 缺省不注入则静默跳过(内置工具也可被测试/脚本直调,无宿主时 no-op)。
163
+ */
164
+ setOnMemoryRecalled(callback) {
165
+ this.onMemoryRecalled = callback;
166
+ }
167
+ /**
168
+ * 注入召回互斥排除集提供者(memory-tool-recall-design §5.1 工具互斥)。
169
+ * search_memories 用它排除「正文或摘要已在眼前」轮次的 round-summary,避免与装配期内容重复。
170
+ * 提供者返回精确集合(`loop.getExclusionRoundIds()`:视图内 ∪ 被替换 ∪ 在途),
171
+ * 语义为「该轮内容已在上下文,其摘要不应再被召回带回」——从视图派生,与模型窗口无关。
172
+ * 缺省不注入则不过滤(保持测试/独立调用可直接触发)。
173
+ */
174
+ setExclusionRoundIdsProvider(provider) {
175
+ this.exclusionRoundIdsProvider = provider;
106
176
  }
107
177
  // ─── 路径安全(内置 + 自定义工具共享) ──────────────────────────
108
178
  /**
@@ -127,96 +197,304 @@ export class BuiltinToolHandlers {
127
197
  const e = toError(err);
128
198
  throw toolError('路径不在白名单内', e.message, [
129
199
  '确认路径在白名单内(项目目录/数据目录/显式 allowedPaths)',
130
- '查看审计日志:~/.memora/logs/memora.log',
200
+ '查看审计日志:路径拒绝事件经内核 logger 输出,见宿主日志通道(IDE 输出面板 / 终端 stderr)',
131
201
  ], e, ToolErrorCode.PATH_NOT_ALLOWED);
132
202
  }
133
203
  }
204
+ /**
205
+ * 读前预检:目标是目录时抛语义化工具错误(read_file 与 run_code script_path 共用)
206
+ *
207
+ * 两处曾各自实现同一段「stat + isDirectory 判定 + MemoraError 移交」;文案因工具而异,
208
+ * 故工具名与提示参数化。stat 失败(ENOENT/权限)不在此处理——移交调用方统一异常路径。
209
+ *
210
+ * @param absolutePath 目标绝对路径(已过白名单校验)
211
+ * @param displayPath 用于文案的相对路径(可读性)
212
+ * @param toolLabel 工具显示名(如 'read_file' / 'run_code script_path')
213
+ * @param hint 目录误用时的行动指引
214
+ */
215
+ async assertFileNotDir(absolutePath, displayPath, toolLabel, hint) {
216
+ try {
217
+ const stats = await stat(absolutePath);
218
+ if (stats.isDirectory()) {
219
+ throw toolError(`${toolLabel} 目标是目录`, `${displayPath}:这是目录,不是文件`, [hint], undefined, ToolErrorCode.ARGUMENT_ERROR);
220
+ }
221
+ }
222
+ catch (err) {
223
+ // stat 失败(如 ENOENT/权限)移交下方异常路径统一报错;工具错误直接抛出
224
+ if (err instanceof MemoraError)
225
+ throw err;
226
+ }
227
+ }
134
228
  // ─── 内置工具实现 ──────────────────────────────────────
135
229
  /**
136
- * 读取文件(带路径白名单校验)
230
+ * 读取文件(带路径白名单校验;支持按行分段)
231
+ *
232
+ * **分段语义(大文本统一通道)**
233
+ * - `offset`:起始行号(1-based,默认 1);`limit`:最多返回行数(省略 = 尽可能多)。
234
+ * - **同源不变量**:返回内容的 token 数**恒 ≤ `LOOP_CONSTANTS.SINGLE_TOOL_RESULT_MAX_TOKENS`**。
235
+ * 这保证 read_file 的结果永不触发入口关落盘(阈值同键)→「路径即引用」不会自我嵌套
236
+ * (详见该常量注释)。故本方法**不**使用字符数上限,而是按 token 预算逐行填充。
237
+ * - **截断诚实化**:一旦未能读到文件末尾,脚注给出「已显示第 X–Y 行(共 M 行)」与续读
238
+ * offset;静默追加 `…` 会让 LLM 既不知道后面还有内容、也没有续读手段(静默截断 = 假阴性)。
137
239
  */
138
- async readFile(relativePath) {
240
+ async readFile(relativePath, offset, limit) {
139
241
  if (!relativePath) {
140
242
  throw toolError('read_file 工具调用缺少 path 参数', 'LLM 未传 path', ['检查 personality.md 是否明确了 read_file 用法', '检查 LLM 输出'], undefined, ToolErrorCode.ARGUMENT_ERROR);
141
243
  }
142
244
  const absolutePath = this.resolveSafePath(relativePath);
143
245
  this.guardPathOrThrow(absolutePath, 'read_file');
246
+ // 读前预检:目标是目录时给出可执行指引(read_file 语义是读文件;
247
+ // EISDIR 原生错误对 LLM 无意义,直接提示改用 list_dir)
248
+ await this.assertFileNotDir(absolutePath, relativePath, 'read_file', '改用 list_dir 列出该目录下的内容');
144
249
  try {
145
250
  const content = await readFile(absolutePath, 'utf-8');
146
- // 读取文件时自动触发生成/更新作品投影(fire-and-forget,不阻塞读取)
147
- if (this.workProjection) {
148
- this.workProjection.ensureProjection(absolutePath, content, relativePath).catch((err) => {
149
- logger.warn({ err, path: absolutePath }, '作品投影生成失败');
150
- });
151
- }
152
- return content;
251
+ // 返回净化(去控制字符/ANSI,**不做字符数截断**——按行分段的 token 预算在下方收口)
252
+ return this.sliceFileByLineBudget(stripControlChars(content), relativePath, offset, limit);
153
253
  }
154
254
  catch (err) {
155
255
  const e = toError(err);
156
256
  // 区分 ENOENT(文件不存在)和其他 IO 错误
157
257
  if (e instanceof MemoraError)
158
258
  throw e;
159
- if (err !== null &&
160
- typeof err === 'object' &&
161
- 'code' in err &&
162
- err.code === 'ENOENT') {
163
- throw toolError('read_file 文件不存在', `${absolutePath}:文件不存在`, ['确认路径正确', '使用 list_dir 查看目录结构'], e, ToolErrorCode.FILE_NOT_FOUND);
259
+ if (isNodeErrorCode(err, 'ENOENT')) {
260
+ // 前置兄弟目录提示:siblingDirHint 是"失败即给证据"——但内核事件
261
+ // 落盘 summary 只取前 100 字符(loop.ts tool_result)。若把它拼接在长路径**之后**,
262
+ // 危害:路径串本身(盘根+文件名)往往已占满 100 字 → 兄弟目录清单被整个截掉,
263
+ // LLM 重放/治理只见「文件不存在」却看不到同级真实文件名(HALL-1 实测 seq261/262
264
+ // 的 summary 停在 `[同` 即此)。故**前置**清单:保证 summary 前 100 字先到达证据,
265
+ // 而非路径。路径本身对"自查文件名"零贡献,放后面不损失。
266
+ const hint = await this.siblingDirHint(absolutePath);
267
+ const evidenceFirst = hint
268
+ ? `${hint}\n${absolutePath}:文件不存在`
269
+ : `${absolutePath}:文件不存在`;
270
+ throw toolError('read_file 文件不存在', evidenceFirst, ['确认路径正确', '使用 list_dir 查看目录结构'], e, ToolErrorCode.FILE_NOT_FOUND);
164
271
  }
165
272
  throw toolError('read_file 读取失败', `${absolutePath}:${e.message}`, ['确认文件权限', '确认路径正确'], e, ToolErrorCode.UNKNOWN);
166
273
  }
167
274
  }
168
275
  /**
169
- * 写入文件(支持 overwrite / append / insert 三种模式)
276
+ * 失败即给证据:read_file 目标不存在(ENOENT)时,返回其**同级目录实际条目**的紧凑清单,
277
+ * 让模型自查真实文件名(优于单纯拒绝;配合失败硬闸在首次失败就给路,比硬拦更止妖)。
278
+ * 父目录不可读 / 无可见条目 → 返回空串(不掩盖原始错误)。条目经 shouldIgnore 过滤,join 拼接。
279
+ */
280
+ async siblingDirHint(filePath) {
281
+ try {
282
+ const parent = dirname(filePath);
283
+ const names = await readdir(parent);
284
+ const visible = names.filter((n) => !this.shouldIgnore(n)).sort();
285
+ if (visible.length === 0)
286
+ return '';
287
+ const sample = visible.slice(0, 20);
288
+ const tail = visible.length > sample.length ? `…(共 ${visible.length} 项)` : '';
289
+ return `\n[同级目录内容,供比对] ${sample.join('、')}${tail}`;
290
+ }
291
+ catch {
292
+ // 父目录不可读/权限问题时放弃证据(不把次要失败盖过原始 ENOENT)
293
+ return '';
294
+ }
295
+ }
296
+ /**
297
+ * 按行分段 + token 预算切片(read_file 返回形态的**单一收口**)
298
+ *
299
+ * 算法:
300
+ * 1. 解析 `offset`(1-based 起始行)/ `limit`(行数上限);offset 超出末尾 → 如实告知;
301
+ * 2. 期望区间 = offset .. min(offset+limit−1, 总行数);
302
+ * 3. 若**不需要**截断(读到末尾且预算够)→ 返回原文,**不追加脚注**(零噪音);
303
+ * 4. 否则先为脚注预留 token,再**二分**出最大可容纳行数(token 随行数单调增 → 二分成立);
304
+ * 5. 脚注如实告知「已显示第 X–Y 行(共 M 行)」与续读 `offset=Y+1`。
305
+ *
306
+ * 退化情形(病态长行:单行本身就超预算)→ 该行按估算比反推字符数截断,并明示已截断。
307
+ *
308
+ * **不变量**:任何返回路径的 token 数都 ≤ `SINGLE_TOOL_RESULT_MAX_TOKENS −
309
+ * TOOL_RESULT_WRAP_OVERHEAD_TOKENS`(**含脚注**)—— 即 wrapped 入上下文后仍 ≤ 单条上限,
310
+ * 从而**结构性不触发**入口关落盘(否则 LLM 每次读文件只看得到路径)。脚注预留用最坏位数
311
+ * (endLine = 总行数)估算,故实际脚注必不超预留。
312
+ *
313
+ * @param content 已净化的文件全文
314
+ * @param displayPath 用于文案的相对路径(越界/退化提示可读)
315
+ * @param offset LLM 传入的起始行号字符串(可选)
316
+ * @param limit LLM 传入的行数上限字符串(可选)
317
+ */
318
+ sliceFileByLineBudget(content, displayPath, offset, limit) {
319
+ // 预算扣除包裹开销:使 wrapped 后仍 ≤ 单条上限(见 TOOL_RESULT_WRAP_OVERHEAD_TOKENS 注释)
320
+ const budget = LOOP_CONSTANTS.SINGLE_TOOL_RESULT_MAX_TOKENS -
321
+ LOOP_CONSTANTS.TOOL_RESULT_WRAP_OVERHEAD_TOKENS;
322
+ const lines = content.split('\n');
323
+ const total = lines.length;
324
+ const start = positiveInt(offset) ?? 1;
325
+ if (start > total) {
326
+ return `[read_file] ${displayPath} 共 ${total} 行;offset=${start} 已超出文件末尾,无可显示内容。`;
327
+ }
328
+ const limitNum = positiveInt(limit);
329
+ const wantedEnd = limitNum === undefined ? total : Math.min(start + limitNum - 1, total);
330
+ // 是否需要截断(= 是否需要脚注):用户 limit 截短了,或整个尾段本身超预算
331
+ const tailTokens = estimateTokensText(lines.slice(start - 1).join('\n'));
332
+ const needFooter = wantedEnd < total || tailTokens > budget;
333
+ if (!needFooter) {
334
+ return lines.slice(start - 1).join('\n');
335
+ }
336
+ // 脚注预留:按最坏位数(endLine 取总行数)估算,实际脚注必不超此预留
337
+ const reserve = estimateTokensText(`\n${formatSegmentationFooter(start, total, total)}`);
338
+ const bodyBudget = budget - reserve;
339
+ // 二分最大可容纳行数 k ∈ [0, wantedEnd − start + 1]
340
+ const maxK = wantedEnd - start + 1;
341
+ let lo = 0;
342
+ let hi = maxK;
343
+ while (lo < hi) {
344
+ const mid = Math.ceil((lo + hi) / 2);
345
+ const tokens = estimateTokensText(lines.slice(start - 1, start - 1 + mid).join('\n'));
346
+ if (tokens <= bodyBudget)
347
+ lo = mid;
348
+ else
349
+ hi = mid - 1;
350
+ }
351
+ const endLine = start + lo - 1;
352
+ if (lo === 0) {
353
+ // 退化:单行即超预算 → 按估算比反推字符数,明示已截断
354
+ const marker = '[该行超过单次读取预算,已截断] ';
355
+ const fillBudget = Math.max(1, bodyBudget - estimateTokensText(marker));
356
+ return (`${marker}${clampCharsToTokenBudget(lines[start - 1], fillBudget)}\n` +
357
+ formatSegmentationFooter(start, start, total));
358
+ }
359
+ // 二分下界保证 body 不超 bodyBudget;脚注按 ceil 可加性不超 reserve
360
+ return `${lines.slice(start - 1, endLine).join('\n')}\n${formatSegmentationFooter(start, endLine, total)}`;
361
+ }
362
+ /**
363
+ * 读取项目脚本文件原始内容(供 run_code script_path 模式执行)
364
+ *
365
+ * 与 readFile 的区别:不截断不净化——脚本代码要原样交给执行器,截断会破坏语法。
366
+ * 仅做路径白名单校验 + 长度上限(SCRIPT_READ_MAX_LEN,防超大脚本滥用)。
367
+ *
368
+ * @param relativePath 相对项目根的文件路径
369
+ * @returns 脚本文件原始内容
370
+ */
371
+ async readScriptFile(relativePath) {
372
+ if (!relativePath) {
373
+ throw toolError('run_code script_path 参数缺失', 'LLM 未传 script_path', ['script_path 不能为空'], undefined, ToolErrorCode.ARGUMENT_ERROR);
374
+ }
375
+ const absolutePath = this.resolveSafePath(relativePath);
376
+ this.guardPathOrThrow(absolutePath, 'run_code');
377
+ // 读前预检:目标是目录时给出可执行指引(脚本语义是文件)
378
+ await this.assertFileNotDir(absolutePath, relativePath, 'run_code script_path', '传入脚本文件路径');
379
+ try {
380
+ const content = await readFile(absolutePath, 'utf-8');
381
+ if (content.length > SCRIPT_READ_MAX_LEN) {
382
+ throw toolError('run_code 脚本文件过长', `脚本超过 ${SCRIPT_READ_MAX_LEN} 字符上限`, ['精简脚本或拆分为多个文件'], undefined, ToolErrorCode.ARGUMENT_ERROR);
383
+ }
384
+ return content;
385
+ }
386
+ catch (err) {
387
+ if (err instanceof MemoraError)
388
+ throw err;
389
+ const e = toError(err);
390
+ if (isNodeErrorCode(err, 'ENOENT')) {
391
+ throw toolError('run_code 脚本文件不存在', `${absolutePath}:文件不存在`, ['确认 script_path 正确', '先 write_file 创建脚本再执行'], e, ToolErrorCode.FILE_NOT_FOUND);
392
+ }
393
+ throw toolError('run_code 读取脚本失败', `${absolutePath}:${e.message}`, ['确认文件可读'], e, ToolErrorCode.UNKNOWN);
394
+ }
395
+ }
396
+ /**
397
+ * 删除项目文件(供 delete_file 工具)
398
+ *
399
+ * 安全策略:路径白名单校验 + 用户确认(guest 模式 / confirmWrites)。
400
+ * 仅支持删除文件,不支持删除目录(防误删)。
401
+ *
402
+ * 与 readScriptFile 同属「临时脚本」闭环(write_file 写 → run_code 执行 → delete_file 清理,
403
+ * 对齐主流 AI IDE 一次性数据处理不留痕的行为约定)。
404
+ *
405
+ * @param relativePath 相对项目根的文件路径
406
+ * @returns 删除结果描述
407
+ */
408
+ async deleteFile(relativePath) {
409
+ if (!relativePath) {
410
+ throw toolError('delete_file 工具调用缺少 path 参数', 'LLM 未传 path', ['检查 personality.md 是否明确了 delete_file 用法'], undefined, ToolErrorCode.ARGUMENT_ERROR);
411
+ }
412
+ const absolutePath = this.resolveSafePath(relativePath);
413
+ this.guardPathOrThrow(absolutePath, 'delete_file');
414
+ try {
415
+ // 删前预检:目标是目录时拒绝(防误删)
416
+ const stats = await stat(absolutePath);
417
+ if (stats.isDirectory()) {
418
+ throw toolError('delete_file 目标是目录', `${relativePath}:这是目录,delete_file 仅支持删除文件`, ['改用专门的删除目录工具(若存在)或手动删除'], undefined, ToolErrorCode.ARGUMENT_ERROR);
419
+ }
420
+ }
421
+ catch (err) {
422
+ // stat 失败(如 ENOENT)视为"已删除",返回成功(幂等性);判定收口 isNodeErrorCode
423
+ if (err instanceof MemoraError)
424
+ throw err;
425
+ if (isNodeErrorCode(err, 'ENOENT')) {
426
+ return `✅ 文件不存在(已删除):${absolutePath}`;
427
+ }
428
+ const e = toError(err);
429
+ throw toolError('delete_file 预检失败', `${absolutePath}:${e.message}`, ['确认路径正确'], e, ToolErrorCode.UNKNOWN);
430
+ }
431
+ // 删除确认:owner 模式默认自动批准,guest 模式 / confirmWrites 需用户确认
432
+ const needConfirm = this.security.permission === 'guest' || this.security.confirmWrites;
433
+ if (needConfirm) {
434
+ const description = `删除文件:${basename(absolutePath)}`;
435
+ const confirmed = await this.security.requestWriteConfirmation(absolutePath, 'delete_file', description);
436
+ if (!confirmed) {
437
+ throw toolError('用户拒绝删除', `用户取消了 delete_file 操作:${absolutePath}`, ['如需删除,请重新发起请求并确认'], undefined, ToolErrorCode.WRITE_REJECTED);
438
+ }
439
+ }
440
+ try {
441
+ await unlink(absolutePath);
442
+ return `✅ 已删除:${absolutePath}`;
443
+ }
444
+ catch (err) {
445
+ const e = toError(err);
446
+ throw toolError('delete_file 删除失败', `${absolutePath}:${e.message}`, ['确认文件未被占用', '确认有删除权限'], e, ToolErrorCode.PERMISSION_DENIED);
447
+ }
448
+ }
449
+ /**
450
+ * 写入文件(支持 overwrite / append / insert / replace,模式清单 SSOT = builtinTools.WRITE_FILE_MODES,
451
+ * 新增模式时本清单与 docs/memora-api-reference.md 的 write_file 行须同步复核)
170
452
  *
171
453
  * 安全策略:
172
454
  * - 路径必须在白名单内
173
455
  * - 写入前需用户确认(WriteExtensions.onBeforeWrite 或 SecurityGuard.requestWriteConfirmation)
174
456
  * - 自动创建不存在的父目录
175
457
  *
458
+ * 参数形态前提(位置参数,当前 6 个):再加 mode 专属参数时须一并评估改收 options 对象,
459
+ * 且四个内置文件工具(read_file / list_dir / search_memories)同批收,不单点特化本工具。
460
+ *
176
461
  * @param relativePath 相对项目根的文件路径
177
462
  * @param content 要写入的内容
178
463
  * @param extensions 写入扩展(可选,用于 diff 确认等)
179
- * @param mode 写入模式:"overwrite" | "append" | "insert",默认 "overwrite"
464
+ * @param mode 写入模式(合法值集 = WriteFileMode 类型,派生自 WRITE_FILE_MODES),默认 "overwrite"
180
465
  * @param insertLine insert 模式下的目标行号(从 1 开始),省略则插入到末尾
466
+ * @param oldString replace 模式下的被替换文本(须在旧内容中唯一出现)
181
467
  */
182
- async writeFile(relativePath, content, extensions, mode = 'overwrite', insertLine) {
468
+ async writeFile(relativePath, content, extensions, mode = 'overwrite', insertLine, oldString) {
183
469
  if (!relativePath) {
184
470
  throw toolError('write_file 工具调用缺少 path 参数', 'LLM 未传 path', ['检查 personality.md 是否明确了 write_file 用法'], undefined, ToolErrorCode.ARGUMENT_ERROR);
185
471
  }
186
472
  if (typeof content !== 'string') {
187
473
  throw toolError('write_file 工具调用缺少 content 参数', 'LLM 未传 content', ['确认 content 是字符串'], undefined, ToolErrorCode.ARGUMENT_ERROR);
188
474
  }
189
- // 校验 mode 参数合法性
190
- const validModes = ['overwrite', 'append', 'insert'];
191
- if (!validModes.includes(mode)) {
192
- throw toolError('write_file 参数错误', `mode 必须是 ${validModes.join('/')} 之一,收到:${mode}`, ['检查 LLM 输出的 mode 参数'], undefined, ToolErrorCode.ARGUMENT_ERROR);
475
+ // 校验 mode 参数合法性(模式清单单一真理源:builtinTools.WRITE_FILE_MODES,
476
+ // 经 isWriteFileMode 守卫窄化后,后续 mode 即 WriteFileMode 类型)
477
+ if (!isWriteFileMode(mode)) {
478
+ throw toolError('write_file 参数错误', `mode 必须是 ${WRITE_FILE_MODES.map((m) => m.name).join('/')} 之一,收到:${mode}`, ['检查 LLM 输出的 mode 参数'], undefined, ToolErrorCode.ARGUMENT_ERROR);
193
479
  }
194
480
  // insert 模式必须提供 insert_line
195
481
  if (mode === 'insert' && !insertLine) {
196
482
  throw toolError('write_file 参数错误', 'insert 模式必须提供 insert_line 参数', ['insert_line 指定插入位置的行号(从 1 开始)'], undefined, ToolErrorCode.ARGUMENT_ERROR);
197
483
  }
198
- // 配置目录拦截:personas/skills/rules 目录的内容应通过专用工具创建
199
- // 防止 LLM 误用 write_file 创建"孤儿文件"——写入 projectPath/personas/ 而非 configDir/personas/
200
- // 检查相对路径的第一级目录(去除前导 ./ / \),不区分大小写
201
- const firstSegment = relativePath
202
- .replace(/^[/\\]+/, '')
203
- .replace(/^\.\//, '')
204
- .split(/[/\\]/)[0]
205
- ?.toLowerCase();
206
- if (firstSegment && BuiltinToolHandlers.CONFIG_DIRS.has(firstSegment)) {
207
- // personas → persona, skills → skill, rules → rule(去尾 s 得到工具名后缀)
208
- const toolSuffix = firstSegment.replace(/s$/, '');
209
- return `错误:${firstSegment}/ 目录下的文件请使用 create_${toolSuffix} 工具创建,不要使用 write_file。create_${toolSuffix} 会正确写入 ${this.configPathHint(toolSuffix)} 并触发热重载。`;
210
- }
211
- // 内容特征检测:检查写入内容是否包含配置文件 frontmatter(source: persona/skill/rule)
212
- // 路径拦截易被绕过(去掉 personas/ 前缀即可),但配置文件 frontmatter 中 source 字段
213
- // 是 FileStore.write 写入的结构特征,LLM 无法绕过。
214
- const configSource = checkForConfigSource(content);
215
- if (configSource) {
216
- return `错误:写入内容包含 ${configSource} 配置文件的 frontmatter 标记(source: ${configSource}),请使用 create_${configSource} 工具创建,不要使用 write_file。create_${configSource} 会正确写入 ${this.configPathHint(configSource)} 并触发热重载。`;
484
+ // replace 模式必须提供非空 old_string(空串会被 indexOf 视为处处匹配,无意义)
485
+ if (mode === 'replace' && !oldString) {
486
+ throw toolError('write_file 参数错误', 'replace 模式必须提供非空 old_string 参数', ['old_string 是待替换的原文片段(须唯一匹配)', '要写入的新内容放在 content'], undefined, ToolErrorCode.ARGUMENT_ERROR);
217
487
  }
218
488
  const absolutePath = this.resolveSafePath(relativePath);
219
489
  this.guardPathOrThrow(absolutePath, 'write_file');
490
+ // 保留名守卫:任务表是内核工具数据(task_table_write/update 管理),不落盘为 markdown 文件。
491
+ // 若放行,LLM 会在同一会话内反复 write_file 写 .memora/task-table.md(成功先例 few-shot 强化 →
492
+ // 下轮沿用),绕过 PlanItem 通道导致顶部任务板不渲染/视角切换失效;软指令压不过先例,
493
+ // 必须确定性拦截——命中即失败,错误样本进会话历史成为「此路不通」负面先例,
494
+ // 引导 LLM 改走 task_table_write 单通道。
495
+ if (isReservedTaskTableFile(absolutePath)) {
496
+ throw toolError('保留文件名:任务表必须用 task_table_write/task_table_update 管理', `write_file 不允许写入 ${basename(absolutePath)}(任务表是内核工具数据,不落盘为文件)`, ['使用 task_table_write 创建/更新任务表', '使用 task_table_update 标记任务项状态'], undefined, ToolErrorCode.ARGUMENT_ERROR);
497
+ }
220
498
  // 读取文件旧内容(如果存在)
221
499
  let beforeContent = null;
222
500
  try {
@@ -227,7 +505,7 @@ export class BuiltinToolHandlers {
227
505
  // 文件不存在 → 新文件,beforeContent 保持 null
228
506
  }
229
507
  // 根据 mode 计算最终写入内容
230
- const finalContent = this.computeWriteContent(mode, content, beforeContent, insertLine);
508
+ const finalContent = this.computeWriteContent(mode, content, beforeContent, insertLine, oldString);
231
509
  // 写入确认:优先使用 WriteExtensions.onBeforeWrite(diff 确认),
232
510
  // 否则回退到 SecurityGuard.requestWriteConfirmation(安全确认)
233
511
  if (extensions?.onBeforeWrite) {
@@ -274,7 +552,15 @@ export class BuiltinToolHandlers {
274
552
  // 返回结果包含写入模式、行数变化等信息
275
553
  const newLines = finalContent.split('\n').length;
276
554
  const oldLines = beforeContent !== null ? beforeContent.split('\n').length : 0;
277
- const modeLabel = mode === 'overwrite' ? '覆盖' : mode === 'append' ? '追加' : `插入到第${insertLine}行`;
555
+ const modeLabel = mode === 'overwrite'
556
+ ? '覆盖'
557
+ : mode === 'append'
558
+ ? '追加'
559
+ : mode === 'insert'
560
+ ? `插入到第${insertLine}行`
561
+ : mode === 'replace'
562
+ ? '精确替换'
563
+ : mode; // 未知模式(校验已挡):如实回显原值,不伪装成某个已知模式
278
564
  return (`✅ 已写入(${modeLabel}):${absolutePath}(${finalContent.length} 字符,${newLines} 行)` +
279
565
  (beforeContent !== null ? ` [旧文件: ${oldLines} 行]` : ' [新文件]'));
280
566
  }
@@ -290,9 +576,10 @@ export class BuiltinToolHandlers {
290
576
  * @param content LLM 提供的写入内容
291
577
  * @param beforeContent 文件旧内容(null 表示新文件)
292
578
  * @param insertLine insert 模式下的行号
579
+ * @param oldString replace 模式下的被替换文本(须唯一匹配)
293
580
  * @returns 最终要写入文件的完整内容
294
581
  */
295
- computeWriteContent(mode, content, beforeContent, insertLine) {
582
+ computeWriteContent(mode, content, beforeContent, insertLine, oldString) {
296
583
  switch (mode) {
297
584
  case 'overwrite':
298
585
  // 全量覆盖:直接使用 content
@@ -320,9 +607,36 @@ export class BuiltinToolHandlers {
320
607
  lines.splice(insertIdx, 0, content);
321
608
  return lines.join('\n');
322
609
  }
323
- default:
324
- // 理论上不会到达(writeFile 已校验 mode),防御性兜底
325
- return content;
610
+ case 'replace': {
611
+ // 精确串替换:old_string 须在旧内容中唯一出现,否则拒绝歧义改写
612
+ // (改错地方毁代码 > 让 LLM 多补一段上下文重试;不做 replace_all,与 toolResultCache 同哲学)
613
+ if (beforeContent === null) {
614
+ throw toolError('文件不存在,无法精确替换', 'replace 模式要求目标文件已存在', ['新建文件请用 mode=overwrite'], undefined, ToolErrorCode.FILE_NOT_FOUND);
615
+ }
616
+ // oldString 非空由 writeFile 前置校验;此处 ?? '' 仅为类型收窄
617
+ const needle = oldString ?? '';
618
+ const firstIdx = beforeContent.indexOf(needle);
619
+ if (firstIdx === -1) {
620
+ throw toolError('replace 未匹配到 old_string', 'old_string 在目标文件中不存在,本次未做任何改动', [
621
+ '先用 read_file 读取文件,确认 old_string 与原文完全一致(含缩进/换行)',
622
+ '若目标位置不存在,改用 overwrite / append / insert',
623
+ ], undefined, ToolErrorCode.ARGUMENT_ERROR);
624
+ }
625
+ // 出现第二次即歧义 → 拒绝,要求扩大上下文
626
+ const secondIdx = beforeContent.indexOf(needle, firstIdx + needle.length);
627
+ if (secondIdx !== -1) {
628
+ throw toolError('replace 匹配到多处 old_string', 'old_string 在文件中出现多次,拒绝歧义替换', ['扩大 old_string 上下文,使其在文件中唯一'], undefined, ToolErrorCode.ARGUMENT_ERROR);
629
+ }
630
+ return (beforeContent.slice(0, firstIdx) + content + beforeContent.slice(firstIdx + needle.length));
631
+ }
632
+ default: {
633
+ // 编译期穷尽守卫:本 switch 覆盖 WRITE_FILE_MODES 全部成员时 mode 窄化为 never;
634
+ // 若新增模式漏加分支,下一行赋值即编译红——比运行时兜底更早暴露「表与 switch 脱节」
635
+ const exhaustiveNever = mode;
636
+ void exhaustiveNever;
637
+ // 运行时兜底(防绕过类型层的调用路径):响亮失败,不静默按 overwrite 落盘(静默回落 = 假阴性)
638
+ throw toolError('write_file 内部错误:未实现的写入模式', `mode=${mode} 在 computeWriteContent 中无分支`, ['检查 WRITE_FILE_MODES 与 computeWriteContent 是否同步'], undefined, ToolErrorCode.ARGUMENT_ERROR);
639
+ }
326
640
  }
327
641
  }
328
642
  /**
@@ -350,10 +664,7 @@ export class BuiltinToolHandlers {
350
664
  }
351
665
  catch (err) {
352
666
  // stat 调用失败:区分 ENOENT(不存在)和其他 IO 错误
353
- if (err !== null &&
354
- typeof err === 'object' &&
355
- 'code' in err &&
356
- err.code === 'ENOENT') {
667
+ if (isNodeErrorCode(err, 'ENOENT')) {
357
668
  const e = toError(err);
358
669
  throw toolError('list_dir 路径不存在', `${absolutePath}:目录不存在`, ['确认路径存在', '使用 list_dir(".") 列出项目根'], e, ToolErrorCode.DIR_NOT_FOUND);
359
670
  }
@@ -370,7 +681,8 @@ export class BuiltinToolHandlers {
370
681
  if (entries.length === 0) {
371
682
  return `(目录为空或所有条目都被忽略)${absolutePath}`;
372
683
  }
373
- return `目录 ${absolutePath} 共有 ${entries.length} 个条目:\n${entries.map((e) => ` ${e}`).join('\n')}`;
684
+ // 返回净化:目录树同为工具结果,做长度上限(避免超大目录整段进上下文)
685
+ return sanitizeExternalText(`目录 ${absolutePath} 共有 ${entries.length} 个条目:\n${entries.map((e) => ` ${e}`).join('\n')}`, DIR_LIST_MAX_LEN);
374
686
  }
375
687
  /**
376
688
  * 递归遍历目录(深度受限 + 忽略特定目录)
@@ -430,34 +742,338 @@ export class BuiltinToolHandlers {
430
742
  if (!query) {
431
743
  throw toolError('search_memories 工具调用缺少 query 参数', 'LLM 未传 query', ['query 不能为空'], undefined, ToolErrorCode.ARGUMENT_ERROR);
432
744
  }
433
- let limit = Number.parseInt(limitStr, 10);
434
- if (Number.isNaN(limit) || limit < 1) {
435
- limit = 10;
436
- }
437
- if (limit > 50) {
438
- limit = 50;
439
- }
440
- const mode = modeStr === 'near' ? 'near' : 'match';
441
- let results = this.memoryIndex.search(query, limit);
442
- // near 模式:过滤只保留所有关键词都命中的结果
443
- if (mode === 'near' && results.length > 0) {
444
- // 复用 segmentText 分词,与 inMemoryStorage 搜索保持一致
745
+ // limit 解析统一收口 parseLimit(非法→默认值,超上限钳制到 50)
746
+ const limit = parseLimit(limitStr, AGENT_CONSTANTS.DEFAULT_RECALL_LIMIT, 50);
747
+ // 工具召回与装配期内容互斥(§5.1):排除「正文或摘要已在眼前」的轮次 round-summary
748
+ // (正文已在眼前,避免重复返回)。由装配期注入的 exclusionRoundIdsProvider 提供
749
+ // (= loop.getExclusionRoundIds() 精确集合);缺省不注入则不过滤(测试/独立调用可直接触发)。
750
+ const excludedRoundIds = this.exclusionRoundIdsProvider
751
+ ? this.exclusionRoundIdsProvider()
752
+ : new Set();
753
+ // memory-tool-recall-design §3.3:注入 MemoryInspector 后走纯关键词搜索(searchByKeyword
754
+ // = 纯关键词 + superseded 过滤 + accessedAt/溯源揭示),否则回退旧关键词 memoryIndex.search。
755
+ // 包进带超时的函数:底部 search_memories 是有用户感知的读工具,语义 embed 是远程调用,
756
+ // 响应性护栏 MEMORY_SEARCH_TIMEOUT_MS(5s)超时降级为提示,不挂死工具调用(见 constants.ts)。
757
+ const hits = await this.withMemorySearchTimeout(async () => this.memoryInspector
758
+ ? await this.memoryInspector.searchByKeyword(query, limit, excludedRoundIds)
759
+ : this.memoryIndex
760
+ .search(query, limit)
761
+ .map((m) => ({
762
+ id: m.id,
763
+ name: m.name,
764
+ source: m.source,
765
+ contentPreview: m.content,
766
+ // 溯源 roundId 用于互斥过滤(与 searchByKeyword 排除口径一致)
767
+ roundId: m.roundId,
768
+ }))
769
+ .filter((h) => !(h.roundId && excludedRoundIds.has(h.roundId))));
770
+ // near 模式("must all keywords" 严格过滤):不论后端(searchByKeyword 或 memoryIndex)
771
+ // 一律在候选结果上做「所有关键词都命中」后置过滤,保证 mode 参数语义恒生效。
772
+ if (modeStr === 'near' && hits.length > 0) {
445
773
  const keywords = segmentLower(query);
446
774
  if (keywords.length > 1) {
447
- results = results.filter((m) => {
448
- const text = `${m.content} ${m.name}`.toLowerCase();
775
+ const filtered = hits.filter((m) => {
776
+ const text = `${m.contentPreview} ${m.name}`.toLowerCase();
449
777
  return keywords.every((kw) => text.includes(kw));
450
778
  });
779
+ hits.length = 0;
780
+ hits.push(...filtered);
781
+ }
782
+ }
783
+ if (hits.length === 0) {
784
+ return `(未找到匹配 "${query}" 的记忆${modeStr === 'near' ? '(near 模式:所有关键词必须命中)' : ''})`;
785
+ }
786
+ // 命中即 touch(§3.4/§3.3「命中 touch」行):fire-and-forget 刷新 accessedAt(backgroundTask + touchScores)。
787
+ // 只 touch 不加权(§5.2 定案):accessedAt 是「使用轨迹」唯一事实源,touchScores 内部只调 storage.touch
788
+ // 刷新访问时间、不写任何重要度字段——命中 = 被 LLM 想起,仅用于排序,不做记忆强化加权。查询低频,无自反馈(自动注入退役后根除正反馈)。
789
+ this.touchHits(hits);
790
+ // memoryRecalled 事件(§2.4「保留改语义」定案):LLM 查询记忆命中 N 条 → 宿主感知提示。
791
+ // 唯一发射位:此处是全库唯一 emit。
792
+ this.onMemoryRecalled?.({ count: hits.length, query });
793
+ const lines = hits.map((m, i) => {
794
+ const preview = truncate(m.contentPreview ?? '', 80);
795
+ const access = m.accessedAt ? ` accessedAt=${m.accessedAt}` : '';
796
+ // 结构化溯源(§3.3「返回」行):round-summary 命中项显式附 sessionId/roundId,LLM 零解析直用 trace_summary
797
+ const trace = m.sessionId
798
+ ? ` trace(${m.sessionId}${m.roundId ? `, round=${m.roundId}` : ''})`
799
+ : '';
800
+ // 命中项仅保留 accessedAt/trace
801
+ return `${i + 1}. [${m.source}:${m.name}] (${access}${trace})\n ${preview.replace(/\n/g, ' ')}`;
802
+ });
803
+ return `搜索 "${query}" 找到 ${hits.length} 条(${modeStr} 模式):\n${lines.join('\n')}`;
804
+ }
805
+ /**
806
+ * 命中即 touch:fire-and-forget 刷新命中记忆的 accessedAt(不涉及任何重要度字段)。
807
+ *
808
+ * 收敛到 keywordsTouch.ts 的单一真理源 touchScores(§5.2 只 touch 不加权,内部只调 storage.touch)——
809
+ * 不为工具命中另造写分路径。backgroundTask 保证 fire-and-forget 不阻塞搜索返回,
810
+ * 且统一并发限流;失败兜底记日志即可(touch 是排序副作用,非主线流程)。
811
+ *
812
+ * @param hits 搜索命中项(取其 id 定位,实际写入交由 touchScores 收敛)
813
+ */
814
+ touchHits(hits) {
815
+ if (hits.length === 0)
816
+ return;
817
+ backgroundTask('search_memories.touch', () => touchScores(this.memoryIndex, hits.map((h) => h.id)));
818
+ }
819
+ /**
820
+ * `search_memories` 响应性护栏包装:给整次记忆搜索设上限。
821
+ *
822
+ * 背景:搜索服务为网络调用时超时远长于本地上限,这里收窄为 MEMORY_SEARCH_TIMEOUT_MS(5s)——
823
+ * 超时降级为「搜索超时」提示(空结果语义),而非挂死工具或抛错中断对话。
824
+ *
825
+ * @param run 返回记忆搜索结果的函数(同步或异步)
826
+ * @returns 搜索完成后返回全量命中;超时则返回空数组(降级提示由调用方近零命中分支承担)
827
+ */
828
+ async withMemorySearchTimeout(run) {
829
+ const timeout = new Promise((_, reject) => {
830
+ const id = setTimeout(() => {
831
+ clearTimeout(id);
832
+ reject(new Error('记忆搜索超时'));
833
+ }, LOOP_CONSTANTS.MEMORY_SEARCH_TIMEOUT_MS);
834
+ // 让定时器不阻塞 Node 事件循环退出(测试/关停场景无悬挂定时器)
835
+ if (typeof id.unref === 'function')
836
+ id.unref();
837
+ });
838
+ try {
839
+ return await Promise.race([run(), timeout]);
840
+ }
841
+ catch (err) {
842
+ // 超时降级:记日志 + 返回空(近零命中分支已有「未找到匹配」文案兜底,不重复造文案)
843
+ logger.warn({ err: toError(err).message }, 'search_memories 搜索超时,已降级为空结果');
844
+ return [];
845
+ }
846
+ }
847
+ /**
848
+ * 追溯轮次摘要的原始对话内容
849
+ *
850
+ * 从记忆索引中查找 `source='round-summary'` 的记忆,
851
+ * 根据 sessionId 和 roundId 过滤。
852
+ * 返回格式化后的摘要列表,包含摘要内容、类型和溯源信息。
853
+ *
854
+ * @param sessionId 会话标识(格式:YYYY-MM-DD-sessionName)
855
+ * @param roundId 轮次 ID(可选,不传则返回最近 N 条摘要)
856
+ * @param limitStr 返回结果数量上限(默认 "5",最大 "20")
857
+ */
858
+ async traceSummary(sessionId, roundId, limitStr) {
859
+ if (!sessionId) {
860
+ throw toolError('trace_summary 工具调用缺少 sessionId 参数', 'LLM 未传 sessionId', ['sessionId 不能为空'], undefined, ToolErrorCode.ARGUMENT_ERROR);
861
+ }
862
+ // limit 解析统一收口 parseLimit(非法→默认 5,超上限钳制到 20)
863
+ const limit = parseLimit(limitStr, 5, 20);
864
+ // 获取所有 round-summary 类型的记忆
865
+ const allSummaries = this.memoryIndex.getBySource(SOURCE_LABELS.ROUND_SUMMARY);
866
+ // 按 sessionId 过滤
867
+ // 会话级前缀与写侧同源(roundSummarySessionPrefix,memory/types):标签变更无需同步两处
868
+ const sessionPrefix = roundSummarySessionPrefix(sessionId);
869
+ const matched = allSummaries.filter((m) => m.id.startsWith(sessionPrefix));
870
+ if (roundId) {
871
+ // 精确匹配轮次
872
+ const exact = matched.find((m) => m.id === `${sessionPrefix}${roundId}`);
873
+ if (!exact) {
874
+ return `(未找到会话 "${sessionId}" 中轮次 "${roundId}" 的摘要)`;
451
875
  }
876
+ // 溯源真实化:优先返回该轮次的原始对话
877
+ // 仅当宿主注入了 sessionStore 且能定位到对应轮次消息时返回原始对话,
878
+ // 否则回退为摘要文本(保证工具始终可用、不因缺注入而报错)。
879
+ const raw = this.loadRawRoundMessages(sessionId, roundId);
880
+ if (raw) {
881
+ // 关键约束:原始对话文本注入前必须过 sanitizeExternalText,去控制字符 + 长度上限。
882
+ // 与 toolExecutor 主路径净化一致,防止用户历史中的控制字符/隐藏指令直通 LLM 上下文。
883
+ const lines = raw.messages
884
+ .map((m) => `[${m.role}] ${sanitizeExternalText(m.content, TRACE_MESSAGE_CHAR_LIMIT)}`)
885
+ .join('\n');
886
+ const truncNote = raw.truncated ? '\n(对话已截断,仍有更多消息)' : '';
887
+ return `会话:${sessionId} | 轮次:${roundId} 原始对话:\n${lines}${truncNote}`;
888
+ }
889
+ const summaryType = exact.summaryType ?? 'general';
890
+ const modifiedInfo = exact.isModified ? '(已手动修改)' : '';
891
+ // 溯源失败降级:按本次查找结果渲染(原始对话已删),不依赖字段标记
892
+ return `会话:${sessionId} | 轮次:${roundId} | 类型:${summaryType}${modifiedInfo}\n摘要:${exact.content}\n(原始对话已删除,仅剩摘要)\n`;
893
+ }
894
+ // 未指定 roundId,返回最近 N 条(按 createdAt 降序)
895
+ if (matched.length === 0) {
896
+ return `(会话 "${sessionId}" 暂无轮次摘要)`;
897
+ }
898
+ matched.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
899
+ const top = matched.slice(0, limit);
900
+ const lines = top.map((m, i) => {
901
+ const roundIdFromMeta = m.roundId ?? 'unknown';
902
+ const summaryType = m.summaryType ?? 'general';
903
+ const preview = truncate(m.content, 120);
904
+ return `${i + 1}. [${summaryType}] 轮次 ${roundIdFromMeta}\n ${preview.replace(/\n/g, ' ')}`;
905
+ });
906
+ return `会话 "${sessionId}" 的轮次摘要(最近 ${top.length} 条):\n${lines.join('\n')}`;
907
+ }
908
+ /**
909
+ * 溯源指定轮次的原始对话消息
910
+ *
911
+ * 通过 sessionStore.loadMessages 定位该会话,按 SessionMessage.roundId 过滤出本轮消息。
912
+ * 规模控制:最多 5 条消息,超过则截断并标记 isTruncated。
913
+ *
914
+ * @param sessionId 会话标识(格式:YYYY-MM-DD-sessionName)
915
+ * @param roundId 轮次 ID
916
+ * @returns 原始消息列表(≤5 条)+ 是否截断;会话存储不可用或未命中该轮次时返回 null
917
+ */
918
+ loadRawRoundMessages(sessionId, roundId) {
919
+ if (!this.sessionStore)
920
+ return null;
921
+ // 解析 date(前 10 位 YYYY-MM-DD)与 session(去掉 "YYYY-MM-DD-" 前缀)
922
+ // 用内核 splitSessionId(SSOT 契约,防 session 名含连字符时切错)
923
+ const parsed = splitSessionId(sessionId);
924
+ if (!parsed.session)
925
+ return null;
926
+ try {
927
+ const all = this.sessionStore.loadMessages(parsed.date, parsed.session);
928
+ const roundMsgs = all.filter((m) => m.roundId === roundId);
929
+ if (roundMsgs.length === 0)
930
+ return null;
931
+ const truncated = roundMsgs.length > TRACE_MESSAGE_LIMIT;
932
+ return {
933
+ messages: roundMsgs.slice(0, TRACE_MESSAGE_LIMIT),
934
+ truncated,
935
+ };
936
+ }
937
+ catch (err) {
938
+ // 会话存储异常时降级(不阻断工具),由调用方回退摘要文本
939
+ logger.warn({ err, sessionId, roundId }, 'trace_summary 读取原始对话失败,降级为摘要');
940
+ return null;
941
+ }
942
+ }
943
+ /**
944
+ * list_sessions:列出历史会话**路标**(会话级摘要),供 LLM 粗定位后再用 trace_summary 下钻。
945
+ *
946
+ * 会话级摘要是「路标」而非记忆——存于 SessionMeta(summary/keyTopics),不进记忆召回池
947
+ * (会话级路标不进记忆库)。本工具是「粗定位 → 细取证」闭环的第一环:
948
+ * 返回最近 N 个会话的路标,LLM 据此挑目标,再用 trace_summary(sessionId) 取该会话轮次摘要。
949
+ *
950
+ * 路标滞后说明:SessionArchiver 在**会话切换前**触发,故当前会话的摘要可能落后于最新对话。
951
+ * 本工具按 updatedAt 降序返回,历史会话路标准确;当前会话以实际上下文为准,不依赖路标。
952
+ *
953
+ * @param limitStr 返回条数上限(默认 "10",最大 "30")
954
+ * @returns 格式化的会话路标列表;无会话存储 / 无会话时返回说明文本(不抛错)
955
+ */
956
+ async listSessions(limitStr) {
957
+ // limit 解析统一收口 parseLimit(非法→默认 10,超上限钳制到 30)
958
+ const limit = parseLimit(limitStr, LIST_SESSIONS_DEFAULT, LIST_SESSIONS_MAX);
959
+ // 未注入会话存储:降级为说明文本(与 trace_summary 的降级哲学一致,不阻塞对话)
960
+ if (!this.sessionStore)
961
+ return '(未配置会话存储,无法列出历史会话)';
962
+ let ids;
963
+ try {
964
+ ids = this.sessionStore.listSessions();
452
965
  }
453
- if (results.length === 0) {
454
- return `(未找到匹配 "${query}" 的记忆${mode === 'near' ? '(near 模式:所有关键词必须命中)' : ''})`;
966
+ catch (err) {
967
+ logger.warn({ err }, 'list_sessions 列举会话失败,降级为空结果');
968
+ return '(读取历史会话失败,请稍后重试)';
969
+ }
970
+ if (ids.length === 0)
971
+ return '(暂无历史会话)';
972
+ const metas = [];
973
+ for (const id of ids) {
974
+ try {
975
+ const meta = this.sessionStore.getSessionMeta(id);
976
+ if (meta)
977
+ metas.push(meta);
978
+ }
979
+ catch (err) {
980
+ // 单个会话 meta 损坏不影响整体列举(降级优先,跳过该条)
981
+ logger.warn({ err, sessionId: id }, 'list_sessions 读取会话元数据失败,跳过该条');
982
+ }
455
983
  }
456
- const lines = results.map((m, i) => {
457
- const preview = truncate(m.content, 80);
458
- return `${i + 1}. [${m.source}:${m.name}] (score=${m.score})\n ${preview.replace(/\n/g, ' ')}`;
984
+ // 按最近活跃降序(历史会话列表的直觉顺序)
985
+ metas.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0));
986
+ const shown = metas.slice(0, limit);
987
+ if (shown.length === 0)
988
+ return '(暂无历史会话)';
989
+ const lines = shown.map((m, i) => {
990
+ const name = getSessionDisplayName(m) || m.sessionId;
991
+ // 路标文本由 LLM 生成自用户内容 → 过 sanitize 防注入,并限长
992
+ const summary = m.summary
993
+ ? `\n ${sanitizeExternalText(m.summary, LIST_SESSIONS_SUMMARY_CHARS)}`
994
+ : '';
995
+ const topics = m.keyTopics && m.keyTopics.length > 0
996
+ ? `\n 主题:${sanitizeExternalText(m.keyTopics.join(' / '), LIST_SESSIONS_SUMMARY_CHARS)}`
997
+ : '';
998
+ return `${i + 1}. ${m.sessionId} · ${sanitizeExternalText(name, 100)}${topics}${summary}`;
459
999
  });
460
- return `搜索 "${query}" 找到 ${results.length} 条(${mode} 模式):\n${lines.join('\n')}`;
1000
+ const moreNote = metas.length > shown.length
1001
+ ? `\n(共 ${metas.length} 个会话,仅显示最近 ${shown.length} 个;需要更多请调大 limit)`
1002
+ : '';
1003
+ return (`历史会话路标(按最近活跃降序,共 ${metas.length} 个):\n${lines.join('\n')}${moreNote}\n\n` +
1004
+ '如需查看某个会话的问答摘要,用 trace_summary 并传入该会话的 sessionId(不传 roundId 即返回该会话最近若干轮)。');
1005
+ }
1006
+ }
1007
+ /**
1008
+ * 拼装 run_team_meeting 的多角色 system prompt(纯函数,可单测)
1009
+ *
1010
+ * 把各角色 persona 全文按「组长置前、组员随后」顺序拼接,并在头部声明任务——
1011
+ * 让模型以各角色设定与专业视角独立评估议题,最后以组长视角汇总。
1012
+ * 组长在 prompt 中显式标注为「组长」,承担唯一汇总视角。
1013
+ *
1014
+ * @param roles 参与会议的角色(name + persona 全文;已过滤未装载的空 persona)
1015
+ * @param leaderName 组长的角色包名(唯一汇总视角)
1016
+ * @param topic 要评估的议题
1017
+ * @returns 完成拼装的多角色 system prompt
1018
+ */
1019
+ function buildTeamMeetingPrompt(roles, leaderName, topic) {
1020
+ const roleLines = roles
1021
+ .map((r, i) => {
1022
+ const tag = r.name === leaderName ? `角色${i + 1}(组长)` : `角色${i + 1}(组员)`;
1023
+ return `── ${tag}名称:${r.name} ──\n${r.persona}`;
1024
+ })
1025
+ .join('\n\n');
1026
+ return (`请以以下各角色的设定与专业视角,分别独立评估议题「${topic}」,给出各自的立场与理由` +
1027
+ `(每个视角一段),最后以组长「${leaderName}」视角做简短汇总。\n\n${roleLines}`);
1028
+ }
1029
+ /**
1030
+ * run_team_meeting 执行实现(工具内嵌 LLM 调用,一次 chat() 完成)
1031
+ *
1032
+ * 串联既有原语实现「评估/评审型小组会议」:解析组名 → 取各角色 persona 全文 → 拼多角色
1033
+ * system prompt → 单次 provider.chat() → 返回评审文本。依赖经参数注入(不持有全局状态),
1034
+ * 由装配层在后续注入时把 rolePackManager 与 provider 收敛进闭包。
1035
+ *
1036
+ * 边界(探索方案 §5.3 实证):
1037
+ * - 只覆盖评估/评审型(各视角独立观点 + 组长汇总),不覆盖你来我往的讨论型会议;
1038
+ * - 角色数隐式受组解析截断约束(组长 1 + 组员 ≤ 4),persona 全文 token 成本高。
1039
+ *
1040
+ * @param params 依赖注入 + 工具入参(resolveTeam/buildPersona/provider 由装配层提供)
1041
+ * @returns 多角色评估文本(已净化 + 长度上限)
1042
+ * @throws 参数缺失 / 组不存在 / 组角色未装载 → MemoraError
1043
+ */
1044
+ export async function runTeamMeetingAssessment(params) {
1045
+ const { group, topic } = params;
1046
+ if (!group || !group.trim()) {
1047
+ throw toolError('run_team_meeting 缺少 group 参数', '未传组名(组名 = 组长角色包名)', ['传组名,如 run_team_meeting("设计组", "议题")'], undefined, ToolErrorCode.ARGUMENT_ERROR);
1048
+ }
1049
+ if (!topic || !topic.trim()) {
1050
+ throw toolError('run_team_meeting 缺少 topic 参数', '未传要评估的议题', ['传一句议题,越具体越好'], undefined, ToolErrorCode.ARGUMENT_ERROR);
1051
+ }
1052
+ // 组解析:组名 = 组长角色包名;未命中即组不存在
1053
+ const team = params.resolveTeam(group);
1054
+ if (!team) {
1055
+ throw toolError('run_team_meeting 组不存在', `未找到组长为「${group}」的组`, ['确认组名与组长角色包名一致', '确认该组已在组名单中配置'], undefined, ToolErrorCode.ARGUMENT_ERROR);
1056
+ }
1057
+ // 取各角色 persona 全文;未装载(buildPersona 返回空)的角色跳过,保证拼入的都有效
1058
+ const roles = [team.leader, ...team.members]
1059
+ .map((name) => ({ name, persona: params.buildPersona(name) }))
1060
+ .filter((r) => r.persona.length > 0);
1061
+ if (roles.length === 0) {
1062
+ throw toolError('run_team_meeting 组角色未装载', `组「${group}」的成员角色包均未装载,无法开会`, ['确认组长与组员角色包已装载'], undefined, ToolErrorCode.ARGUMENT_ERROR);
1063
+ }
1064
+ // 一次 chat() 完成多角色评估 + 组长汇总(拼装见 buildTeamMeetingPrompt)
1065
+ const systemPrompt = buildTeamMeetingPrompt(roles, team.leader, topic);
1066
+ const messages = [
1067
+ { role: 'system', content: systemPrompt },
1068
+ { role: 'user', content: '请按上述要求给出各角色观点与组长汇总的正式评估。' },
1069
+ ];
1070
+ let response = '';
1071
+ for await (const chunk of params.provider.chat(messages)) {
1072
+ if (chunk.content)
1073
+ response += chunk.content;
461
1074
  }
1075
+ if (!response.trim())
1076
+ return '(会议未产生输出)';
1077
+ return sanitizeExternalText(response, MEETING_RESULT_MAX_LEN);
462
1078
  }
463
1079
  //# sourceMappingURL=builtinToolHandlers.js.map