Experience receipts API

Availability: 0.7.0. Check /health on the server you use; a running 0.6.0 server does not expose these routes.

Use experience receipts to preserve what an agent attempted, which execution context it declared, who reported the result and what consumption remains unknown. Registrations and observations survive restart. An observation is an authenticated assertion: storing succeeded does not verify an external effect, qualify a procedure, dispatch a tool or update model weights.

The API uses the same durable learning store as procedural learning. Procedure qualification retains its existing policy and evaluator authority. Models, embeddings, execution and verification of external effects remain external services.

Routes and authority#

Every request uses Authorization: Bearer <credential>, contract_version: 1 and an exact resource scope. Tenant and actor are derived from the live credential. Request fields such as actor, tenant or credential_id are not accepted. Responses use Cache-Control: no-store. JSON bodies are limited to 65,536 bytes, including all field names and encoded strings.

Method and route Required capability Response field
POST /api/v1/experiences experience_write receipt: immutable registration
POST /api/v1/experiences/read experience_read experience: current attempt state
POST /api/v1/experiences/events experience_report and the registered reporter subject event: immutable observation and resulting state
POST /api/v1/experiences/events/read experience_read event: the requested historical observation
POST /api/v1/experiences/history experience_read page: revision-fenced event history
POST /api/v1/experiences/artifacts experience_report, tool_read and the registered reporter subject binding: immutable stored-artifact link
POST /api/v1/experiences/artifacts/read experience_read and tool_read binding: the requested artifact link
POST /api/v1/experiences/lineage experience_read lineage: bounded pinned ancestry
POST /api/v1/experiences/export experience_read export: exact selected observations and accounting summary

Capabilities are independent. Memory, tool and procedure permissions do not grant experience access. A writer chooses reporter_subject_id when registering an attempt; this binding grants no credential or capability. Reporting requires a current credential for that subject with experience_report and the exact scope. Grant experience_read separately when the worker needs to inspect state.

Shared scopes permit distinct writer and reporter subjects within the same tenant and exact grant. A private attempt must name its owning subject as the reporter. Private namespaces of two subjects remain distinct even if every visible resource identifier is identical. Project, mission and agent are exact values; null mission is not a wildcard. See authentication.

Register an attempt#

First have a policy administrator register the immutable execution context through /api/v1/learning/contexts. That context identifies the task, baseline, model, tools, environment, evaluation contract, dataset, harness and permissions. Experience registration binds its server-computed content digest. Tool identities in that context remain declarations; this API does not resolve them into executable artifact bytes.

Then post the following to /api/v1/experiences:

