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. Replace the print line above with:
from alpineagents.testing import FakeModel, tool_call
fake = FakeModel([
tool_call("read_file", path="README.md"),
"README.md describes a Python agent framework.",
])
print(agent.copy(model=fake).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
@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)
def waiting_for_user(state: State) -> bool:
if state.pending_calls or not state.messages:
return False
last = state.messages[-1]
return last.role == "assistant" and not last.tool_calls
@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, a function you write that says the model has answered, or after 30 turns.- To change the agent, add, remove or reorder lines in
coding.
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 one way to talk to it
agent.run(state) # continues the same conversation
- Every change to a State adds one entry to
state.history, andstate.messages,state.turnandstate.answerare computed from it. Nothing else is saved, and nothing is hidden. state.snapshot()returns a frozenStateSnapshot.state.restore(snapshot)goes back to it, andstate.fork()gives a copy to try something on.state.extra_datais a notepad for your own values. Change it withwith state.edit_extra_data() as data:.FileStore("runs")withAgent(store=...)saves the history, sostore.load(id)continues it in another process.
Documentation
tgoddessana.github.io/alpineagents. Read it in this order:
- Concepts: how a run works, then Agent, State, loops and tools. About 15 minutes.
- Guides: one task per page, for example asking before a tool runs or testing an agent.
- API reference: every class and method.
Roadmap
- OpenAI adapter, LiteLLM adapter
- Subagents (
tools=[researcher]). Passing an Agent intools=raisesNotImplementedErrorfor now - 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.
Metadata
Release files for alpineagents 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| alpineagents-0.5.0.tar.gz | 471.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| alpineagents-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 637.2 kB
Release files / alpineagents-0.5.0.tar.gz
| Download URL | alpineagents-0.5.0.tar.gz |
|---|---|
| Size | 471.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
76a47c45d6ecd8e24982e8eb3fc8b5037f6a3589ec0ba403c7ce4481e1b52807
|
|
BLAKE2b-256 checksum How to use checksums |
90fca48e27d03b7c75dd805c3eb837049875f3f9db86937d65907c94e83a0ea5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / alpineagents-0.5.0-py3-none-any.whl
| Download URL | alpineagents-0.5.0-py3-none-any.whl |
|---|---|
| Size | 165.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7947990972e81833421f40cc5347ce50b0987ffa19742d30a6ceb61c9f957434
|
|
BLAKE2b-256 checksum How to use checksums |
e0c1211f208951841ccf1ceec9eb99c59eadcfb383333eb078164229759f6cb2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log