feat: budget-aware retry / graceful-degradation helper (closes #45)#48
Conversation
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>
📝 WalkthroughWalkthroughAdds 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. ChangesBudget-aware retry
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
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 |
Satisfy version-guard after shipping the new retry helper in both packages. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
CHANGELOG.mdREADME.mdexamples/budget_retry.pyjs/src/index.tsjs/src/retry.tsjs/test/retry.test.tssrc/floe_guard/__init__.pysrc/floe_guard/retry.pytests/test_budget_retry.py
Reject non-integer max_attempts and catch Exception so control-flow exits are not retried. Co-authored-by: Cursor <cursoragent@cursor.com>
05a57e9 to
45a0e03
Compare
There was a problem hiding this comment.
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, andasync_with_budget_retryplus tests and a runnable demo example. - TypeScript: introduce
withBudgetRetryplus 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 Exceptioncan catchasyncio.CancelledErroron some supported Python versions, resulting in retries after cancellation. If the intent is to always propagate cancellation immediately, explicitly re-raiseCancelledErrorbefore 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.
# Conflicts: # CHANGELOG.md # pyproject.toml # src/floe_guard/__init__.py
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 (2)
tests/test_budget_retry.py (1)
140-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover async over-cap blocking.
The suite verifies pre-retry
BudgetExceededonly forwith_budget_retry;_next_async_planhas 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 winMake the example’s guard integration explicit.
with_budget_retryleaves 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, soon_degradewill not run unlesscall_modelhides 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
⛔ Files ignored due to path filters (1)
js/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
CHANGELOG.mdREADME.mdjs/package.jsonpyproject.tomlsrc/floe_guard/__init__.pysrc/floe_guard/retry.pytests/test_budget_retry.py
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) andwithBudgetRetry(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 viaon_degrade.Behavior in plain English
Imagine a $1 budget and a call that fails once:
on_degradecallback so you can switch to a cheaper model / pathguard.check(estimated_cost)and abort — no second spendTiny example
Try the no-network demo:
python examples/budget_retry.pyWhat this is / is not
advisory,check)What changed
src/floe_guard/retry.py—with_budget_retry,async_with_budget_retry,RetryPlanjs/src/retry.ts—withBudgetRetryexamples/budget_retry.pytests/test_budget_retry.py,js/test/retry.test.tsTest plan
python -m pytest tests/test_budget_retry.py -v— 6/6 passednpm test -- test/retry.test.ts(injs/) — 5/5 passedpython examples/budget_retry.py— near-limit path prints cheaper-model successSummary by CodeRabbit
max_attemptsis rejected and onlyExceptiontypes are retried (excluding interrupt/cancellation errors).