Files
zclaw_openfang/docs/archive/v1-viking-dead-code/lib/context-builder.ts
iven 1cf3f585d3 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
2026-03-20 22:14:13 +08:00

410 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
}