Loops¶
A loop decides the steps of one turn and when to stop.
Write a loop¶
read_file is the tool from the quick start.
from alpineagents import Agent, State, loop
@loop(until=State.is_answered, limit=30)
def coding(agent: Agent, state: State):
agent.think(state)
if state.wants_tools():
agent.use_tools(state)
agent = Agent(model="claude-sonnet-5", tools=[read_file], loop=coding)
- The function is the body of one turn. It takes
(agent, state)and returns nothing. @looprepeats the body.untilandlimitare both required.Agent(loop=...)sets the loop.agent.runcalls it.
When a loop stops¶
Before every turn, the loop checks three things in this order:
| Order | Check | state.stopped_by |
|---|---|---|
| 1 | state.finish() was called |
"finish" |
| 2 | An until function returns True |
The function's name, for example "is_answered" |
| 3 | limit turns have run in this call |
"limit" |
- The checks run before each turn, so an
untilfunction sees the State after the previous turn. - Reaching
limitdoes not raise. Checkstate.stopped_by == "limit"when you need to know.
until¶
until takes a function from State to bool, or a list of them. The loop stops when any of them returns True.
def spent_too_much(state: State) -> bool:
return state.usage.output_tokens > 20_000
@loop(until=[State.is_answered, spent_too_much], limit=30)
def careful(agent: Agent, state: State):
...
- Pass the function itself:
until=State.is_answered. Notstate.is_answered(), and notstate.is_answered. - Use a named function. Its name becomes
state.stopped_by. Alambdaworks, but warns because it has no name. - The names
finishandlimitare reserved. A function with one of these names raisesValueError.
Stop from inside a turn¶
state.finish(answer) stops the loop before the next turn, with stopped_by == "finish". The loop body and tools
can call it. See Stop conditions.
Blocks¶
A block is a function that takes (agent, state) and does one step of a turn. compact_if_full is a block that
comes with the library. Write your own to reuse a step across loops:
from alpineagents import Agent, State, compact_if_full, loop
def warn_when_long(agent: Agent, state: State):
if state.turn == 20:
state.add_notice("You have used 20 turns. Finish soon.")
@loop(until=State.is_answered, limit=30)
def coding(agent: Agent, state: State):
compact_if_full(agent, state)
warn_when_long(agent, state)
agent.think(state)
if state.wants_tools():
agent.use_tools(state)
Change until or limit¶
copy returns a new loop with different settings. The original does not change.
quick = coding.copy(limit=5)
Rules¶
| Rule | Detail |
|---|---|
The body must return None |
A returned value raises TypeError. Set the answer with state.finish(answer) |
| Each call of the loop counts turns from zero | state.turn keeps counting across calls, the loop's count does not |
Exceptions from the body or an until function propagate |
Nothing is swallowed. See Errors and interruptions |
An async def body makes an async loop |
Run it with agent.arun. See Async |