Skip to content

Repository files navigation

ai-agent

A typed, self-evolving agent harness for tool-using LLMs.

This is a self-evolving agent: it can inspect the repository, make scoped improvements, run checks, and propose changes as pull requests.

Agent = Model + Harness
The model reasons. This repository is the harness: the loop, tools, memory, retrieval, guardrails, and multi-agent runtime that turn a chat completion into a reliable agent.

Built with Clean Architecture, strict typing (mypy), and a test suite that includes mutation testing (mutmut) — designed as both a production starting point and a hireable reference codebase.

Python 3.12+ License: MIT Typed Tests uv


Why this exists

Most “agent demos” hide the interesting part inside a framework black box. This kit makes the harness explicit and readable:

Goal How this repo delivers
Learn / teach agent internals ReAct loop, tool registry, and ports are first-class modules — not magic
Ship a real assistant YAML config, pluggable tools, SQLite memory, DuckDuckGo search, optional Chroma RAG
Stay maintainable Domain → Application → Infrastructure, DI via protocols, no SDK lock-in
Prove quality pytest + mypy + ruff + mutmut on safety-critical paths

Agent harness capabilities

Everything below is deterministic software around the model (OpenRouter today — swap via LLMPort).

Capability Status What you get Where it lives
Decide → act → observe loop Done Bounded ReAct loop (respond / call_tools / done) with max_tool_rounds harness/loop.py
Structured decisions Done Pydantic AgentDecision; coerces tool-name-as-kind mistakes from LLMs domain/models.py
Tool registry & dispatch Done Name → tool map, config select(), safe execute() harness/registry.py
Argument validation Done Required / type / unknown-key checks before any tool I/O harness/tool_args.py
Web tools Done web_search (DuckDuckGo) + http_get (http/https only, size limits) tools/ · adapters/
Workspace tools Done Sandboxed workspace_search / read under workspace_root adapters/workspace_fs.py
Session state Done Per-conversation messages + tool traces; finished sessions restart cleanly domain/state.py · orchestration/runtime.py
Durable memory Done SQLite KV memory tool + session persistence adapters/sqlite_store.py
RAG as a tool Done Local Chroma (+ in-memory tests); ai-agent ingest --docs adapters/chroma_retriever.py
Multi-agent handoff Done Coordinator → message_agent → researcher (sync ask + timeout) orchestration/runtime.py · config/agents/
Serving surfaces Done Console CLI, WebSocket chat (--server), multi-agent demo entrypoints/cli.py · adapters/server.py
Guardrails (light) Done URL scheme allowlist, path jail, AST calculator, Python deny-list + subprocess timeout url_safety · workspace_fs · calculator · python_guard
Provider portability Done LLMPort + httpx OpenRouter adapter (no SDK lock-in) adapters/llm.py
Research Desk Done Personal Operator persona + brief → cited markdown under briefs/ config/agents/operator.yaml · features/brief
HITL approvals Done request_approval tool + optional brief --approve (console Y/n) tools/request_approval.py · adapters/approval.py
Code execution Done run_python — subprocess + AST deny-list (not full container sandbox) tools/run_python.py · harness/python_guard.py
Self-Harness (experimental) Scaffold Mine failures → propose YAML patches → human accept after pytest features/self_harness
Context compaction Done Summarize older turns under a char budget; keep full history in state harness/compaction.py
Runtime graph Done AgentFactory, spawn_agent, depth/child budgets, async execute_many orchestration/ · harness/registry.py
Self-Evolving Engineer Done evolve → plan/patch/checks → scoped commit → push → PR (optional --approve HITL) features/evolve · config/agents/engineer.yaml
  • Evolve artifacts live under .ai_agent/evolve/<run_id>/. | HarnessBank Gene Bank | Done (v1) | (where)×(why) cells + gated screening stubs; evolver ≠ task agent | features/harness_bank | | Organism worker (Phase 2) | Done | CI wait, MergePolicy, file scheduler, STOP kill switch | features/evolve/organism.py | | Ops metrics | Done (log) | JSONL OpsEvent, replay/compare hooks | harness/ops_metrics.py | | OS / container sandbox | — | Scoped limits only (not Docker/Firecracker) | Roadmap |

Model (not harness): whatever you set in YAML (openai/gpt-4o-mini, etc.) via OpenRouter.


Architecture

