Runbook — Predictive Run-Budget Forecast¶
Purpose¶
After each step is recorded, the Rust governance plane projects the run's
end-of-run cost (and tool-call / wall-time consumption) and flags
budget_breach_projected = true when any axis is on track to exceed its
configured cap before the run can terminate. The forecast is a
non-blocking signal — it surfaces in the dashboard and audit log; the
existing budget auto-kill is what actually terminates a run.
Where the forecast comes from¶
- Module:
rust/crates/fd-policy/src/forecast.rs(compute_forecast). - Call site:
rust/services/gateway/src/handlers/runs.rsinsubmit_step_result, right afterincrement_usagewrites the step cost. - The forecast uses
PolicyEngine::default_budget()so it is always evaluated against the same caps the auto-kill enforces. - Persisted on
runsviaRunsRepo::update_forecast(one UPDATE, no joins). See migrationdb/migrations/20260524000001_add_run_forecast.sql.
Projections¶
Two projections are produced per snapshot. Both are deterministic
(rust_decimal) — there is no ML dependency at this layer.
| Projection | Method | When to trust |
|---|---|---|
projected_cost_cents |
Linear: extrapolates current cost against the most-burned axis (cost ratio, tool-call ratio, wall-time ratio). | Conservative — good early-warning. |
ewma_cost_cents |
EWMA of per-step cost, projected over remaining tool-calls. α = 0.3. | Better mid-run signal once the per-step cost rate stabilises. |
The breach flag fires when either projection (or the wall-time already-exceeded fact) crosses a cap. Precedence: wall-time → cost → tool-calls.
API contract¶
GET /v1/runs/{run_id} (additive fields, all optional):
{
"id": "run_…",
"cost_cents": 230,
"tool_calls": 4,
"projected_cost_cents": 712,
"ewma_cost_cents": 690,
"budget_breach_projected": true,
"breach_kind": "cost_cents",
"forecast_at": "2026-05-24T08:42:11.214Z"
}
breach_kind is null when no breach is projected. Allowed values:
cost_cents, tool_calls, wall_time.
SSE contract¶
Event type: run.forecast.updated on the per-run channel run:{run_id}.
{
"id": "evt_…",
"type": "run.forecast.updated",
"channel": "run:run_…",
"timestamp": "2026-05-24T08:42:11.214Z",
"payload": {
"run_id": "run_…",
"projected_cost_cents": 712,
"ewma_cost_cents": 690,
"budget_breach_projected": true,
"breach_kind": "cost_cents",
"at": "2026-05-24T08:42:11.214Z"
}
}
Status — gateway push wiring is deferred. The BFF SSE endpoint
(nextjs/src/app/api/sse/[channel]/route.ts) currently emits this event
shape via the mock generator so dashboard consumers and the schema are
locked in. The gateway→BFF push path uses the same pattern as the
SchemaDriftGuard activation note — to be wired in a follow-up. Polling on
GET /v1/runs/{run_id} (default 2 s) picks up forecast updates immediately.
What to do when the badge fires¶
- Open the run-detail page; the "Projected to exceed …" badge appears next to the status badge with a tooltip showing both projections.
- Compare
projected_cost_centsvs.cost_centsand the configured cap. If the projection is climbing across consecutive snapshots, the run is accelerating — likely a runaway tool loop. - If acceleration is suspected, cancel via
POST /v1/runs/{run_id}/cancel. The auto-kill will still trigger on the actual cap; the forecast just gives you an earlier hand on the kill switch. - For repeated false positives on the EWMA projection at the start of a run, the EWMA seeds from the first step's cost — short, expensive bootstrap steps can over-project. Wait one or two more snapshots before intervening.
Dashboard surfaces¶
- Daily cost chart (
nextjs/src/components/charts/cost-chart.tsx) shows a dashed yellow segment stacked on top of today's actual bar representing the projected additional cost from currently-active runs that have been flagged. - Run header badge
(
nextjs/src/components/runs/budget-projection-badge.tsx) hidden by default; surfaces with a pulsing critical badge whenbudget_breach_projectedistrue.
Operations checklist¶
- The
runs.budget_breach_projectedcolumn ships withDEFAULT FALSE, so pre-existing runs need no backfill. - A partial index
(
runs_budget_breach_projected_idx WHERE budget_breach_projected = TRUE) supports the dashboard's "active runs projected to breach" query. - The forecast snapshot write is best-effort — if the UPDATE fails, the
step result still returns success and the failure is logged via
tracingatwarn. This is intentional; the forecast must not gate run progress.
Related links¶
- Module:
rust/crates/fd-policy/src/forecast.rs - Handler:
rust/services/gateway/src/handlers/runs.rs:submit_step_result - Migration:
db/migrations/20260524000001_add_run_forecast.sql - Dashboard badge:
nextjs/src/components/runs/budget-projection-badge.tsx - Cost chart:
nextjs/src/components/charts/cost-chart.tsx