---
title: Como funciona
description: >-
  O algoritmo de assinatura, por que a correspondência é exata e o que o
  registro realmente grava.
sidebar:
  order: 2
---
## Assinaturas

Cada decisão é normalizada em uma assinatura estável antes de ser usada como chave de cache:

```ts
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.

```txt
{ 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](/quickstart#choosing-intent-and-params).

## O que o registro grava

O registro é um log somente de anexação, não um total acumulado que pode se desviar:

```ts
interface LedgerEvent {
  signature: string;
  kind: "hit" | "miss";
  tokensSpent: number;
  timestamp: number;
}
```

- **Em uma falha**, o Runic registra os `tokensSpent` que 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 `tokensSpent` que 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 registra `0` em vez de inventar um número.

```txt
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](/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](/scope) 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.
