Como funciona
O algoritmo de assinatura, por que a correspondência é exata e o que o registro realmente grava.
Assinaturas
Cada decisão é normalizada em uma assinatura estável antes de ser usada como chave de cache:
export function signature(decision: Decision): string {
const normalized = {
intent: decision.intent,
params: sortKeysDeep(decision.params),
};
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
A normalização é deliberadamente superficial — as chaves de objeto são ordenadas recursivamente para que { a: 1, b: 2 } e { b: 2, a: 1 } gerem hashes idênticos, mas nada relacionado a intent ou aos valores dos parâmetros é alterado. Não há stemming, normalização de maiúsculas/minúsculas nem canonização 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 que correspondência exata, e não semântica
Um cache semântico/aproximado capturaria mais repetições — “resuma este PR” e “dê-me um resumo desta solicitação de pull” resultariam ambos em acertos. Isso também significaria que o Runic teria de avaliar se duas solicitações formuladas de maneira diferente são próximas o suficiente para compartilhar uma resposta em cache, que é exatamente o tipo de decisão não determinística que o Runic foi projetado para evitar.
A correspondência exata significa que um acerto de cache é comprovadamente a mesma decisão, e não provavelmente a mesma decisão. O custo é que os chamadores precisam ser intencionais quanto ao que entra em params — consulte Início rápido.
O que o registro grava
O registro é um log somente de anexação, não um total acumulado que pode se desviar:
interface LedgerEvent {
signature: string;
kind: "hit" | "miss";
tokensSpent: number;
timestamp: number;
}
- Em uma falha, o Runic registra os
tokensSpentque seu código informou para produzir o artefato. - Em um acerto, o Runic procura a falha original para essa assinatura e registra o mesmo valor de
tokensSpentque foi salvo — nunca uma estimativa, nunca um palpite. Se, de alguma forma, não houver nenhuma falha anterior para uma assinatura que está sendo acertada, ele registra0em vez de inventar um 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
O Runic nunca mede o custo de tokens por conta própria. Ele apenas repete o que o código chamador informou por meio de storeResult(decision, artifact, tokensSpent) — consulte Benchmarks para saber como openrouter-savings obtém esse número de uma resposta de API real, em vez de uma estimativa.
Backends de armazenamento
Tanto @runic-labs/cache quanto @runic-labs/ledger são construídos com base em uma pequena interface de armazenamento, com duas implementações distribuídas na v1:
| Backend | Duração | Caso de uso |
|---|---|---|
MemoryCacheStore / MemoryLedgerStore |
Apenas durante a vida do processo | Testes, scripts efêmeros, benchmarks |
FileCacheStore / FileLedgerStore |
Persistido em disco (.runic/ por padrão) |
Um processo de CLI e um processo de agente lendo o mesmo estado sem um servidor em execução |
Ambos têm escopo por sessão na v1 — consulte Escopo para entender por que o compartilhamento entre sessões é deliberadamente uma fase posterior, em vez de algo acrescentado às pressas agora.
A expiração é apenas informativa
As entradas de cache têm uma flag stale, calculada a partir de uma janela configurável (24h por padrão). O Runic nunca remove nem recusa automaticamente uma entrada expirada — é uma informação para o chamador usar, se quiser, e não um mecanismo de imposição. Se o seu caso de uso exigir expiração rígida, verifique entry.stale você mesmo e decida o que fazer com ela.