60db Logo
  • Engineering

Cache keys for voice agents: tenant, language, tools, and policy

Cache keys for voice agents must preserve tenant, language, tool, and policy boundaries. Build isolation tests before reuse.

Editorial date
Reading time
25 min
Tenant, language, tool, and policy key teeth fit one context while a mismatched key stops at a barrier

TL;DR: A voice agent cache key is a claim that two computations are interchangeable within a declared scope. Include every input that can change the reusable artifact, derive tenant and permission context on the server, and recheck current authorization before using a cached result for an action.


A caller asks the same question that another caller asked a minute ago. The text matches, the cache hits, and the answer contains information that belongs to a different account.

A less visible failure uses the correct account but an obsolete permission scope. The cached answer proposes a tool that the current caller can no longer invoke, and the application treats an old successful lookup as fresh approval.

Start with one question: what would make reuse wrong? The voice agent observability guide explains how to connect boundaries with evidence; cache correctness needs those boundaries before hit rate becomes useful.

This guide provides a small executable key builder, mutation and isolation tests, an invalidation contract, and a diagnostic worksheet. All examples are synthetic and describe application design choices, including systems evaluated with 60db Agent, without describing any private product implementation.

Public runtime documentation illustrates why the distinction matters. The vLLM security guide says shared processes do not provide tenant isolation and describes cache salting as mitigation for specific risks, rather than a complete isolation boundary.

Table of contents

  1. Define the artifact before its key
  2. Separate identity from client input
  3. Choose every semantic dimension
  4. Build an owned context snapshot
  5. Test collisions and isolation
  6. Keep authorization outside cache history
  7. Invalidate without hiding stale state
  8. Diagnose a wrong reuse
  9. Operate a bounded cache experiment
  10. Frequently asked questions
  11. Put the contract into review

Define the artifact before its key

A response cache, a prefix cache, a synthesized-audio cache, and a tool-result cache reuse different things. They need different compatibility rules even when they share a storage service or an eviction policy.

A prefix cache reuses computation for a common input prefix. A response cache reuses an output, including decisions affected by sampling settings, tool availability, retrieved information, and current business state.

The vLLM prefix caching documentation describes reuse of shared prefix computation. It does not establish that two complete responses are interchangeable, that an authorization decision remains valid, or that previously generated speech still belongs to the active conversation.

For a streamed synthesis greeting, compatibility can depend on voice, pronunciation settings, output format, locale, and the exact normalized text. For an account balance, the relevant boundary includes the account, the permission scope, and the freshness of the underlying balance.

Write the artifact in a complete sentence before selecting fields: "This entry stores a public greeting's encoded audio under an approved voice configuration." That sentence exposes omissions which a generic prompt_hash name can conceal.

Specify what is excluded too. The greeting cache does not store authorization, caller consent, playback ownership, or a conclusion that the caller heard the greeting; those facts have separate owners and lifetimes.

The playback acknowledgment guide separates local media acceptance from endpoint evidence. A cache hit sits even earlier in that chain, so a successful lookup cannot establish successful delivery.

A good key contract states an equivalence relation. Within the supported domain, equal keys mean that the stored artifact may be reused under the current admission and authorization checks.

This contract has two failure directions. A false miss wastes computation because equivalent inputs produce different keys; a false hit reuses incompatible evidence and can change behavior or expose information.

Treat these costs differently during review. A missed opportunity to reuse a public greeting is usually easier to tolerate than an entry crossing a tenant boundary, even if both reduce the same dashboard's performance score.

Define the cache's observer as well. Timing differences can reveal whether a guessed input was used before, so protecting cached bytes alone does not address every information channel.

Public caching discussions have raised this point directly. The vLLM cache-salting proposal discusses group sharing and prompt protection; it is design history that helps frame questions, while the current documentation owns present behavior.

Avoid turning that historical discussion into a claim about all runtimes. The portable requirement is to decide who may benefit from shared reuse, who may observe its effects, and which deployment boundary enforces that decision.

Separate identity from client input

Derive the tenant identifier from authenticated server context. A request body field named tenant is an assertion from a client until the application verifies it against the authenticated principal.

Do the same for permission scope. A client-provided list of tool names describes what it wants to use; the server's effective capability set describes what the current principal may use.

A caller can belong to an organization and still lack access to one account inside it. If the cached artifact contains account-specific information, an organization-only key is too broad even when the organization identity is authentic.

