Aller au contenu
Runic
Français
Esc
naviguerouvrir⌘Japerçu
Sur cette page

Comment ça marche

L’algorithme de signature, pourquoi la correspondance est exacte et ce que le registre enregistre réellement.

Signatures

Chaque décision est normalisée en une signature stable avant d’être utilisée comme clé 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");
}

La normalisation est volontairement superficielle — les clés des objets sont triées récursivement afin que { a: 1, b: 2 } et { b: 2, a: 1 } produisent un hash identique, mais rien concernant intent ou les valeurs des paramètres n’est modifié. Il n’y a ni racinisation, ni normalisation de la casse, ni canonicalisation sémantique.

{ 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) ──┘

Pourquoi une correspondance exacte, et non sémantique

Un cache sémantique/flou détecterait davantage de répétitions — « résume cette PR » et « donne-moi un résumé de cette pull request » seraient tous deux trouvés. Mais cela signifierait aussi que Runic doit déterminer si deux requêtes formulées différemment sont suffisamment proches pour partager une réponse mise en cache, ce qui est précisément le genre de jugement non déterministe que Runic est conçu pour éviter.

Une correspondance exacte signifie qu’un succès de cache est prouvablement la même décision, et non probablement la même décision. En contrepartie, les appelants doivent choisir délibérément ce qui entre dans params — voir Démarrage rapide.

Ce que le registre enregistre

Le registre est un journal en ajout seul, et non un total cumulé susceptible de dériver :

interface LedgerEvent {
  signature: string;
  kind: "hit" | "miss";
  tokensSpent: number;
  timestamp: number;
}
  • Lors d’un échec, Runic enregistre les tokensSpent que votre code a signalés pour produire l’artefact.
  • Lors d’un succès, Runic recherche l’échec d’origine pour cette signature et enregistre la même valeur tokensSpent que celle qui a été sauvegardée — jamais une estimation, jamais une supposition. Si, pour une raison quelconque, aucun échec antérieur n’existe pour une signature qui est trouvée, il enregistre 0 plutôt que d’inventer un nombre.
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 ne mesure jamais lui-même le coût en tokens. Il ne fait que répéter ce que le code appelant lui a indiqué via storeResult(decision, artifact, tokensSpent) — voir Benchmarks pour savoir comment openrouter-savings obtient ce nombre à partir d’une réponse d’API réelle plutôt que d’une estimation.

Backends de stockage

@runic-labs/cache comme @runic-labs/ledger sont construits autour d’une petite interface de stockage, avec deux implémentations livrées dans la v1 :

Backend Durée de vie Cas d’utilisation
MemoryCacheStore / MemoryLedgerStore Durée de vie du processus uniquement Tests, scripts éphémères, benchmarks
FileCacheStore / FileLedgerStore Persisté sur le disque (.runic/ par défaut) Un processus CLI et un processus d’agent lisant le même état sans serveur en cours d’exécution

Les deux ont une portée par session dans la v1 — voir Portée pour comprendre pourquoi le partage entre sessions est délibérément une phase ultérieure, plutôt qu’une fonctionnalité ajoutée à la hâte maintenant.

L’obsolescence est uniquement indicative

Les entrées du cache portent un indicateur stale, calculé à partir d’une fenêtre configurable (24 h par défaut). Runic n’évince ni ne refuse jamais automatiquement une entrée obsolète — c’est une information sur laquelle l’appelant peut agir s’il le souhaite, et non un mécanisme d’application. Si votre cas d’utilisation nécessite une expiration stricte, vérifiez vous-même entry.stale et décidez quoi en faire.

Cette page vous a-t-elle été utile ?