Skip to content

feat(nip-ad): agent disposition ledger (kind:44300) - #6148

Draft
MajorTal wants to merge 1 commit into
block:mainfrom
MajorTal:claude/agent-disposition-ledger
Draft

feat(nip-ad): agent disposition ledger (kind:44300)#6148
MajorTal wants to merge 1 commit into
block:mainfrom
MajorTal:claude/agent-disposition-ledger

Conversation

@MajorTal

Copy link
Copy Markdown
Contributor

Implements #3091.

A signed, durable, plaintext record of how an agent resolved each request directed at it, so any authorized reader of a channel can verify that a request was answered — and read why it was refused — without parsing chat prose or trusting unverifiable claims.

The model

An obligation is one marked request naming exactly one target agent that is also p-mentioned. That is the unit of accounting, because a disposition is only meaningful if "which obligation does this discharge, and who was obliged?" has exactly one answer. Requests naming several agents are reported as unsupported rather than given a state either agent could discharge.

A disposition binds only when its e/h/p tags match the obligation and its signer is the target agent. The relay validates structure; binding is consumer-side, so every consumer shares one verifier. Without it, any channel member — including a merely p-mentioned human — could close an agent's obligation with a signed event.

Four states. completed and refused are terminal and absorbing: a later weaker observation is recorded as a warning, never a reopening, so "terminal" means terminal. responded and errored leave the obligation open.

The harness cannot emit completed. It observes that a turn ended, not that the work was done — a clean EndTurn covers asking a clarifying question, or answering one message of a batch, just as readily as finishing the job. TurnOutcome has three variants, so the type system enforces this rather than discipline. completed comes only from the target agent's own signed assertion via buzz dispositions emit, which refuses a signer that is not the obligation's target.

Malformed and unsupported requests are reported separately from unanswered ones: filing a client bug as an agent's gap manufactures a gap no agent can ever clear. Coverage is two-sided — a clean channel claim needs both the request set and the disposition histories paginated to exhaustion, because missing one disposition page can hide a refused that turns "settled" into "disputed".

One validity boundary

validate_disposition_event in buzz-core enforces the complete NIP-AD envelope and content contract — kind, exact tag cardinality, canonical hex, content/tag agreement, required reason — and relay ingest calls it rather than keeping a parallel implementation. Two implementations of one contract drift; one implementation with two callers cannot.

How it is verified

  • The lifecycle is stated declaratively in scripts/gen-nip-ad-corpus.mjs, which generates both the 341-case exhaustive corpus and the NIP's normative transition table. CI fails on drift, so the spec table cannot contradict working code.
  • Both the Rust verifier and the TypeScript mirror run both corpora, so a cross-language divergence is a test failure rather than a latent inconsistency.
  • An adapter-conformance test compares the desktop's per-request status field by field against a shared-verifier oracle, with deliberately asymmetric fixtures (a symmetric fixture cannot detect a swap in the dimension under test).
  • A live suite drives signed requests through a real relay over HTTP and WebSocket/NIP-42, including a joined path that runs the actual CLI binary end to end, and a cross-principal spoof that must change nothing.

Deployment dependency

v1 is relay-first. Operators must upgrade the relay to accept and enumerate kind 44300 before enabling any emitter. Emitters do not fall back to kind 9 — they fail to publish against an un-upgraded relay, so the gap is visible rather than silently masked. Kinds are additive, so disabling emission reverts to the status quo without data loss.

Known limitations (v1, deliberate)

  • Terminal claims cannot be corrected. Absorption makes completed/refused irrevocable. A correction relation is a real protocol extension with its own authority and ordering questions; v1 states the limitation, tells agents terminal claims are final, and defers the design rather than inventing one.
  • Multi-agent requests are unsupported, not mis-assigned — reported as such, never counted against an agent.
  • No dual-write/union-read migration from historical kind 9 dispositions; that history is simply not shown.

Testing

  • just ci green (3,545 Rust, 5,003 desktop, 1,465 mobile)
  • Full Playwright smoke project green on CI across all four shards
  • Live relay E2E against a throwaway Postgres database

Reviewed across five external design rounds; the per-round narrative is in the NIP and the design record. Naming is mechanical to change if maintainers prefer a different kind number or tag names.

Screenshots of the desktop surface will follow in a comment.

🤖 Generated with Claude Code

@MajorTal
MajorTal requested a review from a team as a code owner August 17, 2026 18:19
@MajorTal
MajorTal force-pushed the claude/agent-disposition-ledger branch from 782e5af to 4560f59 Compare August 17, 2026 18:41
@MajorTal
MajorTal marked this pull request as draft August 19, 2026 07:11
A signed, durable, plaintext record of how an agent resolved each request
directed at it, so any authorized reader of a channel can verify that a
request was answered and read why it was refused — without parsing chat prose
or trusting unverifiable claims.

Implements block#3091.

An **obligation** is one marked request naming exactly one target agent that
is also `p`-mentioned. That is the unit of accounting, because a disposition
is only meaningful if "which obligation does this discharge, and who was
obliged?" has exactly one answer. Requests naming several agents are reported
as unsupported rather than given a state either agent could discharge.

A disposition **binds** only when its `e`/`h`/`p` tags match the obligation
and its signer is the target agent. The relay validates structure; binding is
consumer-side, so every consumer shares one verifier. Without it, any channel
member — including a merely `p`-mentioned human — could close an agent's
obligation with a signed event.

Four states. `completed` and `refused` are **terminal and absorbing**: a later
weaker observation is recorded as a warning, never a reopening, so "terminal"
means terminal. `responded` and `errored` leave the obligation open.

**The harness cannot emit `completed`.** It observes that a turn ended, not
that the work was done — a clean `EndTurn` covers asking a clarifying
question or answering one message of a batch just as readily as finishing the
job. `TurnOutcome` has three variants, so the type system enforces this rather
than discipline. `completed` comes only from the target agent's own signed
assertion via `buzz dispositions emit`, which refuses a signer that is not the
obligation's target.

Malformed and unsupported requests are reported separately from unanswered
ones: filing a client bug as an agent's gap manufactures a gap no agent can
ever clear. Coverage is two-sided — a clean channel claim needs both the
request set and the disposition histories paginated to exhaustion, because
missing one disposition page can hide a `refused` that turns "settled" into
"disputed".

`validate_disposition_event` in `buzz-core` enforces the complete NIP-AD
envelope and content contract — kind, exact tag cardinality, canonical hex,
content/tag agreement, required `reason` — and **relay ingest calls it**
rather than keeping a parallel implementation. Two implementations of one
contract drift; one implementation with two callers cannot.

The lifecycle is stated declaratively in `scripts/gen-nip-ad-corpus.mjs`,
which generates both the 341-case exhaustive corpus and the NIP's normative
transition table; CI fails on drift. Both the Rust verifier and the TypeScript
mirror run both corpora, so a cross-language divergence is a test failure. An
adapter-conformance test compares the desktop's per-request status field by
field against a shared-verifier oracle. A live suite drives signed requests
through a real relay over HTTP and WebSocket/NIP-42, including a joined path
that runs the actual CLI binary end to end and a cross-principal spoof that
must change nothing.

Squashed from seven iterations across five external design reviews; the
per-round narrative and the surviving known limitations (terminal claims are
irrevocable in v1; multi-agent requests unsupported; no dual-write migration
from kind:9) are recorded in the NIP and the design record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Tal Weiss <major.tal@gmail.com>
@MajorTal
MajorTal force-pushed the claude/agent-disposition-ledger branch from 4560f59 to 1d0c4f1 Compare August 20, 2026 06:40
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.

1 participant