Use the narrowest identity that owns the content. A public documentation excerpt may be shareable across callers, while a retrieved internal document requires the document access boundary and a revision that identifies the content used.

Do not place credentials in keys. Storage consoles, metrics labels, support bundles, and request logs often expose key strings more widely than encrypted values, so a raw token is a poor identifier for any observable cache namespace.

A digest does not make a low-entropy secret safe to publish. If an observer can enumerate likely inputs, hashing a phone number or a small account identifier still permits guesses against the digest.

Use opaque server-managed identifiers for key dimensions and minimize retention. When a runtime supports a secret salt for a specific caching layer, keep that salt out of normal logs and handle rotation as a configuration event.

The application namespace and the runtime salt solve different problems. A namespace separates application entries; a runtime salt can scope reuse in a lower layer that the application response cache does not control.

Apply this distinction when comparing local and managed voice deployments. A managed endpoint can hide cache internals, but the application still owns authenticated request context and must ask the provider what isolation guarantees the public interface supplies.

A cache key also needs a schema version. Without one, changing the meaning of an existing field can accidentally join old and new entries under the same storage prefix during a rolling deployment.

Increment that version when compatibility semantics change. Do not increment it for every code deployment, because unrelated releases should not erase the ability to reason about the same artifact across instances.

Maintain a short owner record for each field. It should say where the value comes from, what makes it change, which component validates it, and what happens when it is missing.

Reject missing security dimensions at the boundary. Substituting an empty string for an absent tenant or permission revision makes invalid contexts converge on one apparently valid namespace.

Authenticated context becomes a canonical key, followed by a separate current authorization gate before reuse

The conceptual figure shows two gates. Key compatibility controls which entry is a candidate; current authorization controls whether the candidate can be used now.

Choose every semantic dimension

For a voice response, language is often more than a two-letter code. The text language, regional formatting, pronunciation choices, script, and selected voice can change the reusable output independently.

Do not merge en-US and en-GB merely because both begin with en. That may be acceptable for a particular public text excerpt, but an audio artifact or a date-formatting answer needs a stated normalization policy.

Mixed-language conversations require another decision. If a response can contain names or quoted text in a second language, the key contract must represent the actual synthesis input and configuration rather than an approximate session label.

The multilingual evaluation guide distinguishes transcript quality from task outcomes. Cache tests need a related separation: equal text does not guarantee equal pronunciation, and equal pronunciation does not establish permission to reuse private content.

Tool compatibility includes the effective schema and behavior, not just the function name. A tool named lookup that changes its required arguments or result interpretation is a different dependency for cached plans and answers.

Use a reviewed tool-schema revision or content digest. If order has no semantic meaning, sort and deduplicate tool identifiers; if order affects a router or prompt, preserve it and document why.

Policy compatibility includes the policy version and the effective permission context used for the artifact. A new policy can invalidate prior outputs even when the user prompt and tool schema remain byte-for-byte identical.

Model compatibility depends on the artifact. A complete output cache generally needs the model revision and generation settings that you promise to preserve; a lower-level prefix cache follows its runtime's own supported compatibility rules.

Retrieval introduces source revisions. A stable document identifier does not identify a stable document body, so pair identity with a revision or a freshness rule enforced at read time.

Time-sensitive facts need a freshness decision beyond key equality. Reusing yesterday's answer under a perfectly designed key can still be wrong if the underlying fact has changed without an entry invalidation event.

The table compares explicit application policies, not vendor products. A check means the named policy enforces that criterion by design; a cross means it leaves that criterion to another mechanism.

Criterion Prompt text only Context key only Context key plus live gate
Distinguishes authenticated tenants ✗ ✓ ✓
Distinguishes declared language and tool revisions ✗ ✓ ✓
Rejects revoked permission at use time ✗ ✗ ✓
Establishes acoustic audibility ✗ ✗ ✗
Reuses across semantically equal tool sets Depends on text ✓ with normalization ✓ with normalization
Handles source freshness ✗ Only with a revision rule ✓ when gate checks source validity

A live gate has a cost, so keep its responsibility precise. It can check a current policy epoch or capability revision without recomputing the entire cached artifact, provided that revision is authoritative for the permission being checked.

Do not add fields simply because they are available. A random request identifier makes every key unique, while a session identifier prevents cross-session reuse even for approved public content.

