Files
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

288 lines
14 KiB
Markdown

NIP-AM
======
Agent Turn Metrics
------------------
`draft` `optional` `relay`
This NIP defines a durable, encrypted event kind for recording per-turn token
usage and estimated cost of AI agent sessions. An agent publishes one
`kind:44200` event per completed turn, NIP-44 encrypted to its owner, so the
owner can account for token usage across agents and harnesses without the
relay — or any third party — learning what the agent did or what it cost.
## Motivation
AI agent harnesses consume model tokens on every turn. Owners running fleets
of agents need durable, harness-independent usage accounting — the equivalent
of a metered bill — for cost attribution, budgeting, and capacity planning.
[NIP-AO](NIP-AO.md) (kind 24200) already streams encrypted session telemetry
between agent and owner, but it is deliberately ephemeral: relays MUST NOT
persist it, so it cannot answer "how many tokens did my agents use last
week?". Transcript-grade durable telemetry is explicitly out of scope — the
persistence-averse reasoning behind NIP-AO's ephemerality contract applies to
conversation content, not to a small usage record. Kind 44200 stores only the
metric: token counts, an estimated cost, and correlation identifiers, all
encrypted to the owner.
## Definitions
- **Agent**: an AI process with its own Nostr keypair, executing sessions on
behalf of an owner.
- **Owner**: the human (or system) whose pubkey the agent was provisioned under.
- **Turn**: one prompt→response cycle of an agent session, as bounded by the
harness (e.g. one ACP `session/prompt` round trip).
- **Turn metric**: a single kind 44200 event recording the usage of one turn.
## Event
`kind:44200` is a regular event by Buzz convention (alongside 44100/44101):
stored,
append-only, never replaced. Each completed turn produces exactly one event.
```json
{
"kind": 44200,
"pubkey": "<agent_pubkey>",
"created_at": <unix_timestamp>,
"content": "<NIP-44 v2 ciphertext>",
"tags": [
["p", "<owner_pubkey>"],
["agent", "<agent_pubkey>"]
],
"sig": "..."
}
```
Events MUST have exactly one `p` tag (the owner) and exactly one `agent` tag
(equal to `pubkey`). The tag layout deliberately mirrors NIP-AO telemetry
frames so existing owner-scoped tooling applies unchanged.
No channel (`h`) tag is used. The channel a turn served is private usage
metadata and lives inside the encrypted payload; keeping it out of the tags
avoids leaking per-channel activity rates to the relay operator and keeps the
event community-global (owner-scoped) rather than channel-scoped.
## Encryption
`content` MUST be encrypted with NIP-44 v2 using `(agent_privkey,
owner_pubkey)` — identical to NIP-AO telemetry. Plaintext SHOULD be zeroized
after encrypt/decrypt. Decrypted payload MUST NOT exceed 65,535 bytes
(payloads are typically well under 1 KB).
## Decrypted Payload
The `content` field decrypts to a UTF-8 JSON object:
```jsonc
{
"harness": "goose", // REQUIRED: harness identifier
"model": "claude-sonnet-4-5", // model id, or null if unknown
"channelId": "<channel_uuid>" | null,
"sessionId": "<session_id>" | null, // REQUIRED when "cumulative" is present
"turnId": "<turn_id>" | null,
"turnSeq": 17 | null, // REQUIRED when "cumulative" is present
"timestamp": "2026-07-01T20:11:03.213Z", // REQUIRED: RFC 3339, end of turn
// Usage for THIS turn (computed delta). Fields are null when the harness
// does not report them — a null MUST NOT be recorded or summed as zero.
// Exception: cache fields (cacheReadTokens, cacheWriteTokens) MUST be
// omitted rather than null when unavailable — see "Numeric validity" below.
"turn": {
"inputTokens": 1234 | null,
"outputTokens": 567 | null,
"totalTokens": 1801 | null,
"costUsd": 0.0123 | null // estimated
},
// Session-cumulative usage as reported at the end of this turn.
"cumulative": {
"inputTokens": 45210 | null,
"outputTokens": 9876 | null,
"totalTokens": 55086 | null,
"costUsd": 0.41 | null // estimated
},
// false when the publisher could not observe the previous turn's
// cumulative baseline (e.g. harness restart mid-session), making the
// "turn" object unreliable for this event.
"deltaReliable": true,
// Billing identity, present only when the publisher can prove applicability
// from the actual endpoint (official provider API) and the actually-requested
// model for the usage represented. Omit this field when applicability cannot
// be proven — it is never inferred from the configured/session "model" field.
// Consumers MUST treat omission as "price unknown"; they MUST NOT infer a
// price from the session "model" field. pricingIdentity is OPTIONAL but NOT
// nullable: when present, "authority" and "model" MUST be non-null strings;
// "cacheClass" is omitted (not null) when it is not applicable.
"pricingIdentity": { // OPTIONAL; omit entirely if unproven
"authority": "api.anthropic.com", // billing authority (not transport provider)
"model": "claude-sonnet-4-5", // actually-requested billable model id
"cacheClass": "ephemeral" // cache-write class; omit when not applicable
},
"stopReason": "end_turn" // optional
}
```
`harness` and `timestamp` are REQUIRED. All other fields are OPTIONAL or
nullable, except as constrained below: `pricingIdentity` is optional but not
nullable (omit it entirely rather than set it to null). Consumers MUST ignore
unknown fields (forward compatibility).
### Ordering and delta recomputation
When a `cumulative` object is present, `sessionId` and `turnSeq` are
REQUIRED. `turnSeq` is a per-session monotonically increasing integer
starting at any value, incremented by the publisher on every published turn
metric for that session; a publisher restart that loses the counter MUST
start a new `sessionId` rather than reuse the old one with a reset `turnSeq`.
Cumulative values form a series only *within* one `sessionId`, ordered by
`turnSeq` — consumers MUST NOT diff cumulative values across different
`sessionId`s, and MUST NOT rely on `created_at` (seconds precision, ambiguous
for same-second turns) for ordering within a session.
If a consumer recomputing deltas observes a cumulative counter that decreases
between consecutive `turnSeq` values (counter reset, harness bug), it MUST
treat the affected turn's usage as unknown (null), not as negative usage.
Publishers likewise MUST NOT emit negative values in `turn`; when the
computed delta would be negative or the previous baseline is unknown, the
publisher sets the affected `turn` counters to null and `deltaReliable:
false`.
Where the harness reports only cumulative counters, the publisher computes
`turn` as the difference between consecutive cumulative snapshots within one
session. Consumers doing exact accounting SHOULD prefer recomputing deltas
from consecutive `cumulative` values and treat `turn` as a convenience.
### Numeric validity and token semantics
All token counts MUST be non-negative integers. `costUsd` MUST be a finite,
non-negative number. `totalTokens` is the harness- or provider-reported
total when available; publishers MUST NOT derive it by summing `inputTokens`
and `outputTokens` (providers may count categories a simple sum misses) —
when no total is reported, `totalTokens` is null. `inputTokens` is the
inclusive input-side total: where the provider reports cache reads/writes
separately (e.g. Anthropic `cache_read_input_tokens` /
`cache_creation_input_tokens`), the publisher folds them into `inputTokens`.
Where the provider exposes a cache component, publishers SHOULD report it in
the optional `cacheReadTokens` / `cacheWriteTokens` fields inside `turn` and
`cumulative`; these are informational subsets of `inputTokens`, not additions
to it. Publishers MUST preserve an explicit zero when the provider reports
zero and MUST omit the field (never null or fabricated zero) when that
component is unavailable to the publisher — including when the provider
supports the component but the harness does not surface it. Treating an
unreported category as zero is incorrect. Note: the payload-wide null
guidance above does not apply to these cache fields; omission is the only
valid representation for an unavailable cache component.
`costUsd` values are estimates (provider list prices at publish time, or a
harness-reported estimate). They are advisory, not billing records.
`pricingIdentity`, when present, identifies the billing authority and
actually-requested model for the usage represented by this event. `authority`
is a registered billing-namespace identifier: exact lowercase hostname, no
scheme, no path, no trailing slash (registered values: `api.anthropic.com`,
`api.openai.com`, `openrouter.ai`; the set extends only by amendment to this
NIP). Pricing lookup is an exact string match on `(authority, model)` — any
deviation loses the price. It is a billing namespace, distinct from the
runtime transport provider. `model` is the
billable model identifier as resolved at the point the request was made, not
the configured/session model. `cacheClass`
is the cache-write class when applicable (e.g. `ephemeral`). Publishers MUST
omit `pricingIdentity` when any of the following apply: the request was routed
through a custom or overridden base URL, a gateway (unless the gateway is the
named billing authority), or an unresolved alias; the billed model identity
cannot be confirmed — for direct connections to an official allowlisted
endpoint, proof is the actually-requested resolved model; for all other routes,
the response MUST supply authoritative billing identity; or the usage
represented within this turn contains contributions from more than one billing
identity (including a mix of identity-bearing and unresolved contributions).
The existing `model` field
retains its non-billing semantics (configured/session model) and is never
overloaded by `pricingIdentity`. Consumers MUST treat omission of
`pricingIdentity` as "price unknown". Consumers MAY recompute cost estimates
using the billing identity and a pricing manifest; they MUST retain
the provenance of any cost value (e.g. `manifest-estimated`, `wire-reported`).
Consumers MUST NOT merge manifest-estimated and wire-reported costs into an
unlabeled total.
`stopReason`, when present, MUST be one of `end_turn`, `max_tokens`,
`cancelled`, `error`, `unknown`. Consumers MUST treat unrecognized
`stopReason` values as `unknown`; the token counts remain valid.
## Publisher Behavior
- Publish exactly one event per completed turn, at turn completion, including
turns that end in cancellation or error when usage was observed.
- Do NOT publish an event for a turn with no observed usage (all counters
unknown); an all-null metric carries no information.
- `created_at` SHOULD equal the payload `timestamp` truncated to seconds.
## Relay Behavior
On receiving a kind 44200 event, a relay MUST:
1. Validate the event signature per NIP-01.
2. Verify `event.pubkey` equals the `agent` tag and that
`is_agent_owner(agent, owner)` holds for the `p` tag via authenticated
ownership lookup. Tag matching alone is insufficient.
3. Store the event durably, scoped to the owner (community-global; no channel
scope).
4. NOT index the event in any full-text search (the ciphertext is not
searchable and must not enter search indexes).
Reads MUST be gated: only an authenticated ([NIP-42](42.md)) reader whose
pubkey equals the `#p` tag value may receive the event. This gate applies to
**every** read path, including explicit `ids` filters — knowing an event id
MUST NOT grant access. (Some p-gated kinds exempt id-addressed lookups on the
theory that knowing the id implies authorization; kind 44200 events are
long-lived and their cleartext envelope leaks turn activity, so no such
exemption is permitted.) Unauthenticated publish or subscribe attempts MUST be
rejected with `AUTH required`; authenticated attempts from a pubkey that is not
the event owner MUST be rejected with `restricted:`.
Relays SHOULD rate-limit kind 44200 to a rate consistent with real turn
frequency (RECOMMENDED: 60 events/minute per agent pubkey).
## Client Behavior
Owners recover usage history with:
```json
{"kinds": [44200], "#p": ["<own_pubkey>"], "since": <window_start>}
```
On receiving an event, a client MUST verify the signature, decrypt with its
own secret key and `event.pubkey`, and ignore events that fail to decrypt or
parse. Clients SHOULD deduplicate by event id. For within-session ordering,
clients MUST use `(sessionId, turnSeq)` from the decrypted payload as
described above; `created_at` is suitable only for coarse time-window
queries.
## Relationship to Other NIPs
- [NIP-AO](NIP-AO.md): same agent↔owner encryption and tag scoping, but
ephemeral and transcript-grade. NIP-AM events MUST NOT carry conversation
content, tool calls, or protocol frames — usage numbers and identifiers only.
- [NIP-09](09.md): the authoring agent (or its owner via relay policy) may
request deletion; relays apply standard deletion semantics.
- [NIP-40](40.md): publishers MAY set `expiration` to bound retention.
## Security Considerations
**Metadata leakage.** `p`, `agent`, and `created_at` are cleartext: a relay
operator learns that agent X completed turns for owner Y at some rate. Turn
rate is already observable from the agent's channel messages; the token
counts, cost, model, and channel remain encrypted.
**No forward secrecy.** NIP-44 does not provide forward secrecy; compromise
of the agent's private key allows decryption of captured ciphertexts.
**Integrity of accounting.** Metrics are self-reported by the agent process.
A compromised agent can under- or over-report. Owners requiring stronger
guarantees must reconcile against provider-side billing.