Provider-agnostic reliability runtime for bounded, observable, resumable, replayable AI agent loops.
Bounded. Resumable. Provider-agnostic. Honest about why it stopped.
⚠️ 0.1 is an alpha foundation. Suitable for evaluation, deterministic tests, and local prototypes; not production-ready.
Install
Requires Python 3.11+. Core runtime depends only on Pydantic.
git clone https://github.com/Fqih/avo.git
cd avo
python -m pip install -e ".[dev,providers,sandbox]"
Optional extras
| Extra | Adds | When you need it |
|---|---|---|
[dev] |
pytest, mypy, ruff, coverage | Local dev + tests |
[providers] |
httpx | Talking to MiniMax, Anthropic, OpenAI-compatible endpoints |
[sandbox] |
docker-py | Using run_shell against a real Docker daemon |
[otel] |
opentelemetry-api, sdk, otlp exporter | Emitting gen_ai.* spans for a run |
[live-benchmark] |
httpx, matplotlib | Running python benchmark/run_benchmark.py |
[mcp] |
mcp SDK | Authoring MCP servers or non-stdio transports |
Verify the install:
avo doctor
Prints resolved provider / model / endpoint without an HTTP call — cheapest smoke test.
Quickstart
One typed tool call, then a final reply. No API key:
import asyncio
from pydantic import BaseModel
from avo import (
AgentRuntime, FunctionTool, ModelResponse, TokenUsage, ToolCall,
)
from avo.providers import FakeProvider
class AddArguments(BaseModel):
left: int
right: int
async def add(arguments: AddArguments) -> object:
return {"sum": arguments.left + arguments.right}
async def main() -> None:
provider = FakeProvider(
[
ModelResponse(
tool_call=ToolCall(
tool_call_id="addition-1", name="add",
arguments={"left": 2, "right": 3},
),
usage=TokenUsage(input_tokens=12, output_tokens=5),
),
ModelResponse(
content="The sum is 5.",
usage=TokenUsage(input_tokens=18, output_tokens=6),
),
]
)
runtime = AgentRuntime(
provider=provider,
tools=[
FunctionTool(
name="add",
description="Add two integers.",
arguments_model=AddArguments,
function=add,
)
],
)
result = await runtime.run("What is 2 + 3?")
print(result.status.value, result.stop_reason.value, result.output)
asyncio.run(main())
examples/basic_agent.py ships this runnable end-to-end.
Configuration
All knobs live in AVO_* env vars. The chat REPL's first-run wizard can persist them to ~/.zshrc / ~/.bashrc.
Provider selection
| Variable | Required | Purpose |
|---|---|---|
AVO_PROVIDER |
yes | ollama | minimax | anthropic | openai | groq | cerebras | openrouter | gemini |
AVO_MODEL |
yes | Default model name for the active provider |
AVO_OLLAMA_BASE_URL |
no | Ollama endpoint (default http://localhost:11434) |
AVO_OLLAMA_MODEL |
no | Ollama-specific model override |
AVO_OLLAMA_API_KEY |
no | Ollama auth header (rarely needed) |
AVO_MINIMAX_API_KEY |
yes for minimax | API key |
AVO_MINIMAX_BASE_URL |
no | Default https://api.minimax.io |
AVO_MINIMAX_MODEL |
no | Provider-specific override |
AVO_MINIMAX_API_STYLE |
no | anthropic (default) or openai |
AVO_ANTHROPIC_API_KEY |
yes for anthropic | API key |
AVO_ANTHROPIC_BASE_URL |
no | Default https://api.anthropic.com |
AVO_ANTHROPIC_MODEL |
no | Provider-specific override |
AVO_OPENAI_API_KEY |
yes for openai | API key |
AVO_OPENAI_BASE_URL |
no | Default https://api.openai.com/v1 |
AVO_OPENAI_MODEL |
no | Provider-specific override |
AVO_GROQ_API_KEY |
yes for groq | Groq API key |
AVO_GROQ_BASE_URL |
no | Default https://api.groq.com/openai/v1 |
AVO_GROQ_MODEL |
no | Provider-specific override |
AVO_CEREBRAS_API_KEY |
yes for cerebras | Cerebras API key |
AVO_CEREBRAS_BASE_URL |
no | Default https://api.cerebras.ai/v1 |
AVO_CEREBRAS_MODEL |
no | Provider-specific override |
AVO_OPENROUTER_API_KEY |
yes for openrouter | OpenRouter API key |
AVO_OPENROUTER_BASE_URL |
no | Default https://openrouter.ai/api/v1 |
AVO_OPENROUTER_MODEL |
no | Provider-specific override |
AVO_GEMINI_API_KEY |
yes for gemini | Gemini API key |
AVO_GEMINI_BASE_URL |
no | Default https://generativelanguage.googleapis.com |
AVO_GEMINI_MODEL |
no | Provider-specific override |
Runtime + policy
| Variable | Default | Purpose |
|---|---|---|
AVO_DATABASE_PATH |
in-memory | SQLite path for the run/event store |
AVO_MAX_TOTAL_TOKENS |
unlimited | Override LoopPolicy.max_total_tokens |
AVO_MAX_RUNTIME_SECONDS |
300 |
Override LoopPolicy.max_runtime_seconds |
AVO_REPEATED_ACTION_LIMIT |
3 |
Override LoopPolicy.repeated_action_limit |
AVO_PERMISSION_MODE |
default |
default / accept_edits / plan / bypass |
AVO_TOOLS_REQUIRE_APPROVAL |
empty | Comma-separated tool names gating on approval_callback |
AVO_USAGE_RATES_INPUT_PER_1K |
unset | Cost rate for input tokens |
AVO_USAGE_RATES_OUTPUT_PER_1K |
unset | Cost rate for output tokens |
AVO_NOTIFY_WEBHOOK |
unset | URL to POST run lifecycle events to |
AVO_NOTIFY_DESKTOP |
0 |
Set to 1 to enable desktop notifications |
See .env.example for a copy-paste template.
Providers
| Provider | Adapter | Notes |
|---|---|---|
| Ollama | OllamaProvider |
Local HTTP, no key. Default for offline dev. |
| MiniMax | MiniMaxProvider |
Anthropic-compatible (default) or OpenAI-compatible style. |
| Anthropic | AnthropicProvider |
Native Anthropic Messages API. |
| OpenAI | OpenAICompatibleProvider |
Any /v1/chat/completions endpoint — OpenAI, vLLM, llama.cpp. |
| Groq | GroqProvider |
OpenAI-compatible Llama / Mixtral inference, low latency. |
| Cerebras | CerebrasProvider |
OpenAI-compatible inference on Cerebras wafer-scale hardware. |
| OpenRouter | OpenRouterProvider |
OpenAI-compatible gateway to 300+ models, free tier included. |
| Google Gemini | GeminiProvider |
Native Gemini generateContent REST API with function calling. |
All eight implement the same ModelProvider Protocol. Swapping providers is one line.
Application tools
avo.app_tools is the optional-but-default toolkit. Tools plug into the existing
FunctionTool / ToolRegistry contract — no changes to the runtime, state machine, or
event log.
| Tool | What it does |
|---|---|
read_file / write_file / edit_file |
Workspace-scoped file I/O |
glob / grep / workspace_map |
Workspace enumeration + search |
git_status |
Branch, modified, optional untracked files |
run_shell |
One shell command in an ephemeral Docker container |
plan_tasks / submit_plan |
Structured plan declaration + persistence |
task |
Dispatch isolated sub-agent run |
web_fetch / web_search |
HTTP GET with hard byte cap / DuckDuckGo HTML search |
Workspace safety
Workspace(root).validate_path(...) rejects ../, symlink escapes, absolute-path
escapes, and null bytes before any I/O. validate_for_write refuses to follow
symlinks at the leaf or any parent. write_file / edit_file open with O_NOFOLLOW on
POSIX. No path the model can ask for exits the workspace root.
Shell sandbox
SandboxExecutor wraps docker-py. Each run_shell call creates a fresh container
(remove=True), runs with network_mode="none" by default, applies a mem_limit and
cpu_quota, times out via the runtime's LoopPolicy.tool_timeout_seconds, and removes
the container before returning. run_shell never calls subprocess on the host — the
sandbox is the only path to the shell.
Suit the network policy to your task:
from avo.app_tools.sandbox import SandboxExecutor
sandbox = SandboxExecutor(
network_mode="bridge", # default "none" — switch when network is required
mem_limit="512m",
cpu_quota=100000,
)
Approval policy
AVO_TOOLS_REQUIRE_APPROVAL lists tool names that must wait for explicit operator
approval. Tools not in the list auto-approve. Wire a custom callback:
from avo.app_tools.approval import build_approval_callback
callback = build_approval_callback(
on_require=lambda call: input(f"approve {call.name}? [y/N] ").lower() == "y",
)
runtime = AgentRuntime(provider=provider, tools=[...], approval_callback=callback)
Observability (OpenTelemetry)
Set AVO_OTEL_ENABLED=1 and the runtime wraps every _drive invocation in a span
tagged with the gen_ai.* semantic conventions:
python -m pip install -e ".[otel]"
export AVO_OTEL_ENABLED=1
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
avo chat
Spans carry avo.run_id, gen_ai.system (provider name), and gen_ai.request.model.
avo.observability.record_usage(...) writes input / output token counts into the
active span, so cost rollups line up with trace data. The module is a noop when the
extra is not installed — no exceptions at import time, no runtime overhead.
Cost tracking
avo cost aggregates every persisted ledger entry (the same avo.db the event store
uses) and prints total tokens + USD spend, with per-run and per-model breakdowns.
Output is human-readable by default and machine-readable with --json:
avo cost --database avo.db --json
{
"run_count": 2,
"total": {"input_tokens": 700, "output_tokens": 370, "total_tokens": 1070},
"cost_usd": "0.0142",
"runs": [...],
"models": [...]
}
Set AVO_USAGE_RATES_INPUT_PER_1K / AVO_USAGE_RATES_OUTPUT_PER_1K to seed the ledger
with USD costs as each provider call returns.
Scaffolding plugins
avo plugin init writes a working plugin to disk so you can iterate on a new tool
without touching project structure by hand:
mkdir ~/projects/my-tool && cd ~/projects/my-tool
avo plugin init my-tool
cd my-tool
avo plugin install . # registers the sample echo tool
The scaffold ships a pyproject.toml declaring an avo.tools entry point, a
register() stub returning a sample FunctionTool, a README.md, and a .gitignore.
Replace the sample tool with your own and the runtime picks it up on the next
avo plugin install ..
CLI
avo [-d DATABASE] <command> [args]
| Command | What it does |
|---|---|
avo doctor |
Verify AVO_* config without an HTTP call. |
avo chat [-d PATH] [--workspace-root DIR] [--session ID] [--new-session] |
Interactive REPL. First run with no provider triggers the setup wizard. |
avo runs list |
Print one line per run. |
avo runs inspect RUN_ID |
Render the chronological trace. |
avo runs resume RUN_ID |
Resume a persisted FakeProvider run with no pending tool call. |
avo plugin install URL | PATH |
Install a plugin from git URL or local path. |
avo plugin list / show NAME / remove NAME [-y] |
Manage installed plugins. |
avo plugin init [NAME] [-d DIR] [--force] |
Scaffold a new plugin (pyproject + sample FunctionTool). |
avo mcp add NAME [--env KEY=VAL]... CMD ARGS... |
Register an MCP server. |
avo mcp list / remove NAME [-y] |
Manage MCP server registrations. |
avo skill install PATH / list / show NAME / remove NAME [-y] |
Manage skill packs. |
avo bench [--turns N] [--task ID] [--output PATH] |
Deterministic FakeProvider benchmark. |
avo runs diff RUN_A RUN_B [--json] |
Compare two persisted runs. |
avo cost [--database PATH] [--json] |
Aggregate token + USD spend across runs. |
avo sandbox run --image IMG --workspace DIR [--network MODE] -- COMMAND ARGS... |
One-shot ephemeral docker sandbox. |
Chat REPL slash commands
| Slash command | Action |
|---|---|
/help |
Print the full slash-command list. |
/provider |
Print provider / model / base URL / key-presence. |
/model [NAME] |
Switch to NAME or pick from the catalog (/model alone). |
/inspect RUN_ID |
Render a stored trace. |
/resume RUN_ID |
Resume a stored run. |
/skills |
List skills under <workspace>/.avo/skills. |
/skill NAME |
Inject a skill body as the next user turn. |
/quit / /exit |
Exit the REPL. |
Examples
examples/ ships runnable Python files, all offline (no API key):
| File | Demonstrates |
|---|---|
examples/basic_agent.py |
One typed tool call, then a final reply. |
examples/repeated_action.py |
Deterministic repeated-action containment. |
examples/resume_after_interrupt.py |
Interrupt mid-flight, reopen SQLite, resume. |
examples/app_tools_demo.py |
Workspace + file tools + permissive approval. |
examples/live_providers/ |
Real-API smoke tests per provider (need AVO_* keys). |
python examples/basic_agent.py
python examples/repeated_action.py
python examples/resume_after_interrupt.py
python examples/app_tools_demo.py
Development
python -m pip install -e ".[dev,providers,sandbox]"
ruff check .
ruff format --check .
mypy src/avo
pytest
Quality gates:
- ruff lint + format — line-length 100, per-file ignores for
examples/,benchmark/. - mypy strict on
src/avo(Pydantic plugin). - pytest
--strict-config --strict-markers, asyncio mode auto. - coverage branch coverage, fail-under 90%.
The suite needs no Docker — SandboxExecutor accepts an injectable client so tests
inject a fake and assert the container config that would be sent. Live Docker integration
is opt-in, same pattern as benchmark/live/tests/.
License
MIT — see LICENSE.
Metadata
Release files for avo 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| avo-0.1.4.tar.gz | 582.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| avo-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 913.3 kB
Release files / avo-0.1.4.tar.gz
| Download URL | avo-0.1.4.tar.gz |
|---|---|
| Size | 582.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2e8eb5250c52c709e4f25adf25bf51c20e2259ae12bbde6e923161d0329e0c73
|
|
BLAKE2b-256 checksum How to use checksums |
5a382861d0320c932f83eca032e1e3a141394adcc88974f92e88ddb9931da764
|
| 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 Sep 15, 2026.
Transparency logRelease files / avo-0.1.4-py3-none-any.whl
| Download URL | avo-0.1.4-py3-none-any.whl |
|---|---|
| Size | 330.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0f9538785ba546798d062c5acd769f778c5dbc5d04fda9fdcaaf21918c1ca967
|
|
BLAKE2b-256 checksum How to use checksums |
9d1eff464fe6c6f8046337d635f459f7804ce1f41e0031175a3fc441e1308a43
|
| 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 Sep 15, 2026.
Transparency log