Skip to content

Attention & review

The attention loop is how you find what needs you and clear it: chronicle.pending is your action queue, chronicle.activity is the read-only event feed of what's happened in your scope, and chronicle.assets approve/reject/bulk_approve is how you action items off the queue.

Both feeds are ACL-bounded server-side — they only ever surface objects you can read, so you never filter for reachability. Both page newest-first on a timestamp cursor.

Pending queue

chronicle.pending lists the actionable items awaiting you across every object you can read. Two kinds today:

kind What it is How to clear it
blocked A variation whose agent paused for input Post a steering message to the experiment's steering_asset_id
report_approval A review-gated report awaiting a decision approve / reject / bulk_approve on its asset_id
for item in chronicle.pending.iter():
    print(item.kind, item.experiment_title, item.asset_id or item.steering_asset_id)

Each PendingItem carries kind, a display-only id, a since timestamp, the variation index, and the experiment context (experiment_id, experiment_slug, experiment_title, owner_principal_id). The kind-specific fields are present only on their kind: agent_id + steering_asset_id for blocked; asset_id + asset_type + asset_title for report_approval.

id is not the approve identity

PendingItem.id is a stable per-row display id, not a cursor and not what you approve. Action a report_approval by its asset_id; let iter handle paging.

list and iter take limit and scope; list also takes before (the cursor) for manual paging — iter walks the pages for you.

scope is reserved

scope is accepted for forward compatibility but not sent — v1 derives scope from the calling key/user's readable set. To narrow, call with a scope-restricted key.

Activity feed

chronicle.activity is a reverse-chronological feed of audited actions — run start/finish, new reports and assets, agent turns, conclusions, retractions — across the experiments you can read. It backfills from the persisted audit log, so a fresh page is never empty. It is read-only: the record of what happened, not a to-do list.

for event in chronicle.activity.iter(limit=50):
    print(event.timestamp, event.action, event.actor_subject, event.resource_id)

Each ActivityItem has action (the audited verb, e.g. run.start, asset.create, experiment.conclude), resource_type + resource_id for the object touched, actor_subject for who did it, an optional via (how it was issued) and status (outcome), and the related experiment_id when one applies. action is an open vocabulary — match on the strings you care about; see ../reference/client.md for the full type. list takes limit + before; iter takes limit.

Approving & rejecting

A report_approval item carries review_required in the asset's pending reasons. Two single-asset actions clear it — both require Write on the asset:

chronicle.assets.approve(asset_id)                       # clears review_required;
                                                         # auto-finalizes if it was the last reason
chronicle.assets.reject(asset_id, reason="results don't support the claim")

approve is idempotent on already-terminal assets. reject is destructive: it clears review_required and transitions the asset pending → abandoned, recording rejected_at and rejection_reason. The reason is required and surfaces in the audit log and UI, so write it for a human.

Bulk approve

bulk_approve is the feed's "approve selected" / "approve all" action. Pass exactly one mode (anything else raises ValueError before a request goes out):

# list pending → approve the safe ones → reject the holdout
safe = [
    i.asset_id
    for i in chronicle.pending.iter()
    if i.kind == "report_approval" and i.asset_id is not None
]
resp = chronicle.assets.bulk_approve(safe[:-1])          # approve selected
chronicle.assets.reject(safe[-1], reason="missing baseline comparison")

chronicle.assets.bulk_approve(all_in_scope=True)         # or: approve everything in scope
Argument Meaning
asset_ids (positional) Approve exactly these assets ("approve selected")
all_in_scope=True Approve every pending review_required asset you can act on ("approve all")

Partial success is normal: the call is HTTP 200 with a per-asset result list, and a 404, permission denial, or validation failure on one asset surfaces as an error row rather than aborting the batch. Each asset's Write permission is re-checked server-side, so an all_in_scope candidate you can read but not write fails its own row.

It returns a BulkApproveResponseresults plus approved / failed counts:

print(f"{resp.approved} approved, {resp.failed} failed")
for r in resp.results:
    if r.status == "error":
        print("failed", r.asset_id, r.error)

Each BulkApproveResult:

Field Meaning
asset_id The asset this row reports on
status approved or error
error Human-readable reason (None unless status == "error")
finalized On success, True if approval auto-transitioned the asset pending → ready, False if it still carries other pending reasons; None on error

See ../reference/client.md for the full PendingItem / ActivityItem / BulkApproveResponse types, and assets.md for the asset lifecycle these decisions move.