Stop a run¶
A loop stops in one of four ways. Pick the one that matches who decides.
| Who decides | How | state.stopped |
|---|---|---|
| A check on the State, before every turn | An until function |
StoppedByUntil(name), the function's name |
| Your code or a tool, at a specific moment | state.finish(answer) |
StoppedByFinish(answer) |
| A fixed maximum number of turns | limit |
StoppedByLimit(turns), the limit |
A permission, when it refuses a call with stop=True |
Denied(reason, stop=True), for example DecideByHuman's no (Ask before a tool runs) |
StoppedByPermission(call, permission) |
Stop on a check¶
from pathlib import Path
from alpineagents import Agent, Message, State, StoppedByLimit, loop, tool, waiting_for_user
@tool
def read_file(path: str) -> str:
"""Read a text file"""
return Path(path).read_text()
def spent_too_much(state: State) -> bool:
return state.usage.output_tokens > 20_000
@loop(until=[waiting_for_user, spent_too_much], limit=30)
def careful(agent: Agent, state: State):
agent.think(state)
if state.pending_calls:
agent.use_tools(state)
agent = Agent(model="claude-sonnet-5", tools=[read_file], loop=careful)
state = State(messages=[Message.user("Summarize every file in docs/")])
agent.run(state)
print(state.stopped)
if isinstance(state.stopped, StoppedByLimit):
print(f"Stopped after {state.stopped.turns} turns. The task may be unfinished.")
waiting_for_useris true when the model has answered: no call is pending, and the last message is the model's and asks for no tool. The default loop stops on it too (Write your own loop).spent_too_muchtakes the State and returnsTrueto stop.untiltakes a list. The loop stops when any function in it returnsTrue.- After the run,
state.stoppedisStoppedByUntil("waiting_for_user"),StoppedByUntil("spent_too_much")orStoppedByLimit(30). The terminal shows the same name, for exampledone: stopped by spent_too_much. - Reaching
limitdoes not raise. Checkstate.stoppedwhen an unfinished run matters, as the last lines do.
Give stop conditions clear names. The name is the only record of why the run stopped.
Stop from a tool¶
Let the model decide when the work is done, and return a structured answer:
from alpineagents import Agent, State, tool
@tool
def submit(summary: str, files_changed: list[str], state: State) -> None:
"""Submit the finished work. Call it once, at the end."""
state.finish({"summary": summary, "files_changed": files_changed})
agent = Agent(
model="claude-sonnet-5",
system="When the work is done, call submit.",
tools=[submit],
)
print(agent.run("Rename the helper functions in utils.py"))
- The model calls
submitwhen it thinks the work is done. state.finish(...)setsstate.answerto the dict. The loop stops before the next turn.agent.runreturns the dict.- The State is now finished for good: see State and history.
The model fills the tool's typed parameters, so the answer has a known shape. It is JSON data: a dataclass or a
Pydantic model comes back as a read-only dict, and Review(**state.answer) rebuilds it.
Stop when the person says no¶
A permission that refuses a call with stop=True stops the loop before its next turn. Unlike finish, the State is
not finished: add the person's next message and run again. See
Ask before a tool runs.
In a loop without @loop¶
state.finish() and a permission's stop=True set state.stopped right away, and run resets it to None when it
starts. A loop written without @loop has to check it itself:
see Loops without @loop.
Related¶
- Loops and stop rules: the order of the checks
- Let tools use the State