Show the number boundary.
Atomic already exposes useful session-level provider usage and cost information. It does not yet persist a workflow run/stage usage ledger from the foreground controller. This page separates observed behavior from a proposed plain-JSON contract and never invents missing metrics.
stage-runner-controller.ts currently records model attempts without usage. The ledger below is proposed; not an existing Atomic runtime record.Provider/model IDs stay raw and exact. A provider/runtime omission is rendered as a named unavailable value with a reason. A reported zero is a numeric zero with state zero; it is never conflated with unavailable. No proxy, estimate, inferred rate, or fabricated token/cost number fills a missing field.
Observed implementation rows
Each row is linked to the immutable public baseline cace643f4b921fce4e5786870232ab83d29e8ae3. “Existing” describes current source behavior; the proposed ledger does not claim that a missing workflow path is implemented.
| Boundary | What Atomic exposes today | Exact public source/docs |
|---|---|---|
| Session totals and context status Existing | SessionStats aggregates assistant, tool-result, and branch-summary usage into input/output/cacheRead/cacheWrite/total tokens plus cost; contextUsage is a separate active-context view and can be unavailable after a compaction boundary until a post-compaction response. | packages/coding-agent/src/core/agent-session-export.ts#L14-L101 packages/coding-agent/src/core/agent-session-types.ts#L192-L209 packages/coding-agent/src/core/usage-totals.ts#L4-L59 packages/coding-agent/docs/extensions.md#ctx-getcontextusage |
| Cache accounting Existing | Cache-miss detection compares adjacent assistant requests and configured model cache rates. It is a diagnostic calculation, not a provider usage event and not a substitute for missing provider metrics. | packages/coding-agent/src/core/cache-stats.ts#L8-L106 packages/coding-agent/docs/models.md#request-wide-cost-tiers |
| Compaction and estimates Existing | Context-window accounting prefers normalized provider usage, then estimates trailing transcript/image tokens for compaction decisions. These estimates must never be copied into a provider billing ledger as reported usage or cost. | packages/coding-agent/src/core/compaction/compaction.ts#L29-L55 packages/coding-agent/src/core/compaction/compaction.ts#L113-L165 packages/coding-agent/docs/compaction.md#when-compaction-runs |
| Exported session message totals Existing | The HTML session export computes message, token, model, and cost totals from assistant messages and renders named zero values when no usage was accumulated; it is a session export, not a workflow run/stage ledger. | packages/coding-agent/src/core/export-html/template-js/entries-navigation.js#L208-L285 packages/coding-agent/docs/custom-provider.md#usage-and-cost |
| Workflow attempt contract Existing contract; usage not wired by the controller | WorkflowModelUsage and WorkflowModelAttempt allow optional input/output/cacheRead/cacheWrite/cost/turns, and durable DBOS envelope validation accepts those optional fields. The foreground stage controller currently records model, success, reasoningLevel, and error only; its modelAttempts entries do not include usage. | packages/workflows/src/shared/authoring-contract-stage.ts#L82-L97 packages/workflows/src/durable/dbos-envelope.ts#L447-L482 packages/workflows/src/runs/foreground/stage-runner-controller.ts#L445-L455 packages/workflows/src/runs/foreground/stage-runner-controller.ts#L1236-L1265 packages/coding-agent/docs/models.md#request-wide-cost-tiers |
Proposed run/stage usage ledger schema
Proposed only. The following JSON is the exact shape description: required and optional fields are explicit, metric availability is represented inside each metric field, and the received event stream is append-ordered and inspectable.
{
"schema": "atomic/workflow-usage-ledger/v1",
"status": "proposed; not an existing Atomic runtime record",
"run": {
"required": [
"ledgerId",
"runId",
"runState",
"createdAt",
"updatedAt",
"events"
],
"optional": [
"workflowId",
"workflowName",
"completedAt",
"failure",
"stages",
"rollup"
],
"fieldTypes": {
"ledgerId": "string",
"runId": "string; exact durable run identity",
"runState": "pending|running|paused|completed|skipped|cancelled|blocked|failed|killed",
"createdAt": "ISO-8601 timestamp",
"updatedAt": "ISO-8601 timestamp",
"events": "array of event objects in append/sequence order; never sorted by a consumer",
"stages": "optional array of stage summaries keyed by exact stageId",
"rollup": "optional numeric-only rollup; omitted metrics remain unavailable"
}
},
"stage": {
"required": [
"stageId",
"stageState",
"events"
],
"optional": [
"stageName",
"startedAt",
"endedAt",
"rollup"
],
"fieldTypes": {
"stageId": "string; exact stage identity",
"stageState": "pending|running|awaiting_input|paused|blocked|completed|failed|skipped",
"events": "array of references/events in the same ordered ledger stream"
}
},
"event": {
"required": [
"eventId",
"sequence",
"kind",
"occurredAt",
"idempotencyKey",
"rawEvent"
],
"optional": [
"stageId",
"attemptId",
"providerId",
"modelId",
"usage",
"state",
"metadata",
"duplicateOf",
"aggregationDisposition",
"conflictOf",
"conflictReason"
],
"fieldTypes": {
"eventId": "string; producer event identity",
"sequence": "integer; strictly increasing within one ledger",
"kind": "run_started|run_state_changed|stage_started|model_attempt|stage_finished|run_finished",
"occurredAt": "ISO-8601 timestamp from the producer",
"idempotencyKey": "string; stable deduplication key for the logical event",
"rawEvent": "object; exact received event copied verbatim and retained in order",
"providerId": "optional raw provider ID exactly as reported; do not normalize or invent",
"modelId": "optional raw model ID exactly as reported; do not normalize or invent",
"stageId": "required for stage/model events; absent for run-only events",
"attemptId": "required for model_attempt events; otherwise optional",
"duplicateOf": "optional eventId of an exact duplicate; duplicate record remains inspectable",
"aggregationDisposition": "optional aggregate|excluded_duplicate|conflict",
"conflictOf": "optional eventId with the same idempotencyKey but a different payload",
"conflictReason": "required when aggregationDisposition is conflict; loud processing error is emitted"
},
"modelAttemptRule": "model_attempt events require providerId/modelId when the runtime reports them and require usage with every metric field object present; absent runtime IDs remain unavailable with a reason rather than invented IDs"
},
"usage": {
"required": [
"input",
"output",
"cacheRead",
"cacheWrite",
"cost",
"turns"
],
"optional": [],
"metricField": {
"required": [
"state",
"value",
"source",
"reason"
],
"optional": [],
"fields": {
"state": "reported|zero|unavailable",
"value": "number|null; number only for reported or explicit zero, null for unavailable",
"source": "string|null; provider/runtime source when known",
"reason": "string|null; required and non-null when state is unavailable"
}
},
"cost": "provider/runtime-reported cost only; no proxy, inferred rate, or estimate for a missing value",
"unavailable": "state unavailable plus value null and a named reason; never silently converted to zero"
},
"orderingAndDeduplication": {
"orderedEvents": "append every received event in producer sequence order and preserve that order in reads",
"exactDuplicates": "retain the full duplicate rawEvent with duplicateOf and aggregationDisposition excluded_duplicate; rollups count the logical event once",
"conflicts": "retain both full rawEvent payloads with conflictOf and aggregationDisposition conflict, emit a loud error, and never overwrite either payload"
},
"legalTransitions": {
"run": {
"pending": [
"running",
"blocked",
"skipped",
"cancelled",
"killed"
],
"running": [
"paused",
"completed",
"failed",
"blocked",
"cancelled",
"killed"
],
"paused": [
"running",
"cancelled",
"killed"
],
"terminal": [
"completed",
"skipped",
"cancelled",
"blocked",
"failed",
"killed"
]
},
"stage": {
"pending": [
"running",
"skipped",
"blocked"
],
"running": [
"awaiting_input",
"paused",
"completed",
"failed",
"skipped",
"blocked"
],
"awaiting_input": [
"running",
"paused",
"failed",
"blocked"
],
"paused": [
"running",
"failed",
"blocked",
"skipped"
],
"terminal": [
"completed",
"failed",
"blocked",
"skipped"
]
}
},
"rollupInvariants": [
"A stage rollup sums each accepted unique model_attempt event belonging to that stage; null/unavailable values are not summed.",
"A run rollup is the numeric sum of its stage rollups and run-level model events, with no event counted through both paths.",
"A reported zero is retained as zero and is distinct from unavailable; unavailable metrics retain their reason and never become a proxy estimate.",
"A terminal run cannot gain new usage events; late events are rejected or recorded as an explicit conflict, never silently attached."
]
}Metric states: zero versus unavailable
Every metric object uses required state, value, source, and reason fields. A reported numeric value has state: reported; an explicitly reported zero has state: zero and value: 0; an unavailable value has state: unavailable, value: null, and a non-empty reason. The example is a shape illustration, not observed usage data.
{
"input": {
"state": "unavailable",
"value": null,
"source": null,
"reason": "provider response omitted input token count"
},
"output": {
"state": "zero",
"value": 0,
"source": "provider/runtime response",
"reason": null
},
"cacheRead": {
"state": "unavailable",
"value": null,
"source": null,
"reason": "runtime did not report cache-read tokens"
},
"cacheWrite": {
"state": "unavailable",
"value": null,
"source": null,
"reason": "runtime did not report cache-write tokens"
},
"cost": {
"state": "unavailable",
"value": null,
"source": null,
"reason": "provider/runtime did not report cost"
},
"turns": {
"state": "reported",
"value": 0,
"source": "workflow runtime",
"reason": null
}
}input: unavailable — reason: provider response omitted input token count; cost: unavailable — reason: provider/runtime did not report cost. The UI must retain these reasons and must not turn them into zeroes or estimates.Ordered events, duplicates, and conflicts
Every received event is retained verbatim in sequence order, including retries. An exact duplicate keeps its complete rawEvent, receives duplicateOf and aggregationDisposition: excluded_duplicate, and is excluded from numeric aggregation only; it is never discarded from inspection. A same-idempotency-key event with a different payload keeps both payloads, records conflictOf and aggregationDisposition: conflict, and emits a loud error. Neither payload is overwritten.
Run/stage states and legal transitions
Proposed run transitions: pending → running|blocked|skipped|cancelled|killed; running → paused|completed|failed|blocked|cancelled|killed; paused → running|cancelled|killed. Proposed stage transitions: pending → running|skipped|blocked; running → awaiting_input|paused|completed|failed|skipped|blocked; awaiting_input → running|paused|failed|blocked; paused → running|failed|blocked|skipped. Completed, skipped, cancelled, blocked, failed, and killed are terminal; terminal events cannot gain new usage silently.
Cross-stage rollup invariants
Stage totals sum each accepted unique model_attempt event for that stage, excluding only duplicate dispositions and never summing null/unavailable values. Run totals are the numeric sum of stage totals plus run-level model events, with no event counted through both paths. Every rollup preserves raw IDs, event order, availability states, and reasons. A missing metric remains unavailable all the way up the rollup.