Start conservative for sensitive artifacts, then remove dimensions only after proving equivalence with boundary fixtures. A cache hit-rate target does not establish that two contexts mean the same thing.

Build an owned context snapshot

The following reference function produces a key for a deliberately narrow artifact: reusable text derived from an exact prompt and an effective server context. It supports only nonempty strings and a set-like collection of tool revision strings.

It does not build prompts, authenticate callers, execute tools, or store cache entries. Those omissions keep the example inspectable, while its tests cover the compatibility guarantees it does claim.

Run the complete fence with Python 3.11 or later as python cache_key_reference.py. It uses only the standard library and prints one completion line after all assertions pass.

from dataclasses import dataclass, FrozenInstanceError
from hashlib import sha256
import json


def text(value):
    if type(value) is not str or not value:
        raise ValueError("expected a nonempty string")
    return value


@dataclass(frozen=True, slots=True)
class Context:
    tenant: str
    subject_scope: str
    language: str
    model_revision: str
    policy_revision: str
    source_revision: str
    tools: tuple[str, ...]

    @classmethod
    def capture(cls, raw):
        names = ("tenant", "subject_scope", "language", "model_revision",
                 "policy_revision", "source_revision", "tools")
        if type(raw) is not dict or set(raw) != set(names):
            raise ValueError("context fields differ from schema")
        tools = raw["tools"]
        if type(tools) not in (list, tuple):
            raise ValueError("tools must be a list or tuple")
        return cls(*(text(raw[name]) for name in names[:-1]),
                   tuple(sorted({text(tool) for tool in tools})))

    def __post_init__(self):
        for name in ("tenant", "subject_scope", "language", "model_revision",
                     "policy_revision", "source_revision"):
            text(getattr(self, name))
        if (type(self.tools) is not tuple or
                any(type(tool) is not str or not tool for tool in self.tools) or
                self.tools != tuple(sorted(set(self.tools)))):
            raise ValueError("tools must be a canonical immutable tuple")


def cache_key(context, prompt):
    if type(context) is not Context:
        raise ValueError("capture a Context first")
    payload = ["voice-text-v1", context.tenant, context.subject_scope,
               context.language, context.model_revision,
               context.policy_revision, context.source_revision,
               context.tools, text(prompt)]
    encoded = json.dumps(payload, ensure_ascii=False, allow_nan=False,
                         separators=(",", ":")).encode("utf-8")
    return "voice-text-v1:" + sha256(encoded).hexdigest()


def rejected(action):
    try:
        action()
    except (ValueError, TypeError, FrozenInstanceError, AttributeError):
        return
    raise AssertionError("invalid operation was accepted")


raw = dict(tenant="tenant-a", subject_scope="account-scope-a",
           language="en-US", model_revision="model-r1",
           policy_revision="policy-r1", source_revision="source-r1",
           tools=["lookup:r2", "confirm:r1"])
context = Context.capture(raw)
key = cache_key(context, "Read the approved public summary")
assert key == cache_key(Context.capture(dict(raw)),
                        "Read the approved public summary")

# Synthetic fixtures: every semantic field changes compatibility.
for name in ("tenant", "subject_scope", "language", "model_revision",
             "policy_revision", "source_revision"):
    changed = dict(raw)
    changed[name] += "-different"
    assert cache_key(Context.capture(changed),
                     "Read the approved public summary") != key
changed = dict(raw, tools=["lookup:r3", "confirm:r1"])
assert cache_key(Context.capture(changed),
                 "Read the approved public summary") != key
assert cache_key(context, "Read another summary") != key
same_set = dict(raw, tools=["confirm:r1", "lookup:r2", "lookup:r2"])
assert cache_key(Context.capture(same_set),
                 "Read the approved public summary") == key

# Ownership: caller aliases cannot change an already captured context.
raw["tenant"] = "tenant-b"
raw["tools"].append("delete:r1")
assert context.tenant == "tenant-a"
assert context.tools == ("confirm:r1", "lookup:r2")
assert cache_key(context, "Read the approved public summary") == key
rejected(lambda: setattr(context, "tenant", "tenant-b"))
rejected(lambda: context.tools.__setitem__(0, "delete:r1"))
rejected(lambda: Context.capture(dict(raw, extra="unknown")))
rejected(lambda: Context.capture(dict(raw, tenant="")))
rejected(lambda: Context.capture(dict(raw, tools="lookup:r2")))
rejected(lambda: Context.capture(dict(raw, tools=[True])))
rejected(lambda: Context("a", "b", "c", "d", "e", "f", ["g"]))
rejected(lambda: cache_key(context, ""))

