A minimal async agent framework in Python. An agent loop drives an LLM that can call tools, with provider details abstracted behind a provider-agnostic facade.
Requires Python >= 3.11 and uv.
cd minimal_agent
uv syncCopy .env.example to .env and set your API key:
cp .env.example .env
# Edit .env — set LLM_BACKEND and LLM_BACKEND_API_KEY| Backend | LLM_BACKEND |
Notes |
|---|---|---|
| OpenAI | openai |
Default. Uses gpt-4o-mini by default |
| Anthropic | anthropic |
Via OpenAI-compatible endpoint |
| OpenRouter | openrouter |
Any model on OpenRouter |
| Local server | localhost |
vLLM, llama.cpp, LM Studio, Ollama — set LLM_BACKEND_BASE_URL |
minimal_agent is an installable library. You create a new project that depends on it, wire up the tools you want, and run it however you like.
my_project/
main.py
pyproject.toml
In pyproject.toml, add minimal_agent as a dependency (path reference to the local package):
[project]
name = "my-project"
requires-python = ">=3.11"
dependencies = [
"minimal-agent",
]
[tool.uv.sources]
minimal-agent = { path = "../minimal_agent" }# main.py
import asyncio
from pathlib import Path
from minimal_agent import Agent, Session, Settings
from minimal_agent.llm import LLM
from minimal_agent.tools.builtin.read_file import ReadFile
from minimal_agent.tools.builtin.run_shell import RunShell
settings = Settings()
workspace = Path.cwd()
llm = LLM(
model=settings.LLM_MODEL,
backend=settings.LLM_BACKEND,
)
agent = Agent(
llm=llm,
tools=[
ReadFile(workspace_root=workspace),
RunShell(workspace_root=workspace),
],
)
async def main():
system_prompt = await agent.build_system_prompt(workspace_root=workspace)
session = Session.create(
model=settings.LLM_MODEL,
backend=settings.LLM_BACKEND,
system_prompt=system_prompt,
)
# Add a user message
from minimal_agent.llm import Message, Role
session.context.add(Message(role=Role.USER, content="List the files in this directory"))
# Run the agent loop
async for message in agent.run(session.context):
if message.role == Role.ASSISTANT and message.content:
print(message.content)
asyncio.run(main())That's a working agent. It reads the user message, calls the LLM, uses tools if needed, and prints the response.
Create a tool by subclassing BaseTool. A tool needs three things: a name, a Pydantic input schema, and an invoke method.
from pydantic import BaseModel, Field
from minimal_agent.tools.base import BaseTool
from minimal_agent.tools.context import ToolContext
class LookupInput(BaseModel):
"""Look up a customer by email."""
email: str = Field(..., description="Customer email address")
class LookupCustomer(BaseTool[LookupInput, str]):
name = "lookup_customer"
input_schema = LookupInput
def __init__(self, db):
self._db = db
async def invoke(self, args: LookupInput, ctx: ToolContext) -> str:
customer = await self._db.find_by_email(args.email)
if not customer:
return "No customer found"
return f"Found: {customer.name} (id={customer.id})"Then pass it to your agent:
agent = Agent(
llm=llm,
tools=[
LookupCustomer(db=my_database),
ReadFile(workspace_root=workspace),
],
)The model sees the tool's name, the docstring on the input schema (as the tool description), and the field descriptions. That's all it needs to decide when and how to call it.
Override these methods on BaseTool for more control:
needs_permission(args)— ReturnTrueto require user confirmation before execution. Use for destructive operations.validate(args)— Semantic validation beyond what Pydantic checks (e.g., "file must exist"). ReturnValidationOk()orValidationErr("reason").render_result_for_assistant(result)— Customize what the model sees after the tool runs. Default isstr(result).
By default, the agent uses a built-in software engineering prompt. Pass your own:
agent = Agent(
llm=llm,
tools=[...],
prompt="You are a customer support agent. Be helpful and concise.",
)Or point to a markdown file:
agent = Agent(
llm=llm,
tools=[...],
prompt=Path("prompts/support_agent.md"),
)Context sources inject dynamic information into the system prompt (git status, directory trees, or anything you define). Implement the ContextSource protocol:
from minimal_agent.system_prompt import ContextSource
class DatabaseSchemaSource:
name = "db_schema"
async def gather(self, workspace_root) -> str:
# Return whatever context you want injected
return "Tables: users, orders, products ..."
agent = Agent(
llm=llm,
tools=[...],
prompt="You are a database assistant.",
context_sources=[DatabaseSchemaSource()],
)read_file, write_file, edit_file, glob, grep, run_shell, spawn_agents, web_search, web_extract, get_weather (stub)
Skills are reusable prompt templates stored as markdown files. Drop a SKILL.md at .minimal_agent/skills/<name>/SKILL.md (project-level) or ~/.minimal_agent/skills/<name>/SKILL.md (user-level), and the agent sees it in its skill list. When the model decides a skill is relevant, it loads the full instructions on demand via the built-in skill tool — cheap metadata always, expensive prompt only when needed.
Skills are auto-discovered when you pass workspace_root to the Agent. Format follows the official Agent Skills Specification. See minimal_agent/README.md for authoring details.
The example/ directory contains a ready-to-run chat application with a FastAPI backend and a React frontend that demonstrates the agent in action.
example/
server/ # FastAPI + SSE streaming
web/ # React + Vite + Tailwind
- Python 3.11+ with uv
- Node.js 18+
- An API key for your chosen LLM backend
cd example/server
uv sync
cp .env.example .env # then edit with your API keys
python main.py # runs on http://localhost:8000Key env vars in .env:
| Variable | Purpose |
|---|---|
LLM_BACKEND |
openai, anthropic, openrouter, or localhost |
LLM_BACKEND_API_KEY |
API key for the chosen backend |
LLM_MODEL |
Model name (e.g. gpt-4o-mini) |
TAVILY_API_KEY |
Enables web_search and web_extract tools |
ALLOWED_WORKSPACES |
Comma-separated paths the agent is allowed to access |
cd example/web
npm install
npm run dev # runs on http://localhost:5173The Vite dev server proxies /api/* requests to the backend on port 8000.
- Open
http://localhost:5173 - Create a new session (pick a workspace directory)
- Chat with the agent — it can read/write/edit files, run shell commands, search the web, and more
The agent streams responses back via Server-Sent Events, and session history is persisted to disk so you can pick up where you left off.
cd minimal_agent
make format # ruff format
make lint # ruff check
make test # pytest