refactor(store): split gatewayStore into specialized domain stores
Major restructuring: - Split monolithic gatewayStore into 5 focused stores: - connectionStore: WebSocket connection and gateway lifecycle - configStore: quickConfig, workspaceInfo, MCP services - agentStore: clones, usage stats, agent management - handStore: hands, approvals, triggers, hand runs - workflowStore: workflows, workflow runs, execution - Update all components to use new stores with selector pattern - Remove
This commit is contained in:
409
docs/archive/v1-viking-dead-code/lib/context-builder.ts
Normal file
409
docs/archive/v1-viking-dead-code/lib/context-builder.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* ContextBuilder - Integrates OpenViking memories into chat context
|
||||
*
|
||||
* Responsible for:
|
||||
* 1. Building enhanced system prompts with relevant memories (L0/L1/L2)
|
||||
* 2. Extracting and saving memories after conversations end
|
||||
* 3. Managing context compaction with memory flush
|
||||
* 4. Reading and injecting agent identity files
|
||||
*
|
||||
* This module bridges the VikingAdapter with chatStore/gateway-client.
|
||||
*/
|
||||
|
||||
import { VikingAdapter, getVikingAdapter, type EnhancedContext } from './viking-adapter';
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface AgentIdentity {
|
||||
soul: string;
|
||||
instructions: string;
|
||||
userProfile: string;
|
||||
heartbeat?: string;
|
||||
}
|
||||
|
||||
export interface ContextBuildResult {
|
||||
systemPrompt: string;
|
||||
memorySummary: string;
|
||||
tokensUsed: number;
|
||||
memoriesInjected: number;
|
||||
}
|
||||
|
||||
export interface CompactionResult {
|
||||
compactedMessages: ChatMessage[];
|
||||
summary: string;
|
||||
memoriesFlushed: number;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ContextBuilderConfig {
|
||||
enabled: boolean;
|
||||
maxMemoryTokens: number;
|
||||
compactionThresholdTokens: number;
|
||||
compactionReserveTokens: number;
|
||||
memoryFlushOnCompact: boolean;
|
||||
autoExtractOnComplete: boolean;
|
||||
minExtractionMessages: number;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: ContextBuilderConfig = {
|
||||
enabled: true,
|
||||
maxMemoryTokens: 6000,
|
||||
compactionThresholdTokens: 15000,
|
||||
compactionReserveTokens: 4000,
|
||||
memoryFlushOnCompact: true,
|
||||
autoExtractOnComplete: true,
|
||||
minExtractionMessages: 4,
|
||||
};
|
||||
|
||||
// === Token Estimation ===
|
||||
|
||||
function estimateTokens(text: string): number {
|
||||
const cjkChars = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
|
||||
const otherChars = text.length - cjkChars;
|
||||
return Math.ceil(cjkChars * 1.5 + otherChars * 0.4);
|
||||
}
|
||||
|
||||
function estimateMessagesTokens(messages: ChatMessage[]): number {
|
||||
return messages.reduce((sum, m) => sum + estimateTokens(m.content) + 4, 0);
|
||||
}
|
||||
|
||||
// === ContextBuilder Implementation ===
|
||||
|
||||
export class ContextBuilder {
|
||||
private viking: VikingAdapter;
|
||||
private config: ContextBuilderConfig;
|
||||
private identityCache: Map<string, { identity: AgentIdentity; cachedAt: number }> = new Map();
|
||||
private static IDENTITY_CACHE_TTL = 5 * 60 * 1000; // 5 min
|
||||
|
||||
constructor(config?: Partial<ContextBuilderConfig>) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
this.viking = getVikingAdapter();
|
||||
}
|
||||
|
||||
// === Core: Build Context for a Chat Message ===
|
||||
|
||||
async buildContext(
|
||||
userMessage: string,
|
||||
agentId: string,
|
||||
_existingMessages: ChatMessage[] = []
|
||||
): Promise<ContextBuildResult> {
|
||||
if (!this.config.enabled) {
|
||||
return {
|
||||
systemPrompt: '',
|
||||
memorySummary: '',
|
||||
tokensUsed: 0,
|
||||
memoriesInjected: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if OpenViking is available
|
||||
const connected = await this.viking.isConnected();
|
||||
if (!connected) {
|
||||
console.warn('[ContextBuilder] OpenViking not available, skipping memory injection');
|
||||
return {
|
||||
systemPrompt: '',
|
||||
memorySummary: '',
|
||||
tokensUsed: 0,
|
||||
memoriesInjected: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 1: Load agent identity
|
||||
const identity = await this.loadIdentity(agentId);
|
||||
|
||||
// Step 2: Build enhanced context with memories
|
||||
const enhanced = await this.viking.buildEnhancedContext(
|
||||
userMessage,
|
||||
agentId,
|
||||
{ maxTokens: this.config.maxMemoryTokens, includeTrace: true }
|
||||
);
|
||||
|
||||
// Step 3: Compose system prompt
|
||||
const systemPrompt = this.composeSystemPrompt(identity, enhanced);
|
||||
|
||||
// Step 4: Build summary for UI display
|
||||
const memorySummary = this.buildMemorySummary(enhanced);
|
||||
|
||||
return {
|
||||
systemPrompt,
|
||||
memorySummary,
|
||||
tokensUsed: enhanced.totalTokens + estimateTokens(systemPrompt),
|
||||
memoriesInjected: enhanced.memories.length,
|
||||
};
|
||||
}
|
||||
|
||||
// === Identity Loading ===
|
||||
|
||||
async loadIdentity(agentId: string): Promise<AgentIdentity> {
|
||||
// Check cache
|
||||
const cached = this.identityCache.get(agentId);
|
||||
if (cached && Date.now() - cached.cachedAt < ContextBuilder.IDENTITY_CACHE_TTL) {
|
||||
return cached.identity;
|
||||
}
|
||||
|
||||
// Try loading from OpenViking first, fall back to defaults
|
||||
let soul = '';
|
||||
let instructions = '';
|
||||
let userProfile = '';
|
||||
let heartbeat = '';
|
||||
|
||||
try {
|
||||
[soul, instructions, userProfile, heartbeat] = await Promise.all([
|
||||
this.viking.getIdentityFromViking(agentId, 'soul').catch(() => ''),
|
||||
this.viking.getIdentityFromViking(agentId, 'instructions').catch(() => ''),
|
||||
this.viking.getIdentityFromViking(agentId, 'user_profile').catch(() => ''),
|
||||
this.viking.getIdentityFromViking(agentId, 'heartbeat').catch(() => ''),
|
||||
]);
|
||||
} catch {
|
||||
// OpenViking not available, use empty defaults
|
||||
}
|
||||
|
||||
const identity: AgentIdentity = {
|
||||
soul: soul || DEFAULT_SOUL,
|
||||
instructions: instructions || DEFAULT_INSTRUCTIONS,
|
||||
userProfile: userProfile || '',
|
||||
heartbeat: heartbeat || '',
|
||||
};
|
||||
|
||||
this.identityCache.set(agentId, { identity, cachedAt: Date.now() });
|
||||
return identity;
|
||||
}
|
||||
|
||||
// === Context Compaction ===
|
||||
|
||||
async checkAndCompact(
|
||||
messages: ChatMessage[],
|
||||
agentId: string
|
||||
): Promise<CompactionResult | null> {
|
||||
const totalTokens = estimateMessagesTokens(messages);
|
||||
if (totalTokens < this.config.compactionThresholdTokens) {
|
||||
return null; // No compaction needed
|
||||
}
|
||||
|
||||
let memoriesFlushed = 0;
|
||||
|
||||
// Step 1: Memory flush before compaction
|
||||
if (this.config.memoryFlushOnCompact) {
|
||||
const keepCount = 5;
|
||||
const messagesToFlush = messages.slice(0, -keepCount);
|
||||
if (messagesToFlush.length >= this.config.minExtractionMessages) {
|
||||
try {
|
||||
const result = await this.viking.extractAndSaveMemories(
|
||||
messagesToFlush.map(m => ({ role: m.role, content: m.content })),
|
||||
agentId,
|
||||
'compaction'
|
||||
);
|
||||
memoriesFlushed = result.saved;
|
||||
console.log(`[ContextBuilder] Memory flush: saved ${memoriesFlushed} memories before compaction`);
|
||||
} catch (err) {
|
||||
console.warn('[ContextBuilder] Memory flush failed:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Create summary of older messages
|
||||
const keepCount = 5;
|
||||
const oldMessages = messages.slice(0, -keepCount);
|
||||
const recentMessages = messages.slice(-keepCount);
|
||||
|
||||
const summary = this.createCompactionSummary(oldMessages);
|
||||
|
||||
const compactedMessages: ChatMessage[] = [
|
||||
{ role: 'system', content: `[之前的对话摘要]\n${summary}` },
|
||||
...recentMessages,
|
||||
];
|
||||
|
||||
return {
|
||||
compactedMessages,
|
||||
summary,
|
||||
memoriesFlushed,
|
||||
};
|
||||
}
|
||||
|
||||
// === Post-Conversation Memory Extraction ===
|
||||
|
||||
async extractMemoriesFromConversation(
|
||||
messages: ChatMessage[],
|
||||
agentId: string,
|
||||
conversationId?: string
|
||||
): Promise<{ saved: number; userMemories: number; agentMemories: number }> {
|
||||
if (!this.config.autoExtractOnComplete) {
|
||||
return { saved: 0, userMemories: 0, agentMemories: 0 };
|
||||
}
|
||||
|
||||
if (messages.length < this.config.minExtractionMessages) {
|
||||
return { saved: 0, userMemories: 0, agentMemories: 0 };
|
||||
}
|
||||
|
||||
const connected = await this.viking.isConnected();
|
||||
if (!connected) {
|
||||
return { saved: 0, userMemories: 0, agentMemories: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.viking.extractAndSaveMemories(
|
||||
messages.map(m => ({ role: m.role, content: m.content })),
|
||||
agentId,
|
||||
conversationId
|
||||
);
|
||||
console.log(
|
||||
`[ContextBuilder] Extracted ${result.saved} memories (user: ${result.userMemories}, agent: ${result.agentMemories})`
|
||||
);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.warn('[ContextBuilder] Memory extraction failed:', err);
|
||||
return { saved: 0, userMemories: 0, agentMemories: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// === Identity Sync ===
|
||||
|
||||
async syncIdentityFiles(
|
||||
agentId: string,
|
||||
files: { soul?: string; instructions?: string; userProfile?: string; heartbeat?: string }
|
||||
): Promise<void> {
|
||||
const connected = await this.viking.isConnected();
|
||||
if (!connected) return;
|
||||
|
||||
const syncTasks: Promise<void>[] = [];
|
||||
|
||||
if (files.soul) {
|
||||
syncTasks.push(this.viking.syncIdentityToViking(agentId, 'SOUL.md', files.soul));
|
||||
}
|
||||
if (files.instructions) {
|
||||
syncTasks.push(this.viking.syncIdentityToViking(agentId, 'AGENTS.md', files.instructions));
|
||||
}
|
||||
if (files.userProfile) {
|
||||
syncTasks.push(this.viking.syncIdentityToViking(agentId, 'USER.md', files.userProfile));
|
||||
}
|
||||
if (files.heartbeat) {
|
||||
syncTasks.push(this.viking.syncIdentityToViking(agentId, 'HEARTBEAT.md', files.heartbeat));
|
||||
}
|
||||
|
||||
await Promise.allSettled(syncTasks);
|
||||
|
||||
// Invalidate cache
|
||||
this.identityCache.delete(agentId);
|
||||
}
|
||||
|
||||
// === Configuration ===
|
||||
|
||||
updateConfig(config: Partial<ContextBuilderConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
getConfig(): Readonly<ContextBuilderConfig> {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.config.enabled;
|
||||
}
|
||||
|
||||
// === Private Helpers ===
|
||||
|
||||
private composeSystemPrompt(identity: AgentIdentity, enhanced: EnhancedContext): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
if (identity.soul) {
|
||||
sections.push(identity.soul);
|
||||
}
|
||||
|
||||
if (identity.instructions) {
|
||||
sections.push(identity.instructions);
|
||||
}
|
||||
|
||||
if (identity.userProfile) {
|
||||
sections.push(`## 用户画像\n${identity.userProfile}`);
|
||||
}
|
||||
|
||||
if (enhanced.systemPromptAddition) {
|
||||
sections.push(enhanced.systemPromptAddition);
|
||||
}
|
||||
|
||||
return sections.join('\n\n');
|
||||
}
|
||||
|
||||
private buildMemorySummary(enhanced: EnhancedContext): string {
|
||||
if (enhanced.memories.length === 0) {
|
||||
return '无相关记忆';
|
||||
}
|
||||
|
||||
const parts: string[] = [
|
||||
`已注入 ${enhanced.memories.length} 条相关记忆`,
|
||||
`Token 消耗: L0=${enhanced.tokensByLevel.L0} L1=${enhanced.tokensByLevel.L1} L2=${enhanced.tokensByLevel.L2}`,
|
||||
];
|
||||
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
private createCompactionSummary(messages: ChatMessage[]): string {
|
||||
// Create a concise summary of compacted messages
|
||||
const userMessages = messages.filter(m => m.role === 'user');
|
||||
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
||||
|
||||
const topics = userMessages
|
||||
.map(m => {
|
||||
const text = m.content.trim();
|
||||
return text.length > 50 ? text.slice(0, 50) + '...' : text;
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
const summary = [
|
||||
`对话包含 ${messages.length} 条消息(${userMessages.length} 条用户消息,${assistantMessages.length} 条助手回复)`,
|
||||
topics.length > 0 ? `讨论主题:${topics.join(';')}` : '',
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
// === Default Identity Content ===
|
||||
|
||||
const DEFAULT_SOUL = `# ZCLAW 人格
|
||||
|
||||
你是 ZCLAW(小龙虾),一个基于 OpenClaw 定制的中文 AI 助手。
|
||||
|
||||
## 核心特质
|
||||
|
||||
- **高效执行**: 你不只是出主意,你会真正动手完成任务
|
||||
- **中文优先**: 默认使用中文交流,必要时切换英文
|
||||
- **专业可靠**: 对技术问题给出精确答案,不确定时坦诚说明
|
||||
- **主动服务**: 定期检查任务清单,主动推进未完成的工作
|
||||
|
||||
## 语气
|
||||
|
||||
简洁、专业、友好。避免过度客套,直接给出有用信息。`;
|
||||
|
||||
const DEFAULT_INSTRUCTIONS = `# Agent 指令
|
||||
|
||||
## 操作规范
|
||||
|
||||
1. 执行文件操作前,先确认目标路径
|
||||
2. 执行 Shell 命令前,评估安全风险
|
||||
3. 长时间任务需定期汇报进度
|
||||
|
||||
## 记忆管理
|
||||
|
||||
- 重要的用户偏好自动记录
|
||||
- 项目上下文保存到工作区
|
||||
- 对话结束时总结关键信息`;
|
||||
|
||||
// === Singleton ===
|
||||
|
||||
let _instance: ContextBuilder | null = null;
|
||||
|
||||
export function getContextBuilder(config?: Partial<ContextBuilderConfig>): ContextBuilder {
|
||||
if (!_instance || config) {
|
||||
_instance = new ContextBuilder(config);
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
export function resetContextBuilder(): void {
|
||||
_instance = null;
|
||||
}
|
||||
Reference in New Issue
Block a user