ADR-017: RL-Driven Tool-Budget Calibration¶
Metadata¶
- Status: Proposed
- Date: 2026-07-08
- Decision Makers: Vijaykumar Singh
- Related ADRs: ADR-004 (Tool System Architecture)
Context¶
The session tool budget (tool_call_budget) is a static, globally configured value
that bounds how many tool calls an agent may make per session. It does not adapt
to observed execution reality. Analysis of the global RL database
(~/.victor/victor.db) revealed two principled signals that the static budget
ignores:
- Per-tool learned value (
rl_tool_q): tools with high selection counts and
high Q-values are "worth" more budget headroom than the baseline implies; tools
with low Q-values are budget sinks. - Aggregate outcome history (
decision_outcome): a high observed failure rate
indicates the agent is spending budget on fruitless tool calls and should be
tightened.
The current static budget therefore over-allocates budget when execution is
failing and under-allocates when high-value tools are consistently succeeding.
The question is how to let the budget respond to these signals without
destabilizing the runtime, coupling config types to observability data, or
introducing a parallel data path.
Decision¶
Introduce a three-phase, pure, settings-gated, default-off calibration
pipeline. The budget is calibrated by composing three pure modules that map RL
signals onto an immutable ToolSettings overlay:
BudgetSignalReader(victor/framework/rl/budget_signal_reader.py) —
loads aggregatedrl_tool_qQ-values anddecision_outcome
success/failure/reward aggregates from the existing global database via the
existingdb.cursor()/Tables.*access pattern. No new tables, no new DB
path.BudgetCalibrator(victor/framework/rl/budget_calibration.py) — pure
policy that maps the signals into aBudgetRecommendationcarrying a calibrated
tool_call_budget, aconfidencescore, and arationalestring.apply_budget_calibration()seam
(victor/framework/rl/budget_calibration_seam.py) — the single composition
entry point that runs reader → calibrator → overlay, gated by two settings:
tool_budget_calibration_enabled: bool = False(opt-in, mirroring the
prompt-optimization precedent of "controlled entirely via settings").tool_budget_calibration_min_confidence: float = 0.5(0.0–1.0).
The identity-return contract (the load-bearing invariant)¶
The seam returns settings.tools by identity whenever it does not apply an
overlay — i.e. when the toggle is off, when confidence is below threshold, or
when confidence is <= 0.0 (cold-start / degraded DB reads). Pipeline
exceptions are caught and also retain the baseline. This has two consequences:
- Zero behavior change by default. Default-off + identity-return means the
feature is inert until a user explicitly opts in and the calibrator is
sufficiently confident. No FEP-mandated behavior change occurs at
implementation time; the behavior change occurs only at opt-in. - One wiring site, zero consumption-site changes. Because the overlay is a
new immutableToolSettingswith the same schema, all ~30 existing
settings.tools.tool_call_budgetreaders (streaming/tool execution,
coordinators, tool builders, slash commands) consume the calibrated value
with no code change.
Rationale¶
- Why settings-gated, not feature-flag-gated? CLAUDE.md establishes the
precedent that runtime evolution behavior (prompt optimization, GEPA) is
"controlled entirely via settings, not feature flags." Budget calibration is
the same class of concern; settings-gating keeps it consistent and avoids
expanding theFeatureFlagenum. - Why immutable overlay + identity-return? It makes the feature provably
reversible (re-toggle off → baseline restored, unchanged object) and makes
the default-off path free of any allocation or branching beyond the toggle
check. - Why pure policy?
BudgetCalibrator.recommend()is a pure function of its
inputs. This is maximally testable (no I/O, no global state) and lets the same
pipeline serve both an "inspect" (dry-run) mode and an "apply" mode without a
parallel harness. - Pro: Adaptivity to observed execution; principled signal derivation;
safe-by-default; minimal blast radius. - Con: Adds two optional settings to a surface already carrying ~26 config
groups; opt-in means the benefit is realized only by users who enable it.
Consequences¶
- Positive: The budget can tighten on observed failure and relax on
consistent high-value tool success, reducing wasted tool calls and cost while
preserving headroom for productive sessions. The pipeline is reusable for A/B
evaluation without runtime commitment. - Negative: Introduces a bootstrap-time DB read when enabled (amortized once
per session). Opt-in means most users see no change; the feature requires
accumulated RL signal to produce non-baseline recommendations. - Neutral:
ToolSettingsadds two new optional fields with safe defaults
and bounds (tool_budget_calibration_enabled=False,
tool_budget_calibration_min_confidencein [0,1]); no existing field is removed
or renamed, so no migration is needed and all 4 canonical budget consumers
require no edits. The global DB schema is unchanged.
Implementation¶
Completed (committed, 34 tests GREEN across 3 modules):
| Phase | Commit | Module | Tests |
|---|---|---|---|
| 1 — Pure policy | 0f6f8db45 |
budget_calibration.py |
20 |
| 2 — Persistence adapter | b33ee3c87 |
budget_signal_reader.py |
8 |
| 3 — Application seam | b47861ef4 |
budget_calibration_seam.py + 2 settings |
6 |
Docs parity: docs/reference/configuration-options.md documents both settings
(commit 6cca24360).
Deferred (blocked on FEP-0002 review — see "References"):
- The single wiring call inside
Agent.create()(victor/framework/agent.py,
after the existingsession_config.apply_to_settings(settings)call), assigning
settings.tools = apply_budget_calibration(settings). (Review correction: the
apply point isAgent.create(), notAgentFactory.create().) - An integration test asserting the orchestrator receives the overlay only when
no explicit budget override wins. - Runtime behavior change is intentionally gated behind the FEP's review window.
Completed prerequisite (unblocked):
SessionConfig.apply_to_settings()now appliesself.tool_budgetto the
canonicalsettings.tools.tool_call_budgetfield (previously validated but
dropped — a pre-existing dead-wire bug). TDD coverage in
tests/unit/framework/test_session_config.py::TestSessionConfigApplyToolBudget
(3 tests). This makes the FEP-0002explicit_overrideprecedence path
exercisable against the real CLI override.
Alternatives Considered¶
- Mutate
ToolSettingsin place. Rejected: breaks immutability of the
overlay and the "single mutation point isSessionConfig" mandate. - Add a
calibration_statusprovenance field toToolSettings. Rejected:
couples a config type to runtime observability; read in ~30 sites. Provenance
is surfaced via structured logs and a slash command instead, keeping the config
object pure. - Feature-flag gating. Rejected: inconsistent with the settings-driven
precedent for runtime-evolution behavior. - Per-tool dynamic budget allocation at call time. Rejected as
out-of-scope: this ADR addresses session budget calibration, not per-call
admission control.
References¶
- FEP-0002: RL-Driven Tool-Budget Calibration (
docs/feps/fep-0002-rl-budget-calibration.md) - ADR-004: Tool System Architecture
docs/reference/configuration-options.md— "Tool Execution & Budgets"- CLAUDE.md — "Prompt Optimization Settings" (settings-vs-flag precedent)
Revision History¶
| Date | Version | Changes | Author |
|---|---|---|---|
| 2026-07-08 | 1.0 | Initial ADR | Vijaykumar Singh |