Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
cached/reasoning subsets from input/output (#137, thanks @kevin-lozada-santos).
Cache-write premiums and streaming usage remain outside this change.

### Changed

- `context_compaction` derives compaction capability from `controls.compaction_supported`
(set by `wrap_complete`) instead of a `has_hook` config flag. The flag is removed from
governance config — compaction works automatically when using `wrap_complete` (the
documented integration path). A one-time warning is logged when the policy degrades to
telemetry (no hook available).

## [0.3.0] - 2026-09-12

### Added
Expand Down
2 changes: 1 addition & 1 deletion benchmarking/browseruse/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def _steering_policies(*, limit_micros: int) -> dict[str, Any]:
"tool_fix": {"registry": list(BROWSERUSE_ACTION_REGISTRY), "k": 2},
"tool_output_cap": {"cap_tokens": 8000},
"output_runaway": {"repeats": 12, "domination": 0.9, "max_retries": 2},
"context_compaction": {"ctx_max": 100_000, "has_hook": True},
"context_compaction": {"ctx_max": 100_000},
"cost_guard": {"budget": "run_llm_cap", "threshold": 0.8, "mode": "minimize"},
}

Expand Down
2 changes: 1 addition & 1 deletion benchmarking/common/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def tokenops_config(*, limit_micros: int) -> dict[str, Any]:
# tool_fix registry is research-agent shaped; browser-use actions differ — skip for live bench
"tool_output_cap": {"cap_tokens": 8000},
"output_runaway": {"repeats": 12, "domination": 0.9, "max_retries": 2},
"context_compaction": {"ctx_max": 100_000, "has_hook": True},
"context_compaction": {"ctx_max": 100_000},
"cost_guard": {"budget": "run_llm_cap", "threshold": 0.8, "mode": "minimize"},
},
}
Expand Down
2 changes: 1 addition & 1 deletion benchmarking/metagpt/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def tokenops_config_steering(*, limit_micros: int, max_react_loop: int = 100) ->
"tool_fix": {"registry": list(METAGPT_ACTION_REGISTRY), "k": 2},
"tool_output_cap": {"cap_tokens": 6000},
"output_runaway": {"repeats": 10, "domination": 0.88, "max_retries": 2},
"context_compaction": {"ctx_max": 80_000, "has_hook": True},
"context_compaction": {"ctx_max": 80_000},
"cost_guard": {"budget": "run_llm_cap", "threshold": 0.8, "mode": "minimize"},
},
}
Expand Down
17 changes: 15 additions & 2 deletions docs/policies/context_compaction.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# context_compaction — keep the prompt lean and cache-friendly

Companion to `halt.md`. Default; needs a hook.
Companion to `halt.md`. Default; capability derived from controls.

Code: `tokenops-dev/src/tokenops/control/policies/context_compaction.py`
Tests: `tokenops-dev/tests/test_context_compaction.py`
Expand All @@ -15,6 +15,19 @@ discount), dedup tool outputs by hash, and summarize only filler while pinning t
prompt, schema, constraints, and state. Without a prompt-assembly hook it degrades to
telemetry. **Never HALTs.**

## Where the hook comes from

Compaction capability is **derived at runtime** from `controls.compaction_supported`,
not declared in governance config. `wrap_complete` (`integration.py`) sets this flag to
`True` because it IS the prompt-assembly hook — the `_compact_messages` call at
`integration.py:252` rewrites the outgoing messages before dispatch.

If you follow the [Tier-1 quickstart](../../README.md#-quickstart) (use
`wrap_complete`), compaction works out of the box. No `has_hook` config needed.

Without `wrap_complete` (e.g. `RaiseControls`), the policy degrades to telemetry
(ALLOW) and a one-time warning is logged.

## Detect (formula)

