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.
- {agent_tokenops-0.3.0/src/agent_tokenops.egg-info → agent_tokenops-0.4.0}/PKG-INFO +2 -2
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/pyproject.toml +2 -2
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0/src/agent_tokenops.egg-info}/PKG-INFO +2 -2
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/SOURCES.txt +9 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/requires.txt +1 -1
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/__init__.py +1 -1
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/config/default.yaml +1 -1
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/__init__.py +3 -8
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/boundary.py +7 -19
- agent_tokenops-0.4.0/src/tokenops/control/config.py +279 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/context.py +17 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/core.py +18 -7
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/crossing.py +21 -26
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/engine.py +32 -13
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/integration.py +34 -13
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/ledger.py +2 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/ledger_backend.py +2 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/models.py +4 -1
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/__init__.py +7 -3
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/context_compaction.py +40 -11
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/step_cap.py +4 -4
- agent_tokenops-0.4.0/src/tokenops/control/policies/time_budget.py +57 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/trajectory_hint.py +4 -2
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/store.py +3 -3
- agent_tokenops-0.4.0/src/tokenops/control/usage.py +43 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/anthropic.py +5 -1
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/openai.py +6 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/types.py +4 -0
- agent_tokenops-0.4.0/src/tokenops/ui/policy_labels.py +18 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/run_detail.py +7 -4
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/views/admin.py +42 -28
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/views/dashboard.py +2 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_actuators.py +1 -1
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_boundary.py +3 -3
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_chronicle_boundary.py +5 -5
- agent_tokenops-0.4.0/tests/test_context_compaction.py +121 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_crossing_hook.py +5 -5
- agent_tokenops-0.4.0/tests/test_demo_policy_names.py +149 -0
- agent_tokenops-0.4.0/tests/test_governance_event_names.py +214 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_policies_wrap_integration.py +65 -1
- agent_tokenops-0.4.0/tests/test_policy_names.py +279 -0
- agent_tokenops-0.4.0/tests/test_provider_usage.py +161 -0
- agent_tokenops-0.4.0/tests/test_time_budget.py +73 -0
- agent_tokenops-0.4.0/tests/test_ui_policy_names.py +177 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_wrap_adapters.py +4 -4
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_wrap_complete_chronicle.py +4 -4
- agent_tokenops-0.3.0/src/tokenops/control/config.py +0 -199
- agent_tokenops-0.3.0/tests/test_context_compaction.py +0 -38
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/LICENSE.txt +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/README.md +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/setup.cfg +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/dependency_links.txt +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/agent_tokenops.egg-info/top_level.txt +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/adapters/__init__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/adapters/langchain.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/config/__init__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/config/loader.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/attribution.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/client.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/dev_plane.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/governance_cache.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/http.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/http_store.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/instrument.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/_util.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/concurrency_cap.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/cost_budget.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/cost_guard.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/output_runaway.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/pre_call_worst_case.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/progress_guard.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/tool_fix.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/policies/tool_output_cap.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/pricing.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/propagate.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/request_context.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/run.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/__init__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/compress.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/enqueue.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/gates.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/hint.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/scope.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/serialize.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/control/trajectory/worker.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/demo.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/env.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/__init__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/providers/factory.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/server/__init__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/server/__main__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/server/app.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/__init__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/app.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/store_client.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/theme.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/src/tokenops/ui/views/__init__.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_a2a_health.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_apply.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_attribution.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_attribution_ledger_policies_e2e.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_concurrency_cap.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_config.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_control_plane_app.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_control_plane_client.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_cost_budget.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_cost_guard.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_cross_process_budget_gating.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_demo.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_governance_config_cache.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_governance_per_agent.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_integration.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_ledger.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_ledger_backend_contract.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_ledger_backend_mode.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_llm_boundary_precall.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_output_runaway.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_policy_coordination.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_pre_call_worst_case.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_preview_mode.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_pricing.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_progress_guard.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_propagate.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_reason_encoding.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_server_enforcement.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_step_cap.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_store.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_thread_safety.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_tokenops_run.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_tool_fix.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_tool_output_cap.py +0 -0
- {agent_tokenops-0.3.0 → agent_tokenops-0.4.0}/tests/test_trajectory_hint.py +0 -0
- {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
|
+
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
|
|
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.
|
|
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.
|
|
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
|
+
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
|
|
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,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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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,
|
|
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
|
|
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
|
-
"""
|
|
65
|
-
|
|
66
|
-
``
|
|
67
|
-
|
|
68
|
-
|
|
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):
|