Skip to content

Plan before acting

The model writes a plan in the first turn, without tools. From the second turn it carries out the plan.

from pathlib import Path

from alpineagents import Agent, Message, State, loop, tool, waiting_for_user


@tool
def read_file(path: str) -> str:
    """Read a text file"""
    return Path(path).read_text()


@tool
def list_files() -> list[str]:
    """List the files in this folder"""
    return sorted(p.name for p in Path(".").iterdir())


@loop(until=waiting_for_user, limit=30)
def plan_then_act(agent: Agent, state: State):
    if state.turn == 0:
        agent.think(state, tools=[])
        state.add_message(Message.notice("Now carry out the plan."))
        return
    agent.think(state)
    if state.pending_calls:
        agent.use_tools(state)


agent = Agent(model="claude-sonnet-5", tools=[read_file, list_files], loop=plan_then_act)
print(agent.run("Find out what this folder is for"))
  1. In the first turn, agent.think(state, tools=[]) shows the model no tools, so the reply is text: the plan.
  2. A reply without tool calls makes waiting_for_user true. state.add_message(Message.notice(...)) adds a message after it, so the loop continues.
  3. From the second turn, agent.think(state) shows every tool.

Other tool sets per turn

think(state, tools=[...]) accepts any subset of the Agent's tools. For example, only reading tools for the first five turns. Inside the loop body above:

tools = [read_file, list_files] if state.turn < 5 else None
agent.think(state, tools=tools)

tools=None shows all of the Agent's tools.

To pick them by what they do rather than by name, use the tools' hints. This also picks the read-only tools of MCP servers:

readers = [t for t in agent.tool_map.values() if t.read_only]
agent.think(state, tools=readers if state.turn < 5 else None)