```
Expand Down Expand Up @@ -47,7 +60,7 @@ HALT: a bloated prompt or a lost cache discount is a cost issue, not a safety st
| `est_input = ctx_max` | `WARN` → `MUTATE` |
| `est_input` well below | `None` |
| `est_input ≈ ctx_max/2`, rising across recent llm steps | trips early |
| `has_hook=False` | `ALLOW` (telemetry only) — never HALT |
| No `compaction_supported` on controls | `ALLOW` (telemetry only) — never HALT |

## Status

Expand Down
1 change: 0 additions & 1 deletion examples/config/brief.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ governance:
mode: minimize
context_compaction:
ctx_max: 100000
has_hook: false
output_runaway:
repeats: 4
max_retries: 2
1 change: 0 additions & 1 deletion examples/config/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ governance:
mode: minimize
context_compaction:
ctx_max: 100000
has_hook: false
output_runaway:
repeats: 4
max_retries: 2
1 change: 0 additions & 1 deletion examples/config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ governance:
mode: minimize
context_compaction:
ctx_max: 100000
has_hook: false
output_runaway:
repeats: 4
max_retries: 2
1 change: 0 additions & 1 deletion examples/config/triad.compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ governance:
mode: minimize
context_compaction:
ctx_max: 100000
has_hook: false
output_runaway:
repeats: 4
max_retries: 2
1 change: 0 additions & 1 deletion examples/config/triad.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ governance:
mode: minimize
context_compaction:
ctx_max: 100000
has_hook: false
output_runaway:
repeats: 4
max_retries: 2
1 change: 0 additions & 1 deletion src/tokenops/config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ governance:
mode: minimize
context_compaction:
ctx_max: 100000
has_hook: false
output_runaway:
repeats: 4
max_retries: 2
4 changes: 2 additions & 2 deletions src/tokenops/control/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
tool_output_cap: { cap_tokens: 8000 }
progress_guard: { window: 6, repeats: 3, max_corrections: 2 }
cost_guard: { budget: run_llm_cap, threshold: 0.8, mode: minimize }
context_compaction: { ctx_max: 100000, has_hook: false }
context_compaction: { ctx_max: 100000 }
output_runaway: { repeats: 4, max_retries: 2 }

Fail closed: an unknown policy key, a missing budget reference, or a missing required
Expand Down Expand Up @@ -104,7 +104,7 @@ def budget(self, ref: str) -> Budget:
velocity_m=p.get("velocity_m", 5),
),
"context_compaction": lambda p, c: context_compaction.build(
p["ctx_max"], window=p.get("window", 4), has_hook=p.get("has_hook", True)
p["ctx_max"], window=p.get("window", 4)
),
"output_runaway": lambda p, c: output_runaway.build(
n=p.get("n", 3),
Expand Down
17 changes: 17 additions & 0 deletions src/tokenops/control/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class BoundRun:
_registration: ContextVar[RunRegistration | None] = ContextVar("run_registration", default=None)
_span: ContextVar[SpanContext | None] = ContextVar("run_span", default=None)
_governance: ContextVar[GovernanceContext | None] = ContextVar("governance", default=None)
_current_controls: ContextVar[Any | None] = ContextVar("current_controls", default=None)


@dataclass(frozen=True, kw_only=True)
Expand All @@ -60,6 +61,21 @@ def clear_governance() -> None:
_governance.set(None)


def current_controls() -> Any:
"""Return the active ``AgentControls`` for the current governed call, or ``None``."""
return _current_controls.get()


def set_current_controls(controls: Any) -> Any:
"""Set the active controls context; returns a token for ``reset_current_controls``."""
return _current_controls.set(controls)


def reset_current_controls(token: Any) -> None:
"""Reset the controls context to its previous value."""
_current_controls.reset(token)


def current_registration() -> RunRegistration | None:
return _registration.get()

Expand Down Expand Up @@ -90,6 +106,7 @@ def clear() -> None:
_registration.set(None)
_span.set(None)
_governance.set(None)
_current_controls.set(None)


@contextmanager
Expand Down
32 changes: 22 additions & 10 deletions src/tokenops/control/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,14 @@ class ApplyControls:
**Directives are bounded.** ``carry`` dedups and stops at ``max_carry``. ``cost_guard``
fires *because* spend is high, so an unbounded pile of steer messages would add prompt
tokens at exactly the wrong moment.

``compaction_supported`` advertises that the host can honour a MUTATE ``compact``
action (set by ``wrap_complete`` which supplies the prompt-assembly hook). Policies
that need the hook read this instead of a config flag — capability is derived, not
declared.
"""

compaction_supported: bool = False
carry: list[str] = field(default_factory=list)
event_log: list[Action] = field(default_factory=list)
call: _ResolvedCall = field(default_factory=_ResolvedCall)
Expand Down Expand Up @@ -321,13 +327,19 @@ def _refuse_if_halted(self, run_id: str) -> None:
)

def _enforce(self, signals: Sequence[Signal]) -> None:
for sig in sorted(signals, key=lambda s: _SEVERITY_RANK[s.severity], reverse=True):
policy = self._policy_by_name.get(sig.detector)
if policy is None:
continue # a detector with no paired policy is observe-only telemetry
action = policy.decide(sig, self.ledger)
if action.kind is ActionKind.HALT and self.enforce:
# set the durable flag BEFORE applying, so the kill switch survives a
# swallowed raise. Idempotent — marking twice is harmless.
self.ledger.mark_halted(action.run_id, action.reason)
self.controls.apply(action) # may raise Halt and unwind the agent loop
from tokenops.control.context import reset_current_controls, set_current_controls

tok = set_current_controls(self.controls)
try:
for sig in sorted(signals, key=lambda s: _SEVERITY_RANK[s.severity], reverse=True):
policy = self._policy_by_name.get(sig.detector)
if policy is None:
continue # a detector with no paired policy is observe-only telemetry
action = policy.decide(sig, self.ledger)
if action.kind is ActionKind.HALT and self.enforce:
# set the durable flag BEFORE applying, so the kill switch survives a
# swallowed raise. Idempotent — marking twice is harmless.
self.ledger.mark_halted(action.run_id, action.reason)
self.controls.apply(action) # may raise Halt and unwind the agent loop
finally:
reset_current_controls(tok)
1 change: 1 addition & 0 deletions src/tokenops/control/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def wrap_complete(
seg = f"run:{attr.run_id}"
boundary_id = f"{service or attr.agent}.chat"
traced = wrap_llm(boundary_id, dispatch)
controls.compaction_supported = True # wrap_complete IS the prompt-assembly hook

def governed(p: str, m: str, messages) -> object:
from tokenops.control.crossing import reset_wrap_owns_precall, wrap_owns_precall
Expand Down
38 changes: 31 additions & 7 deletions src/tokenops/control/policies/context_compaction.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""context_compaction — default; needs a prompt-assembly hook.
"""context_compaction — default; derive compaction capability from controls.

LLD row:
Detect: est_input ≥ ctx_max OR est_input rising over recent(run, W) (estimate from last
Expand All @@ -10,10 +10,16 @@

Fires at pre_call (it shapes the *next* prompt). Without an assembly hook it can only
observe — so it emits the signal for the dashboard and takes no action. It never HALTs.

Capability is derived at runtime from ``controls.compaction_supported`` (set by
``wrap_complete`` which supplies the prompt-assembly hook), not from a governance config
flag. See ``docs/policies/context_compaction.md``.
"""

from __future__ import annotations

import logging

from tokenops.control.core import (
Action,
ActionKind,
Expand All @@ -25,6 +31,8 @@
Signal,
)

_log = logging.getLogger(__name__)


class ContextCompactionDetector(Detector):
"""WARN when the estimated input is at/over the context ceiling, or rising across the
Expand Down Expand Up @@ -70,15 +78,31 @@ def pre_call(self, request: CallRequest, view: LedgerView) -> Signal | None:

class ContextCompactionPolicy(Policy):
"""MUTATE the prompt if an assembly hook exists; otherwise telemetry-only (ALLOW). Never
HALT — losing the cache discount or a bloated prompt is not a reason to kill a run."""
HALT — losing the cache discount or a bloated prompt is not a reason to kill a run.

Compaction capability is read from ``controls.compaction_supported`` (advertised by
``wrap_complete``), not from a config flag.
"""

name = "context_compaction"

def __init__(self, has_hook: bool = True) -> None:
self.has_hook = has_hook
_telemetry_only_logged: bool = False

def decide(self, signal: Signal, view: LedgerView) -> Action:
if not self.has_hook:
from tokenops.control.context import current_controls

controls = current_controls()
compaction_supported = (
getattr(controls, "compaction_supported", False) if controls else False
)
if not compaction_supported:
if not ContextCompactionPolicy._telemetry_only_logged:
_log.warning(
"context_compaction: no compaction hook available — "
"policy will emit telemetry only (ALLOW). "
"Use wrap_complete for full compaction support."
)
ContextCompactionPolicy._telemetry_only_logged = True
return Action(
kind=ActionKind.ALLOW,
run_id=signal.run_id,
Expand All @@ -94,5 +118,5 @@ def decide(self, signal: Signal, view: LedgerView) -> Action:
)


def build(ctx_max: int, *, window: int = 4, has_hook: bool = True) -> tuple[Detector, Policy]:
return ContextCompactionDetector(ctx_max, window=window), ContextCompactionPolicy(has_hook)
def build(ctx_max: int, *, window: int = 4) -> tuple[Detector, Policy]:
return ContextCompactionDetector(ctx_max, window=window), ContextCompactionPolicy()
2 changes: 1 addition & 1 deletion src/tokenops/ui/views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"tool_output_cap": '{"cap_tokens": 8000}',
"progress_guard": '{"window": 6, "repeats": 3, "max_corrections": 2}',
"cost_guard": '{"threshold": 0.8, "mode": "minimize"}',
"context_compaction": '{"ctx_max": 100000, "has_hook": false}',
"context_compaction": '{"ctx_max": 100000}',
"output_runaway": '{"repeats": 4, "max_retries": 2}',
}

Expand Down
2 changes: 1 addition & 1 deletion tests/test_actuators.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def test_deep_compaction_rewrites_outgoing_messages():
ledger = Ledger(price=toy_price)
controls = ApplyControls()
gov = Governor(ledger, controls)
gov.register(*context_compaction.build(ctx_max=10, has_hook=True)) # tiny ctx → always trips
gov.register(*context_compaction.build(ctx_max=10)) # tiny ctx → always trips
attr = _attr("r3")
ledger.open_run("r3")

Expand Down
22 changes: 19 additions & 3 deletions tests/test_context_compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from conftest import FakeView, make_attr, make_step
from tokenops.control import ActionKind, CallRequest, Usage
from tokenops.control.context import reset_current_controls, set_current_controls
from tokenops.control.engine import ApplyControls
from tokenops.control.policies import context_compaction


Expand All @@ -13,11 +15,23 @@ def _req(est):
)


def _with_compaction_supported():
"""Set up a controls context where compaction is supported (simulates wrap_complete)."""
controls = ApplyControls()
controls.compaction_supported = True
tok = set_current_controls(controls)
return tok


def test_trips_at_ctx_max_and_mutates():
det, pol = context_compaction.build(ctx_max=10_000)
sig = det.pre_call(_req(10_000), FakeView())
assert sig.severity.value == "warn"
assert pol.decide(sig, FakeView()).kind is ActionKind.MUTATE
tok = _with_compaction_supported()
try:
assert pol.decide(sig, FakeView()).kind is ActionKind.MUTATE
finally:
reset_current_controls(tok)


def test_below_silent():
Expand All @@ -33,6 +47,8 @@ def test_rising_trend_trips_early():


def test_no_hook_is_telemetry_only():
det, pol = context_compaction.build(ctx_max=10_000, has_hook=False)
"""Without compaction_supported on controls, the policy returns ALLOW (telemetry only)."""
det, pol = context_compaction.build(ctx_max=10_000)
sig = det.pre_call(_req(10_000), FakeView())
assert pol.decide(sig, FakeView()).kind is ActionKind.ALLOW # never HALT, never mutate
# No controls in context → compaction not supported → ALLOW
assert pol.decide(sig, FakeView()).kind is ActionKind.ALLOW
2 changes: 1 addition & 1 deletion tests/test_policies_wrap_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def same_tool(i: int):
def test_it_context_compaction_rewrites_messages_via_wrap():
controls = ApplyControls()
gov = Governor(Ledger(price=toy_price), controls)
gov.register(*context_compaction.build(ctx_max=10, has_hook=True))
gov.register(*context_compaction.build(ctx_max=10))
attr = _attr("r-ccx")
gov.ledger.open_run("r-ccx")
dispatch, calls = _dispatch()
Expand Down
Loading