@zooique/memora 1.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.en.md +272 -0
  2. package/README.md +150 -38
  3. package/dist/agent/agent.d.ts +257 -115
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +747 -400
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/agent/assembler.d.ts +32 -16
  8. package/dist/agent/assembler.d.ts.map +1 -1
  9. package/dist/agent/assembler.js +83 -47
  10. package/dist/agent/assembler.js.map +1 -1
  11. package/dist/agent/builtinToolHandlers.d.ts +27 -2
  12. package/dist/agent/builtinToolHandlers.d.ts.map +1 -1
  13. package/dist/agent/builtinToolHandlers.js +78 -5
  14. package/dist/agent/builtinToolHandlers.js.map +1 -1
  15. package/dist/agent/constants.d.ts +4 -0
  16. package/dist/agent/constants.d.ts.map +1 -1
  17. package/dist/agent/constants.js +4 -0
  18. package/dist/agent/constants.js.map +1 -1
  19. package/dist/agent/contextManager.d.ts +8 -0
  20. package/dist/agent/contextManager.d.ts.map +1 -1
  21. package/dist/agent/contextManager.js +7 -2
  22. package/dist/agent/contextManager.js.map +1 -1
  23. package/dist/agent/guardrail.d.ts +4 -2
  24. package/dist/agent/guardrail.d.ts.map +1 -1
  25. package/dist/agent/guardrail.js +13 -5
  26. package/dist/agent/guardrail.js.map +1 -1
  27. package/dist/agent/loop.d.ts +137 -15
  28. package/dist/agent/loop.d.ts.map +1 -1
  29. package/dist/agent/loop.js +449 -270
  30. package/dist/agent/loop.js.map +1 -1
  31. package/dist/agent/managers/archiveCoordinator.d.ts +126 -22
  32. package/dist/agent/managers/archiveCoordinator.d.ts.map +1 -1
  33. package/dist/agent/managers/archiveCoordinator.js +239 -44
  34. package/dist/agent/managers/archiveCoordinator.js.map +1 -1
  35. package/dist/agent/managers/autoConfigRefiner.d.ts.map +1 -1
  36. package/dist/agent/managers/autoConfigRefiner.js +4 -8
  37. package/dist/agent/managers/autoConfigRefiner.js.map +1 -1
  38. package/dist/agent/managers/chatLockManager.d.ts +116 -0
  39. package/dist/agent/managers/chatLockManager.d.ts.map +1 -0
  40. package/dist/agent/managers/chatLockManager.js +191 -0
  41. package/dist/agent/managers/chatLockManager.js.map +1 -0
  42. package/dist/agent/managers/configManager.d.ts +87 -3
  43. package/dist/agent/managers/configManager.d.ts.map +1 -1
  44. package/dist/agent/managers/configManager.js +139 -2
  45. package/dist/agent/managers/configManager.js.map +1 -1
  46. package/dist/agent/managers/dedupManager.d.ts +169 -0
  47. package/dist/agent/managers/dedupManager.d.ts.map +1 -0
  48. package/dist/agent/managers/dedupManager.js +336 -0
  49. package/dist/agent/managers/dedupManager.js.map +1 -0
  50. package/dist/agent/managers/insightExtractor.d.ts +31 -3
  51. package/dist/agent/managers/insightExtractor.d.ts.map +1 -1
  52. package/dist/agent/managers/insightExtractor.js +130 -59
  53. package/dist/agent/managers/insightExtractor.js.map +1 -1
  54. package/dist/agent/managers/llmJudgeHelper.d.ts +38 -0
  55. package/dist/agent/managers/llmJudgeHelper.d.ts.map +1 -0
  56. package/dist/agent/managers/llmJudgeHelper.js +49 -0
  57. package/dist/agent/managers/llmJudgeHelper.js.map +1 -0
  58. package/dist/agent/managers/memoryAdvisor.d.ts +102 -2
  59. package/dist/agent/managers/memoryAdvisor.d.ts.map +1 -1
  60. package/dist/agent/managers/memoryAdvisor.js +254 -10
  61. package/dist/agent/managers/memoryAdvisor.js.map +1 -1
  62. package/dist/agent/managers/memoryDecayScheduler.d.ts +127 -1
  63. package/dist/agent/managers/memoryDecayScheduler.d.ts.map +1 -1
  64. package/dist/agent/managers/memoryDecayScheduler.js +283 -4
  65. package/dist/agent/managers/memoryDecayScheduler.js.map +1 -1
  66. package/dist/agent/managers/memoryGovernance.d.ts +56 -0
  67. package/dist/agent/managers/memoryGovernance.d.ts.map +1 -0
  68. package/dist/agent/managers/memoryGovernance.js +63 -0
  69. package/dist/agent/managers/memoryGovernance.js.map +1 -0
  70. package/dist/agent/managers/memoryInspector.d.ts +113 -30
  71. package/dist/agent/managers/memoryInspector.d.ts.map +1 -1
  72. package/dist/agent/managers/memoryInspector.js +165 -32
  73. package/dist/agent/managers/memoryInspector.js.map +1 -1
  74. package/dist/agent/managers/relationBuilder.d.ts +4 -4
  75. package/dist/agent/managers/relationBuilder.d.ts.map +1 -1
  76. package/dist/agent/managers/relationBuilder.js +5 -4
  77. package/dist/agent/managers/relationBuilder.js.map +1 -1
  78. package/dist/agent/managers/sessionArchiver.d.ts +12 -1
  79. package/dist/agent/managers/sessionArchiver.d.ts.map +1 -1
  80. package/dist/agent/managers/sessionArchiver.js +87 -30
  81. package/dist/agent/managers/sessionArchiver.js.map +1 -1
  82. package/dist/agent/managers/sessionManager.d.ts.map +1 -1
  83. package/dist/agent/managers/sessionManager.js +9 -17
  84. package/dist/agent/managers/sessionManager.js.map +1 -1
  85. package/dist/agent/managers/streamAccumulator.d.ts +45 -0
  86. package/dist/agent/managers/streamAccumulator.d.ts.map +1 -0
  87. package/dist/agent/managers/streamAccumulator.js +40 -0
  88. package/dist/agent/managers/streamAccumulator.js.map +1 -0
  89. package/dist/agent/managers/textPolishManager.d.ts +62 -0
  90. package/dist/agent/managers/textPolishManager.d.ts.map +1 -0
  91. package/dist/agent/managers/textPolishManager.js +96 -0
  92. package/dist/agent/managers/textPolishManager.js.map +1 -0
  93. package/dist/agent/managers/workProjection.d.ts +16 -1
  94. package/dist/agent/managers/workProjection.d.ts.map +1 -1
  95. package/dist/agent/managers/workProjection.js +48 -24
  96. package/dist/agent/managers/workProjection.js.map +1 -1
  97. package/dist/agent/personaMatcher.d.ts +29 -0
  98. package/dist/agent/personaMatcher.d.ts.map +1 -0
  99. package/dist/agent/personaMatcher.js +60 -0
  100. package/dist/agent/personaMatcher.js.map +1 -0
  101. package/dist/agent/toolExecutor.d.ts +24 -4
  102. package/dist/agent/toolExecutor.d.ts.map +1 -1
  103. package/dist/agent/toolExecutor.js +30 -4
  104. package/dist/agent/toolExecutor.js.map +1 -1
  105. package/dist/agent/tracer.d.ts +31 -0
  106. package/dist/agent/tracer.d.ts.map +1 -1
  107. package/dist/agent/tracer.js +31 -0
  108. package/dist/agent/tracer.js.map +1 -1
  109. package/dist/agent/types.d.ts +133 -0
  110. package/dist/agent/types.d.ts.map +1 -1
  111. package/dist/agent/types.js +38 -0
  112. package/dist/agent/types.js.map +1 -1
  113. package/dist/agent/userFactExtractor.d.ts.map +1 -1
  114. package/dist/agent/userFactExtractor.js +7 -0
  115. package/dist/agent/userFactExtractor.js.map +1 -1
  116. package/dist/config/loader.d.ts +124 -236
  117. package/dist/config/loader.d.ts.map +1 -1
  118. package/dist/config/loader.js +296 -118
  119. package/dist/config/loader.js.map +1 -1
  120. package/dist/eval/evalRunner.d.ts +94 -0
  121. package/dist/eval/evalRunner.d.ts.map +1 -0
  122. package/dist/eval/evalRunner.js +141 -0
  123. package/dist/eval/evalRunner.js.map +1 -0
  124. package/dist/eval/evalTypes.d.ts +21 -0
  125. package/dist/eval/evalTypes.d.ts.map +1 -1
  126. package/dist/eval/evalTypes.js +15 -0
  127. package/dist/eval/evalTypes.js.map +1 -1
  128. package/dist/eval/scenarios.d.ts +33 -0
  129. package/dist/eval/scenarios.d.ts.map +1 -0
  130. package/dist/eval/scenarios.js +109 -0
  131. package/dist/eval/scenarios.js.map +1 -0
  132. package/dist/index.d.ts +45 -6
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +45 -5
  135. package/dist/index.js.map +1 -1
  136. package/dist/llm/abortSignal.d.ts +27 -0
  137. package/dist/llm/abortSignal.d.ts.map +1 -0
  138. package/dist/llm/abortSignal.js +44 -0
  139. package/dist/llm/abortSignal.js.map +1 -0
  140. package/dist/llm/embedding.d.ts +13 -12
  141. package/dist/llm/embedding.d.ts.map +1 -1
  142. package/dist/llm/embedding.js +5 -17
  143. package/dist/llm/embedding.js.map +1 -1
  144. package/dist/llm/factory.d.ts +19 -5
  145. package/dist/llm/factory.d.ts.map +1 -1
  146. package/dist/llm/factory.js +19 -37
  147. package/dist/llm/factory.js.map +1 -1
  148. package/dist/llm/openaiCompatible.d.ts.map +1 -1
  149. package/dist/llm/openaiCompatible.js +17 -34
  150. package/dist/llm/openaiCompatible.js.map +1 -1
  151. package/dist/logging/logger.d.ts +3 -1
  152. package/dist/logging/logger.d.ts.map +1 -1
  153. package/dist/logging/logger.js +47 -50
  154. package/dist/logging/logger.js.map +1 -1
  155. package/dist/memory/governance.d.ts +50 -0
  156. package/dist/memory/governance.d.ts.map +1 -0
  157. package/dist/memory/governance.js +70 -0
  158. package/dist/memory/governance.js.map +1 -0
  159. package/dist/memory/hybridMerge.d.ts +0 -4
  160. package/dist/memory/hybridMerge.d.ts.map +1 -1
  161. package/dist/memory/hybridMerge.js +19 -4
  162. package/dist/memory/hybridMerge.js.map +1 -1
  163. package/dist/memory/inMemoryRelationStore.d.ts +7 -0
  164. package/dist/memory/inMemoryRelationStore.d.ts.map +1 -1
  165. package/dist/memory/inMemoryRelationStore.js +15 -0
  166. package/dist/memory/inMemoryRelationStore.js.map +1 -1
  167. package/dist/memory/inMemoryStorage.d.ts +36 -1
  168. package/dist/memory/inMemoryStorage.d.ts.map +1 -1
  169. package/dist/memory/inMemoryStorage.js +70 -19
  170. package/dist/memory/inMemoryStorage.js.map +1 -1
  171. package/dist/memory/loader.d.ts +2 -0
  172. package/dist/memory/loader.d.ts.map +1 -1
  173. package/dist/memory/loader.js +8 -1
  174. package/dist/memory/loader.js.map +1 -1
  175. package/dist/memory/lockManager.d.ts +7 -2
  176. package/dist/memory/lockManager.d.ts.map +1 -1
  177. package/dist/memory/lockManager.js +35 -17
  178. package/dist/memory/lockManager.js.map +1 -1
  179. package/dist/memory/projectManager.d.ts +8 -7
  180. package/dist/memory/projectManager.d.ts.map +1 -1
  181. package/dist/memory/projectManager.js +33 -15
  182. package/dist/memory/projectManager.js.map +1 -1
  183. package/dist/memory/projectRegistry.d.ts +34 -1
  184. package/dist/memory/projectRegistry.d.ts.map +1 -1
  185. package/dist/memory/projectRegistry.js +100 -31
  186. package/dist/memory/projectRegistry.js.map +1 -1
  187. package/dist/memory/recall.d.ts +16 -0
  188. package/dist/memory/recall.d.ts.map +1 -1
  189. package/dist/memory/recall.js +35 -20
  190. package/dist/memory/recall.js.map +1 -1
  191. package/dist/memory/relationStore.d.ts +20 -3
  192. package/dist/memory/relationStore.d.ts.map +1 -1
  193. package/dist/memory/sourceValidation.d.ts +17 -0
  194. package/dist/memory/sourceValidation.d.ts.map +1 -1
  195. package/dist/memory/sourceValidation.js +27 -3
  196. package/dist/memory/sourceValidation.js.map +1 -1
  197. package/dist/memory/storageInterface.d.ts +26 -1
  198. package/dist/memory/storageInterface.d.ts.map +1 -1
  199. package/dist/memory/store.d.ts +22 -1
  200. package/dist/memory/store.d.ts.map +1 -1
  201. package/dist/memory/store.js +58 -20
  202. package/dist/memory/store.js.map +1 -1
  203. package/dist/memory/types.d.ts +44 -31
  204. package/dist/memory/types.d.ts.map +1 -1
  205. package/dist/memory/types.js +63 -24
  206. package/dist/memory/types.js.map +1 -1
  207. package/dist/memory/userProfile.d.ts +32 -8
  208. package/dist/memory/userProfile.d.ts.map +1 -1
  209. package/dist/memory/userProfile.js +105 -44
  210. package/dist/memory/userProfile.js.map +1 -1
  211. package/dist/memory/vectorStore.d.ts +16 -8
  212. package/dist/memory/vectorStore.d.ts.map +1 -1
  213. package/dist/memory/vectorStore.js +49 -22
  214. package/dist/memory/vectorStore.js.map +1 -1
  215. package/dist/persona/personaManager.d.ts +38 -63
  216. package/dist/persona/personaManager.d.ts.map +1 -1
  217. package/dist/persona/personaManager.js +136 -202
  218. package/dist/persona/personaManager.js.map +1 -1
  219. package/dist/skill/skillManager.d.ts +11 -57
  220. package/dist/skill/skillManager.d.ts.map +1 -1
  221. package/dist/skill/skillManager.js +46 -141
  222. package/dist/skill/skillManager.js.map +1 -1
  223. package/dist/skill/types.d.ts +2 -3
  224. package/dist/skill/types.d.ts.map +1 -1
  225. package/dist/utils/array.d.ts +24 -0
  226. package/dist/utils/array.d.ts.map +1 -0
  227. package/dist/utils/array.js +24 -0
  228. package/dist/utils/array.js.map +1 -0
  229. package/dist/utils/configResourceManager.d.ts +111 -0
  230. package/dist/utils/configResourceManager.d.ts.map +1 -0
  231. package/dist/utils/configResourceManager.js +148 -0
  232. package/dist/utils/configResourceManager.js.map +1 -0
  233. package/dist/utils/errors.d.ts +27 -8
  234. package/dist/utils/errors.d.ts.map +1 -1
  235. package/dist/utils/errors.js +31 -9
  236. package/dist/utils/errors.js.map +1 -1
  237. package/dist/utils/eventEmitter.d.ts +88 -4
  238. package/dist/utils/eventEmitter.d.ts.map +1 -1
  239. package/dist/utils/eventEmitter.js +23 -0
  240. package/dist/utils/eventEmitter.js.map +1 -1
  241. package/dist/utils/frontmatter.d.ts.map +1 -1
  242. package/dist/utils/frontmatter.js +4 -2
  243. package/dist/utils/frontmatter.js.map +1 -1
  244. package/dist/utils/math.d.ts +11 -0
  245. package/dist/utils/math.d.ts.map +1 -1
  246. package/dist/utils/math.js +17 -0
  247. package/dist/utils/math.js.map +1 -1
  248. package/dist/utils/objects.d.ts +17 -0
  249. package/dist/utils/objects.d.ts.map +1 -0
  250. package/dist/utils/objects.js +19 -0
  251. package/dist/utils/objects.js.map +1 -0
  252. package/dist/utils/segmenter.d.ts +11 -10
  253. package/dist/utils/segmenter.d.ts.map +1 -1
  254. package/dist/utils/segmenter.js +23 -1
  255. package/dist/utils/segmenter.js.map +1 -1
  256. package/dist/utils/strings.d.ts +22 -1
  257. package/dist/utils/strings.d.ts.map +1 -1
  258. package/dist/utils/strings.js +29 -1
  259. package/dist/utils/strings.js.map +1 -1
  260. package/dist/utils/time.d.ts +18 -3
  261. package/dist/utils/time.d.ts.map +1 -1
  262. package/dist/utils/time.js +24 -8
  263. package/dist/utils/time.js.map +1 -1
  264. package/dist/utils/toError.d.ts +8 -4
  265. package/dist/utils/toError.d.ts.map +1 -1
  266. package/dist/utils/toError.js +25 -5
  267. package/dist/utils/toError.js.map +1 -1
  268. package/docs/memora-api-reference.md +1235 -0
  269. package/docs/memora-/346/216/245/345/205/245/346/214/207/345/215/227.md +808 -0
  270. package/package.json +49 -22
  271. package/dist/agent/managers/memoryMutator.d.ts +0 -92
  272. package/dist/agent/managers/memoryMutator.d.ts.map +0 -1
  273. package/dist/agent/managers/memoryMutator.js +0 -94
  274. package/dist/agent/managers/memoryMutator.js.map +0 -1
