agent-tokenops 0.3.0__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 (133) hide show
  1. {agent_tokenops-0.3.0/src/agent_tokenops.egg-info → agent_tokenops-0.4.0}/PKG-INFO +2 -2
  2. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/pyproject.toml +2 -2
  3. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0/src/agent_tokenops.egg-info}/PKG-INFO +2 -2
  4. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/SOURCES.txt +9 -0
  5. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/requires.txt +1 -1
  6. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/__init__.py +1 -1
  7. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/config/default.yaml +1 -1
  8. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/__init__.py +3 -8
  9. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/boundary.py +7 -19
  10. agent_tokenops-0.4.0/src/tokenops/control/config.py +279 -0
  11. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/context.py +17 -0
  12. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/core.py +18 -7
  13. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/crossing.py +21 -26
  14. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/engine.py +32 -13
  15. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/integration.py +34 -13
  16. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/ledger.py +2 -0
  17. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/ledger_backend.py +2 -0
  18. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/models.py +4 -1
  19. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/__init__.py +7 -3
  20. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/context_compaction.py +40 -11
  21. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/step_cap.py +4 -4
  22. agent_tokenops-0.4.0/src/tokenops/control/policies/time_budget.py +57 -0
  23. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/trajectory_hint.py +4 -2
  24. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/store.py +3 -3
  25. agent_tokenops-0.4.0/src/tokenops/control/usage.py +43 -0
  26. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/anthropic.py +5 -1
  27. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/openai.py +6 -0
  28. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/types.py +4 -0
  29. agent_tokenops-0.4.0/src/tokenops/ui/policy_labels.py +18 -0
  30. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/run_detail.py +7 -4
  31. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/views/admin.py +42 -28
  32. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/views/dashboard.py +2 -0
  33. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_actuators.py +1 -1
  34. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_boundary.py +3 -3
  35. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_chronicle_boundary.py +5 -5
  36. agent_tokenops-0.4.0/tests/test_context_compaction.py +121 -0
  37. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_crossing_hook.py +5 -5
  38. agent_tokenops-0.4.0/tests/test_demo_policy_names.py +149 -0
  39. agent_tokenops-0.4.0/tests/test_governance_event_names.py +214 -0
  40. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_policies_wrap_integration.py +65 -1
  41. agent_tokenops-0.4.0/tests/test_policy_names.py +279 -0
  42. agent_tokenops-0.4.0/tests/test_provider_usage.py +161 -0
  43. agent_tokenops-0.4.0/tests/test_time_budget.py +73 -0
  44. agent_tokenops-0.4.0/tests/test_ui_policy_names.py +177 -0
  45. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_wrap_adapters.py +4 -4
  46. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_wrap_complete_chronicle.py +4 -4
  47. agent_tokenops-0.3.0/src/tokenops/control/config.py +0 -199
  48. agent_tokenops-0.3.0/tests/test_context_compaction.py +0 -38
  49. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/LICENSE.txt +0 -0
  50. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/README.md +0 -0
  51. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/setup.cfg +0 -0
  52. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/dependency_links.txt +0 -0
  53. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/top_level.txt +0 -0
  54. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/adapters/__init__.py +0 -0
  55. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/adapters/langchain.py +0 -0
  56. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/config/__init__.py +0 -0
  57. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/config/loader.py +0 -0
  58. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/attribution.py +0 -0
  59. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/client.py +0 -0
  60. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/dev_plane.py +0 -0
  61. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/governance_cache.py +0 -0
  62. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/http.py +0 -0
  63. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/http_store.py +0 -0
  64. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/instrument.py +0 -0
  65. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/_util.py +0 -0
  66. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/concurrency_cap.py +0 -0
  67. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/cost_budget.py +0 -0
  68. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/cost_guard.py +0 -0
  69. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/output_runaway.py +0 -0
  70. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/pre_call_worst_case.py +0 -0
  71. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/progress_guard.py +0 -0
  72. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/tool_fix.py +0 -0
  73. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/tool_output_cap.py +0 -0
  74. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/pricing.py +0 -0
  75. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/propagate.py +0 -0
  76. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/request_context.py +0 -0
  77. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/run.py +0 -0
  78. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/__init__.py +0 -0
  79. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/compress.py +0 -0
  80. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/enqueue.py +0 -0
  81. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/gates.py +0 -0
  82. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/hint.py +0 -0
  83. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/scope.py +0 -0
  84. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/serialize.py +0 -0
  85. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/worker.py +0 -0
  86. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/demo.py +0 -0
  87. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/env.py +0 -0
  88. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/__init__.py +0 -0
  89. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/factory.py +0 -0
  90. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/server/__init__.py +0 -0
  91. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/server/__main__.py +0 -0
  92. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/server/app.py +0 -0
  93. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/__init__.py +0 -0
  94. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/app.py +0 -0
  95. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/store_client.py +0 -0
  96. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/theme.py +0 -0
  97. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/views/__init__.py +0 -0
  98. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_a2a_health.py +0 -0
  99. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_apply.py +0 -0
  100. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_attribution.py +0 -0
  101. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_attribution_ledger_policies_e2e.py +0 -0
  102. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_concurrency_cap.py +0 -0
  103. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_config.py +0 -0
  104. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_control_plane_app.py +0 -0
  105. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_control_plane_client.py +0 -0
  106. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_cost_budget.py +0 -0
  107. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_cost_guard.py +0 -0
  108. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_cross_process_budget_gating.py +0 -0
  109. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_demo.py +0 -0
  110. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_governance_config_cache.py +0 -0
  111. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_governance_per_agent.py +0 -0
  112. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_integration.py +0 -0
  113. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_ledger.py +0 -0
  114. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_ledger_backend_contract.py +0 -0
  115. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_ledger_backend_mode.py +0 -0
  116. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_llm_boundary_precall.py +0 -0
  117. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_output_runaway.py +0 -0
  118. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_policy_coordination.py +0 -0
  119. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_pre_call_worst_case.py +0 -0
  120. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_preview_mode.py +0 -0
  121. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_pricing.py +0 -0
  122. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_progress_guard.py +0 -0
  123. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_propagate.py +0 -0
  124. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_reason_encoding.py +0 -0
  125. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_server_enforcement.py +0 -0
  126. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_step_cap.py +0 -0
  127. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_store.py +0 -0
  128. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_thread_safety.py +0 -0
  129. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_tokenops_run.py +0 -0
  130. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_tool_fix.py +0 -0
  131. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_tool_output_cap.py +0 -0
  132. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_trajectory_hint.py +0 -0
  133. {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_trajectory_hint_e2e.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-tokenops
3
- Version: 0.3.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-tokenops"
7
- version = "0.3.0"
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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-tokenops
3
- Version: 0.3.0
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
@@ -37,6 +37,7 @@ src/tokenops/control/propagate.py
37
37
  src/tokenops/control/request_context.py
38
38
  src/tokenops/control/run.py
39
39
  src/tokenops/control/store.py
40
+ src/tokenops/control/usage.py
40
41
  src/tokenops/control/policies/__init__.py
41
42
  src/tokenops/control/policies/_util.py
42
43
  src/tokenops/control/policies/concurrency_cap.py
@@ -47,6 +48,7 @@ src/tokenops/control/policies/output_runaway.py
47
48
  src/tokenops/control/policies/pre_call_worst_case.py
48
49
  src/tokenops/control/policies/progress_guard.py
49
50
  src/tokenops/control/policies/step_cap.py
51
+ src/tokenops/control/policies/time_budget.py
50
52
  src/tokenops/control/policies/tool_fix.py
51
53
  src/tokenops/control/policies/tool_output_cap.py
52
54
  src/tokenops/control/policies/trajectory_hint.py
@@ -68,6 +70,7 @@ src/tokenops/server/__main__.py
68
70
  src/tokenops/server/app.py
69
71
  src/tokenops/ui/__init__.py
70
72
  src/tokenops/ui/app.py
73
+ src/tokenops/ui/policy_labels.py
71
74
  src/tokenops/ui/run_detail.py
72
75
  src/tokenops/ui/store_client.py
73
76
  src/tokenops/ui/theme.py
@@ -91,7 +94,9 @@ tests/test_cost_guard.py
91
94
  tests/test_cross_process_budget_gating.py
92
95
  tests/test_crossing_hook.py
93
96
  tests/test_demo.py
97
+ tests/test_demo_policy_names.py
94
98
  tests/test_governance_config_cache.py
99
+ tests/test_governance_event_names.py
95
100
  tests/test_governance_per_agent.py
96
101
  tests/test_integration.py
97
102
  tests/test_ledger.py
@@ -101,20 +106,24 @@ tests/test_llm_boundary_precall.py
101
106
  tests/test_output_runaway.py
102
107
  tests/test_policies_wrap_integration.py
103
108
  tests/test_policy_coordination.py
109
+ tests/test_policy_names.py
104
110
  tests/test_pre_call_worst_case.py
105
111
  tests/test_preview_mode.py
106
112
  tests/test_pricing.py
107
113
  tests/test_progress_guard.py
108
114
  tests/test_propagate.py
115
+ tests/test_provider_usage.py
109
116
  tests/test_reason_encoding.py
110
117
  tests/test_server_enforcement.py
111
118
  tests/test_step_cap.py
112
119
  tests/test_store.py
113
120
  tests/test_thread_safety.py
121
+ tests/test_time_budget.py
114
122
  tests/test_tokenops_run.py
115
123
  tests/test_tool_fix.py
116
124
  tests/test_tool_output_cap.py
117
125
  tests/test_trajectory_hint.py
118
126
  tests/test_trajectory_hint_e2e.py
127
+ tests/test_ui_policy_names.py
119
128
  tests/test_wrap_adapters.py
120
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
@@ -4,7 +4,7 @@ from tokenops.env import load_env
4
4
 
5
5
  load_env()
6
6
 
7
- __version__ = "0.3.0"
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 (
@@ -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
 
@@ -0,0 +1,279 @@
1
+ """Config factory — turn a declarative governance config into a wired Governor.
2
+
3
+ A policy template is just a ``build(...) -> (Detector, Policy)`` factory; this module maps
4
+ config keys to those factories, resolves budget references, and registers everything on a
5
+ Governor backed by a Ledger that holds the same budgets.
6
+
7
+ Config shape (dict, typically parsed from YAML)::
8
+
9
+ governance:
10
+ budgets:
11
+ - id: run_llm_cap
12
+ limit_micros: 1_000_000 # null/omitted → unlimited accumulator
13
+ dimension: run # run | user | agent | tenant | tag
14
+ policies:
15
+ cost_budget: { budget: run_llm_cap }
16
+ pre_call_worst_case:{ budget: run_llm_cap, default_max_output: 1024 }
17
+ step_cap: { max_steps: 20 }
18
+ concurrency_cap: { max_concurrent: 4, mode: reject }
19
+ tool_fix: { registry: [search], k: 3 }
20
+ tool_output_cap: { cap_tokens: 8000 }
21
+ progress_guard: { window: 6, repeats: 3, max_corrections: 2 }
22
+ cost_guard: { budget: run_llm_cap, threshold: 0.8, mode: minimize }
23
+ context_compaction: { ctx_max: 100000 }
24
+ output_runaway: { repeats: 4, max_retries: 2 }
25
+
26
+ Fail closed: an unknown policy key, a missing budget reference, or a missing required
27
+ parameter raises at build time — never silently skipped.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from collections.abc import Callable, Mapping
33
+ from dataclasses import dataclass, field
34
+ from types import MappingProxyType
35
+ from typing import TYPE_CHECKING, Any
36
+
37
+ from tokenops.control.core import Detector, Policy
38
+ from tokenops.control.engine import AgentControls, ApplyControls, Governor, PreviewControls
39
+ from tokenops.control.ledger import Budget, Ledger, PriceFn
40
+ from tokenops.control.ledger_backend import LedgerBackend
41
+ from tokenops.control.models import GovernanceMode
42
+ from tokenops.control.policies import (
43
+ concurrency_cap,
44
+ context_compaction,
45
+ cost_budget,
46
+ cost_guard,
47
+ output_runaway,
48
+ pre_call_worst_case,
49
+ progress_guard,
50
+ step_cap,
51
+ time_budget,
52
+ tool_fix,
53
+ tool_output_cap,
54
+ )
55
+
56
+ if TYPE_CHECKING:
57
+ from tokenops.control.store import Store
58
+
59
+
60
+ @dataclass
61
+ class _Ctx:
62
+ """Resolution context handed to each template adapter."""
63
+
64
+ budgets: dict[str, Budget]
65
+ price: PriceFn
66
+
67
+ def budget(self, ref: str) -> Budget:
68
+ try:
69
+ return self.budgets[ref]
70
+ except KeyError:
71
+ raise ValueError(
72
+ f"policy references unknown budget {ref!r}; known: {sorted(self.budgets)}"
73
+ ) from None
74
+
75
+
76
+ _Factory = Callable[[Mapping[str, Any], _Ctx], tuple[Detector, Policy]]
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class PolicyTemplate:
81
+ """Metadata keyed by the canonical policy ID, shared by config, Store, and UI.
82
+
83
+ ``default_params`` are Admin form examples, not implicit runtime configuration.
84
+ A missing ``factory`` marks a known but unavailable template; stored instances may
85
+ retain its ID, but ``build_governor`` must reject it.
86
+ """
87
+
88
+ display_name: str
89
+ factory: _Factory | None
90
+ default_params: Mapping[str, Any] = field(default_factory=dict)
91
+ requires_budget: bool = False
92
+ disabled_reason: str | None = None
93
+
94
+
95
+ # ID = module stem = Detector.name = Policy.name = YAML key = docs/policies/<id>.md.
96
+ # Display names are labels, never aliases accepted by the config loader.
97
+ POLICY_TEMPLATES: Mapping[str, PolicyTemplate] = MappingProxyType(
98
+ {
99
+ "cost_budget": PolicyTemplate(
100
+ display_name="Cost budget",
101
+ factory=lambda p, c: cost_budget.build(c.budget(p["budget"])),
102
+ requires_budget=True,
103
+ ),
104
+ "pre_call_worst_case": PolicyTemplate(
105
+ display_name="Pre-call worst case",
106
+ factory=lambda p, c: pre_call_worst_case.build(
107
+ c.budget(p["budget"]), c.price, default_max_output=p.get("default_max_output", 1024)
108
+ ),
109
+ default_params={"default_max_output": 1024},
110
+ requires_budget=True,
111
+ ),
112
+ "step_cap": PolicyTemplate(
113
+ display_name="Step cap",
114
+ factory=lambda p, c: step_cap.build(p["max_steps"]),
115
+ default_params={"max_steps": 20},
116
+ ),
117
+ "time_budget": PolicyTemplate(
118
+ display_name="Time budget",
119
+ factory=lambda p, c: time_budget.build(p["max_seconds"]),
120
+ default_params={"max_seconds": 60.0},
121
+ ),
122
+ "concurrency_cap": PolicyTemplate(
123
+ display_name="Concurrency cap",
124
+ factory=lambda p, c: concurrency_cap.build(
125
+ p["max_concurrent"],
126
+ dimension=p.get("dimension", "run"),
127
+ tag_key=p.get("tag_key"),
128
+ mode=p.get("mode", "reject"),
129
+ retry_after_s=p.get("retry_after_s", 1.0),
130
+ ),
131
+ default_params={"max_concurrent": 4, "mode": "reject"},
132
+ ),
133
+ "tool_fix": PolicyTemplate(
134
+ display_name="Tool fix",
135
+ factory=lambda p, c: tool_fix.build(
136
+ p["registry"], schema=p.get("schema"), k=p.get("k", 3)
137
+ ),
138
+ default_params={"registry": ["search"], "k": 3},
139
+ ),
140
+ "tool_output_cap": PolicyTemplate(
141
+ display_name="Tool output cap",
142
+ factory=lambda p, c: tool_output_cap.build(p.get("cap_tokens", 8000)),
143
+ default_params={"cap_tokens": 8000},
144
+ ),
145
+ "progress_guard": PolicyTemplate(
146
+ display_name="Progress guard",
147
+ factory=lambda p, c: progress_guard.build(
148
+ window=p.get("window", 6),
149
+ repeats=p.get("repeats", 3),
150
+ max_corrections=p.get("max_corrections", 2),
151
+ simhash_threshold=p.get("simhash_threshold", 4),
152
+ ),
153
+ default_params={"window": 6, "repeats": 3, "max_corrections": 2},
154
+ ),
155
+ "cost_guard": PolicyTemplate(
156
+ display_name="Cost guard",
157
+ factory=lambda p, c: cost_guard.build(
158
+ c.budget(p["budget"]),
159
+ threshold=p.get("threshold", 0.8),
160
+ mode=p.get("mode", "minimize"),
161
+ downgrade_to=p.get("downgrade_to"),
162
+ velocity_micros_per_step=p.get("velocity_micros_per_step"),
163
+ velocity_m=p.get("velocity_m", 5),
164
+ ),
165
+ default_params={"threshold": 0.8, "mode": "minimize"},
166
+ requires_budget=True,
167
+ ),
168
+ "context_compaction": PolicyTemplate(
169
+ display_name="Context compaction",
170
+ factory=lambda p, c: context_compaction.build(p["ctx_max"], window=p.get("window", 4)),
171
+ default_params={"ctx_max": 100000},
172
+ ),
173
+ "output_runaway": PolicyTemplate(
174
+ display_name="Output runaway",
175
+ factory=lambda p, c: output_runaway.build(
176
+ n=p.get("n", 3),
177
+ repeats=p.get("repeats", 4),
178
+ domination=p.get("domination", 0.5),
179
+ max_retries=p.get("max_retries", 2),
180
+ ),
181
+ default_params={"repeats": 4, "max_retries": 2},
182
+ ),
183
+ # Re-enable only with persistent plane-side trajectory routes and quality gates.
184
+ # The standalone policy module remains available for research/tests.
185
+ "trajectory_hint": PolicyTemplate(
186
+ display_name="Trajectory hint",
187
+ factory=None,
188
+ disabled_reason=(
189
+ "trajectory_hint is temporarily disabled (remote-only control-plane work); "
190
+ "remove it from your governance config. See "
191
+ "docs/policies/trajectory_hint.md and config.build_governor for details."
192
+ ),
193
+ ),
194
+ }
195
+ )
196
+
197
+
198
+ def policy_template_ids(*, include_disabled: bool = False) -> tuple[str, ...]:
199
+ """Canonical built-in IDs, sorted for display; never include spelling aliases."""
200
+ return tuple(
201
+ sorted(
202
+ name
203
+ for name, template in POLICY_TEMPLATES.items()
204
+ if include_disabled or template.factory is not None
205
+ )
206
+ )
207
+
208
+
209
+ def parse_budgets(specs) -> dict[str, Budget]:
210
+ out: dict[str, Budget] = {}
211
+ for spec in specs or []:
212
+ b = Budget(
213
+ budget_id=spec["id"],
214
+ limit_micros=spec.get("limit_micros"), # None → unlimited
215
+ dimension=spec.get("dimension", "run"),
216
+ tag_key=spec.get("tag_key"),
217
+ period=spec.get("period", "lifetime"),
218
+ )
219
+ out[b.budget_id] = b
220
+ return out
221
+
222
+
223
+ def build_governor(
224
+ config: Mapping[str, Any],
225
+ price: PriceFn,
226
+ controls: AgentControls | None = None,
227
+ *,
228
+ store: Store | None = None,
229
+ backend: LedgerBackend | None = None,
230
+ enforce: bool = True,
231
+ ) -> Governor:
232
+ """Build a fully-registered Governor (and its Ledger, on ``governor.ledger``) from a
233
+ declarative governance config. ``config`` is the ``governance:`` block (or a mapping
234
+ that contains ``budgets`` / ``policies``).
235
+
236
+ Pass exactly one of ``store`` (local Store — tests only) or ``backend`` (a
237
+ :class:`~tokenops.control.ledger_backend.LedgerBackend` — the control plane, direct
238
+ or fake) to share spend, inflight, and halt state across A2A agent processes.
239
+ Neither means a fully in-memory, single-process Ledger."""
240
+ gov_cfg = config.get("governance", config)
241
+ budgets = parse_budgets(gov_cfg.get("budgets"))
242
+
243
+ ledger = Ledger(budgets=list(budgets.values()), price=price, store=store, backend=backend)
244
+ governor = Governor(ledger, controls, enforce=enforce)
245
+ ctx = _Ctx(budgets=budgets, price=price)
246
+
247
+ for name, params in (gov_cfg.get("policies") or {}).items():
248
+ template = POLICY_TEMPLATES.get(name)
249
+ if template is None:
250
+ raise ValueError(f"unknown policy {name!r}; known: {list(policy_template_ids())}")
251
+ if template.factory is None:
252
+ raise ValueError(template.disabled_reason or f"policy {name!r} is unavailable")
253
+ detector, policy = template.factory(params or {}, ctx)
254
+ if detector.name != name or policy.name != name:
255
+ raise ValueError(
256
+ f"policy template {name!r} built detector/policy names "
257
+ f"{detector.name!r}/{policy.name!r}; both must match the config key"
258
+ )
259
+ governor.register(detector, policy)
260
+
261
+ return governor
262
+
263
+
264
+ def build_governance_stack(
265
+ config: Mapping[str, Any],
266
+ price: PriceFn,
267
+ *,
268
+ store: Store | None = None,
269
+ backend: LedgerBackend | None = None,
270
+ mode: GovernanceMode = GovernanceMode.ENFORCE,
271
+ ) -> tuple[Governor, ApplyControls | PreviewControls]:
272
+ """Wire a Governor + OUT connector for enforce or preview mode."""
273
+ enforce = mode is not GovernanceMode.PREVIEW
274
+ controls: ApplyControls | PreviewControls
275
+ controls = ApplyControls() if enforce else PreviewControls()
276
+ governor = build_governor(
277
+ config, price, controls, store=store, backend=backend, enforce=enforce
278
+ )
279
+ return governor, controls
@@ -36,6 +36,7 @@ class BoundRun:
36
36
  _registration: ContextVar[RunRegistration | None] = ContextVar("run_registration", default=None)
37
37
  _span: ContextVar[SpanContext | None] = ContextVar("run_span", default=None)
38
38
  _governance: ContextVar[GovernanceContext | None] = ContextVar("governance", default=None)
39
+ _current_controls: ContextVar[Any | None] = ContextVar("current_controls", default=None)
39
40
 
40
41
 
41
42
  @dataclass(frozen=True, kw_only=True)
@@ -60,6 +61,21 @@ def clear_governance() -> None:
60
61
  _governance.set(None)
61
62
 
62
63
 
64
+ def current_controls() -> Any:
65
+ """Return the active ``AgentControls`` for the current governed call, or ``None``."""
66
+ return _current_controls.get()
67
+
68
+
69
+ def set_current_controls(controls: Any) -> Any:
70
+ """Set the active controls context; returns a token for ``reset_current_controls``."""
71
+ return _current_controls.set(controls)
72
+
73
+
74
+ def reset_current_controls(token: Any) -> None:
75
+ """Reset the controls context to its previous value."""
76
+ _current_controls.reset(token)
77
+
78
+
63
79
  def current_registration() -> RunRegistration | None:
64
80
  return _registration.get()
65
81
 
@@ -90,6 +106,7 @@ def clear() -> None:
90
106
  _registration.set(None)
91
107
  _span.set(None)
92
108
  _governance.set(None)
109
+ _current_controls.set(None)
93
110
 
94
111
 
95
112
  @contextmanager
@@ -50,7 +50,7 @@ __all__ = [
50
50
  #: Cost in micro-US-dollars. ``$1.00 == 1_000_000``. Integer math avoids float drift.
51
51
  Micros = int
52
52
 
53
- #: Chronicle boundary_kind, LLD vocabulary. Not "model"/"delegation" — these literals.
53
+ #: Chronicle kind, LLD vocabulary. Not "model"/"delegation" — these literals.
54
54
  NodeType = Literal["llm", "tool", "delegate"]
55
55
 
56
56
 
@@ -61,11 +61,14 @@ NodeType = Literal["llm", "tool", "delegate"]
61
61
 
62
62
  @dataclass(frozen=True, kw_only=True)
63
63
  class Usage:
64
- """Provider-reported token *totals* for one call — never the streamed text.
65
-
66
- ``cached`` and ``reasoning`` mirror the hidden, costly categories providers report
67
- (OpenAI ``*_tokens_details``; Anthropic ``cache_read_input_tokens``). Track them or
68
- the spend most likely to surprise you stays invisible.
64
+ """Disjoint, independently priceable token buckets for one call.
65
+
66
+ ``input`` excludes cache reads; ``output`` excludes reasoning. ``cached`` and
67
+ ``reasoning`` are billed separately, never added to inclusive provider totals.
68
+ Prompt/context size is ``input + cached``; completion size is ``output + reasoning``.
69
+ Dispatch adapters and agent steps report inclusive totals and are normalized at
70
+ the boundary. Native Anthropic input already excludes cache reads. Cache-write
71
+ premium accounting is not yet supported.
69
72
  """
70
73
 
71
74
  input: int = 0
@@ -134,6 +137,8 @@ class Observation:
134
137
  result_hash: str | None = None
135
138
  # delegate only: child run total reported up the A2A hop
136
139
  rolled_up_cost_micros: Micros = 0
140
+ # compaction only: tokens_before / tokens_after / tokens_saved
141
+ compaction: Mapping[str, int] | None = None
137
142
 
138
143
 
139
144
  @dataclass(frozen=True, kw_only=True)
@@ -215,7 +220,12 @@ class ActionKind(str, Enum):
215
220
  @dataclass(frozen=True, kw_only=True)
216
221
  class Action:
217
222
  """The decision applied to a run. One flat dataclass for every kind so the OUT
218
- connector stays a single polymorphic ``apply(action)``; unused payloads are ``None``."""
223
+ connector stays a single polymorphic ``apply(action)``; unused payloads are ``None``.
224
+
225
+ ``policy_id`` is the deciding template's canonical ID, not a configured instance ID.
226
+ The Governor sets it from the routed signal; standalone actions may omit it.
227
+ Attribution does not change decision equality or hashing.
228
+ """
219
229
 
220
230
  kind: ActionKind
221
231
  run_id: str
@@ -230,6 +240,7 @@ class Action:
230
240
  compact: bool = False
231
241
  # REJECT / QUEUE payload
232
242
  retry_after_s: float | None = None
243
+ policy_id: str | None = field(default=None, compare=False)
233
244
 
234
245
 
235
246
  class Halt(BaseException):