src/ai_agent/
├── domain/           # models, ConversationState, Tool protocol, ports
├── harness/          # Agent, ReAct loop, ToolRegistry, guards
├── features/         # brief, self_harness, evolve, harness_bank
├── adapters/         # OpenRouter, SQLite, Chroma, DuckDuckGo, FS, git, gh, WebSocket
├── orchestration/    # multi-agent runtime + AgentFactory / spawn
├── tools/            # agent-facing tools over ports
├── support/          # shared helpers (console I/O)
└── entrypoints/      # CLI composition root
                  ┌─────────────────────────────────────────┐
  User / WS  ───► │  AgentRuntime (orchestration)           │
                  │    inbox → Agent.step → outbox / spawn  │
                  └───────────────┬─────────────────────────┘
                                  │
                  ┌───────────────▼─────────────────────────┐
                  │  Harness loop (application)             │
                  │  decide → ToolRegistry (gather) → obs   │
                  └───────┬─────────────────────┬───────────┘
                          │                     │
                 LLMPort (model)         tools → ports → infra
                 OpenRouter              search / FS / git / PR / RAG

Dependency rule: inward only. The loop depends on protocols, never on httpx/Chroma/SQLite directly. See DECISIONS.md. Marketing line: Agent = Model + Harness—and the harness can evolve under proof.


Quick start

Requirements: Python 3.12+, an OpenRouter API key.

git clone https://github.com/pythonbyte/ai-agent.git
cd ai-agent

uv sync --all-extras
cp .env.example .env   # OPENROUTER_API_KEY=...

uv run ai-agent -c config/agent_config.yaml

With pip:

pip install -e ".[dev,rag]"
ai-agent -c config/agent_config.yaml

Modes

Mode Command Notes
Console uv run ai-agent -c config/agent_config.yaml Single agent
Research operator uv run ai-agent -c config/agents/operator.yaml Interactive Research Desk
One-shot brief uv run ai-agent brief "agent harness" Writes briefs/YYYYMMDD_slug.md
Brief + approve uv run ai-agent brief "topic" --approve Console Y/n before write
Evolve (engineer) uv run ai-agent evolve "…" Auto branch → commit → push → PR (--approve = HITL)
Evolve worker uv run ai-agent evolve-worker [--auto-merge] Phase 2 organism tick
HarnessBank uv run ai-agent harness-bank list|screen|compare Gene Bank + screening
Ops uv run ai-agent ops events / ops replay <path> Metrics + trace replay
WebSocket uv run ai-agent --server -v then websocat ws://localhost:8765 Chat-style plain text replies
Docker WebSocket docker compose up --build then websocat ws://localhost:8765 Agent runs in container; terminal client on host
Multi-agent uv run ai-agent --multi-agent -v Coordinator + researcher handoff
RAG ingest uv run ai-agent ingest --docs docs/ Needs [rag] extra
Harness propose uv run ai-agent harness propose Mine .ai_agent/failuresproposals/
Harness accept uv run ai-agent harness accept <id> Pytest gate, then merge into YAML

Personal Operator / Research Desk

First product vertical on this harness: a research operator that turns a topic into a cited brief.

uv run ai-agent brief "agent harness"
# → briefs/20260731_agent-harness.md  (Summary / Key findings / Sources / Open questions)

uv run ai-agent -c config/agents/operator.yaml   # interactive

The operator prefers web_searchhttp_get → local retrieve / workspace_search, remembers prefs via memory (pref.* keys), and never invents sources. Optional --approve gates publication. Irreversible future actions use the request_approval tool.


Moonshot: Self-Harness

Industry frontier (not “solved”): a fixed model improves the software around itself — prompts, tool descriptions, loop budgets — from execution evidence, without weight updates. Canonical loop: weakness mining → harness proposal → validation (held-in improves, held-out does not regress).

Reading path

Resource Why
Self-Harness (arXiv) Core paradigm + results
Lil’Log — Harness Engineering for Self-Improvement Map of self-improvement vs weight updates
LangChain — Anatomy of an Agent Harness Agent = Model + Harness vocabulary
Anthropic — Effective harnesses for long-running agents Long-horizon reliability

Scaffold in this repo (human-gated): failures auto-log on step exceptions; harness propose writes a JSON patch (prompt append / max_tool_rounds only); harness accept runs a pytest subset then merges into YAML. No auto-merge. No arbitrary Python edits in v0.

uv run ai-agent harness record-failure "Timed out waiting for researcher"
uv run ai-agent harness propose
uv run ai-agent harness accept patch_… -c config/agents/operator.yaml

Upgrade path: HarnessBank Gene Bank + gated screening (harness-bank screen) and the Self-Evolving Engineer (evolve) for code PRs under PathPolicy. See HarnessBank (arXiv:2607.13683).


Self-Evolving Engineer

uv run ai-agent evolve "add PathPolicy unit tests"
# optional HITL: add --approve
# → .ai_agent/evolve/<run_id>/{run.json,plan.md,result.json}
# → branch + PR after local checks (human merges)

uv run ai-agent evolve-worker              # schedule next wake; respect STOP
uv run ai-agent evolve-worker --auto-merge # Phase 2: CI + MergePolicy only

