InterviewPrepKit

Home / Coding / Agent Coding / The Agent Loop / Enforcing Stop Conditions

Enforcing Stop Conditions

medium 00:00
Solving tips
  • An agent needs more than one exit: a step cap, a time budget, and a natural finish. Any one of them firing must stop the loop.
  • Take the clock as a passed-in callable so the loop is deterministic and testable — never read the real wall clock inside logic you want to grade.
  • Return why you stopped, not just the answer; the caller often retries or escalates based on the reason.

Build an agent loop that can stop for more than one reason. A basic loop stops when the model says it is done, but a production agent also has to stop when it has taken too many steps or spent too long. This exercise is the control logic that enforces all three limits and reports which one fired.

How the limits work

You keep a running list of messages and, each turn, call model(messages). The loop must respect three stop conditions and return the reason for the one that triggers first:

  • Final answer — the model returns {"type": "final", "content": "..."}. Stop and return (content, "final").
  • Step cap — you have made max_steps model calls without a final answer. Stop and return (None, "max_steps").
  • Time budget — the elapsed time (measured with the injected clock) has reached time_budget. Stop and return (None, "budget").

clock is a zero-argument callable that returns a number that only increases (think seconds). Record the start time once, then compare clock() - start against time_budget before each model call. Because the clock is passed in, the whole loop is deterministic: the same clock stub always produces the same result.

Task

Complete run_with_limits(model, tools, user_msg, clock, max_steps=10, time_budget=None):

  1. Start messages with {"role": "user", "content": user_msg} and record start = clock().
  2. Before each model call, if time_budget is not None and clock() - start >= time_budget, stop with (None, "budget").
  3. Otherwise call the model. On a final answer return (content, "final"). On a tool call, append the assistant message, run the named tool, and append {"role": "tool", "name": ..., "content": <output>}.
  4. Allow at most max_steps model calls. If they are exhausted with no final answer, return (None, "max_steps").

Example

def make_clock(step=1.0, start=0.0):
    # returns start, start+step, start+2*step, ... on successive calls
    t = {"now": start}
    def clock():
        v = t["now"]; t["now"] += step; return v
    return clock

def never_finishes(messages):
    # always asks for a tool, so only a limit can stop the loop
    return {"role": "assistant", "type": "tool", "name": "noop", "args": {}}

tools = {"noop": lambda args: "ok"}

# clock ticks 0,1,2,3,...; budget of 3 stops before the 3rd model call
run_with_limits(never_finishes, tools, "go", make_clock(), max_steps=10, time_budget=3)
# -> (None, "budget")

def finishes(messages):
    return {"role": "assistant", "type": "final", "content": "done"}

run_with_limits(finishes, tools, "go", make_clock(), max_steps=10, time_budget=3)
# -> ("done", "final")

Constraints

  • Check the time budget before spending a model call, so an already-exhausted budget stops immediately.
  • Handle an unknown tool name gracefully (append an error result rather than crashing).
  • Do not call any real LLM, network, or wall clock — model, the tools, and clock are all provided.

Write your solution, then hit Run tests to check it — or get a mock grade from the AI coach.

The coach remembers this session — revise your code and ask again, and it grades your progress. It gives hints, not the answer.
Report a bug