# Delimiter ambiguity is avoided by serializing structured fields.
a = Context.capture(dict(raw, tenant="a|b", subject_scope="c"))
b = Context.capture(dict(raw, tenant="a", subject_scope="b|c"))
assert cache_key(a, "x") != cache_key(b, "x")
print("cache key reference fixtures passed")

The snapshot owns its values. Mutable caller dictionaries and tool lists are copied into immutable strings and a canonical tuple, and the frozen record rejects ordinary property rebinding.

Python's dataclass documentation explains the behavior of frozen fields. Freezing an outer record alone would not freeze a nested caller list, which is why the example converts the supported nested shape before retaining it.

This is an application correctness boundary, not protection against hostile code already running inside the Python process. Code that can bypass object semantics or modify the interpreter is outside the example's trust model.

The encoding uses an ordered array and explicit JSON separators. It avoids delimiter ambiguity without claiming to implement the cross-language canonicalization rules in RFC 8785.

If other languages must produce identical bytes, select a shared canonical format and publish conformance vectors. Python's JSON documentation also warns about resource consumption when decoding untrusted input, so bound payload size before parsing at a network boundary.

Test collisions and isolation

The most useful collision test changes one semantic dimension at a time. It asks whether an incompatible context produces a different key, rather than hoping a large random corpus happens to include the dangerous pair.

Start with tenant, subject scope, language, model revision, policy revision, source revision, tool revision, and exact prompt. For each field, write why a change can alter the reusable artifact and add a negative pair that demonstrates that difference.

Then test equivalence. Tool identifiers are a set in the reference contract, so reorderings and duplicates must produce the same key; if your application treats ordering as meaningful, that fixture should change with the contract.

Test ownership separately from equality. Capture a context, mutate the original dictionary and nested tool list, and confirm the captured key remains stable because the snapshot retained owned values.

Ordinary rebinding and item mutation need fixtures too. A frozen property with a mutable child can pass a superficial immutability review while still changing the meaning of an approved proposal after the key was created.

A cryptographic digest reduces accidental byte-level collision risk, but the more common application failure is semantic omission. Hashing an incomplete context perfectly still produces an unsafe cache.

Do not call a few fixtures proof that SHA-256 never collides. The tests establish the example's serialization and field coverage for declared cases; the hash function's security properties come from its specification and implementation.

Use synthetic values with clear relationships. A fixture named tenant-a and another named tenant-b teach the boundary without exposing real customer identifiers or giving an invented example the appearance of production evidence.

Synthetic context pairs show tenant and policy changes produce different keys while reordered equivalent tool sets keep the same key

Keep a test that demonstrates the old failure when correcting a real implementation. For example, a prompt-only builder should produce the same key for different tenants, while the corrected builder must separate that exact pair.

The voice agent regression testing guide explains why a replay corpus needs expected outcomes. For caching, an expected outcome includes whether reuse is allowed, whether the entry is fresh, and which live checks still apply.

Add integration fixtures around the storage adapter after the pure function passes. Test namespace selection, key encoding, accidental truncation, serialization round trips, and a missing dimension rejected before any lookup occurs.

A shared-cache test should use two authenticated test principals through the real gateway. Merely constructing two dictionaries inside one unit test does not prove that the gateway derives different authenticated contexts correctly.

Verify observability too. A debug endpoint that reveals a full key payload can undo careful privacy choices even when the storage namespace itself is sound.

Keep authorization outside cache history

A cached proposal is historical evidence. It says that a computation produced an output under a particular context, not that the current caller still has authority to perform the proposed action.

Separate read reuse from action admission. Before a business action, load current permission and task state, validate the exact proposed values, and confirm that the operation still belongs to the active conversation generation.

The workflow enforcement guide treats state transitions as controlled actions. The same principle prevents a cached answer from skipping a confirmation step or restoring a permission that was revoked after entry creation.

Consider a synthetic appointment proposal under controlled speculative tool use. The cache can return a description of a slot, but availability, caller authorization, and confirmation must be checked at the write boundary because each can change independently of the description.

An idempotency record has a different job. It can report a historical action result for an exact retry, while the current task state remains canceled or closed.

Do not let a retry reopen that terminal state. The shutdown semantics guide provides the related lifecycle boundary: completion evidence and present permission are separate facts with separate owners.

