Skip to main content

Avo logo

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 (default 256m) and cpu_quota (default 50000),
  • 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

avo-0.1.1.tar.gz (722.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

avo-0.1.1-py3-none-any.whl (165.3 kB view details)

Uploaded Python 3

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

Hashes for avo-0.1.1.tar.gz
Algorithm Hash digest
SHA256 f4fb0a1a1d0f425c57a46c7134cca03a2e2172f39a428983b5576f81f0bffb9a
MD5 787bdc5f6927c50cab3ba5ebc20ac1c3
BLAKE2b-256 387d46ff0fc445a7e040fe7278bc882bca88a0dfa3e1cf271e156e7a7546b4aa

See more details on using hashes here.

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

Hashes for avo-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 97f0de585e774c48ddadc43bff41607925220e00139df7bf87657bffe61795f8
MD5 a562c1e59eb050d648d4fd84821c7a8e
BLAKE2b-256 5822d3ad662d58afadf9669f5f169ca65e256e51ab120e08a04c663704a7fd33

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page