No-progress detector
Implement the check that names the turn an agent loop starts repeating itself, catching two-call cycles and reordered arguments but not a retry that worked.
On this page
A stuck loop is cheap per turn and expensive in total, and neither cap notices it quickly. The no-progress check is the exit that does, provided it can tell a loop going round from a loop recovering.
The situation
A team's support agent stops when the same tool is called with the same arguments twice in a row, the strict check from agent loops and stop conditions. Over 2,000 runs it missed three kinds of stuck loop and stopped one kind of healthy one. 17 runs alternated between get_order(123) and get_shipment(123) until the turn cap of 25. 4 cycled through three calls. 6 repeated one call with its argument keys reordered, {"id": 123, "full": true} then {"full": true, "id": 123}, which a raw-string comparison counts as two different calls. And 9 healthy runs were stopped because they retried a call once after a timeout and got an answer the second time.
Each stuck run spent about 20 turns it did not need. The harness wants one function that names the turn at which the loop is first seen repeating, with a window of recent turns as its only memory.
The question
Implement the detector so that the strict repeat, the two-call cycle and the reordered keys all fire on the turn the repetition completes, and the retry after a timeout does not.
def solve(calls, window):
"""calls: one (name, args, result) per turn, oldest first; turn i is
calls[i - 1]. window: how many recent turns the detector keeps, so it can
see a cycle of up to window // 2 turns.
Returns the first turn t at which the last 2p turns are one block of p
turns played twice, for some p with 1 <= p <= window // 2, or None if that
never happens. The same call with a different result is progress."""
S = ("search_tickets", {"q": "refund delay"}, "3 tickets: 811, 812, 840")
A = ("get_order", {"id": 123}, '{"status": "shipped"}')
B = ("get_shipment", {"id": 123}, '{"eta": "Friday"}')
solve([S, S, S], 2) -> 2
solve([A, B, A, B, A, B], 2) -> None
solve([A, B, A, B, A, B], 4) -> 4
solve([("get_order", {"id": 123}, "error: timed out after 5 s"), A], 2) -> None
A strong answer · Rubric · Where answers go wrong · Solution
Write your answer first, then open