A cache lookup that awaits remote storage creates a race. Permission can change while the request waits, so checking authorization only before await leaves an obsolete approval in the continuation.

Perform the final check after the last asynchronous wait before the protected action. If another wait occurs between that check and local submission, the design needs another check or a stronger atomic admission boundary.

Generation checks come first when stale callbacks can affect current work. An old response must be rejected as obsolete before its age or error path can cancel a newer response that now owns the conversation.

This is the same ownership rule described in the interruption safety guide. Caching changes how quickly output arrives; it does not change which response is allowed to speak.

A clear barrier matters for cached audio. Once an interruption invalidates old playback, a new cache hit still needs to wait for the adapter's declared clear barrier and then recheck generation and age before ordered local submission.

Do not infer those guarantees from cache state. The cache should return a candidate artifact and provenance; the conversation controller decides whether the artifact can enter the current output path.

Keep that interface small. Returning a stored byte string plus artifact revision is easier to audit than returning an object with methods that silently schedule playback, execute tools, and refresh permissions.

Invalidate without hiding stale state

Time to live limits how long an entry remains eligible for storage reuse. It does not express every business invalidation rule, and it does not guarantee that an already retrieved value remains valid until it is used.

The Redis EXPIRE reference defines expiration for a key. That storage mechanism is useful, but the application still needs to decide which source or policy change makes a previously valid artifact obsolete.

Versioned namespaces make invalidation explicit. A policy revision changes the key for new reads, while old entries can expire under a bounded retention policy without a global delete operation on the request path.

This trades immediate physical deletion for logical separation. Sensitive deletion requirements can still require removal of stored values and backups under the applicable data policy; a namespace change alone does not erase bytes.

Distinguish missing, expired, revoked, incompatible, and unavailable outcomes when recovery differs. A missing public greeting may be recomputed; a revoked permission must stop the action; unavailable authorization evidence must follow the configured failure policy.

A silent empty result hides those differences. It encourages callers to treat every failure as a cache miss and can produce a fallback computation under the same invalid context.

Use explicit result labels at the adapter boundary. The exact type can remain small, but the controller should be able to select a safe recovery action without parsing a log message or guessing why a value is absent.

Policy changes during a rolling deployment need a compatibility rule. If older instances still accept the previous revision, switching only the writer does not ensure that all readers reject obsolete entries.

Roll out validation before issuing the new revision. Then observe which readers still request the old namespace and retire it when the deployment and rollback window permit.

The cold-start capacity guide explains another operational consequence: broad invalidation can turn a warm fleet into a workload with expensive misses, even when every process is already running.

Plan miss capacity before a large namespace change. An invalidation event should not be the first time the system discovers that its dependency cannot sustain uncached demand.

Diagnose a wrong reuse

Begin with the artifact that was reused and the expected compatibility boundary. Avoid starting from the hit-rate dashboard, because a high hit rate cannot distinguish correct sharing from cross-context mistakes.

Compare the authenticated context used to create the entry with the context used to read it. Retain safe revision identifiers and reason codes, while keeping raw prompts, credentials, account details, and secret salts out of routine telemetry.

If a semantic dimension differs but the key matches, the key contract or normalization is incomplete. If all dimensions match but the stored value differs from its declared artifact, inspect serialization, namespace selection, storage corruption handling, and adapter ownership.

If the entry was valid at lookup but invalid at use, inspect the final admission gate. A log that records only lookup time cannot explain a permission change or interruption that happened during the wait before action execution.

A diagnostic decision tree separates context mismatch, stale policy, and stored-value inspection

Record the difference between a cache hit and a usable hit. A usable hit passed compatibility, freshness, current authorization, and generation admission for its declared operation.

Measure rejected hits by reason. This makes an unexpectedly high revoked-policy count visible without turning every rejection into an application error or retrying the same forbidden action.

Avoid high-cardinality labels for tenants and prompts. Keep aggregate counters in metrics and place narrowly retained, access-controlled correlation evidence in the tracing system when investigation requires it.

The voice outcome metrics guide explains why denominators matter. For cache reports, name whether the denominator is lookups, hits, admitted hits, completed responses, or verified successful tasks.

A lower latency among admitted hits can coexist with more rejected requests. Report rejection and fallback counts alongside latency so a selective cache experiment cannot appear successful by excluding its difficult cases.

