Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.3] - 2026-07-24

### Fixed

- `register_run` now upserts a dashboard `runs` row so Admin list/detail show the run
without a separate `create_run`. Ledger spend overlays `cost_micros`; halt/clear
keep the dashboard row in sync.

## [0.1.2] - 2026-07-24

### Added
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ graph TD
UI: POST /v1/tasks (task only; no run_id)
├─ instrument_app binds RequestContext (service, intent, provider, model)
├─ with tokenops_run(client=…): → register-or-join + SpanContext + governance
├─ store/client.create_run(RunRecord status="running")
│ register_run also upserts a dashboard runs row (status="running")
├─ optional create_run/update_run for richer status/cost/steps (not required for visibility)
├─ agent.run(…, complete_fn=wrap_complete(bound.…))
│ pre_call → worst-case / concurrency detectors
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "agent-tokenops"
version = "0.1.2"
version = "0.1.3"
description = "TokenOps control plane and SDK for run-aware agent token governance"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/tokenops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

load_env()

__version__ = "0.1.2"
__version__ = "0.1.3"


def init() -> None:
Expand Down
39 changes: 37 additions & 2 deletions src/tokenops/control/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from collections.abc import Callable
from typing import Any, TypeVar

from tokenops.control.ledger import LIFETIME, RUN_TOTAL_BUDGET
from tokenops.control.models import (
BudgetSpec,
PolicyInstance,
Expand Down Expand Up @@ -377,6 +378,18 @@ def register_run(self, reg: RunRegistration) -> RunRegistration:
(reg.run_id, reg.intent, json.dumps(reg.user_dims), reg.mode.value, time.time()),
)
self._db.commit()
# Dashboard reads ``runs``; registration alone must make the run visible.
# Explicit create_run/update_run remain for richer status/cost/steps (REPLACE-safe).
agent = reg.intent or "agent"
self.create_run(
RunRecord(
run_id=reg.run_id,
agent=agent,
status="running",
dims=dict(reg.user_dims),
task=reg.intent or None,
)
)
return reg

@_locked
Expand Down Expand Up @@ -484,15 +497,27 @@ def update_run(self, run_id: str, **fields) -> None:
@_locked
def get_run(self, run_id: str) -> RunRecord | None:
row = self._db.execute("SELECT * FROM runs WHERE run_id=?", (run_id,)).fetchone()
return _run(row) if row else None
return self._run_with_ledger_cost(row) if row else None

@_locked
def list_runs(self, *, problematic_only: bool = False, limit: int = 200) -> list[RunRecord]:
sql = "SELECT * FROM runs"
if problematic_only:
sql += " WHERE status IN ('halted','throttled','error')"
sql += " ORDER BY started_at DESC LIMIT ?"
return [_run(r) for r in self._db.execute(sql, (limit,))]
return [self._run_with_ledger_cost(r) for r in self._db.execute(sql, (limit,))]

def _run_with_ledger_cost(self, row: sqlite3.Row) -> RunRecord:
"""Build a RunRecord; prefer ``__run_total__`` ledger spend when present."""
rec = _run(row)
spent_row = self._db.execute(
"SELECT spent_micros FROM ledger_spent "
"WHERE budget_id=? AND segment_key=? AND period=?",
(RUN_TOTAL_BUDGET.budget_id, f"run:{rec.run_id}", LIFETIME),
).fetchone()
if spent_row is not None:
rec.cost_micros = int(spent_row[0])
return rec

@_locked
def run_tag_keys(self, *, limit: int = 500) -> list[str]:
Expand Down Expand Up @@ -581,6 +606,11 @@ def ledger_mark_halted(self, run_id: str, reason: str = "") -> None:
"halt_reason=COALESCE(excluded.halt_reason, ledger_halt.halt_reason)",
(run_id, reason or None),
)
# Keep dashboard row in sync when present; ignore missing runs rows.
self._db.execute(
"UPDATE runs SET status='halted', halt_reason=? WHERE run_id=?",
(reason or None, run_id),
)
self._db.commit()

@_locked
Expand All @@ -606,6 +636,11 @@ def ledger_clear_halt(self, run_id: str) -> None:
"ON CONFLICT(run_id) DO UPDATE SET halted=0, halt_reason=NULL",
(run_id,),
)
# Clear halt_reason only — do not invent a completed/running status.
self._db.execute(
"UPDATE runs SET halt_reason=NULL WHERE run_id=?",
(run_id,),
)
self._db.commit()

# ---- trajectory hint index -------------------------------------------- #
Expand Down
62 changes: 61 additions & 1 deletion tests/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@

from conftest import toy_price
from tokenops.control import build_governor
from tokenops.control.models import BudgetSpec, PolicyInstance, RunRecord, Segment
from tokenops.control.ledger import LIFETIME, RUN_TOTAL_BUDGET
from tokenops.control.models import (
BudgetSpec,
PolicyInstance,
RunRecord,
RunRegistration,
Segment,
)
from tokenops.control.store import Store


Expand Down Expand Up @@ -77,6 +84,59 @@ def test_run_records_and_problematic_filter(store):
assert problematic[0].halt_reason == "budget exhausted"


def test_register_run_creates_dashboard_row(store):
"""Registration alone must surface on the Admin Dashboard (list_runs / get_run)."""
store.register_run(
RunRegistration(
run_id="reg-only",
intent="summarize",
user_dims={"team": "growth"},
)
)
got = store.get_run("reg-only")
assert got is not None
assert got.agent == "summarize"
assert got.status == "running"
assert got.task == "summarize"
assert got.dims == {"team": "growth"}
assert got.cost_micros == 0
listed = store.list_runs()
assert [r.run_id for r in listed] == ["reg-only"]

store.ledger_add_spent(RUN_TOTAL_BUDGET.budget_id, "run:reg-only", LIFETIME, 12_345)
assert store.get_run("reg-only").cost_micros == 12_345
assert store.list_runs()[0].cost_micros == 12_345

# Explicit create_run after register remains REPLACE-safe.
store.create_run(
RunRecord(
run_id="reg-only",
agent="summarize",
status="completed",
cost_micros=99,
dims={"team": "growth"},
task="summarize",
)
)
after = store.get_run("reg-only")
assert after.status == "completed"
assert after.cost_micros == 12_345 # ledger still overlays

store.ledger_mark_halted("reg-only", reason="budget exhausted")
halted = store.get_run("reg-only")
assert halted.status == "halted"
assert halted.halt_reason == "budget exhausted"

store.ledger_clear_halt("reg-only")
cleared = store.get_run("reg-only")
assert cleared.halt_reason is None
assert cleared.status == "halted" # clear does not invent a new status

store.register_run(RunRegistration(run_id="no-intent"))
assert store.get_run("no-intent").agent == "agent"
assert store.get_run("no-intent").task is None


def test_seed_default_governance_if_empty(tmp_path, monkeypatch):
monkeypatch.delenv("TOKENOPS_SKIP_GOVERNANCE_SEED", raising=False)
s = Store(str(tmp_path / "seed.db"), auto_seed=False)
Expand Down
Loading