feat: expose expected_cost + est_calls_remaining on BudgetAdvisory (#49)#53
Conversation
…loe-Labs#49) advisory() already knows the guard's next-call estimate (the costlier of the last LLM and last tool call) but never surfaced it. Planners could see how many dollars were left, not how many more calls that buys. Add two additive fields to BudgetAdvisory (py) / the BudgetAdvisory interface (js): - expected_cost / expectedCost: the next-call estimate; 0.0 until the first call is recorded. - est_calls_remaining / estCallsRemaining: floor(remaining_usd / expected_cost), or None/null when no call has been recorded yet (unknown, not zero). Both are derived in advisory() from existing state - no new tracking, no new locks. Existing consumers are unaffected (frozen dataclass gains defaulted fields; the TS object gains two keys). README + CHANGELOG updated.
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthrough
ChangesAdvisory headroom estimates
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant BudgetGuard
participant BudgetAdvisory
Caller->>BudgetGuard: advisory()
BudgetGuard->>BudgetGuard: Read settled spend and last recorded costs
BudgetGuard->>BudgetAdvisory: Calculate expected cost and remaining calls
BudgetAdvisory-->>Caller: Return advisory headroom fields
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 13: Update the CHANGELOG.md entry for py 0.11.0 / js 0.8.0 to avoid the
future date: keep it under Unreleased without a date, or replace the date with
the actual release date when publishing.
In `@js/src/guard.ts`:
- Around line 116-127: Update the exported BudgetAdvisory type so the new
expectedCost and estCallsRemaining fields are optional, preserving source
compatibility for consumers constructing advisory objects without them. Keep
their existing meanings and value types for consumers that provide or read these
fields.
In `@js/test/advisory.test.ts`:
- Around line 61-68: Extend the advisory tests around BudgetGuard.advisory to
record both an LLM cost and a tool cost with different values, then assert
expectedCost uses the larger cost and estCallsRemaining is calculated from that
maximum. Preserve the existing lastToolCost test while adding parity coverage
for the combined-cost case.
In `@src/floe_guard/guard.py`:
- Around line 585-589: Use the exact floor for estimated remaining calls by
removing the 1e-9 adjustment from the int calculation in src/floe_guard/guard.py
lines 585-589. Apply the same change to the corresponding calculation in
js/src/guard.ts lines 578-581, and add a boundary test confirming a legitimate
quotient just below an integer is floored below that integer.
In `@tests/test_advisory.py`:
- Around line 67-82: Update test_est_calls_remaining_after_a_call and
test_est_calls_remaining_uses_costlier_of_llm_and_tool to initialize cost state
through the public record() and record_tool() APIs instead of assigning
_last_llm_cost, _last_tool_cost, or spent_usd directly. Use deterministic inputs
and a manual price where needed, then preserve the existing advisory
expected_cost and est_calls_remaining assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5b148efe-9159-4e3f-9609-f455f7d93359
⛔ Files ignored due to path filters (1)
js/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
CHANGELOG.mdREADME.mdjs/package.jsonjs/src/guard.tsjs/test/advisory.test.tspyproject.tomlsrc/floe_guard/guard.pytests/test_advisory.py
Address CodeRabbit feedback on Floe-Labs#53: - BudgetAdvisory.expectedCost / estCallsRemaining are now optional in TS, so the additive fields don't break any code constructing an advisory literal (advisory() still always sets them). Python already ships defaulted fields. - Add a JS test asserting expectedCost is the costlier of the last LLM and tool call, matching the Python suite. - Keep the CHANGELOG entry under Unreleased instead of a future release date.
Address CodeRabbit: the Python advisory tests set private cost fields directly, so they'd pass even if record()/record_tool() stopped feeding advisory(). Drive state through the public API with a deterministic manual price instead.
There was a problem hiding this comment.
Pull request overview
This PR extends BudgetGuard.advisory() in both Python and TypeScript to expose the guard’s internal next-call cost estimate and a derived “calls remaining” headroom metric, so planners can reason in terms of calls (not just dollars) while keeping the change additive/backward-compatible.
Changes:
- Add
expected_cost/expectedCostandest_calls_remaining/estCallsRemainingto the advisory payload (computed from existing last-call state). - Add Python + JS tests covering cold (unknown) and warm (floor division) behavior, plus “max(last LLM, last tool)” logic.
- Bump package versions and update README + CHANGELOG to document the new advisory fields.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/floe_guard/guard.py |
Adds two defaulted fields to BudgetAdvisory and computes them in BudgetGuard.advisory(). |
tests/test_advisory.py |
Adds new Python test cases for expected_cost and est_calls_remaining. |
js/src/guard.ts |
Extends BudgetAdvisory interface and returns new fields from BudgetGuard.advisory(). |
js/test/advisory.test.ts |
Adds new JS parity tests for the new advisory fields. |
README.md |
Documents the new advisory fields and their meaning (None until first call). |
CHANGELOG.md |
Records the new advisory fields under Unreleased. |
pyproject.toml |
Bumps Python package version to 0.11.0. |
js/package.json |
Bumps JS package version to 0.8.0. |
js/package-lock.json |
Updates lockfile version metadata for 0.8.0. |
Files not reviewed (1)
- js/package-lock.json: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
CHANGELOG.md (1)
15-18: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDocument the two cold-state values separately.
expected_costis0.0/0before the first call; onlyest_calls_remainingisNone/null. The current wording incorrectly says both fields areNone/null.Suggested wording
- recorded. `None`/`null` until the first call is recorded. + recorded. `expected_cost` is `0.0`/`0`, while `est_calls_remaining` is + `None`/`null` until the first call is recorded.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CHANGELOG.md` around lines 15 - 18, Update the changelog entry for advisory() so it states that expected_cost is 0.0/0 before the first call, while only est_calls_remaining is None/null; preserve the existing TypeScript names and issue reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@CHANGELOG.md`:
- Around line 15-18: Update the changelog entry for advisory() so it states that
expected_cost is 0.0/0 before the first call, while only est_calls_remaining is
None/null; preserve the existing TypeScript names and issue reference.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 6a42d9f7-353f-4245-9787-830ae9470346
📒 Files selected for processing (4)
CHANGELOG.mdjs/src/guard.tsjs/test/advisory.test.tstests/test_advisory.py
|
@CodeRabbit review |
✅ Action performedReview finished.
|
| [project] | ||
| name = "floe-guard" | ||
| version = "0.10.0" | ||
| version = "0.11.0" | ||
| description = "Local budget guardrail for AI agents — hard-stops a runaway loop before its next LLM call crosses a spend ceiling. No account, no network." |
Closes #49.
Problem
advisory()already computes the guard's next-call estimate internally (thecostlier of the last LLM and last tool call — the value
_default_estimate_lockeduses to size a default reservation), but never surfaces it. A planner can see how
many dollars are left, not how many more calls that buys.
Change
Two additive fields on
BudgetAdvisory(py) and theBudgetAdvisoryinterface (js):expected_cost/expectedCost— the next-call estimate;0.0until thefirst call is recorded.
est_calls_remaining/estCallsRemaining—floor(remaining_usd / expected_cost),or
None/nullwhen no call has been recorded yet (unknown, not a misleading0).Both are derived in
advisory()from existing state — no new tracking, no newlocks. A
+1e-9epsilon absorbs float noise in the division (e.g.0.6/0.2→3, not2), matching the existingused_bpsepsilon and keeping Pythonint()/ JS
Math.floorparity.Compatibility
Fully backward-compatible. The frozen dataclass gains two defaulted fields; the
TS object gains two keys. Existing
advisory()consumers are unaffected.Tests
245 passed, 1 skipped(3 new advisory cases: cold estimate →None;warm estimate →
floor; costlier-of-llm-and-tool).80 passed,tsc --noEmitclean (2 new parity cases).Versions bumped per the version-guard: py 0.11.0 / js 0.8.0, CHANGELOG updated.
Summary by CodeRabbit
expected_cost(next-call estimate) andest_calls_remaining(how many more calls the remaining budget can support).expected_costuses the higher of the last priced LLM and tool costs;est_calls_remainingis unavailable until a call is recorded.advisory()documentation to describe the new headroom indicators and their meaning.