For a privacy investigation, stop broad reuse before optimizing diagnosis. Preserve the minimum evidence needed to determine scope, follow the incident process, and avoid copying affected payloads into a new analytics dataset.

Operate a bounded cache experiment

Run the first experiment with a narrowly approved artifact. A fixed public greeting or public documentation fragment gives you a useful compatibility surface without combining private retrieval and business writes in one change.

Establish uncached behavior first. Measure the same output boundary, workload mix, language distribution, dependency version, and warm-state definition that you will use after enabling the cache.

The latency measurement guide separates model and end-to-end boundaries. A faster storage lookup does not establish faster caller-perceived response if synthesis or playback still dominates the path.

Change one reuse policy at a time. Compare prompt-only matching with the proposed context key only in an isolated test environment, and use negative fixtures to demonstrate why the broader policy is unsafe before exposing it to real traffic.

Keep expiry and revocation scenarios in the experiment. A cache that behaves well only when permissions never change has not exercised the boundary that makes it safe to deploy.

A conceptual trade-off compares wider sharing with more reuse and narrower sharing with stronger context separation

The trade-off is workload-specific. Narrow sharing can reduce reuse, but no general percentage describes that cost without a measured workload and an explicit boundary for the saved computation.

Include cold misses after deployment, policy rotation, source updates, and cache loss. The bursty arrival testing guide gives a useful way to keep those miss storms separate from normal warm traffic.

Admission control remains necessary during a miss storm. The bounded admission guide explains how queue age and concurrency limits keep the fallback path from becoming an unbounded backlog.

Synthetic scenarios show public matching context may reuse, a different tenant misses, revoked authority denies, and an interrupted response discards

A useful experiment grid separates identity changes from content changes. Hold the prompt and source revision constant while changing the tenant, then hold tenant and prompt constant while changing the subject scope; these pairs reveal whether the gateway is collapsing authenticated identities before the key builder even receives them.

For language fixtures, use an input whose interpretation depends on the declared locale. A spoken date or an address abbreviation is more informative than an arbitrary greeting because the expected artifact difference has a reason that a reviewer can inspect.

For tools, make one schema revision require a field that the previous revision did not require. The expected miss then follows from a concrete contract change, and an accidental hit would be visible in the proposed arguments rather than only in a digest comparison.

For policy fixtures, revoke one capability after the entry is written and before it is read. Repeat with revocation during the storage wait so that the adapter test exercises the final check rather than merely proving that an initial permission lookup works.

Include rollback in the grid. If an older application version returns after a failed deployment, it should either understand the active key schema and policy epoch or reject the incompatible context; it should not silently select a default namespace that combines entries from different contracts.

Record the storage operation attempted in each fixture. A rejected context should produce no read in a protected namespace, while a valid miss may produce a read followed by bounded recomputation, and a revoked live gate should produce no protected write even if the cache returned a candidate.

Keep the report compact enough to review beside the implementation. For each case, retain the synthetic input difference, expected disposition, actual disposition, and named boundary observed; avoid storing a full fabricated conversation when one changed field explains the invariant.

A performance result should use that same disposition vocabulary. Report time spent in lookup separately from time spent validating authority, recomputing a miss, waiting for capacity, and reaching the chosen delivery boundary, because a single combined latency number hides which component a cache policy improved.

Before release, ask a reviewer to explain each scenario without reading the implementation. If the table, diagram, code, and prose disagree about whether reuse is allowed, repair the contract before tuning eviction or storage capacity.

Keep the experiment report honest. Passing the reference fixtures establishes the small key function's declared behavior; it does not establish provider isolation, acoustic performance, production savings, or a complete privacy audit.


Frequently asked questions

What is a voice agent cache key?

A cache key identifies an artifact under a declared compatibility contract. For a voice application, that contract can include authenticated tenant, permission scope, language, source revision, tool revision, and model configuration, depending on what is stored.

Is hashing the prompt enough?

Prompt hashing is enough only when the reusable artifact depends solely on that exact prompt within an already enforced scope. Private retrieval, tool permissions, language settings, or policy changes usually add other inputs, so a prompt-only key needs a much narrower justification.

Should tenant identifiers come from request bodies?

Derive them from authenticated server context. If the client supplies a requested tenant or account, validate that selection against the principal's current access before creating the context snapshot or reading a scoped cache entry.

Can two users in one tenant share entries?

