Result Caching (S-03)

Delibera can cache DebateResult to avoid re-running identical debates. The cache key is derived from a SHA-256 hash of the debate-defining inputs (question, members, strategy, knowledge, configuration).

Not the same thing: cost gating#

Caching and cost gating are both about money, but they sit on opposite sides of a debate.

Result cache (IDebateCache) Cost gate (ICostGate)
Question it answers Has this exact debate already been run? May this debate still spend?
Consulted Before execution Before each member call
Effect of a hit/denial Returns the stored DebateResult Skips the call; the debate returns a degraded result carrying the spend so far
Requires prices No WithCostLimit does; WithTokenBudget does not

A cache hit avoids the spend entirely; a cost gate bounds the spend of a debate that actually runs. Both can be configured on the same council, and both leave the result cache untouched.

.WithTokenBudget(200_000)                       // no price list needed
.WithRateLimit(20, TimeSpan.FromMinutes(1))      // Queue / Throw / Drop, PerModel by default

Exceeding a ceiling never throws — see the cost-limit section of QuickStart.md.

Cache Behavior#

Mode Read Write Use Case
Disabled No No Default — no caching
ReadWrite Yes Yes Production — return cached or execute & cache
ReadOnly Yes No Stale cache is acceptable, don't cache new results
WriteThrough No Yes Always execute, always cache (warm-up)
Bypass No No Force fresh execution even when cache is configured

Quick Start#

In-Memory Cache#

services.AddDelibera(configuration)
    .UseInMemoryCache(ttl: TimeSpan.FromHours(1));

File Cache#

services.AddDelibera(configuration)
    .UseFileCache(directory: "./debate-cache", ttl: TimeSpan.FromDays(7));

Redis Cache#

services.AddRedisDebateOrchestrator(configuration);
services.UseRedisCache(ttl: TimeSpan.FromHours(24));

Per-Debate Control#

var builder = new CouncilBuilder()
    .WithUserPrompt("What is the best architecture?")
    .AddMember("gpt-4o", provider, "Architect")
    .WithCacheBehavior(CacheBehavior.ReadWrite);  // or Bypass, WriteThrough, etc.

var executor = builder.Build();
var result = await executor.ExecuteAsync();

Console.WriteLine($"Cache hit: {result.CacheHit}");   // true if served from cache
Console.WriteLine($"Cache key: {result.CacheKey}");    // e.g. "A3F2B8C1D4E5F6A7"
Console.WriteLine($"Cached at: {result.CachedAt}");    // original cache timestamp

Cache Key Generation#

DebateCacheKeyGenerator produces a deterministic 16-character hex key from:

  • User prompt (PromptContext.UserPrompt)
  • Member display names (sorted alphabetically)
  • Strategy name
  • Max rounds
  • Temperature
  • System prompt
  • Knowledge content hash (SHA-256)
var key = DebateCacheKeyGenerator.Generate(
    context, members, "Standard", maxRounds: 4, temperature: 0.7f, systemPrompt);
// => "A3F2B8C1D4E5F6A7"

Two debates with identical inputs always produce the same key.

IDebateCache Interface#

public interface IDebateCache
{
    Task<DebateResult?> GetAsync(string cacheKey, CancellationToken ct = default);
    Task SetAsync(string cacheKey, DebateResult result, TimeSpan? ttl = null, CancellationToken ct = default);
    Task InvalidateAsync(string cacheKey, CancellationToken ct = default);
    Task<bool> ExistsAsync(string cacheKey, CancellationToken ct = default);
}

Implementation Details#

InMemoryDebateCache#

Uses IMemoryCache with sliding expiration. Suitable for single-instance development and testing.

FileDebateCache#

Persists DebateResult as JSON files ({cacheKey}.cache.json) in a configurable directory. TTL is enforced via file modification time — expired files are deleted on read. Suitable for local development and CLI benchmark caching.

RedisDebateCache#

Uses StackExchange.Redis with SETEX for TTL. Shares the Redis connection with RedisDebateOrchestrator. Suitable for distributed/production deployments.

OpenTelemetry#

When caching is enabled, the delibera.cache.hit counter is incremented on each cache hit, tagged with cache_backend (e.g., InMemoryDebateCache, FileDebateCache, RedisDebateCache).

appsettings.json#

{
  "Delibera": {
    "Cache": {
      "Provider": "InMemory",
      "DefaultTtlMinutes": 60,
      "FileDirectory": "./debate-cache",
      "RedisTtlHours": 24
    }
  }
}
Field Description
Provider InMemory, File, Redis, or None
DefaultTtlMinutes Default TTL for cache entries
FileDirectory Directory for FileDebateCache
RedisTtlHours TTL for RedisDebateCache

DebateResponse DTO#

When a debate result is served from cache, the HTTP response includes:

{
  "debateId": "a1b2c3d4",
  "status": "Completed",
  "cacheHit": true,
  "cacheKey": "A3F2B8C1D4E5F6A7",
  ...
}

Source: docs/caching.md on GitHub — edit it there and this page updates on the next deploy.