Skip to content

feat: canonical identity function for agent invocations - #1493

Open
lambdabaa wants to merge 1 commit into
akashgit:mainfrom
lambdabaa:identity-function
Open

feat: canonical identity function for agent invocations#1493
lambdabaa wants to merge 1 commit into
akashgit:mainfrom
lambdabaa:identity-function

Conversation

@lambdabaa

Copy link
Copy Markdown
Collaborator

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)

What Becomes Kept?
Project path (literal + resolved) {project} subpaths survive
Home directory {home} subpaths survive
Temp root {tmp} subpaths survive
UUIDs (dashed or 32-hex) {uuid}
ISO-8601 datetimes {timestamp} date-only strings kept (a date is usually content)
13-digit epoch-millis {timestamp} 10-digit numbers kept (can be legitimate content)
Whitespace runs single space

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 attempt is a digest component, and TestReloopRegression::test_rejected_retry_must_be_cache_miss encodes 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

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shared identity function for agent invocations (prompt normalization + content hashing)

1 participant