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

109 lines (108 loc) • 4.99 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/. /** * Bridge per-call LLM usage telemetry (#576) to the events stream. * * `usage-telemetry.ts` stays dependency-free of the events/db layer so the * low-level `client.ts` never imports persistence. This module is the wiring: * it installs a {@link LlmUsageSink} that persists each {@link LlmUsageRecord} * as one `llm_usage` event. * * Why reuse the events table (vs a dedicated table): volume is low (~100 * calls/day), the records are append-only and time-windowed exactly like every * other event, and `akm health` already aggregates per-window event reads — a * separate table would duplicate retention (`purgeOldEvents`), reads, and * migration surface for no benefit. See the commit message for #576. * * Every record is written through `appendEvent`, which is itself best-effort * (a write failure logs once and never throws). Combined with the sink-error * swallowing in `emitLlmUsage`, telemetry can never break a real run. */ import { appendEvent } from "../core/events.js"; import { clearLlmUsageSink, hasLlmUsageSink, setLlmUsageSink } from "./usage-telemetry.js"; /** Event type for persisted per-call LLM usage telemetry. */ export const LLM_USAGE_EVENT = "llm_usage"; /** Event type for the owning sink's terminal-record count marker. */ export const LLM_USAGE_SUMMARY_EVENT = "llm_usage_summary"; /** * Project a usage record into event metadata, dropping `undefined` token * fields so an absent-usage call records only `{stage, model, durationMs}`. */ function toEventMetadata(record) { const metadata = { durationMs: record.durationMs, outcome: record.outcome, modelSource: record.modelSource, }; if (record.stage !== undefined) metadata.stage = record.stage; if (record.engine !== undefined) metadata.engine = record.engine; if (record.process !== undefined) metadata.process = record.process; if (record.model !== undefined) metadata.model = record.model; if (record.finishReason !== undefined) metadata.finishReason = record.finishReason; if (record.promptTokens !== undefined) metadata.promptTokens = record.promptTokens; if (record.completionTokens !== undefined) metadata.completionTokens = record.completionTokens; if (record.totalTokens !== undefined) metadata.totalTokens = record.totalTokens; if (record.reasoningTokens !== undefined) metadata.reasoningTokens = record.reasoningTokens; if (record.errorCode !== undefined) metadata.errorCode = record.errorCode; return metadata; } /** * Install a usage sink that persists each LLM call as an `llm_usage` event via * `appendEvent`. Returns a disposer that clears the sink — call it in a * `finally` block so per-run wiring does not leak across runs (and so the * test-isolation harness sees a clean sink between tests). * * `ctx` should carry the same long-lived `state.db` handle the caller already * opened for its other events. A getter is resolved for every append so a * caller can replace its context binding without replacing this owning sink. * When omitted, `appendEvent` falls back to its default open-insert-close path. * * `onRecord`, when supplied, runs synchronously for every terminal record * BEFORE persistence — improve's first-engine-response heartbeat (#957) uses * it to know the run is no longer silent, without this module taking on any * dependency of its own on improve's lifecycle. */ export function installLlmUsagePersistence(ctx, onRecord) { let expectedTerminalRecords = 0; let disposed = false; setLlmUsageSink((record) => { expectedTerminalRecords += 1; onRecord?.(record); appendEvent({ eventType: LLM_USAGE_EVENT, metadata: toEventMetadata(record) }, typeof ctx === "function" ? ctx() : ctx); }); return () => { if (disposed) return; disposed = true; clearLlmUsageSink(); try { appendEvent({ eventType: LLM_USAGE_SUMMARY_EVENT, metadata: { expectedTerminalRecords } }, typeof ctx === "function" ? ctx() : ctx); } catch { // Persistence remains best-effort even during run teardown. } }; } /** * Like {@link installLlmUsagePersistence}, but a no-op when a sink is already * installed — used by standalone entry points (`akm proposal drain`) * that may also run as a sub-step of `akm improve`. When invoked inside an * enclosing run the existing per-run sink keeps ownership; the returned * disposer then does nothing, so the enclosing run's `finally` still clears it. */ export function installLlmUsagePersistenceIfAbsent(ctx) { if (hasLlmUsageSink()) return () => { }; return installLlmUsagePersistence(ctx); }