Skip to content

feat: expose expected_cost + est_calls_remaining on BudgetAdvisory (#49)#53

Merged
achris7 merged 5 commits into
Floe-Labs:mainfrom
hatimanees:feat/advisory-expected-cost
Jul 25, 2026
Merged

feat: expose expected_cost + est_calls_remaining on BudgetAdvisory (#49)#53
achris7 merged 5 commits into
Floe-Labs:mainfrom
hatimanees:feat/advisory-expected-cost

Conversation

@hatimanees

@hatimanees hatimanees commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes #49.

Problem

advisory() already computes the guard's next-call estimate internally (the
costlier of the last LLM and last tool call — the value _default_estimate_locked
uses 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 the BudgetAdvisory interface (js):

  • expected_cost / expectedCost — the next-call estimate; 0.0 until the
    first call is recorded.
  • est_calls_remaining / estCallsRemainingfloor(remaining_usd / expected_cost),
    or None/null when no call has been recorded yet (unknown, not a misleading 0).

Both are derived in advisory() from existing state — no new tracking, no new
locks. A +1e-9 epsilon absorbs float noise in the division (e.g. 0.6/0.2
3, not 2), matching the existing used_bps epsilon and keeping Python int()
/ JS Math.floor parity.

Compatibility

Fully backward-compatible. The frozen dataclass gains two defaulted fields; the
TS object gains two keys. Existing advisory() consumers are unaffected.

Tests

  • Python: 245 passed, 1 skipped (3 new advisory cases: cold estimate → None;
    warm estimate → floor; costlier-of-llm-and-tool).
  • JS: 80 passed, tsc --noEmit clean (2 new parity cases).

Versions bumped per the version-guard: py 0.11.0 / js 0.8.0, CHANGELOG updated.

Summary by CodeRabbit

  • New Features
    • Budget advisories now report call headroom: expected_cost (next-call estimate) and est_calls_remaining (how many more calls the remaining budget can support).
    • expected_cost uses the higher of the last priced LLM and tool costs; est_calls_remaining is unavailable until a call is recorded.
    • Existing advisory/budget and utilization fields remain unchanged.
  • Documentation
    • Updated advisory() documentation to describe the new headroom indicators and their meaning.
  • Tests
    • Added/extended unit coverage for initial state, post-call computation, and the higher-cost selection behavior.
  • Chores
    • Bumped Python and JavaScript package versions.

…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.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 857c4e1c-92df-4269-b2af-c7d1157d29cf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

BudgetAdvisory now reports expected next-call cost and estimated remaining calls in Python and TypeScript. Implementations derive these from recorded costs and settled budget, with tests, documentation, changelog, and package versions updated.

Changes

Advisory headroom estimates

Layer / File(s) Summary
Python advisory estimates
src/floe_guard/guard.py, tests/test_advisory.py
Python adds expected_cost and est_calls_remaining, deriving them from recorded call costs and remaining budget with coverage for initial and populated states.
TypeScript advisory estimates
js/src/guard.ts, js/test/advisory.test.ts
TypeScript adds expectedCost and estCallsRemaining, computes them in advisory(), and tests empty and recorded-call behavior.
Release and documentation updates
pyproject.toml, js/package.json, CHANGELOG.md, README.md
Python and JavaScript versions are incremented, and the new advisory fields are documented.

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
Loading

Possibly related PRs

Suggested reviewers: achris7, shivamfloe

Poem

I’m a rabbit with a budget in sight,
Counting each carrot call just right.
New fields hop into the advisory stream,
Showing call headroom bright and keen.
The ledger helps plan every bite!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: exposing new BudgetAdvisory fields in Python and JavaScript.
Linked Issues check ✅ Passed The changes add expected_cost and est_calls_remaining with the requested semantics and preserve existing advisory fields.
Out of Scope Changes check ✅ Passed The docs, tests, and version bumps align with the feature and do not introduce unrelated functionality.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b8faa47 and 4d6a17b.

⛔ Files ignored due to path filters (1)
  • js/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • CHANGELOG.md
  • README.md
  • js/package.json
  • js/src/guard.ts
  • js/test/advisory.test.ts
  • pyproject.toml
  • src/floe_guard/guard.py
  • tests/test_advisory.py

Comment thread CHANGELOG.md Outdated
Comment thread js/src/guard.ts Outdated
Comment thread js/test/advisory.test.ts
Comment thread src/floe_guard/guard.py
Comment thread tests/test_advisory.py Outdated
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/expectedCost and est_calls_remaining/estCallsRemaining to 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.

Comment thread js/test/advisory.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Document the two cold-state values separately.

expected_cost is 0.0/0 before the first call; only est_calls_remaining is None/null. The current wording incorrectly says both fields are None/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

📥 Commits

Reviewing files that changed from the base of the PR and between 4d6a17b and bdf4fee.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • js/src/guard.ts
  • js/test/advisory.test.ts
  • tests/test_advisory.py

@achris7
achris7 requested a review from Copilot July 25, 2026 21:07
@achris7

achris7 commented Jul 25, 2026

Copy link
Copy Markdown
Member

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • js/package-lock.json: Generated file

Comment thread pyproject.toml
Comment on lines 5 to 8
[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."
@achris7
achris7 merged commit ebdbc04 into Floe-Labs:main Jul 25, 2026
10 checks passed
@hatimanees
hatimanees deleted the feat/advisory-expected-cost branch July 26, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] floe-guard: expose expected_cost + est_calls_remaining on BudgetAdvisory

3 participants