Summary
Composing two bare-agent primitives — CircuitBreaker.wrapProvider + Loop — silently disables LLM cost accounting, so a bareguard budget.maxCostUsd cap accrues zero token cost and never halts.
Root cause
CircuitBreaker.wrapProvider(provider, key) returns only { generate } (src/circuit-breaker.js:105–107) — it does not carry over provider.model.
Loop derives cost from const model = this.provider.model || null (src/loop.js:181) and estimateCost(model, usage) returns null when !model (src/loop.js:29).
- Net: when the Loop's provider is a wrapped provider (the documented resilience pattern),
model is null, roundCost is null, onLlmResult reports costUsd: null, and budget.maxCostUsd never accrues. The cost cap is a no-op on the wrapped path.
Repro
const breaker = new CircuitBreaker({ threshold: 5 });
const wrapped = breaker.wrapProvider(anthropic, 'anthropic'); // anthropic.model set
const loop = new Loop({ provider: wrapped, onLlmResult });
// onLlmResult receives costUsd: null for every round → budget never accrues
Impact
Any consumer that wraps its provider in CircuitBreaker (recommended for resilience) loses budget.maxCostUsd enforcement on the agent loop. Found while validating multis (first baresuite customer) — the per-run cost cap was silently unenforced.
Suggested fix (either)
wrapProvider preserves passthrough props: return { ...provider, generate } (or at least copy model).
Loop falls back to the generate result's model: const model = result.model || this.provider.model || null.
Option 2 is more robust (model can vary per response, e.g. Fallback provider).
Summary
Composing two bare-agent primitives —
CircuitBreaker.wrapProvider+Loop— silently disables LLM cost accounting, so a bareguardbudget.maxCostUsdcap accrues zero token cost and never halts.Root cause
CircuitBreaker.wrapProvider(provider, key)returns only{ generate }(src/circuit-breaker.js:105–107) — it does not carry overprovider.model.Loopderives cost fromconst model = this.provider.model || null(src/loop.js:181) andestimateCost(model, usage)returnsnullwhen!model(src/loop.js:29).modelisnull,roundCostisnull,onLlmResultreportscostUsd: null, andbudget.maxCostUsdnever accrues. The cost cap is a no-op on the wrapped path.Repro
Impact
Any consumer that wraps its provider in
CircuitBreaker(recommended for resilience) losesbudget.maxCostUsdenforcement on the agent loop. Found while validating multis (first baresuite customer) — the per-run cost cap was silently unenforced.Suggested fix (either)
wrapProviderpreserves passthrough props: return{ ...provider, generate }(or at least copymodel).Loopfalls back to the generate result's model:const model = result.model || this.provider.model || null.Option 2 is more robust (model can vary per response, e.g. Fallback provider).