feat: canonical identity function for agent invocations - #1493
Open
lambdabaa wants to merge 1 commit into
Open
Conversation
…git#1485) New factory/identity.py with normalize_prompt() and invocation_identity(): shared infrastructure for node-level caching, record-and-replay testing, and artifact provenance. Consumed by nothing yet, by design. normalize_prompt() canonicalizes prompt/task text per a documented table (module docstring): project/home/temp roots become placeholders, UUIDs, ISO-8601 datetimes, and 13-digit epoch-millis are templated, whitespace runs collapse. Deliberately conservative: dates, small numbers, unknown paths, and all semantic content are kept. invocation_identity() digests (role, model, normalized prompt, normalized task, attempt, sorted input artifact hashes) as sha256 hex. Attempt number is load-bearing: a RELOOP retry runs with byte-identical inputs, so an identity that excludes the attempt would make an identity-keyed cache replay the just-rejected output forever. Co-Authored-By: Claude Code <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1485.
What this does
Adds
factory/identity.py— the canonical identity function for agent invocations — with no consumers yet, exactly as the issue specifies. It is infrastructure for three planned subsystems that all need to answer "is this agent invocation the same as that one?": node-level caching (the Ledger proposal), record-and-replay integration testing, and artifact provenance.Two functions:
normalize_prompt(prompt, *, project_path=None)— canonicalizes prompt/task text so machine- and run-volatile noise stops changing hashes. The full rule set lives in one table in the module docstring, so a normalization change is reviewable the same way a prompt-template change is (and visible as the cache-invalidation event it is).invocation_identity(request, *, attempt=0, input_artifact_hashes=None)— sha256 hex over(role, model, normalized prompt, normalized task, attempt, sorted input artifact hashes).The normalization table (short version)
{project}{home}{tmp}{uuid}{timestamp}{timestamp}Everything else — numbers, unknown paths, file contents, instructions — is kept. The bias is deliberately toward keeping: a cache that misses occasionally is an annoyance; a cache that hits when it shouldn't hides prompt regressions.
One consumer contract worth flagging: normalization only templates roots it knows about. If a consumer's prompts embed a per-run workspace (a tmp scratch dir, a worktree), it should pass that root as
project_path, and then the run-scoped root is templated while semantic subpaths ({project}/inputs/x.json) survive. This is documented in the module docstring and pinned by a test.The RELOOP trap
The subtle requirement from the issue is explicitly handled and explicitly tested: a node rejected and retried via a RELOOP verdict runs with byte-identical inputs. If the identity excluded the attempt number, an identity-keyed cache would replay the just-rejected output, the gate would reject it again, and the workflow would loop forever without consuming budget. So
attemptis a digest component, andTestReloopRegression::test_rejected_retry_must_be_cache_missencodes the failure mode as a simulation: first attempt cached, retry key must miss, second retry must miss again.Scope
No storage layer, no cassette format, no caching or replay machinery — the identity function is backend-agnostic so those decisions stay independent. It is project-agnostic by design (identical requests against different projects produce the same digest); consumers that need per-project separation namespace keys themselves.
Testing
30 tests in
tests/test_identity.py: one per normalization rule (including the kept-content cases: date-only strings, small numbers, unknown absolute paths), the six properties from the issue (path-independence across machines and home roots, semantic sensitivity for prompt and task, attempt sensitivity, model sensitivity, artifact-hash order-insensitivity and content-sensitivity, determinism), invariants worth pinning (cwd and session fields excluded from identity), the RELOOP regression, and the volatility-doesn't-break-hits flip side. Ruff and mypy clean.🤖 Generated with Claude Code