agent-tokenops 0.2.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {agent_tokenops-0.2.1/src/agent_tokenops.egg-info → agent_tokenops-0.4.0}/PKG-INFO +24 -23
  2. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/README.md +20 -21
  3. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/pyproject.toml +16 -5
  4. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0/src/agent_tokenops.egg-info}/PKG-INFO +24 -23
  5. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/SOURCES.txt +13 -0
  6. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/requires.txt +4 -1
  7. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/__init__.py +1 -1
  8. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/config/default.yaml +1 -1
  9. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/__init__.py +20 -10
  10. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/boundary.py +7 -19
  11. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/client.py +69 -32
  12. agent_tokenops-0.4.0/src/tokenops/control/config.py +279 -0
  13. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/context.py +17 -0
  14. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/core.py +18 -7
  15. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/crossing.py +21 -26
  16. agent_tokenops-0.4.0/src/tokenops/control/dev_plane.py +70 -0
  17. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/engine.py +32 -13
  18. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/http_store.py +12 -3
  19. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/integration.py +34 -13
  20. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/ledger.py +232 -27
  21. agent_tokenops-0.4.0/src/tokenops/control/ledger_backend.py +279 -0
  22. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/models.py +15 -2
  23. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/__init__.py +7 -3
  24. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/context_compaction.py +40 -11
  25. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/step_cap.py +4 -4
  26. agent_tokenops-0.4.0/src/tokenops/control/policies/time_budget.py +57 -0
  27. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/trajectory_hint.py +4 -2
  28. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/run.py +18 -3
  29. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/store.py +19 -5
  30. agent_tokenops-0.4.0/src/tokenops/control/usage.py +43 -0
  31. agent_tokenops-0.4.0/src/tokenops/demo.py +194 -0
  32. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/providers/anthropic.py +5 -1
  33. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/providers/openai.py +6 -0
  34. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/providers/types.py +4 -0
  35. agent_tokenops-0.4.0/src/tokenops/ui/policy_labels.py +18 -0
  36. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/run_detail.py +7 -4
  37. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/views/admin.py +42 -28
  38. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/views/dashboard.py +2 -0
  39. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_actuators.py +1 -1
  40. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_boundary.py +3 -3
  41. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_chronicle_boundary.py +5 -5
  42. agent_tokenops-0.4.0/tests/test_context_compaction.py +121 -0
  43. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_control_plane_app.py +16 -11
  44. agent_tokenops-0.4.0/tests/test_control_plane_client.py +65 -0
  45. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_crossing_hook.py +5 -5
  46. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_demo.py +9 -8
  47. agent_tokenops-0.4.0/tests/test_demo_policy_names.py +149 -0
  48. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_governance_config_cache.py +0 -4
  49. agent_tokenops-0.4.0/tests/test_governance_event_names.py +214 -0
  50. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_ledger.py +40 -0
  51. agent_tokenops-0.4.0/tests/test_ledger_backend_contract.py +142 -0
  52. agent_tokenops-0.4.0/tests/test_ledger_backend_mode.py +163 -0
  53. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_policies_wrap_integration.py +69 -1
  54. agent_tokenops-0.4.0/tests/test_policy_names.py +279 -0
  55. agent_tokenops-0.4.0/tests/test_provider_usage.py +161 -0
  56. agent_tokenops-0.4.0/tests/test_time_budget.py +73 -0
  57. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_tokenops_run.py +22 -14
  58. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_trajectory_hint.py +12 -1
  59. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_trajectory_hint_e2e.py +10 -1
  60. agent_tokenops-0.4.0/tests/test_ui_policy_names.py +177 -0
  61. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_wrap_adapters.py +4 -4
  62. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_wrap_complete_chronicle.py +4 -4
  63. agent_tokenops-0.2.1/src/tokenops/control/config.py +0 -178
  64. agent_tokenops-0.2.1/src/tokenops/demo.py +0 -113
  65. agent_tokenops-0.2.1/tests/test_context_compaction.py +0 -38
  66. agent_tokenops-0.2.1/tests/test_control_plane_client.py +0 -88
  67. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/LICENSE.txt +0 -0
  68. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/setup.cfg +0 -0
  69. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/dependency_links.txt +0 -0
  70. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/top_level.txt +0 -0
  71. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/adapters/__init__.py +0 -0
  72. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/adapters/langchain.py +0 -0
  73. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/config/__init__.py +0 -0
  74. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/config/loader.py +0 -0
  75. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/attribution.py +0 -0
  76. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/governance_cache.py +0 -0
  77. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/http.py +0 -0
  78. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/instrument.py +0 -0
  79. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/_util.py +0 -0
  80. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/concurrency_cap.py +0 -0
  81. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/cost_budget.py +0 -0
  82. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/cost_guard.py +0 -0
  83. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/output_runaway.py +0 -0
  84. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/pre_call_worst_case.py +0 -0
  85. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/progress_guard.py +0 -0
  86. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/tool_fix.py +0 -0
  87. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/policies/tool_output_cap.py +0 -0
  88. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/pricing.py +0 -0
  89. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/propagate.py +0 -0
  90. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/request_context.py +0 -0
  91. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/__init__.py +0 -0
  92. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/compress.py +0 -0
  93. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/enqueue.py +0 -0
  94. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/gates.py +0 -0
  95. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/hint.py +0 -0
  96. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/scope.py +0 -0
  97. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/serialize.py +0 -0
  98. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/worker.py +0 -0
  99. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/env.py +0 -0
  100. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/providers/__init__.py +0 -0
  101. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/providers/factory.py +0 -0
  102. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/server/__init__.py +0 -0
  103. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/server/__main__.py +0 -0
  104. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/server/app.py +0 -0
  105. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/__init__.py +0 -0
  106. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/app.py +0 -0
  107. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/store_client.py +0 -0
  108. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/theme.py +0 -0
  109. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/src/tokenops/ui/views/__init__.py +0 -0
  110. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_a2a_health.py +0 -0
  111. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_apply.py +0 -0
  112. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_attribution.py +0 -0
  113. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_attribution_ledger_policies_e2e.py +0 -0
  114. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_concurrency_cap.py +0 -0
  115. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_config.py +0 -0
  116. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_cost_budget.py +0 -0
  117. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_cost_guard.py +0 -0
  118. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_cross_process_budget_gating.py +0 -0
  119. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_governance_per_agent.py +0 -0
  120. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_integration.py +0 -0
  121. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_llm_boundary_precall.py +0 -0
  122. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_output_runaway.py +0 -0
  123. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_policy_coordination.py +0 -0
  124. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_pre_call_worst_case.py +0 -0
  125. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_preview_mode.py +0 -0
  126. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_pricing.py +0 -0
  127. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_progress_guard.py +0 -0
  128. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_propagate.py +0 -0
  129. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_reason_encoding.py +0 -0
  130. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_server_enforcement.py +0 -0
  131. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_step_cap.py +0 -0
  132. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_store.py +0 -0
  133. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_thread_safety.py +0 -0
  134. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_tool_fix.py +0 -0
  135. {agent_tokenops-0.2.1 → agent_tokenops-0.4.0}/tests/test_tool_output_cap.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-tokenops
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: TokenOps control plane and SDK for run-aware agent token governance
5
5
  Author: Susheem Koul, Tisha Chawla