Touch .ai_agent/evolve/STOP to halt forever-loops.


Built-in tools

Tool Purpose
web_search DuckDuckGo search (titles, URLs, snippets)
http_get Fetch http(s) page text (timeout + size cap)
workspace_search Search / read files under workspace_root
workspace_list List paths (engineer)
apply_patch Unified diff under PathPolicy
run_checks pytest / ruff / mypy presets
git_status / git_diff / git_commit Git survey + approved commit
open_pull_request Push branch + gh pr create (approved)
spawn_agent Dynamic role spawn with depth/child budgets
retrieve Semantic search over ingested docs (Chroma)
memory Durable key/value facts (SQLite)
calculator Safe AST arithmetic
run_python Run short Python snippets (subprocess + import/call deny-list)
current_time Clock / timezone
note Ephemeral scratchpad (demo)
message_agent Ask another runtime agent (multi-agent)
request_approval Pause for human Y/n before irreversible actions

Enable tools by name in YAML — the registry resolves them at composition time.


Configuration

model: openai/gpt-4o-mini
system_prompt: >
  You are a helpful assistant with tools. Prefer tools for facts and
  computation instead of guessing.
max_tool_rounds: 5
personality:
  tone: professional
  style: concise
greeting: "Hello! How can I help?"
workspace_root: "."
sqlite_path: ".ai_agent/state.db"
chroma_path: ".ai_agent/chroma"
tools:
  - calculator
  - run_python
  - web_search
  - http_get
  - workspace_search
  - memory
  - retrieve
compaction:
  enabled: true
  max_context_chars: 48000
  keep_recent_messages: 12
  max_summary_chars: 4000

Multi-agent personas live under config/agents/.


Add a custom tool

from ai_agent import BaseTool, ToolParameter, ToolResult

class EchoTool(BaseTool):
    def __init__(self) -> None:
        super().__init__(
            name="echo",
            description="Return text in UPPERCASE.",
            parameters=[
                ToolParameter(name="text", type="string", description="Text", required=True)
            ],
        )

    async def execute(self, arguments: dict[str, object]) -> ToolResult:
        text = str(arguments.get("text", ""))
        return ToolResult(tool_name=self.name, success=True, output=text.upper())

Register at the composition root (see examples/custom_tool.py).


Run in Docker (terminal ↔ container)

Architecture:

Your terminal  --WebSocket-->  localhost:8765  -->  Docker (ai-agent --server)

The CLI console (ai-agent without --server) needs a TTY inside the container and is awkward for “type on host → reply from Docker.” Use the WebSocket server instead.

# 1) Ensure .env has OPENROUTER_API_KEY
cp -n .env.example .env

# 2) Start the agent server in Docker
docker compose up --build

# 3) From another terminal on the host, chat:
websocat ws://localhost:8765
# type a message, Enter → agent reply comes back as plain text

Without websocat:

# one-shot with Python
python - <<'PY'
import asyncio, websockets
async def main():
    async with websockets.connect("ws://localhost:8765") as ws:
        print(await ws.recv())          # greeting
        await ws.send("What is 2+2? Use tools if needed.")
        print(await ws.recv())
asyncio.run(main())
PY

Operator persona in Docker:

docker compose run --service-ports agent \
  ai-agent --server --host 0.0.0.0 --port 8765 -c config/agents/operator.yaml -v

Requirement: server must listen on 0.0.0.0 (compose already does). localhost inside the container would not accept host connections.


Quality bar

Tests mirror source packages under tests/ (domain/, harness/, features/, …). See AGENTS.md.

uv run pytest
uv run pytest --cov=src/ai_agent
uv run ruff check src tests
uv run ruff format src tests
uv run mypy src
uv run mutmut run      # high-value pure modules
uv run mutmut browse   # triage survivors
Check Role
pytest Behavior with a scripted fake LLM — no API key in CI
mypy disallow_untyped_defs on src/
ruff Lint + format
mutmut Mutation tests on validation / sandbox / registry

Survivors are expected on a first baseline (many are equivalent string/bound tweaks). Kill important ones with focused assertions; don’t gate CI on 100% without triage.


Project status

Area Maturity
Single-agent ReAct harness Production-shaped reference
Research Desk / Personal Operator Shipped (brief + operator YAML)
Multi-agent coordinator / researcher Demo-ready
RAG Optional extra; local Chroma
HITL approvals Light scaffold (request_approval + brief --approve)
Self-Harness Experimental propose/accept only — human gate required
Observability / heavy sandbox Intentionally out of scope (for now)

License

MIT © Eduardo Chiarotti (pythonbyte)

About

A typed, clean-architecture tool-using conversational agent kit.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages