When a downstream tool is genuinely down, retrying is worse than useless: every attempt costs a step, a timeout and a slot in the context window, and the agent's plan drifts further from reality with every empty observation. A circuit breaker stops the bleeding - after enough consecutive failures it opens and rejects calls without making them, then cautiously probes for recovery.
Three states:
The counters reset on every state change, so a fresh open always requires failure_threshold fresh consecutive failures.
Implement:
def run_circuit_breaker(events, failure_threshold, cooldown, half_open_successes):
Return:
Returns a dict with those three keys. The breaker starts closed.
events = [{"t": 1, "result": "failure"}, {"t": 2, "result": "failure"},
{"t": 3, "result": "failure"}, {"t": 4, "result": "success"}]
print(run_circuit_breaker(events, 2, 10, 1))
# {'final_state': 'open', 'rejected': [3, 4], 'transitions': ['closed->open@2']}
Two consecutive failures open the breaker at t=2; the cooldown runs to t=12, so both later events are rejected without ever being tried.
events = [{'t':1,'result':'failure'},{'t':2,'result':'failure'},{'t':3,'result':'failure'},{'t':4,'result':'success'}]
print(run_circuit_breaker(events, 2, 10, 1)){'final_state': 'open', 'rejected': [3, 4], 'transitions': ['closed->open@2']}The failures at t=1 and t=2 are two consecutive failures, hitting the threshold of 2, so the breaker opens at t=2 with a cooldown to t=12. The events at t=3 and t=4 both fall inside the cooldown, so they are rejected without being executed - the success at t=4 never gets a chance to reset anything.
1 <= len(events) <= 1000; t values are non-decreasing intsfailure_threshold >= 1, cooldown >= 0, half_open_successes >= 1t >= opened_at + cooldown is processed under half-open rules, not skippedtransitions uses the exact format "<from>-><to>@<t>"