agent-tokenops 0.2.1__tar.gz → 0.3.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.
- {agent_tokenops-0.2.1/src/agent_tokenops.egg-info → agent_tokenops-0.3.0}/PKG-INFO +23 -22
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/README.md +20 -21
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/pyproject.toml +15 -4
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0/src/agent_tokenops.egg-info}/PKG-INFO +23 -22
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/SOURCES.txt +4 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/requires.txt +3 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/__init__.py +1 -1
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/__init__.py +17 -2
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/client.py +69 -32
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/config.py +32 -11
- agent_tokenops-0.3.0/src/tokenops/control/dev_plane.py +70 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/http_store.py +12 -3
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/ledger.py +230 -27
- agent_tokenops-0.3.0/src/tokenops/control/ledger_backend.py +277 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/models.py +11 -1
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/run.py +18 -3
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/store.py +17 -3
- agent_tokenops-0.3.0/src/tokenops/demo.py +194 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_control_plane_app.py +16 -11
- agent_tokenops-0.3.0/tests/test_control_plane_client.py +65 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_demo.py +9 -8
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_governance_config_cache.py +0 -4
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_ledger.py +40 -0
- agent_tokenops-0.3.0/tests/test_ledger_backend_contract.py +142 -0
- agent_tokenops-0.3.0/tests/test_ledger_backend_mode.py +163 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_policies_wrap_integration.py +4 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_tokenops_run.py +22 -14
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_trajectory_hint.py +12 -1
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_trajectory_hint_e2e.py +10 -1
- agent_tokenops-0.2.1/src/tokenops/demo.py +0 -113
- agent_tokenops-0.2.1/tests/test_control_plane_client.py +0 -88
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/LICENSE.txt +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/setup.cfg +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/dependency_links.txt +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/top_level.txt +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/adapters/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/adapters/langchain.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/config/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/config/default.yaml +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/config/loader.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/attribution.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/boundary.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/context.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/core.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/crossing.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/engine.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/governance_cache.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/http.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/instrument.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/integration.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/_util.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/concurrency_cap.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/context_compaction.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/cost_budget.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/cost_guard.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/output_runaway.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/pre_call_worst_case.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/progress_guard.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/step_cap.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/tool_fix.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/tool_output_cap.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/trajectory_hint.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/pricing.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/propagate.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/request_context.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/compress.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/enqueue.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/gates.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/hint.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/scope.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/serialize.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/worker.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/env.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/anthropic.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/factory.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/openai.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/types.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/server/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/server/__main__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/server/app.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/app.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/run_detail.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/store_client.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/theme.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/views/__init__.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/views/admin.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/views/dashboard.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_a2a_health.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_actuators.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_apply.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_attribution.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_attribution_ledger_policies_e2e.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_boundary.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_chronicle_boundary.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_concurrency_cap.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_config.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_context_compaction.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_cost_budget.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_cost_guard.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_cross_process_budget_gating.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_crossing_hook.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_governance_per_agent.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_integration.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_llm_boundary_precall.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_output_runaway.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_policy_coordination.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_pre_call_worst_case.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_preview_mode.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_pricing.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_progress_guard.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_propagate.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_reason_encoding.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_server_enforcement.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_step_cap.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_store.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_thread_safety.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_tool_fix.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_tool_output_cap.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_wrap_adapters.py +0 -0
- {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_wrap_complete_chronicle.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-tokenops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -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
|
-
>
|
|
182
|
-
>
|
|
183
|
-
>
|
|
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
|
-
>
|
|
235
|
-
>
|
|
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` |
|
|
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
|
-
|
|
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
|
-
>
|
|
135
|
-
>
|
|
136
|
-
>
|
|
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
|
-
>
|
|
188
|
-
>
|
|
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` |
|
|
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
|
-
|
|
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.
|
|
7
|
+
version = "0.3.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"
|
|
@@ -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
|
|
78
|
-
"
|
|
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
|
|
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.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -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
|
-
>
|
|
182
|
-
>
|
|
183
|
-
>
|
|
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
|
-
>
|
|
235
|
-
>
|
|
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` |
|
|
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
|
-
|
|
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,6 +30,7 @@ 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
|
|
@@ -93,6 +95,8 @@ tests/test_governance_config_cache.py
|
|
|
93
95
|
tests/test_governance_per_agent.py
|
|
94
96
|
tests/test_integration.py
|
|
95
97
|
tests/test_ledger.py
|
|
98
|
+
tests/test_ledger_backend_contract.py
|
|
99
|
+
tests/test_ledger_backend_mode.py
|
|
96
100
|
tests/test_llm_boundary_precall.py
|
|
97
101
|
tests/test_output_runaway.py
|
|
98
102
|
tests/test_policies_wrap_integration.py
|
|
@@ -62,7 +62,15 @@ from tokenops.control.integration import (
|
|
|
62
62
|
wrap_complete,
|
|
63
63
|
wrap_stream,
|
|
64
64
|
)
|
|
65
|
-
from tokenops.control.ledger import Budget, Ledger,
|
|
65
|
+
from tokenops.control.ledger import Budget, Ledger, LocalRunState, segment_key
|
|
66
|
+
from tokenops.control.ledger_backend import (
|
|
67
|
+
AggregateState,
|
|
68
|
+
ApplyResult,
|
|
69
|
+
HttpLedgerBackend,
|
|
70
|
+
LedgerBackend,
|
|
71
|
+
LedgerEvent,
|
|
72
|
+
PrecheckRequest,
|
|
73
|
+
)
|
|
66
74
|
|
|
67
75
|
# Process-wide: re-attach after every reset_session (Chronicle clears on_crossing).
|
|
68
76
|
install_crossing_hook()
|
|
@@ -104,8 +112,15 @@ __all__ = [
|
|
|
104
112
|
# ledger
|
|
105
113
|
"Budget",
|
|
106
114
|
"Ledger",
|
|
107
|
-
"
|
|
115
|
+
"LocalRunState",
|
|
108
116
|
"segment_key",
|
|
117
|
+
# ledger backend (remote-only)
|
|
118
|
+
"LedgerBackend",
|
|
119
|
+
"HttpLedgerBackend",
|
|
120
|
+
"LedgerEvent",
|
|
121
|
+
"PrecheckRequest",
|
|
122
|
+
"AggregateState",
|
|
123
|
+
"ApplyResult",
|
|
109
124
|
# harness
|
|
110
125
|
"Governor",
|
|
111
126
|
"RaiseControls",
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"""SDK client for the TokenOps control plane.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
``ControlPlaneClient`` is the only supported way a live agent reaches the plane —
|
|
4
|
+
``from_env()`` requires ``CONTROL_PLANE_URL`` / ``TOKENOPS_URL`` and raises if neither
|
|
5
|
+
is set. There is no embedded / local-file fallback: tokenops has no ledger of its own
|
|
6
|
+
(tokenops#118, the remote-only rewrite) — every spend/inflight/halt read or write goes
|
|
7
|
+
to the control plane over HTTP via :meth:`backend` (a
|
|
8
|
+
:class:`~tokenops.control.ledger_backend.LedgerBackend`), and registration/governance
|
|
9
|
+
config/dashboard rows go through :class:`~tokenops.control.http_store.HttpStore`. Both
|
|
10
|
+
are HTTP-only; neither ever opens a local SQLite file.
|
|
11
|
+
|
|
12
|
+
The ``store=`` constructor kwarg is a explicit, visible test-only escape hatch (dependency
|
|
13
|
+
injection for unit/integration tests that want to exercise policy logic against a local
|
|
14
|
+
:class:`~tokenops.control.store.Store` without a running plane) — it is never reachable
|
|
15
|
+
from ``from_env()``, so no environment misconfiguration can silently select it.
|
|
11
16
|
"""
|
|
12
17
|
|
|
13
18
|
from __future__ import annotations
|
|
@@ -17,8 +22,11 @@ import os
|
|
|
17
22
|
from collections.abc import Mapping
|
|
18
23
|
from typing import Any, cast
|
|
19
24
|
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
20
27
|
from tokenops.control.http import post_run, post_run_sync
|
|
21
28
|
from tokenops.control.http_store import HttpStore
|
|
29
|
+
from tokenops.control.ledger_backend import HttpLedgerBackend, LedgerBackend
|
|
22
30
|
from tokenops.control.models import (
|
|
23
31
|
GovernanceMode,
|
|
24
32
|
RunAlreadyRegisteredError,
|
|
@@ -46,7 +54,7 @@ def _mode_value(mode: GovernanceMode | str | None) -> GovernanceMode:
|
|
|
46
54
|
|
|
47
55
|
|
|
48
56
|
class ControlPlaneClient:
|
|
49
|
-
"""Talk to
|
|
57
|
+
"""Talk to the remote control plane over HTTP (or, in tests only, a local Store)."""
|
|
50
58
|
|
|
51
59
|
def __init__(
|
|
52
60
|
self,
|
|
@@ -54,36 +62,43 @@ class ControlPlaneClient:
|
|
|
54
62
|
url: str | None = None,
|
|
55
63
|
store: Store | None = None,
|
|
56
64
|
timeout: float = 30.0,
|
|
65
|
+
client: httpx.Client | None = None,
|
|
57
66
|
) -> None:
|
|
58
67
|
if bool(url) == bool(store):
|
|
59
68
|
raise ValueError("exactly one of url or store is required")
|
|
60
69
|
self._url = url.rstrip("/") if url else None
|
|
61
70
|
self._store = store
|
|
62
71
|
self._hybrid_store: Any = None
|
|
72
|
+
self._backend: LedgerBackend | None = None
|
|
63
73
|
self._timeout = timeout
|
|
74
|
+
self._client = client # optional injected httpx.Client — tests only
|
|
64
75
|
|
|
65
76
|
@classmethod
|
|
66
77
|
def from_env(cls, *, timeout: float = 30.0) -> ControlPlaneClient:
|
|
67
|
-
"""Build a client from ``
|
|
78
|
+
"""Build a client from ``CONTROL_PLANE_URL`` / ``TOKENOPS_URL``.
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
Raises if neither is set — tokenops has no embedded / local-file ledger to fall
|
|
81
|
+
back to (tokenops#118). Point this at a running control plane: ``control-plane
|
|
82
|
+
serve`` locally, or the Docker image, for anything other than tests that pass
|
|
83
|
+
``store=`` explicitly.
|
|
72
84
|
"""
|
|
73
85
|
from tokenops.control.crossing import install_crossing_hook
|
|
74
86
|
|
|
75
87
|
install_crossing_hook()
|
|
76
|
-
embedded = os.environ.get("TOKENOPS_EMBEDDED", "").strip() == "1"
|
|
77
88
|
url = (
|
|
78
89
|
os.environ.get("CONTROL_PLANE_URL")
|
|
79
90
|
or os.environ.get("TOKENOPS_URL")
|
|
80
91
|
or os.environ.get("TOKENOPS_CONTROL_PLANE_URL")
|
|
81
92
|
or ""
|
|
82
93
|
).strip()
|
|
83
|
-
if
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
if not url:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
"CONTROL_PLANE_URL (or TOKENOPS_URL) is required — tokenops has no "
|
|
97
|
+
"embedded/local ledger; point it at a running control plane. Run "
|
|
98
|
+
"`control-plane serve` locally (see the agentplane-control-plane "
|
|
99
|
+
"package) or use its Docker image, then set CONTROL_PLANE_URL to it."
|
|
100
|
+
)
|
|
101
|
+
return cls(url=url, timeout=timeout)
|
|
87
102
|
|
|
88
103
|
@property
|
|
89
104
|
def url(self) -> str | None:
|
|
@@ -91,29 +106,51 @@ class ControlPlaneClient:
|
|
|
91
106
|
|
|
92
107
|
@property
|
|
93
108
|
def embedded(self) -> bool:
|
|
109
|
+
"""``True`` only for the explicit test-only ``store=`` constructor path.
|
|
110
|
+
|
|
111
|
+
Never ``True`` for a client built by :meth:`from_env` — there is no
|
|
112
|
+
environment variable that selects a local ledger.
|
|
113
|
+
"""
|
|
94
114
|
return self._store is not None
|
|
95
115
|
|
|
96
116
|
@property
|
|
97
117
|
def store(self) -> Store | None:
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
:meth:`require_store` over using this directly. Escape hatch only (§6).
|
|
102
|
-
"""
|
|
118
|
+
"""The explicit test-only local Store, or ``None`` in the (only production)
|
|
119
|
+
remote mode. Prefer :meth:`backend`, :meth:`governance_config_for`, and
|
|
120
|
+
:meth:`resolve_run` over using this directly."""
|
|
103
121
|
return self._store
|
|
104
122
|
|
|
123
|
+
@property
|
|
124
|
+
def backend(self) -> LedgerBackend:
|
|
125
|
+
"""The :class:`~tokenops.control.ledger_backend.LedgerBackend` for this client
|
|
126
|
+
— every spend/inflight/halt read (``precheck``) and write (``events:batch``)
|
|
127
|
+
goes through this. Only valid in remote mode (``from_env()`` always is)."""
|
|
128
|
+
if self._url is None:
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
"no LedgerBackend for a store=-constructed (test-only) ControlPlaneClient"
|
|
131
|
+
)
|
|
132
|
+
if self._backend is None:
|
|
133
|
+
key = os.environ.get("CONTROL_PLANE_API_KEY") or os.environ.get("TOKENOPS_API_KEY")
|
|
134
|
+
self._backend = HttpLedgerBackend(
|
|
135
|
+
self._url, api_key=key, timeout=self._timeout, client=self._client
|
|
136
|
+
)
|
|
137
|
+
return self._backend
|
|
138
|
+
|
|
105
139
|
def require_store(self) -> Store:
|
|
106
|
-
"""
|
|
140
|
+
"""Registration / governance-config / dashboard-row transport.
|
|
107
141
|
|
|
108
|
-
|
|
109
|
-
:class:`HttpStore` — never a local DB file.
|
|
142
|
+
The test-only ``store=`` path returns that Store. Remote mode (``from_env()``)
|
|
143
|
+
returns :class:`HttpStore` — HTTP only, never a local DB file. Ledger accounting
|
|
144
|
+
(spend/inflight/halt) does **not** go through this — see :meth:`backend`.
|
|
110
145
|
"""
|
|
111
146
|
if self._store is not None:
|
|
112
147
|
return self._store
|
|
113
148
|
if self._hybrid_store is None:
|
|
114
149
|
assert self._url is not None
|
|
115
150
|
key = os.environ.get("CONTROL_PLANE_API_KEY") or os.environ.get("TOKENOPS_API_KEY")
|
|
116
|
-
self._hybrid_store = HttpStore(
|
|
151
|
+
self._hybrid_store = HttpStore(
|
|
152
|
+
self._url, api_key=key, timeout=self._timeout, client=self._client
|
|
153
|
+
)
|
|
117
154
|
return cast(Store, self._hybrid_store)
|
|
118
155
|
|
|
119
156
|
def register_run(
|
|
@@ -203,9 +240,9 @@ class ControlPlaneClient:
|
|
|
203
240
|
def should_mount_run_registration() -> bool:
|
|
204
241
|
"""Whether an agent app should expose ``POST /v1/runs``.
|
|
205
242
|
|
|
206
|
-
|
|
207
|
-
|
|
243
|
+
Always ``False`` — registration is centralized on the plane (tokenops#118); there
|
|
244
|
+
is no embedded mode left for an agent to self-host it under. Kept (rather than
|
|
245
|
+
deleted outright) so existing ``if should_mount_run_registration(): ...`` call
|
|
246
|
+
sites keep working unchanged while they're cleaned up.
|
|
208
247
|
"""
|
|
209
|
-
|
|
210
|
-
return True
|
|
211
|
-
return not (os.environ.get("TOKENOPS_URL") or "").strip()
|
|
248
|
+
return False
|