UNPKG

@tencentdb-agent-memory/memory-tencentdb

Version:

Four-layer local memory system plugin for OpenClaw — auto-captures, structures, and profiles conversational knowledge using local LLM + SQLite vector search (L0→L1→L2→L3 pipeline)

337 lines (306 loc) 11.5 kB
/** * Opik observability tracer for offload server. * Wraps the opik npm package with graceful degradation when not installed. * Configuration is read from environment variables (no OpenClaw plugin config). */ // Opik client types (minimal shape to avoid hard dependency) interface OpikClient { trace(params: Record<string, unknown>): OpikTrace; flush(): Promise<void>; } interface OpikTrace { update(params: Record<string, unknown>): void; end(): void; span(params: Record<string, unknown>): OpikSpan; } interface OpikSpan { update(params: Record<string, unknown>): void; end(): void; } // ─── Module State ──────────────────────────────────────────────────────────── let client: OpikClient | null = null; let tracerEnabled = false; let tracerInitTried = false; // ─── Helpers ───────────────────────────────────────────────────────────────── function extractLayerTag(stage: string): string { const match = stage.match(/^(L\d+(?:\.\d+)?)/i); if (!match) return "Lx-unknown"; return match[1].toUpperCase(); } function durationBucketTag(ms: number): string { if (typeof ms !== "number" || ms < 0) return "duration:unknown"; if (ms < 1000) return "duration:<1s"; if (ms < 5000) return "duration:1-5s"; if (ms < 15000) return "duration:5-15s"; if (ms < 30000) return "duration:15-30s"; return "duration:>30s"; } function formatDuration(ms: number): string { if (typeof ms !== "number" || ms < 0) return "?"; if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(2)}s`; } // ─── Logger Interface ──────────────────────────────────────────────────────── export interface TracerLogger { info: (message: string, ...args: unknown[]) => void; warn: (message: string, ...args: unknown[]) => void; debug?: (message: string, ...args: unknown[]) => void; } // ─── Init ──────────────────────────────────────────────────────────────────── /** * Initialize the offload server Opik tracer. * Reads config from environment variables: * OPIK_ENABLED — set to "true" to enable (default: disabled) * OPIK_URL_OVERRIDE — Opik server URL * OPIK_API_KEY — API key * OPIK_WORKSPACE — workspace name (default: "default") * OPIK_PROJECT_NAME — project name (default: "openclaw-offload-server") */ export async function initServerOpikTracer(logger: TracerLogger): Promise<void> { if (tracerInitTried) return; tracerInitTried = true; try { const enabled = process.env.OPIK_ENABLED === "true"; if (!enabled) { logger.debug?.("[offload-server] Opik tracer disabled (OPIK_ENABLED != true)"); return; } const apiUrl = process.env.OPIK_URL_OVERRIDE; const apiKey = process.env.OPIK_API_KEY; const workspaceName = process.env.OPIK_WORKSPACE ?? "default"; const projectName = process.env.OPIK_PROJECT_NAME ?? "openclaw-offload-server"; // Dynamic import — graceful when opik is not installed let OpikConstructor: new (params: Record<string, unknown>) => OpikClient; let disableOpikLogger: (() => void) | undefined; try { const opikModule = await import("opik") as { Opik: new (params: Record<string, unknown>) => OpikClient; disableLogger?: () => void; setLoggerLevel?: (level: string) => void; }; OpikConstructor = opikModule.Opik; disableOpikLogger = opikModule.disableLogger; } catch { logger.debug?.("[offload-server] opik package not available, tracer disabled"); return; } // Suppress opik internal logs (flush messages, ANSI color noise) if (disableOpikLogger) { disableOpikLogger(); } client = new OpikConstructor({ ...(apiKey ? { apiKey } : {}), ...(apiUrl ? { apiUrl } : {}), workspaceName, projectName, }); tracerEnabled = true; logger.info( `[offload-server] Opik tracer enabled: project=${projectName}, workspace=${workspaceName}`, ); } catch (err) { tracerEnabled = false; client = null; logger.warn(`[offload-server] Opik tracer init failed: ${String(err)}`); } } /** * Check if the tracer is enabled and ready to trace. */ export function isTracerEnabled(): boolean { return tracerEnabled && client !== null; } // ─── Trace: Model I/O (L1/L1.5/L2 LLM calls) ─────────────────────────────── /** * Trace LLM model I/O for offload server L1/L1.5/L2 stages. */ export function traceServerModelIo(params: { sessionId: string; stage: string; model: string; systemPrompt: string; userPrompt: string; responseContent: string; usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number }; status: "ok" | "error"; errorMessage?: string; durationMs: number; logger?: TracerLogger; }): void { if (!tracerEnabled || !client) return; try { const layerTag = extractLayerTag(params.stage); const threadId = params.sessionId || `offload-server-${Date.now()}`; const dur = params.durationMs; const durStr = formatDuration(dur); const durBucket = durationBucketTag(dur); const skTag = `session:${params.sessionId || "unknown"}`; const trace = client.trace({ name: `${params.model} · offload-server · ${params.stage} · ${durStr}`, threadId, metadata: { plugin: "openclaw-offload-server", category: "llm", stage: params.stage, layer: layerTag, model: params.model, sessionId: params.sessionId, durationMs: dur, duration: durStr, }, tags: ["offload-server", "llm", layerTag, durBucket, skTag], }); const span = trace.span({ name: `${params.model} · ${params.stage} · ${durStr}`, type: "llm", model: params.model, input: { systemPrompt: params.systemPrompt, userPrompt: params.userPrompt, }, metadata: { stage: params.stage, layer: layerTag, sessionId: params.sessionId, durationMs: dur, duration: durStr, }, }); span.update({ output: { responseContent: params.responseContent, usage: params.usage, durationMs: dur, duration: durStr, error: params.errorMessage, }, metadata: { status: params.status, durationMs: dur, }, }); span.end(); trace.end(); void client.flush().catch(() => undefined); } catch (err) { params.logger?.warn?.(`[offload-server] Opik model I/O trace failed: ${String(err)}`); } } // ─── Trace: Compaction Decision ────────────────────────────────────────────── /** * Trace compaction (L3) decision and results. */ export function traceServerCompaction(params: { sessionId: string; level: string; ratio: number; contextWindow: number; totalTokensBefore: number; totalTokensAfter: number; originalMsgCount: number; compactedMsgCount: number; report: Record<string, unknown>; messages: unknown[]; durationMs: number; logger?: TracerLogger; }): void { if (!tracerEnabled || !client) return; try { const threadId = params.sessionId || `offload-server-${Date.now()}`; const dur = params.durationMs; const durStr = formatDuration(dur); const durBucket = durationBucketTag(dur); const skTag = `session:${params.sessionId || "unknown"}`; const trace = client.trace({ name: `compaction · L3 · ${params.level} · ${durStr} [${params.sessionId}]`, threadId, input: { level: params.level, ratio: params.ratio, contextWindow: params.contextWindow, totalTokensBefore: params.totalTokensBefore, originalMsgCount: params.originalMsgCount, }, metadata: { plugin: "openclaw-offload-server", category: "compaction", stage: "L3", layer: "L3", level: params.level, sessionId: params.sessionId, durationMs: dur, duration: durStr, }, tags: ["offload-server", "compaction", "L3", `level:${params.level}`, durBucket, skTag], }); // Serialize messages for full snapshot const serializedMessages = params.messages.map((msg: any, i: number) => { const role = msg.role ?? "unknown"; const content = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.map((c: any) => { if (c.type === "text") return c.text; if (c.type === "tool_use") return `[tool_use: ${c.name} id=${c.id}]`; if (c.type === "tool_result") return `[tool_result: id=${c.tool_use_id} content=${typeof c.content === "string" ? c.content.slice(0, 500) : JSON.stringify(c.content).slice(0, 500)}]`; return `[${c.type ?? "unknown"}]`; }).join("\n") : ""; return { i, role, content, ...(msg._mmdContextMessage ? { mmdCtx: true } : {}), ...(msg._offloaded ? { offloaded: true } : {}) }; }); trace.update({ output: { totalTokensAfter: params.totalTokensAfter, compactedMsgCount: params.compactedMsgCount, tokenReduction: params.totalTokensBefore - params.totalTokensAfter, report: params.report, messages: serializedMessages, }, }); trace.end(); void client.flush().catch(() => undefined); } catch (err) { params.logger?.warn?.(`[offload-server] Opik compaction trace failed: ${String(err)}`); } } // ─── Trace: Task Decision (L1.5 judgment result) ───────────────────────────── /** * Trace L1.5 task judgment decision. */ export function traceServerTaskDecision(params: { sessionId: string; judgment: Record<string, unknown>; durationMs: number; logger?: TracerLogger; }): void { if (!tracerEnabled || !client) return; try { const threadId = params.sessionId || `offload-server-${Date.now()}`; const dur = params.durationMs; const durStr = formatDuration(dur); const skTag = `session:${params.sessionId || "unknown"}`; const trace = client.trace({ name: `task-decision · L1.5 · ${durStr} [${params.sessionId}]`, threadId, input: { stage: "L1.5", sessionId: params.sessionId, }, metadata: { plugin: "openclaw-offload-server", category: "decision", stage: "L1.5", layer: "L1.5", sessionId: params.sessionId, durationMs: dur, duration: durStr, }, tags: ["offload-server", "decision", "L1.5", skTag], }); trace.update({ output: params.judgment }); trace.end(); void client.flush().catch(() => undefined); } catch (err) { params.logger?.warn?.(`[offload-server] Opik task decision trace failed: ${String(err)}`); } }