6
6
  License-Expression: MIT
@@ -20,7 +20,7 @@ Classifier: Topic :: System :: Systems Administration
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE.txt
23
- Requires-Dist: agent-chronicle>=0.3.0
23
+ Requires-Dist: agent-chronicle<0.6,>=0.5.0
24
24
  Requires-Dist: openai>=1.0
25
25
  Requires-Dist: anthropic>=0.40
26
26
  Requires-Dist: pyyaml>=6.0
@@ -38,6 +38,8 @@ Requires-Dist: ruff>=0.8; extra == "dev"
38
38
  Requires-Dist: mypy>=1.13; extra == "dev"
39
39
  Requires-Dist: pre-commit>=4.0; extra == "dev"
40
40
  Requires-Dist: types-PyYAML>=6.0; extra == "dev"
41
+ Provides-Extra: contract
42
+ Requires-Dist: agentplane-control-plane>=0.2.0; extra == "contract"
41
43
  Provides-Extra: examples
42
44
  Requires-Dist: ddgs>=9.0; extra == "examples"
43
45
  Requires-Dist: langchain-core>=0.3; extra == "examples"
@@ -100,7 +102,10 @@ add your own.
100
102
 
101
103
  ## 🚀 Quickstart
102
104
 
103
- Requires Python 3.10+.
105
+ Requires Python 3.10+ and a running control plane — TokenOps has no ledger of its
106
+ own, so every agent (even a single process) governs against one. Zero-setup taste:
107
+ `python -m tokenops.demo` launches a throwaway plane for you automatically. For your
108
+ own agent, start one first (see [Quickdeploy](#-quickdeploy) below), then:
104
109
 
105
110
  ### 1. Put it in your agent
106
111
 
@@ -123,14 +128,16 @@ Anywhere else (Cursor, Copilot, ...), paste this:
123
128
  <details>
124
129
  <summary><b>Manual</b>, about ten lines</summary>
125
130
 
126
- Wrap your model call once, then hand the wrapped version to your agent.
131
+ Wrap your model call once, then hand the wrapped version to your agent. Needs
132
+ `CONTROL_PLANE_URL` (or `TOKENOPS_URL`) pointing at a running control plane —
133
+ see [Quickdeploy](#-quickdeploy).
127
134
 
128
135
  ```python
129
136
  from tokenops import ControlPlaneClient, tokenops_run
130
137
  from tokenops.control import Halt, wrap_complete
131
138
  from tokenops.providers import complete
132
139
 
133
- client = ControlPlaneClient.from_env()
140
+ client = ControlPlaneClient.from_env() # raises if CONTROL_PLANE_URL isn't set
134
141
 
135
142
  with tokenops_run(client=client, service="my-agent", intent="research",
136
143
  provider="openai", model="gpt-4o") as bound:
@@ -178,9 +185,9 @@ run is out, even from another process.
178
185
  ## 🐳 Quickdeploy
179
186
 
180
187
  > [!TIP]
181
- > The control plane (`python -m tokenops.server`) shares one budget across
182
- > processes and powers the dashboard. A single-process agent doesn't need it
183
- > running at all.
188
+ > Every agent needs a control plane running — TokenOps has no ledger of its own.
189
+ > A single-process agent still needs one, just not the multi-process sharing this
190
+ > section is about. `python -m tokenops.server` powers the dashboard too.
184
191
 
185
192
  One command, plane + dashboard:
186
193
 
@@ -231,8 +238,8 @@ export TOKENOPS_URL=http://localhost:7700
231
238
  export TOKENOPS_DB=tokenops.db # plane and every agent read the same file
232
239
  ```
233
240
 
234
- > `TOKENOPS_EMBEDDED=1` overrides `TOKENOPS_URL`. Leave it unset here, or each
235
- > process silently falls back to its own local ledger and gets the full budget.
241
+ > tokenops has no ledger of its own — every process governs against the plane at
242
+ > `TOKENOPS_URL`. There is no local-fallback env var to accidentally leave unset.
236
243
 
237
244
  PyPI name is `agent-tokenops`; the import is `tokenops`. Extras:
238
245
  `pip install "agent-tokenops[examples]"` for the LangChain benches,
@@ -289,7 +296,7 @@ flowchart LR
289
296
  | **Control plane** (`python -m tokenops.server`) | `POST /v1/runs`, shared SQLite, Admin/Dashboard | Agent loops, LLM calls, tools |
290
297
  | **SDK (in agents)** | `tokenops_run`, `wrap_complete`, ledger/policies, Chronicle crossing hook | Ad-hoc run IDs; mounting `/v1/runs` when `TOKENOPS_URL` is set |
291
298
 
292
- Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay.
299
+ Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay, accepted at [REALM](https://realm-workshop.github.io/) (Research on Agent Language Models), a workshop at EMNLP 2026.
293
300
 
294
301
  </details>
295
302
 
@@ -298,22 +305,15 @@ Chronicle records decision boundaries; TokenOps attaches as the cost/governance
298
305
 
299
306
  | Variable | Purpose |
300
307
  |---|---|
301
- | `TOKENOPS_URL` | Remote plane base URL (e.g. `http://localhost:7700`) → HTTP `register_run` |
302
- | `TOKENOPS_EMBEDDED` | Set to `1` to force in-process `Store` (tests / single-process) |
303
- | `TOKENOPS_DB` | SQLite path shared by plane + agents |
308
+ | `TOKENOPS_URL` (or `CONTROL_PLANE_URL`) | The control plane's base URL (e.g. `http://localhost:8800`) — **required** |
304
309
  | `TOKENOPS_CONFIG` | YAML for governance seed (core: `src/tokenops/config/default.yaml`) |
305
310
 
306
- `TOKENOPS_URL` also accepts the aliases `CONTROL_PLANE_URL` and
311
+ tokenops has no ledger or run registry of its own — `ControlPlaneClient.from_env()`
312
+ raises if `TOKENOPS_URL`/`CONTROL_PLANE_URL` isn't set, rather than silently falling
313
+ back to anything local. Every process talks to the same plane, so they share one
314
+ budget by construction. `TOKENOPS_URL` also accepts the alias
307
315
  `TOKENOPS_CONTROL_PLANE_URL`.
308
316
 
309
- Production / multi-process: set `TOKENOPS_URL`; agents must **not** mount `/v1/runs`. Tests: `TOKENOPS_EMBEDDED=1` (or omit URL).
310
-
311
- > **Precedence.** `ControlPlaneClient.from_env` takes the HTTP path only when a
312
- > URL is set **and** `TOKENOPS_EMBEDDED` is not `1`. Setting both falls back to a
313
- > local SQLite file with no warning, and every process then gets its own full
314
- > budget. Check with
315
- > `print("embedded" if client.embedded else client.url)`.
316
-
317
317
  </details>
318
318
 
319
319
  <details>
@@ -372,6 +372,7 @@ tests/ # unit + e2e
372
372
  [LinkedIn](https://www.linkedin.com/posts/microsoft-developers_who-spent-all-the-tokens-tokenops-gives-activity-7499191980715982848-224b) and [X](https://x.com/msdev/status/2093425027500978292).
373
373
  - **[Who spent all the tokens? Real-time, run-scoped cost control for AI agents](https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/)**: *Command Line*, a Microsoft publication.
374
374
  - **[FinOps for AI Agents: Who Spent All the Tokens?](https://www.youtube.com/watch?v=GJX19pNhmSw)**: talk at the **AI Engineer World's Fair**, San Francisco.
375
+ - **Selected to participate in the [GitHub Secure Open Source Fund](https://github.com/open-source/github-secure-open-source-fund)** (Session 5), GitHub Security Lab's open source security program.
375
376
 
376
377
  ## 🛟 Support
377
378
 
@@ -53,7 +53,10 @@ add your own.
53
53
 
54
54
  ## 🚀 Quickstart
55
55
 
56
- Requires Python 3.10+.
56
+ Requires Python 3.10+ and a running control plane — TokenOps has no ledger of its
57
+ own, so every agent (even a single process) governs against one. Zero-setup taste:
58
+ `python -m tokenops.demo` launches a throwaway plane for you automatically. For your
59
+ own agent, start one first (see [Quickdeploy](#-quickdeploy) below), then:
57
60
 
58
61
  ### 1. Put it in your agent
59
62
 
@@ -76,14 +79,16 @@ Anywhere else (Cursor, Copilot, ...), paste this:
76
79
  <details>
77
80
  <summary><b>Manual</b>, about ten lines</summary>
78
81
 
79
- Wrap your model call once, then hand the wrapped version to your agent.
82
+ Wrap your model call once, then hand the wrapped version to your agent. Needs
83
+ `CONTROL_PLANE_URL` (or `TOKENOPS_URL`) pointing at a running control plane —
84
+ see [Quickdeploy](#-quickdeploy).
80
85
 
81
86
  ```python
82
87
  from tokenops import ControlPlaneClient, tokenops_run
83
88
  from tokenops.control import Halt, wrap_complete
84
89
  from tokenops.providers import complete
85
90
 
86
- client = ControlPlaneClient.from_env()
91
+ client = ControlPlaneClient.from_env() # raises if CONTROL_PLANE_URL isn't set
87
92
 
88
93
  with tokenops_run(client=client, service="my-agent", intent="research",
89
94
  provider="openai", model="gpt-4o") as bound:
@@ -131,9 +136,9 @@ run is out, even from another process.
131
136
  ## 🐳 Quickdeploy
132
137
 
133
138
  > [!TIP]
134
- > The control plane (`python -m tokenops.server`) shares one budget across
135
- > processes and powers the dashboard. A single-process agent doesn't need it
136
- > running at all.
139
+ > Every agent needs a control plane running — TokenOps has no ledger of its own.
140
+ > A single-process agent still needs one, just not the multi-process sharing this
141
+ > section is about. `python -m tokenops.server` powers the dashboard too.
137
142
 
138
143
  One command, plane + dashboard:
139
144
 
@@ -184,8 +189,8 @@ export TOKENOPS_URL=http://localhost:7700
184
189
  export TOKENOPS_DB=tokenops.db # plane and every agent read the same file
185
190
  ```
186
191
 
187
- > `TOKENOPS_EMBEDDED=1` overrides `TOKENOPS_URL`. Leave it unset here, or each
188
- > process silently falls back to its own local ledger and gets the full budget.
192
+ > tokenops has no ledger of its own — every process governs against the plane at
193
+ > `TOKENOPS_URL`. There is no local-fallback env var to accidentally leave unset.
189
194
 
190
195
  PyPI name is `agent-tokenops`; the import is `tokenops`. Extras:
191
196
  `pip install "agent-tokenops[examples]"` for the LangChain benches,
@@ -242,7 +247,7 @@ flowchart LR
242
247
  | **Control plane** (`python -m tokenops.server`) | `POST /v1/runs`, shared SQLite, Admin/Dashboard | Agent loops, LLM calls, tools |
243
248
  | **SDK (in agents)** | `tokenops_run`, `wrap_complete`, ledger/policies, Chronicle crossing hook | Ad-hoc run IDs; mounting `/v1/runs` when `TOKENOPS_URL` is set |
244
249
 
245
- Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay.
250
+ Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay, accepted at [REALM](https://realm-workshop.github.io/) (Research on Agent Language Models), a workshop at EMNLP 2026.
246
251
 
247
252
  </details>
248
253
 
@@ -251,22 +256,15 @@ Chronicle records decision boundaries; TokenOps attaches as the cost/governance
251
256
 
252
257
  | Variable | Purpose |
253
258
  |---|---|
254
- | `TOKENOPS_URL` | Remote plane base URL (e.g. `http://localhost:7700`) → HTTP `register_run` |
255
- | `TOKENOPS_EMBEDDED` | Set to `1` to force in-process `Store` (tests / single-process) |
256
- | `TOKENOPS_DB` | SQLite path shared by plane + agents |
259
+ | `TOKENOPS_URL` (or `CONTROL_PLANE_URL`) | The control plane's base URL (e.g. `http://localhost:8800`) — **required** |
257
260
  | `TOKENOPS_CONFIG` | YAML for governance seed (core: `src/tokenops/config/default.yaml`) |
258
261
 
259
- `TOKENOPS_URL` also accepts the aliases `CONTROL_PLANE_URL` and
262
+ tokenops has no ledger or run registry of its own — `ControlPlaneClient.from_env()`
263
+ raises if `TOKENOPS_URL`/`CONTROL_PLANE_URL` isn't set, rather than silently falling
264
+ back to anything local. Every process talks to the same plane, so they share one
265
+ budget by construction. `TOKENOPS_URL` also accepts the alias
260
266
  `TOKENOPS_CONTROL_PLANE_URL`.
261
267
 
262
- Production / multi-process: set `TOKENOPS_URL`; agents must **not** mount `/v1/runs`. Tests: `TOKENOPS_EMBEDDED=1` (or omit URL).
263
-
264
- > **Precedence.** `ControlPlaneClient.from_env` takes the HTTP path only when a
265
- > URL is set **and** `TOKENOPS_EMBEDDED` is not `1`. Setting both falls back to a
266
- > local SQLite file with no warning, and every process then gets its own full
267
- > budget. Check with
268
- > `print("embedded" if client.embedded else client.url)`.
269
-
270
268
  </details>
271
269
 
272
270
  <details>
@@ -325,6 +323,7 @@ tests/ # unit + e2e
325
323
  [LinkedIn](https://www.linkedin.com/posts/microsoft-developers_who-spent-all-the-tokens-tokenops-gives-activity-7499191980715982848-224b) and [X](https://x.com/msdev/status/2093425027500978292).
326
324
  - **[Who spent all the tokens? Real-time, run-scoped cost control for AI agents](https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/)**: *Command Line*, a Microsoft publication.
327
325
  - **[FinOps for AI Agents: Who Spent All the Tokens?](https://www.youtube.com/watch?v=GJX19pNhmSw)**: talk at the **AI Engineer World's Fair**, San Francisco.
326
+ - **Selected to participate in the [GitHub Secure Open Source Fund](https://github.com/open-source/github-secure-open-source-fund)** (Session 5), GitHub Security Lab's open source security program.
328
327
 
329
328
  ## 🛟 Support
330
329
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-tokenops"
7
- version = "0.2.1"
7
+ version = "0.4.0"
8
8
  description = "TokenOps control plane and SDK for run-aware agent token governance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -34,7 +34,7 @@ classifiers = [
34
34
  "Topic :: System :: Systems Administration",
35
35
  ]
36
36
  dependencies = [
37
- "agent-chronicle>=0.3.0",
37
+ "agent-chronicle>=0.5.0,<0.6",
38
38
  "openai>=1.0",
39
39
  "anthropic>=0.40",
40
40
  "pyyaml>=6.0",
@@ -56,6 +56,14 @@ dev = [
56
56
  "pre-commit>=4.0",
57
57
  "types-PyYAML>=6.0",
58
58
  ]
59
+ # The verified-fake contract suite + e2e run against a real control plane in-process.
60
+ # Not in `dev` because the 0.2.0 line is not on PyPI yet — the tests that need it
61
+ # `pytest.importorskip("control_plane")`, so `[dev]` alone stays green. To run them:
62
+ # pip install -e ".[dev,contract]" # once 0.2.0 is released, or
63
+ # pip install "agentplane-control-plane @ git+https://github.com/theagentplane/control-plane@main"
64
+ contract = [
65
+ "agentplane-control-plane>=0.2.0",
66
+ ]
59
67
  examples = [
60
68
  "ddgs>=9.0",
61
69
  "langchain-core>=0.3",
@@ -74,10 +82,13 @@ testpaths = ["tests"]
74
82
  pythonpath = ["src", "."]
75
83
  asyncio_mode = "strict"
76
84
  markers = [
77
- "e2e: example / bench end-to-end tests (not run in default CI)",
78
- "live: requires API keys or vendored frameworks",
85
+ "e2e: example / bench end-to-end tests — real servers, real Governor/policy",
86
+ " enforcement, faked model+network only. Part of the default suite (see",
87
+ " addopts) precisely so real run+policy regressions are caught.",
88
+ "live: requires real API keys or vendored frameworks — self-skips without them,",
89
+ " excluded from the default run since it needs secrets to do anything.",
79
90
  ]
80
- addopts = "-m 'not e2e and not live'"
91
+ addopts = "-m 'not live'"
81
92
 
82
93
  [tool.ruff]
83
94
  target-version = "py310"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-tokenops
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: TokenOps control plane and SDK for run-aware agent token governance
5
5
  Author: Susheem Koul, Tisha Chawla
6
6
  License-Expression: MIT
@@ -20,7 +20,7 @@ Classifier: Topic :: System :: Systems Administration
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE.txt
23
- Requires-Dist: agent-chronicle>=0.3.0
23
+ Requires-Dist: agent-chronicle<0.6,>=0.5.0
24
24
  Requires-Dist: openai>=1.0
25
25
  Requires-Dist: anthropic>=0.40
26
26
  Requires-Dist: pyyaml>=6.0
@@ -38,6 +38,8 @@ Requires-Dist: ruff>=0.8; extra == "dev"
38
38
  Requires-Dist: mypy>=1.13; extra == "dev"
39
39
  Requires-Dist: pre-commit>=4.0; extra == "dev"
40
40
  Requires-Dist: types-PyYAML>=6.0; extra == "dev"
41
+ Provides-Extra: contract
42
+ Requires-Dist: agentplane-control-plane>=0.2.0; extra == "contract"
41
43
  Provides-Extra: examples
42
44
  Requires-Dist: ddgs>=9.0; extra == "examples"
43
45
  Requires-Dist: langchain-core>=0.3; extra == "examples"
@@ -100,7 +102,10 @@ add your own.
100
102
 
101
103
  ## 🚀 Quickstart
102
104
 
103
- Requires Python 3.10+.
105
+ Requires Python 3.10+ and a running control plane — TokenOps has no ledger of its
106
+ own, so every agent (even a single process) governs against one. Zero-setup taste:
107
+ `python -m tokenops.demo` launches a throwaway plane for you automatically. For your
108
+ own agent, start one first (see [Quickdeploy](#-quickdeploy) below), then:
104
109
 
105
110
  ### 1. Put it in your agent
106
111
 
@@ -123,14 +128,16 @@ Anywhere else (Cursor, Copilot, ...), paste this:
123
128
  <details>
124
129
  <summary><b>Manual</b>, about ten lines</summary>
125
130
 
126
- Wrap your model call once, then hand the wrapped version to your agent.
131
+ Wrap your model call once, then hand the wrapped version to your agent. Needs
132
+ `CONTROL_PLANE_URL` (or `TOKENOPS_URL`) pointing at a running control plane —
133
+ see [Quickdeploy](#-quickdeploy).
127
134
 
128
135
  ```python
129
136
  from tokenops import ControlPlaneClient, tokenops_run
130
137
  from tokenops.control import Halt, wrap_complete
131
138
  from tokenops.providers import complete
132
139
 
133
- client = ControlPlaneClient.from_env()
140
+ client = ControlPlaneClient.from_env() # raises if CONTROL_PLANE_URL isn't set
134
141
 
135
142
  with tokenops_run(client=client, service="my-agent", intent="research",
136
143
  provider="openai", model="gpt-4o") as bound:
@@ -178,9 +185,9 @@ run is out, even from another process.
178
185
  ## 🐳 Quickdeploy
179
186
 
180
187
  > [!TIP]
181
- > The control plane (`python -m tokenops.server`) shares one budget across
182
- > processes and powers the dashboard. A single-process agent doesn't need it
183
- > running at all.
188
+ > Every agent needs a control plane running — TokenOps has no ledger of its own.
189
+ > A single-process agent still needs one, just not the multi-process sharing this
190
+ > section is about. `python -m tokenops.server` powers the dashboard too.
184
191
 
185
192
  One command, plane + dashboard:
186
193
 
@@ -231,8 +238,8 @@ export TOKENOPS_URL=http://localhost:7700
231
238
  export TOKENOPS_DB=tokenops.db # plane and every agent read the same file
232
239
  ```
233
240
 
234
- > `TOKENOPS_EMBEDDED=1` overrides `TOKENOPS_URL`. Leave it unset here, or each
235
- > process silently falls back to its own local ledger and gets the full budget.
241
+ > tokenops has no ledger of its own — every process governs against the plane at
242
+ > `TOKENOPS_URL`. There is no local-fallback env var to accidentally leave unset.
236
243
 
237
244
  PyPI name is `agent-tokenops`; the import is `tokenops`. Extras:
238
245
  `pip install "agent-tokenops[examples]"` for the LangChain benches,
@@ -289,7 +296,7 @@ flowchart LR
289
296
  | **Control plane** (`python -m tokenops.server`) | `POST /v1/runs`, shared SQLite, Admin/Dashboard | Agent loops, LLM calls, tools |
290
297
  | **SDK (in agents)** | `tokenops_run`, `wrap_complete`, ledger/policies, Chronicle crossing hook | Ad-hoc run IDs; mounting `/v1/runs` when `TOKENOPS_URL` is set |
291
298
 
292
- Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay.
299
+ Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay, accepted at [REALM](https://realm-workshop.github.io/) (Research on Agent Language Models), a workshop at EMNLP 2026.
293
300
 
294
301
  </details>
295
302
 
@@ -298,22 +305,15 @@ Chronicle records decision boundaries; TokenOps attaches as the cost/governance
298
305
 
299
306
  | Variable | Purpose |
300
307
  |---|---|
301
- | `TOKENOPS_URL` | Remote plane base URL (e.g. `http://localhost:7700`) → HTTP `register_run` |
302
- | `TOKENOPS_EMBEDDED` | Set to `1` to force in-process `Store` (tests / single-process) |
303
- | `TOKENOPS_DB` | SQLite path shared by plane + agents |
308
+ | `TOKENOPS_URL` (or `CONTROL_PLANE_URL`) | The control plane's base URL (e.g. `http://localhost:8800`) — **required** |
304
309
  | `TOKENOPS_CONFIG` | YAML for governance seed (core: `src/tokenops/config/default.yaml`) |
305
310
 
306
- `TOKENOPS_URL` also accepts the aliases `CONTROL_PLANE_URL` and
311
+ tokenops has no ledger or run registry of its own — `ControlPlaneClient.from_env()`
312
+ raises if `TOKENOPS_URL`/`CONTROL_PLANE_URL` isn't set, rather than silently falling
313
+ back to anything local. Every process talks to the same plane, so they share one
314
+ budget by construction. `TOKENOPS_URL` also accepts the alias
307
315
  `TOKENOPS_CONTROL_PLANE_URL`.
308
316
 
309
- Production / multi-process: set `TOKENOPS_URL`; agents must **not** mount `/v1/runs`. Tests: `TOKENOPS_EMBEDDED=1` (or omit URL).
310
-
311
- > **Precedence.** `ControlPlaneClient.from_env` takes the HTTP path only when a
312
- > URL is set **and** `TOKENOPS_EMBEDDED` is not `1`. Setting both falls back to a
313
- > local SQLite file with no warning, and every process then gets its own full
314
- > budget. Check with
315
- > `print("embedded" if client.embedded else client.url)`.
316
-
317
317
  </details>
318
318
 
319
319
  <details>
@@ -372,6 +372,7 @@ tests/ # unit + e2e
372
372
  [LinkedIn](https://www.linkedin.com/posts/microsoft-developers_who-spent-all-the-tokens-tokenops-gives-activity-7499191980715982848-224b) and [X](https://x.com/msdev/status/2093425027500978292).
373
373
  - **[Who spent all the tokens? Real-time, run-scoped cost control for AI agents](https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/)**: *Command Line*, a Microsoft publication.
374
374
  - **[FinOps for AI Agents: Who Spent All the Tokens?](https://www.youtube.com/watch?v=GJX19pNhmSw)**: talk at the **AI Engineer World's Fair**, San Francisco.
375
+ - **Selected to participate in the [GitHub Secure Open Source Fund](https://github.com/open-source/github-secure-open-source-fund)** (Session 5), GitHub Security Lab's open source security program.
375
376
 
376
377
  ## 🛟 Support
377
378
 
@@ -22,6 +22,7 @@ src/tokenops/control/config.py
22
22
  src/tokenops/control/context.py
23
23
  src/tokenops/control/core.py
24
24
  src/tokenops/control/crossing.py
25
+ src/tokenops/control/dev_plane.py
25
26
  src/tokenops/control/engine.py
26
27
  src/tokenops/control/governance_cache.py
27
28
  src/tokenops/control/http.py
@@ -29,12 +30,14 @@ src/tokenops/control/http_store.py
29
30
  src/tokenops/control/instrument.py
30
31
  src/tokenops/control/integration.py
31
32
  src/tokenops/control/ledger.py
33
+ src/tokenops/control/ledger_backend.py
32
34
  src/tokenops/control/models.py
33
35
  src/tokenops/control/pricing.py
34
36
  src/tokenops/control/propagate.py
35
37
  src/tokenops/control/request_context.py
36
38
  src/tokenops/control/run.py
37
39
  src/tokenops/control/store.py
40
+ src/tokenops/control/usage.py
38
41
  src/tokenops/control/policies/__init__.py
39
42
  src/tokenops/control/policies/_util.py
40
43
  src/tokenops/control/policies/concurrency_cap.py
@@ -45,6 +48,7 @@ src/tokenops/control/policies/output_runaway.py
45
48
  src/tokenops/control/policies/pre_call_worst_case.py
46
49
  src/tokenops/control/policies/progress_guard.py
47
50
  src/tokenops/control/policies/step_cap.py
51
+ src/tokenops/control/policies/time_budget.py
48
52
  src/tokenops/control/policies/tool_fix.py
49
53
  src/tokenops/control/policies/tool_output_cap.py
50
54
  src/tokenops/control/policies/trajectory_hint.py
@@ -66,6 +70,7 @@ src/tokenops/server/__main__.py
66
70
  src/tokenops/server/app.py
67
71
  src/tokenops/ui/__init__.py
68
72
  src/tokenops/ui/app.py
73
+ src/tokenops/ui/policy_labels.py
69
74
  src/tokenops/ui/run_detail.py
70
75
  src/tokenops/ui/store_client.py
71
76
  src/tokenops/ui/theme.py
@@ -89,28 +94,36 @@ tests/test_cost_guard.py
89
94
  tests/test_cross_process_budget_gating.py
90
95
  tests/test_crossing_hook.py
91
96
  tests/test_demo.py
97
+ tests/test_demo_policy_names.py
92
98
  tests/test_governance_config_cache.py
99
+ tests/test_governance_event_names.py
93
100
  tests/test_governance_per_agent.py
94
101
  tests/test_integration.py
95
102
  tests/test_ledger.py
103
+ tests/test_ledger_backend_contract.py
104
+ tests/test_ledger_backend_mode.py
96
105
  tests/test_llm_boundary_precall.py
97
106
  tests/test_output_runaway.py
98
107
  tests/test_policies_wrap_integration.py
99
108
  tests/test_policy_coordination.py
109
+ tests/test_policy_names.py
100
110
  tests/test_pre_call_worst_case.py
101
111
  tests/test_preview_mode.py
102
112
  tests/test_pricing.py
103
113
  tests/test_progress_guard.py
104
114
  tests/test_propagate.py
115
+ tests/test_provider_usage.py
105
116
  tests/test_reason_encoding.py
106
117
  tests/test_server_enforcement.py
107
118
  tests/test_step_cap.py
108
119
  tests/test_store.py
109
120
  tests/test_thread_safety.py
121
+ tests/test_time_budget.py
110
122
  tests/test_tokenops_run.py
111
123
  tests/test_tool_fix.py
112
124
  tests/test_tool_output_cap.py
113
125
  tests/test_trajectory_hint.py
114
126
  tests/test_trajectory_hint_e2e.py
127
+ tests/test_ui_policy_names.py
115
128
  tests/test_wrap_adapters.py
116
129
  tests/test_wrap_complete_chronicle.py
@@ -1,4 +1,4 @@
1
- agent-chronicle>=0.3.0
1
+ agent-chronicle<0.6,>=0.5.0
2
2
  openai>=1.0
3
3
  anthropic>=0.40
4
4
  pyyaml>=6.0
@@ -8,6 +8,9 @@ httpx>=0.27
8
8
  fastapi>=0.115
9
9
  uvicorn>=0.32
10
10
 
11
+ [contract]
12
+ agentplane-control-plane>=0.2.0
13
+
11
14
  [dev]
12
15
  pytest>=8.0
13
16
  pytest-asyncio>=0.24
@@ -4,7 +4,7 @@ from tokenops.env import load_env
4
4
 
5
5
  load_env()
6
6
 
7
- __version__ = "0.2.1"
7
+ __version__ = "0.4.0"
8
8
 
9
9
 
10
10
  def init() -> None:
@@ -1,5 +1,6 @@
1
1
  # TokenOps control plane seed. Consumed by tokenops.control.build_governor / Store auto-seed.
2
2
  # Budgets are spend caps bound to a segment; policies are the (detect, fix) templates.
3
+ # Policy keys are canonical IDs, not display labels or instance IDs. See docs/product/policies-index.md.
3
4
  governance:
4
5
  budgets:
5
6
  - id: run_llm_cap
@@ -31,7 +32,6 @@ governance:
31
32
  mode: minimize
32
33
  context_compaction:
33
34
  ctx_max: 100000
34
- has_hook: false
35
35
  output_runaway:
36
36
  repeats: 4
37
37
  max_retries: 2
@@ -3,14 +3,9 @@
3
3
  The Account (ledger) + Enforce (policies) half of the control plane. Consumes
4
4
  attributed boundary crossings, records them, and decides: steer or stop.
5
5
 
6
- Ten policies (delegation_cap dropped — fan-out is concurrency_cap, spend is
7
- cost_budget / pre_call_worst_case), evaluated by moment:
8
- pre_call : concurrency_cap, tool_fix, context_compaction, cost_guard, pre_call_worst_case
9
- stream : output_runaway
10
- observe : cost_budget, step_cap, tool_output_cap, progress_guard
11
-
12
- Source of truth: docs/policies/ and docs/governance-policy.md. ``core.py`` is the
13
- canonical vocabulary.
6
+ Built-in IDs and display labels live in ``config.POLICY_TEMPLATES``; see the single
7
+ glossary at ``docs/product/policies-index.md`` and behavior docs at ``docs/policies/``.
8
+ ``core.py`` is the canonical runtime vocabulary.
14
9
  """
15
10
 
16
11
  from tokenops.control.attribution import (
@@ -62,7 +57,15 @@ from tokenops.control.integration import (
62
57
  wrap_complete,
63
58
  wrap_stream,
64
59
  )
65
- from tokenops.control.ledger import Budget, Ledger, RunState, segment_key
60
+ from tokenops.control.ledger import Budget, Ledger, LocalRunState, segment_key
61
+ from tokenops.control.ledger_backend import (
62
+ AggregateState,
63
+ ApplyResult,
64
+ HttpLedgerBackend,
65
+ LedgerBackend,
66
+ LedgerEvent,
67
+ PrecheckRequest,
68
+ )
66
69
 
67
70
  # Process-wide: re-attach after every reset_session (Chronicle clears on_crossing).
68
71
  install_crossing_hook()
@@ -104,8 +107,15 @@ __all__ = [
104
107
  # ledger
105
108
  "Budget",
106
109
  "Ledger",
107
- "RunState",
110
+ "LocalRunState",
108
111
  "segment_key",
112
+ # ledger backend (remote-only)
113
+ "LedgerBackend",
114
+ "HttpLedgerBackend",
115
+ "LedgerEvent",
116
+ "PrecheckRequest",
117
+ "AggregateState",
118
+ "ApplyResult",
109
119
  # harness
110
120
  "Governor",
111
121
  "RaiseControls",
@@ -15,6 +15,7 @@ from typing import Any
15
15
  from tokenops.control.attribution import _build_attribution, require_registration
16
16
  from tokenops.control.context import current_governance, current_span
17
17
  from tokenops.control.core import NodeType, Observation, Usage
18
+ from tokenops.control.usage import usage_from_counts
18
19
 
19
20
  _KIND_MAP: dict[str, NodeType] = {
20
21
  "llm": "llm",
@@ -55,6 +56,7 @@ def observation_from_crossing(
55
56
  model: str = "",
56
57
  ts: float | None = None,
57
58
  extra_tags: Mapping[str, str] | None = None,
59
+ compaction: Mapping[str, int] | None = None,
58
60
  ) -> Observation:
59
61
  reg = require_registration()
60
62
  attr = _build_attribution(reg, service=service)
@@ -73,24 +75,9 @@ def observation_from_crossing(
73
75
 
74
76
  if node_type == "llm":
75
77
  usage_obj = getattr(result, "usage", None)
76
- if usage_obj is not None:
77
- usage = Usage(
78
- input=int(
79
- getattr(usage_obj, "prompt_tokens", 0)
80
- or getattr(usage_obj, "input_tokens", 0)
81
- or 0
82
- ),
83
- output=int(
84
- getattr(usage_obj, "completion_tokens", 0)
85
- or getattr(usage_obj, "output_tokens", 0)
86
- or 0
87
- ),
88
- )
89
- else:
90
- usage = Usage(
91
- input=int(getattr(result, "input_tokens", 0) or 0),
92
- output=int(getattr(result, "output_tokens", 0) or 0),
93
- )
78
+ usage = usage_from_counts(
79
+ usage_obj if usage_obj is not None else result, native=usage_obj is not None
80
+ )
94
81
  text = getattr(result, "content", None)
95
82
  if text is None and hasattr(result, "completion"):
96
83
  text = getattr(result, "completion", result)
@@ -116,7 +103,7 @@ def observation_from_crossing(
116
103
  raw_roll = getattr(result, "rolled_up_cost_micros", None)
117
104
  if raw_roll is None:
118
105
  raw_roll = input_state.get("rolled_up_cost_micros", 0)
119
- rolled_up = int(raw_roll) if isinstance(raw_roll, (int, float, str)) else 0
106
+ rolled_up = int(raw_roll) if isinstance(raw_roll, int | float | str) else 0
120
107
  output = dict(result) if isinstance(result, dict) else {"result": str(result)}
121
108
 
122
109
  return Observation(
@@ -133,6 +120,7 @@ def observation_from_crossing(
133
120
  result_hash=result_hash,
134
121
  rolled_up_cost_micros=rolled_up,
135
122
  boundary_tags=tags,
123
+ compaction=compaction,
136
124
  **_span_fields(service),
137
125
  )
138
126