agent-tokenops 2.0.0__tar.gz → 2.1.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-2.0.0/src/agent_tokenops.egg-info → agent_tokenops-2.1.0}/PKG-INFO +2 -2
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/pyproject.toml +1 -1
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0/src/agent_tokenops.egg-info}/PKG-INFO +2 -2
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/SOURCES.txt +20 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/requires.txt +1 -1
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/_version.py +1 -1
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/crossing.py +36 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/config.py +18 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/core.py +4 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/engine.py +18 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/__init__.py +3 -0
- agent_tokenops-2.1.0/src/tokenops/framework/policies/tool_output_prune.py +80 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning.py +185 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_c.py +28 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_clex.py +171 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_clike.py +169 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_go.py +31 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_java.py +23 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_js.py +59 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_jslex.py +199 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_languages.py +73 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_python.py +178 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_rust.py +29 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_shell.py +42 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_shlex.py +203 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_sql.py +41 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_sqllex.py +95 -0
- agent_tokenops-2.1.0/src/tokenops/framework/pruning_types.py +78 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_policy_names.py +1 -1
- agent_tokenops-2.1.0/tests/test_tool_output_prune.py +175 -0
- agent_tokenops-2.1.0/tests/test_tool_output_prune_languages.py +366 -0
- agent_tokenops-2.1.0/tests/test_tool_output_prune_scripts.py +330 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/LICENSE.txt +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/README.md +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/setup.cfg +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/dependency_links.txt +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/top_level.txt +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/adapters/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/adapters/langchain.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/chronicle_trace_utils.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/complete_wrapper.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/ledger_writers.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/run.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/step_observation.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/stream_wrapper.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/config/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/config/default.yaml +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/config/loader.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/demo.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/env.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/attribution.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/boundary.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/budget.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/context.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger_backend.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger_events.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger_types.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/message_steering.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/models.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/observation_utils.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/_util.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/concurrency_cap.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/context_compaction.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/cost_budget.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/cost_guard.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/output_runaway.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/pre_call_worst_case.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/progress_guard.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/step_cap.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/time_budget.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/tool_fix.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/tool_output_cap.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policy_labels.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/pricing.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/request_context.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/usage.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/interfaces/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/interfaces/http.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/interfaces/instrument.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/anthropic.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/factory.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/openai.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/types.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/py.typed +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/__init__.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/control_plane_client.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/control_plane_errors.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/control_plane_types.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/ledger_event_writer.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_a2a_health.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_a2a_server_errors.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_actuators.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_apply.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_async_ledger.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_attribution.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_attribution_ledger_policies_e2e.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_boundary.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_chronicle_boundary.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_compaction_metadata.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_concurrency_cap.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_config.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_context_compaction.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_cost_budget.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_cost_guard.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_cross_process_budget_gating.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_crossing_hook.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_demo.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_governance_event_names.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_httpx_instrumentation.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_integration.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_layering.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_ledger.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_llm_boundary_precall.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_output_runaway.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_policies_wrap_integration.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_policy_coordination.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_pre_call_worst_case.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_preview_mode.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_pricing.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_progress_guard.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_provider_usage.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_reason_encoding.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_repositories_control_plane_client.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_repositories_ledger_event_writer.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_step_cap.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_thread_safety.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_time_budget.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_tokenops_run.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_tool_fix.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_tool_output_cap.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_wrap_adapters.py +0 -0
- {agent_tokenops-2.0.0 → agent_tokenops-2.1.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: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Run-aware token governance for agents: budgets, policies and halts on Chronicle and the AgentPlane control plane
|
|
5
5
|
Author: Susheem Koul, Tisha Chawla
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,7 +31,7 @@ Requires-Dist: python-dotenv>=1.0
|
|
|
31
31
|
Requires-Dist: httpx>=0.27
|
|
32
32
|
Requires-Dist: fastapi>=0.115
|
|
33
33
|
Provides-Extra: contract
|
|
34
|
-
Requires-Dist: agentplane-control-plane>=1.0.
|
|
34
|
+
Requires-Dist: agentplane-control-plane>=1.0.2; extra == "contract"
|
|
35
35
|
Provides-Extra: examples
|
|
36
36
|
Requires-Dist: ddgs>=9.0; extra == "examples"
|
|
37
37
|
Requires-Dist: langchain-core>=0.3; extra == "examples"
|
|
@@ -66,7 +66,7 @@ dev = [
|
|
|
66
66
|
# The tests that need it `pytest.importorskip("control_plane")`, so a plain `dev` install
|
|
67
67
|
# stays green. To run them: `pip install -e ".[contract]"`.
|
|
68
68
|
contract = [
|
|
69
|
-
"agentplane-control-plane>=1.0.
|
|
69
|
+
"agentplane-control-plane>=1.0.2",
|
|
70
70
|
]
|
|
71
71
|
examples = [
|
|
72
72
|
"ddgs>=9.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-tokenops
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Run-aware token governance for agents: budgets, policies and halts on Chronicle and the AgentPlane control plane
|
|
5
5
|
Author: Susheem Koul, Tisha Chawla
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,7 +31,7 @@ Requires-Dist: python-dotenv>=1.0
|
|
|
31
31
|
Requires-Dist: httpx>=0.27
|
|
32
32
|
Requires-Dist: fastapi>=0.115
|
|
33
33
|
Provides-Extra: contract
|
|
34
|
-
Requires-Dist: agentplane-control-plane>=1.0.
|
|
34
|
+
Requires-Dist: agentplane-control-plane>=1.0.2; extra == "contract"
|
|
35
35
|
Provides-Extra: examples
|
|
36
36
|
Requires-Dist: ddgs>=9.0; extra == "examples"
|
|
37
37
|
Requires-Dist: langchain-core>=0.3; extra == "examples"
|
|
@@ -41,6 +41,22 @@ src/tokenops/framework/models.py
|
|
|
41
41
|
src/tokenops/framework/observation_utils.py
|
|
42
42
|
src/tokenops/framework/policy_labels.py
|
|
43
43
|
src/tokenops/framework/pricing.py
|
|
44
|
+
src/tokenops/framework/pruning.py
|
|
45
|
+
src/tokenops/framework/pruning_c.py
|
|
46
|
+
src/tokenops/framework/pruning_clex.py
|
|
47
|
+
src/tokenops/framework/pruning_clike.py
|
|
48
|
+
src/tokenops/framework/pruning_go.py
|
|
49
|
+
src/tokenops/framework/pruning_java.py
|
|
50
|
+
src/tokenops/framework/pruning_js.py
|
|
51
|
+
src/tokenops/framework/pruning_jslex.py
|
|
52
|
+
src/tokenops/framework/pruning_languages.py
|
|
53
|
+
src/tokenops/framework/pruning_python.py
|
|
54
|
+
src/tokenops/framework/pruning_rust.py
|
|
55
|
+
src/tokenops/framework/pruning_shell.py
|
|
56
|
+
src/tokenops/framework/pruning_shlex.py
|
|
57
|
+
src/tokenops/framework/pruning_sql.py
|
|
58
|
+
src/tokenops/framework/pruning_sqllex.py
|
|
59
|
+
src/tokenops/framework/pruning_types.py
|
|
44
60
|
src/tokenops/framework/request_context.py
|
|
45
61
|
src/tokenops/framework/usage.py
|
|
46
62
|
src/tokenops/framework/policies/__init__.py
|
|
@@ -56,6 +72,7 @@ src/tokenops/framework/policies/step_cap.py
|
|
|
56
72
|
src/tokenops/framework/policies/time_budget.py
|
|
57
73
|
src/tokenops/framework/policies/tool_fix.py
|
|
58
74
|
src/tokenops/framework/policies/tool_output_cap.py
|
|
75
|
+
src/tokenops/framework/policies/tool_output_prune.py
|
|
59
76
|
src/tokenops/interfaces/__init__.py
|
|
60
77
|
src/tokenops/interfaces/http.py
|
|
61
78
|
src/tokenops/interfaces/instrument.py
|
|
@@ -111,5 +128,8 @@ tests/test_time_budget.py
|
|
|
111
128
|
tests/test_tokenops_run.py
|
|
112
129
|
tests/test_tool_fix.py
|
|
113
130
|
tests/test_tool_output_cap.py
|
|
131
|
+
tests/test_tool_output_prune.py
|
|
132
|
+
tests/test_tool_output_prune_languages.py
|
|
133
|
+
tests/test_tool_output_prune_scripts.py
|
|
114
134
|
tests/test_wrap_adapters.py
|
|
115
135
|
tests/test_wrap_complete_chronicle.py
|
|
@@ -124,6 +124,40 @@ def on_leave(info: CallInfo) -> None:
|
|
|
124
124
|
_inflight_seg.set(None)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
_TEXT_KEYS = ("output", "stdout", "text", "content")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _tool_text(result: Any) -> str | None:
|
|
131
|
+
"""The text of a tool result: the string itself, or the first string under a conventional key of a dict."""
|
|
132
|
+
if isinstance(result, str):
|
|
133
|
+
return result
|
|
134
|
+
if isinstance(result, dict):
|
|
135
|
+
for key in _TEXT_KEYS:
|
|
136
|
+
value = result.get(key)
|
|
137
|
+
if isinstance(value, str):
|
|
138
|
+
return value
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _shape_tool_result(governor: Any, controls: Any, name: str, result: Any) -> Any:
|
|
143
|
+
"""Let the pruner policies rewrite the tool result text *before* it is observed, so the cap measures what the agent
|
|
144
|
+
will receive. The rewrite is left on ``controls.shaped_result`` for the host to apply; the wrapped function's own
|
|
145
|
+
return value is never changed here."""
|
|
146
|
+
text = _tool_text(result)
|
|
147
|
+
shape = getattr(governor, "shape_tool_result", None)
|
|
148
|
+
if text is None or shape is None:
|
|
149
|
+
return result
|
|
150
|
+
shaped = shape(name, text)
|
|
151
|
+
if shaped is None:
|
|
152
|
+
return result
|
|
153
|
+
if controls is not None and hasattr(controls, "shaped_result"):
|
|
154
|
+
controls.shaped_result = shaped
|
|
155
|
+
if isinstance(result, str):
|
|
156
|
+
return shaped
|
|
157
|
+
key = next(k for k in _TEXT_KEYS if isinstance(result.get(k), str))
|
|
158
|
+
return {**result, key: shaped}
|
|
159
|
+
|
|
160
|
+
|
|
127
161
|
def on_crossing(info: CallInfo, result: Any) -> None:
|
|
128
162
|
"""Project crossing → Observation when a hop attribution + governance are bound."""
|
|
129
163
|
from tokenops.framework.boundary import emit_observation, observation_from_crossing
|
|
@@ -146,6 +180,8 @@ def on_crossing(info: CallInfo, result: Any) -> None:
|
|
|
146
180
|
comp = getattr(call, "compaction", None) if call is not None else None
|
|
147
181
|
if comp is not None:
|
|
148
182
|
compaction = dict(comp)
|
|
183
|
+
if _kind(info) == "tool" and governor is not None:
|
|
184
|
+
result = _shape_tool_result(governor, controls, info.name, result)
|
|
149
185
|
obs = observation_from_crossing(
|
|
150
186
|
boundary_id=info.name,
|
|
151
187
|
kind=_kind(info),
|
|
@@ -51,6 +51,7 @@ from tokenops.framework.policies import (
|
|
|
51
51
|
time_budget,
|
|
52
52
|
tool_fix,
|
|
53
53
|
tool_output_cap,
|
|
54
|
+
tool_output_prune,
|
|
54
55
|
)
|
|
55
56
|
|
|
56
57
|
|
|
@@ -139,6 +140,23 @@ POLICY_TEMPLATES: Mapping[str, PolicyTemplate] = MappingProxyType(
|
|
|
139
140
|
factory=lambda p, c: tool_output_cap.build(p.get("cap_tokens", 8000)),
|
|
140
141
|
default_params={"cap_tokens": 8000},
|
|
141
142
|
),
|
|
143
|
+
"tool_output_prune": PolicyTemplate(
|
|
144
|
+
display_name="Tool output prune",
|
|
145
|
+
factory=lambda p, c: tool_output_prune.build(
|
|
146
|
+
comments=p.get("comments", True),
|
|
147
|
+
docstrings=p.get("docstrings", True),
|
|
148
|
+
min_saved_chars=p.get("min_saved_chars", 200),
|
|
149
|
+
languages=p.get("languages", ["python"]),
|
|
150
|
+
keep_jsdoc_types=p.get("keep_jsdoc_types", True),
|
|
151
|
+
),
|
|
152
|
+
default_params={
|
|
153
|
+
"comments": True,
|
|
154
|
+
"docstrings": True,
|
|
155
|
+
"min_saved_chars": 200,
|
|
156
|
+
"languages": ["python"],
|
|
157
|
+
"keep_jsdoc_types": True,
|
|
158
|
+
},
|
|
159
|
+
),
|
|
142
160
|
"progress_guard": PolicyTemplate(
|
|
143
161
|
display_name="Progress guard",
|
|
144
162
|
factory=lambda p, c: progress_guard.build(
|
|
@@ -327,6 +327,10 @@ class Detector(ABC):
|
|
|
327
327
|
"""Inspect on a clock, independent of events (timeouts, stalls)."""
|
|
328
328
|
return None
|
|
329
329
|
|
|
330
|
+
def shape_tool_result(self, boundary_id: str, text: str) -> str | None:
|
|
331
|
+
"""Rewrite a tool result's text before it is observed (pruning). Return the new text, or None to leave it."""
|
|
332
|
+
return None
|
|
333
|
+
|
|
330
334
|
def reset(self, trace_id: str) -> None:
|
|
331
335
|
"""Drop any per-run state held for ``trace_id``."""
|
|
332
336
|
return None
|
|
@@ -137,6 +137,9 @@ class ApplyControls:
|
|
|
137
137
|
call: _ResolvedCall = field(default_factory=_ResolvedCall)
|
|
138
138
|
retry: bool = False
|
|
139
139
|
tool_result_override: str | None = None # deep INJECT: substitute the last tool result
|
|
140
|
+
shaped_result: str | None = (
|
|
141
|
+
None # a pruner's rewrite of the last tool result (applied before the cap measures)
|
|
142
|
+
)
|
|
140
143
|
max_carry: int = 3 # steer messages carried onto one dispatch; the rest are dropped
|
|
141
144
|
dropped_carry: int = 0 # how many were dropped, for the dashboard
|
|
142
145
|
|
|
@@ -154,6 +157,12 @@ class ApplyControls:
|
|
|
154
157
|
self.tool_result_override = None
|
|
155
158
|
return val
|
|
156
159
|
|
|
160
|
+
def take_shaped_result(self) -> str | None:
|
|
161
|
+
"""Consume the pruned text of the last tool result, if a pruner rewrote it. One-shot — cleared on read."""
|
|
162
|
+
val = self.shaped_result
|
|
163
|
+
self.shaped_result = None
|
|
164
|
+
return val
|
|
165
|
+
|
|
157
166
|
def apply(self, action: Action) -> None:
|
|
158
167
|
kind = action.kind
|
|
159
168
|
if kind is ActionKind.ALLOW:
|
|
@@ -319,6 +328,15 @@ class Governor:
|
|
|
319
328
|
self._enforce(signals)
|
|
320
329
|
return step
|
|
321
330
|
|
|
331
|
+
def shape_tool_result(self, boundary_id: str, text: str) -> str | None:
|
|
332
|
+
"""Run each detector's ``shape_tool_result`` in registration order; the new text, or None if none changed it."""
|
|
333
|
+
shaped = text
|
|
334
|
+
for d in self._detectors:
|
|
335
|
+
new = d.shape_tool_result(boundary_id, shaped)
|
|
336
|
+
if new is not None:
|
|
337
|
+
shaped = new
|
|
338
|
+
return None if shaped == text else shaped
|
|
339
|
+
|
|
322
340
|
def tick(self, now: float) -> None:
|
|
323
341
|
signals = [s for d in self._detectors if (s := d.tick(now, self.ledger)) is not None]
|
|
324
342
|
self._enforce(signals)
|
|
@@ -22,6 +22,7 @@ context_compaction in process: the recent step window
|
|
|
22
22
|
output_runaway the current call's output only
|
|
23
23
|
tool_fix the current call's tool request only
|
|
24
24
|
tool_output_cap the current call's tool output only
|
|
25
|
+
tool_output_prune the current call's tool output only (rewrites it before the cap measures it)
|
|
25
26
|
time_budget the wall clock since the hop started
|
|
26
27
|
===================== ==========================================================
|
|
27
28
|
|
|
@@ -40,6 +41,7 @@ from tokenops.framework.policies import (
|
|
|
40
41
|
time_budget,
|
|
41
42
|
tool_fix,
|
|
42
43
|
tool_output_cap,
|
|
44
|
+
tool_output_prune,
|
|
43
45
|
)
|
|
44
46
|
|
|
45
47
|
__all__ = [
|
|
@@ -54,4 +56,5 @@ __all__ = [
|
|
|
54
56
|
"time_budget",
|
|
55
57
|
"tool_fix",
|
|
56
58
|
"tool_output_cap",
|
|
59
|
+
"tool_output_prune",
|
|
57
60
|
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""tool_output_prune — hide what a coding agent rarely needs from a tool result, before the cap measures it.
|
|
2
|
+
|
|
3
|
+
LLD row:
|
|
4
|
+
Detect: the tool result text is a block of source code in an enabled language (Python by default; Go, Java, Rust,
|
|
5
|
+
C/C++ on request): clean lexing, that language's structure, not a diff, grep hit list or test report.
|
|
6
|
+
Fix: SHAPE — replace the result the agent sees by the same text with comments and docstrings hidden. Code
|
|
7
|
+
lines stay verbatim and the line count is unchanged (hidden blocks become blank lines), so exact-text
|
|
8
|
+
edits and cited line numbers still hold. Never HALT, never drops code, no model, no tokenizer.
|
|
9
|
+
|
|
10
|
+
Unlike ``tool_output_cap`` this does not wait for a result to be large: it shapes a result of any size, and it runs
|
|
11
|
+
*before* the cap so the cap measures what the agent will actually receive. The shaped text is handed to the host
|
|
12
|
+
through ``controls.take_shaped_result()``; a host that does not call it is unaffected. It works from the result text
|
|
13
|
+
alone (the command is not needed), so it only prunes text it can prove is source code in an enabled language, and refuses when two languages score close.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
|
|
20
|
+
from tokenops.framework.core import Action, ActionKind, Detector, LedgerView, Policy, Signal
|
|
21
|
+
from tokenops.framework.pruning import prune_any
|
|
22
|
+
from tokenops.framework.pruning_languages import resolve_languages
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ToolOutputPruneDetector(Detector):
|
|
26
|
+
"""Shapes a tool result; raises no signals (the saving is reported through the controls)."""
|
|
27
|
+
|
|
28
|
+
name = "tool_output_prune"
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
comments: bool = True,
|
|
34
|
+
docstrings: bool = True,
|
|
35
|
+
min_saved_chars: int = 200,
|
|
36
|
+
languages: str | Sequence[str] = ("python",),
|
|
37
|
+
keep_jsdoc_types: bool = True,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.handlers = resolve_languages(languages, keep_jsdoc_types=keep_jsdoc_types)
|
|
40
|
+
self.comments = comments
|
|
41
|
+
self.docstrings = docstrings
|
|
42
|
+
self.min_saved_chars = min_saved_chars
|
|
43
|
+
|
|
44
|
+
def shape_tool_result(self, boundary_id: str, text: str) -> str | None:
|
|
45
|
+
return prune_any(
|
|
46
|
+
text,
|
|
47
|
+
self.handlers,
|
|
48
|
+
comments=self.comments,
|
|
49
|
+
docstrings=self.docstrings,
|
|
50
|
+
min_saved_chars=self.min_saved_chars,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ToolOutputPrunePolicy(Policy):
|
|
55
|
+
"""Nothing to decide: the detector shapes the result itself. Kept so the template registers like the others."""
|
|
56
|
+
|
|
57
|
+
name = "tool_output_prune"
|
|
58
|
+
|
|
59
|
+
def decide(self, signal: Signal, view: LedgerView) -> Action:
|
|
60
|
+
return Action(kind=ActionKind.ALLOW, trace_id=signal.trace_id, reason=signal.reason)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build(
|
|
64
|
+
*,
|
|
65
|
+
comments: bool = True,
|
|
66
|
+
docstrings: bool = True,
|
|
67
|
+
min_saved_chars: int = 200,
|
|
68
|
+
languages: str | Sequence[str] = ("python",),
|
|
69
|
+
keep_jsdoc_types: bool = True,
|
|
70
|
+
) -> tuple[Detector, Policy]:
|
|
71
|
+
return (
|
|
72
|
+
ToolOutputPruneDetector(
|
|
73
|
+
comments=comments,
|
|
74
|
+
docstrings=docstrings,
|
|
75
|
+
min_saved_chars=min_saved_chars,
|
|
76
|
+
languages=languages,
|
|
77
|
+
keep_jsdoc_types=keep_jsdoc_types,
|
|
78
|
+
),
|
|
79
|
+
ToolOutputPrunePolicy(),
|
|
80
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Hide comments and docstrings in a tool result that is source code, from the result text alone.
|
|
2
|
+
|
|
3
|
+
The pruner does not know the command that produced the text, and the text may be a fragment (a ``sed`` range can start or
|
|
4
|
+
end inside a docstring, mid-block, or mid-statement). So it only acts when a language handler can show the text is that
|
|
5
|
+
language's source, and it only hides what the handler can prove is a comment or docstring. When in doubt it returns
|
|
6
|
+
``None`` and the result goes through untouched. It never changes a code line, and it keeps the number of lines the same
|
|
7
|
+
(a hidden block becomes blank lines), so line numbers the agent has seen or will cite still hold.
|
|
8
|
+
|
|
9
|
+
This module is the shared driver: the detection exclusions (diffs, ``grep -n`` hits, numbered listings, test reports),
|
|
10
|
+
the line-count-preserving rewrite, the note, the minimum-saving rule and the safety contract. A ``LanguageHandler``
|
|
11
|
+
(see ``pruning_types``) only finds the spans. Pure, model-free and dependency-free.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
|
|
19
|
+
from tokenops.framework.pruning_python import HIDDEN_DOCSTRING, PythonHandler
|
|
20
|
+
from tokenops.framework.pruning_types import LanguageHandler, Span
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"HIDDEN_DOCSTRING",
|
|
24
|
+
"NOTE",
|
|
25
|
+
"looks_like_python_source",
|
|
26
|
+
"prune",
|
|
27
|
+
"prune_any",
|
|
28
|
+
"select_handler",
|
|
29
|
+
"prune_python",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
NOTE = (
|
|
33
|
+
"[TokenOps: comments and docstrings are hidden in this output; line numbers are unchanged. "
|
|
34
|
+
"`grep -n` shows a specific comment or docstring line.]"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Output that merely contains code lines: diffs, grep -n hits, test reports. Never pruned.
|
|
38
|
+
_NOT_SOURCE = re.compile(
|
|
39
|
+
r"^(diff --git |@@ |\+\+\+ |--- [ab]/|={3,} \S.* ={3,}$|E {3}|[\w./\\-]+:\d+:)", re.M
|
|
40
|
+
)
|
|
41
|
+
_NUMBERED = re.compile(
|
|
42
|
+
r"^\s*\d+[\t:|]\s", re.M
|
|
43
|
+
) # cat -n / nl listings: most lines start with a number
|
|
44
|
+
MIN_LINES = 8
|
|
45
|
+
|
|
46
|
+
_PYTHON = PythonHandler()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _passes_exclusions(text: str) -> bool:
|
|
50
|
+
"""False for an output that merely contains code lines (a diff, grep hits, a numbered listing, a test report)."""
|
|
51
|
+
lines = [ln for ln in text.split("\n") if ln.strip()]
|
|
52
|
+
if len(lines) < MIN_LINES or _NOT_SOURCE.search(text):
|
|
53
|
+
return False
|
|
54
|
+
return len(_NUMBERED.findall(text)) <= 0.4 * len(lines)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_source_of(text: str, handler: LanguageHandler) -> bool:
|
|
58
|
+
"""True when `text` is a block of `handler`'s language, not an output that merely contains some code."""
|
|
59
|
+
return _passes_exclusions(text) and handler.score(text) >= handler.min_score
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def looks_like_python_source(text: str) -> bool:
|
|
63
|
+
"""True when `text` is a block of Python source, not an output that merely contains some code."""
|
|
64
|
+
return _is_source_of(text, _PYTHON)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def select_handler(text: str, handlers: Sequence[LanguageHandler]) -> LanguageHandler | None:
|
|
68
|
+
"""The handler whose language `text` is, or None when none is clearly it.
|
|
69
|
+
|
|
70
|
+
One enabled handler needs no comparison. With several, the best score must reach its minimum and clearly beat the
|
|
71
|
+
runner-up (at least 1.5 times and 2 more); two close scores mean the language is ambiguous, so nothing is pruned."""
|
|
72
|
+
if len(handlers) == 1:
|
|
73
|
+
return handlers[0]
|
|
74
|
+
ranked = sorted(
|
|
75
|
+
((h.score(text), i, h) for i, h in enumerate(handlers)), key=lambda t: (-t[0], t[1])
|
|
76
|
+
)
|
|
77
|
+
if not ranked or ranked[0][0] < ranked[0][2].min_score:
|
|
78
|
+
return None
|
|
79
|
+
top = ranked[0][0]
|
|
80
|
+
second = ranked[1][0]
|
|
81
|
+
if second and (top < 1.5 * second or top - second < 2):
|
|
82
|
+
return None
|
|
83
|
+
return ranked[0][2]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _position(span: Span) -> tuple[int, int]:
|
|
87
|
+
return span.first_row, span.col
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _hide_comment(lines: list[str], s: Span) -> None:
|
|
91
|
+
"""Blank or trim the comment `s` in `lines`, keeping every code character and the number of lines."""
|
|
92
|
+
first = lines[s.first_row - 1]
|
|
93
|
+
before = first[: s.col]
|
|
94
|
+
if s.first_row == s.last_row:
|
|
95
|
+
after = first[s.end_col :] if s.end_col >= 0 else ""
|
|
96
|
+
if not after.strip():
|
|
97
|
+
lines[s.first_row - 1] = before.rstrip() if before.strip() else ""
|
|
98
|
+
elif not before.strip():
|
|
99
|
+
lines[s.first_row - 1] = before + after.lstrip()
|
|
100
|
+
else:
|
|
101
|
+
lines[s.first_row - 1] = before.rstrip() + " " + after.lstrip()
|
|
102
|
+
return
|
|
103
|
+
lines[s.first_row - 1] = before.rstrip() if before.strip() else ""
|
|
104
|
+
for i in range(s.first_row, s.last_row - 1):
|
|
105
|
+
lines[i] = ""
|
|
106
|
+
last = lines[s.last_row - 1]
|
|
107
|
+
after = last[s.end_col :] if s.end_col >= 0 else ""
|
|
108
|
+
lines[s.last_row - 1] = after.lstrip() if after.strip() else ""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def prune(
|
|
112
|
+
text: str,
|
|
113
|
+
handler: LanguageHandler,
|
|
114
|
+
*,
|
|
115
|
+
comments: bool = True,
|
|
116
|
+
docstrings: bool = True,
|
|
117
|
+
min_saved_chars: int = 200,
|
|
118
|
+
) -> str | None:
|
|
119
|
+
"""`text` with comments and/or docstrings hidden and the same number of lines, or None to leave it as it is.
|
|
120
|
+
|
|
121
|
+
None when: it is not `handler`'s source, the handler refuses it, it would save fewer than `min_saved_chars` (after
|
|
122
|
+
the note), or the handler says the code would differ."""
|
|
123
|
+
if not (comments or docstrings) or not _is_source_of(text, handler):
|
|
124
|
+
return None
|
|
125
|
+
found = handler.spans(text)
|
|
126
|
+
if found is None:
|
|
127
|
+
return None
|
|
128
|
+
original = text.split("\n")
|
|
129
|
+
lines = list(original)
|
|
130
|
+
n = len(lines)
|
|
131
|
+
wanted = [
|
|
132
|
+
s
|
|
133
|
+
for s in found
|
|
134
|
+
if (comments if s.kind == "comment" else docstrings) and not handler.keep(s)
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# Right to left, so that editing a comment never moves the column of one before it on the same line.
|
|
138
|
+
for s in sorted((s for s in wanted if s.kind == "comment"), key=_position, reverse=True):
|
|
139
|
+
if s.last_row <= n:
|
|
140
|
+
_hide_comment(lines, s)
|
|
141
|
+
for s in wanted:
|
|
142
|
+
if s.kind == "docstring" and s.last_row <= n:
|
|
143
|
+
first = original[s.first_row - 1]
|
|
144
|
+
indent = first[: len(first) - len(first.lstrip(" \t"))]
|
|
145
|
+
lines[s.first_row - 1] = indent + handler.hidden_doc
|
|
146
|
+
for i in range(s.first_row, s.last_row):
|
|
147
|
+
lines[i] = ""
|
|
148
|
+
|
|
149
|
+
out = "\n".join(lines)
|
|
150
|
+
if len(text) - len(out) - len(NOTE) - 1 < min_saved_chars:
|
|
151
|
+
return None
|
|
152
|
+
if not handler.same_code(text, out):
|
|
153
|
+
return None
|
|
154
|
+
# Never strip trailing blank lines: a hidden last comment leaves some, and the line count must not change.
|
|
155
|
+
if text.endswith("\n"):
|
|
156
|
+
return out + NOTE + "\n"
|
|
157
|
+
return out + "\n" + NOTE
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def prune_any(
|
|
161
|
+
text: str,
|
|
162
|
+
handlers: Sequence[LanguageHandler],
|
|
163
|
+
*,
|
|
164
|
+
comments: bool = True,
|
|
165
|
+
docstrings: bool = True,
|
|
166
|
+
min_saved_chars: int = 200,
|
|
167
|
+
) -> str | None:
|
|
168
|
+
"""`prune` with the handler that `select_handler` picks for `text` among `handlers`."""
|
|
169
|
+
if not handlers or not (comments or docstrings) or not _passes_exclusions(text):
|
|
170
|
+
return None
|
|
171
|
+
handler = select_handler(text, handlers)
|
|
172
|
+
if handler is None:
|
|
173
|
+
return None
|
|
174
|
+
return prune(
|
|
175
|
+
text, handler, comments=comments, docstrings=docstrings, min_saved_chars=min_saved_chars
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def prune_python(
|
|
180
|
+
text: str, *, comments: bool = True, docstrings: bool = True, min_saved_chars: int = 200
|
|
181
|
+
) -> str | None:
|
|
182
|
+
"""`prune` with the Python handler."""
|
|
183
|
+
return prune(
|
|
184
|
+
text, _PYTHON, comments=comments, docstrings=docstrings, min_saved_chars=min_saved_chars
|
|
185
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""C and C++ handler for tool-output pruning.
|
|
2
|
+
|
|
3
|
+
``#pragma``, ``#if 0`` blocks and preprocessor lines are code, so they are never touched. A ``//`` comment that ends in a
|
|
4
|
+
backslash continues on the next line. Raw strings (``R"x(...)x"``) and digit separators (``1'000``) are handled by the lexer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from tokenops.framework.pruning_clike import CStyleHandler
|
|
12
|
+
from tokenops.framework.pruning_types import CLexConfig
|
|
13
|
+
|
|
14
|
+
_NOT_OTHER = r"(?!func\b|fn\b|def\b|pub\b|public\b|private\b|protected\b|static\s+void\s+main)"
|
|
15
|
+
_STRUCTURE = re.compile(
|
|
16
|
+
r"^\s*#\s*(?:include|define|undef|ifdef|ifndef|if|elif|else|endif|pragma|error)\b"
|
|
17
|
+
r"|^\s*(?:typedef\b|namespace\b|template\s*<|extern\s+\"C\")"
|
|
18
|
+
r"|^\s*(?:struct|union|enum|class)\s+\w+\s*(?::[^{]*)?\{?$"
|
|
19
|
+
rf"|^{_NOT_OTHER}[A-Za-z_][\w\s\*&:<>,~]*\s[\*&]*\w+(?:::\w+)*\([^;{{}}]*\)\s*(?:const\s*)?(?:\{{|$)"
|
|
20
|
+
r"|\b(?:nullptr|NULL)\b|\bstd::\w+|\w->\w|\b(?:printf|fprintf|malloc|calloc|free|sizeof)\s*\(",
|
|
21
|
+
re.M,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CHandler(CStyleHandler):
|
|
26
|
+
name = "c"
|
|
27
|
+
cfg = CLexConfig(line_continuation=True, cpp_raw_strings=True, digit_separators=True)
|
|
28
|
+
structure = _STRUCTURE
|