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.
Files changed (137) hide show
  1. {agent_tokenops-2.0.0/src/agent_tokenops.egg-info → agent_tokenops-2.1.0}/PKG-INFO +2 -2
  2. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/pyproject.toml +1 -1
  3. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0/src/agent_tokenops.egg-info}/PKG-INFO +2 -2
  4. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/SOURCES.txt +20 -0
  5. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/requires.txt +1 -1
  6. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/_version.py +1 -1
  7. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/crossing.py +36 -0
  8. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/config.py +18 -0
  9. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/core.py +4 -0
  10. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/engine.py +18 -0
  11. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/__init__.py +3 -0
  12. agent_tokenops-2.1.0/src/tokenops/framework/policies/tool_output_prune.py +80 -0
  13. agent_tokenops-2.1.0/src/tokenops/framework/pruning.py +185 -0
  14. agent_tokenops-2.1.0/src/tokenops/framework/pruning_c.py +28 -0
  15. agent_tokenops-2.1.0/src/tokenops/framework/pruning_clex.py +171 -0
  16. agent_tokenops-2.1.0/src/tokenops/framework/pruning_clike.py +169 -0
  17. agent_tokenops-2.1.0/src/tokenops/framework/pruning_go.py +31 -0
  18. agent_tokenops-2.1.0/src/tokenops/framework/pruning_java.py +23 -0
  19. agent_tokenops-2.1.0/src/tokenops/framework/pruning_js.py +59 -0
  20. agent_tokenops-2.1.0/src/tokenops/framework/pruning_jslex.py +199 -0
  21. agent_tokenops-2.1.0/src/tokenops/framework/pruning_languages.py +73 -0
  22. agent_tokenops-2.1.0/src/tokenops/framework/pruning_python.py +178 -0
  23. agent_tokenops-2.1.0/src/tokenops/framework/pruning_rust.py +29 -0
  24. agent_tokenops-2.1.0/src/tokenops/framework/pruning_shell.py +42 -0
  25. agent_tokenops-2.1.0/src/tokenops/framework/pruning_shlex.py +203 -0
  26. agent_tokenops-2.1.0/src/tokenops/framework/pruning_sql.py +41 -0
  27. agent_tokenops-2.1.0/src/tokenops/framework/pruning_sqllex.py +95 -0
  28. agent_tokenops-2.1.0/src/tokenops/framework/pruning_types.py +78 -0
  29. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_policy_names.py +1 -1
  30. agent_tokenops-2.1.0/tests/test_tool_output_prune.py +175 -0
  31. agent_tokenops-2.1.0/tests/test_tool_output_prune_languages.py +366 -0
  32. agent_tokenops-2.1.0/tests/test_tool_output_prune_scripts.py +330 -0
  33. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/LICENSE.txt +0 -0
  34. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/README.md +0 -0
  35. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/setup.cfg +0 -0
  36. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/dependency_links.txt +0 -0
  37. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/agent_tokenops.egg-info/top_level.txt +0 -0
  38. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/__init__.py +0 -0
  39. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/adapters/__init__.py +0 -0
  40. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/adapters/langchain.py +0 -0
  41. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/__init__.py +0 -0
  42. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/chronicle_trace_utils.py +0 -0
  43. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/complete_wrapper.py +0 -0
  44. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/ledger_writers.py +0 -0
  45. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/run.py +0 -0
  46. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/step_observation.py +0 -0
  47. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/capabilities/stream_wrapper.py +0 -0
  48. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/config/__init__.py +0 -0
  49. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/config/default.yaml +0 -0
  50. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/config/loader.py +0 -0
  51. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/demo.py +0 -0
  52. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/env.py +0 -0
  53. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/__init__.py +0 -0
  54. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/attribution.py +0 -0
  55. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/boundary.py +0 -0
  56. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/budget.py +0 -0
  57. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/context.py +0 -0
  58. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger.py +0 -0
  59. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger_backend.py +0 -0
  60. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger_events.py +0 -0
  61. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/ledger_types.py +0 -0
  62. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/message_steering.py +0 -0
  63. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/models.py +0 -0
  64. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/observation_utils.py +0 -0
  65. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/_util.py +0 -0
  66. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/concurrency_cap.py +0 -0
  67. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/context_compaction.py +0 -0
  68. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/cost_budget.py +0 -0
  69. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/cost_guard.py +0 -0
  70. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/output_runaway.py +0 -0
  71. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/pre_call_worst_case.py +0 -0
  72. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/progress_guard.py +0 -0
  73. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/step_cap.py +0 -0
  74. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/time_budget.py +0 -0
  75. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/tool_fix.py +0 -0
  76. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policies/tool_output_cap.py +0 -0
  77. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/policy_labels.py +0 -0
  78. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/pricing.py +0 -0
  79. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/request_context.py +0 -0
  80. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/framework/usage.py +0 -0
  81. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/interfaces/__init__.py +0 -0
  82. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/interfaces/http.py +0 -0
  83. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/interfaces/instrument.py +0 -0
  84. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/__init__.py +0 -0
  85. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/anthropic.py +0 -0
  86. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/factory.py +0 -0
  87. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/openai.py +0 -0
  88. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/providers/types.py +0 -0
  89. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/py.typed +0 -0
  90. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/__init__.py +0 -0
  91. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/control_plane_client.py +0 -0
  92. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/control_plane_errors.py +0 -0
  93. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/control_plane_types.py +0 -0
  94. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/src/tokenops/repositories/ledger_event_writer.py +0 -0
  95. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_a2a_health.py +0 -0
  96. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_a2a_server_errors.py +0 -0
  97. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_actuators.py +0 -0
  98. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_apply.py +0 -0
  99. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_async_ledger.py +0 -0
  100. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_attribution.py +0 -0
  101. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_attribution_ledger_policies_e2e.py +0 -0
  102. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_boundary.py +0 -0
  103. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_chronicle_boundary.py +0 -0
  104. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_compaction_metadata.py +0 -0
  105. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_concurrency_cap.py +0 -0
  106. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_config.py +0 -0
  107. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_context_compaction.py +0 -0
  108. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_cost_budget.py +0 -0
  109. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_cost_guard.py +0 -0
  110. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_cross_process_budget_gating.py +0 -0
  111. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_crossing_hook.py +0 -0
  112. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_demo.py +0 -0
  113. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_governance_event_names.py +0 -0
  114. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_httpx_instrumentation.py +0 -0
  115. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_integration.py +0 -0
  116. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_layering.py +0 -0
  117. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_ledger.py +0 -0
  118. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_llm_boundary_precall.py +0 -0
  119. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_output_runaway.py +0 -0
  120. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_policies_wrap_integration.py +0 -0
  121. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_policy_coordination.py +0 -0
  122. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_pre_call_worst_case.py +0 -0
  123. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_preview_mode.py +0 -0
  124. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_pricing.py +0 -0
  125. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_progress_guard.py +0 -0
  126. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_provider_usage.py +0 -0
  127. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_reason_encoding.py +0 -0
  128. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_repositories_control_plane_client.py +0 -0
  129. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_repositories_ledger_event_writer.py +0 -0
  130. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_step_cap.py +0 -0
  131. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_thread_safety.py +0 -0
  132. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_time_budget.py +0 -0
  133. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_tokenops_run.py +0 -0
  134. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_tool_fix.py +0 -0
  135. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_tool_output_cap.py +0 -0
  136. {agent_tokenops-2.0.0 → agent_tokenops-2.1.0}/tests/test_wrap_adapters.py +0 -0
  137. {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.0.0
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.1; extra == "contract"
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.1",
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.0.0
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.1; extra == "contract"
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
@@ -8,7 +8,7 @@ httpx>=0.27
8
8
  fastapi>=0.115
9
9
 
10
10
  [contract]
11
- agentplane-control-plane>=1.0.1
11
+ agentplane-control-plane>=1.0.2
12
12
 
13
13
  [examples]
14
14
  ddgs>=9.0
@@ -1,3 +1,3 @@
1
1
  """Single source of the package version (read by the build backend and at runtime)."""
2
2
 
3
- __version__ = "2.0.0"
3
+ __version__ = "2.1.0"
@@ -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