Cómo funciona
El algoritmo de firma, por qué la coincidencia es exacta y qué registra realmente el libro mayor.
Firmas
Cada decisión se normaliza en una firma estable antes de usarse como clave de caché:
export function signature(decision: Decision): string {
const normalized = {
intent: decision.intent,
params: sortKeysDeep(decision.params),
};
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
La normalización es deliberadamente superficial: las claves de los objetos se ordenan de forma recursiva para que { a: 1, b: 2 } y { b: 2, a: 1 } generen hashes idénticos, pero no se modifica nada sobre intent ni los valores de los parámetros. No hay reducción a la raíz, normalización de mayúsculas/minúsculas ni canonicalización semántica.
{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 482 } }
{ intent: "summarize_pr", params: { pr: 482, repo: "acme/widgets" } }
│ │
└──────────────── same signature ──────┘
{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 483 } }
│
└── different signature (different pr) ──┘
Por qué coincidencia exacta y no coincidencia semántica
Una caché semántica/difusa capturaría más repeticiones: «resume este PR» y «dame un resumen de esta solicitud de extracción» producirían un acierto. También significaría que Runic tendría que juzgar si dos solicitudes redactadas de forma diferente son lo suficientemente parecidas como para compartir una respuesta almacenada en caché, que es precisamente el tipo de decisión no determinista que Runic está diseñado para evitar.
La coincidencia exacta significa que un acierto de caché es demostrablemente la misma decisión, no probablemente la misma decisión. El coste es que quienes llaman deben ser deliberados sobre lo que incluyen en params; consulta Inicio rápido.
Qué registra el libro mayor
El libro mayor es un registro de solo anexado, no un total acumulado que pueda desviarse:
interface LedgerEvent {
signature: string;
kind: "hit" | "miss";
tokensSpent: number;
timestamp: number;
}
- En un fallo, Runic registra los
tokensSpentque tu código informó para producir el artefacto. - En un acierto, Runic busca el fallo original para esa firma y registra el mismo valor de
tokensSpentque se guardó; nunca una estimación, nunca una suposición. Si de alguna manera no existe ningún fallo previo para una firma que está generando un acierto, registra0en lugar de inventar un número.
summary.totalSpent = sum of all miss events
summary.totalSaved = sum of all hit events
tokensWithoutRunic = totalSpent + totalSaved (what it would've cost with no cache at all)
savingsPercent = totalSaved / tokensWithoutRunic
Runic nunca mide por sí mismo el coste de tokens. Solo repite lo que el código que llama le indicó mediante storeResult(decision, artifact, tokensSpent); consulta Benchmarks para saber cómo openrouter-savings obtiene ese número de una respuesta de API real en lugar de una estimación.
Backends de almacenamiento
Tanto @runic-labs/cache como @runic-labs/ledger se basan en una pequeña interfaz de almacenamiento, con dos implementaciones incluidas en la v1:
| Backend | Duración | Caso de uso |
|---|---|---|
MemoryCacheStore / MemoryLedgerStore |
Solo durante la vida del proceso | Pruebas, scripts efímeros, benchmarks |
FileCacheStore / FileLedgerStore |
Persistido en disco (.runic/ de forma predeterminada) |
Un proceso de CLI y un proceso de agente que leen el mismo estado sin un servidor en ejecución |
Ambos tienen ámbito por sesión en la v1; consulta Ámbito para saber por qué compartir entre sesiones es deliberadamente una fase posterior, en lugar de algo añadido apresuradamente ahora.
La obsolescencia es solo informativa
Las entradas de caché incluyen una marca stale, calculada a partir de una ventana configurable (24 h de forma predeterminada). Runic nunca expulsa ni rechaza automáticamente una entrada obsoleta: es información para que quien llama actúe si lo desea, no un mecanismo de aplicación. Si tu caso de uso necesita una expiración estricta, comprueba entry.stale por tu cuenta y decide qué hacer con ella.