Skip to content

alpineagents

CI License: MIT Docs

A Python agent framework where the agent loop is a function you write.

  • Agent holds the settings: the model, the system prompt and the tools.
  • State holds one conversation: its history, the messages the model sees next, and the answer. You change it only through its methods.
  • A loop takes both and repeats one turn until a stop condition is true.

Why

In most frameworks the loop lives inside the library. LangGraph has you declare it as a graph. The OpenAI Agents SDK and PydanticAI run it for you.

In alpineagents the loop is a few lines of Python in your own code:

  • until= decides when it stops.
  • limit= sets the maximum number of turns.
  • Compaction runs only where you call it.

The default loop, alpineagents.default_loop, is written the same way. Read it, or copy it as a starting point. Misuse raises an error that says how to fix it.

Install

Requires Python 3.11 or later.

pip install alpineagents     # or: uv add alpineagents

Anthropic models read ANTHROPIC_API_KEY. OpenAI-compatible servers read OPENAI_API_KEY.

Quick start

Define a tool, create an Agent, run a task:

from pathlib import Path

from alpineagents import Agent, ToolError, tool


@tool
def read_file(path: str) -> str:
    """Read a text file"""
    file = Path(path)
    if not file.exists():
        raise ToolError(f"No such file: {path}")
    return file.read_text()


agent = Agent(model="claude-sonnet-5", tools=[read_file])
print(agent.run("Summarize README.md in three lines"))
  • @tool turns the function into a tool. The type hints and the docstring tell the model how to call it.
  • ToolError tells the model the call failed, and the run continues. Any other exception stops the run.
  • agent.run repeats turns until the model answers, then returns the answer.
  • Progress is printed to the terminal while it runs.

Without an API key

FakeModel returns prepared replies in order. It needs no API key and no network:

from pathlib import Path

from alpineagents import Agent, ToolError, tool
from alpineagents.testing import FakeModel, tool_call


@tool
def read_file(path: str) -> str:
    """Read a text file"""
    file = Path(path)
    if not file.exists():
        raise ToolError(f"No such file: {path}")
    return file.read_text()


fake = FakeModel([
    tool_call("read_file", path="README.md"),
    "README.md describes a Python agent framework.",
])
agent = Agent(model=fake, tools=[read_file])
print(agent.run("Summarize README.md"))

The first reply asks for read_file. The Agent runs the tool, and the second reply is the answer.

Write your own loop

The Agent above uses default_loop. To change what happens in a turn, write the loop yourself:

from pathlib import Path

from alpineagents import Agent, State, ToolError, compact_if_full, loop, tool, waiting_for_user


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


@tool
def read_file(path: str) -> str:
    """Read a file"""
    file = Path(path)
    if not file.exists():
        raise ToolError(f"No such file: {path}")
    return file.read_text()


@tool
def write_file(path: str, content: str) -> None:
    """Create a file, or replace its content"""
    Path(path).write_text(content)


@loop(until=waiting_for_user, limit=30)
def coding(agent: Agent, state: State):
    compact_if_full(agent, state)
    agent.think(state)
    if state.pending_calls:
        agent.use_tools(state)


agent = Agent(
    model="claude-sonnet-5",
    system="You are a coding assistant. Read a file before you change it.",
    tools=[list_files, read_file, write_file],
    loop=coding,
)
print(agent.run("Add a test for the add() function in calc.py"))
  • coding is one turn: summarize the context if it is more than 60% full, ask the model, run the tools it asked for.
  • @loop repeats the turn. It stops when waiting_for_user is true (the model answered and waits for the user), or after 30 turns. Any function from State to bool works as until.
  • To change the agent, add, remove or reorder lines in coding.

Write your own loop explains each part.

Keep and inspect the State

agent.run("text") creates a State and throws it away. To keep it, create it yourself:

from alpineagents import Message, State

state = State(messages=[Message.user("Add a test for the add() function in calc.py")])
agent.run(state)

print(state.answer, state.stopped, state.usage.cost)
state.add_message(Message.user("Now run the tests"))  # the person's next message
agent.run(state)  # continues the same conversation

add_message also takes Message.notice(...), a message from your code such as a test result.

Every change to a State adds an entry to state.history, and everything else is computed from it. state.restore(snapshot) goes back to a state.snapshot() taken earlier, state.fork() tries another path from here, and FileStore with Agent(store=...) saves the history to continue it in another process.

Subagents

An Agent in tools= becomes a tool the model can call. The Agent runs on the prompt it gets, and its answer is the tool result:

from pathlib import Path

from alpineagents import Agent, tool


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


researcher = Agent(
    model="claude-sonnet-5",
    name="researcher",
    description="Reads files and reports what they say",
    system="Read what you need, then answer with the facts only.",
    tools=[read_file],
)
lead = Agent(model="claude-sonnet-5", tools=[researcher])
print(lead.run("Which Python versions does this project support? Ask the researcher to read pyproject.toml"))
  • name and description are the tool's name and description. The model calls it with a prompt.
  • The researcher sees only that prompt. It runs in a State of its own, linked to the lead's (state.child(...)).
  • lead's permissions apply to the researcher's tool calls too, and its cost includes the researcher's.
  • Any run started inside a tool call is a subagent in the same way, so a @tool can start Agents with its own input.

Use subagents has the rest.

Documentation

tgoddessana.github.io/alpineagents

  1. Learn: seven short pages, read in order. Your first agent, tools, your own loop, the conversation, approval, save and resume and testing.
  2. Guides: one task per page, in any order, for example writing permission rules.
  3. Concepts: how it works, with the precise rules.
  4. API reference: every class and method.

Roadmap

  • OpenAI adapter, LiteLLM adapter
  • Skills (skills=). Passing skills= raises NotImplementedError for now
  • agent.run_tool, agent.load_skill
  • More tool result types (File, image URLs)
  • alpineagents add CLI (copies the default loop and block sources into your project)
  • Per-adapter server-side compaction optimizations

Development

uv venv && uv pip install -e ".[dev]"
.venv/bin/python -m pytest -q

License

MIT. See LICENSE.