@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.
- package/README.en.md +272 -0
- package/README.md +150 -38
- package/dist/agent/agent.d.ts +257 -115
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +747 -400
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/assembler.d.ts +32 -16
- package/dist/agent/assembler.d.ts.map +1 -1
- package/dist/agent/assembler.js +83 -47
- package/dist/agent/assembler.js.map +1 -1
- package/dist/agent/builtinToolHandlers.d.ts +27 -2
- package/dist/agent/builtinToolHandlers.d.ts.map +1 -1
- package/dist/agent/builtinToolHandlers.js +78 -5
- package/dist/agent/builtinToolHandlers.js.map +1 -1
- package/dist/agent/constants.d.ts +4 -0
- package/dist/agent/constants.d.ts.map +1 -1
- package/dist/agent/constants.js +4 -0
- package/dist/agent/constants.js.map +1 -1
- package/dist/agent/contextManager.d.ts +8 -0
- package/dist/agent/contextManager.d.ts.map +1 -1
- package/dist/agent/contextManager.js +7 -2
- package/dist/agent/contextManager.js.map +1 -1
- package/dist/agent/guardrail.d.ts +4 -2
- package/dist/agent/guardrail.d.ts.map +1 -1
- package/dist/agent/guardrail.js +13 -5
- package/dist/agent/guardrail.js.map +1 -1
- package/dist/agent/loop.d.ts +137 -15
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +449 -270
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/managers/archiveCoordinator.d.ts +126 -22
- package/dist/agent/managers/archiveCoordinator.d.ts.map +1 -1
- package/dist/agent/managers/archiveCoordinator.js +239 -44
- package/dist/agent/managers/archiveCoordinator.js.map +1 -1
- package/dist/agent/managers/autoConfigRefiner.d.ts.map +1 -1
- package/dist/agent/managers/autoConfigRefiner.js +4 -8
- package/dist/agent/managers/autoConfigRefiner.js.map +1 -1
- package/dist/agent/managers/chatLockManager.d.ts +116 -0
- package/dist/agent/managers/chatLockManager.d.ts.map +1 -0
- package/dist/agent/managers/chatLockManager.js +191 -0
- package/dist/agent/managers/chatLockManager.js.map +1 -0
- package/dist/agent/managers/configManager.d.ts +87 -3
- package/dist/agent/managers/configManager.d.ts.map +1 -1
- package/dist/agent/managers/configManager.js +139 -2
- package/dist/agent/managers/configManager.js.map +1 -1
- package/dist/agent/managers/dedupManager.d.ts +169 -0
- package/dist/agent/managers/dedupManager.d.ts.map +1 -0
- package/dist/agent/managers/dedupManager.js +336 -0
- package/dist/agent/managers/dedupManager.js.map +1 -0
- package/dist/agent/managers/insightExtractor.d.ts +31 -3
- package/dist/agent/managers/insightExtractor.d.ts.map +1 -1
- package/dist/agent/managers/insightExtractor.js +130 -59
- package/dist/agent/managers/insightExtractor.js.map +1 -1
- package/dist/agent/managers/llmJudgeHelper.d.ts +38 -0
- package/dist/agent/managers/llmJudgeHelper.d.ts.map +1 -0
- package/dist/agent/managers/llmJudgeHelper.js +49 -0
- package/dist/agent/managers/llmJudgeHelper.js.map +1 -0
- package/dist/agent/managers/memoryAdvisor.d.ts +102 -2
- package/dist/agent/managers/memoryAdvisor.d.ts.map +1 -1
- package/dist/agent/managers/memoryAdvisor.js +254 -10
- package/dist/agent/managers/memoryAdvisor.js.map +1 -1
- package/dist/agent/managers/memoryDecayScheduler.d.ts +127 -1
- package/dist/agent/managers/memoryDecayScheduler.d.ts.map +1 -1
- package/dist/agent/managers/memoryDecayScheduler.js +283 -4
- package/dist/agent/managers/memoryDecayScheduler.js.map +1 -1
- package/dist/agent/managers/memoryGovernance.d.ts +56 -0
- package/dist/agent/managers/memoryGovernance.d.ts.map +1 -0
- package/dist/agent/managers/memoryGovernance.js +63 -0
- package/dist/agent/managers/memoryGovernance.js.map +1 -0
- package/dist/agent/managers/memoryInspector.d.ts +113 -30
- package/dist/agent/managers/memoryInspector.d.ts.map +1 -1
- package/dist/agent/managers/memoryInspector.js +165 -32
- package/dist/agent/managers/memoryInspector.js.map +1 -1
- package/dist/agent/managers/relationBuilder.d.ts +4 -4
- package/dist/agent/managers/relationBuilder.d.ts.map +1 -1
- package/dist/agent/managers/relationBuilder.js +5 -4
- package/dist/agent/managers/relationBuilder.js.map +1 -1
- package/dist/agent/managers/sessionArchiver.d.ts +12 -1
- package/dist/agent/managers/sessionArchiver.d.ts.map +1 -1
- package/dist/agent/managers/sessionArchiver.js +87 -30
- package/dist/agent/managers/sessionArchiver.js.map +1 -1
- package/dist/agent/managers/sessionManager.d.ts.map +1 -1
- package/dist/agent/managers/sessionManager.js +9 -17
- package/dist/agent/managers/sessionManager.js.map +1 -1
- package/dist/agent/managers/streamAccumulator.d.ts +45 -0
- package/dist/agent/managers/streamAccumulator.d.ts.map +1 -0
- package/dist/agent/managers/streamAccumulator.js +40 -0
- package/dist/agent/managers/streamAccumulator.js.map +1 -0
- package/dist/agent/managers/textPolishManager.d.ts +62 -0
- package/dist/agent/managers/textPolishManager.d.ts.map +1 -0
- package/dist/agent/managers/textPolishManager.js +96 -0
- package/dist/agent/managers/textPolishManager.js.map +1 -0
- package/dist/agent/managers/workProjection.d.ts +16 -1
- package/dist/agent/managers/workProjection.d.ts.map +1 -1
- package/dist/agent/managers/workProjection.js +48 -24
- package/dist/agent/managers/workProjection.js.map +1 -1
- package/dist/agent/personaMatcher.d.ts +29 -0
- package/dist/agent/personaMatcher.d.ts.map +1 -0
- package/dist/agent/personaMatcher.js +60 -0
- package/dist/agent/personaMatcher.js.map +1 -0
- package/dist/agent/toolExecutor.d.ts +24 -4
- package/dist/agent/toolExecutor.d.ts.map +1 -1
- package/dist/agent/toolExecutor.js +30 -4
- package/dist/agent/toolExecutor.js.map +1 -1
- package/dist/agent/tracer.d.ts +31 -0
- package/dist/agent/tracer.d.ts.map +1 -1
- package/dist/agent/tracer.js +31 -0
- package/dist/agent/tracer.js.map +1 -1
- package/dist/agent/types.d.ts +133 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +38 -0
- package/dist/agent/types.js.map +1 -1
- package/dist/agent/userFactExtractor.d.ts.map +1 -1
- package/dist/agent/userFactExtractor.js +7 -0
- package/dist/agent/userFactExtractor.js.map +1 -1
- package/dist/config/loader.d.ts +124 -236
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +296 -118
- package/dist/config/loader.js.map +1 -1
- package/dist/eval/evalRunner.d.ts +94 -0
- package/dist/eval/evalRunner.d.ts.map +1 -0
- package/dist/eval/evalRunner.js +141 -0
- package/dist/eval/evalRunner.js.map +1 -0
- package/dist/eval/evalTypes.d.ts +21 -0
- package/dist/eval/evalTypes.d.ts.map +1 -1
- package/dist/eval/evalTypes.js +15 -0
- package/dist/eval/evalTypes.js.map +1 -1
- package/dist/eval/scenarios.d.ts +33 -0
- package/dist/eval/scenarios.d.ts.map +1 -0
- package/dist/eval/scenarios.js +109 -0
- package/dist/eval/scenarios.js.map +1 -0
- package/dist/index.d.ts +45 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -5
- package/dist/index.js.map +1 -1
- package/dist/llm/abortSignal.d.ts +27 -0
- package/dist/llm/abortSignal.d.ts.map +1 -0
- package/dist/llm/abortSignal.js +44 -0
- package/dist/llm/abortSignal.js.map +1 -0
- package/dist/llm/embedding.d.ts +13 -12
- package/dist/llm/embedding.d.ts.map +1 -1
- package/dist/llm/embedding.js +5 -17
- package/dist/llm/embedding.js.map +1 -1
- package/dist/llm/factory.d.ts +19 -5
- package/dist/llm/factory.d.ts.map +1 -1
- package/dist/llm/factory.js +19 -37
- package/dist/llm/factory.js.map +1 -1
- package/dist/llm/openaiCompatible.d.ts.map +1 -1
- package/dist/llm/openaiCompatible.js +17 -34
- package/dist/llm/openaiCompatible.js.map +1 -1
- package/dist/logging/logger.d.ts +3 -1
- package/dist/logging/logger.d.ts.map +1 -1
- package/dist/logging/logger.js +47 -50
- package/dist/logging/logger.js.map +1 -1
- package/dist/memory/governance.d.ts +50 -0
- package/dist/memory/governance.d.ts.map +1 -0
- package/dist/memory/governance.js +70 -0
- package/dist/memory/governance.js.map +1 -0
- package/dist/memory/hybridMerge.d.ts +0 -4
- package/dist/memory/hybridMerge.d.ts.map +1 -1
- package/dist/memory/hybridMerge.js +19 -4
- package/dist/memory/hybridMerge.js.map +1 -1
- package/dist/memory/inMemoryRelationStore.d.ts +7 -0
- package/dist/memory/inMemoryRelationStore.d.ts.map +1 -1
- package/dist/memory/inMemoryRelationStore.js +15 -0
- package/dist/memory/inMemoryRelationStore.js.map +1 -1
- package/dist/memory/inMemoryStorage.d.ts +36 -1
- package/dist/memory/inMemoryStorage.d.ts.map +1 -1
- package/dist/memory/inMemoryStorage.js +70 -19
- package/dist/memory/inMemoryStorage.js.map +1 -1
- package/dist/memory/loader.d.ts +2 -0
- package/dist/memory/loader.d.ts.map +1 -1
- package/dist/memory/loader.js +8 -1
- package/dist/memory/loader.js.map +1 -1
- package/dist/memory/lockManager.d.ts +7 -2
- package/dist/memory/lockManager.d.ts.map +1 -1
- package/dist/memory/lockManager.js +35 -17
- package/dist/memory/lockManager.js.map +1 -1
- package/dist/memory/projectManager.d.ts +8 -7
- package/dist/memory/projectManager.d.ts.map +1 -1
- package/dist/memory/projectManager.js +33 -15
- package/dist/memory/projectManager.js.map +1 -1
- package/dist/memory/projectRegistry.d.ts +34 -1
- package/dist/memory/projectRegistry.d.ts.map +1 -1
- package/dist/memory/projectRegistry.js +100 -31
- package/dist/memory/projectRegistry.js.map +1 -1
- package/dist/memory/recall.d.ts +16 -0
- package/dist/memory/recall.d.ts.map +1 -1
- package/dist/memory/recall.js +35 -20
- package/dist/memory/recall.js.map +1 -1
- package/dist/memory/relationStore.d.ts +20 -3
- package/dist/memory/relationStore.d.ts.map +1 -1
- package/dist/memory/sourceValidation.d.ts +17 -0
- package/dist/memory/sourceValidation.d.ts.map +1 -1
- package/dist/memory/sourceValidation.js +27 -3
- package/dist/memory/sourceValidation.js.map +1 -1
- package/dist/memory/storageInterface.d.ts +26 -1
- package/dist/memory/storageInterface.d.ts.map +1 -1
- package/dist/memory/store.d.ts +22 -1
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +58 -20
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/types.d.ts +44 -31
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/memory/types.js +63 -24
- package/dist/memory/types.js.map +1 -1
- package/dist/memory/userProfile.d.ts +32 -8
- package/dist/memory/userProfile.d.ts.map +1 -1
- package/dist/memory/userProfile.js +105 -44
- package/dist/memory/userProfile.js.map +1 -1
- package/dist/memory/vectorStore.d.ts +16 -8
- package/dist/memory/vectorStore.d.ts.map +1 -1
- package/dist/memory/vectorStore.js +49 -22
- package/dist/memory/vectorStore.js.map +1 -1
- package/dist/persona/personaManager.d.ts +38 -63
- package/dist/persona/personaManager.d.ts.map +1 -1
- package/dist/persona/personaManager.js +136 -202
- package/dist/persona/personaManager.js.map +1 -1
- package/dist/skill/skillManager.d.ts +11 -57
- package/dist/skill/skillManager.d.ts.map +1 -1
- package/dist/skill/skillManager.js +46 -141
- package/dist/skill/skillManager.js.map +1 -1
- package/dist/skill/types.d.ts +2 -3
- package/dist/skill/types.d.ts.map +1 -1
- package/dist/utils/array.d.ts +24 -0
- package/dist/utils/array.d.ts.map +1 -0
- package/dist/utils/array.js +24 -0
- package/dist/utils/array.js.map +1 -0
- package/dist/utils/configResourceManager.d.ts +111 -0
- package/dist/utils/configResourceManager.d.ts.map +1 -0
- package/dist/utils/configResourceManager.js +148 -0
- package/dist/utils/configResourceManager.js.map +1 -0
- package/dist/utils/errors.d.ts +27 -8
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +31 -9
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/eventEmitter.d.ts +88 -4
- package/dist/utils/eventEmitter.d.ts.map +1 -1
- package/dist/utils/eventEmitter.js +23 -0
- package/dist/utils/eventEmitter.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +4 -2
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/math.d.ts +11 -0
- package/dist/utils/math.d.ts.map +1 -1
- package/dist/utils/math.js +17 -0
- package/dist/utils/math.js.map +1 -1
- package/dist/utils/objects.d.ts +17 -0
- package/dist/utils/objects.d.ts.map +1 -0
- package/dist/utils/objects.js +19 -0
- package/dist/utils/objects.js.map +1 -0
- package/dist/utils/segmenter.d.ts +11 -10
- package/dist/utils/segmenter.d.ts.map +1 -1
- package/dist/utils/segmenter.js +23 -1
- package/dist/utils/segmenter.js.map +1 -1
- package/dist/utils/strings.d.ts +22 -1
- package/dist/utils/strings.d.ts.map +1 -1
- package/dist/utils/strings.js +29 -1
- package/dist/utils/strings.js.map +1 -1
- package/dist/utils/time.d.ts +18 -3
- package/dist/utils/time.d.ts.map +1 -1
- package/dist/utils/time.js +24 -8
- package/dist/utils/time.js.map +1 -1
- package/dist/utils/toError.d.ts +8 -4
- package/dist/utils/toError.d.ts.map +1 -1
- package/dist/utils/toError.js +25 -5
- package/dist/utils/toError.js.map +1 -1
- package/docs/memora-api-reference.md +1235 -0
- package/docs/memora-/346/216/245/345/205/245/346/214/207/345/215/227.md +808 -0
- package/package.json +49 -22
- package/dist/agent/managers/memoryMutator.d.ts +0 -92
- package/dist/agent/managers/memoryMutator.d.ts.map +0 -1
- package/dist/agent/managers/memoryMutator.js +0 -94
- package/dist/agent/managers/memoryMutator.js.map +0 -1
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
|
|
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 ??
|
|
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
|
-
//
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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.
|
|
347
|
-
this.
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
|
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.
|
|
487
|
+
this.metrics.toolCallCount++;
|
|
429
488
|
yield { type: 'tool_start', toolCallId: tc.id, name: tc.function.name, args: tc.function.arguments };
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
//
|
|
517
|
-
|
|
549
|
+
// onAbort 提到外层,便于 race 结束后清理监听器
|
|
550
|
+
let onAbort = null;
|
|
518
551
|
const abortPromise = new Promise((resolve) => {
|
|
519
|
-
|
|
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.
|
|
698
|
-
totalInputTokens: this.
|
|
699
|
-
totalOutputTokens: this.
|
|
810
|
+
callCount: this.metrics.llmCallCount,
|
|
811
|
+
totalInputTokens: this.metrics.totalInputTokens,
|
|
812
|
+
totalOutputTokens: this.metrics.totalOutputTokens,
|
|
700
813
|
},
|
|
701
814
|
recall: {
|
|
702
|
-
totalCount: this.
|
|
703
|
-
hitCount: this.
|
|
704
|
-
hitRate:
|
|
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.
|
|
708
|
-
failureCount: this.
|
|
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
|