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 BulkApproveResponse — results 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.