alpineagents¶
A Python agent framework where the agent loop is a function you write.
Agentholds the settings: the model, the system prompt and the tools.Stateholds 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"))
@toolturns the function into a tool. The type hints and the docstring tell the model how to call it.ToolErrortells the model the call failed, and the run continues. Any other exception stops the run.agent.runrepeats 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"))
codingis one turn: summarize the context if it is more than 60% full, ask the model, run the tools it asked for.@looprepeats the turn. It stops whenwaiting_for_useris true (the model answered and waits for the user), or after 30 turns. Any function from State toboolworks asuntil.- 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"))
nameanddescriptionare the tool's name and description. The model calls it with aprompt.- 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
@toolcan start Agents with its own input.
Use subagents has the rest.
Documentation¶
tgoddessana.github.io/alpineagents
- Learn: seven short pages, read in order. Your first agent, tools, your own loop, the conversation, approval, save and resume and testing.
- Guides: one task per page, in any order, for example writing permission rules.
- Concepts: how it works, with the precise rules.
- API reference: every class and method.
Roadmap¶
- OpenAI adapter, LiteLLM adapter
- Skills (
skills=). Passingskills=raisesNotImplementedErrorfor now agent.run_tool,agent.load_skill- More tool result types (
File, image URLs) alpineagents addCLI (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.