Skip to main content

exoclaw-turn-budget

Per-turn and per-day token + iteration budgets for exoclaw, with progressive warnings before any cutoff and configurable enforcement actions.

Why

Provider weekly quotas are measured in tokens (or "prompts" that translate to tokens under the hood). A single runaway turn — say, an unbounded research loop chaining hundreds of expensive web_search calls — can consume an entire week's quota in 90 minutes.

A hard max_iterations cap stops the loop but kills useful work mid-task with no warning. This plugin instead provides:

  • Turn budget — caps iterations and tokens per individual turn. Warnings inject as user-role messages at configurable thresholds (default 50/80/90%) so the agent can wrap up gracefully. Default enforcement is CUTOFF.
  • Daily budget — tracks cumulative tokens for the configured primary models (e.g., the GLM family for z.ai's pooled weekly quota), auto-resets at the UTC day boundary. Default enforcement is FALLBACK — silently demote to a cheaper model when the daily allotment is spent so the bot keeps working through end-of-day instead of going dark.

Both layers are built on a single shared BudgetWrapper primitive and share the same Enforcement enum: OBSERVE, WARN, CUTOFF, FALLBACK.

Install

pip install exoclaw-turn-budget

Usage

Stack the wrappers (turn → daily → real provider):

from exoclaw import Exoclaw
from exoclaw_turn_budget import (
    BudgetWrapper,
    DailyBudgetConfig,
    DailyBudgetTracker,
    Enforcement,
    TurnBudgetConfig,
    TurnBudgetPolicy,
    TurnBudgetTracker,
)

turn_tracker = TurnBudgetTracker(TurnBudgetConfig(
    iteration_budget=50,
    token_budget=1_500_000,
    warning_thresholds=(0.5, 0.8, 0.9),
    enforcement=Enforcement.CUTOFF,
))

daily_tracker = DailyBudgetTracker(DailyBudgetConfig(
    daily_budget=35_000_000,
    primary_models=("glm-4.7", "glm-5.1"),  # what counts against the daily pool
    enforcement=Enforcement.FALLBACK,
    fallback_model="minimax/minimax-m2.7",
))

# Stack: real provider → daily layer → turn layer → AgentLoop
provider = BudgetWrapper(real_provider, daily_tracker)
provider = BudgetWrapper(provider, turn_tracker)

app = Exoclaw(
    provider=provider,
    iteration_policy=TurnBudgetPolicy(turn_tracker),  # bridges turn cutoff into the loop
    conversation=conversation,
)

The wrapper picks up enforcement and fallback_model from tracker.config automatically — no need to set them in two places.

Turn budget configuration

Parameter Default Description
iteration_budget 50 Hard cap on LLM iterations within one turn. None to disable.
token_budget 1_500_000 Hard cap on total tokens (input + output, summed across iterations). None to disable.
warning_thresholds (0.5, 0.8, 0.9) Fractions at which to inject a warning message. Each fires at most once per turn.
enforcement Enforcement.CUTOFF Action at 100% utilization. See "Enforcement modes" below.
fallback_model None Required when enforcement == FALLBACK.
warning_template see config Template for threshold warnings. Substitutions: {scope}, {pct}, {used}, {cap}, {unit}.
cutoff_template see config Template for the cutoff message used by CUTOFF/WARN. Same substitutions.

Both budgets are consumed simultaneously — whichever exhausts first triggers enforcement. The substitution variables describe whichever axis is closer to exhaustion when the message fires (so {used}/{cap} {unit} reads as either 40/50 iterations or 1200000/1500000 tokens).

Daily budget configuration

Parameter Default Description
daily_budget 35_000_000 Tokens allowed per day for the primary models. Aim slightly below weekly_quota / 7.
primary_models () Models that count against the budget. Empty tuple means "all models count".
warning_thresholds () Fractions at which to inject a warning. Default empty (silent).
enforcement Enforcement.FALLBACK Action at 100% utilization.
fallback_model None Required when enforcement == FALLBACK.
reset_hour_utc 0 Hour of day (0–23, UTC) at which the budget rolls over.
warning_template / cutoff_template / fallback_template see config Same substitutions as the turn config. fallback_template is used at the cutoff point when enforcement == FALLBACK.

Tokens spent on the fallback_model do not count against the daily budget — when primary_models is set, only matches deplete it.

Durability — the BudgetStateStore protocol

By default the daily counter lives in process memory. A container restart at 14:30 UTC after spending 7M of 10M would reset the counter to zero — bad for long-running server deploys.

DailyBudgetTracker accepts an optional store= argument implementing the BudgetStateStore protocol:

class BudgetStateStore(Protocol):
    def load(self) -> dict | None: ...   # called once at construction
    def save(self, state: dict) -> None: ...   # called on every record/reset
    def clear(self) -> None: ...   # ops use only

Three implementations:

Class Source When to use
InMemoryBudgetStore this package, default Chip deploys, tests, anything where restart-resets-counter is fine
FileBudgetStore(path) this package Server deploys — JSON file with atomic write (temp + os.rename); a SIGKILL mid-write leaves the previous state intact rather than corrupting the counter
DBOSBudgetStore exoclaw-executor-dbos (planned) Deployments that already use DBOS for durable agent-loop state — wraps reads/writes in @DBOS.step() for full workflow-replay safety
from exoclaw_turn_budget import DailyBudgetTracker, FileBudgetStore

tracker = DailyBudgetTracker(
    config,
    store=FileBudgetStore("/var/lib/luna/budget-state.json"),
)

Security: when using FileBudgetStore with an agent that has filesystem tools (read/write/edit/exec), put the state path outside the agent's workspace. Both WorkspaceTool's allowed_dir and ExecTool's restrict_to_workspace block paths outside the workspace boundary, so a state file inside workspace/ would let a prompt-injected or self-correcting agent edit its own quota counter.

A typical layout:

/root/.nanobot/                  ← volume-mounted, durable across restarts
├── budget-state.json            ← FileBudgetStore path (outside workspace)
└── workspace/                   ← agent's filesystem tools allowed here
    ├── skills/
    └── ...

Enforcement modes

Mode Behavior at 100% utilization
OBSERVE Track only — nothing happens. Useful for measurement before flipping on real enforcement.
WARN Inject the cutoff message as a one-time user-role notice, then forward unchanged. Continues working past the limit.
CUTOFF Synthesize a final response containing the cutoff message and stop. The inner provider is not called.
FALLBACK Rewrite the model argument to fallback_model and forward. Tokens used on the fallback don't count against the budget.

Threshold warnings (50/80/90%) are orthogonal — they always fire if warning_thresholds is non-empty, regardless of the enforcement mode chosen for the cutoff.

How warnings reach the agent

When utilization crosses a threshold, the next chat() call has the warning appended as a synthetic user-role message at the end of messages. The agent sees:

[Budget notice] You've used 80% of your turn budget
(40/50 iterations). Wrap up your current line of work and
respond to the user soon.

The injection is ephemeral — only that one model call sees it, but the agent's response (the wrap-up plan it adopts) gets persisted as normal. Each threshold fires at most once per turn; subsequent iterations don't repeat the same warning. Threshold warnings are also suppressed once the budget is exhausted, so the agent doesn't get a stale "you're at 50%" notice after it has already hit the limit.

Observability hooks

The plugin doesn't emit logs or metrics on its own — it stays silent so consumers can route signal through whatever telemetry stack they already use. Two optional callbacks on BudgetWrapper cover the lifecycle:

def on_threshold_crossed(*, scope, threshold, utilization, used, cap, unit): ...
def on_limit_reached(*, scope, utilization, used, cap, unit): ...

provider = BudgetWrapper(
    inner, tracker,
    on_threshold_crossed=lambda **kw: logger.info("turn_budget_threshold", **kw),
    on_limit_reached=lambda **kw: logger.info("turn_budget_exhausted", **kw),
)
  • scope"turn" or "daily" (from the tracker's SCOPE).
  • threshold — fraction in warning_thresholds that was just crossed (on_threshold_crossed only).
  • utilization — current ratio (0.01.0+).
  • used / cap / unit — whichever axis is closer to exhaustion ("iterations" or "tokens"), so the payload matches the in-band warning template substitutions.

Both hooks are dedup'd internally — on_threshold_crossed fires once per threshold crossing, on_limit_reached fires once per exhaustion event (and re-arms when the tracker resets, so the next turn or day boundary can fire it again). Exceptions raised inside a hook are swallowed so a buggy callback can't crash the agent loop.

Subagents

Each AgentLoop instance gets its own TurnBudgetTracker. To budget a parent turn together with its subagent spawns, share the same tracker (and policy) when constructing the subagent loop. The DailyBudgetTracker is naturally shared across all loops in a process since it's keyed on wall-clock time, not turn boundaries.

Composition with other policies

TurnBudgetPolicy only handles turn-boundary detection and the optional cutoff. Pair it with exoclaw-loop-detection when you also want pattern-based detection of degenerate tool-call loops — they don't conflict, but exoclaw only accepts a single iteration_policy. A meta-policy that delegates to both is left as an exercise.

MicroPython compatibility

This package opts into the workspace's MP CI gate via [tool.exoclaw] mp_compat = true. The runtime branches (plain-class Enforcement constants instead of enum.Enum, dual @dataclass / hand-written __init__ configs, time.time() day-key) are exercised by tests/micro/test_imports.py.

Release files for exoclaw-turn-budget 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for exoclaw-turn-budget 0.6.0
File Size Uploaded
exoclaw_turn_budget-0.6.0.tar.gz 19.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for exoclaw-turn-budget 0.6.0
File Interpreter ABI Platform
exoclaw_turn_budget-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.3 kB

Release files / exoclaw_turn_budget-0.6.0.tar.gz

Download URL exoclaw_turn_budget-0.6.0.tar.gz
Size 19.1 kB
Tags Source
SHA-256 checksum
How to use checksums
35c2615df2a5ff7b1eefcdbd45c378ef2275bd553cf065be9ece6b2b658837a6
BLAKE2b-256 checksum
How to use checksums
e93cab08766cb9e3b04324dbac1942f22a274b021f0f49c25ee76a7e8fa74953
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / exoclaw_turn_budget-0.6.0-py3-none-any.whl

Download URL exoclaw_turn_budget-0.6.0-py3-none-any.whl
Size 23.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bee42b6d8c3af6606c3e1acec0f07f0eceef41cea16108fe755efa0585e60ce8
BLAKE2b-256 checksum
How to use checksums
48c90df6a6ca89bcddd82b526b7a45c70fc60dc8eec5cd571d458d3af4501c4c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page