@zooique/memora 0.1.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 (239) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/agent/agent.d.ts +343 -0
  4. package/dist/agent/agent.d.ts.map +1 -0
  5. package/dist/agent/agent.js +893 -0
  6. package/dist/agent/agent.js.map +1 -0
  7. package/dist/agent/assembler.d.ts +77 -0
  8. package/dist/agent/assembler.d.ts.map +1 -0
  9. package/dist/agent/assembler.js +115 -0
  10. package/dist/agent/assembler.js.map +1 -0
  11. package/dist/agent/builtinToolHandlers.d.ts +96 -0
  12. package/dist/agent/builtinToolHandlers.d.ts.map +1 -0
  13. package/dist/agent/builtinToolHandlers.js +388 -0
  14. package/dist/agent/builtinToolHandlers.js.map +1 -0
  15. package/dist/agent/builtinTools.d.ts +35 -0
  16. package/dist/agent/builtinTools.d.ts.map +1 -0
  17. package/dist/agent/builtinTools.js +75 -0
  18. package/dist/agent/builtinTools.js.map +1 -0
  19. package/dist/agent/constants.d.ts +67 -0
  20. package/dist/agent/constants.d.ts.map +1 -0
  21. package/dist/agent/constants.js +67 -0
  22. package/dist/agent/constants.js.map +1 -0
  23. package/dist/agent/contextManager.d.ts +130 -0
  24. package/dist/agent/contextManager.d.ts.map +1 -0
  25. package/dist/agent/contextManager.js +287 -0
  26. package/dist/agent/contextManager.js.map +1 -0
  27. package/dist/agent/loop.d.ts +288 -0
  28. package/dist/agent/loop.d.ts.map +1 -0
  29. package/dist/agent/loop.js +756 -0
  30. package/dist/agent/loop.js.map +1 -0
  31. package/dist/agent/managers/autoConfigRefiner.d.ts +39 -0
  32. package/dist/agent/managers/autoConfigRefiner.d.ts.map +1 -0
  33. package/dist/agent/managers/autoConfigRefiner.js +150 -0
  34. package/dist/agent/managers/autoConfigRefiner.js.map +1 -0
  35. package/dist/agent/managers/configManager.d.ts +114 -0
  36. package/dist/agent/managers/configManager.d.ts.map +1 -0
  37. package/dist/agent/managers/configManager.js +186 -0
  38. package/dist/agent/managers/configManager.js.map +1 -0
  39. package/dist/agent/managers/insightExtractor.d.ts +141 -0
  40. package/dist/agent/managers/insightExtractor.d.ts.map +1 -0
  41. package/dist/agent/managers/insightExtractor.js +420 -0
  42. package/dist/agent/managers/insightExtractor.js.map +1 -0
  43. package/dist/agent/managers/memoryAdvisor.d.ts +96 -0
  44. package/dist/agent/managers/memoryAdvisor.d.ts.map +1 -0
  45. package/dist/agent/managers/memoryAdvisor.js +198 -0
  46. package/dist/agent/managers/memoryAdvisor.js.map +1 -0
  47. package/dist/agent/managers/memoryInspector.d.ts +231 -0
  48. package/dist/agent/managers/memoryInspector.d.ts.map +1 -0
  49. package/dist/agent/managers/memoryInspector.js +327 -0
  50. package/dist/agent/managers/memoryInspector.js.map +1 -0
  51. package/dist/agent/managers/sessionManager.d.ts +89 -0
  52. package/dist/agent/managers/sessionManager.d.ts.map +1 -0
  53. package/dist/agent/managers/sessionManager.js +178 -0
  54. package/dist/agent/managers/sessionManager.js.map +1 -0
  55. package/dist/agent/managers/userFactExtractor.d.ts +25 -0
  56. package/dist/agent/managers/userFactExtractor.d.ts.map +1 -0
  57. package/dist/agent/managers/userFactExtractor.js +81 -0
  58. package/dist/agent/managers/userFactExtractor.js.map +1 -0
  59. package/dist/agent/managers/workProjection.d.ts +117 -0
  60. package/dist/agent/managers/workProjection.d.ts.map +1 -0
  61. package/dist/agent/managers/workProjection.js +290 -0
  62. package/dist/agent/managers/workProjection.js.map +1 -0
  63. package/dist/agent/messageHistory.d.ts +157 -0
  64. package/dist/agent/messageHistory.d.ts.map +1 -0
  65. package/dist/agent/messageHistory.js +288 -0
  66. package/dist/agent/messageHistory.js.map +1 -0
  67. package/dist/agent/toolExecutor.d.ts +137 -0
  68. package/dist/agent/toolExecutor.d.ts.map +1 -0
  69. package/dist/agent/toolExecutor.js +209 -0
  70. package/dist/agent/toolExecutor.js.map +1 -0
  71. package/dist/agent/tracer.d.ts +122 -0
  72. package/dist/agent/tracer.d.ts.map +1 -0
  73. package/dist/agent/tracer.js +64 -0
  74. package/dist/agent/tracer.js.map +1 -0
  75. package/dist/agent/types.d.ts +98 -0
  76. package/dist/agent/types.d.ts.map +1 -0
  77. package/dist/agent/types.js +19 -0
  78. package/dist/agent/types.js.map +1 -0
  79. package/dist/config/loader.d.ts +229 -0
  80. package/dist/config/loader.d.ts.map +1 -0
  81. package/dist/config/loader.js +194 -0
  82. package/dist/config/loader.js.map +1 -0
  83. package/dist/eval/evalTypes.d.ts +118 -0
  84. package/dist/eval/evalTypes.d.ts.map +1 -0
  85. package/dist/eval/evalTypes.js +102 -0
  86. package/dist/eval/evalTypes.js.map +1 -0
  87. package/dist/index.d.ts +61 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +44 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/llm/embedding.d.ts +62 -0
  92. package/dist/llm/embedding.d.ts.map +1 -0
  93. package/dist/llm/embedding.js +162 -0
  94. package/dist/llm/embedding.js.map +1 -0
  95. package/dist/llm/factory.d.ts +39 -0
  96. package/dist/llm/factory.d.ts.map +1 -0
  97. package/dist/llm/factory.js +108 -0
  98. package/dist/llm/factory.js.map +1 -0
  99. package/dist/llm/openaiCompatible.d.ts +63 -0
  100. package/dist/llm/openaiCompatible.d.ts.map +1 -0
  101. package/dist/llm/openaiCompatible.js +340 -0
  102. package/dist/llm/openaiCompatible.js.map +1 -0
  103. package/dist/llm/provider.d.ts +91 -0
  104. package/dist/llm/provider.d.ts.map +1 -0
  105. package/dist/llm/provider.js +14 -0
  106. package/dist/llm/provider.js.map +1 -0
  107. package/dist/llm/types.d.ts +25 -0
  108. package/dist/llm/types.d.ts.map +1 -0
  109. package/dist/llm/types.js +7 -0
  110. package/dist/llm/types.js.map +1 -0
  111. package/dist/logging/logger.d.ts +39 -0
  112. package/dist/logging/logger.d.ts.map +1 -0
  113. package/dist/logging/logger.js +279 -0
  114. package/dist/logging/logger.js.map +1 -0
  115. package/dist/logging/loggerInterface.d.ts +33 -0
  116. package/dist/logging/loggerInterface.d.ts.map +1 -0
  117. package/dist/logging/loggerInterface.js +2 -0
  118. package/dist/logging/loggerInterface.js.map +1 -0
  119. package/dist/memory/inMemoryRelationStore.d.ts +51 -0
  120. package/dist/memory/inMemoryRelationStore.d.ts.map +1 -0
  121. package/dist/memory/inMemoryRelationStore.js +65 -0
  122. package/dist/memory/inMemoryRelationStore.js.map +1 -0
  123. package/dist/memory/inMemoryStorage.d.ts +97 -0
  124. package/dist/memory/inMemoryStorage.d.ts.map +1 -0
  125. package/dist/memory/inMemoryStorage.js +177 -0
  126. package/dist/memory/inMemoryStorage.js.map +1 -0
  127. package/dist/memory/loader.d.ts +49 -0
  128. package/dist/memory/loader.d.ts.map +1 -0
  129. package/dist/memory/loader.js +93 -0
  130. package/dist/memory/loader.js.map +1 -0
  131. package/dist/memory/projectManager.d.ts +182 -0
  132. package/dist/memory/projectManager.d.ts.map +1 -0
  133. package/dist/memory/projectManager.js +441 -0
  134. package/dist/memory/projectManager.js.map +1 -0
  135. package/dist/memory/recall.d.ts +77 -0
  136. package/dist/memory/recall.d.ts.map +1 -0
  137. package/dist/memory/recall.js +147 -0
  138. package/dist/memory/recall.js.map +1 -0
  139. package/dist/memory/relationStore.d.ts +78 -0
  140. package/dist/memory/relationStore.d.ts.map +1 -0
  141. package/dist/memory/relationStore.js +2 -0
  142. package/dist/memory/relationStore.js.map +1 -0
  143. package/dist/memory/sessionStore.d.ts +84 -0
  144. package/dist/memory/sessionStore.d.ts.map +1 -0
  145. package/dist/memory/sessionStore.js +2 -0
  146. package/dist/memory/sessionStore.js.map +1 -0
  147. package/dist/memory/storageInterface.d.ts +107 -0
  148. package/dist/memory/storageInterface.d.ts.map +1 -0
  149. package/dist/memory/storageInterface.js +2 -0
  150. package/dist/memory/storageInterface.js.map +1 -0
  151. package/dist/memory/store.d.ts +50 -0
  152. package/dist/memory/store.d.ts.map +1 -0
  153. package/dist/memory/store.js +160 -0
  154. package/dist/memory/store.js.map +1 -0
  155. package/dist/memory/types.d.ts +189 -0
  156. package/dist/memory/types.d.ts.map +1 -0
  157. package/dist/memory/types.js +230 -0
  158. package/dist/memory/types.js.map +1 -0
  159. package/dist/memory/userProfile.d.ts +156 -0
  160. package/dist/memory/userProfile.d.ts.map +1 -0
  161. package/dist/memory/userProfile.js +315 -0
  162. package/dist/memory/userProfile.js.map +1 -0
  163. package/dist/memory/vectorStore.d.ts +75 -0
  164. package/dist/memory/vectorStore.d.ts.map +1 -0
  165. package/dist/memory/vectorStore.js +144 -0
  166. package/dist/memory/vectorStore.js.map +1 -0
  167. package/dist/persona/personaManager.d.ts +121 -0
  168. package/dist/persona/personaManager.d.ts.map +1 -0
  169. package/dist/persona/personaManager.js +349 -0
  170. package/dist/persona/personaManager.js.map +1 -0
  171. package/dist/persona/types.d.ts +32 -0
  172. package/dist/persona/types.d.ts.map +1 -0
  173. package/dist/persona/types.js +5 -0
  174. package/dist/persona/types.js.map +1 -0
  175. package/dist/security/pathGuard.d.ts +121 -0
  176. package/dist/security/pathGuard.d.ts.map +1 -0
  177. package/dist/security/pathGuard.js +276 -0
  178. package/dist/security/pathGuard.js.map +1 -0
  179. package/dist/skill/skillManager.d.ts +82 -0
  180. package/dist/skill/skillManager.d.ts.map +1 -0
  181. package/dist/skill/skillManager.js +198 -0
  182. package/dist/skill/skillManager.js.map +1 -0
  183. package/dist/skill/types.d.ts +28 -0
  184. package/dist/skill/types.d.ts.map +1 -0
  185. package/dist/skill/types.js +5 -0
  186. package/dist/skill/types.js.map +1 -0
  187. package/dist/utils/errors.d.ts +86 -0
  188. package/dist/utils/errors.d.ts.map +1 -0
  189. package/dist/utils/errors.js +143 -0
  190. package/dist/utils/errors.js.map +1 -0
  191. package/dist/utils/eventEmitter.d.ts +87 -0
  192. package/dist/utils/eventEmitter.d.ts.map +1 -0
  193. package/dist/utils/eventEmitter.js +79 -0
  194. package/dist/utils/eventEmitter.js.map +1 -0
  195. package/dist/utils/frontmatter.d.ts +24 -0
  196. package/dist/utils/frontmatter.d.ts.map +1 -0
  197. package/dist/utils/frontmatter.js +44 -0
  198. package/dist/utils/frontmatter.js.map +1 -0
  199. package/dist/utils/json.d.ts +20 -0
  200. package/dist/utils/json.d.ts.map +1 -0
  201. package/dist/utils/json.js +65 -0
  202. package/dist/utils/json.js.map +1 -0
  203. package/dist/utils/loggerHolder.d.ts +37 -0
  204. package/dist/utils/loggerHolder.d.ts.map +1 -0
  205. package/dist/utils/loggerHolder.js +49 -0
  206. package/dist/utils/loggerHolder.js.map +1 -0
  207. package/dist/utils/math.d.ts +5 -0
  208. package/dist/utils/math.d.ts.map +1 -0
  209. package/dist/utils/math.js +19 -0
  210. package/dist/utils/math.js.map +1 -0
  211. package/dist/utils/path.d.ts +28 -0
  212. package/dist/utils/path.d.ts.map +1 -0
  213. package/dist/utils/path.js +33 -0
  214. package/dist/utils/path.js.map +1 -0
  215. package/dist/utils/safeTimer.d.ts +26 -0
  216. package/dist/utils/safeTimer.d.ts.map +1 -0
  217. package/dist/utils/safeTimer.js +49 -0
  218. package/dist/utils/safeTimer.js.map +1 -0
  219. package/dist/utils/scanner.d.ts +54 -0
  220. package/dist/utils/scanner.d.ts.map +1 -0
  221. package/dist/utils/scanner.js +115 -0
  222. package/dist/utils/scanner.js.map +1 -0
  223. package/dist/utils/segmenter.d.ts +30 -0
  224. package/dist/utils/segmenter.d.ts.map +1 -0
  225. package/dist/utils/segmenter.js +80 -0
  226. package/dist/utils/segmenter.js.map +1 -0
  227. package/dist/utils/strings.d.ts +18 -0
  228. package/dist/utils/strings.d.ts.map +1 -0
  229. package/dist/utils/strings.js +25 -0
  230. package/dist/utils/strings.js.map +1 -0
  231. package/dist/utils/time.d.ts +23 -0
  232. package/dist/utils/time.d.ts.map +1 -0
  233. package/dist/utils/time.js +31 -0
  234. package/dist/utils/time.js.map +1 -0
  235. package/dist/utils/toError.d.ts +13 -0
  236. package/dist/utils/toError.d.ts.map +1 -0
  237. package/dist/utils/toError.js +22 -0
  238. package/dist/utils/toError.js.map +1 -0
  239. package/package.json +73 -0
