UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

149 lines (148 loc) • 6.86 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * LLM helper for the `akm index` memory-inference pass (#201). * * Compresses a single memory body into one higher-signal derived memory. The * pass itself (in `src/indexer/passes/memory-inference.ts`) is responsible for * deciding which memories are pending, persisting the derived memory with the * correct frontmatter (`inferred: true`, `source: <parent-ref>`), and marking * the parent as processed for idempotency. * * This module is intentionally tiny and stateless so tests can stub it via * `mock.module("../src/llm/memory-infer", ...)` without hitting a network. * * The symbolic LLM runner comes from the current index-pass execution * resolution and is passed straight through. */ import memoryInferSystemPrompt from "../assets/prompts/memory-infer-system.md" with { type: "text" }; import memoryInferUserPrompt from "../assets/prompts/memory-infer-user.md" with { type: "text" }; import { toErrorMessage } from "../core/common.js"; import { parseEmbeddedJsonResponse } from "../core/parse.js"; import { warn } from "../core/warn.js"; import { runnerLlmConnection } from "../integrations/agent/runner.js"; import { callStructured } from "./structured-call.js"; const SYSTEM_PROMPT = memoryInferSystemPrompt; const USER_PROMPT_PREFIX = memoryInferUserPrompt; const PROMPT_PLACEHOLDERS = new Set([ "short title string", "one sentence summary string", "tag1", "tag2", "search phrase 1", "search phrase 2", "2-3 sentence compressed body preserving key facts verbatim", ]); /** * Strict JSON Schema for the derived-memory payload. Sent as `response_format` * unless the engine sets `supportsJsonSchema: false`; the client drops it once * for a provider that rejects it. * * Extends the responseSchema lift (PR 1, asset-writers-investigation §5) to * the memory-inference path. Mirrors the validation gate below * (title/description/content + non-empty tags/searchHints) so a * schema-compliant response is guaranteed to pass the downstream check * — no more "incomplete derived memory payload from LLM; skipping memory" * for shape-only failures. */ const DERIVED_MEMORY_JSON_SCHEMA = { type: "object", properties: { title: { type: "string", minLength: 1 }, description: { type: "string", minLength: 1 }, content: { type: "string", minLength: 1 }, tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 8 }, searchHints: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 6 }, }, required: ["title", "description", "content", "tags", "searchHints"], additionalProperties: false, }; /** * Compress a single memory body into one derived memory via the configured LLM. * * Returns `undefined` on any failure (timeout, invalid JSON, empty response). * Errors are logged via `warn()` but never thrown — a failed split for one memory * must not abort the rest of the index pass. * * Routes through `callStructured({ feature: "memory_inference", ... })` so the * feature gate, error classification, and onFallback hook are honoured uniformly * (Fix C5). */ export async function compressMemoryToDerivedMemory(llmRunner, body, signal, akmConfig, onFallback, telemetry, onRetryAttempt, onNotices) { const trimmedBody = body.trim(); if (!trimmedBody) return undefined; const userPrompt = `${USER_PROMPT_PREFIX}${trimmedBody}`; // Memory inference is always gated: no config closes the gate instead of // taking callStructured's ungated path for direct callers. if (!akmConfig) { onFallback?.({ feature: "memory_inference", reason: "disabled" }); return undefined; } return callStructured({ feature: "memory_inference", akmConfig, runner: llmRunner, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: userPrompt }, ], request: { // The engine's or the process's configured temperature; 0.1 only when neither sets one. temperature: runnerLlmConnection(llmRunner)?.temperature ?? 0.1, ...(Object.hasOwn(llmRunner, "timeoutMs") ? { timeoutMs: llmRunner.timeoutMs } : {}), signal, responseSchema: DERIVED_MEMORY_JSON_SCHEMA, onRetryAttempt, }, onNotices, parse: (raw) => { if (!raw) return undefined; const parsed = parseEmbeddedJsonResponse(raw); if (!parsed) { warn("memory inference: invalid JSON response from LLM; skipping memory."); return undefined; } const title = typeof parsed.title === "string" ? parsed.title.trim() : ""; const description = typeof parsed.description === "string" ? parsed.description.trim() : ""; const content = typeof parsed.content === "string" ? parsed.content.trim() : ""; const tags = Array.isArray(parsed.tags) ? parsed.tags .filter((t) => typeof t === "string") .map((t) => t.trim()) .filter(Boolean) .slice(0, 8) : []; const searchHints = Array.isArray(parsed.searchHints) ? parsed.searchHints .filter((h) => typeof h === "string") .map((h) => h.trim()) .filter(Boolean) .slice(0, 6) : []; if (!title || !description || !content || tags.length === 0 || searchHints.length === 0) { warn("memory inference: incomplete derived memory payload from LLM; skipping memory."); return undefined; } if ([title, description, content, ...tags, ...searchHints].some((value) => PROMPT_PLACEHOLDERS.has(value.toLowerCase()))) { warn("memory inference: prompt placeholder response from LLM; skipping memory."); return undefined; } return { title, description, tags, searchHints, content }; }, onError: (cls, err) => { if (cls === "html") { if (telemetry) telemetry.htmlErrorCount = (telemetry.htmlErrorCount ?? 0) + 1; warn(`memory inference: provider returned HTML instead of JSON; skipping memory: ${toErrorMessage(err)}`); return undefined; } warn(`memory inference failed: ${toErrorMessage(err)}`); return undefined; }, fallback: undefined, onFallback, }); }