{
  "contract_version": 1,
  "scope": {
    "project_id": "project",
    "mission_id": null,
    "agent_id": "agent",
    "visibility": "shared"
  },
  "request": {
    "id": "attempt-v1",
    "context_id": "context-v1",
    "reporter_subject_id": "observer",
    "accounting_unit": "test-credit-v1",
    "input": {
      "reference": "fixture:input",
      "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    },
    "parent": null
  }
}

The repeated digest is a synthetic placeholder. Supply the actual SHA-256 of the referenced input in an integration. The server preserves the reference and digest without fetching their content. accounting_unit names your versioned consumption unit; the database does not infer currency, price or provider billing.

The response contains contract_version: 1 and receipt, with the original request, authenticated actor, tenant, namespace, context_digest, optional parent_event_digest, server recorded_at_millis, schema_version and receipt_digest. Save the returned context_digest for observations.

Registration starts at revision 1, with outcome: null and no event. It records intent only. An identical request ID and content from the same subject returns the original receipt, including its original credential and timestamp. Changed content or a different creating subject returns 409. A rotated credential for the same subject may retry after current authorization succeeds.

Record an observation#

Post to /api/v1/experiences/events as the bound reporter. Replace the example context digest with the exact value returned during registration:

{
  "contract_version": 1,
  "scope": {
    "project_id": "project",
    "mission_id": null,
    "agent_id": "agent",
    "visibility": "shared"
  },
  "attempt_id": "attempt-v1",
  "command": {
    "event_id": "observation-v1",
    "expected_revision": 1,
    "context_digest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
    "outcome": "unknown",
    "evidence": {
      "reference": "fixture:trace",
      "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    },
    "cost_units": null,
    "latency_ms": null
  }
}

The event records the command, authenticated actor, resulting record, server timestamp, schema version and event_digest. It advances the attempt to revision 2 in the same synchronous write batch. The evaluation/verifier contract identity comes from the immutable registered context; the credential identifies the reporter. Neither establishes that the referenced verifier actually ran.

Current outcome Accepted new outcome Meaning
null unknown, succeeded, failed, cancelled First observation
unknown Any of the four outcomes Further uncertainty or a reported result
succeeded, failed, cancelled The same outcome only Additional evidence or late consumption, preserving the terminal outcome

cancelled is a reporter assertion, not a cancellation request or proof that a remote process stopped. An execution with unknown completion stays unknown. Record corrections that require a different terminal outcome as a new, explicitly linked attempt; this release does not replace or automatically supersede history.

Event IDs are unique within an attempt. Identical command retries by the same subject return the original event even after later observations. Changed commands under the same event ID return 409. For a new event, expected_revision must match current state. Concurrent different events using one revision have one winner; losers must read current state before deciding whether to submit a new observation. Never change an event ID merely to make an uncertain retry look new.

Preserve unknown resource consumption#

cost_units and latency_ms are cumulative observations for the whole attempt, not increments. Missing or null means unknown; 0 means a reported zero. Known cumulative values cannot decrease relative to an earlier known value. Different attempts may use different accounting units; do not sum them without an explicit conversion policy.

The current record distinguishes two views:

Fields Meaning
reported_cost_units, reported_latency_ms Values in the latest observation, possibly unknown
observed_cost_units, observed_latency_ms Greatest known cumulative observations, retained when a later report is unknown

For example, a report of cost 9 followed by null produces reported cost null and observed cost 9. The latter is a retained lower bound, not evidence that total cost is exactly 9. A later same-outcome report may supply 12. It cannot supply 8. An outcome may be succeeded while cost remains unknown; admission of that receipt is not budget qualification. Existing procedural evaluation still requires its own valid evidence and accounting.

Read current state and historical evidence#

Post to /api/v1/experiences/read:

{
  "contract_version": 1,
  "scope": {"project_id":"project","mission_id":null,"agent_id":"agent","visibility":"shared"},
  "attempt_id": "attempt-v1"
}

For /api/v1/experiences/events/read, add "event_id": "observation-v1". That operation returns the historical event, not the latest state. There is no cursor, listing, automatic graph traversal or semantic search over experiences in this version. Server wall-clock timestamps are audit metadata; revision and event identity define the per-attempt order.

To branch from an observed experience, create a new attempt with:

{"parent": {"attempt_id":"attempt-v1","event_id":"observation-v1"}}

The parent event must already exist in the same authorized namespace. The server binds its exact event_digest; later parent observations do not alter the child. An attempt cannot name itself as parent. References to existing events support an acyclic creation history, but do not implement a replay simulator. Inputs from another execution context remain distinguishable through each attempt's context.

Limits, errors and durability#

Condition HTTP status and code
Invalid JSON, extra fields, bad digest, decreasing consumption or invalid private reporter 400 invalid_request
Unsupported contract version 400 unsupported_contract_version
Missing, expired, revoked or rotated-old credential 401 unauthorized
Missing capability/grant or wrong reporting subject 403 forbidden
Context, attempt or parent/event absent from the authorized namespace 404 record_not_found
Stale expected revision 409 revision_conflict
Changed request/event, mismatched context digest or terminal outcome 409 idempotency_conflict
Body exceeds 65,536 bytes 413 invalid_request
Stored identity, digest or receipt/state inconsistency 500 storage_inconsistency

Record IDs, context IDs, event IDs and accounting units support 1–512 UTF-8 bytes; reporter subjects support 1–256 without control characters. Evidence references support 1–2048 bytes. Values must be nonblank. Digests use exactly 64 lowercase hexadecimal characters, without a sha256: prefix. Consumption values are unsigned 64-bit integers. Revisions start at 1; a submitted revision must allow its unsigned 64-bit successor.

Acknowledged writes synchronize the RocksDB WAL. Each observation and resulting state commit atomically. Process-kill tests exercise recovery of registrations, events and idempotency; they do not establish hardware power-loss tolerance. Authorization is checked on each request, including retries. An operation already authorized may finish while revocation races with it, as in the existing platform authorization contract.

Server receipt/event digests identify serialized records; use the returned values as opaque identities rather than reimplementing their serialization in clients. They detect inconsistent records, not malicious filesystem operators or false external evidence. This release has no experience deletion, retention policy, source-revocation propagation, evidence deduplication, independent execution verification or automatic strategy extraction. See the experience-memory design for the separate acceptance criteria for those extensions.

Read a bounded observation history#

POST /api/v1/experiences/history requires experience_read for the exact scope. Use this endpoint to audit an attempt without remembering every event ID:

{
  "contract_version": 1,
  "scope": {"project_id": "project", "mission_id": null, "agent_id": "agent", "visibility": "shared"},
  "attempt_id": "attempt-v1",
  "query": {"limit": 32, "cursor": null}
}

The response's page contains receipt_digest, through_revision, events, scanned_events, next_cursor and complete. Pass next_cursor unchanged in the next request. Stop only when complete is true. Every continuation rechecks the credential and scope; a cursor grants no access and is bound to the attempt receipt.

The first page freezes the attempt's current revision. Later reports are omitted from that traversal. This is a logical revision snapshot of immutable events, not a retained database snapshot. Event IDs are ordered by UTF-8 byte length, then by byte value, matching storage keys; this is not chronological order. Sort a completed export by each event's record.revision when chronology is required.

limit is a candidate budget from 1 through 64. Each request examines at most that many event candidates; authentication, receipt and fence lookups are additional fixed reads. A page can be empty and still have a cursor when newly appended events fall beyond the fence. A full final page can require an extra empty continuation. New reports can increase scan work, but never enter the frozen result set or duplicate previously returned events when the cursor is preserved. The service retains no pagination session, so continuation survives restart.

An absent attempt returns 404. A cursor for a different receipt returns 409; invalid bounds or missing continuation/fence events return 400. This endpoint also works for attempts written before history pagination was introduced.

Bind a stored tool artifact to an observation#

When an external worker has used or produced a tool, it can attach a verified stored artifact identity to an exact observation. Register the immutable tool artifact first. Then call POST /api/v1/experiences/artifacts with experience_report and tool_read in the same scope, using the attempt's bound reporter subject:

{
  "contract_version": 1,
  "scope": {"project_id": "project", "mission_id": null, "agent_id": "agent", "visibility": "shared"},
  "attempt_id": "attempt-v1",
  "binding": {
    "id": "binding-v1",
    "event_id": "observation-v1",
    "artifact_id": "tool-v1",
    "artifact_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "role": "candidate"
  }
}

Replace the example digest with artifact_digest returned by artifact registration. The database verifies the stored source, dependency lock and complete artifact proposal digests, and rejects a mismatched submitted digest with 409. The artifact and observation must already exist in the exact authorized namespace; absence returns 404. The roles baseline, candidate and output describe the reporter's assertion. They do not prove execution, fitness, independent evaluation or use by a particular model.

The returned binding includes the immutable request, attempt ID, original receipt_digest, exact event_digest, verified source_digest and dependency_digest, authenticated actor, server time and opaque binding_digest. It contains no source code. Binding does not revise the attempt, change its outcome or qualify a procedure. Late bindings are allowed and do not establish that the artifact existed when an external execution occurred.

A binding ID is immutable within an attempt. An identical request from the same reporter subject returns the original binding, including after credential rotation or restart. Conflicting reuse returns 409. All writes synchronize the WAL. Read it with POST /api/v1/experiences/artifacts/read using the usual contract_version, scope, attempt_id and binding_id. Reading requires both experience_read and tool_read; every read revalidates the artifact and event. Multiple bindings to one artifact do not constitute independent evidence.

Trace the recorded lineage#

POST /api/v1/experiences/lineage reads the exact parent observations pinned by an attempt. It requires experience_read for the exact scope:

{
  "contract_version": 1,
  "scope": {"project_id": "project", "mission_id": null, "agent_id": "agent", "visibility": "shared"},
  "attempt_id": "attempt-v1",
  "max_depth": 16
}

The response's lineage contains the immutable origin receipt and ancestors in immediate-parent-to-root order. Each ancestor is the complete historical event whose digest was pinned by its child. A parent that later succeeds, fails or receives a consumption update does not change this historical path. A root attempt returns an empty ancestry and complete: true.

max_depth is required and allows 1–64 ancestor events. When the limit is reached before the root, complete is false and next_parent plus next_parent_digest identify the next unread link. To continue, request the lineage of the last returned ancestor's attempt ID. Its immutable receipt starts at the next link; concatenate the ancestor arrays without repeating the previous page's origin. Each request rechecks authorization and scope. The response is not a live-state snapshot, a listing of descendants or an enumeration of possible branches.

The reader fails closed when a visited parent is missing, its digest disagrees with the child, or a visited cycle is detected. An absent requested origin returns 404; malformed depth returns 400; inconsistent stored lineage returns 500. A truncated path reports its limit explicitly and is never labeled complete.

This records declared provenance. It does not prove a causal dependency, replay an execution, invent unobserved transitions or certify the truth of a report.

Export an exact observation cohort#

POST /api/v1/experiences/export requires experience_read and accepts an explicit set of 1–64 observations, with one event per distinct attempt. Supply each exact event_digest, the common context_digest and the common accounting_unit:

{
  "contract_version": 1,
  "scope": {"project_id": "project", "mission_id": null, "agent_id": "agent", "visibility": "shared"},
  "selection": {
    "context_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "accounting_unit": "test-credit-v1",
    "events": [{
      "attempt_id": "attempt-v1",
      "event_id": "observation-v1",
      "event_digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    }]
  }
}

Use digests returned by the service, replacing the placeholders above. The response's export contains full immutable events sorted by attempt ID in UTF-8 byte order, method_version: "qilbee.experience-export.v1", coverage: "explicit_event_set", the common context and accounting unit, a summary, and export_digest. Request order does not affect the output. Later observations cannot alter an exported historical event. The server generates the same output for the same event set after restart; it does not create an export job, retain a snapshot session or write a new ledger record.

The summary counts attempts, succeeded, failed, cancelled and unknown. For each of cost_units and latency_ms, it reports:

Field Meaning
known_reports Selected events with a non-null reported value, including zero
unknown_reports Selected events with a null reported value
reported_total Sum of reported values, or null if any report is unknown
observed_lower_bound Sum of retained known cumulative lower bounds; never a claim of complete consumption

Totals are exact unsigned decimal strings, allowing sums beyond unsigned 64-bit range without rounding. Individual event values keep their existing unsigned 64-bit representation; use a lossless JSON parser when needed. A known reported value is still the reporter's cumulative observation, not a certified final bill. An unknown outcome remains a separate count.

The response is all-or-error: no partial cohort is returned. Missing or out-of-scope events return 404; mismatched event hashes, context or accounting unit return 409; duplicate attempts or an invalid count return 400. The standard 65,536-byte request-body limit also applies. No event is replaced with its latest state, excluded silently, or inferred from a parent. Missing observations and attempts outside the submitted set are not included in the denominator.

The export covers exactly the submitted set, not the entire namespace, a random sample or independently verified trials. Artifact bindings and ancestors are available through their own endpoints; the export does not implicitly include them. See evaluate experience evidence for a reproducible comparison workflow and the limits of these summaries.