avo
A provider-agnostic reliability runtime for bounded, observable, resumable, and replayable AI agent loops.
Bounded. Resumable. Provider-agnostic. Honest about why it stopped.
avo packages a strict state machine, an append-only event history, configurable
safety policies, a provider-neutral interface, and a sandboxed application-tools
layer. To use it you install the avo package, set AVO_* environment variables,
and import avo from Python.
⚠️ avo 0.1 is an alpha foundation. It is suitable for evaluation, deterministic testing, and local prototypes; it is not production-ready.
Install
Requires Python 3.11+. The core runtime depends only on Pydantic.
From this repository
git clone https://github.com/Fqih/avo.git
cd Avo
python -m pip install -e ".[dev]"
Optional extras
| Extra | Adds | When you need it |
|---|---|---|
[dev] |
pytest, mypy, ruff, coverage | Local development + test runs |
[providers] |
httpx |
Talking to MiniMax, Anthropic, or any OpenAI-compatible endpoint |
[sandbox] |
docker (docker-py) |
Using run_shell against a real Docker daemon |
[live-benchmark] |
httpx, matplotlib |
Running python benchmark/run_benchmark.py and reproducing case-study charts |
[mcp] |
mcp SDK |
Authoring MCP servers or using SDK transports beyond stdio |
# Most common: providers + sandbox + dev tooling
python -m pip install -e ".[dev,providers,sandbox]"
[sandbox] is required only for the run_shell tool. Everything else (chat REPL,
FakeProvider, SQLite store, file tools) works without Docker.
Verify the install
avo doctor
This prints the resolved provider / model / endpoint without sending any HTTP request — the cheapest possible smoke test.
Configuration
Avo is configured through the AVO_* environment-variable family. Set them
once per shell (or persist to ~/.zshrc / ~/.bashrc via the avo chat first-run
wizard) and every run picks them up.
Environment variables
Every variable the runtime reads is AVO_*-prefixed. Set them in your shell or via
a .env file.
Provider selection
| Variable | Required | Purpose |
|---|---|---|
AVO_PROVIDER |
yes | ollama | minimax | anthropic | openai |
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 | MiniMax-specific model 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 | Anthropic-specific model 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 | OpenAI-specific model override |
Runtime and policy overrides
| Variable | Default | Purpose |
|---|---|---|
AVO_DATABASE_PATH |
empty (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 that gate on approval_callback |
AVO_USAGE_RATES_INPUT_PER_1K |
unset | Cost rate for input tokens (for ledger) |
AVO_USAGE_RATES_OUTPUT_PER_1K |
unset | Cost rate for output tokens (for ledger) |
Notifications
| Variable | Default | Purpose |
|---|---|---|
AVO_NOTIFY_WEBHOOK |
unset | URL to POST run lifecycle events to |
AVO_NOTIFY_DESKTOP |
0 |
Set to 1 to enable desktop notifications |
Pick a provider and export
| Provider | Local? | Needs API key | Default style |
|---|---|---|---|
ollama |
yes | no | /api/chat |
minimax |
no | yes | Anthropic-compatible |
anthropic |
no | yes | Anthropic Messages API |
openai |
no | yes | OpenAI Chat Completions |
# Ollama (local, no key)
export AVO_PROVIDER=ollama
export AVO_MODEL=llama3.1
# optional: export AVO_OLLAMA_BASE_URL=http://localhost:11434
# MiniMax
export AVO_PROVIDER=minimax
export AVO_MODEL=MiniMax-M3
export AVO_MINIMAX_API_KEY='paste-real-key-here'
# optional: export AVO_MINIMAX_API_STYLE=anthropic # or "openai"
# optional: export AVO_MINIMAX_BASE_URL=https://api.minimax.io
# Anthropic
export AVO_PROVIDER=anthropic
export AVO_MODEL=claude-sonnet-4-6
export AVO_ANTHROPIC_API_KEY='paste-real-key-here'
# OpenAI-compatible (any vendor exposing /v1/chat/completions)
export AVO_PROVIDER=openai
export AVO_MODEL=gpt-5.6
export AVO_OPENAI_API_KEY='paste-real-key-here'
# optional: export AVO_OPENAI_BASE_URL=https://api.openai.com/v1
Use getpass if you script the export. See .env.example for a full
template — placeholders only, never commit real keys.
Smoke-test
avo doctor # verifies config without an HTTP call
avo chat --workspace-root . # interactive REPL, drives one run per input
On a fresh machine with no AVO_PROVIDER set, avo chat launches an
interactive first-run wizard that asks for the provider, hidden-prompt API key, and
model. At the end it offers (default No) to persist the variables to ~/.zshrc or
~/.bashrc so subsequent shells see them automatically. Nothing is written unless you
type y/yes.
A single factory builds the right provider from the environment:
from avo.config import build_provider_from_env
provider = build_provider_from_env() # raises ConfigError if anything required is missing
Providers
Avo ships with four built-in adapters. All read configuration through the
AVO_-prefixed environment; agent code never touches URLs or keys directly.
| Provider | Adapter | Notes |
|---|---|---|
| Ollama | OllamaProvider |
Local HTTP, no key. Default for offline development. |
| 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, etc. |
Every adapter implements the same ModelProvider Protocol, so swapping providers is a
one-line change. FakeProvider records scripted ModelResponses so tests never need an
API key:
from avo import AgentRuntime, FakeProvider, ModelResponse
runtime = AgentRuntime(
provider=FakeProvider([
ModelResponse(content="Hello from a scripted provider."),
]),
)
Quickstart
One typed tool call, then a final reply, no API key required:
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.
Application tools
avo.app_tools is the optional-but-default toolkit for code-writing and ops agents.
All tools plug in via the existing FunctionTool / ToolRegistry contract — no change
to the runtime, the state machine, or the event log.
| Tool | Module | What it does |
|---|---|---|
read_file |
file_tools.read_file_tool |
Read a file inside the active workspace. |
write_file |
file_tools.write_file_tool |
Write (overwrite) a file inside the active workspace. |
edit_file |
edit_file.edit_file_tool |
Surgical string replacement (requires unique match unless replace_all=True). |
glob |
glob_tool.glob_tool |
Enumerate files matching a glob, paths returned relative to workspace root. |
grep |
grep_tool.grep_tool |
Regex search with optional include_glob and context_lines. |
workspace_map |
workspace_map.workspace_map_tool |
Compact file map + recently-modified files. |
git_status |
git_status.git_status_tool |
Branch, modified files, optional untracked files. |
run_shell |
shell_tool.run_shell_tool |
One shell command inside an ephemeral Docker container. |
plan_tasks |
plan_tasks.plan_tasks_tool |
Declare / update / complete a structured execution plan. |
submit_plan |
plan_tool.submit_plan_tool |
Record the active run's plan for permission_mode=plan. |
task |
task_tool.task_tool |
Dispatch an isolated sub-agent run (explore or general). |
web_fetch |
web_fetch.web_fetch_tool |
HTTP GET with a hard max_bytes cap; http/https only. |
web_search |
web_search.web_search_tool |
Web search via DuckDuckGo HTML; no API key required. |
Workspace safety
Workspace(root).validate_path(...) rejects ../, symlink escapes, absolute-path
escapes, and null bytes before any I/O. validate_for_write also refuses to follow
symlinks at the leaf or any parent; write_file and edit_file open with O_NOFOLLOW
on POSIX as a second line of defense. There is no path the model can ask for that 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"(default — fully offline), - applies a
mem_limit(default256m) andcpu_quota(default50000), - times out via the runtime's
LoopPolicy.tool_timeout_seconds, - removes the container before returning.
run_shell never calls subprocess on the host. The sandbox is the only path to
the shell.
Wiring tools into the runtime
from contextlib import contextmanager
from avo import AgentRuntime, FunctionTool
from avo.app_tools.file_tools import bind_workspace, read_file_tool, write_file_tool
from avo.app_tools.shell_tool import bind_sandbox, run_shell_tool
from avo.app_tools.sandbox import SandboxExecutor
from avo.app_tools.workspace import Workspace
workspace = Workspace("/srv/agent-workspace")
sandbox = SandboxExecutor(network_mode="none", mem_limit="256m")
with bind_workspace(workspace), bind_sandbox(sandbox):
runtime = AgentRuntime(
provider=provider,
tools=[read_file_tool(), write_file_tool(), run_shell_tool()],
)
result = await runtime.run("Add a Makefile to the workspace root.")
The workspace binding is required — calling read_file / write_file / run_shell
outside a bind_workspace(...) block raises WorkspaceNotBoundError /
SandboxNotBoundError. This prevents the runtime from ever reaching the host filesystem
without an explicit workspace decision.
Approval policy
AVO_TOOLS_REQUIRE_APPROVAL is a comma- or whitespace-separated list of tool
names that must wait for explicit operator approval before the runtime executes them.
Tools not in the list are auto-approved without invoking any callback. For 0.1 the
built-in callback denies listed tools (returning False so the runtime stops with
StopReason.POLICY_DENIED); wrap with on_require=... to escalate to an interactive
prompter:
from avo.app_tools.approval import build_approval_callback
callback = build_approval_callback(
on_require=lambda call: print(f"approving {call.name}({call.arguments})"),
)
runtime = AgentRuntime(provider=provider, tools=[...], approval_callback=callback)
CLI
avo [-d DATABASE] <command> [args]
| Command | What it does |
|---|---|
avo doctor |
Verify AVO_* configuration without an HTTP call. |
avo chat [-d DATABASE] [--workspace-root PATH] |
Interactive REPL; one AgentRuntime.run per input. First run with no provider triggers the setup wizard. |
avo runs list |
Print one line per run: RUN_ID, STATE, STOP_REASON, STEPS. |
avo runs inspect RUN_ID |
Render the chronological trace (text). |
avo runs resume RUN_ID |
Resume a persisted run whose latest checkpoint uses the built-in FakeProvider and has no pending tool call. |
The chat REPL accepts slash commands:
| Slash command | Action |
|---|---|
/provider |
Print provider / model / base URL / key-presence. |
/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 needed):
| File | Demonstrates |
|---|---|
examples/basic_agent.py |
One typed tool call followed by a final response. |
examples/repeated_action.py |
Deterministic repeated-action containment before the third side effect. |
examples/resume_after_interrupt.py |
Interrupt mid-flight, reopen SQLite, resume without replaying side effects. |
examples/app_tools_demo.py |
Workspace + file tools + permissive approval; walks the runtime through a one-step file edit. |
examples/live_providers/ |
Real-API smoke tests for each provider (require AVO_* keys). |
Run any of them:
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 (from pyproject.toml):
- ruff lint + format — line-length 100, strict per-file ignores for
examples/,benchmark/. - mypy strict on
src/avo(Pydantic plugin enabled). - pytest
--strict-config --strict-markers, asyncio mode auto. - coverage branch coverage, fail-under 90%.
The pytest suite does not require Docker — SandboxExecutor accepts an injectable
client so the suite injects a fake and asserts the container configuration that would
be sent to docker. Live Docker integration is opt-in, same pattern as
benchmark/live/tests/.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file avo-0.1.1.tar.gz.
File metadata
- Download URL: avo-0.1.1.tar.gz
- Upload date:
- Size: 722.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4fb0a1a1d0f425c57a46c7134cca03a2e2172f39a428983b5576f81f0bffb9a
|
|
| MD5 |
787bdc5f6927c50cab3ba5ebc20ac1c3
|
|
| BLAKE2b-256 |
387d46ff0fc445a7e040fe7278bc882bca88a0dfa3e1cf271e156e7a7546b4aa
|
File details
Details for the file avo-0.1.1-py3-none-any.whl.
File metadata
- Download URL: avo-0.1.1-py3-none-any.whl
- Upload date:
- Size: 165.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97f0de585e774c48ddadc43bff41607925220e00139df7bf87657bffe61795f8
|
|
| MD5 |
a562c1e59eb050d648d4fd84821c7a8e
|
|
| BLAKE2b-256 |
5822d3ad662d58afadf9669f5f169ca65e256e51ab120e08a04c663704a7fd33
|