// Observer compress — turns one tool_result into one observation via LLM. import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { complete } from "@earendil-works/pi-ai"; import { shouldSkipByArgs } from "../config.js"; import { insertObservation } from "../store/db.js"; import { buildObservationPrompt } from "./prompts.js"; import { parseObservationXml } from "./parser.js"; import type { Observation } from "../types.js"; export interface CompressInput { sessionId: string; project: string; toolName: string; toolInput: unknown; toolResult: unknown; isError: boolean; cwd: string; observedAt: number; } export async function compressOne(ctx: ExtensionContext, input: CompressInput): Promise { // Cheap pre-filter: skip obviously noisy calls. if (shouldSkipByArgs(input.toolName, input.toolInput)) return; // Need a model + auth. const model = ctx.model; if (!model) { console.warn("[pi-mem-cc] no current model available for observer compression"); return; } const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model); if (!auth.ok) { console.warn("[pi-mem-cc] model auth unavailable, skipping observation:", auth.error); return; } if (!auth.apiKey) { console.warn("[pi-mem-cc] model returned no apiKey, skipping observation"); return; } const userPrompt = buildObservationPrompt({ toolName: input.toolName, input: input.toolInput, result: input.toolResult, isError: input.isError, cwd: input.cwd, observedAt: input.observedAt, }); let responseText: string; try { const response = await complete( model, { messages: [ { role: "user" as const, content: [{ type: "text" as const, text: userPrompt }], timestamp: Date.now(), }, ], }, { apiKey: auth.apiKey, headers: auth.headers, }, ); responseText = response.content .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join(""); } catch (err) { console.warn("[pi-mem-cc] observer compression failed:", err); return; } const parsed = parseObservationXml(responseText); if (!parsed.valid) { console.debug("[pi-mem-cc] observer output did not parse:", parsed.reason, responseText.slice(0, 200)); return; } if (parsed.value === null) { // observer chose to skip — nothing to store return; } const obs: Observation = { sessionId: input.sessionId, project: input.project, type: parsed.value.type, title: parsed.value.title, subtitle: parsed.value.subtitle, facts: parsed.value.facts, narrative: parsed.value.narrative, concepts: parsed.value.concepts, filesRead: parsed.value.filesRead, filesModified: parsed.value.filesModified, sourceToolName: input.toolName, createdAt: input.observedAt, }; try { insertObservation(obs); } catch (err) { console.warn("[pi-mem-cc] failed to persist observation:", err); } }