Skip to content

feat: budget-aware retry / graceful-degradation helper (closes #45)#48

Merged
achris7 merged 4 commits into
Floe-Labs:mainfrom
hatimanees:feat/budget-aware-retry
Jul 23, 2026
Merged

feat: budget-aware retry / graceful-degradation helper (closes #45)#48
achris7 merged 4 commits into
Floe-Labs:mainfrom
hatimanees:feat/budget-aware-retry

Conversation

@hatimanees

@hatimanees hatimanees commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #45.

Agents already know how much budget is left (remaining_usd / advisory().near_limit), but retries did not use that signal. A failed call would either retry the same expensive path or not think about budget at all.

This PR adds a thin helper — with_budget_retry (Python) and withBudgetRetry (TypeScript) — that decides whether to retry based on budget headroom. It does not pick models or own the HTTP/retry transport. The caller still owns what "cheaper" means via on_degrade.

Behavior in plain English

Imagine a $1 budget and a call that fails once:

Situation What the helper does
Ample budget (lots of money left) Retry the same call again
Near the limit (~80% spent by default) Call your on_degrade callback so you can switch to a cheaper model / path
Retry would go over the ceiling Call guard.check(estimated_cost) and abort — no second spend

Tiny example

from floe_guard import BudgetGuard, RetryPlan, with_budget_retry

guard = BudgetGuard(limit_usd=1.00)

def on_degrade(exc, advisory):
    # YOU choose the cheaper model — floe-guard only asks when near_limit
    return RetryPlan(call=mini_model, estimated_cost=0.01)

result = with_budget_retry(
    guard,
    premium_model,
    estimated_cost=0.20,
    max_attempts=2,
    on_degrade=on_degrade,
)

Try the no-network demo: python examples/budget_retry.py

What this is / is not

What changed

  • Python: src/floe_guard/retry.pywith_budget_retry, async_with_budget_retry, RetryPlan
  • TypeScript: js/src/retry.tswithBudgetRetry
  • Example: examples/budget_retry.py
  • Tests: tests/test_budget_retry.py, js/test/retry.test.ts
  • Docs: README + CHANGELOG

Test plan

  • python -m pytest tests/test_budget_retry.py -v — 6/6 passed
  • npm test -- test/retry.test.ts (in js/) — 5/5 passed
  • python examples/budget_retry.py — near-limit path prints cheaper-model success
  • Reviewer: skim acceptance cases (ample / near-limit / over-cap) in the tests

Summary by CodeRabbit

  • New Features
    • Added budget-aware retry helpers for Python (sync/async) and TypeScript, including configurable retry plans, retryability, and “near-limit” graceful degradation.
    • Budget is checked before each retry; retries are blocked when they would exceed the available budget.
  • Documentation
    • Added a “Budget-aware retry” section with Python and TypeScript examples and behavior details.
  • Tests
    • Added sync/async tests for normal retries, degraded-plan fallback, over-budget blocking, non-retryable errors, and invalid retry settings.
  • Bug Fixes
    • Improved Python validation/retry behavior: non-integer max_attempts is rejected and only Exception types are retried (excluding interrupt/cancellation errors).

Compose retries over remaining budget: ample headroom retries as-is, near-limit asks on_degrade for a cheaper path, and over-cap aborts via check() before spending.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds budget-aware retry helpers for Python and TypeScript. Retries can use the original call, switch to a degraded plan near the budget limit, or stop before execution when the estimated retry cost exceeds the budget. Tests, exports, documentation, version metadata, and an example are included.

Changes

Budget-aware retry

Layer / File(s) Summary
Python retry implementation and validation
src/floe_guard/retry.py, src/floe_guard/__init__.py, tests/test_budget_retry.py
Adds synchronous and asynchronous retry helpers with retry predicates, degradation callbacks, pre-retry budget checks, public exports, and coverage for retry, degradation, blocking, validation, and interruption behavior.
TypeScript retry implementation and validation
js/src/retry.ts, js/src/index.ts, js/test/retry.test.ts
Adds retry plan and options types, withBudgetRetry, public exports, and Vitest coverage for retry, degradation, budget blocking, error filtering, and input validation.
Usage documentation and runnable example
README.md, CHANGELOG.md, examples/budget_retry.py
Documents budget-aware retry behavior and adds a no-network example showing premium-call failure, cheaper degradation, and call counts.
Package release metadata
pyproject.toml, js/package.json
Updates the Python and JavaScript package versions.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant RetryHelper
  participant Provider
  participant BudgetGuard
  participant DegradeCallback
  Caller->>RetryHelper: start operation
  RetryHelper->>Provider: execute initial call
  Provider-->>RetryHelper: transient failure
  RetryHelper->>BudgetGuard: request advisory
  BudgetGuard-->>RetryHelper: near-limit status
  RetryHelper->>DegradeCallback: request cheaper RetryPlan
  DegradeCallback-->>RetryHelper: return degraded plan
  RetryHelper->>BudgetGuard: check estimated retry cost
  BudgetGuard-->>RetryHelper: permit or reject retry
  RetryHelper->>Provider: execute selected retry call
  Provider-->>Caller: return result or final error
Loading

Possibly related PRs

Suggested reviewers: achris7, shivamfloe, rajbhensdadiya

Poem

I’m a rabbit with retries tucked under my ear,
Checking the budget before hops draw near.
When coins run low, I choose carrots small,
And skip the costly leap past the wall.
Python and TypeScript now bounce bright—
Cheaper hops keep the budget light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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 summarizes the main change: adding budget-aware retry with graceful degradation.
Linked Issues check ✅ Passed The PR implements the requested Python and TypeScript helpers, including ample-budget retries, near-limit degradation, and over-cap aborts.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are present; docs, examples, tests, changelog, and version bumps all support the linked feature.
✨ 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.

Satisfy version-guard after shipping the new retry helper in both packages.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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: 2

🤖 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 `@src/floe_guard/retry.py`:
- Around line 72-73: Update both retry helpers’ max_attempts validation at
src/floe_guard/retry.py lines 72-73 and 99-100 to reject non-integer values
before calling range(), while preserving the existing max_attempts >= 1
validation and ValueError behavior.
- Around line 80-83: Replace BaseException with Exception in both retry catch
blocks in sync_with_budget_retry and async_with_budget_retry, so
KeyboardInterrupt, SystemExit, and asyncio.CancelledError propagate without
retrying; add a regression test verifying cancellation propagation.
🪄 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: 226503c8-ee37-4bda-b0ba-ebf21e5b84df

📥 Commits

Reviewing files that changed from the base of the PR and between a187fdb and 9e959d9.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • examples/budget_retry.py
  • js/src/index.ts
  • js/src/retry.ts
  • js/test/retry.test.ts
  • src/floe_guard/__init__.py
  • src/floe_guard/retry.py
  • tests/test_budget_retry.py

Comment thread src/floe_guard/retry.py Outdated
Comment thread src/floe_guard/retry.py Outdated
Reject non-integer max_attempts and catch Exception so control-flow exits are not retried.

Co-authored-by: Cursor <cursoragent@cursor.com>
@hatimanees
hatimanees force-pushed the feat/budget-aware-retry branch from 05a57e9 to 45a0e03 Compare July 23, 2026 17:52
@hatimanees

Copy link
Copy Markdown
Contributor Author

@achris7 : for #45 — helper retries as-is with headroom, degrades via on_degrade near limit, and aborts over-cap with check(). Example + Py/TS; adapters left alone per out-of-scope.

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

Adds an opt-in, budget-aware retry/composition helper to both the Python and TypeScript SDKs so callers can (a) retry normally with ample headroom, (b) degrade to a cheaper retry plan near the limit via a callback, and (c) hard-block a retry that would exceed the ceiling via guard.check().

Changes:

  • Python: introduce RetryPlan, with_budget_retry, and async_with_budget_retry plus tests and a runnable demo example.
  • TypeScript: introduce withBudgetRetry plus tests and public exports via the JS index barrel.
  • Docs/releases: README section, CHANGELOG entries, and version bumps for both packages.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_budget_retry.py New Python test coverage for ample/near-limit/over-budget behavior and control-flow exceptions.
src/floe_guard/retry.py Implements the new Python retry helpers and RetryPlan.
src/floe_guard/init.py Exposes the new retry APIs at the package top level; bumps __version__.
README.md Documents budget-aware retry usage and points to the example.
pyproject.toml Bumps Python package version to 0.9.1.
js/test/retry.test.ts New Vitest coverage for TypeScript helper behavior.
js/src/retry.ts Implements withBudgetRetry and related types/options.
js/src/index.ts Re-exports retry helper/types from the JS package entrypoint.
js/package.json Bumps JS package version to 0.6.0.
js/package-lock.json Updates lockfile version fields to 0.6.0.
examples/budget_retry.py Adds a no-network demo showcasing near-limit degradation.
CHANGELOG.md Adds release notes for the new helpers and max-attempt validation/catch behavior.
Files not reviewed (1)
  • js/package-lock.json: Generated file
Comments suppressed due to low confidence (1)

src/floe_guard/retry.py:118

  • Same cancellation issue in the async helper: except Exception can catch asyncio.CancelledError on some supported Python versions, resulting in retries after cancellation. If the intent is to always propagate cancellation immediately, explicitly re-raise CancelledError before applying retry logic.
        try:
            return await plan.call()
        except Exception as exc:
            if attempt >= max_attempts or not should_retry(exc):
                raise

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/floe_guard/retry.py
# Conflicts:
#	CHANGELOG.md
#	pyproject.toml
#	src/floe_guard/__init__.py
@achris7
achris7 merged commit b8faa47 into Floe-Labs:main Jul 23, 2026
9 of 10 checks passed

@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 (2)
tests/test_budget_retry.py (1)

140-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover async over-cap blocking.

The suite verifies pre-retry BudgetExceeded only for with_budget_retry; _next_async_plan has a separate budget-check path. Add an async case asserting the primary runs once and the second call is never attempted when the retry estimate exceeds the ceiling.

🤖 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 `@tests/test_budget_retry.py` around lines 140 - 165, The async retry tests
lack coverage for over-cap blocking in _next_async_plan. Add a case for
async_with_budget_retry where the primary raises RetryableError, the retry
estimate exceeds the BudgetGuard ceiling, and assert the primary runs once while
no second attempt occurs; preserve the existing retry behavior for permitted
costs.
README.md (1)

173-187: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the example’s guard integration explicit.

with_budget_retry leaves the first attempt unchanged and only checks before retries. As written, the fresh guard is not near its limit, and neither callable shows budget checking or spend recording, so on_degrade will not run unless call_model hides that integration. Add a note or show the caller-side guard wiring.

🤖 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 `@README.md` around lines 173 - 187, Make the README example explicitly
demonstrate caller-side BudgetGuard integration: show or note that the model
callables check the guard before execution and record spend afterward, so the
retry path can trigger on budget exhaustion. Update the premium_model and
mini_model example flow or add concise surrounding guidance while preserving the
existing with_budget_retry configuration.
🤖 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 `@README.md`:
- Around line 173-187: Make the README example explicitly demonstrate
caller-side BudgetGuard integration: show or note that the model callables check
the guard before execution and record spend afterward, so the retry path can
trigger on budget exhaustion. Update the premium_model and mini_model example
flow or add concise surrounding guidance while preserving the existing
with_budget_retry configuration.

In `@tests/test_budget_retry.py`:
- Around line 140-165: The async retry tests lack coverage for over-cap blocking
in _next_async_plan. Add a case for async_with_budget_retry where the primary
raises RetryableError, the retry estimate exceeds the BudgetGuard ceiling, and
assert the primary runs once while no second attempt occurs; preserve the
existing retry behavior for permitted costs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 56ab0d43-3f1b-4c15-a561-bd61b88b0759

📥 Commits

Reviewing files that changed from the base of the PR and between 9e959d9 and 67e70c9.

⛔ Files ignored due to path filters (1)
  • js/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • CHANGELOG.md
  • README.md
  • js/package.json
  • pyproject.toml
  • src/floe_guard/__init__.py
  • src/floe_guard/retry.py
  • tests/test_budget_retry.py

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: budget-aware retry / graceful-degradation helper

3 participants