Metadata-Version: 2.5
Name: avo
Version: 0.1.2
Summary: Reliable execution infrastructure for long-running AI agents.
Project-URL: Homepage, https://github.com/Fqih/avo
Project-URL: Repository, https://github.com/Fqih/avo
Project-URL: Issues, https://github.com/Fqih/avo/issues
Author: FaqihHakim
License: MIT License
        
        Copyright (c) 2026 FaqihHakim
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agents,ai,reliability,runtime,sqlite
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: pydantic<3,>=2.8
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: coverage[toml]>=7.6; extra == 'dev'
Requires-Dist: hatchling>=1.26; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: live-benchmark
Requires-Dist: httpx>=0.27; extra == 'live-benchmark'
Requires-Dist: matplotlib>=3.8; extra == 'live-benchmark'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: providers
Requires-Dist: httpx>=0.27; extra == 'providers'
Provides-Extra: sandbox
Requires-Dist: docker>=7.0; extra == 'sandbox'
Description-Content-Type: text/markdown

<div align="center">

<img src="logo.svg" width="240" alt="avo logo">

**Provider-agnostic reliability runtime for bounded, observable, resumable, replayable AI agent loops.**

*Bounded. Resumable. Provider-agnostic. Honest about why it stopped.*

</div>

---

> ⚠️ 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.

```bash
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 |
| `[live-benchmark]` | httpx, matplotlib | Running `python benchmark/run_benchmark.py` |
| `[mcp]` | mcp SDK | Authoring MCP servers or non-stdio transports |

Verify the install:

```bash
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:

```python
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` |
| `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 |

### 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`](.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. |

All four 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:

```python
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:

```python
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)
```

---

## CLI

```bash
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 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. |

### 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). |

```bash
python examples/basic_agent.py
python examples/repeated_action.py
python examples/resume_after_interrupt.py
python examples/app_tools_demo.py
```

---

## Development

```bash
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`](LICENSE).