They can share only artifacts whose content and permissions are equivalent for both users. Organization membership alone does not prove access to every account or document, so the effective subject scope can require a narrower boundary than tenant identity.

Does a runtime salt replace application authorization?

No. A salt can scope a particular runtime's cache reuse, while application authorization decides whether the current principal may read information or execute an action. Keep both responsibilities explicit and verify provider-specific behavior against its current public documentation.

Why include language when text matches?

Language configuration can affect pronunciation, number formatting, regional dates, and voice selection. If the artifact is language-independent, document that narrower contract and test it; do not infer equivalence from a shared language prefix alone.

Do tool names belong in the key?

Include the effective tool contract when it can affect the stored artifact. A schema revision or content digest is usually more informative than a name because arguments and result meaning can change while the name remains stable.

Why sort the tool list in the example?

The reference contract defines tools as a set, so order and duplicate entries have no meaning. Sorting and deduplication make equivalent sets produce the same bytes; applications with order-sensitive prompts must preserve and test that order instead.

Does a frozen dataclass freeze nested values?

It prevents ordinary field assignment, but it does not recursively freeze caller-owned mutable children. The example captures strings and converts the supported tool list to an immutable tuple before retaining it, then tests alias mutation and rebinding separately.

Is the example a complete canonical JSON implementation?

It is a deterministic encoding for its declared Python value shape. Cross-language byte equality requires a shared canonicalization specification and conformance vectors; the example does not claim RFC 8785 compliance.

Does SHA-256 make an incomplete key safe?

No. A digest preserves the distinctions represented by its input bytes; it cannot recover a tenant, policy revision, or source version that the application omitted. Test semantic field coverage before debating hash performance.

Should authorization tokens appear in keys?

Avoid raw credentials in cache keys, logs, and metrics. Use server-managed scope identifiers and revisions, then validate current permission at the protected action boundary without exposing the credential material used to establish that permission.

Is a time to live sufficient invalidation?

A time to live bounds storage reuse under a time policy. Revocation, source deletion, and changed business state can require earlier rejection, so retain an authoritative revision or live validation rule when those events matter.

What happens when policy changes during lookup?

The post-wait admission gate should see the new policy state and reject the candidate when it is no longer valid. A check performed only before the remote lookup cannot protect an action taken after permission changes.

Can an exact retry restore canceled speech?

An exact retry can report historical computation evidence, but it must not restore playback ownership after cancellation. Current generation and terminal task state remain authoritative even when the cached artifact is byte-for-byte identical to a previous result.

Should cache misses automatically retry?

A genuine miss can trigger bounded recomputation when the current context remains valid. Revoked, expired, overloaded, and unavailable outcomes need separate recovery decisions, because converting them all to misses can repeat forbidden or stale work.

Which cache metric should we optimize first?

Begin with correctness failures and usable-hit accounting. After compatibility and admission tests pass, measure saved computation and latency at a named boundary alongside rejected hits, fallback failures, and the total offered workload.

Can cache keys contain personal identifiers if hashed?

Hashing does not reliably conceal an identifier drawn from a guessable domain. Use opaque server-managed context identifiers, minimize exposed metadata, and apply the project's retention and access policy to diagnostic evidence as well as cached values.

How should a team test cache isolation?

Combine pure-function field-pair tests with two authenticated test principals through the real gateway and storage adapter. The former checks key construction; the latter checks that authentication, namespace selection, and provider behavior preserve the intended boundary.

Does this guide describe 60db Agent internals?

No. The guide provides generic reference contracts and synthetic fixtures for evaluating a voice application. Any product-specific guarantee should come from the product's documented public interface and evidence collected at the boundary your application uses.

Put the contract into review

Write the artifact sentence, list its semantic dimensions, and identify each field's authoritative owner. Keep the current authorization and generation gate separate from the historical computation represented by the cache entry.

Run the complete reference fence, then add your application's storage and gateway tests. Include one incompatible-context pair for every key dimension, one equivalent-context pair for every normalization rule, and one mutation fixture for every retained mutable input shape.

Review the final diagram against the action boundary. A cache hit should lead to a candidate artifact, followed by current checks, with rejected and stale paths ending in explicit outcomes rather than drifting back toward an action.

For an application evaluated with 60db Agent, use the same public-boundary discipline: measure what the interface demonstrates, keep private payloads out of reports, and establish permission where the action occurs. Start with one approved artifact and expand reuse only when the next boundary has its own evidence.