Skip to content

feat(loops): per-loop cost budget enforced as a hard stop (max_cost_usd) (#1155) - #1367

Merged
vybe merged 11 commits into
devfrom
AndriiPasternak31/issue-1155
Jun 28, 2026
Merged

feat(loops): per-loop cost budget enforced as a hard stop (max_cost_usd) (#1155)#1367
vybe merged 11 commits into
devfrom
AndriiPasternak31/issue-1155

Conversation

@AndriiPasternak31

Copy link
Copy Markdown
Contributor

Summary

Adds an optional per-loop USD cost budget (max_cost_usd) to sequential agent loops (#1155), enforced as a hard stop alongside the existing max_runs cap and max_duration_seconds wall-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

  • Boundary-only precedence (per iteration, after the deadline check): user_stoppeddeadline_exceededbudget_exhausted → run → stop_signal_matched; natural exit max_runs_reached. The current run always finishes, so one run — including the first — can overshoot.
  • Cost-accumulation integrity: only finite, positive run 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.
  • No upper cap (Field(gt=0)) — sub-cent budgets allowed; zero/negative/NaN rejected at the model boundary.
  • GET /api/loops/{id} returns max_cost_usd + total_cost (summed from run rows on read; 0.0 for a zero-run loop).

Surfaces touched (kept in sync)

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) and test_loops_router_validation.py (Pydantic gt=0, service threading, total_cost on read). 37 passed locally.

Security

/cso --diff clean at the 8/10 daily gate — report committed under docs/security-reports/.

Closes #1155

🤖 Generated with Claude Code

AndriiPasternak31 and others added 10 commits June 28, 2026 03:20
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>
Thread the #1155 budget column through the insert + row-to-dict mapping
so it round-trips from create to read.

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>
Pass max_cost_usd from StartLoopRequest into start_loop, and surface
budget + spend on GET /api/loops/{id}: total_cost is summed from the
run rows on read (no stored column to drift; sum(()) → 0.0). (#1155)

Refs #1155

Co-Authored-By: Claude <noreply@anthropic.com>
Add the optional per-loop USD budget (z.number().gt(0)) to the
run_agent_loop MCP tool and TrinityClient.startLoop, proxying to the
gated backend endpoint. Keeps the third surface in sync (#1155).

Refs #1155

Co-Authored-By: Claude <noreply@anthropic.com>
Optional "Max cost (USD)" field on the start form, a "Budget: spent /
limit" line on active loops, and the budget_exhausted stop-reason label
(#1155).

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>
Update requirements, architecture (Sequential Agent Loops), the
run-agent-loop feature flow + index, and user docs (agent API reference,
agent-loops automation) for the #1155 per-loop USD budget.

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>
@AndriiPasternak31
AndriiPasternak31 marked this pull request as ready for review June 28, 2026 02:33
@AndriiPasternak31
AndriiPasternak31 requested a review from vybe June 28, 2026 02:33
@AndriiPasternak31 AndriiPasternak31 self-assigned this Jun 28, 2026
…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 vybe 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.

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.

@vybe
vybe merged commit a93ea00 into dev Jun 28, 2026
20 checks passed
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.

2 participants