Loops and blocks¶
Learn: Write your own loop. Concepts: Loops and stop rules. Guide: Stop a run.
loop
¶
loop(fn: Any = None, /, *, until: Any = None, limit: Any = None) -> Any
Turns a one-turn function into a loop that takes (agent, state) and returns the answer.
until and limit are both required, so how the loop stops is always written next to it. The body
returns nothing; to set the answer yourself, call state.finish(answer). An async def body makes an
async loop for agent.arun.
Example
from alpineagents import waiting_for_user
@loop(until=waiting_for_user, limit=50)
def coding(agent: Agent, state: State):
compact_if_full(agent, state)
agent.think(state)
if state.pending_calls:
agent.use_tools(state)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
until
|
Any
|
A |
None
|
limit
|
Any
|
The most turns one run takes. Reaching it stops quietly with
|
None
|
Returns:
| Type | Description |
|---|---|
Any
|
A decorator that turns the body into a |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Loop
¶
Loop(body: Callable[[Any, State], Any], *, until: Any, limit: Any)
A loop made by @loop. Calling loop(agent, state) runs the body turn by turn and returns
state.answer.
Before every turn it checks, in order:
state.stoppedalready set bystate.finish()(StoppedByFinish) or by a permission'sDenied(..., stop=True)(StoppedByPermission(call, ...)): stops and keeps it- each
untilfunction: stops withstate.stopped == StoppedByUntil(name), the function's name - the turn count against
limit: stops withstate.stopped == StoppedByLimit(limit)
A stop the loop decides (2 and 3) is recorded in the history as a StopEntry. Each call counts turns from
zero. A nested loop's until/limit stop ends only that loop: the outer loop checks its own until and
limit and, if it goes on, records StopEntry(None), so state.stopped goes back to None.
Exceptions from the body or the until functions propagate as is.
An async loop (async def body) returns a coroutine that does the same; its until functions stay
plain functions.
The body must return None: a returned value raises TypeError (set the answer with
state.finish(answer)), and so does an awaitable returned by a sync loop. The checks run before each turn,
so an until function sees the State after the previous turn. A loop written without @loop makes none
of these checks: it tests state.stopped itself, and after a @loop called inside it stopped on its own
until or limit (which leaves state.stopped set), it tests only for StoppedByFinish and
StoppedByPermission or is itself written with @loop. An until function can read
state.snapshot() first so that all the values it reads belong to one moment.
Usually created with @loop. Every check happens here, when the decorator is applied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
Callable[[Any, State], Any]
|
The one-turn function |
required |
until
|
Any
|
A |
required |
limit
|
Any
|
The most turns, an integer of 1 or more. |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
|
until
instance-attribute
¶
until: tuple[Condition, ...] = tuple(checked)
The stop conditions, each a State -> bool function.
is_async
instance-attribute
¶
is_async: bool = is_async_callable(body)
True if the body is async def. Calling the loop then returns a coroutine.
copy
¶
copy(*, until: Any = ..., limit: Any = ...) -> Loop
Returns a new Loop with until or limit changed. The original is unchanged.
Example
quick = coding.copy(limit=5)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
until
|
Any
|
New stop conditions. Omit to keep the current ones. |
...
|
limit
|
Any
|
A new turn limit. Omit to keep the current one. |
...
|
Raises:
| Type | Description |
|---|---|
TypeError
|
A new value fails the same checks as |
ValueError
|
A new value fails the same checks as |
default_loop
¶
The loop agent.run uses when the Agent has no loop of its own.
Each turn compacts the messages if the context is over 60% full, asks the model, and runs the tools it asked
for. Stops when waiting_for_user is true (stopped by waiting_for_user) or after 50 turns. Copy it as a
starting point for your own loop.
adefault_loop
async
¶
The async version of default_loop, used by agent.arun when the Agent has no loop of its own.
waiting_for_user
¶
waiting_for_user(state: State) -> bool
True when the model has handed the turn back: nothing waits for a tool result, and the last message is the model's reply without tool calls.
The default loops stop on it, and so can yours: @loop(until=waiting_for_user, limit=30). It says the model
is waiting for the user, not that the task is done; to end a task for good, call state.finish(answer). A
message added after the reply (state.add_message) makes it false again, so the loop goes on. For another
rule, write your own State -> bool function.
Reads one snapshot, so the checks see the same State even while another thread adds a message.
Example
from alpineagents import Agent, State, loop, waiting_for_user
@loop(until=waiting_for_user, limit=30)
def coding(agent: Agent, state: State):
agent.think(state)
if state.pending_calls:
agent.use_tools(state)
CompactIfFull
¶
CompactIfFull(at: float = 0.6, instructions: str | None = None)
A block that compacts the messages when the context is fuller than at.
Call it at the start of a turn, before agent.think. compact_if_full is one with the defaults (at=0.6,
no instructions). Compaction replaces the messages with the first message and a summary and rebuilds the
provider's cache. In an async loop use acall (or acompact_if_full): calling it in an async run raises
TypeError the first time it compacts, which can be many turns in.
Example
CompactIfFull(at=0.5, instructions="Keep file paths and test results")(agent, state)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
at
|
float
|
The fraction of the model's context window ( |
0.6
|
instructions
|
str | None
|
What the summary must keep, added to the default summary prompt. |
None
|
instructions
instance-attribute
¶
instructions = instructions
What the summary should keep, or None.
compact_if_full
module-attribute
¶
compact_if_full = CompactIfFull()
Compacts the context when it is more than 60% full. Call it as compact_if_full(agent, state) at the start
of a turn. The same as CompactIfFull().
acompact_if_full
module-attribute
¶
acompact_if_full = compact_if_full.acall
The async version of compact_if_full, for async loops: await acompact_if_full(agent, state).