@@ -1,10 +1,24 @@
1
- import { LOOP_CONSTANTS } from '../agent/constants.js';
1
+ import { AGENT_CONSTANTS, LOOP_CONSTANTS } from '../agent/constants.js';
2
2
  import { ContextManager } from '../agent/contextManager.js';
3
3
  import { runGuardrails } from '../agent/guardrail.js';
4
4
  import { NOOP_TRACER, TRACE_SPANS } from '../agent/tracer.js';
5
- import { MemoraError, isRetryableErrorCode, toError } from '../utils/errors.js';
5
+ import { MemoraError, isAbortError, isRetryableErrorCode, toError } from '../utils/errors.js';
6
6
  import { safeSetTimeout } from '../utils/safeTimer.js';
7
+ import { roundTo } from '../utils/math.js';
7
8
  import { logger } from '../logging/logger.js';
9
+ /** AgentLoop 运行时指标纯状态容器(progressive-refactor §2.1 模式 A) */
10
+ class LoopMetrics {
11
+ llmCallCount = 0;
12
+ totalInputTokens = 0;
13
+ totalOutputTokens = 0;
14
+ recallTotalCount = 0;
15
+ recallHitCount = 0;
16
+ toolCallCount = 0;
17
+ toolFailureCount = 0;
18
+ get hitRate() {
19
+ return this.recallTotalCount > 0 ? this.recallHitCount / this.recallTotalCount : 0;
20
+ }
21
+ }
8
22
  export class AgentLoop {
9
23
  opts;
10
24
  messages = [];
@@ -17,34 +31,29 @@ export class AgentLoop {
17
31
  guardrailRules;
18
32
  /** Reflection 最大重试次数(默认 2) */
19
33
  maxReflectionRetries;
34
+ /**
35
+ * 当前轮次已推送的 REFLECTION_HINT 次数(显式计数器)
36
+ *
37
+ * 替代旧实现通过 messages.filter(startsWith('[REFLECTION_HINT]')).length 推断的方式——
38
+ * 当 ContextManager 裁剪中间段消息时,REFLECTION_HINT 可能被裁掉导致计数失真。
39
+ * 显式字段不受 messages 数组变动影响,状态机更健壮。
40
+ */
41
+ reflectionCountThisTurn = 0;
20
42
  /** 宿主可覆盖的 UI 消息文本(已填充默认值) */
21
43
  ui;
44
+ /** 护栏规则正则编译失败回调(从 opts.onGuardrailError 提取,用于 GuardrailUI) */
45
+ guardrailUI;
22
46
  /** 上下文超限时是否自动生成摘要 */
23
47
  enableContextSummary;
24
48
  /** 上下文管理器(从 loop 提取的 token 估算 + 截断 + 摘要职责) */
25
49
  contextManager;
26
- // ─── 运行时指标统计字段 ──────────────────────────
27
- // 累计值,从 AgentLoop 构造起累加,供 getMetrics() 返回快照。
28
- // 设计为私有字段而非外部注入,保持 AgentLoop 自洽。
29
- /** LLM 调用总次数(含重试,每次 provider.chat 调用 +1) */
30
- metricLlmCallCount = 0;
31
- /** 累计输入 token 数(基于 estimateTokens 粗略估算) */
32
- metricTotalInputTokens = 0;
33
- /** 累计输出 token 数(基于 estimateTokens 粗略估算) */
34
- metricTotalOutputTokens = 0;
35
- /** 记忆召回总次数(每轮 processUserInput +1) */
36
- metricRecallTotalCount = 0;
37
- /** 记忆召回命中次数(召回结果非空 +1) */
38
- metricRecallHitCount = 0;
39
- /** 工具调用总次数 */
40
- metricToolCallCount = 0;
41
- /** 工具调用失败次数(结果以 [ERR 开头) */
42
- metricToolFailureCount = 0;
50
+ // ─── 运行时指标统计 ──────────────────────────────
51
+ metrics = new LoopMetrics();
43
52
  // metricTruncationCount 已移至 ContextManager.truncationCount
44
53
  constructor(opts) {
45
54
  this.opts = opts;
46
55
  this.maxIterations = opts.maxIterations ?? 20;
47
- this.maxContextTokens = opts.maxContextTokens ?? 32000;
56
+ this.maxContextTokens = opts.maxContextTokens ?? AGENT_CONSTANTS.DEFAULT_MAX_CONTEXT_TOKENS;
48
57
  this.tracer = opts.tracer ?? NOOP_TRACER;
49
58
  this.guardrailRules = opts.guardrailRules ?? [];
50
59
  this.maxReflectionRetries = opts.maxReflectionRetries ?? 2;
@@ -63,8 +72,15 @@ export class AgentLoop {
63
72
  guardrailWarningPrefix: opts.messages?.guardrailWarningPrefix ?? '[Guardrail Warning]',
64
73
  outputBlockedByGuard: opts.messages?.outputBlockedByGuard ??
65
74
  ((rule) => `Output blocked by guardrail rule "${rule}"`),
75
+ reflectionHint: opts.messages?.reflectionHint ??
76
+ ((remaining) => `[REFLECTION_HINT] 上次工具调用失败,错误可重试。请分析错误原因,修正参数后重新调用工具。剩余反思次数:${remaining}`),
66
77
  };
67
78
  this.enableContextSummary = opts.enableContextSummary ?? true;
79
+ // 护栏规则正则编译失败回调(从 opts 提取,供 GuardrailUI 使用)
80
+ this.guardrailUI = {
81
+ inputBlockedByGuard: this.ui.inputBlockedByGuard,
82
+ onRegexError: opts.onGuardrailError,
83
+ };
68
84
  // 上下文管理器(token 估算 + 截断 + 摘要)
69
85
  // 注入 tracer,让 generateContextSummary 有 span 埋点
70
86
  this.contextManager = new ContextManager({
@@ -72,6 +88,7 @@ export class AgentLoop {
72
88
  provider: opts.provider,
73
89
  contextTruncatedFn: this.ui.contextTruncated,
74
90
  tracer: this.tracer,
91
+ onContextTruncated: opts.onContextTruncated,
75
92
  });
76
93
  // 初始化 system prompt(基于永驻记忆,加前缀)
77
94
  const prefix = opts.systemPromptPrefix ?? '';
@@ -81,7 +98,13 @@ export class AgentLoop {
81
98
  });
82
99
  }
83
100
  /**
84
- * 处理一轮用户输入
101
+ * 处理一轮用户输入(编排方法)
102
+ *
103
+ * 拆分为 4 个子方法:
104
+ * - handleRecallAndInputGuard:召回注入 + 输入护栏
105
+ * - handleIteration:单次迭代编排(abort 检查 + LLM 调用 + 分支路由)
106
+ * - handleToolCalls:工具调用分支 + Reflection
107
+ * - handleTextResponse:纯文本结束 + 输出护栏
85
108
  *
86
109
  * @param userInput - 用户原始输入
87
110
  * @param recalledMemories - 记忆召回结果(Agent.memory.search() 产出),
@@ -95,168 +118,22 @@ export class AgentLoop {
95
118
  inputLength: userInput.length,
96
119
  });
97
120
  try {
98
- // 注入记忆召回结果(agent上下文组装协议 §1:Agent 记忆召回结果层)
99
- const recallSpan = this.tracer.startSpan(TRACE_SPANS.RECALL, {
100
- recallCount: recalledMemories?.length ?? 0,
101
- });
102
- // 将召回记忆以 system 消息注入(优先级高、不污染 user 输入)
103
- // 替代旧方案:嵌入 user 消息+反指令→模型易混淆
104
- if (recalledMemories?.length) {
105
- this.injectRecallAsSystem(recalledMemories);
106
- }
107
- // 补充 span 属性:让宿主监控面板能按命中/未命中过滤
108
- recallSpan.setAttribute('hit', recalledMemories !== undefined && recalledMemories.length > 0);
109
- recallSpan.end();
110
- // 召回命中率统计:每轮对话算一次召回,结果非空算命中
111
- this.metricRecallTotalCount++;
112
- if (recalledMemories && recalledMemories.length > 0) {
113
- this.metricRecallHitCount++;
114
- }
115
- // 有记忆召回时,通知上层(用于 UI 展示"召回透明度"——记忆名称 + 相似度)
116
- // 暴露 id/name/score/source 摘要,不泄露完整 content
117
- if (recalledMemories?.length) {
118
- yield {
119
- type: 'recall',
120
- memories: recalledMemories.map((m) => ({
121
- id: m.id,
122
- name: m.name,
123
- score: m.score,
124
- source: m.source,
125
- })),
126
- };
127
- }
128
- // 输入护栏检查:在用户输入注入上下文之前,检查是否命中护栏规则
129
- // 护栏自身异常时降级为"放行 + 记日志",不阻断用户对话
130
- const inputGuardResult = runGuardrails(this.guardrailRules, userInput, this.ui);
131
- if (inputGuardResult.blocked) {
132
- // P3: try/finally 确保 done 一定送达,即使 text yield 异常
133
- // guardrailBlocked: true 让 eval 框架和宿主 UI 通过结构化字段判断护栏触发
134
- try {
135
- yield {
136
- type: 'text',
137
- content: inputGuardResult.message ?? 'Input blocked by guardrail',
138
- guardrailBlocked: true,
139
- };
140
- }
141
- finally {
142
- yield { type: 'done' };
143
- }
121
+ // 1. 召回注入 + 输入护栏(返回 true 表示已 block 并 yield done,应 return)
122
+ if (yield* this.handleRecallAndInputGuard(userInput, recalledMemories))
144
123
  return;
145
- }
146
- if (inputGuardResult.warning) {
147
- // warn 级别只通知,不阻断
148
- yield {
149
- type: 'text',
150
- content: `${this.ui.guardrailWarningPrefix} ${inputGuardResult.warning}`,
151
- };
152
- }
153
- // 安全规范 §6:用户输入用 <user_input> 标签包裹,增强 LLM 对注入攻击的免疫力
124
+ // 2. 用户消息 push(安全规范 §6:用 <user_input> 标签包裹,增强 LLM 对注入攻击的免疫力)
154
125
  this.messages.push({ role: 'user', content: `<user_input>${userInput}</user_input>` });
126
+ // 重置当前轮次的反思计数器(每轮用户输入独立计算反思次数)
127
+ this.reflectionCountThisTurn = 0;
128
+ // 3. 迭代循环
155
129
  let iteration = 0;
156
130
  while (iteration < this.maxIterations) {
157
131
  iteration++;
158
- logger.debug({ iteration, messageCount: this.messages.length }, 'Agent Loop 迭代');
159
- // 每次迭代前检查是否已被取消
160
- if (signal?.aborted) {
161
- yield { type: 'aborted', reason: this.ui.abortedByUser };
162
- return;
163
- }
164
- // 调用 LLM(带重试 + 截断保护)
165
- const chatOpts = this.buildChatOptions();
166
- // 上下文摘要:如果启用且首次截断,生成摘要
167
- let contextSummary;
168
- if (this.enableContextSummary &&
169
- this.contextManager.shouldTruncate(this.messages)) {
170
- // 摘要缓存管理已移至 ContextManager.getOrCreateSummary
171
- // 传入 signal,让摘要生成可被用户取消中断(避免 generator 挂起)
172
- contextSummary = await this.contextManager.getOrCreateSummary(this.messages, signal);
173
- }
174
- const safeMessages = this.contextManager.truncateMessages(this.messages, contextSummary);
175
- // 截断后同步替换工作记忆,防止 messages 数组无限增长
176
- // 持久化由 MessageHistory 负责,工作记忆只需保留当前上下文窗口内的消息
177
- if (safeMessages !== this.messages) {
178
- this.messages = [...safeMessages];
179
- }
180
- const llmResult = yield* this.callLlmWithRetry(safeMessages, chatOpts, signal, iteration);
181
- if (llmResult.aborted) {
182
- // LLM 调用中断时仍保留已生成的部分文本到上下文消息列表
183
- // 让下一轮 LLM 能看到中断响应(追加 interrupted 标记让 LLM 识别非完整回复)
184
- // 注意:工具调用中断(execResult.aborted)不在此处理,因 executeToolCalls
185
- // 已 push assistant(含 toolCalls),追加文本标记会破坏工具调用结构
186
- if (llmResult.fullContent.trim()) {
187
- this.messages.push({
188
- role: 'assistant',
189
- content: llmResult.fullContent + this.ui.interrupted,
190
- });
191
- }
192
- yield { type: 'aborted', reason: this.ui.abortedByUser };
193
- return;
194
- }
195
- // 工具调用分支
196
- if (llmResult.toolCalls && llmResult.toolCalls.length > 0) {
197
- const execResult = yield* this.executeToolCalls(llmResult.toolCalls, llmResult.fullContent, signal);
198
- if (execResult.aborted) {
199
- yield { type: 'aborted', reason: this.ui.abortedByUser };
200
- return;
201
- }
202
- // Reflection(反思/自修正):检查是否有可重试的错误
203
- // 如果工具结果中有 retryable 错误,在 LLM 上下文中追加反思提示
204
- // 帮助 LLM 聚焦于修正而非放弃
205
- const hasRetryableError = this.messages
206
- .slice(-llmResult.toolCalls.length) // 只看本轮工具结果
207
- .some((m) => m.role === 'tool' && this.isRetryableToolError(m.content));
208
- if (hasRetryableError) {
209
- // 反思次数限制:通过前缀匹配统计已推送的 REFLECTION_HINT 消息
210
- // (实际推送的 content 带有后缀说明,需用 startsWith 而非严格相等)
211
- const reflectionHint = this.messages.filter((m) => m.role === 'system' && m.content.startsWith('[REFLECTION_HINT]')).length;
212
- if (reflectionHint < this.maxReflectionRetries) {
213
- this.messages.push({
214
- role: 'system',
215
- content: `[REFLECTION_HINT] 上次工具调用失败,错误可重试。请分析错误原因,修正参数后重新调用工具。剩余反思次数:${this.maxReflectionRetries - reflectionHint}`,
216
- });
217
- }
218
- }
219
- // 继续循环:把工具结果回填给 LLM
220
- continue;
221
- }
222
- // 纯文本结束
223
- if (llmResult.fullContent) {
224
- this.messages.push({ role: 'assistant', content: llmResult.fullContent });
225
- }
226
- else {
227
- // LLM 返回空响应(既无文本也无工具调用)的兜底处理
228
- // 正常 LLM 不会返回空响应,但某些 provider 异常/边界情况下可能发生
229
- logger.warn({ iteration }, 'LLM 返回空响应(无文本、无工具调用),使用兜底提示');
230
- const fallbackText = '(模型未返回有效内容,请重试或换一种方式提问)';
231
- this.messages.push({ role: 'assistant', content: fallbackText });
232
- yield { type: 'text', content: fallbackText };
233
- }
234
- // 输出护栏检查:在响应返回给用户之前,检查是否命中护栏规则
235
- const outputGuardResult = runGuardrails(this.guardrailRules, llmResult.fullContent, this.ui);
236
- if (outputGuardResult.blocked) {
237
- // P3: try/finally 确保 done 一定送达,即使 text yield 异常
238
- // guardrailBlocked: true 让 eval 框架和宿主 UI 通过结构化字段判断护栏触发
239
- try {
240
- yield {
241
- type: 'text',
242
- content: outputGuardResult.message ?? 'Output blocked by guardrail',
243
- guardrailBlocked: true,
244
- };
245
- }
246
- finally {
247
- yield { type: 'done' };
248
- }
132
+ const result = yield* this.handleIteration(iteration, signal);
133
+ if (result === 'aborted' || result === 'done')
249
134
  return;
250
- }
251
- if (outputGuardResult.warning) {
252
- yield {
253
- type: 'text',
254
- content: `${this.ui.guardrailWarningPrefix} ${outputGuardResult.warning}`,
255
- };
256
- }
257
- yield { type: 'done' };
258
- return;
259
135
  }
136
+ // 4. 最大迭代兜底
260
137
  logger.warn({ iterations: iteration }, '达到最大迭代次数');
261
138
  yield { type: 'text', content: this.ui.maxIterationsReached };
262
139
  yield { type: 'done' };
@@ -265,6 +142,206 @@ export class AgentLoop {
265
142
  responseSpan.end();
266
143
  }
267
144
  }
145
+ /**
146
+ * 召回注入 + 输入护栏(processUserInput 子方法 1/4)
147
+ *
148
+ * 职责:
149
+ * - 注入记忆召回结果(system 消息,优先级高、不污染 user 输入)
150
+ * - 召回命中率统计(metricRecallTotalCount / metricRecallHitCount)
151
+ * - 通知上层 UI 召回透明度(yield recall chunk)
152
+ * - 输入护栏检查(block 时 yield text + done,warn 时 yield text)
153
+ *
154
+ * @yields recall / text(guardrail block/warn)/ done(block 时)
155
+ * @returns true 表示输入被 block 已 yield done,调用方应 return;false 表示继续
156
+ */
157
+ async *handleRecallAndInputGuard(userInput, recalledMemories) {
158
+ // 注入记忆召回结果 + 统计 + 透明度通知(提取到 _injectRecall,编码约定 §6)
159
+ yield* this._injectRecall(recalledMemories);
160
+ // 输入护栏检查:在用户输入注入上下文之前,检查是否命中护栏规则
161
+ // 护栏自身异常时降级为"放行 + 记日志",不阻断用户对话
162
+ const inputGuardSpan = this.tracer.startSpan(TRACE_SPANS.GUARDRAIL_INPUT, {
163
+ ruleCount: this.guardrailRules.length,
164
+ });
165
+ const inputGuardResult = runGuardrails(this.guardrailRules, userInput, this.guardrailUI);
166
+ inputGuardSpan.setAttribute('blocked', inputGuardResult.blocked);
167
+ inputGuardSpan.setAttribute('warned', !!inputGuardResult.warning);
168
+ inputGuardSpan.end();
169
+ if (inputGuardResult.blocked) {
170
+ // P3: try/finally 确保 done 一定送达,即使 text yield 异常
171
+ // guardrailBlocked: true 让 eval 框架和宿主 UI 通过结构化字段判断护栏触发
172
+ try {
173
+ yield {
174
+ type: 'text',
175
+ content: inputGuardResult.message ?? 'Input blocked by guardrail',
176
+ guardrailBlocked: true,
177
+ };
178
+ }
179
+ finally {
180
+ yield { type: 'done' };
181
+ }
182
+ return true;
183
+ }
184
+ if (inputGuardResult.warning) {
185
+ // warn 级别只通知,不阻断
186
+ yield {
187
+ type: 'text',
188
+ content: `${this.ui.guardrailWarningPrefix} ${inputGuardResult.warning}`,
189
+ };
190
+ }
191
+ return false;
192
+ }
193
+ /**
194
+ * 单次迭代编排(processUserInput 子方法 2/4)
195
+ *
196
+ * 职责:
197
+ * - abort 检查
198
+ * - 上下文摘要 + 截断
199
+ * - LLM 调用(callLlmWithRetry)
200
+ * - LLM 中断处理
201
+ * - 分支路由:工具调用 → handleToolCalls;纯文本 → handleTextResponse
202
+ *
203
+ * @yields text / aborted / done(由子方法委托)
204
+ * @returns 'aborted' | 'done' | 'continue'(continue 表示继续下一轮迭代)
205
+ */
206
+ async *handleIteration(iteration, signal) {
207
+ logger.debug({ iteration, messageCount: this.messages.length }, 'Agent Loop 迭代');
208
+ // 每次迭代前检查是否已被取消
209
+ if (signal?.aborted) {
210
+ yield { type: 'aborted', reason: this.ui.abortedByUser };
211
+ return 'aborted';
212
+ }
213
+ // 调用 LLM(带重试 + 截断保护)
214
+ const chatOpts = this.buildChatOptions();
215
+ // 上下文摘要:如果启用且首次截断,生成摘要
216
+ let contextSummary;
217
+ if (this.enableContextSummary &&
218
+ this.contextManager.shouldTruncate(this.messages)) {
219
+ // 摘要缓存管理已移至 ContextManager.getOrCreateSummary
220
+ // 传入 signal,让摘要生成可被用户取消中断(避免 generator 挂起)
221
+ contextSummary = await this.contextManager.getOrCreateSummary(this.messages, signal);
222
+ }
223
+ const safeMessages = this.contextManager.truncateMessages(this.messages, contextSummary);
224
+ // 截断后同步替换工作记忆,防止 messages 数组无限增长
225
+ // 持久化由 MessageHistory 负责,工作记忆只需保留当前上下文窗口内的消息
226
+ if (safeMessages !== this.messages) {
227
+ this.messages = [...safeMessages];
228
+ }
229
+ const llmResult = yield* this.callLlmWithRetry(safeMessages, chatOpts, signal, iteration);
230
+ if (llmResult.aborted) {
231
+ // LLM 调用中断时仍保留已生成的部分文本到上下文消息列表
232
+ // 让下一轮 LLM 能看到中断响应(追加 interrupted 标记让 LLM 识别非完整回复)
233
+ // 注意:工具调用中断(execResult.aborted)不在此处理,因 executeToolCalls
234
+ // 已 push assistant(含 toolCalls),追加文本标记会破坏工具调用结构
235
+ if (llmResult.fullContent.trim()) {
236
+ this.messages.push({
237
+ role: 'assistant',
238
+ content: llmResult.fullContent + this.ui.interrupted,
239
+ });
240
+ }
241
+ yield { type: 'aborted', reason: this.ui.abortedByUser };
242
+ return 'aborted';
243
+ }
244
+ // 工具调用分支
245
+ if (llmResult.toolCalls && llmResult.toolCalls.length > 0) {
246
+ return yield* this.handleToolCalls(llmResult, signal);
247
+ }
248
+ // 纯文本结束分支
249
+ return yield* this.handleTextResponse(llmResult);
250
+ }
251
+ /**
252
+ * 工具调用分支 + Reflection(processUserInput 子方法 3/4)
253
+ *
254
+ * 职责:
255
+ * - executeToolCalls 执行工具调用
256
+ * - abort 检查(工具执行中断)
257
+ * - Reflection:检查可重试错误,追加反思提示
258
+ *
259
+ * @yields aborted(工具执行中断时)
260
+ * @returns 'aborted' | 'continue'(continue 表示工具结果已回填,继续下一轮 LLM 调用)
261
+ */
262
+ async *handleToolCalls(llmResult, signal) {
263
+ const execResult = yield* this.executeToolCalls(llmResult.toolCalls, llmResult.fullContent, signal);
264
+ if (execResult.aborted) {
265
+ yield { type: 'aborted', reason: this.ui.abortedByUser };
266
+ return 'aborted';
267
+ }
268
+ // Reflection(反思/自修正):检查是否有可重试的错误
269
+ // 如果工具结果中有 retryable 错误,在 LLM 上下文中追加反思提示
270
+ // 帮助 LLM 聚焦于修正而非放弃
271
+ const hasRetryableError = this.messages
272
+ .slice(-llmResult.toolCalls.length) // 只看本轮工具结果
273
+ .some((m) => m.role === 'tool' && this.isRetryableToolError(m.content));
274
+ if (hasRetryableError) {
275
+ // 反思次数限制:使用显式计数器,避免 messages 裁剪导致计数失真
276
+ if (this.reflectionCountThisTurn < this.maxReflectionRetries) {
277
+ this.reflectionCountThisTurn++;
278
+ this.messages.push({
279
+ role: 'system',
280
+ content: this.ui.reflectionHint(this.maxReflectionRetries - this.reflectionCountThisTurn),
281
+ });
282
+ }
283
+ }
284
+ // 继续循环:把工具结果回填给 LLM
285
+ return 'continue';
286
+ }
287
+ /**
288
+ * 纯文本结束 + 输出护栏(processUserInput 子方法 4/4)
289
+ *
290
+ * 职责:
291
+ * - push assistant 消息(含空响应兜底)
292
+ * - 输出护栏检查(block 时 yield text + done,warn 时 yield text)
293
+ * - yield done 结束本轮对话
294
+ *
295
+ * @yields text(空响应兜底 / guardrail block/warn)/ done
296
+ * @returns 'done'(调用方收到后 return)
297
+ */
298
+ async *handleTextResponse(llmResult) {
299
+ // 输出护栏检查:在响应返回给用户之前,检查是否命中护栏规则
300
+ // 注意:护栏检查必须在 messages.push 之前执行,否则被 block 的内容仍会进入下一轮 LLM 上下文
301
+ const outputGuardSpan = this.tracer.startSpan(TRACE_SPANS.GUARDRAIL_OUTPUT, {
302
+ ruleCount: this.guardrailRules.length,
303
+ });
304
+ const outputGuardResult = runGuardrails(this.guardrailRules, llmResult.fullContent, this.guardrailUI);
305
+ outputGuardSpan.setAttribute('blocked', outputGuardResult.blocked);
306
+ outputGuardSpan.setAttribute('warned', !!outputGuardResult.warning);
307
+ outputGuardSpan.end();
308
+ if (outputGuardResult.blocked) {
309
+ // P3: try/finally 确保 done 一定送达,即使 text yield 异常
310
+ // guardrailBlocked: true 让 eval 框架和宿主 UI 通过结构化字段判断护栏触发
311
+ // block 时不 push 到 messages——被 block 的内容不应进入下一轮 LLM 上下文
312
+ try {
313
+ yield {
314
+ type: 'text',
315
+ content: outputGuardResult.message ?? 'Output blocked by guardrail',
316
+ guardrailBlocked: true,
317
+ };
318
+ }
319
+ finally {
320
+ yield { type: 'done' };
321
+ }
322
+ return 'done';
323
+ }
324
+ // 护栏通过后再 push 到对话历史——确保被 block/warn 的内容不污染 LLM 上下文
325
+ if (llmResult.fullContent) {
326
+ this.messages.push({ role: 'assistant', content: llmResult.fullContent });
327
+ }
328
+ else {
329
+ // LLM 返回空响应(既无文本也无工具调用)的兜底处理
330
+ // 正常 LLM 不会返回空响应,但某些 provider 异常/边界情况下可能发生
331
+ logger.warn('LLM 返回空响应(无文本、无工具调用),使用兜底提示');
332
+ const fallbackText = '(模型未返回有效内容,请重试或换一种方式提问)';
333
+ this.messages.push({ role: 'assistant', content: fallbackText });
334
+ yield { type: 'text', content: fallbackText };
335
+ }
336
+ if (outputGuardResult.warning) {
337
+ yield {
338
+ type: 'text',
339
+ content: `${this.ui.guardrailWarningPrefix} ${outputGuardResult.warning}`,
340
+ };
341
+ }
342
+ yield { type: 'done' };
343
+ return 'done';
344
+ }
268
345
  /**
269
346
  * 调用 LLM(带指数退避重试)
270
347
  *
@@ -306,36 +383,10 @@ export class AgentLoop {
306
383
  break;
307
384
  }
308
385
  if (attempt > 0) {
309
- // 仅在流式输出前失败时重试(streamStarted = false)
310
- const delay = LOOP_CONSTANTS.RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
311
- logger.warn({ attempt, delay, error: lastError?.message }, 'LLM 调用失败,重试中');
312
- // 发射 retry chunk:让宿主 UI 显示"网络波动,重试中 N/M...",消除 3 秒静默
313
- yield {
314
- type: 'retry',
315
- attempt,
316
- maxRetries: LOOP_CONSTANTS.MAX_LLM_RETRIES,
317
- delayMs: delay,
318
- error: lastError?.message ?? 'unknown error',
319
- };
320
- // 重试延迟期间支持 abort:用 Promise.race 替代单纯的 setTimeout
321
- await new Promise((resolve) => {
322
- if (signal?.aborted) {
323
- resolve();
324
- return;
325
- }
326
- const timeoutId = safeSetTimeout(() => {
327
- signal?.removeEventListener('abort', onAbort);
328
- resolve();
329
- }, delay);
330
- const onAbort = () => {
331
- clearTimeout(timeoutId);
332
- resolve();
333
- };
334
- signal?.addEventListener('abort', onAbort, { once: true });
335
- });
386
+ yield* this._waitForRetryWithAbort(attempt, lastError, signal);
387
+ // Reset streaming state after retry delay
336
388
  fullContent = '';
337
389
  toolCalls = undefined;
338
- // 延迟后再次检查 abort
339
390
  if (signal?.aborted) {
340
391
  aborted = true;
341
392
  break;
@@ -343,8 +394,8 @@ export class AgentLoop {
343
394
  }
344
395
  try {
345
396
  // LLM 指标统计:每次 provider.chat 调用 +1,输入 token 累计
346
- this.metricLlmCallCount++;
347
- this.metricTotalInputTokens += this.contextManager.estimateTokens(safeMessages);
397
+ this.metrics.llmCallCount++;
398
+ this.metrics.totalInputTokens += this.contextManager.estimateTokens(safeMessages);
348
399
  // safeMessages 为 readonly Message[],provider.chat 期望 Message[];
349
400
  // 通过浅拷贝转换为可变数组,避免类型断言。
350
401
  for await (const chunk of this.opts.provider.chat([...safeMessages], effectiveOpts)) {
@@ -362,7 +413,7 @@ export class AgentLoop {
362
413
  }
363
414
  }
364
415
  // 输出 token 统计:成功时累计输出 token
365
- this.metricTotalOutputTokens += this.contextManager.estimateTokens([
416
+ this.metrics.totalOutputTokens += this.contextManager.estimateTokens([
366
417
  { role: 'assistant', content: fullContent },
367
418
  ]);
368
419
  break; // 成功,退出重试循环
@@ -372,7 +423,7 @@ export class AgentLoop {
372
423
  lastError = e;
373
424
  // AbortError 表示用户主动取消或超时中断,不重试,直接标记 aborted 退出
374
425
  // 避免用户点击停止后仍继续发起 LLM 请求,防止 UI 卡在"停止生成"状态
375
- if (e.name === 'AbortError' || (err instanceof DOMException && err.name === 'AbortError')) {
426
+ if (isAbortError(err)) {
376
427
  aborted = true;
377
428
  break;
378
429
  }
@@ -380,13 +431,13 @@ export class AgentLoop {
380
431
  // 流式已开始输出,不能重试(用户已看到部分结果),向上抛出
381
432
  llmSpan.recordException(e);
382
433
  llmSpan.end();
383
- throw err;
434
+ throw lastError;
384
435
  }
385
436
  if (attempt >= LOOP_CONSTANTS.MAX_LLM_RETRIES) {
386
437
  // 重试次数耗尽
387
438
  llmSpan.recordException(e);
388
439
  llmSpan.end();
389
- throw err;
440
+ throw lastError;
390
441
  }
391
442
  // 继续重试(超时/网络错误等在流式开始前均可重试)
392
443
  }
@@ -418,52 +469,34 @@ export class AgentLoop {
418
469
  content: fullContent,
419
470
  toolCalls,
420
471
  });
421
- // 执行工具
472
+ // 执行工具(E-803 并行优化:独立 tool_call 并发执行,事件按原始顺序 yield)
473
+ // 设计:方案 B(保持顺序的并发)
474
+ // - 并发发起所有工具执行(Promise.all,真并发,总耗时 ≈ 最慢的工具)
475
+ // - 批量 yield tool_start(UI 按 toolCallId 创建所有工具卡片)
476
+ // - 按原始顺序 push messages + yield tool_result(保证 Reflection slice(-N) 正确)
477
+ // - messages 顺序确定 → Reflection 的 slice(-toolCalls.length) 仍取到本轮完整结果
478
+ // - 宿主 UI 按 toolCallId 配对 tool_start/tool_result,不依赖严格交替顺序
479
+ // 执行前检查取消(批量,避免 abort 后还发起工具)
480
+ if (signal?.aborted) {
481
+ return { aborted: true };
482
+ }
483
+ // 1. 批量 yield tool_start + 并发发起所有工具执行
484
+ const toolPromises = [];
422
485
  for (const tc of toolCalls) {
423
- // 工具执行前检查取消
424
- if (signal?.aborted) {
425
- return { aborted: true };
426
- }
427
486
  // 工具调用统计:每次工具执行 +1
428
- this.metricToolCallCount++;
487
+ this.metrics.toolCallCount++;
429
488
  yield { type: 'tool_start', toolCallId: tc.id, name: tc.function.name, args: tc.function.arguments };
430
- // 工具执行 Span
431
- const toolSpan = this.tracer.startSpan(TRACE_SPANS.TOOL_EXEC, {
432
- toolName: tc.function.name,
433
- });
434
- // 工具执行可能因文件不存在、路径越界等原因失败
435
- // 捕获异常并转为结构化错误结果字符串,回传给 LLM 让其自行调整策略
436
- // 避免错误直接传播到 agent.chat() 导致整个对话中断
437
- // 错误结果包含 [ERR:TOOL:code] 前缀,供 Reflection 逻辑解析
438
- let result;
439
- try {
440
- // 工具执行包裹 signal 中断,避免 abort 无法中断卡住的 generator
441
- // 原 await toolExecutor 不响应 signal,工具卡住时 generator 挂起、_chatBusy 泄漏
442
- result = await this.raceToolWithSignal(tc.function.name, tc.function.arguments, signal);
443
- }
444
- catch (err) {
445
- const e = toError(err);
446
- toolSpan.recordException(e);
447
- if (err instanceof MemoraError) {
448
- const code = err.errorCode ?? 'UNKNOWN';
449
- result = `[ERR:TOOL:${code}] 错误:${err.title}${err.detail ? ` — ${err.detail}` : ''}`;
450
- logger.warn({ tool: tc.function.name, errorCode: code, title: err.title }, '工具执行失败,错误已回传给 LLM');
451
- }
452
- else {
453
- result = `[ERR:TOOL:UNKNOWN] 错误:工具执行异常 — ${e.message}`;
454
- logger.error({ tool: tc.function.name, err }, '工具执行异常');
455
- }
456
- }
457
- toolSpan.end();
458
- this.messages.push({
459
- role: 'tool',
460
- content: result,
461
- toolCallId: tc.id,
462
- });
463
- // 工具失败统计:结果以 [ERR 开头算失败
464
- if (result.startsWith('[ERR')) {
465
- this.metricToolFailureCount++;
466
- }
489
+ // 并发发起工具执行(不 await,收集 Promise 由 Promise.all 统一等待)
490
+ toolPromises.push(this.executeOneTool(tc, signal));
491
+ }
492
+ // 2. 等待全部工具完成(真并发,总耗时 ≈ 最慢的工具而非所有工具之和)
493
+ const results = await Promise.all(toolPromises);
494
+ // 3. 处理工具消息与失败统计
495
+ this._processToolResults(toolCalls, results);
496
+ // 4. 按原始顺序 yield tool_result
497
+ for (let i = 0; i < toolCalls.length; i++) {
498
+ const tc = toolCalls[i];
499
+ const result = results[i];
467
500
  yield {
468
501
  type: 'tool_result',
469
502
  toolCallId: tc.id,
@@ -513,17 +546,74 @@ export class AgentLoop {
513
546
  return '[ERR:TOOL:ABORTED] 错误:工具执行被中断';
514
547
  }
515
548
  // 创建 abort 监听 Promise(signal abort 时 resolve 错误字符串)
516
- // 注意:工具先完成时,abort 监听器会残留在 signal 上直到 signal 触发或被 GC,
517
- // 但 { once: true } 保证只触发一次,且无副作用,可接受。
549
+ // onAbort 提到外层,便于 race 结束后清理监听器
550
+ let onAbort = null;
518
551
  const abortPromise = new Promise((resolve) => {
519
- const onAbort = () => resolve('[ERR:TOOL:ABORTED] 错误:工具执行被中断');
552
+ onAbort = () => resolve('[ERR:TOOL:ABORTED] 错误:工具执行被中断');
520
553
  signal.addEventListener('abort', onAbort, { once: true });
521
554
  });
522
555
  // Promise.race 竞争:工具先完成返回结果,signal 先 abort 返回错误字符串
556
+ // race 结束后清理监听器,避免 N 次并发工具调用累积 N 个残留监听器
557
+ // ({ once: true } 只保证触发一次,不保证未触发时被移除)
523
558
  return Promise.race([
524
559
  this.opts.toolExecutor(name, args),
525
560
  abortPromise,
526
- ]);
561
+ ]).finally(() => {
562
+ if (onAbort)
563
+ signal.removeEventListener('abort', onAbort);
564
+ });
565
+ }
566
+ /**
567
+ * 执行单个工具(E-803 抽取:为并行化提供独立执行单元)
568
+ *
569
+ * 职责:
570
+ * - startSpan / endSpan(工具执行 Span,并发时 span 时间重叠,可观测性改进)
571
+ * - raceToolWithSignal 竞争包裹(兼容 signal 中断,每个工具独立 race)
572
+ * - 异常捕获并转为结构化错误字符串([ERR:TOOL:code] 前缀,供 Reflection 解析)
573
+ *
574
+ * 不含职责(由 executeToolCalls 主循环控制,保证顺序确定):
575
+ * - yield tool_start / tool_result(事件顺序由主循环批量 yield 保证)
576
+ * - messages.push(消息顺序由主循环按原始顺序 push,确保 Reflection slice 正确)
577
+ * - metricToolCallCount / metricToolFailureCount(统计由主循环控制)
578
+ *
579
+ * 并发安全:本方法无共享状态,多个 executeOneTool 可同时执行。
580
+ * toolExecutor 内部无状态(纯分发 + 参数校验),天然支持并发调用。
581
+ *
582
+ * @param tc 单个工具调用描述(id + function.name + function.arguments)
583
+ * @param signal 可选的 AbortSignal
584
+ * @returns 工具结果字符串(成功)或 [ERR:TOOL:code] 错误字符串(失败)
585
+ */
586
+ async executeOneTool(tc, signal) {
587
+ // 工具执行 Span(并发时多个 span 时间重叠,tracer 可观测并发度)
588
+ const toolSpan = this.tracer.startSpan(TRACE_SPANS.TOOL_EXEC, {
589
+ toolName: tc.function.name,
590
+ });
591
+ try {
592
+ // 工具执行包裹 signal 中断,避免 abort 无法中断卡住的 generator
593
+ // raceToolWithSignal 天然兼容并发:每个调用独立 race,{ once: true } 监听器无副作用
594
+ return await this.raceToolWithSignal(tc.function.name, tc.function.arguments, signal);
595
+ }
596
+ catch (err) {
597
+ // 工具执行可能因文件不存在、路径越界等原因失败
598
+ // 捕获异常并转为结构化错误结果字符串,回传给 LLM 让其自行调整策略
599
+ // 避免错误直接传播到 agent.chat() 导致整个对话中断
600
+ const e = toError(err);
601
+ toolSpan.recordException(e);
602
+ if (err instanceof MemoraError) {
603
+ const code = err.errorCode ?? 'UNKNOWN';
604
+ const result = `[ERR:TOOL:${code}] 错误:${err.title}${err.detail ? ` — ${err.detail}` : ''}`;
605
+ logger.warn({ tool: tc.function.name, errorCode: code, title: err.title }, '工具执行失败,错误已回传给 LLM');
606
+ return result;
607
+ }
608
+ else {
609
+ const result = `[ERR:TOOL:UNKNOWN] 错误:工具执行异常 — ${e.message}`;
610
+ logger.error({ tool: tc.function.name, err }, '工具执行异常');
611
+ return result;
612
+ }
613
+ }
614
+ finally {
615
+ toolSpan.end();
616
+ }
527
617
  }
528
618
  /**
529
619
  * 构建 system prompt(注入人格 + 规则 + 领域 + 工具描述)
@@ -544,6 +634,12 @@ export class AgentLoop {
544
634
  })
545
635
  .join('\n');
546
636
  prompt += `\n\n## 可用工具\n\n你可以通过 tool_call 调用以下工具:\n${toolDescs}`;
637
+ // 工具选择规则:肯定式引导,放在工具描述之后作为 LLM 选工具时的决策依据
638
+ prompt += `\n\n## 工具选择规则(必须遵守)\n\n`
639
+ + `- 创建/修改角色(Persona)→ 必须使用 create_persona,禁止使用 write_file\n`
640
+ + `- 创建/修改技能(Skill)→ 必须使用 create_skill,禁止使用 write_file\n`
641
+ + `- 创建/修改规则(Rule)→ 必须使用 create_rule,禁止使用 write_file\n`
642
+ + `- 以上三种配置文件的任何操作,永远不要使用 write_file 工具`;
547
643
  }
548
644
  return prompt;
549
645
  }
@@ -643,6 +739,27 @@ export class AgentLoop {
643
739
  this.opts.systemPromptPrefix = newPrefix;
644
740
  this.rebuildSystemMessage();
645
741
  }
742
+ /**
743
+ * 刷新 bootstrap 记忆段(设定面板 CRUD 专用)
744
+ *
745
+ * 设定面板对 rule/skill 执行增删改后,调用此方法用最新的记忆数组
746
+ * 重建 system prompt 中的 bootstrap 段,使变更立即对当前会话生效。
747
+ *
748
+ * 与 refreshPersonaPrefix 的区别:
749
+ * - refreshPersonaPrefix 替换 systemPromptPrefix(角色 + 画像)
750
+ * - refreshBootstrapMemories 替换 bootstrapMemories(rule + skill)
751
+ *
752
+ * 调用链:ConfigManager.deleteRule/updateRule/deleteSkill
753
+ * → refreshBootstrapMemories 回调(assembler 注入)
754
+ * → loop.refreshBootstrapMemories(memories)
755
+ * → rebuildSystemMessage()
756
+ *
757
+ * @param memories 最新的 rule + skill 活跃记忆数组(由 ConfigManager.getBootstrapMemories 提供)
758
+ */
759
+ refreshBootstrapMemories(memories) {
760
+ this.opts.bootstrapMemories = memories;
761
+ this.rebuildSystemMessage();
762
+ }
646
763
  /**
647
764
  * 注入情感基调到 system prompt(Phase 2.1:AffectController)
648
765
  *
@@ -688,24 +805,20 @@ export class AgentLoop {
688
805
  * @returns AgentMetrics 快照(decay 字段为 null,由 Agent 层填充)
689
806
  */
690
807
  getMetrics() {
691
- // 计算召回命中率:totalCount 为 0 时返回 0,避免除零
692
- const hitRate = this.metricRecallTotalCount > 0
693
- ? this.metricRecallHitCount / this.metricRecallTotalCount
694
- : 0;
695
808
  return {
696
809
  llm: {
697
- callCount: this.metricLlmCallCount,
698
- totalInputTokens: this.metricTotalInputTokens,
699
- totalOutputTokens: this.metricTotalOutputTokens,
810
+ callCount: this.metrics.llmCallCount,
811
+ totalInputTokens: this.metrics.totalInputTokens,
812
+ totalOutputTokens: this.metrics.totalOutputTokens,
700
813
  },
701
814
  recall: {
702
- totalCount: this.metricRecallTotalCount,
703
- hitCount: this.metricRecallHitCount,
704
- hitRate: Math.round(hitRate * 1000) / 1000, // 保留 3 位小数
815
+ totalCount: this.metrics.recallTotalCount,
816
+ hitCount: this.metrics.recallHitCount,
817
+ hitRate: roundTo(this.metrics.hitRate, 3), // 保留 3 位小数
705
818
  },
706
819
  tools: {
707
- callCount: this.metricToolCallCount,
708
- failureCount: this.metricToolFailureCount,
820
+ callCount: this.metrics.toolCallCount,
821
+ failureCount: this.metrics.toolFailureCount,
709
822
  },
710
823
  context: {
711
824
  truncationCount: this.contextManager.truncationCount,
@@ -787,12 +900,78 @@ export class AgentLoop {
787
900
  const match = result.match(/^\[ERR:TOOL:(\w+)\]/);
788
901
  if (!match)
789
902
  return false;
790
- // regex 捕获组保证 match[1] 非空,但使用空值兜底避免非空断言
791
903
  const codeStr = match[1] ?? '';
792
904
  if (!codeStr)
793
905
  return false;
794
906
  const code = codeStr;
795
907
  return isRetryableErrorCode(code);
796
908
  }
909
+ /**
910
+ * 重试延迟 + abort 支持(从 callLlmWithRetry 提取,编码约定 §6 ≤60 行约束)
911
+ *
912
+ * 发射 retry chunk、等待指数退避延迟(支持中途 abort),返回 void。
913
+ * 调用方在延迟后自行检查 signal.aborted 决定是否退出重试循环。
914
+ */
915
+ async *_waitForRetryWithAbort(attempt, lastError, signal) {
916
+ const delay = LOOP_CONSTANTS.RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
917
+ logger.warn({ attempt, delay, error: lastError?.message }, 'LLM 调用失败,重试中');
918
+ yield {
919
+ type: 'retry',
920
+ attempt,
921
+ maxRetries: LOOP_CONSTANTS.MAX_LLM_RETRIES,
922
+ delayMs: delay,
923
+ error: lastError?.message ?? 'unknown error',
924
+ };
925
+ await new Promise((resolve) => {
926
+ if (signal?.aborted) {
927
+ resolve();
928
+ return;
929
+ }
930
+ const timeoutId = safeSetTimeout(() => {
931
+ signal?.removeEventListener('abort', onAbort);
932
+ resolve();
933
+ }, delay);
934
+ const onAbort = () => { clearTimeout(timeoutId); resolve(); };
935
+ signal?.addEventListener('abort', onAbort, { once: true });
936
+ });
937
+ }
938
+ /**
939
+ * 处理工具执行结果:push messages + yield tool_result(从 executeToolCalls 提取)
940
+ */
941
+ _processToolResults(toolCalls, results) {
942
+ for (let i = 0; i < toolCalls.length; i++) {
943
+ const tc = toolCalls[i];
944
+ const result = results[i];
945
+ this.messages.push({ role: 'tool', content: result, toolCallId: tc.id });
946
+ if (result.startsWith('[ERR')) {
947
+ this.metrics.toolFailureCount++;
948
+ }
949
+ }
950
+ }
951
+ /**
952
+ * 注入记忆召回结果 + 统计 + 透明度通知(从 handleRecallAndInputGuard 提取)
953
+ */
954
+ async *_injectRecall(recalledMemories) {
955
+ const recallSpan = this.tracer.startSpan(TRACE_SPANS.RECALL, {
956
+ recallCount: recalledMemories?.length ?? 0,
957
+ });
958
+ if (recalledMemories?.length) {
959
+ this.injectRecallAsSystem(recalledMemories);
960
+ }
961
+ recallSpan.setAttribute('hit', recalledMemories !== undefined && recalledMemories.length > 0);
962
+ recallSpan.end();
963
+ this.metrics.recallTotalCount++;
964
+ if (recalledMemories && recalledMemories.length > 0) {
965
+ this.metrics.recallHitCount++;
966
+ }
967
+ if (recalledMemories?.length) {
968
+ yield {
969
+ type: 'recall',
970
+ memories: recalledMemories.map((m) => ({
971
+ id: m.id, name: m.name, score: m.score, source: m.source,
972
+ })),
973
+ };
974
+ }
975
+ }
797
976
  }
798
977
  //# sourceMappingURL=loop.js.map