feat(loops): per-loop cost budget enforced as a hard stop (max_cost_usd) (#1155) - #1367
Merged
Conversation
Per-loop USD cost budget storage (#1155). Adds the nullable `max_cost_usd REAL` column to `agent_loops` across all schema sources of truth: - SQLite DDL in db/schema.py + db/tables.py MetaData - SQLite migration `agent_loops_max_cost` in db/migrations.py - Alembic revision 0007 (ADD COLUMN IF NOT EXISTS) for PostgreSQL NULL = no budget. Mirrors the #1156 max_duration_seconds pattern. Refs #1155 Co-Authored-By: Claude <noreply@anthropic.com>
#1155 request/response contract: - StartLoopRequest.max_cost_usd: Optional[float] Field(gt=0) — rejects zero/negative/NaN at the model boundary; NULL = no budget. - LoopStatusResponse.max_cost_usd + total_cost (float, default 0.0) — total_cost computed on read so a zero-run loop reports 0.0. Refs #1155 Co-Authored-By: Claude <noreply@anthropic.com>
The runner accumulates each completed run's cost and stops before the next run once accumulated cost meets/exceeds the budget, with stop_reason='budget_exhausted' (#1155). Boundary-only precedence (after the deadline check): the current run always finishes, so one run — including the first — can overshoot. Cost-accumulation integrity: only finite positive costs count; NaN/inf is ignored so it can't poison the accumulator; NULL/unknown cost is fail-open (counts as 0) and WARN-logged when a budget is active. Refs #1155 Co-Authored-By: Claude <noreply@anthropic.com>
Runner tests (TestBudget): boundary stop, in-flight run not killed, NULL-cost fail-open + WARN, no-budget runs all, NaN doesn't poison the accumulator, budget-vs-stop_signal precedence. Router tests: Pydantic gt=0 rejection, threading to the service, and total_cost summed on read (zero-run → 0.0). (#1155) Refs #1155 Co-Authored-By: Claude <noreply@anthropic.com>
Point-in-time /cso --diff audit of the max_cost_usd change — clean at the 8/10 daily gate. Refs #1155 Co-Authored-By: Claude <noreply@anthropic.com>
…e-1155 # Conflicts: # docs/memory/architecture.md # docs/memory/feature-flows.md # docs/memory/feature-flows/run-agent-loop.md # docs/memory/requirements.md # src/backend/db/loops.py # src/backend/db/migrations.py # src/backend/db/schema.py # src/backend/db/tables.py # src/backend/models.py # src/backend/routers/loops.py # src/backend/services/loop_service.py # src/frontend/src/components/LoopsPanel.vue # src/mcp-server/src/client.ts # src/mcp-server/src/tools/loops.ts # tests/unit/test_loop_service.py # tests/unit/test_loops_router_validation.py
vybe
approved these changes
Jun 28, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Validated via /validate-pr: clean. Issue #1155 (public P2 feat, status-ready), Closes-keyword present, security clean, no new env/top-level-module. Rebased onto dev: resolved 16 conflicts (both-add across loops files; Alembic 0007_agent_loops_max_cost renumbered → 0008 chained off 0007_agent_loops_no_progress; SQLite runner keeps both name-keyed entries; merged the shared #740 paragraph + run-agent-loop.md prose; requirements §38.3 cost / §38.4 no-progress). py_compile + linear Alembic chain verified. APPROVE.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional per-loop USD cost budget (
max_cost_usd) to sequential agent loops (#1155), enforced as a hard stop alongside the existingmax_runscap andmax_duration_secondswall-clock deadline (#1156).The runner accumulates each completed run's cost and stops before the next run once accumulated cost meets/exceeds the budget, exiting with
stop_reason="budget_exhausted".Behaviour
user_stopped→deadline_exceeded→budget_exhausted→ run →stop_signal_matched; natural exitmax_runs_reached. The current run always finishes, so one run — including the first — can overshoot.NaN/infis ignored so it can't poison the accumulator;NULL/unknown cost is fail-open (counts as 0) andWARN-logged when a budget is active.Field(gt=0)) — sub-cent budgets allowed; zero/negative/NaNrejected at the model boundary.GET /api/loops/{id}returnsmax_cost_usd+total_cost(summed from run rows on read;0.0for a zero-run loop).Surfaces touched (kept in sync)
agent_loops.max_cost_usd REAL— SQLite (schema.py/tables.py/migrations.py) + Alembic revision0007(dual-track per invariant Feature/vector log retention #3/Fix git pushing bug #9).models.py,routers/loops.py,services/loop_service.py,db/loops.py.run_agent_looptool +TrinityClient(invariant feat: SMARTS trading pipeline with Telegram notifications and Miro visualization #13).LoopsPanel.vue— budget input, spend display, stop-reason label.Tests
tests/unit/test_loop_service.py::TestBudget(boundary stop, in-flight run not killed, NULL fail-open + WARN, no-budget, NaN guard, budget-vs-signal precedence) andtest_loops_router_validation.py(Pydanticgt=0, service threading,total_coston read). 37 passed locally.Security
/cso --diffclean at the 8/10 daily gate — report committed underdocs/security-reports/.Closes #1155
🤖 Generated with Claude Code