@@ -0,0 +1,756 @@
1
+ import { LOOP_CONSTANTS } from '../agent/constants.js';
2
+ import { ContextManager } from '../agent/contextManager.js';
3
+ import { NOOP_TRACER, TRACE_SPANS } from '../agent/tracer.js';
4
+ import { MemoraError, isRetryableErrorCode, toError } from '../utils/errors.js';
5
+ import { logger } from '../logging/logger.js';
6
+ export class AgentLoop {
7
+ opts;
8
+ messages = [];
9
+ maxIterations;
10
+ /** 上下文窗口 token 上限(默认 8000,约 32K 中文字符) */
11
+ maxContextTokens;
12
+ /** 可观测性 Tracer(默认 NOOP_TRACER 零开销) */
13
+ tracer;
14
+ /** 内容护栏规则(启动时加载,运行时不可变) */
15
+ guardrailRules;
16
+ /** Reflection 最大重试次数(默认 2) */
17
+ maxReflectionRetries;
18
+ /** 宿主可覆盖的 UI 消息文本(已填充默认值) */
19
+ ui;
20
+ /** 上下文超限时是否自动生成摘要 */
21
+ enableContextSummary;
22
+ /** 上下文管理器(QC-R2-08:从 loop 提取的 token 估算 + 截断 + 摘要职责) */
23
+ contextManager;
24
+ // ─── R-103 运行时指标统计字段 ──────────────────────────
25
+ // 累计值,从 AgentLoop 构造起累加,供 getMetrics() 返回快照。
26
+ // 设计为私有字段而非外部注入,保持 AgentLoop 自洽。
27
+ /** LLM 调用总次数(含重试,每次 provider.chat 调用 +1) */
28
+ metricLlmCallCount = 0;
29
+ /** 累计输入 token 数(基于 estimateTokens 粗略估算) */
30
+ metricTotalInputTokens = 0;
31
+ /** 累计输出 token 数(基于 estimateTokens 粗略估算) */
32
+ metricTotalOutputTokens = 0;
33
+ /** 记忆召回总次数(每轮 processUserInput +1) */
34
+ metricRecallTotalCount = 0;
35
+ /** 记忆召回命中次数(召回结果非空 +1) */
36
+ metricRecallHitCount = 0;
37
+ /** 工具调用总次数 */
38
+ metricToolCallCount = 0;
39
+ /** 工具调用失败次数(结果以 [ERR 开头) */
40
+ metricToolFailureCount = 0;
41
+ // metricTruncationCount 已移至 ContextManager.truncationCount(QC-R2-08)
42
+ constructor(opts) {
43
+ this.opts = opts;
44
+ this.maxIterations = opts.maxIterations ?? 20;
45
+ this.maxContextTokens = opts.maxContextTokens ?? 32000;
46
+ this.tracer = opts.tracer ?? NOOP_TRACER;
47
+ this.guardrailRules = opts.guardrailRules ?? [];
48
+ this.maxReflectionRetries = opts.maxReflectionRetries ?? 2;
49
+ this.ui = {
50
+ abortedByUser: opts.messages?.abortedByUser ?? 'User cancelled the conversation',
51
+ maxIterationsReached: opts.messages?.maxIterationsReached ?? '\n\n[Max iterations reached]',
52
+ contextTruncated: opts.messages?.contextTruncated ??
53
+ ((skipped, kept) => `[Context window management] ${skipped} earlier messages have been trimmed to maintain conversation flow. ${kept} recent messages are preserved along with the full system prompt. Ask the user if you need to review earlier content.`),
54
+ recentConversationLabel: opts.messages?.recentConversationLabel ?? '[Recent conversation]',
55
+ userLabel: opts.messages?.userLabel ?? 'User',
56
+ assistantLabel: opts.messages?.assistantLabel ?? 'Assistant',
57
+ inputBlockedByGuard: opts.messages?.inputBlockedByGuard ??
58
+ ((rule) => `Input blocked by guardrail rule "${rule}"`),
59
+ guardrailWarningPrefix: opts.messages?.guardrailWarningPrefix ?? '[Guardrail Warning]',
60
+ outputBlockedByGuard: opts.messages?.outputBlockedByGuard ??
61
+ ((rule) => `Output blocked by guardrail rule "${rule}"`),
62
+ };
63
+ this.enableContextSummary = opts.enableContextSummary ?? true;
64
+ // QC-R2-08:上下文管理器(token 估算 + 截断 + 摘要)
65
+ this.contextManager = new ContextManager({
66
+ maxContextTokens: this.maxContextTokens,
67
+ provider: opts.provider,
68
+ contextTruncatedFn: this.ui.contextTruncated,
69
+ });
70
+ // 初始化 system prompt(基于永驻记忆,加前缀)
71
+ const prefix = opts.systemPromptPrefix ?? '';
72
+ this.messages.push({
73
+ role: 'system',
74
+ content: prefix + this.buildSystemPrompt(opts.bootstrapMemories),
75
+ });
76
+ }
77
+ /**
78
+ * 处理一轮用户输入
79
+ *
80
+ * @param userInput - 用户原始输入
81
+ * @param recalledMemories - 记忆召回结果(Agent.memory.search() 产出),
82
+ * 可选。传入时自动注入到上下文,实现"Agent 记忆召回结果"层
83
+ * @param signal - 可选的 AbortSignal,用于取消正在进行的对话(V-105)
84
+ * 泊文等宿主 UI 传入 AbortController.signal,用户点击"取消"时触发 abort
85
+ */
86
+ async *processUserInput(userInput, recalledMemories, signal) {
87
+ // 创建顶层 response span,由 try/finally 统一管理生命周期
88
+ const responseSpan = this.tracer.startSpan(TRACE_SPANS.RESPONSE, {
89
+ inputLength: userInput.length,
90
+ });
91
+ try {
92
+ // 注入记忆召回结果(agent上下文组装协议 §1:Agent 记忆召回结果层)
93
+ const recallSpan = this.tracer.startSpan(TRACE_SPANS.RECALL, {
94
+ recallCount: recalledMemories?.length ?? 0,
95
+ });
96
+ // 将召回记忆以 system 消息注入(优先级高、不污染 user 输入)
97
+ // 替代旧方案:嵌入 user 消息+反指令→模型易混淆
98
+ if (recalledMemories?.length) {
99
+ this.injectRecallAsSystem(recalledMemories);
100
+ }
101
+ const userInputClean = userInput;
102
+ // R-103 补充 span 属性:让宿主监控面板能按命中/未命中过滤
103
+ recallSpan.setAttribute('hit', recalledMemories !== undefined && recalledMemories.length > 0);
104
+ recallSpan.end();
105
+ // R-103 召回命中率统计:每轮对话算一次召回,结果非空算命中
106
+ this.metricRecallTotalCount++;
107
+ if (recalledMemories && recalledMemories.length > 0) {
108
+ this.metricRecallHitCount++;
109
+ }
110
+ // 有记忆召回时,通知上层(用于 UI 展示"召回透明度"——记忆名称 + 相似度)
111
+ // 仅暴露 name/score/source 摘要,不泄露完整 content
112
+ if (recalledMemories?.length) {
113
+ yield {
114
+ type: 'recall',
115
+ memories: recalledMemories.map((m) => ({
116
+ name: m.name,
117
+ score: m.score,
118
+ source: m.source,
119
+ })),
120
+ };
121
+ }
122
+ // 输入护栏检查:在用户输入注入上下文之前,检查是否命中护栏规则
123
+ // 护栏自身异常时降级为"放行 + 记日志",不阻断用户对话
124
+ const inputGuardResult = this.runInputGuardrails(userInput);
125
+ if (inputGuardResult.blocked) {
126
+ // P3: try/finally 确保 done 一定送达,即使 text yield 异常
127
+ try {
128
+ yield { type: 'text', content: inputGuardResult.message ?? 'Input blocked by guardrail' };
129
+ }
130
+ finally {
131
+ yield { type: 'done' };
132
+ }
133
+ return;
134
+ }
135
+ if (inputGuardResult.warning) {
136
+ // warn 级别只通知,不阻断
137
+ yield {
138
+ type: 'text',
139
+ content: `${this.ui.guardrailWarningPrefix} ${inputGuardResult.warning}`,
140
+ };
141
+ }
142
+ // 安全规范 §6:用户输入用 <user_input> 标签包裹,增强 LLM 对注入攻击的免疫力
143
+ this.messages.push({ role: 'user', content: `<user_input>${userInputClean}</user_input>` });
144
+ let iteration = 0;
145
+ while (iteration < this.maxIterations) {
146
+ iteration++;
147
+ logger.debug({ iteration, messageCount: this.messages.length }, 'Agent Loop 迭代');
148
+ // V-105:每次迭代前检查是否已被取消
149
+ if (signal?.aborted) {
150
+ yield { type: 'aborted', reason: this.ui.abortedByUser };
151
+ return;
152
+ }
153
+ // 调用 LLM(带重试 + 截断保护)
154
+ const chatOpts = this.buildChatOptions();
155
+ // 上下文摘要:如果启用且首次截断,生成摘要
156
+ let contextSummary;
157
+ if (this.enableContextSummary &&
158
+ this.contextManager.shouldTruncate(this.messages)) {
159
+ // QC-R2-08:摘要缓存管理已移至 ContextManager.getOrCreateSummary
160
+ contextSummary = await this.contextManager.getOrCreateSummary(this.messages);
161
+ }
162
+ const safeMessages = this.contextManager.truncateMessages(this.messages, contextSummary);
163
+ // SEC-01: 截断后同步替换工作记忆,防止 messages 数组无限增长
164
+ // 持久化由 MessageHistory 负责,工作记忆只需保留当前上下文窗口内的消息
165
+ if (safeMessages !== this.messages) {
166
+ this.messages = [...safeMessages];
167
+ }
168
+ const llmResult = yield* this.callLlmWithRetry(safeMessages, chatOpts, signal, iteration);
169
+ if (llmResult.aborted) {
170
+ yield { type: 'aborted', reason: this.ui.abortedByUser };
171
+ return;
172
+ }
173
+ // 工具调用分支
174
+ if (llmResult.toolCalls && llmResult.toolCalls.length > 0) {
175
+ const execResult = yield* this.executeToolCalls(llmResult.toolCalls, llmResult.fullContent, signal);
176
+ if (execResult.aborted) {
177
+ yield { type: 'aborted', reason: this.ui.abortedByUser };
178
+ return;
179
+ }
180
+ // Reflection(反思/自修正):检查是否有可重试的错误
181
+ // 如果工具结果中有 retryable 错误,在 LLM 上下文中追加反思提示
182
+ // 帮助 LLM 聚焦于修正而非放弃
183
+ const hasRetryableError = this.messages
184
+ .slice(-llmResult.toolCalls.length) // 只看本轮工具结果
185
+ .some((m) => m.role === 'tool' && this.isRetryableToolError(m.content));
186
+ if (hasRetryableError) {
187
+ // 反思次数限制:通过前缀匹配统计已推送的 REFLECTION_HINT 消息
188
+ // (实际推送的 content 带有后缀说明,需用 startsWith 而非严格相等)
189
+ const reflectionHint = this.messages.filter((m) => m.role === 'system' && m.content.startsWith('[REFLECTION_HINT]')).length;
190
+ if (reflectionHint < this.maxReflectionRetries) {
191
+ this.messages.push({
192
+ role: 'system',
193
+ content: `[REFLECTION_HINT] 上次工具调用失败,错误可重试。请分析错误原因,修正参数后重新调用工具。剩余反思次数:${this.maxReflectionRetries - reflectionHint}`,
194
+ });
195
+ }
196
+ }
197
+ // 继续循环:把工具结果回填给 LLM
198
+ continue;
199
+ }
200
+ // 纯文本结束
201
+ if (llmResult.fullContent) {
202
+ this.messages.push({ role: 'assistant', content: llmResult.fullContent });
203
+ }
204
+ else {
205
+ // LLM 返回空响应(既无文本也无工具调用)的兜底处理
206
+ // 正常 LLM 不会返回空响应,但某些 provider 异常/边界情况下可能发生
207
+ logger.warn({ iteration }, 'LLM 返回空响应(无文本、无工具调用),使用兜底提示');
208
+ const fallbackText = '(模型未返回有效内容,请重试或换一种方式提问)';
209
+ this.messages.push({ role: 'assistant', content: fallbackText });
210
+ yield { type: 'text', content: fallbackText };
211
+ }
212
+ // 输出护栏检查:在响应返回给用户之前,检查是否命中护栏规则
213
+ const outputGuardResult = this.runOutputGuardrails(llmResult.fullContent);
214
+ if (outputGuardResult.blocked) {
215
+ // P3: try/finally 确保 done 一定送达,即使 text yield 异常
216
+ try {
217
+ yield { type: 'text', content: outputGuardResult.message ?? 'Output blocked by guardrail' };
218
+ }
219
+ finally {
220
+ yield { type: 'done' };
221
+ }
222
+ return;
223
+ }
224
+ if (outputGuardResult.warning) {
225
+ yield {
226
+ type: 'text',
227
+ content: `${this.ui.guardrailWarningPrefix} ${outputGuardResult.warning}`,
228
+ };
229
+ }
230
+ yield { type: 'done' };
231
+ return;
232
+ }
233
+ logger.warn({ iterations: iteration }, '达到最大迭代次数');
234
+ yield { type: 'text', content: this.ui.maxIterationsReached };
235
+ yield { type: 'done' };
236
+ }
237
+ finally {
238
+ responseSpan.end();
239
+ }
240
+ }
241
+ /**
242
+ * 调用 LLM(带指数退避重试)
243
+ *
244
+ * 仅在流式输出前失败时重试(streamStarted = false),
245
+ * 流式已开始则直接向上抛出(用户已看到部分结果)。
246
+ *
247
+ * @param safeMessages - 截断后的消息数组
248
+ * @param chatOpts - LLM 调用选项
249
+ * @param signal - 可选的 AbortSignal
250
+ * @param iteration - 当前迭代次数(用于 tracing)
251
+ * @yields AgentChunk 文本片段
252
+ * @returns LLM 调用结果(fullContent + toolCalls + aborted 状态)
253
+ */
254
+ async *callLlmWithRetry(safeMessages, chatOpts, signal, iteration) {
255
+ let fullContent = '';
256
+ let toolCalls = undefined;
257
+ let streamStarted = false;
258
+ let lastError = null;
259
+ let aborted = false;
260
+ // 将 AbortSignal 和超时配置传入 provider,
261
+ // 确保 fetch 请求和 SSE 流读取都能被及时中断(用户取消/超时)
262
+ const effectiveOpts = {
263
+ ...chatOpts,
264
+ signal,
265
+ timeoutMs: LOOP_CONSTANTS.LLM_TIMEOUT_MS,
266
+ };
267
+ // LLM 调用 Span(涵盖重试循环)
268
+ const llmSpan = this.tracer.startSpan(TRACE_SPANS.LLM_CALL, {
269
+ model: this.opts.provider.name,
270
+ messageCount: safeMessages.length,
271
+ iteration,
272
+ });
273
+ // R-103 补充 span 属性:让宿主监控面板能按 token 消耗过滤
274
+ llmSpan.setAttribute('inputTokens', this.contextManager.estimateTokens(safeMessages));
275
+ for (let attempt = 0; attempt <= LOOP_CONSTANTS.MAX_LLM_RETRIES; attempt++) {
276
+ if (attempt > 0) {
277
+ // 仅在流式输出前失败时重试(streamStarted = false)
278
+ const delay = LOOP_CONSTANTS.RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
279
+ logger.warn({ attempt, delay, error: lastError?.message }, 'LLM 调用失败,重试中');
280
+ await new Promise((r) => setTimeout(r, delay));
281
+ fullContent = '';
282
+ toolCalls = undefined;
283
+ }
284
+ try {
285
+ // R-103 LLM 指标统计:每次 provider.chat 调用 +1,输入 token 累计
286
+ this.metricLlmCallCount++;
287
+ this.metricTotalInputTokens += this.contextManager.estimateTokens(safeMessages);
288
+ // safeMessages 为 readonly Message[],provider.chat 期望 Message[];
289
+ // 通过浅拷贝转换为可变数组,避免类型断言。
290
+ for await (const chunk of this.opts.provider.chat([...safeMessages], effectiveOpts)) {
291
+ streamStarted = true;
292
+ if (signal?.aborted) {
293
+ aborted = true;
294
+ break;
295
+ }
296
+ if (chunk.content) {
297
+ fullContent += chunk.content;
298
+ yield { type: 'text', content: chunk.content };
299
+ }
300
+ if (chunk.toolCalls) {
301
+ toolCalls = [...(toolCalls ?? []), ...chunk.toolCalls];
302
+ }
303
+ }
304
+ // R-103 输出 token 统计:成功时累计输出 token
305
+ this.metricTotalOutputTokens += this.contextManager.estimateTokens([
306
+ { role: 'assistant', content: fullContent },
307
+ ]);
308
+ break; // 成功,退出重试循环
309
+ }
310
+ catch (err) {
311
+ const e = toError(err);
312
+ lastError = e;
313
+ if (streamStarted) {
314
+ // 流式已开始输出,不能重试(用户已看到部分结果),向上抛出
315
+ llmSpan.recordException(e);
316
+ llmSpan.end();
317
+ throw err;
318
+ }
319
+ if (attempt >= LOOP_CONSTANTS.MAX_LLM_RETRIES) {
320
+ // 重试次数耗尽
321
+ llmSpan.recordException(e);
322
+ llmSpan.end();
323
+ throw err;
324
+ }
325
+ // 继续重试(超时/网络错误等在流式开始前均可重试)
326
+ }
327
+ }
328
+ if (aborted) {
329
+ llmSpan.end();
330
+ return { fullContent, toolCalls, aborted: true };
331
+ }
332
+ // LLM 调用成功,结束 span
333
+ llmSpan.end();
334
+ return { fullContent, toolCalls, aborted: false };
335
+ }
336
+ /**
337
+ * 执行工具调用列表
338
+ *
339
+ * 遍历 LLM 返回的 toolCalls,逐个执行并收集结果。
340
+ * 工具执行异常会被捕获并转为结构化错误字符串回传给 LLM,
341
+ * 而非直接中断对话。
342
+ *
343
+ * @param toolCalls - LLM 返回的工具调用列表
344
+ * @param fullContent - LLM 返回的文本内容
345
+ * @param signal - 可选的 AbortSignal
346
+ * @yields AgentChunk 工具开始/结果片段
347
+ * @returns 执行结果(aborted 状态)
348
+ */
349
+ async *executeToolCalls(toolCalls, fullContent, signal) {
350
+ this.messages.push({
351
+ role: 'assistant',
352
+ content: fullContent,
353
+ toolCalls,
354
+ });
355
+ // 执行工具
356
+ for (const tc of toolCalls) {
357
+ // V-105:工具执行前检查取消
358
+ if (signal?.aborted) {
359
+ return { aborted: true };
360
+ }
361
+ // R-103 工具调用统计:每次工具执行 +1
362
+ this.metricToolCallCount++;
363
+ yield { type: 'tool_start', toolCallId: tc.id, name: tc.function.name, args: tc.function.arguments };
364
+ // 工具执行 Span
365
+ const toolSpan = this.tracer.startSpan(TRACE_SPANS.TOOL_EXEC, {
366
+ toolName: tc.function.name,
367
+ });
368
+ // 工具执行可能因文件不存在、路径越界等原因失败
369
+ // 捕获异常并转为结构化错误结果字符串,回传给 LLM 让其自行调整策略
370
+ // 避免错误直接传播到 agent.chat() 导致整个对话中断
371
+ // 错误结果包含 [ERR:TOOL:code] 前缀,供 Reflection 逻辑解析
372
+ let result;
373
+ try {
374
+ result = await this.opts.toolExecutor(tc.function.name, tc.function.arguments);
375
+ }
376
+ catch (err) {
377
+ const e = toError(err);
378
+ toolSpan.recordException(e);
379
+ if (err instanceof MemoraError) {
380
+ const code = err.errorCode ?? 'UNKNOWN';
381
+ result = `[ERR:TOOL:${code}] 错误:${err.title}${err.detail ? ` — ${err.detail}` : ''}`;
382
+ logger.warn({ tool: tc.function.name, errorCode: code, title: err.title }, '工具执行失败,错误已回传给 LLM');
383
+ }
384
+ else {
385
+ result = `[ERR:TOOL:UNKNOWN] 错误:工具执行异常 — ${e.message}`;
386
+ logger.error({ tool: tc.function.name, err }, '工具执行异常');
387
+ }
388
+ }
389
+ toolSpan.end();
390
+ this.messages.push({
391
+ role: 'tool',
392
+ content: result,
393
+ toolCallId: tc.id,
394
+ });
395
+ // R-103 工具失败统计:结果以 [ERR 开头算失败
396
+ if (result.startsWith('[ERR')) {
397
+ this.metricToolFailureCount++;
398
+ }
399
+ yield {
400
+ type: 'tool_result',
401
+ toolCallId: tc.id,
402
+ name: tc.function.name,
403
+ ok: !result.startsWith('[ERR'),
404
+ summary: result.slice(0, 100),
405
+ };
406
+ }
407
+ return { aborted: false };
408
+ }
409
+ /**
410
+ * 构建 system prompt(注入人格 + 规则 + 领域 + 工具描述)
411
+ */
412
+ buildSystemPrompt(memories) {
413
+ const sections = memories.map((m) => `## ${m.name}\n\n${m.content}`).join('\n\n---\n\n');
414
+ let prompt = `# Memora Agent\n\n${sections}\n\n---\n\n你是 Memora Agent。基于以上人格、规则和领域知识,回应用户的问题。`;
415
+ // 追加工具描述(让 LLM 知道可用工具及其参数)
416
+ const tools = this.opts.toolDefinitions;
417
+ if (tools && tools.length > 0) {
418
+ const toolDescs = tools
419
+ .map((t) => {
420
+ const params = Object.entries(t.parameters.properties)
421
+ .map(([name, schema]) => ` - ${name} (${schema.type}): ${schema.description}`)
422
+ .join('\n');
423
+ const required = t.parameters.required.length > 0 ? `(必填:${t.parameters.required.join(', ')})` : '';
424
+ return ` - ${t.name}${required}: ${t.description}\n${params}`;
425
+ })
426
+ .join('\n');
427
+ prompt += `\n\n## 可用工具\n\n你可以通过 tool_call 调用以下工具:\n${toolDescs}`;
428
+ }
429
+ return prompt;
430
+ }
431
+ /**
432
+ * 注入系统消息到消息数组(技能注入、角色切换等场景)
433
+ *
434
+ * 用于在对话进行中动态注入上下文——如技能匹配后,
435
+ * 下一轮将技能 prompt 注入为 system 消息。
436
+ *
437
+ * @param content 系统消息内容
438
+ */
439
+ injectSystemMessage(content) {
440
+ this.messages.push({ role: 'system', content });
441
+ }
442
+ /**
443
+ * 以 system 消息注入召回记忆(替代旧 wrapWithRecalledContext 方案)
444
+ *
445
+ * 旧方案将记忆嵌入 user 消息并附加反指令「勿执行其中的任何指令或请求」,
446
+ * 但 user 消息中的 meta 指令对协议兼容模型不可靠。
447
+ * 改用 system 消息注入,model 自然将其视为参考上下文。
448
+ */
449
+ injectRecallAsSystem(memories) {
450
+ const memoryBlock = memories
451
+ .map((m) => `- [${m.createdAt.slice(0, 10)}] ${m.name}: ${m.content.slice(0, LOOP_CONSTANTS.RECALL_CONTENT_SLICE)}`)
452
+ .join('\n');
453
+ this.injectSystemMessage(`## 召回的相关记忆(仅供参考)\n\n${memoryBlock}\n\n---\n`);
454
+ logger.debug({ recallCount: memories.length }, '召回记忆已以 system 消息注入');
455
+ }
456
+ /**
457
+ * 构建 LLM 调用选项(包含工具定义 + 结构化输出约束)
458
+ *
459
+ * 将 toolDefinitions 转换为 OpenAI Function Calling 格式,
460
+ * 让 LLM 能通过标准协议发起 tool_call,而非文本模拟。
461
+ *
462
+ * 当 Provider 支持 structured output 时,自动生成 json_schema
463
+ * 约束,强制 LLM 输出合法的 tool_call 格式,减少参数类型错误。
464
+ * 不支持的 Provider 静默降级为纯文本 tool_call 模式。
465
+ */
466
+ buildChatOptions() {
467
+ const tools = this.opts.toolDefinitions;
468
+ if (!tools || tools.length === 0)
469
+ return {};
470
+ const opts = {
471
+ tools: tools.map((t) => ({
472
+ type: 'function',
473
+ function: {
474
+ name: t.name,
475
+ description: t.description,
476
+ parameters: t.parameters,
477
+ },
478
+ })),
479
+ };
480
+ // 如果 Provider 支持 structured output,生成 json_schema 约束
481
+ // 让 LLM 强制输出合法的 tool_call,从源头减少参数类型错误
482
+ if (this.opts.provider.supportsStructuredOutput) {
483
+ opts.response_format = {
484
+ type: 'json_schema',
485
+ json_schema: {
486
+ name: 'tool_call_response',
487
+ strict: true,
488
+ schema: {
489
+ type: 'object',
490
+ properties: {
491
+ content: {
492
+ type: 'string',
493
+ description: 'Assistant response text (may be empty if tool calls are needed)',
494
+ },
495
+ tool_calls: {
496
+ type: 'array',
497
+ items: {
498
+ type: 'object',
499
+ properties: {
500
+ name: { type: 'string' },
501
+ arguments: { type: 'object' },
502
+ },
503
+ required: ['name', 'arguments'],
504
+ },
505
+ },
506
+ },
507
+ },
508
+ },
509
+ };
510
+ }
511
+ return opts;
512
+ }
513
+ /**
514
+ * 刷新工具定义(registerTool 后调用)
515
+ *
516
+ * 当宿主项目通过 agent.registerTool() 注册新工具后,
517
+ * 需要更新 system prompt 中的工具描述,让 LLM 能看到新工具。
518
+ * 重建 messages[0] 的 system prompt 内容。
519
+ *
520
+ * @param toolDefinitions 最新的工具定义列表(内置 + 自定义)
521
+ */
522
+ refreshToolDefinitions(toolDefinitions) {
523
+ // 注意:修改 opts.toolDefinitions 是有意为之的副作用——
524
+ // 后续 buildSystemPrompt() 需要读取最新的工具列表
525
+ this.opts.toolDefinitions = toolDefinitions;
526
+ // 重建 messages[0] 的 system prompt
527
+ this.rebuildSystemMessage();
528
+ }
529
+ /**
530
+ * 运行时切换 LLM Provider
531
+ *
532
+ * 用于多 Provider 路由场景:用户切换 API 时,
533
+ * Agent 调用此方法更新 AgentLoop 的 provider 引用。
534
+ * 后续 chat() 调用使用新 Provider。
535
+ *
536
+ * @param provider 新的 LlmProvider 实例
537
+ */
538
+ setProvider(provider) {
539
+ this.opts.provider = provider;
540
+ }
541
+ /**
542
+ * 刷新角色 prompt
543
+ *
544
+ * 当角色切换时,更新系统 prompt 前缀的角色部分。
545
+ * 保留 bootstrapMemories 和 toolDefinitions 不变,只替换 prefix。
546
+ *
547
+ * @param newPrefix 新的系统 prompt 前缀(包含新角色 + 用户画像)
548
+ */
549
+ refreshPersonaPrefix(newPrefix) {
550
+ this.opts.systemPromptPrefix = newPrefix;
551
+ this.rebuildSystemMessage();
552
+ }
553
+ /**
554
+ * 注入情感基调到 system prompt(Phase 2.1:AffectController)
555
+ *
556
+ * 在角色前缀和 bootstrap 记忆之间插入情感描述文本。
557
+ * 与 refreshPersonaPrefix 独立——角色切换不会清除情感注入。
558
+ *
559
+ * 注入位置:systemPromptPrefix + affectPrefix + bootstrapMemories + toolDefinitions
560
+ *
561
+ * @param affectString 情感描述文本(如"当前对话基调:温暖、直接"),传空字符串清除注入
562
+ */
563
+ injectAffect(affectString) {
564
+ this.opts.affectPrefix = affectString;
565
+ this.rebuildSystemMessage();
566
+ }
567
+ /**
568
+ * 重建 messages[0] 的 system prompt
569
+ */
570
+ rebuildSystemMessage() {
571
+ const sysMsg = this.messages[0];
572
+ if (sysMsg && sysMsg.role === 'system') {
573
+ const prefix = this.opts.systemPromptPrefix ?? '';
574
+ const affect = this.opts.affectPrefix ? `\n${this.opts.affectPrefix}\n` : '';
575
+ this.messages[0] = {
576
+ role: 'system',
577
+ content: prefix + affect + this.buildSystemPrompt(this.opts.bootstrapMemories),
578
+ };
579
+ }
580
+ }
581
+ /**
582
+ * 获取消息历史(用于持久化)
583
+ */
584
+ getMessages() {
585
+ return this.messages;
586
+ }
587
+ /**
588
+ * 获取 AgentLoop 运行时指标快照(R-103 可观测性增强)
589
+ *
590
+ * 返回 LLM 调用、记忆召回、工具调用、上下文管理四个维度的累计指标。
591
+ * 衰减指标(decay)由 Agent 层填充,此处返回 null。
592
+ *
593
+ * 纯只读、同步、零副作用——适合宿主项目定期轮询构建监控面板。
594
+ *
595
+ * @returns AgentMetrics 快照(decay 字段为 null,由 Agent 层填充)
596
+ */
597
+ getMetrics() {
598
+ // 计算召回命中率:totalCount 为 0 时返回 0,避免除零
599
+ const hitRate = this.metricRecallTotalCount > 0
600
+ ? this.metricRecallHitCount / this.metricRecallTotalCount
601
+ : 0;
602
+ return {
603
+ llm: {
604
+ callCount: this.metricLlmCallCount,
605
+ totalInputTokens: this.metricTotalInputTokens,
606
+ totalOutputTokens: this.metricTotalOutputTokens,
607
+ },
608
+ recall: {
609
+ totalCount: this.metricRecallTotalCount,
610
+ hitCount: this.metricRecallHitCount,
611
+ hitRate: Math.round(hitRate * 1000) / 1000, // 保留 3 位小数
612
+ },
613
+ tools: {
614
+ callCount: this.metricToolCallCount,
615
+ failureCount: this.metricToolFailureCount,
616
+ },
617
+ context: {
618
+ truncationCount: this.contextManager.truncationCount,
619
+ messageCount: this.messages.length,
620
+ estimatedTokens: this.contextManager.estimateTokens(this.messages),
621
+ },
622
+ // 衰减指标由 Agent 层填充,AgentLoop 不持有衰减逻辑
623
+ decay: null,
624
+ };
625
+ }
626
+ /**
627
+ * 获取最近 N 轮对话(Layer 5: 最近对话注入)
628
+ *
629
+ * 从 messages 数组中提取最近 N 轮 user + assistant 消息,
630
+ * 用于注入 system prompt,让 LLM 在用户输入无信息量时仍能看到上下文。
631
+ *
632
+ * @param rounds - 要获取的轮次数(默认 3)
633
+ * @returns 最近 N 轮的 user + assistant 消息数组
634
+ */
635
+ getRecentHistory(rounds = 3) {
636
+ // 过滤出 user + assistant 消息(排除 system 和 tool)
637
+ const conversationMessages = this.messages.filter((m) => m.role === 'user' || m.role === 'assistant');
638
+ // 取最后 N 轮(每轮 = 1 user + 1 assistant,共 2 条消息)
639
+ const recentMessages = conversationMessages.slice(-rounds * 2);
640
+ return recentMessages.map((m) => ({
641
+ role: m.role,
642
+ content: m.content,
643
+ }));
644
+ }
645
+ /**
646
+ * 恢复历史消息(用于重启后恢复对话)
647
+ * 会跳过 system 消息,只恢复 user/assistant/tool 消息
648
+ *
649
+ * @param historyMessages - 要恢复的历史消息列表
650
+ */
651
+ restoreHistory(historyMessages) {
652
+ // 过滤掉 system 消息(我们已经有初始化的 system prompt 了)
653
+ const nonSystemMessages = historyMessages.filter((m) => m.role !== 'system');
654
+ if (nonSystemMessages.length === 0) {
655
+ logger.debug({ messageCount: 0 }, '没有需要恢复的历史消息');
656
+ return;
657
+ }
658
+ // 保持第一条消息是 system prompt(构造函数保证 messages[0] 存在)
659
+ const systemPrompt = this.messages[0];
660
+ if (!systemPrompt) {
661
+ logger.warn({ hasSystemPrompt: false }, 'restoreHistory: 没有 system prompt,跳过恢复');
662
+ return;
663
+ }
664
+ this.messages = [systemPrompt, ...nonSystemMessages];
665
+ logger.info({ messageCount: nonSystemMessages.length }, '恢复历史对话消息');
666
+ }
667
+ /**
668
+ * 清理上一轮对话注入的临时 system 消息
669
+ *
670
+ * 每轮 chat() 前调用,仅保留 messages[0](永久 system prompt)和
671
+ * 所有 user/assistant/tool 消息(对话历史)。
672
+ * 防止 recallAndInject() / injectActiveSkill() / truncateMessages()
673
+ * 累积的临时 system 消息堆叠,避免 LLM 收到大量冗余指令。
674
+ */
675
+ cleanTemporarySystemMessages() {
676
+ if (this.messages.length <= 1)
677
+ return;
678
+ const permanent = this.messages[0];
679
+ const conversationHistory = this.messages.slice(1).filter((m) => m.role !== 'system');
680
+ const removedCount = this.messages.length - 1 - conversationHistory.length;
681
+ this.messages = [permanent, ...conversationHistory];
682
+ if (removedCount > 0) {
683
+ logger.debug({ removedCount, remainingMessages: this.messages.length }, '临时 system 消息已清理');
684
+ }
685
+ }
686
+ // ─── 护栏与 Reflection 辅助方法 ──────────────────────────
687
+ /**
688
+ * 输入护栏检查
689
+ *
690
+ * 在用户输入注入上下文之前运行,遍历所有 guardrail 规则,
691
+ * 用正则匹配用户输入。命中 block action 时返回 blocked=true。
692
+ *
693
+ * 护栏自身异常(正则编译失败等)降级为"放行 + 记日志",
694
+ * 永远不阻断用户对话(降级优先原则)。
695
+ */
696
+ runInputGuardrails(input) {
697
+ if (this.guardrailRules.length === 0)
698
+ return { blocked: false };
699
+ for (const rule of this.guardrailRules) {
700
+ try {
701
+ // 从记忆内容中提取 pattern(格式:pattern: /regex/ action: block|warn)
702
+ const patternMatch = rule.content.match(/pattern:\s*(.+)/);
703
+ const actionMatch = rule.content.match(/action:\s*(block|warn)/);
704
+ if (!patternMatch || !actionMatch)
705
+ continue;
706
+ const pattern = patternMatch[1]?.trim();
707
+ const action = actionMatch[1]?.trim();
708
+ if (!pattern || !action)
709
+ continue;
710
+ // 去掉正则定界符 //
711
+ const regexStr = pattern.startsWith('/') && pattern.endsWith('/') ? pattern.slice(1, -1) : pattern;
712
+ const regex = new RegExp(regexStr, 'i');
713
+ if (regex.test(input)) {
714
+ if (action === 'block') {
715
+ logger.warn({ rule: rule.name, pattern: regexStr }, '输入护栏阻断');
716
+ return { blocked: true, message: this.ui.inputBlockedByGuard(rule.name) };
717
+ }
718
+ logger.warn({ rule: rule.name, pattern: regexStr }, '输入护栏警告');
719
+ return { blocked: false, warning: this.ui.inputBlockedByGuard(rule.name) };
720
+ }
721
+ }
722
+ catch (err) {
723
+ // 护栏自身异常降级:放行 + 记日志
724
+ logger.error({ rule: rule.name, err }, '护栏规则执行异常,已降级放行');
725
+ }
726
+ }
727
+ return { blocked: false };
728
+ }
729
+ /**
730
+ * 输出护栏检查
731
+ *
732
+ * 在 LLM 响应返回给用户之前运行,防止敏感信息泄露。
733
+ * 输入/输出共享同一护栏规则集。
734
+ */
735
+ runOutputGuardrails(output) {
736
+ return this.runInputGuardrails(output);
737
+ }
738
+ /**
739
+ * 判断工具错误结果是否可重试(Reflection 用)
740
+ *
741
+ * 解析工具结果中的 [ERR:TOOL:code] 前缀,
742
+ * 调用 isRetryableErrorCode 判断。
743
+ */
744
+ isRetryableToolError(result) {
745
+ const match = result.match(/^\[ERR:TOOL:(\w+)\]/);
746
+ if (!match)
747
+ return false;
748
+ // regex 捕获组保证 match[1] 非空,但使用空值兜底避免非空断言
749
+ const codeStr = match[1] ?? '';
750
+ if (!codeStr)
751
+ return false;
752
+ const code = codeStr;
753
+ return isRetryableErrorCode(code);
754
+ }
755
+ }
756
+ //# sourceMappingURL=loop.js.map