What you'll learn: how the capstone's WorkflowRunner stops a loop with a step cap and a time limit, what the step log holds and why it holds no private data, and how to save a stopped run as JSON and resume it without repeating work.
At a glance
Figure 1. The runner, its two budgets and what it records
How to read this: the two budgets are checked before every step, and the step log keeps a short summary of each step.
- A model loop needs two limits: a step cap and a time limit.
- The step cap bounds steps, not model calls. A repaired reply makes one step cost two calls.
- The time limit is checked between steps. It does not interrupt a running step.
- The step log holds summaries, not arguments or results, so it is safe to keep.
- Observations plus the log are the whole state, so a stopped run can be saved and resumed.
Scenario
Composite scenario, not a real company. At Lumen Industrial Supply, a fictional distributor, a sales engineer pastes in a customer request: vibration monitoring on 40 pumps, a humid plant, an existing OPC UA setup. The assistant searches the catalog, reads product records and checks protocols.
In a pilot, one request makes the planner repeat searches with slightly different wording. Nobody notices until the model bill arrives. The team wants a hard stop, a record of each step, and a way to continue a run that stopped early. All numbers are illustrative.
What you will be able to do
- Explain what
max_stepsandtimeout_sbound, and what they do not. - Read a step log and say which step failed and why, without any private data in it.
- Predict the status and step count of a run from its limits and a fake clock.
- Save a stopped run as JSON and resume it so finished calls are not repeated.
Prerequisites
- A bounded workflow or an agent?, Tool definitions and validated arguments and Permissions the application enforces.
- Theory: Agents, tools and MCP and Reliability and scaling. This page adds the hands-on side.
Core ideas
Two limits, checked before each step
| Limit | Parameter and default | Checked | What it does not do |
|---|---|---|---|
| Steps | max_steps=14 |
By the for loop |
Does not count model calls |
| Wall clock | timeout_s=5.0 |
At the top of each step | Does not cancel a running step |
Anthropic's Building effective agents says it is common to include stopping conditions, such as a maximum number of iterations, to keep control. The capstone adds a time limit too, because a step cap alone does not protect a user from a slow tool.
Verified: capstone/fde-product-discovery-copilot/app/workflow/runner.py
def run(self, requirements: Requirements) -> WorkflowResult:
result = WorkflowResult("max_steps")
started = self.clock()
seen: set[str] = set()
for step in range(1, self.max_steps + 1):
if self.clock() - started > self.timeout_s:
result.status = "timeout"
return result
tick = self.clock()
try:
decision = complete_structured(
self.provider, plan_messages(requirements, result.observations, self.registry.describe()), PlanDecision
)
except StructuredOutputError:
result.status = "planner_error"
return result
if decision.action == "final":
result.status = "completed"
return result
if decision.action == "request_approval":
self._request_approval(decision, step, tick, result)
return result
self._run_tool(decision, step, tick, seen, result)
return result # status stays "max_steps"
The status starts as max_steps. If the loop ends without a return, that is the answer. Every other exit sets its own status first.
The clock is a parameter (clock=time.monotonic by default). Python's documentation says time.monotonic() cannot go backwards and is not affected by system clock updates, and that only the difference between two calls is meaningful. So the runner subtracts two readings, and a test can pass a fake clock.
Predict first. A fake clock moves 2 seconds every time anyone reads it. The runner has timeout_s=3, max_steps=14, and a planner with ten different searches queued. How many steps run, and what is the status?
Answer
One step, status timeout. The clock reads 0 (start), 2 (the check, not above 3), 4 (step start) and 6 (step end). At the next check it reads 8, and 8 is above 3, so the loop stops. The limit is checked only between steps, so one step overshoots a small budget.
Figure 2. States of one run and the exits that limits add
How to read this: only Completed and AwaitingApproval are planned endings; the other three are limits or failures, and the run keeps what it learned.
Steps are not model calls
A reply that is not valid JSON is repaired once, and the repair is a second model call inside the same step. The worst case is two calls per step.
Verified: capstone/fde-product-discovery-copilot/lessons/M4-L4/test_m4_l4_resume.py