Circuit Breaker State Machine for a Failing Tool
Problem Statement
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.
Background
Three states:
- closed - calls go through. A failure increments a consecutive-failure counter; a success resets it to zero. When the counter reaches failure_threshold, the breaker opens and records opened_at = t of that event.
- open - any event at time t < opened_at + cooldown is rejected: the call is not executed at all, and its t is recorded. The first event at t >= opened_at + cooldown transitions the breaker to half-open and is then processed by the half-open rules.
- half-open - a trial window. Successes accumulate; on reaching half_open_successes the breaker closes and all counters reset. A single failure re-opens it immediately with opened_at = t of that failure.
The counters reset on every state change, so a fresh open always requires failure_threshold fresh consecutive failures.
Your Task
Implement:
def run_circuit_breaker(events, failure_threshold, cooldown, half_open_successes):
- events: list of {"t": int, "result": "success"|"failure"}, already in ascending time order.
Return:
- final_state: "closed", "open" or "half_open".
- rejected: list of t values that were short-circuited, in event order.
- transitions: list of strings "<from>-><to>@<t>", in the order they happened.
Input/Output Format
Returns a dict with those three keys. The breaker starts closed.
Sample
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.
Example
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.
Constraints
1 <= len(events) <= 1000;tvalues are non-decreasing intsfailure_threshold >= 1,cooldown >= 0,half_open_successes >= 1- A rejected event is never executed and never affects any counter
- The probe event at
t >= opened_at + cooldownis processed under half-open rules, not skipped - Every state change resets the failure and success counters
transitionsuses the exact format"<from>-><to>@<t>"