@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/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent 门面类 — Memora 宿主项目接入入口
|
|
3
3
|
*
|
|
4
|
-
* 设计文档(ADR-010 · Agent 门面类)要求宿主项目通过 `import { Agent } from '@memora
|
|
4
|
+
* 设计文档(ADR-010 · Agent 门面类)要求宿主项目通过 `import { Agent } from '@zooique/memora'`
|
|
5
5
|
* 一行代码接入。本类负责组件组装和核心对话编排,
|
|
6
6
|
* 领域专属操作委托给专职 Manager(PersonaManager / ToolExecutor / SkillManager / ConfigManager / InsightExtractor / MemoryInspector)。
|
|
7
7
|
*
|
|
@@ -22,35 +22,31 @@
|
|
|
22
22
|
* - 记忆查看 → MemoryInspector
|
|
23
23
|
* - 薄包装方法移除,调用方改为 agent.<manager>.xxx()
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { getBaseName } from '../utils/path.js';
|
|
26
26
|
import { AGENT_CONSTANTS } from '../agent/constants.js';
|
|
27
27
|
import { ProjectManager } from '../memory/projectManager.js';
|
|
28
28
|
import { SecurityGuard } from '../security/pathGuard.js';
|
|
29
|
-
import { recall } from '../memory/recall.js';
|
|
30
|
-
import { extractUserFacts } from '../agent/userFactExtractor.js';
|
|
29
|
+
import { recall, boostScores } from '../memory/recall.js';
|
|
31
30
|
import { assembleComponents } from '../agent/assembler.js';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
31
|
+
import { matchPersonaByLlm } from '../agent/personaMatcher.js';
|
|
32
|
+
import { chatBusyError, configError, isAbortError, toError } from '../utils/errors.js';
|
|
34
33
|
import { SessionManager } from '../agent/managers/sessionManager.js';
|
|
34
|
+
import { ChatLockManager } from '../agent/managers/chatLockManager.js';
|
|
35
35
|
import { MemoryDecayScheduler } from '../agent/managers/memoryDecayScheduler.js';
|
|
36
|
+
import { MemoryGovernance } from '../agent/managers/memoryGovernance.js';
|
|
36
37
|
import { ArchiveCoordinator } from '../agent/managers/archiveCoordinator.js';
|
|
37
|
-
import { TypedEventEmitter } from '../utils/eventEmitter.js';
|
|
38
|
+
import { TypedEventEmitter, AGENT_EVENTS, AGENT_EVENT_SET } from '../utils/eventEmitter.js';
|
|
38
39
|
import { logger } from '../logging/logger.js';
|
|
40
|
+
import { TRACE_SPANS, NOOP_TRACER } from '../agent/tracer.js';
|
|
39
41
|
// ─── 模块级常量 ─────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
// 必须与 utils/eventEmitter.ts 的 AgentEventMap 键集保持一致(9 个事件)
|
|
42
|
-
const AGENT_EVENT_NAMES = new Set([
|
|
43
|
-
'memoryAdded', 'personaSwitched', 'decayCompleted',
|
|
44
|
-
'memoryRecalled', 'sessionForked', 'insightExtracted',
|
|
45
|
-
'conflictDetected', 'projectSwitched', 'skillMatched',
|
|
46
|
-
]);
|
|
42
|
+
// AGENT_EVENTS / AGENT_EVENT_SET 已迁移至 utils/eventEmitter.ts(与 AgentEventMap 同处,单一真理源)
|
|
47
43
|
// ─── Agent 门面类 ───────────────────────────────────────
|
|
48
44
|
/**
|
|
49
45
|
* Memora Agent 门面类
|
|
50
46
|
*
|
|
51
47
|
* **设计哲学**:单 Agent,单配置,单记忆。每个 Agent 实例拥有独立的
|
|
52
48
|
* 对话管线(AgentLoop)、独立的消息历史(MessageHistory)和独立的
|
|
53
|
-
* 运行时状态(
|
|
49
|
+
* 运行时状态(chatBusy)。activeSkill 已移除——技能匹配改为当轮实时注入,无需跨轮状态缓存。
|
|
54
50
|
*
|
|
55
51
|
* **多实例**:宿主可通过 new Agent({ dataDir: './agent2' }) 创建
|
|
56
52
|
* 独立实例。只要 dataDir 不同,它们拥有完全隔离的记忆库和会话。
|
|
@@ -95,47 +91,56 @@ export class Agent extends TypedEventEmitter {
|
|
|
95
91
|
insightExtractor = null;
|
|
96
92
|
configManager = null;
|
|
97
93
|
memoryInspector = null;
|
|
98
|
-
/**
|
|
99
|
-
|
|
94
|
+
/**
|
|
95
|
+
* 语义去重管理器(L1 LLM 记忆治理)
|
|
96
|
+
*
|
|
97
|
+
* SPLIT-3 闭环(2026-07-21):从 MemoryInspector 拆分出 deduplicateMemories 职责,
|
|
98
|
+
* 让 MemoryInspector 回归纯存储读写。Agent.deduplicateMemories() 委托本对象。
|
|
99
|
+
*/
|
|
100
|
+
dedupManager = null;
|
|
101
|
+
/**
|
|
102
|
+
* 记忆顾问(L3 冲突检测 / sourceHealth / suggest)
|
|
103
|
+
*
|
|
104
|
+
* v2 PROXY-1 闭环:Agent.detectConflicts 直接调用 advisor,
|
|
105
|
+
* 不再经 MemoryInspector 转发,消除 3 层无意义代理。
|
|
106
|
+
* sourceHealth/suggest 仍由 inspector 转发以保持 agent.memory 统一入口语义。
|
|
107
|
+
*/
|
|
108
|
+
memoryAdvisor = null;
|
|
109
|
+
/** 记忆治理统一门面(L0/L1/L2/L3 + 诊断) */
|
|
110
|
+
_governance = null;
|
|
100
111
|
workProjection = null;
|
|
101
112
|
/** AutoConfigRefiner(模式 3:Agent 智能总结) */
|
|
102
113
|
autoConfigRefiner = null;
|
|
103
114
|
/** SessionArchiver(会话内容归档器,content 类记忆) */
|
|
104
115
|
sessionArchiver = null;
|
|
116
|
+
/** TextPolishManager(文本润色管理器,LLM 语法修正 + 表达优化) */
|
|
117
|
+
textPolisher = null;
|
|
105
118
|
/** 会话管理器(从 Agent 拆分出的会话管理职责) */
|
|
106
119
|
_sessionManager = null;
|
|
107
|
-
|
|
108
|
-
activeSkill = null;
|
|
120
|
+
// activeSkill 字段已移除:matchAndInjectSkill 改为当轮实时匹配注入,无需跨轮状态缓存
|
|
109
121
|
_initialized = false;
|
|
110
122
|
// 项目上下文(AgentContext 与 ProjectContext 等价,直接使用后者避免重复字段)
|
|
111
123
|
pctx = null;
|
|
112
|
-
/** chat() 并发锁 */
|
|
113
|
-
_chatBusy = false;
|
|
114
124
|
/**
|
|
115
|
-
* chat()
|
|
116
|
-
*
|
|
117
|
-
* 设计目的:
|
|
118
|
-
* 原锁是简单布尔值 `_chatBusy`,无 owner 校验。超时回调与 finally 块无差别清理,
|
|
119
|
-
* 导致 race condition:T=0 A 获取锁 → T=180s 超时释放 → T=181s B 获取锁
|
|
120
|
-
* → T=182s A 的 finally 误清 B 的锁/计时器/controller。
|
|
121
|
-
*
|
|
122
|
-
* 防护方案:
|
|
123
|
-
* - 获取锁时 token 递增:`const myToken = ++this._chatLockToken`
|
|
124
|
-
* - 超时回调校验 `this._chatLockToken === myToken` 后才释放
|
|
125
|
-
* - finally 块校验 `this._chatLockToken === myToken` 后才清理
|
|
126
|
-
* - 不匹配时跳过清理,避免误清新调用者的资源
|
|
125
|
+
* chat() 并发锁管理器(init 时创建,close 时销毁)
|
|
127
126
|
*
|
|
128
|
-
*
|
|
127
|
+
* 职责:chat() 并发锁 + token 校验(race condition 防护)+ 超时保护 + 外部 signal 合并。
|
|
128
|
+
* 详见 ChatLockManager 类注释。
|
|
129
129
|
*/
|
|
130
|
-
|
|
131
|
-
/** 聊天锁超时计时器(防止 LLM 卡死时锁永久持有) */
|
|
132
|
-
chatLockTimer = null;
|
|
133
|
-
/** chat() 内部 AbortController(超时时中断 generator,防止并发) */
|
|
134
|
-
chatAbortController = null;
|
|
135
|
-
/** 记忆衰减定时器(已迁移至 MemoryDecayScheduler,此字段保留用于 close 时引用判断) */
|
|
136
|
-
decayTimer = null;
|
|
130
|
+
chatLockManager = null;
|
|
137
131
|
/** 最近一次 chat() 调用的时间戳 */
|
|
138
132
|
_lastInteractionAt = null;
|
|
133
|
+
/**
|
|
134
|
+
* 对话进行中暂存的配置重载请求集合(chatLock 释放后补执行)
|
|
135
|
+
*
|
|
136
|
+
* 场景:用户在对话中通过 create_persona / create_skill 工具创建配置,
|
|
137
|
+
* reloadConfig 因 chatLock 冲突失败时,将 source 暂存于此。
|
|
138
|
+
* chat() 的 finally 块释放锁后遍历此集合逐个补执行 reloadConfig,
|
|
139
|
+
* 兑现"对话结束后自动加载"的承诺(见 tools.ts createConfigHandler 提示文案)。
|
|
140
|
+
*
|
|
141
|
+
* 用 Set 而非数组:同一 source 多次请求只需补执行一次(去重)。
|
|
142
|
+
*/
|
|
143
|
+
pendingConfigReload = new Set();
|
|
139
144
|
// ─── 衰减职责已拆分至 MemoryDecayScheduler ──────────
|
|
140
145
|
// metricDecayRunCount / metricTotalDecayedCount / metricLastDecayAt 字段
|
|
141
146
|
// 位于 MemoryDecayScheduler 内部,Agent 通过 memoryDecayScheduler.getMetrics() 读取
|
|
@@ -147,7 +152,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
147
152
|
super();
|
|
148
153
|
this.#config = {
|
|
149
154
|
projectPath: opts.projectPath,
|
|
150
|
-
dataDir: opts.dataDir
|
|
155
|
+
dataDir: opts.dataDir,
|
|
151
156
|
registryDir: opts.registryDir,
|
|
152
157
|
maxContextTokens: opts.maxContextTokens ?? AGENT_CONSTANTS.DEFAULT_MAX_CONTEXT_TOKENS,
|
|
153
158
|
personaName: opts.persona,
|
|
@@ -169,27 +174,42 @@ export class Agent extends TypedEventEmitter {
|
|
|
169
174
|
};
|
|
170
175
|
this.#provider = opts.provider;
|
|
171
176
|
this.#backgroundProvider = opts.backgroundProvider ?? null;
|
|
172
|
-
// 如需自定义日志,请在创建 Agent 前调用 `import { setLogger } from '@memora
|
|
177
|
+
// 如需自定义日志,请在创建 Agent 前调用 `import { setLogger } from '@zooique/memora'` 全局设置
|
|
173
178
|
}
|
|
174
179
|
// ─── 生命周期 ─────────────────────────────────────────
|
|
175
180
|
/**
|
|
176
181
|
* 初始化 Agent:加载索引、组装内部组件
|
|
177
182
|
*/
|
|
178
183
|
async init(projectPathOverride) {
|
|
179
|
-
|
|
180
|
-
// 修复 #3:close() 失败不应阻塞 init() 重建
|
|
181
|
-
// 原 close() 异常(如 awaitPendingArchives 超时)会传播到 init() 调用方,
|
|
182
|
-
// 导致 Agent 处于不可用状态。此处捕获后继续重建。
|
|
183
|
-
try {
|
|
184
|
-
await this.close();
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
logger.warn({ err }, 'init() 中 close() 旧实例失败,继续重建');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
184
|
+
await this.disposePreviousInstance();
|
|
190
185
|
if (projectPathOverride) {
|
|
191
186
|
this.#config.projectPath = projectPathOverride;
|
|
192
187
|
}
|
|
188
|
+
const pctx = await this.initializeProject();
|
|
189
|
+
await this.assembleComponents(pctx);
|
|
190
|
+
this.pctx = pctx;
|
|
191
|
+
this.validateCoreComponents();
|
|
192
|
+
this.createPostInitComponents(pctx);
|
|
193
|
+
this._initialized = true;
|
|
194
|
+
return pctx;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 关闭旧实例(如果已初始化),失败不阻塞重建
|
|
198
|
+
*/
|
|
199
|
+
async disposePreviousInstance() {
|
|
200
|
+
if (!this._initialized)
|
|
201
|
+
return;
|
|
202
|
+
try {
|
|
203
|
+
await this.close();
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
logger.warn({ err }, 'init() 中 close() 旧实例失败,继续重建');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 创建 ProjectManager 并初始化项目上下文
|
|
211
|
+
*/
|
|
212
|
+
async initializeProject() {
|
|
193
213
|
this.projectManager = new ProjectManager({
|
|
194
214
|
dataDir: this.#config.dataDir,
|
|
195
215
|
storage: this.#config.storage,
|
|
@@ -197,9 +217,12 @@ export class Agent extends TypedEventEmitter {
|
|
|
197
217
|
// SecurityGuard 由 Agent 层创建,解除 memory→security 反向依赖
|
|
198
218
|
createSecurityGuard: (projectPath, memoraDir, configDir, agentDataDir) => new SecurityGuard(projectPath, memoraDir, this.#config.allowedPaths, this.#config.confirmWrites, this.#config.permission, configDir ?? this.#config.configDir, agentDataDir ?? this.#config.dataDir),
|
|
199
219
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
220
|
+
return this.projectManager.initProject(this.#config.projectPath, undefined, this.#config.configDir);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 校验核心组件(loop / history)非空
|
|
224
|
+
*/
|
|
225
|
+
validateCoreComponents() {
|
|
203
226
|
if (!this.loop || !this.history) {
|
|
204
227
|
throw configError('Agent 初始化失败', '组件组装后 loop 或 history 为空(可能 assembleComponents 抛异常被静默吞掉)', [
|
|
205
228
|
'检查 assembleComponents() 是否有未捕获的异常',
|
|
@@ -207,76 +230,50 @@ export class Agent extends TypedEventEmitter {
|
|
|
207
230
|
'确认 API Key 已配置(环境变量或配置文件)',
|
|
208
231
|
]);
|
|
209
232
|
}
|
|
210
|
-
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 创建 init() 后期组件(初始化完成后才需要的组件)
|
|
236
|
+
*/
|
|
237
|
+
createPostInitComponents(pctx) {
|
|
238
|
+
// chat() 并发锁管理器(生命周期与 Agent 实例一致)
|
|
239
|
+
this.chatLockManager = new ChatLockManager();
|
|
211
240
|
// 归档操作委托给 ArchiveCoordinator
|
|
212
|
-
// 使用 getter 回调注入依赖,close 时 null 化字段后 getter 自然返回 null
|
|
213
241
|
this.archiveCoordinator = new ArchiveCoordinator({
|
|
214
242
|
getUserProfile: () => this.#userProfile,
|
|
215
243
|
getInsightExtractor: () => this.insightExtractor,
|
|
216
244
|
getSessionArchiver: () => this.sessionArchiver,
|
|
245
|
+
getArchiveMode: () => this.#config.archiveMode,
|
|
217
246
|
emit: (event, payload) => this.emit(event, payload),
|
|
218
247
|
});
|
|
219
248
|
// 记忆衰减职责委托给 MemoryDecayScheduler
|
|
220
249
|
this.memoryDecayScheduler = new MemoryDecayScheduler({
|
|
221
250
|
tracer: this.#config.tracer,
|
|
222
|
-
onDecayCompleted: (payload) =>
|
|
251
|
+
onDecayCompleted: (payload) => {
|
|
252
|
+
this.emit(AGENT_EVENTS.decayCompleted, payload);
|
|
253
|
+
// MIND2-D4:从 agent.evaluateTimeliness() 迁移到 governance.evaluateTimeliness()
|
|
254
|
+
void this._governance?.evaluateTimeliness().catch((err) => {
|
|
255
|
+
logger.warn({ err }, 'L2 时效性评估自动触发失败(已降级,不影响衰减循环)');
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
backgroundProvider: this.#backgroundProvider,
|
|
259
|
+
index: pctx.index,
|
|
223
260
|
});
|
|
224
261
|
this.memoryDecayScheduler.start(pctx.index, AGENT_CONSTANTS.DECAY_INTERVAL_MS);
|
|
225
|
-
//
|
|
226
|
-
this.
|
|
227
|
-
return pctx;
|
|
262
|
+
// 记忆治理统一门面(L0/L1/L2/L3 + 诊断)
|
|
263
|
+
this._governance = new MemoryGovernance(this.dedupManager, this.memoryDecayScheduler, this.memoryAdvisor);
|
|
228
264
|
}
|
|
229
265
|
/**
|
|
230
266
|
* 流式对话(核心 API)
|
|
231
267
|
*/
|
|
232
268
|
async *chat(input, signal) {
|
|
233
269
|
this.assertInitialized('chat');
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
if (this._chatBusy) {
|
|
238
|
-
throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再发起新对话', [
|
|
239
|
-
'等待上一轮 chat() 的 AsyncGenerator 耗尽(收到 done 事件)',
|
|
240
|
-
'宿主程序应确保同一时间只有一个 chat() 调用',
|
|
241
|
-
]);
|
|
242
|
-
}
|
|
243
|
-
this._chatBusy = true;
|
|
244
|
-
// 分配本调用的 token,超时回调和 finally 块据此判断是否仍是当前持有者
|
|
245
|
-
// 避免 race condition:超时释放后新调用获取锁,旧 finally 误清新调用者的资源
|
|
246
|
-
const myToken = ++this._chatLockToken;
|
|
247
|
-
// 内部 AbortController,超时时中断 generator 而非仅释放锁
|
|
248
|
-
const internalAbort = new AbortController();
|
|
249
|
-
this.chatAbortController = internalAbort;
|
|
250
|
-
// 合并外部 signal:外部 abort 时也触发内部
|
|
251
|
-
const onExternalAbort = () => internalAbort.abort();
|
|
252
|
-
signal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
253
|
-
// 修复 #4:addEventListener 对已 aborted 的 signal 不触发回调
|
|
254
|
-
// 需手动检查并触发 internalAbort,否则外部已取消的请求仍会进入主流程
|
|
255
|
-
if (signal?.aborted) {
|
|
256
|
-
internalAbort.abort();
|
|
257
|
-
}
|
|
258
|
-
const combinedSignal = internalAbort.signal;
|
|
259
|
-
// 超时保护:LLM 卡死时中断 generator + 释放锁,防止并发
|
|
260
|
-
// 超时回调校验 token 后才清理,避免误清新调用者的资源
|
|
261
|
-
this.chatLockTimer = safeSetTimeout(() => {
|
|
262
|
-
// 令牌不匹配:锁已被新调用者获取(或本调用已正常退出),跳过清理
|
|
263
|
-
if (this._chatLockToken !== myToken) {
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
logger.warn({ timeoutMs: AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS }, 'chat() 锁超时,中断 generator 并释放锁');
|
|
267
|
-
internalAbort.abort();
|
|
268
|
-
this._chatBusy = false;
|
|
269
|
-
this.chatLockTimer = null;
|
|
270
|
-
this.chatAbortController = null;
|
|
271
|
-
}, AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS);
|
|
270
|
+
this.validateChatInput(input);
|
|
271
|
+
const lockCtx = this.acquireChatLock(signal);
|
|
272
|
+
const { myToken, combinedSignal, cleanupExternalSignal } = lockCtx;
|
|
272
273
|
try {
|
|
273
274
|
this._lastInteractionAt = new Date();
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
this.requireNonNull(this.loop, 'loop').cleanTemporarySystemMessages();
|
|
277
|
-
// 基元驱动召回(双通道:语义 + 关键词)
|
|
278
|
-
yield { type: 'thinking', phase: 'recalling' };
|
|
279
|
-
const recalledMemories = await this.recallAndInject(input);
|
|
275
|
+
// 上下文准备:角色匹配 → 记忆召回 → 技能注入 → 历史追加
|
|
276
|
+
const recalledMemories = yield* this.prepareChatContext(input, combinedSignal);
|
|
280
277
|
if (combinedSignal.aborted) {
|
|
281
278
|
yield {
|
|
282
279
|
type: 'aborted',
|
|
@@ -284,78 +281,128 @@ export class Agent extends TypedEventEmitter {
|
|
|
284
281
|
};
|
|
285
282
|
return;
|
|
286
283
|
}
|
|
287
|
-
//
|
|
288
|
-
yield
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
284
|
+
// LLM 流式调用 → 助手消息写历史 → 后处理
|
|
285
|
+
yield* this.executeChatLoop(input, recalledMemories, combinedSignal);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
// 仅当本调用仍是当前锁持有者时才清理资源(token 校验)
|
|
289
|
+
this.chatLockManager?.release(myToken);
|
|
290
|
+
cleanupExternalSignal();
|
|
291
|
+
// 补执行对话期间暂存的配置重载请求
|
|
292
|
+
await this.flushPendingConfigReload();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 校验 chat 输入
|
|
297
|
+
*/
|
|
298
|
+
validateChatInput(input) {
|
|
299
|
+
if (input.length > AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH) {
|
|
300
|
+
throw configError('输入过长', `输入超过最大长度限制(${AGENT_CONSTANTS.CHAT_INPUT_MAX_LENGTH / 1024}KB)`, ['缩短输入内容', '分多次对话发送']);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 获取对话锁 — 返回锁上下文
|
|
305
|
+
*/
|
|
306
|
+
acquireChatLock(signal) {
|
|
307
|
+
const chatLock = this.chatLockManager;
|
|
308
|
+
if (!chatLock)
|
|
309
|
+
throw configError('ChatLockManager', 'ChatLockManager 未初始化', []);
|
|
310
|
+
if (chatLock.isBusy) {
|
|
311
|
+
throw chatBusyError('发起新对话');
|
|
312
|
+
}
|
|
313
|
+
const { token: myToken, internalAbort } = chatLock.acquire(AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS);
|
|
314
|
+
const cleanupExternalSignal = chatLock.attachExternalSignal(signal, internalAbort);
|
|
315
|
+
return { myToken, internalAbort, cleanupExternalSignal, combinedSignal: internalAbort.signal };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 准备对话上下文:角色匹配 → 记忆召回 → 技能注入 → 用户消息写历史
|
|
319
|
+
* @returns 召回的记忆列表(供 loop.processUserInput 注入为 system 消息)
|
|
320
|
+
*/
|
|
321
|
+
async *prepareChatContext(input, combinedSignal) {
|
|
322
|
+
const loop = this.requireLoop;
|
|
323
|
+
loop.cleanTemporarySystemMessages();
|
|
324
|
+
// 角色自动匹配(回答前执行)
|
|
325
|
+
await this.tryAutoMatchPersona(input);
|
|
326
|
+
// 基元驱动召回(双通道:语义 + 关键词)
|
|
327
|
+
yield { type: 'thinking', phase: 'recalling' };
|
|
328
|
+
const recalledMemories = await this.recallAndInject(input);
|
|
329
|
+
if (combinedSignal.aborted)
|
|
330
|
+
return recalledMemories;
|
|
331
|
+
// 技能关键词/正则匹配(当轮立即注入生效)
|
|
332
|
+
yield { type: 'thinking', phase: 'processing' };
|
|
333
|
+
this.matchAndInjectSkill(input);
|
|
334
|
+
const history = this.requireHistory;
|
|
335
|
+
await history.appendUser(input);
|
|
336
|
+
return recalledMemories;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 执行 LLM 流式循环 → 追加助手消息 → 触发后处理
|
|
340
|
+
*/
|
|
341
|
+
async *executeChatLoop(input, recalledMemories, combinedSignal) {
|
|
342
|
+
const loop = this.requireLoop;
|
|
343
|
+
const history = this.requireHistory;
|
|
344
|
+
let assistantContent = '';
|
|
345
|
+
let wasAborted = false;
|
|
346
|
+
try {
|
|
347
|
+
for await (const chunk of loop.processUserInput(input, recalledMemories, combinedSignal)) {
|
|
348
|
+
yield chunk;
|
|
349
|
+
if (chunk.type === 'text') {
|
|
350
|
+
assistantContent += chunk.content;
|
|
315
351
|
}
|
|
316
|
-
else {
|
|
317
|
-
|
|
318
|
-
return;
|
|
352
|
+
else if (chunk.type === 'aborted') {
|
|
353
|
+
wasAborted = true;
|
|
319
354
|
}
|
|
320
355
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (assistantContent.trim()) {
|
|
326
|
-
const interruptedMark = this.#config.messages?.interrupted ?? '\n\n[已中断]';
|
|
327
|
-
try {
|
|
328
|
-
await this.requireNonNull(this.history, 'history').appendAssistant(assistantContent + interruptedMark);
|
|
329
|
-
}
|
|
330
|
-
catch (err) {
|
|
331
|
-
logger.warn({ err }, '中断消息历史写入失败');
|
|
332
|
-
}
|
|
333
|
-
}
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
if (isAbortError(err)) {
|
|
359
|
+
yield { type: 'aborted', reason: 'User cancelled the conversation' };
|
|
334
360
|
return;
|
|
335
361
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
logger.warn({ err }, '助手消息历史写入失败');
|
|
362
|
+
else {
|
|
363
|
+
yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
|
|
364
|
+
return;
|
|
342
365
|
}
|
|
343
|
-
// 后处理阶段
|
|
344
|
-
yield { type: 'thinking', phase: 'archiving' };
|
|
345
|
-
await this.postProcess(input, assistantContent);
|
|
346
366
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
this.chatAbortController = null;
|
|
353
|
-
if (this.chatLockTimer) {
|
|
354
|
-
clearSafeTimeout(this.chatLockTimer);
|
|
355
|
-
this.chatLockTimer = null;
|
|
367
|
+
if (wasAborted) {
|
|
368
|
+
if (assistantContent.trim()) {
|
|
369
|
+
const interruptedMark = this.#config.messages?.interrupted ?? '\n\n[已中断]';
|
|
370
|
+
try {
|
|
371
|
+
await history.appendAssistant(assistantContent + interruptedMark);
|
|
356
372
|
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
logger.warn({ err }, '中断消息历史写入失败');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// 追加助手消息到历史(best-effort)
|
|
380
|
+
try {
|
|
381
|
+
await history.appendAssistant(assistantContent);
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
logger.warn({ err }, '助手消息历史写入失败');
|
|
385
|
+
}
|
|
386
|
+
// 后处理阶段
|
|
387
|
+
yield { type: 'thinking', phase: 'archiving' };
|
|
388
|
+
await this.postProcess(input, assistantContent);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 补执行对话期间暂存的配置重载请求
|
|
392
|
+
*/
|
|
393
|
+
async flushPendingConfigReload() {
|
|
394
|
+
if (this.pendingConfigReload.size === 0)
|
|
395
|
+
return;
|
|
396
|
+
const pending = Array.from(this.pendingConfigReload);
|
|
397
|
+
this.pendingConfigReload.clear();
|
|
398
|
+
for (const src of pending) {
|
|
399
|
+
try {
|
|
400
|
+
await this.reloadConfig(src);
|
|
401
|
+
this.emit(AGENT_EVENTS.configReloaded, { source: src });
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
logger.warn({ err, source: src }, '补执行配置重载失败');
|
|
357
405
|
}
|
|
358
|
-
signal?.removeEventListener('abort', onExternalAbort);
|
|
359
406
|
}
|
|
360
407
|
}
|
|
361
408
|
/**
|
|
@@ -369,7 +416,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
369
416
|
*
|
|
370
417
|
* 本方法让宿主在确认 generator 挂起后强制释放锁,让用户能立即发起新对话。
|
|
371
418
|
*
|
|
372
|
-
*
|
|
419
|
+
* 安全机制(委托至 ChatLockManager.forceRelease):
|
|
373
420
|
* - 递增 _chatLockToken,让原 chat() 的 finally 块检测到 token 不匹配后
|
|
374
421
|
* 跳过资源清理(避免误清新调用者的 _chatBusy/chatAbortController)
|
|
375
422
|
* - abort chatAbortController,让响应 signal 的 await 点(如 fetch)退出
|
|
@@ -384,22 +431,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
384
431
|
* - 调用后不应再消费原 chat() generator 的后续 chunk(token 已变,chunk 无意义)
|
|
385
432
|
*/
|
|
386
433
|
forceReleaseChatLock() {
|
|
387
|
-
|
|
388
|
-
return;
|
|
389
|
-
// 递增 token 让原 chat() 的 finally 块跳过清理(避免误清新调用者的资源)
|
|
390
|
-
this._chatLockToken++;
|
|
391
|
-
// abort 当前 generator(响应 signal 的 await 点会 throw AbortError 退出)
|
|
392
|
-
if (this.chatAbortController) {
|
|
393
|
-
this.chatAbortController.abort();
|
|
394
|
-
this.chatAbortController = null;
|
|
395
|
-
}
|
|
396
|
-
// 清理锁超时定时器(避免后续触发重复清理)
|
|
397
|
-
if (this.chatLockTimer) {
|
|
398
|
-
clearSafeTimeout(this.chatLockTimer);
|
|
399
|
-
this.chatLockTimer = null;
|
|
400
|
-
}
|
|
401
|
-
this._chatBusy = false;
|
|
402
|
-
logger.warn('对话锁被强制释放(宿主无进展超时兜底)');
|
|
434
|
+
this.chatLockManager?.forceRelease();
|
|
403
435
|
}
|
|
404
436
|
/**
|
|
405
437
|
* 召回记忆 + 注入最近对话上下文
|
|
@@ -412,17 +444,41 @@ export class Agent extends TypedEventEmitter {
|
|
|
412
444
|
* @returns 召回的记忆列表
|
|
413
445
|
*/
|
|
414
446
|
async recallAndInject(input) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
447
|
+
// 实际 recall() 函数耗时 span(区别于 loop.ts 的 RECALL 注入 span)
|
|
448
|
+
const tracer = this.#config.tracer ?? NOOP_TRACER;
|
|
449
|
+
const recallSpan = tracer.startSpan(TRACE_SPANS.RECALL_ACTUAL, {
|
|
450
|
+
queryLength: input.length,
|
|
451
|
+
hasVectorStore: !!this.#config.vectorStore,
|
|
419
452
|
});
|
|
453
|
+
let recalledMemories = [];
|
|
454
|
+
try {
|
|
455
|
+
recalledMemories = await recall(this.requirePctx.index, input, {
|
|
456
|
+
limit: AGENT_CONSTANTS.DEFAULT_RECALL_LIMIT,
|
|
457
|
+
vectorStore: this.#config.vectorStore,
|
|
458
|
+
excludeSources: this.#config.recallExcludeSources,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
recallSpan.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
recallSpan.setAttribute('resultCount', recalledMemories.length);
|
|
467
|
+
recallSpan.end();
|
|
468
|
+
}
|
|
420
469
|
if (recalledMemories.length > 0) {
|
|
421
|
-
this.emit(
|
|
470
|
+
this.emit(AGENT_EVENTS.memoryRecalled, { count: recalledMemories.length, query: input });
|
|
471
|
+
// FIX-P1-2:boost 持久化拆分为 fire-and-forget,不阻塞 chat 读路径
|
|
472
|
+
// boost 是软指标(每次 +0.05,上限 1.0),写入失败仅 log 不影响 chat 流程
|
|
473
|
+
const ids = recalledMemories.map((m) => m.id);
|
|
474
|
+
void boostScores(this.requirePctx.index, ids).catch((err) => {
|
|
475
|
+
logger.warn({ err }, 'boost 持久化失败(不影响 chat 流程)');
|
|
476
|
+
this.emit(AGENT_EVENTS.boostPersistFailed, { memoryId: ids.join(','), message: toError(err).message });
|
|
477
|
+
});
|
|
422
478
|
}
|
|
423
479
|
// Layer 5: 最近对话注入
|
|
424
|
-
const loop = this.
|
|
425
|
-
const recentHistory = loop.getRecentHistory(
|
|
480
|
+
const loop = this.requireLoop;
|
|
481
|
+
const recentHistory = loop.getRecentHistory(AGENT_CONSTANTS.DEFAULT_RECENT_HISTORY_ROUNDS);
|
|
426
482
|
if (recentHistory.length > 0) {
|
|
427
483
|
const msgs = this.#config.messages;
|
|
428
484
|
const label = msgs?.recentConversationLabel ?? '[Recent conversation]';
|
|
@@ -438,16 +494,34 @@ export class Agent extends TypedEventEmitter {
|
|
|
438
494
|
return recalledMemories;
|
|
439
495
|
}
|
|
440
496
|
/**
|
|
441
|
-
*
|
|
497
|
+
* 匹配技能并立即注入本轮对话(当轮生效,不延迟到下一轮)
|
|
498
|
+
*
|
|
499
|
+
* 与 persona 的 tryAutoMatchPersona 同模式:匹配 → 注入 → 本轮 LLM 即生效。
|
|
500
|
+
* 原设计为 injectActiveSkill(注入上一轮匹配结果),导致用户说"写代码"的第一轮
|
|
501
|
+
* 得不到技能增强,需再发一条消息才生效。已改为实时匹配注入。
|
|
502
|
+
*
|
|
503
|
+
* 匹配策略:regex trigger 优先(score=1.0),其次关键词匹配(阈值 0.3)。
|
|
504
|
+
* 清理机制:cleanTemporarySystemMessages() 每轮开头清除临时 system 消息,
|
|
505
|
+
* 技能 prompt 自然不会累积到下一轮。
|
|
506
|
+
*
|
|
507
|
+
* @param input 用户输入文本
|
|
442
508
|
*/
|
|
443
|
-
|
|
444
|
-
if (this.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
509
|
+
matchAndInjectSkill(input) {
|
|
510
|
+
if (!this.skillManager || !this.loop)
|
|
511
|
+
return;
|
|
512
|
+
try {
|
|
513
|
+
const match = this.skillManager.match(input);
|
|
514
|
+
if (match) {
|
|
515
|
+
const skillPrompt = this.skillManager.buildSystemPrompt(match.skill.name);
|
|
516
|
+
if (skillPrompt) {
|
|
517
|
+
this.loop.injectSystemMessage(skillPrompt);
|
|
518
|
+
logger.debug({ skill: match.skill.name, score: match.score }, '技能 prompt 已注入(当轮生效)');
|
|
519
|
+
}
|
|
520
|
+
this.emit(AGENT_EVENTS.skillMatched, { skill: match.skill.name, score: match.score });
|
|
449
521
|
}
|
|
450
|
-
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
logger.warn({ err }, '技能匹配失败');
|
|
451
525
|
}
|
|
452
526
|
}
|
|
453
527
|
/**
|
|
@@ -464,94 +538,59 @@ export class Agent extends TypedEventEmitter {
|
|
|
464
538
|
* (会话归档实现后,`insights-only` 将跳过对话原始内容自动归档)
|
|
465
539
|
*/
|
|
466
540
|
async postProcess(input, assistantContent) {
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.emit('personaSwitched', { from: prevName, to: matchedPersona });
|
|
475
|
-
if (this.loop) {
|
|
476
|
-
const profilePrompt = this.userProfile?.buildSystemPrompt() ?? '';
|
|
477
|
-
const personaPrompt = this.personaManager.buildSystemPrompt();
|
|
478
|
-
const newPrefix = [personaPrompt, profilePrompt].filter(Boolean).join('\n\n') +
|
|
479
|
-
([personaPrompt, profilePrompt].some(Boolean) ? '\n\n---\n\n' : '');
|
|
480
|
-
this.loop.refreshPersonaPrefix(newPrefix);
|
|
481
|
-
}
|
|
482
|
-
logger.info({ persona: matchedPersona }, '角色自动切换');
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch (err) {
|
|
486
|
-
logger.warn({ err }, '角色自动匹配失败');
|
|
487
|
-
}
|
|
541
|
+
// postProcess 全流程 span(角色匹配 + 技能匹配 + 归档 + AutoConfigRefiner)
|
|
542
|
+
const tracer = this.#config.tracer ?? NOOP_TRACER;
|
|
543
|
+
const span = tracer.startSpan(TRACE_SPANS.POST_PROCESS, {
|
|
544
|
+
archiveMode: this.#config.archiveMode,
|
|
545
|
+
});
|
|
546
|
+
try {
|
|
547
|
+
await this.doPostProcess(input, assistantContent);
|
|
488
548
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
try {
|
|
492
|
-
const match = this.skillManager.match(input);
|
|
493
|
-
if (match) {
|
|
494
|
-
this.activeSkill = match.skill.name;
|
|
495
|
-
// 发射 skillMatched 事件:宿主 UI 可据此展示当前激活技能
|
|
496
|
-
this.emit('skillMatched', { skill: match.skill.name, score: match.score });
|
|
497
|
-
logger.debug({ skill: match.skill.name, score: match.score }, '技能匹配,下一轮注入');
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
catch (err) {
|
|
501
|
-
logger.warn({ err }, '技能匹配失败');
|
|
502
|
-
}
|
|
549
|
+
finally {
|
|
550
|
+
span.end();
|
|
503
551
|
}
|
|
504
|
-
|
|
505
|
-
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* postProcess 内部实现(为 span 埋点提供 try/finally 包裹边界)
|
|
555
|
+
*
|
|
556
|
+
* 原 postProcess 逻辑完整保留于此,由外层 postProcess 负责 span 生命周期管理。
|
|
557
|
+
*/
|
|
558
|
+
async doPostProcess(input, assistantContent) {
|
|
559
|
+
// 技能匹配已迁移到 chat() 开头的 matchAndInjectSkill()(当轮实时生效),
|
|
560
|
+
// 与 persona 的 tryAutoMatchPersona 同模式,消除"第一轮无技能"的一轮延迟问题。
|
|
561
|
+
// ADR-015 + FIX-P1-4: archiveMode 三态控制集中到 ArchiveCoordinator
|
|
562
|
+
// 此处统一传 { autoTriggered: true },由 ArchiveCoordinator 内部按 archiveMode 判断是否跳过:
|
|
563
|
+
// - manual 模式 → 跳过 profile/insight 自动归档(用户需手动调用)
|
|
564
|
+
// - full / insights-only 模式 → 执行
|
|
506
565
|
// 角色匹配/技能匹配/AutoConfigRefiner 属"配置学习"行为,非归档,每轮都执行。
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
566
|
+
const history = this.requireHistory;
|
|
567
|
+
// 用户画像实时归档(委托 ArchiveCoordinator,与手动归档路径统一,消除 DRY 违反)
|
|
568
|
+
try {
|
|
569
|
+
// fire-and-forget 包装:registerPendingArchive 确保 close() 时等待后台归档完成
|
|
570
|
+
// 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
|
|
571
|
+
const archiveFactsPromise = this.requireArchiveCoordinator.archiveProfileFacts(input, { autoTriggered: true }).then(() => { }, (err) => {
|
|
572
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
573
|
+
logger.warn({ err, stage: 'profile' }, '归档失败');
|
|
574
|
+
this.emit(AGENT_EVENTS.archiveFailed, { stage: 'profile', message: message.slice(0, 200) });
|
|
575
|
+
});
|
|
576
|
+
history.registerPendingArchive(archiveFactsPromise);
|
|
510
577
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (!skipAutoArchive && this.#userProfile) {
|
|
514
|
-
try {
|
|
515
|
-
const turnIndex = `turn-${Date.now()}`;
|
|
516
|
-
const facts = extractUserFacts(input, turnIndex);
|
|
517
|
-
// 注册到 pendingArchives,确保 close() 时等待后台归档完成,避免写入已关闭的存储
|
|
518
|
-
const archiveFactsPromise = this.#userProfile.archiveFacts(facts).then((entries) => {
|
|
519
|
-
// 发射 memoryAdded 事件:仅对已确认且写入存储的条目(confirmed=true)
|
|
520
|
-
for (const entry of entries) {
|
|
521
|
-
if (entry.confirmed) {
|
|
522
|
-
this.emit('memoryAdded', { id: entry.id, source: 'profile', name: entry.value });
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}).catch((err) => {
|
|
526
|
-
logger.warn({ err }, '用户画像实时归档失败');
|
|
527
|
-
});
|
|
528
|
-
this.requireNonNull(this.history, 'history').registerPendingArchive(archiveFactsPromise);
|
|
529
|
-
}
|
|
530
|
-
catch (err) {
|
|
531
|
-
logger.warn({ err }, '用户画像归档初始化失败');
|
|
532
|
-
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
logger.warn({ err }, '用户画像归档初始化失败');
|
|
533
580
|
}
|
|
534
|
-
// 输入分类 → Insight
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
logger.warn({ err }, 'Insight 提取失败');
|
|
548
|
-
});
|
|
549
|
-
this.requireNonNull(this.history, 'history').registerPendingArchive(p);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
catch (err) {
|
|
553
|
-
logger.warn({ err }, 'Insight 提取初始化失败');
|
|
554
|
-
}
|
|
581
|
+
// 输入分类 → Insight 提取(委托 ArchiveCoordinator,与手动归档路径统一)
|
|
582
|
+
try {
|
|
583
|
+
// fire-and-forget 包装:classify 判断由 ArchiveCoordinator 内部完成
|
|
584
|
+
// 失败时发射 archiveFailed 事件,让宿主 UI 可通知用户(而非静默吞没)
|
|
585
|
+
const archiveInsightPromise = this.requireArchiveCoordinator.archiveInsight(input, assistantContent, { autoTriggered: true }).then(() => { }, (err) => {
|
|
586
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
587
|
+
logger.warn({ err, stage: 'insight' }, '归档失败');
|
|
588
|
+
this.emit(AGENT_EVENTS.archiveFailed, { stage: 'insight', message: message.slice(0, 200) });
|
|
589
|
+
});
|
|
590
|
+
history.registerPendingArchive(archiveInsightPromise);
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
logger.warn({ err }, 'Insight 提取初始化失败');
|
|
555
594
|
}
|
|
556
595
|
// AutoConfigRefiner(模式 3:Agent 智能总结)
|
|
557
596
|
if (this.autoConfigRefiner) {
|
|
@@ -560,7 +599,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
560
599
|
const analyzePromise = this.autoConfigRefiner.analyze(input, assistantContent).catch((err) => {
|
|
561
600
|
logger.warn({ err }, 'AutoConfigRefiner 分析失败');
|
|
562
601
|
});
|
|
563
|
-
|
|
602
|
+
history.registerPendingArchive(analyzePromise);
|
|
564
603
|
}
|
|
565
604
|
catch (err) {
|
|
566
605
|
logger.warn({ err }, 'AutoConfigRefiner 初始化失败');
|
|
@@ -587,7 +626,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
587
626
|
*/
|
|
588
627
|
forkSession(targetSession) {
|
|
589
628
|
this.assertInitialized('forkSession');
|
|
590
|
-
return this.
|
|
629
|
+
return this.requireSessionManager.forkSession(targetSession);
|
|
591
630
|
}
|
|
592
631
|
/**
|
|
593
632
|
* 切换到指定项目
|
|
@@ -598,12 +637,8 @@ export class Agent extends TypedEventEmitter {
|
|
|
598
637
|
async switchProject(nameOrPath) {
|
|
599
638
|
this.assertInitialized('switchProject', ['projectManager', 'provider']);
|
|
600
639
|
// 对话进行中切换项目会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
'等待上一轮 chat() 的 AsyncGenerator 耗尽',
|
|
604
|
-
]);
|
|
605
|
-
}
|
|
606
|
-
const pm = this.requireNonNull(this.projectManager, 'projectManager');
|
|
640
|
+
this.assertNotBusy('切换项目');
|
|
641
|
+
const pm = this.projectManager;
|
|
607
642
|
const projects = pm.list;
|
|
608
643
|
let target = projects.find((p) => p.name === nameOrPath || p.path === nameOrPath);
|
|
609
644
|
if (!target) {
|
|
@@ -611,14 +646,25 @@ export class Agent extends TypedEventEmitter {
|
|
|
611
646
|
target = projects.find((p) => p.name.toLowerCase() === nameOrPathLower || p.path.toLowerCase() === nameOrPathLower);
|
|
612
647
|
}
|
|
613
648
|
const projectPath = target ? target.path : nameOrPath;
|
|
614
|
-
const projectName = target ? target.name :
|
|
649
|
+
const projectName = target ? target.name : getBaseName(nameOrPath);
|
|
615
650
|
const newPctx = await pm.initProject(projectPath, projectName, this.#config.configDir);
|
|
616
651
|
// 记录源项目路径(用于事件),切换前 pctx 可能不存在(首次初始化)
|
|
617
652
|
const fromProjectPath = this.pctx?.projectPath ?? null;
|
|
618
|
-
|
|
619
|
-
|
|
653
|
+
// 先尝试重建组件,失败时回滚 pctx 防止状态不一致
|
|
654
|
+
try {
|
|
655
|
+
this.pctx = newPctx;
|
|
656
|
+
await this.rebuildComponentsWithCurrentCtx();
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
// 回滚:恢复旧 pctx(若存在)
|
|
660
|
+
if (fromProjectPath) {
|
|
661
|
+
logger.warn({ err: toError(err) }, 'switchProject: 组件重建失败,回滚项目上下文');
|
|
662
|
+
this.pctx = { projectPath: fromProjectPath };
|
|
663
|
+
}
|
|
664
|
+
throw err;
|
|
665
|
+
}
|
|
620
666
|
// 发射项目切换事件(供宿主 UI 刷新项目相关界面)
|
|
621
|
-
this.emit(
|
|
667
|
+
this.emit(AGENT_EVENTS.projectSwitched, {
|
|
622
668
|
from: fromProjectPath,
|
|
623
669
|
to: projectPath,
|
|
624
670
|
projectName,
|
|
@@ -643,6 +689,18 @@ export class Agent extends TypedEventEmitter {
|
|
|
643
689
|
messages: this.#config.messages,
|
|
644
690
|
enableContextSummary: this.#config.enableContextSummary,
|
|
645
691
|
existingSkillManager: this.skillManager,
|
|
692
|
+
onWorkProjectionGenerated: (sourcePath, summary) => {
|
|
693
|
+
this.emit(AGENT_EVENTS.workProjectionGenerated, { sourcePath, summary });
|
|
694
|
+
},
|
|
695
|
+
onContextTruncated: (skippedCount, keptCount) => {
|
|
696
|
+
this.emit(AGENT_EVENTS.contextTruncated, { skippedCount, keptCount });
|
|
697
|
+
},
|
|
698
|
+
onDedupCompleted: (report) => {
|
|
699
|
+
this.emit(AGENT_EVENTS.dedupCompleted, { deduplicatedCount: report.deduplicatedCount, demotedIds: report.demotedIds });
|
|
700
|
+
},
|
|
701
|
+
onGuardrailError: (rule, message) => {
|
|
702
|
+
this.emit(AGENT_EVENTS.guardrailError, { rule, message });
|
|
703
|
+
},
|
|
646
704
|
});
|
|
647
705
|
this.history = result.history;
|
|
648
706
|
this.loop = result.loop;
|
|
@@ -653,14 +711,16 @@ export class Agent extends TypedEventEmitter {
|
|
|
653
711
|
this.insightExtractor = result.insightExtractor;
|
|
654
712
|
this.configManager = result.configManager;
|
|
655
713
|
this.memoryInspector = result.memoryInspector;
|
|
656
|
-
this
|
|
714
|
+
this.dedupManager = result.dedupManager;
|
|
715
|
+
this.memoryAdvisor = result.memoryAdvisor;
|
|
657
716
|
this.autoConfigRefiner = result.autoConfigRefiner;
|
|
658
717
|
this.workProjection = result.workProjection;
|
|
659
718
|
this.sessionArchiver = result.sessionArchiver;
|
|
719
|
+
this.textPolisher = result.textPolisher;
|
|
660
720
|
// 绑定冲突检测回调,InsightExtractor 检测到 contradicts 时 emit('conflictDetected')
|
|
661
721
|
// 与 bindGetRecentHistory 同模式:解决 Agent 晚于 InsightExtractor 创建的时序循环依赖
|
|
662
722
|
this.insightExtractor.bindOnConflict((info) => {
|
|
663
|
-
this.emit(
|
|
723
|
+
this.emit(AGENT_EVENTS.conflictDetected, info);
|
|
664
724
|
});
|
|
665
725
|
// 注入 VectorStore 到 MemoryInspector,启用混合搜索
|
|
666
726
|
if (this.memoryInspector && this.#config.vectorStore) {
|
|
@@ -692,21 +752,19 @@ export class Agent extends TypedEventEmitter {
|
|
|
692
752
|
*/
|
|
693
753
|
createSessionManager() {
|
|
694
754
|
const forwardEvent = (event, data) => {
|
|
695
|
-
if (
|
|
755
|
+
if (AGENT_EVENT_SET.has(event)) {
|
|
696
756
|
this.emit(event, data);
|
|
697
757
|
}
|
|
698
758
|
};
|
|
699
|
-
return new SessionManager(
|
|
759
|
+
return new SessionManager(
|
|
760
|
+
// 惰性 getter:SessionManager 内部调用时 Agent 已 init,用 ! 窄化
|
|
761
|
+
() => this.history, () => this.loop, this.#config.sessionStore, () => this.chatLockManager?.isBusy ?? false, forwardEvent);
|
|
700
762
|
}
|
|
701
763
|
// ─── Provider 管理 ────────────────────────────────────
|
|
702
764
|
setProvider(provider) {
|
|
703
765
|
// 对话进行中切换 Provider 会导致同一 processUserInput 循环内前后两次 LLM 调用命中不同 Provider
|
|
704
766
|
// (模型上下文窗口假设不一致 → 可能导致上下文截断逻辑误判或 tool_call 格式不兼容)
|
|
705
|
-
|
|
706
|
-
throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换 Provider', [
|
|
707
|
-
'等待上一轮 chat() 的 AsyncGenerator 耗尽',
|
|
708
|
-
]);
|
|
709
|
-
}
|
|
767
|
+
this.assertNotBusy('切换 Provider');
|
|
710
768
|
this.#provider = provider;
|
|
711
769
|
if (this.loop) {
|
|
712
770
|
this.loop.setProvider(provider);
|
|
@@ -715,11 +773,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
715
773
|
}
|
|
716
774
|
setBackgroundProvider(provider) {
|
|
717
775
|
// 与 setProvider 一致,对话进行中禁止切换后台 Provider
|
|
718
|
-
|
|
719
|
-
throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换后台 Provider', [
|
|
720
|
-
'等待上一轮 chat() 的 AsyncGenerator 耗尽',
|
|
721
|
-
]);
|
|
722
|
-
}
|
|
776
|
+
this.assertNotBusy('切换后台 Provider');
|
|
723
777
|
this.#backgroundProvider = provider;
|
|
724
778
|
// 同步更新 AutoConfigRefiner 的后台 Provider
|
|
725
779
|
if (this.autoConfigRefiner) {
|
|
@@ -736,15 +790,12 @@ export class Agent extends TypedEventEmitter {
|
|
|
736
790
|
* @param mode 目标模式
|
|
737
791
|
*/
|
|
738
792
|
setArchiveMode(mode) {
|
|
739
|
-
|
|
740
|
-
throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换归档模式', [
|
|
741
|
-
'等待上一轮 chat() 的 AsyncGenerator 耗尽',
|
|
742
|
-
]);
|
|
743
|
-
}
|
|
793
|
+
this.assertNotBusy('切换归档模式');
|
|
744
794
|
const prev = this.#config.archiveMode;
|
|
745
795
|
if (prev === mode)
|
|
746
796
|
return; // 幂等:无变更直接返回
|
|
747
797
|
this.#config.archiveMode = mode;
|
|
798
|
+
this.emit(AGENT_EVENTS.archiveModeChanged, { from: prev, to: mode });
|
|
748
799
|
logger.info({ from: prev, to: mode }, '归档模式已切换');
|
|
749
800
|
}
|
|
750
801
|
/**
|
|
@@ -755,20 +806,180 @@ export class Agent extends TypedEventEmitter {
|
|
|
755
806
|
getArchiveMode() {
|
|
756
807
|
return this.#config.archiveMode;
|
|
757
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* 手动切换角色(宿主 UI 角色选择器入口)
|
|
811
|
+
*
|
|
812
|
+
* 与 doPostProcess 中的自动匹配走同一条事件链路,确保:
|
|
813
|
+
* 1. AgentLoop 的 systemPromptPrefix 立即刷新(下一次对话使用新角色 prompt)
|
|
814
|
+
* 2. 发射 personaSwitched 事件,触发 spriteLifecycleManager 的完整副作用:
|
|
815
|
+
* - emit('personaChanged') 通知宿主 UI
|
|
816
|
+
* - proactiveEngine.addNotice('persona', ...) 记录通知
|
|
817
|
+
* - perceptionCoordinator.refreshBeforeChat() 基于新角色 traits 重新推导情感基调
|
|
818
|
+
*
|
|
819
|
+
* 对话进行中切换角色会破坏当前 system prompt,与项目切换一致拒绝。
|
|
820
|
+
*
|
|
821
|
+
* @param name 目标角色名
|
|
822
|
+
* @returns 切换成功返回新角色的 system prompt 段;角色不存在或切换失败返回 null
|
|
823
|
+
*/
|
|
824
|
+
switchPersona(name) {
|
|
825
|
+
this.assertInitialized('switchPersona');
|
|
826
|
+
this.assertNotBusy('切换角色');
|
|
827
|
+
if (!this.personaManager)
|
|
828
|
+
return null;
|
|
829
|
+
// 同名切换幂等:直接返回当前 prompt,不触发事件链路
|
|
830
|
+
if (this.personaManager.activeName === name) {
|
|
831
|
+
return this.personaManager.buildSystemPrompt();
|
|
832
|
+
}
|
|
833
|
+
const prevName = this.personaManager.activeName;
|
|
834
|
+
try {
|
|
835
|
+
this.personaManager.switchPersona(name);
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
// 角色不存在时 PersonaManager 抛 MemoraError,降级为 null 返回
|
|
839
|
+
logger.warn({ err: toError(err).message, name }, '手动切换角色失败');
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
// 同步刷新 AgentLoop 的角色前缀(关键:否则下一次对话仍用旧角色 prompt)
|
|
843
|
+
// 与 doPostProcess 自动匹配共用同一段逻辑,ADR-017 枝叶层 2 次提取
|
|
844
|
+
this.refreshPersonaPrefixOnLoop();
|
|
845
|
+
// 发射切换事件,触发宿主 UI 刷新 + 感知重推导 + 通知队列记录
|
|
846
|
+
this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: name });
|
|
847
|
+
logger.info({ from: prevName, to: name }, '角色手动切换');
|
|
848
|
+
return this.personaManager.buildSystemPrompt();
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* 获取角色切换锁定状态(透传 PersonaManager,P0-2 用户体验打磨)
|
|
852
|
+
*
|
|
853
|
+
* 与 switchPersona 分离:switchPersona 仍返回 string | null 不变,
|
|
854
|
+
* 锁定原因查询走独立路径,避免破坏既有契约(cli.ts、sprite.test.ts 等消费者无感)。
|
|
855
|
+
*
|
|
856
|
+
* 宿主 IPC 层调用此方法前置判断锁定状态,区分"切换失败"原因
|
|
857
|
+
* (locked / busy / not_found / invalid),让用户知道为什么没反应。
|
|
858
|
+
*
|
|
859
|
+
* @returns locked 是否处于锁定状态;unlockAt 锁定自动恢复时间戳(ms epoch),未锁定时为 null
|
|
860
|
+
*/
|
|
861
|
+
getPersonaSwitchLockStatus() {
|
|
862
|
+
this.assertInitialized('getPersonaSwitchLockStatus');
|
|
863
|
+
if (!this.personaManager)
|
|
864
|
+
return { locked: false, unlockAt: null };
|
|
865
|
+
return this.personaManager.getSwitchLockStatus();
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* 刷新 AgentLoop 的 systemPromptPrefix(角色 prompt + profile prompt)
|
|
869
|
+
*
|
|
870
|
+
* 提取自 doPostProcess 自动匹配 + switchPersona 手动切换两处共用逻辑(ADR-017 枝叶层 2 次提取)。
|
|
871
|
+
* 组装规则:[personaPrompt, profilePrompt].filter(Boolean).join('\n\n') + 末尾分隔符 '---'
|
|
872
|
+
*
|
|
873
|
+
* 调用时机:
|
|
874
|
+
* - tryAutoMatchPersona 中角色自动匹配成功后(chat() 回答前)
|
|
875
|
+
* - switchPersona 手动切换成功后
|
|
876
|
+
* - loop 为 null 时静默跳过(init 前或 close 后的边界场景)
|
|
877
|
+
*/
|
|
878
|
+
refreshPersonaPrefixOnLoop() {
|
|
879
|
+
if (!this.loop)
|
|
880
|
+
return;
|
|
881
|
+
const profilePrompt = this.userProfile?.buildSystemPrompt() ?? '';
|
|
882
|
+
const personaPrompt = this.personaManager?.buildSystemPrompt() ?? '';
|
|
883
|
+
const newPrefix = [personaPrompt, profilePrompt].filter(Boolean).join('\n\n') +
|
|
884
|
+
([personaPrompt, profilePrompt].some(Boolean) ? '\n\n---\n\n' : '');
|
|
885
|
+
this.loop.refreshPersonaPrefix(newPrefix);
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* 角色自动匹配(best-effort:失败不阻塞对话流程)
|
|
889
|
+
*
|
|
890
|
+
* 在 chat() 回答前执行,确保本轮 LLM 调用就用匹配到的角色 system prompt。
|
|
891
|
+
* 两层匹配策略:关键词高置信度 → LLM 辅助(低置信度且 backgroundProvider 已注入时)。
|
|
892
|
+
* LLM 辅助匹配在 agent 层执行,遵循 backend_layers_rules §分层职责(persona/ 不直接调 LLM)。
|
|
893
|
+
*
|
|
894
|
+
* 匹配成功时:切换角色 + 发射 personaSwitched 事件 + 刷新 AgentLoop 前缀。
|
|
895
|
+
* 匹配失败/异常时:静默降级,保持当前角色。
|
|
896
|
+
*
|
|
897
|
+
* @param input 用户输入文本
|
|
898
|
+
*/
|
|
899
|
+
async tryAutoMatchPersona(input) {
|
|
900
|
+
if (!this.personaManager)
|
|
901
|
+
return;
|
|
902
|
+
try {
|
|
903
|
+
let matchedPersona = null;
|
|
904
|
+
if (this.personaManager.canAutoMatch()) {
|
|
905
|
+
matchedPersona = this.personaManager.autoMatch(input);
|
|
906
|
+
// 关键词低置信度且 backgroundProvider 已注入 → LLM 辅助语义匹配
|
|
907
|
+
const bgProvider = this.#backgroundProvider;
|
|
908
|
+
if (!matchedPersona && bgProvider) {
|
|
909
|
+
matchedPersona = await matchPersonaByLlm(bgProvider, this.personaManager.list, this.personaManager.activeName, input);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (matchedPersona) {
|
|
913
|
+
const prevName = this.personaManager.activeName;
|
|
914
|
+
this.personaManager.switchPersona(matchedPersona);
|
|
915
|
+
this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: matchedPersona });
|
|
916
|
+
// 刷新 AgentLoop 的角色前缀(与 switchPersona 共用同一段逻辑,ADR-017 枝叶层 2 次提取)
|
|
917
|
+
this.refreshPersonaPrefixOnLoop();
|
|
918
|
+
logger.info({ persona: matchedPersona }, '角色自动切换');
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
// 关键词 + LLM 均未命中,且当前角色不是列表首个角色 → 回退到首个角色
|
|
922
|
+
// "切过去回不来":用户从默认切到散文作者后,输入无关话题应回到默认角色
|
|
923
|
+
// 使用 list[0] 而非硬编码 'default':防止 default 角色被删除后 fallback 抛异常
|
|
924
|
+
const shouldFallback = this.shouldFallbackToDefault();
|
|
925
|
+
if (shouldFallback) {
|
|
926
|
+
const fallbackTarget = this.personaManager.list[0]?.name ?? 'default';
|
|
927
|
+
const prevName = this.personaManager.activeName;
|
|
928
|
+
this.personaManager.switchPersona(fallbackTarget);
|
|
929
|
+
this.emit(AGENT_EVENTS.personaSwitched, { from: prevName, to: fallbackTarget });
|
|
930
|
+
this.refreshPersonaPrefixOnLoop();
|
|
931
|
+
logger.info({ persona: fallbackTarget }, '角色回退默认');
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
logger.warn({ err }, '角色自动匹配失败');
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* 判断是否应回退到默认角色
|
|
941
|
+
*
|
|
942
|
+
* 回退条件(全部满足):
|
|
943
|
+
* 1. 当前激活角色不是默认角色(已在默认角色则无需回退)
|
|
944
|
+
* 2. 角色管理器存在且非锁定状态(锁定时 switchPersona 也会被拦截,回退无意义)
|
|
945
|
+
*
|
|
946
|
+
* 注意:bgProvider 未配置时也会触发回退——因为关键词 + LLM 均未命中说明当前角色不匹配本轮对话,
|
|
947
|
+
* 回退默认角色比卡在错误角色上更合理。
|
|
948
|
+
*
|
|
949
|
+
* @returns 是否应回退到默认角色
|
|
950
|
+
*/
|
|
951
|
+
shouldFallbackToDefault() {
|
|
952
|
+
if (!this.personaManager)
|
|
953
|
+
return false;
|
|
954
|
+
// manual 模式下不回退:用户明确固定了角色,回退会破坏其意图
|
|
955
|
+
if (this.personaManager.currentMode !== 'auto')
|
|
956
|
+
return false;
|
|
957
|
+
const status = this.personaManager.getSwitchLockStatus();
|
|
958
|
+
if (status.locked)
|
|
959
|
+
return false; // 锁定中不回退,避免无意义调用 switchPersona
|
|
960
|
+
// 当前角色已是���表首个角色("默认"角色)→ 无需回退
|
|
961
|
+
const defaultName = this.personaManager.list[0]?.name;
|
|
962
|
+
return defaultName !== undefined && this.personaManager.activeName !== defaultName;
|
|
963
|
+
}
|
|
758
964
|
/**
|
|
759
965
|
* 手动触发 profile facts 归档(manual 模式下使用)
|
|
760
966
|
*
|
|
761
967
|
* manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
|
|
762
968
|
* full / insights-only 模式下也可调用(会重复归档,但不推荐)。
|
|
763
969
|
*
|
|
970
|
+
* FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
|
|
971
|
+
* 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
|
|
972
|
+
* 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
|
|
973
|
+
*
|
|
764
974
|
* 归档逻辑已委托给 ArchiveCoordinator
|
|
765
975
|
*
|
|
766
976
|
* @param input 本轮用户输入
|
|
977
|
+
* @param options 触发选项(autoTriggered 默认 false,即手动触发)
|
|
767
978
|
* @returns 写入/更新的 UserProfileEntry 列表
|
|
768
979
|
*/
|
|
769
|
-
async archiveProfileFacts(input) {
|
|
980
|
+
async archiveProfileFacts(input, options) {
|
|
770
981
|
this.assertInitialized('archiveProfileFacts');
|
|
771
|
-
return this.
|
|
982
|
+
return this.requireArchiveCoordinator.archiveProfileFacts(input, options);
|
|
772
983
|
}
|
|
773
984
|
/**
|
|
774
985
|
* 手动触发 insight 提取(manual 模式下使用)
|
|
@@ -776,15 +987,20 @@ export class Agent extends TypedEventEmitter {
|
|
|
776
987
|
* manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
|
|
777
988
|
* 内部仍走 classify 判断(避免无价值输入浪费 LLM 调用)。
|
|
778
989
|
*
|
|
990
|
+
* FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
|
|
991
|
+
* 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
|
|
992
|
+
* 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
|
|
993
|
+
*
|
|
779
994
|
* 归档逻辑已委托给 ArchiveCoordinator
|
|
780
995
|
*
|
|
781
996
|
* @param input 本轮用户输入
|
|
782
997
|
* @param assistantContent 本轮助手回复内容
|
|
998
|
+
* @param options 触发选项(autoTriggered 默认 false,即手动触发)
|
|
783
999
|
* @returns 写入/更新的 Memory 列表
|
|
784
1000
|
*/
|
|
785
|
-
async archiveInsight(input, assistantContent) {
|
|
1001
|
+
async archiveInsight(input, assistantContent, options) {
|
|
786
1002
|
this.assertInitialized('archiveInsight');
|
|
787
|
-
return this.
|
|
1003
|
+
return this.requireArchiveCoordinator.archiveInsight(input, assistantContent, options);
|
|
788
1004
|
}
|
|
789
1005
|
/**
|
|
790
1006
|
* 手动归档会话内容(content 类记忆)
|
|
@@ -792,15 +1008,32 @@ export class Agent extends TypedEventEmitter {
|
|
|
792
1008
|
* 适用于 `insights-only` / `manual` 模式下用户手动触发会话内容归档。
|
|
793
1009
|
* `full` 模式下由宿主在会话切换前自动调用,无需用户干预。
|
|
794
1010
|
*
|
|
1011
|
+
* FIX-P1-4:新增 options 参数透传给 ArchiveCoordinator。
|
|
1012
|
+
* 宿主自动触发时传 `{ autoTriggered: true }`,由 ArchiveCoordinator 内部按模式判断;
|
|
1013
|
+
* 用户手动触发时无需传 options(默认 autoTriggered=false,无条件执行)。
|
|
1014
|
+
*
|
|
795
1015
|
* 归档逻辑已委托给 ArchiveCoordinator
|
|
796
1016
|
*
|
|
797
1017
|
* @param date 会话日期 YYYY-MM-DD
|
|
798
1018
|
* @param session 会话标识(不含日期前缀)
|
|
1019
|
+
* @param options 触发选项(autoTriggered 默认 false,即手动触发)
|
|
799
1020
|
* @returns 归档结果(memories 可能为空,表示无归档价值或 LLM 失败)
|
|
800
1021
|
*/
|
|
801
|
-
async archiveSessionContent(date, session) {
|
|
1022
|
+
async archiveSessionContent(date, session, options) {
|
|
802
1023
|
this.assertInitialized('archiveSessionContent');
|
|
803
|
-
return this.
|
|
1024
|
+
return this.requireArchiveCoordinator.archiveSessionContent(date, session, options);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* 获取 pending 归档队列长度(MIND2-C3)
|
|
1028
|
+
*
|
|
1029
|
+
* 返回待重试的失败归档数。归档失败时输入入队,下次同阶段归档调用时自动重试。
|
|
1030
|
+
* 供宿主 UI 展示"N 条待补归档"提示,或判断是否需要手动触发重试。
|
|
1031
|
+
*
|
|
1032
|
+
* @returns pending 队列长度(0 表示无待补)
|
|
1033
|
+
*/
|
|
1034
|
+
getPendingArchiveCount() {
|
|
1035
|
+
this.assertInitialized('getPendingArchiveCount');
|
|
1036
|
+
return this.requireArchiveCoordinator.getPendingArchiveCount();
|
|
804
1037
|
}
|
|
805
1038
|
// ─── 配置重载(事件驱动) ───────────────────────
|
|
806
1039
|
/**
|
|
@@ -812,7 +1045,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
812
1045
|
* 支持的 source:
|
|
813
1046
|
* - 'skill' → SkillManager.reload() 清空缓存重新扫描 skills/ 目录
|
|
814
1047
|
* - 'persona' → PersonaManager.reload() 清空缓存重新扫描 personas/ 目录(保持激活角色)
|
|
815
|
-
* - 'rule' → 无操作(rule
|
|
1048
|
+
* - 'rule' → 无操作(rule 类型由 ConfigManager CRUD 即时同步 bootstrap 段,详见 deleteRule/updateRule)
|
|
816
1049
|
* - 'guardrail' → 抛错(guardrail 是 AgentLoop 的 readonly 数组,需 rebuildComponents 才能重载)
|
|
817
1050
|
* - undefined → 重载 skill + persona(全量重载,不含 guardrail)
|
|
818
1051
|
*
|
|
@@ -821,36 +1054,58 @@ export class Agent extends TypedEventEmitter {
|
|
|
821
1054
|
*/
|
|
822
1055
|
async reloadConfig(source) {
|
|
823
1056
|
this.assertInitialized('reloadConfig');
|
|
824
|
-
if (this.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1057
|
+
if (this.chatLockManager?.isBusy) {
|
|
1058
|
+
// 对话进行中无法热重载:将 source 暂存,待 chat() finally 块释放锁后补执行
|
|
1059
|
+
// source 为 undefined(全量重载)时不暂存——全量重载无具体来源,补执行语义不明
|
|
1060
|
+
if (source) {
|
|
1061
|
+
this.pendingConfigReload.add(source);
|
|
1062
|
+
logger.info({ source }, '对话进行中,配置重载已暂存,将在对话结束后补执行');
|
|
1063
|
+
}
|
|
1064
|
+
throw chatBusyError('重载配置');
|
|
828
1065
|
}
|
|
829
1066
|
// guardrail 需重建 AgentLoop,不属于热重载范畴
|
|
830
1067
|
if (source === 'guardrail') {
|
|
831
1068
|
throw configError('guardrail 不支持热重载', 'guardrail 规则是 AgentLoop 的 readonly 数组,需调用 rebuildComponents() 重建', ['使用 rebuildComponents() 重建组件(代价较高)']);
|
|
832
1069
|
}
|
|
833
|
-
// rule
|
|
1070
|
+
// rule 类型由 ConfigManager.deleteRule/updateRule CRUD 即时同步 bootstrap 段(refreshBootstrapMemories 回调),无需 reloadConfig
|
|
834
1071
|
if (source === 'rule') {
|
|
835
|
-
logger.info('rule
|
|
1072
|
+
logger.info('rule 类型由 ConfigManager CRUD 即时同步 bootstrap 段,reloadConfig 跳过');
|
|
836
1073
|
return { skill: 0, persona: 0 };
|
|
837
1074
|
}
|
|
838
1075
|
const result = { skill: 0, persona: 0 };
|
|
1076
|
+
const errors = [];
|
|
839
1077
|
// 按需重载:source 缺省时全量重载,否则只重载指定类型
|
|
840
1078
|
const shouldReloadSkill = !source || source === 'skill';
|
|
841
1079
|
const shouldReloadPersona = !source || source === 'persona';
|
|
842
1080
|
if (shouldReloadSkill && this.skillManager) {
|
|
843
|
-
|
|
1081
|
+
try {
|
|
1082
|
+
result.skill = await this.skillManager.reload();
|
|
1083
|
+
}
|
|
1084
|
+
catch (err) {
|
|
1085
|
+
errors.push(toError(err));
|
|
1086
|
+
logger.warn({ err: toError(err) }, 'reloadConfig: skillManager.reload 失败');
|
|
1087
|
+
}
|
|
844
1088
|
}
|
|
845
1089
|
if (shouldReloadPersona && this.personaManager) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1090
|
+
try {
|
|
1091
|
+
result.persona = await this.personaManager.reload();
|
|
1092
|
+
// 角色重载后,刷新 AgentLoop 的角色前缀(使新角色内容立即注入 system prompt)
|
|
1093
|
+
if (this.loop) {
|
|
1094
|
+
const newPrefix = this.personaManager.buildSystemPrompt();
|
|
1095
|
+
this.loop.refreshPersonaPrefix(newPrefix);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
catch (err) {
|
|
1099
|
+
errors.push(toError(err));
|
|
1100
|
+
logger.warn({ err: toError(err) }, 'reloadConfig: personaManager.reload 失败');
|
|
851
1101
|
}
|
|
852
1102
|
}
|
|
853
|
-
|
|
1103
|
+
if (errors.length > 0) {
|
|
1104
|
+
logger.warn({ source, ...result, errorCount: errors.length }, '配置热重载部分失败');
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
logger.info({ source, ...result }, '配置已热重载');
|
|
1108
|
+
}
|
|
854
1109
|
return result;
|
|
855
1110
|
}
|
|
856
1111
|
// ─── 组件访问 ─────────────────────────────────────────
|
|
@@ -862,11 +1117,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
862
1117
|
*/
|
|
863
1118
|
async rebuildComponents() {
|
|
864
1119
|
// 对话进行中重建组件会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
|
|
865
|
-
|
|
866
|
-
throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再重建组件', [
|
|
867
|
-
'等待上一轮 chat() 的 AsyncGenerator 耗尽',
|
|
868
|
-
]);
|
|
869
|
-
}
|
|
1120
|
+
this.assertNotBusy('重建组件');
|
|
870
1121
|
await this.rebuildComponentsWithCurrentCtx();
|
|
871
1122
|
}
|
|
872
1123
|
// ─── 守卫方法 ───────────────────────────────────────────
|
|
@@ -897,17 +1148,55 @@ export class Agent extends TypedEventEmitter {
|
|
|
897
1148
|
}
|
|
898
1149
|
}
|
|
899
1150
|
/**
|
|
900
|
-
*
|
|
1151
|
+
* 断言对话未进行中 — 统一守卫,避免在 chat() 进行中执行会破坏状态一致性的操作
|
|
901
1152
|
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
1153
|
+
* 提取原因:switchProject / setProvider / setBackgroundProvider / setArchiveMode /
|
|
1154
|
+
* switchPersona / rebuildComponents 等 6 处方法均有相同的 `if (isBusy) throw chatBusyError('XXX')` 守卫,
|
|
1155
|
+
* 违反 DRY 原则(v2 神木回天 REPEAT-4 闭环)。
|
|
1156
|
+
*
|
|
1157
|
+
* 注意:reloadConfig 不使用本方法——它在 isBusy 时需暂存 source 而非直接抛错。
|
|
1158
|
+
*
|
|
1159
|
+
* @param operation 操作名称(用于错误消息,如 "切换项目")
|
|
905
1160
|
*/
|
|
906
|
-
|
|
907
|
-
if (
|
|
908
|
-
throw
|
|
1161
|
+
assertNotBusy(operation) {
|
|
1162
|
+
if (this.chatLockManager?.isBusy) {
|
|
1163
|
+
throw chatBusyError(operation);
|
|
909
1164
|
}
|
|
910
|
-
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* 断言 getter:获取 loop,若 null 则抛出明确错误
|
|
1168
|
+
* 替代 `this.loop!` 非空断言,提供更好的重构安全性
|
|
1169
|
+
*/
|
|
1170
|
+
get requireLoop() {
|
|
1171
|
+
if (!this.loop)
|
|
1172
|
+
throw configError('AgentLoop 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
|
|
1173
|
+
return this.loop;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* 断言 getter:获取 history,若 null 则抛出明确错误
|
|
1177
|
+
*/
|
|
1178
|
+
get requireHistory() {
|
|
1179
|
+
if (!this.history)
|
|
1180
|
+
throw configError('MessageHistory 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
|
|
1181
|
+
return this.history;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* 断言 getter:获取 pctx,若 null 则抛出明确错误
|
|
1185
|
+
*/
|
|
1186
|
+
get requirePctx() {
|
|
1187
|
+
if (!this.pctx)
|
|
1188
|
+
throw configError('ProjectContext 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
|
|
1189
|
+
return this.pctx;
|
|
1190
|
+
}
|
|
1191
|
+
get requireArchiveCoordinator() {
|
|
1192
|
+
if (!this.archiveCoordinator)
|
|
1193
|
+
throw configError('ArchiveCoordinator 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
|
|
1194
|
+
return this.archiveCoordinator;
|
|
1195
|
+
}
|
|
1196
|
+
get requireSessionManager() {
|
|
1197
|
+
if (!this._sessionManager)
|
|
1198
|
+
throw configError('SessionManager 未初始化', undefined, ['在调用此方法前执行 await agent.init()']);
|
|
1199
|
+
return this._sessionManager;
|
|
911
1200
|
}
|
|
912
1201
|
// ─── 记忆生命周期 ───────────────────────────────────────
|
|
913
1202
|
// ─── runMemoryDecay 已迁移至 MemoryDecayScheduler.runOnce ─────
|
|
@@ -916,65 +1205,96 @@ export class Agent extends TypedEventEmitter {
|
|
|
916
1205
|
* 关闭 Agent,释放 SQLite 连接等资源
|
|
917
1206
|
*/
|
|
918
1207
|
async close() {
|
|
919
|
-
//
|
|
920
|
-
// 检测到 token
|
|
921
|
-
this.
|
|
1208
|
+
// 清理 chat 锁管理器(递增 token 使进行中的 chat() generator 的 finally 块
|
|
1209
|
+
// 检测到 token 变化后跳过资源清理,close 已接管清理职责)
|
|
1210
|
+
this.chatLockManager?.dispose();
|
|
1211
|
+
this.chatLockManager = null;
|
|
922
1212
|
// 清理 MemoryDecayScheduler(含定时器和 storage 引用)
|
|
1213
|
+
// FIX-P0-1:先 stop() abort L2 评估的 LLM 调用,再 awaitInflight() 等待 Promise 完成,
|
|
1214
|
+
// 防止 close 后 LLM 回调 upsert 已关闭的 storage
|
|
923
1215
|
if (this.memoryDecayScheduler) {
|
|
924
1216
|
this.memoryDecayScheduler.stop();
|
|
1217
|
+
try {
|
|
1218
|
+
await this.memoryDecayScheduler.awaitInflight();
|
|
1219
|
+
}
|
|
1220
|
+
catch (err) {
|
|
1221
|
+
logger.warn({ err: toError(err) }, 'close: memoryDecayScheduler.awaitInflight 失败');
|
|
1222
|
+
}
|
|
925
1223
|
this.memoryDecayScheduler = null;
|
|
926
1224
|
}
|
|
1225
|
+
// FIX-P0-1:等待 WorkProjection 的 inflight LLM 生成完成,防止 close 后 upsert 已关闭的 storage
|
|
1226
|
+
if (this.workProjection) {
|
|
1227
|
+
try {
|
|
1228
|
+
await this.workProjection.awaitInflight();
|
|
1229
|
+
}
|
|
1230
|
+
catch (err) {
|
|
1231
|
+
logger.warn({ err: toError(err) }, 'close: workProjection.awaitInflight 失败');
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
927
1234
|
// 清理 ArchiveCoordinator(无定时器,只需释放引用)
|
|
928
1235
|
this.archiveCoordinator = null;
|
|
929
|
-
// 清理定时器
|
|
930
|
-
if (this.decayTimer) {
|
|
931
|
-
clearSafeInterval(this.decayTimer);
|
|
932
|
-
this.decayTimer = null;
|
|
933
|
-
}
|
|
934
|
-
if (this.chatLockTimer) {
|
|
935
|
-
clearSafeTimeout(this.chatLockTimer);
|
|
936
|
-
this.chatLockTimer = null;
|
|
937
|
-
}
|
|
938
|
-
if (this.chatAbortController) {
|
|
939
|
-
this.chatAbortController.abort();
|
|
940
|
-
this.chatAbortController = null;
|
|
941
|
-
}
|
|
942
1236
|
// 清理 PersonaManager 的角色切换防抖锁计时器,防止关闭后回调触发
|
|
943
1237
|
if (this.personaManager) {
|
|
944
1238
|
this.personaManager.close();
|
|
945
1239
|
}
|
|
946
|
-
|
|
1240
|
+
// 先等待后台归档完成,再移除事件监听器
|
|
1241
|
+
// 顺序敏感:若先 removeAllListeners,fire-and-forget 的归档 Promise 在 await 期间
|
|
1242
|
+
// reject 时 emit 会变成 no-op(listeners 已清空),archiveFailed 事件丢失
|
|
947
1243
|
if (this.history) {
|
|
948
|
-
|
|
1244
|
+
try {
|
|
1245
|
+
await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
|
|
1246
|
+
}
|
|
1247
|
+
catch (err) {
|
|
1248
|
+
logger.warn({ err: toError(err) }, 'close: awaitPendingArchives 失败');
|
|
1249
|
+
}
|
|
949
1250
|
}
|
|
1251
|
+
// pending archives 完成后再移除监听器,确保归档失败的 emit 能送达
|
|
1252
|
+
this.removeAllListeners();
|
|
950
1253
|
if (this.projectManager) {
|
|
951
|
-
|
|
1254
|
+
try {
|
|
1255
|
+
await this.projectManager.shutdown();
|
|
1256
|
+
}
|
|
1257
|
+
catch (err) {
|
|
1258
|
+
logger.warn({ err: toError(err) }, 'close: projectManager.shutdown 失败');
|
|
1259
|
+
}
|
|
952
1260
|
}
|
|
953
1261
|
this._initialized = false;
|
|
1262
|
+
this.nullifyAllComponents();
|
|
1263
|
+
// 清理次要状态字段,防止 re-init 后残留上一会话状态
|
|
1264
|
+
this._lastInteractionAt = null;
|
|
1265
|
+
this.pendingConfigReload.clear();
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* 统一 null 化所有组件字段(新增 Manager 时在此处追加一行)
|
|
1269
|
+
*
|
|
1270
|
+
* 仅处理"纯 null 化"字段,带副作用的清理(dispose/stop/close/shutdown)
|
|
1271
|
+
* 仍由 close() 显式调用,顺序敏感不可合并。
|
|
1272
|
+
*/
|
|
1273
|
+
nullifyAllComponents() {
|
|
1274
|
+
// Provider
|
|
954
1275
|
this.#backgroundProvider = null;
|
|
955
|
-
|
|
1276
|
+
// 核心组件
|
|
956
1277
|
this.history = null;
|
|
957
1278
|
this.loop = null;
|
|
958
|
-
this.
|
|
959
|
-
|
|
1279
|
+
this.toolExec = null;
|
|
1280
|
+
// 专职 Manager
|
|
1281
|
+
this.personaManager = null;
|
|
1282
|
+
this.#userProfile = null;
|
|
1283
|
+
this.skillManager = null;
|
|
960
1284
|
this.insightExtractor = null;
|
|
961
1285
|
this.configManager = null;
|
|
962
1286
|
this.memoryInspector = null;
|
|
963
|
-
this
|
|
1287
|
+
this.dedupManager = null;
|
|
1288
|
+
this.memoryAdvisor = null;
|
|
1289
|
+
this._governance = null;
|
|
1290
|
+
this.workProjection = null;
|
|
964
1291
|
this.autoConfigRefiner = null;
|
|
965
1292
|
this.sessionArchiver = null;
|
|
1293
|
+
this.textPolisher = null;
|
|
1294
|
+
this._sessionManager = null;
|
|
1295
|
+
// 项目管理
|
|
1296
|
+
this.projectManager = null;
|
|
966
1297
|
this.pctx = null;
|
|
967
|
-
// 补全剩余 manager 字段 null 化,与上述字段处理方式一致
|
|
968
|
-
// (原实现仅 null 化部分 manager,toolExec/personaManager/#userProfile/skillManager/workProjection 遗漏)
|
|
969
|
-
this.toolExec = null;
|
|
970
|
-
this.personaManager = null;
|
|
971
|
-
this.#userProfile = null;
|
|
972
|
-
this.skillManager = null;
|
|
973
|
-
this.workProjection = null;
|
|
974
|
-
// 清理次要状态字段,防止 re-init 后残留上一会话状态
|
|
975
|
-
this.activeSkill = null;
|
|
976
|
-
this._lastInteractionAt = null;
|
|
977
|
-
// 衰减指标已迁移至 MemoryDecayScheduler,close 时通过 stop() 销毁实例
|
|
978
1298
|
}
|
|
979
1299
|
// ─── 只读访问器 ───────────────────────────────────────
|
|
980
1300
|
get initialized() {
|
|
@@ -983,9 +1303,30 @@ export class Agent extends TypedEventEmitter {
|
|
|
983
1303
|
get context() {
|
|
984
1304
|
return this.pctx;
|
|
985
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* @deprecated 直接暴露 AgentLoop 破坏封装,将在下一主版本移除。
|
|
1308
|
+
* 使用 getMessages() / getMessageCount() 等有界接口替代。
|
|
1309
|
+
*/
|
|
986
1310
|
get agentLoop() {
|
|
987
1311
|
return this.loop;
|
|
988
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* 获取当前消息列表(只读副本,不可绕过 Agent 编排链路直接修改)
|
|
1315
|
+
*/
|
|
1316
|
+
getMessages() {
|
|
1317
|
+
return this.loop?.getMessages() ?? [];
|
|
1318
|
+
}
|
|
1319
|
+
/** 获取当前消息数量 */
|
|
1320
|
+
getMessageCount() {
|
|
1321
|
+
return this.loop?.getMessages().length ?? 0;
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* @deprecated 直接暴露 MessageHistory 破坏封装,将在下一主版本移除。
|
|
1325
|
+
* 使用 Agent 的对话管理方法(chat/chatSync/forkSession)替代直接操作。
|
|
1326
|
+
*/
|
|
1327
|
+
get agentHistory() {
|
|
1328
|
+
return this.history;
|
|
1329
|
+
}
|
|
989
1330
|
/**
|
|
990
1331
|
* 注入情感基调到 system prompt(Phase 2.1:AffectController)
|
|
991
1332
|
*
|
|
@@ -995,20 +1336,33 @@ export class Agent extends TypedEventEmitter {
|
|
|
995
1336
|
* @param affectString 情感描述文本,传空字符串清除注入
|
|
996
1337
|
*/
|
|
997
1338
|
injectAffect(affectString) {
|
|
998
|
-
this.loop
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
return this.history;
|
|
1339
|
+
if (!this.loop)
|
|
1340
|
+
throw configError('AgentLoop', 'AgentLoop 未初始化,无法注入 affect', []);
|
|
1341
|
+
this.loop.injectAffect(affectString);
|
|
1002
1342
|
}
|
|
1003
1343
|
get provider() {
|
|
1004
1344
|
return this.#provider;
|
|
1005
1345
|
}
|
|
1006
1346
|
get isBusy() {
|
|
1007
|
-
return this.
|
|
1347
|
+
return this.chatLockManager?.isBusy ?? false;
|
|
1008
1348
|
}
|
|
1009
1349
|
get lastInteractionAt() {
|
|
1010
1350
|
return this._lastInteractionAt;
|
|
1011
1351
|
}
|
|
1352
|
+
/**
|
|
1353
|
+
* 记忆治理统一门面(L0 衰减 / L1 去重 / L2 时效性 / L3 冲突 / 诊断 / 推荐)
|
|
1354
|
+
*
|
|
1355
|
+
* 统一入口替代散落在 Agent 上的 6 个独立方法:
|
|
1356
|
+
* agent.governance.decay() → 原 runMemoryDecayOnce()
|
|
1357
|
+
* agent.governance.deduplicate() → 原 deduplicateMemories()
|
|
1358
|
+
* agent.governance.evaluateTimeliness() → 原 evaluateTimeliness()
|
|
1359
|
+
* agent.governance.detectConflicts() → 原 detectConflicts()
|
|
1360
|
+
* agent.governance.sourceHealth() → 原 sourceHealth()
|
|
1361
|
+
* agent.governance.suggest() → 原 suggest()
|
|
1362
|
+
*/
|
|
1363
|
+
get governance() {
|
|
1364
|
+
return this._governance;
|
|
1365
|
+
}
|
|
1012
1366
|
// ─── 运行时指标 ────────────────────────────────
|
|
1013
1367
|
/**
|
|
1014
1368
|
* 获取 Agent 运行时指标快照(可观测性增强)
|
|
@@ -1055,7 +1409,7 @@ export class Agent extends TypedEventEmitter {
|
|
|
1055
1409
|
// - 访问器(getter):返回 Manager | null,供宿主项目链式调用和优雅降级
|
|
1056
1410
|
// (如 `agent.persona?.activeName`、`if (!agent.memory) return []`)
|
|
1057
1411
|
// - 门面方法(snapshot/inspect/stats/searchMemories 等):通过
|
|
1058
|
-
// assertInitialized
|
|
1412
|
+
// assertInitialized 抛 MemoraError,提供明确错误信息
|
|
1059
1413
|
// 宿主项目使用访问器时需自行判空,或使用门面方法获得自动错误处理。
|
|
1060
1414
|
/**
|
|
1061
1415
|
* 角色管理器(可能为 null)
|
|
@@ -1107,17 +1461,6 @@ export class Agent extends TypedEventEmitter {
|
|
|
1107
1461
|
get memory() {
|
|
1108
1462
|
return this.memoryInspector;
|
|
1109
1463
|
}
|
|
1110
|
-
/**
|
|
1111
|
-
* 记忆写入器(可能为 null)—— upsert / delete / restore / purge / purgeExpired /
|
|
1112
|
-
* addRelation / removeRelation
|
|
1113
|
-
*
|
|
1114
|
-
* P1-2 拆分:从 MemoryInspector 拆出写操作代理,与 MemoryInspector 严格分工。
|
|
1115
|
-
* 返回 null 时表示 Agent 未初始化或存储层未就绪。
|
|
1116
|
-
* 宿主项目常用模式:`const m = agent.memoryMutator; if (!m) return; m.upsert(...)`
|
|
1117
|
-
*/
|
|
1118
|
-
get memoryMutator() {
|
|
1119
|
-
return this.#memoryMutator;
|
|
1120
|
-
}
|
|
1121
1464
|
/**
|
|
1122
1465
|
* 项目管理器(可能为 null)
|
|
1123
1466
|
*
|
|
@@ -1166,5 +1509,9 @@ export class Agent extends TypedEventEmitter {
|
|
|
1166
1509
|
get sessionManager() {
|
|
1167
1510
|
return this._sessionManager;
|
|
1168
1511
|
}
|
|
1512
|
+
/** 文本润色管理器(宿主可通过此 getter 调用 polish 方法) */
|
|
1513
|
+
get polish() {
|
|
1514
|
+
return this.textPolisher;
|
|
1515
|
+
}
|
|
1169
1516
|
}
|
|
1170
1517
|
//# sourceMappingURL=agent.js.map
|