Metadata-Version: 2.4
Name: agentx-dev
Version: 3.8.0
Summary: Build LLM agents in Python, and get agentx: a coding assistant for your terminal. GPT and Claude, permission-gated tools, skills, MCP, team jobs with helpers that run in parallel, RAG, memory and evals.
Author-email: Bruce-Arhin Shadrach <brucearhin098@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/shadrach098/agentx_dev
Project-URL: Bug Tracker, https://github.com/shadrach098/agentx_dev/issues
Project-URL: Source, https://github.com/shadrach098/agentx_dev
Keywords: llm,agents,ai-agents,multi-agent,coding-assistant,terminal,cli,openai,anthropic,claude,gpt,mcp,rag,tool-calling,supervisor,skills
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: openai>=1.0.0
Requires-Dist: pydantic>=2.0
Requires-Dist: PyYAML>=5.3.1
Requires-Dist: rich>=10.0
Requires-Dist: prompt_toolkit>=3.0
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.36; extra == "anthropic"
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.10; extra == "mcp"
Requires-Dist: keyring>=24; extra == "mcp"
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
Requires-Dist: opentelemetry-sdk>=1.20; extra == "otel"
Provides-Extra: rag
Requires-Dist: chromadb>=0.4; extra == "rag"
Requires-Dist: qdrant-client>=1.6; extra == "rag"
Requires-Dist: psycopg[binary]>=3.1; extra == "rag"
Provides-Extra: chroma
Requires-Dist: chromadb>=0.4; extra == "chroma"
Provides-Extra: qdrant
Requires-Dist: qdrant-client>=1.6; extra == "qdrant"
Provides-Extra: pgvector
Requires-Dist: psycopg[binary]>=3.1; extra == "pgvector"
Provides-Extra: excel
Requires-Dist: pandas>=1.5; extra == "excel"
Requires-Dist: openpyxl>=3.1; extra == "excel"
Provides-Extra: voice
Requires-Dist: sounddevice>=0.4; extra == "voice"
Requires-Dist: numpy>=1.24; extra == "voice"
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: httpx>=0.24; extra == "dev"
Requires-Dist: numpy>=1.24; extra == "dev"
Requires-Dist: pandas>=1.5; extra == "dev"
Requires-Dist: openpyxl>=3.1; extra == "dev"
Provides-Extra: all
Requires-Dist: anthropic>=0.36; extra == "all"
Requires-Dist: mcp<2,>=1.10; extra == "all"
Requires-Dist: keyring>=24; extra == "all"
Requires-Dist: opentelemetry-api>=1.20; extra == "all"
Requires-Dist: opentelemetry-sdk>=1.20; extra == "all"
Requires-Dist: chromadb>=0.4; extra == "all"
Requires-Dist: qdrant-client>=1.6; extra == "all"
Requires-Dist: psycopg[binary]>=3.1; extra == "all"
Requires-Dist: pandas>=1.5; extra == "all"
Requires-Dist: openpyxl>=3.1; extra == "all"
Requires-Dist: sounddevice>=0.4; extra == "all"
Requires-Dist: numpy>=1.24; extra == "all"
Dynamic: license-file

# agentx_dev

Two things in one install:

1. **A framework for building LLM agents in Python.** Runners with permission-gated tools, skills, a Supervisor
   that plans multi-step work and runs independent steps **in parallel**, sub-agents, RAG, memory, MCP, evals,
   prompt caching and the Batch API. The same code runs on GPT and Claude.
2. **`agentx`, a coding assistant for your terminal**, built on that framework. It
   reads, edits and runs code in your project and asks before it changes anything. Big jobs go to a **team of
   helpers that work in parallel**: reading helpers side by side, and helpers the supervisor makes for the job
   (`security-reviewer`, `test-writer`). It connects MCP servers, keeps notes per project, and holds a token
   limit per message.

```bash
pip install "agentx-dev[anthropic,mcp]"     # then, in a project folder:
agentx
```

| Start here | |
|---|---|
| **The framework** | [Your first agent in 5 lines](#your-first-agent--5-lines) · [Multi-agent orchestration](#multi-agent-orchestration-supervisor) · [Skills](#skills) · [MCP](#connect-an-mcp-server) |
| **`agentx` in the terminal** | [The terminal guide](https://github.com/shadrach098/agentx_dev/blob/main/docs/guides/cli.md) · [Team jobs](https://github.com/shadrach098/agentx_dev/blob/main/docs/guides/cli.md#team-jobs) · [Full-screen mode](https://github.com/shadrach098/agentx_dev/blob/main/docs/guides/cli.md#full-screen-mode) |
| **Recipes and answers** | [Patterns cookbook](https://github.com/shadrach098/agentx_dev/blob/main/docs/cookbook/patterns.md) · [FAQ](https://github.com/shadrach098/agentx_dev/blob/main/docs/cookbook/faq.md) · [Troubleshooting](https://github.com/shadrach098/agentx_dev/blob/main/docs/cookbook/troubleshooting.md) |

### Helpers that work in parallel

- **In the framework:** `AsyncSupervisor` runs plan steps that do not depend on each other at the same time, and
  starts each step the moment its own dependencies finish (`depends_on`); `max_parallel=N` caps how many run at
  once. A runner can also hand side jobs to fresh sub-agents (`delegate`).
- **In `agentx`:** a team job's reading steps run side by side (up to three), while a step that edits files or runs
  commands runs alone, so nothing reads a file while another step changes it. The supervisor can make a helper for
  a step, with a name, instructions and only the tools it needs, on any model you allow. A live block shows every
  running helper, what it has done, its time and its tokens.

Every code block below is something you can paste and run.

---

## What's new in 3.8 — skills, the app window and remote use

An agent can now read short **skills**: guides for a kind of work ("how to explore a project", "how to change
files safely"). The prompt carries one line per skill; the model opens the full text only when it needs it, by
calling a `use_skill` tool. That keeps the prompt small and — because the list is built the same way every time —
identical from one request to the next, which is what lets the provider's prompt cache (Anthropic's explicit cache,
OpenAI's automatic prefix cache) reuse it. It works the same with GPT and Claude.

```python
from agentx_dev import AgentRunner, AgentType, Permissions

runner = AgentRunner(model=llm, agent=AgentType.ReAct,
                     permissions=Permissions.full_access(["./project"]),
                     skills=True)        # ~/.agentx/skills; the built-in skills are created there on first use
```

| What | Detail |
|---|---|
| **Built in** | Six skills ship with agentx and are created on first use: `exploring-a-project`, `changing-files`, `running-code`, `web-research`, `delegating-work`, `auditing-a-project`. Together they cover every default tool. A skill is listed only when the agent has at least one of its tools. Edit one and agentx never overwrites it; delete one and it stays deleted. |
| **Your own tools** | Tools no skill covers are described in one generated skill, `custom-tools` (built in memory, never written to disk). |
| **Install from a link** | In the `agentx` terminal app: `/skills install https://github.com/obra/superpowers` (or a folder). It downloads the exact commit, shows what it found and asks before saving. `/skills` lists, `/skills remove <name>` deletes. Skills written for Claude work as they are. |
| **Use one from the prompt** | In `agentx`, type `/`: the pop-up lists the commands, then the skills (`/changing-files`, `/exploring-a-project`, ...). Pick one, add what you want (`/changing-files rename the helper to util`) and the agent reads the skill first. `/skills` lists them all. |
| **Smaller requests** | In `agentx`, old tool results are sent as a one-line stub; a file's stub names the file and says to grep it or read only the lines needed, so the agent does not read it all again. The newest results stay whole (20,000 to 40,000 characters, team helpers too), and the cut moves in steps so the start of each request stays the same and the provider's prompt cache bills it at its cheaper rate. Once a conversation is long, older skill texts become a note on how to reopen them. Big files are read in parts of 30,000 characters, command output is cut at 30,000, a skill is sent once per conversation, and summarising starts at about 15,000 tokens. In code: `trim_results=True` or a `TrimPolicy`, `Permissions.read_max_chars`. `/usage` shows what your sessions use and how much came from the cache. |
| **Skills first** | The skill list tells the model to check it before choosing its tools and to prefer a skill you installed when one fits; your own skills are listed ahead of the built-in ones. |
| **A token limit per message** | In `agentx`, a message that has used 200,000 tokens (every model, team helpers included) stops and asks before it goes on; a no wraps up, and the agent answers with what it has found. `/limit 400k` changes it, `/limit 0` turns it off, `"tokenLimit"` sets it in settings (a project may only lower it). In code: `model.on_usage = fn` is called after every reply. |
| **Web search in `agentx`** | `web_search` and `web_fetch` (readable page text, at most 30,000 characters, marked as data from the internet). The `web-research` skill is listed and the team's explorer does web research. `default` and `plan` mode ask before each lookup. |
| **No more long hangs** | A model request waits up to 5 minutes, a timeout is retried once at most, a rate limit waits as long as asked, and every retry is shown. One stuck request used to hold the spinner for about 12 minutes. |
| **MCP servers in `agentx`** | `/mcp add <name> <url>` (or a program on your computer), `/mcp login` signs in through the browser and keeps the sign-in in your system's credential store, renewed by itself. The tools reach the agent and the team helpers as `<name>__<tool>`: the ones the server marks read-only run without asking, the rest ask first (`/mcp tools`, `/mcp allow`, `/mcp ask`). A server that takes a token instead (GitHub) reads it from an environment variable. Only you can add a server. |
| **A new start screen** | `agentx` opens with a box (the mark, model, folder, tips for this folder, what's new), warnings on one line each, and the mode under the input: `⏵⏵ auto mode on (shift+tab to cycle) · model · folder`. |
| **Team jobs, faster and cheaper** | In `agentx`, explorer steps that do not depend on each other run side by side (up to 3); a step that edits or runs commands still runs alone, and every prompt still comes one at a time from the main thread. A helper that reads the same unchanged file whole a third time gets a note instead, each step may use one token limit's worth (at least 100,000) before it is told to report, and the team's report reaches the chat whole (up to 20,000 characters, was 6,000). The limit question takes `a`: go on and do not ask again in this message. |
| **Full-screen mode** | `agentx --fullscreen` (or `/fullscreen on`): the welcome box, the input and the status line stay put while only the conversation scrolls (mouse wheel, PgUp / PgDn). Questions stay above the input, and you can type while the agent works. Off by default. |
| **Looking around is one tree** | In `agentx`, skill reads, recall, reads and searches in a row are one tree (`● Skills  using-superpowers` / `├─ recall …  ⎿ Found 2 notes` / `└─ read_path …  ⎿ Read lines 1-60`), redrawn in place with a blinking yellow dot and the running step blinking white, then printed with a green dot. The agent's narration between them is not printed, and a skill's text never is. |
| **Helpers in the background** | A team's helpers work in the background: their calls are not printed. A block above the spinner, redrawn in place, shows each running step (`├ explorer · security audit  Read 14 files · Searched 6 patterns  1m 12s · 38k tokens`); a finished step leaves one line (`✓ explorer · security audit  Done (20 tool uses · 41k tokens · 1m 30s)`) and the start of its report. `/team log` lists every call. |
| **Helpers the supervisor makes** | Besides explorer, coder and runner, a team's supervisor may make a helper for a step: a name (`security-reviewer`), instructions, and tools from agentx's gated set (never delete, move or another team). It may run on any of your helper models (`security-reviewer@gpt-6-luna`). The plan shows it and its tools first; a reading one runs beside other readers. Up to 6 per job. |
| **An audit skill** | `auditing-a-project`: map the project, run its own checks, read it by area with explorers side by side, verify the findings in the code, then report Fix first / Medium / Low / Done well / Checks / Test gaps. `/auditing-a-project` starts one. |
| **Input colours** | In `agentx`, the `/name` you type is coloured (command cyan, skill magenta, unknown red) and the selected row of the `/` pop-up is highlighted. |
| **Notes and recall** | `agentx` writes a readable note per worthwhile turn to `.agentx/notes/` and indexes it (OpenAI embeddings with `OPENAI_API_KEY`, local search otherwise). The agent searches them with a `recall` tool, a fresh conversation starts with the closest ones, and `/memory [words]` shows or searches them. |
| **Only you install** | The agent has no tool to install or remove a skill, and its file tools cannot write to `.agentx` or `~/.agentx`. Downloads are unpacked with path, link and size limits. |
| **The app window** | `agentx --ui` opens the same agentx in a desktop-style window: conversations on the left (each with a short title the model writes), model and mode menus, approvals as cards with buttons, a Stop button, what it is doing with its time and tokens, team progress, a **Details** panel (context, tokens, team work, MCP servers with their own Sign in buttons, memory), **Settings**, paste a screenshot into the message, and a microphone button. Light or dark follows the system; closing the window quits agentx. See [The app window](docs/guides/cli.md#the-app-window). |
| **Use it from your iPad or phone** | `agentx --remote` (or `/remote-control` in a running session) prints a link for your own devices through Tailscale, and the `ssh` command for another computer; nothing is opened to your Wi-Fi or the internet. The terminal keeps working and shows the same conversation. MCP sign-ins open as a card on the device you use. The link keeps working after the computer sleeps. See [Remote control](docs/guides/cli.md#remote-control-from-an-ipad-a-phone-or-another-computer). |
| **One conversation per device** | Each device that opens the window gets its own conversation and agent, working at the same time in the same folder; the computer's conversation is listed as **Live** and can be opened to share it. A window says when another conversation is working in the folder. |
| **The model's reasoning, shown** | On Claude Opus 5.5, Sonnet 5.5 and Fable 5.1, and on GPT reasoning models through the Responses API, agentx asks for a readable summary of the reasoning the model does anyway (same cost) and shows it as **Thinking**, in the window and before an edit or a command in the terminal. In the framework: `Claude(show_reasoning=True)`, `GPT(show_reasoning=True)`. |
| **Agents can look at pictures** | `read_path` on a PNG, JPEG, GIF or WebP file shows the model the picture itself, on Claude and GPT, so an agent or a team helper that takes a screenshot (Playwright, say) can see how the page really looks. In `agentx`, point it at one by its location; the tool line reads `⎿ Picture, 1440x900`. Big pictures are made smaller or refused; each request carries the newest three. See [Pictures](docs/guides/cli.md#what-it-can-do). |
| **`/voice`** | In `agentx`, type `/voice`, speak, press Enter: the words land in the input line to check before you send them. OpenAI's speech-to-text writes them out (Claude takes no audio), so it needs `OPENAI_API_KEY`; the microphone needs `pip install "agentx-dev[voice]"`. See [Voice](docs/guides/cli.md#voice). |
| **Type while it works** | In `agentx`, what you type while the spinner runs goes to the agent at its next step (to a team job's helpers during one); it waits under the spinner as `◌ ...` and leaves one line once read: `❯ text  ✓ read by <helper>`. `/skip <step>` drops a team step not started yet. |
| **Opt in** | `skills=None` (the default) changes nothing for existing code. The terminal app turns skills on. |

[Skills guide](docs/guides/skills.md) · [in the `agentx` command](docs/guides/cli.md#skills). Nothing breaks when you upgrade.

---

## What's new in 3.7 — the `agentx` terminal assistant

`pip install agentx-dev` now also installs `agentx`, a coding assistant for your
terminal, built on this framework. Run it in a project folder and ask for changes: it reads
files, edits them and runs commands there, and asks you first. It works with GPT and Claude, from
`OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.

```bash
pip install agentx-dev          # Claude also needs: pip install "agentx-dev[anthropic]"
export ANTHROPIC_API_KEY=sk-ant-...   # or OPENAI_API_KEY
cd my-project
agentx                          # or: python -m agentx_dev
```

| Feature | What you get |
|---|---|
| **Approvals and modes** | Reads run; edits, commands, delete and move ask first, with a diff or the full command shown. Modes `default`, `accept-edits`, `plan` and `auto` (set on purpose only, never from Shift+Tab or a project file). `a` ("don't ask again") remembers all edits, or a plain command prefix, for the conversation. |
| **Trust a folder** | The first start in a folder asks "Accessing workspace ... Yes / No". Nothing from the folder (`AGENTX.md`, its settings) is read before you say yes. `.git`, `.agentx` and shell/git config files are never written, in any mode. |
| **`AGENTX.md`** | Instructions for the model (how to run the tests, the layout, conventions), read from your home folder and from the working folder up. `/init` drafts one. |
| **Sessions** | Conversations autosave. `--continue`, `--resume [ID]` and `/resume` pick one up again. |
| **Slash commands** | `/help`, `/clear`, `/model`, `/mode`, `/settings`, `/resume`, `/sessions`, `/init`, `/team`, `/team-models`, `/exit`. Typing `/` shows the list. |
| **Team jobs** | For a big job the agent calls `start_team_job` (or you type `/team <task>`): a supervisor plans up to six steps and three helpers (explorer reads, coder edits, runner runs commands) carry them out through the same approval gate. One message may run up to 5 team jobs, so a big task continues batch after batch. A helper that runs out of its 100 steps hands on its partial work. |
| **Helper models** | `teamModels` in the settings, or `/team-models gpt-6.1-sol="hard coding" gpt-5.5`, lets the supervisor choose per step which model a helper runs on (`coder@claude-opus-5-5`); Claude and GPT can be mixed. Each model can carry a note on what it is for; without one, the chat model writes an `auto:` note. |
| **Project memory** | Each folder keeps its own model (`/model <name> --folder`) and a work log in `.agentx/log.md`. A fresh conversation starts from the newest log entries, and a very long conversation is summarised before it overflows. |

[Guide](docs/guides/cli.md). Not in this version: token-by-token output, a headless mode (`agentx -p`), MCP configuration, custom commands and hooks.

Framework changes in 3.7.0 that help anyone, not only the command:

- **`Session.stream(user_input, **kw)`** is like `Session.invoke` but yields the runner's events and records the result on the final `completion` event.
- **Conversations continue after a final answer given as a function call** (`React_` / `respond`). Before, the next turn was rejected by OpenAI ("An assistant message with 'tool_calls' must be followed by tool messages"). The history a run returns now closes that call, and an older saved history is repaired when resumed.
- **`edit_file` keeps a file's line endings.** It used to rewrite them (LF to CRLF on Windows, CRLF to LF elsewhere); now every byte outside the edit stays, and a model's bare newlines in `find` / `replace` are put in the file's own ending.
- **New base dependency:** `prompt_toolkit>=3.0`.

### Upgrade notes

- Nothing breaks. `agentx` is a new console script installed with the package; your code is not affected.
- The two fixes above change behaviour for `Session`, `chat_history` and `edit_file` users (for the better); the new dependency is installed by `pip install -U agentx-dev`.

Full notes: [docs/guides/upgrading.md](docs/guides/upgrading.md).

## What's new in 3.6 — sub-agents

A Supervisor can now create its own helpers.
The planner defines a helper inline (its own instructions and tools), and a
specialist can hand a side job to a fresh agent and get a short summary back.
You set the most a helper may ever do, once.

```python
from agentx_dev import Supervisor, SpawnConfig

supervisor = Supervisor(
    model=model, agents=my_specialists,
    spawn_config=SpawnConfig(enabled=True, capabilities={"web", "files_read"}),
)
result = supervisor.run("Compare the pricing pages of our three competitors")
print(result.spawned)       # the helpers it created, with outcomes
```

| Feature | What you get |
|---|---|
| **Inline helpers** | A plan step can carry `new_agent` (name, instructions, tools). Reused by name, discarded when the run ends. |
| **`delegate` tool** | A specialist hands part of its work to a fresh sub-agent (clean context) and gets a summary back. |
| **One ceiling** | `SpawnConfig(capabilities=..., tools=..., max_spawns=..., max_depth=...)` bounds every helper. Over-asks are clipped, not fatal. |
| **Sync and async** | `AsyncSupervisor` takes `spawn_config=` too; spawned steps run in parallel. |
| **Persistent mode** | On by default with a safe ceiling (web plus read-only files); helpers share the run deadline; recovery plans can spawn a replacement. |
| **Ask the operator** | `Supervisor(ask_user=True)` lets the planner ask for facts the task leaves out ("which three competitors?") and gives every agent an `ask_user` tool. Works in a notebook's input box and in terminals; `ask_user=my_function` routes questions through your own channel. `ask_when_done=True` makes the Supervisor ask what to do next when it finishes, and `follow_up=True` keeps going on your replies ([details](docs/guides/sub-agents.md#asking-when-the-work-is-done)). |
| **Long-term memory** | `Supervisor(memory=store)` (any vector store: `VectorStore`, Chroma, Qdrant, pgvector) gives the planner and every step the few relevant stored items, saves operator answers and finished results, and answers an exact repeat of a question from memory instead of asking again. |

[Guide](docs/guides/sub-agents.md). To let the Supervisor ask you for missing facts, see [Asking the operator](docs/guides/sub-agents.md#asking-the-operator). To keep what it learns between runs, see [Long-term memory](docs/guides/sub-agents.md#long-term-memory).

### Upgrade notes

- Nothing is on unless you configure it, except persistent supervisors (`persistence=` with no `spawn_config`), which now allow web and read-only file helpers. Pass `spawn_config=SpawnConfig(enabled=False)` to opt out.
- Helpers a Supervisor spawns no longer stay on it after the run.
- A spawn is no longer refused because an existing specialist has the same tools.

Full notes: [docs/guides/upgrading.md](docs/guides/upgrading.md).

## What's new in 3.5 — agents that keep working

Long jobs no longer end at the first wall. Turn on persistent mode and
an agent that gets stuck changes approach, a long history is compacted
instead of overflowing, and a run stops when it's done or out of time
or budget — with an honest report either way.

```python
from agentx_dev import AgentRunner, AgentType, Persistence

runner = AgentRunner(model=model, agent=AgentType.ReAct, tools=tools,
                     persistence=Persistence(max_minutes=60))
result = runner.invoke("Get the failing tests passing")
print(result.outcome)       # done | stuck | out_of_time | out_of_budget | iteration_limit
```

| Feature | What you get |
|---|---|
| **Recover, don't quit** | After a few failing or repeated turns the agent is asked to name the root cause, then to try a different approach, then to probe its assumptions, before it's allowed to give up. Real progress resets the ladder. |
| **Honest outcomes** | Every run reports `outcome`. A specialist that gave up is no longer reported as a success. |
| **Long histories** | Old turns are summarized into notes; the task, attached files and recent turns stay as they were. |
| **Supervisors that replan** | A failed step is replanned around, completed steps are kept, and every specialist shares one deadline. |
| **Limits** | `max_minutes` plus the model's cost cap (`configure_limits(budget_usd=...)`). Transient provider errors (429, 5xx, timeouts) are retried until the deadline. |

[Guide](docs/guides/long-running-agents.md).

### Upgrade notes

- Persistence is opt-in; nothing changes unless you pass `persistence=`.
- A `Supervisor` now treats a specialist that ended with an outcome
  other than `done` (for example `iteration_limit`) as a failed attempt:
  it retries once with feedback, then flags the step instead of
  reporting success.
- While persistence is set, the tool-result cache is off.

Full notes: [docs/guides/upgrading.md](docs/guides/upgrading.md).

## What's new in 3.4 — media input and models that adapt

Send images, PDFs and audio to GPT or Claude with one type. The model
classes now adjust themselves to whatever each model generation accepts,
and sandboxed file paths behave the way you'd write them. Nothing
breaks — see [Upgrading](docs/guides/upgrading.md).

| Feature | What you get |
|---|---|
| **Media input** | `runner.invoke("Describe this", media=["photo.jpg", "report.pdf"])`. Paths, URLs, bytes or `Media` objects. The same code works on `GPT()` and `Claude()`: each model gets its own format, and either provider's native part shape is accepted too. Unsupported pairs — audio to Claude, a PDF URL to GPT — fail fast with a clear `ValueError`. [Media docs](docs/media/overview.md). |
| **Models that adapt** | Parameter support changes between model generations — `reasoning_effort`'s allowed values, `max_tokens` vs `max_completion_tokens`, Claude's `temperature` + `top_p` conflict. When a model rejects a parameter, `GPT` and `Claude` make the smallest change the error asks for (`'none'` → `'low'`), retry, and remember it for that model. Logged at WARNING; `adapt_params=False` opts out. [Details](docs/concepts/models.md#old-and-new-models). |
| **More Claude settings** | `Claude(top_p=, top_k=, thinking=, stop_sequences=)`. `temperature` is only sent when you set it. |
| **Workspace paths** | With a workspace set, `/notes.txt` means the workspace's `notes.txt`, not `C:\notes.txt`. `run_python` now starts in the workspace, so `open("notes.txt")` finds it. |

### Upgrade notes

- `Claude(temperature=)` now defaults to unset. The API default is the
  old `1.0`, so output is the same.
- A parameter a model rejects is adjusted with a WARNING instead of
  raising. Use `adapt_params=False` if you'd rather it fail.
- `run_python` starts in the workspace, not the launch directory.
- Drop any `reasoning_effort="none"` workarounds. Earlier docs
  recommended it, and it's the value newer models reject.

Full notes: [docs/guides/upgrading.md](docs/guides/upgrading.md).

## What's new in 3.3 — dependency DAGs for the Supervisor

Plans stopped being flat lists. A step declares what it consumes, and
the executor derives ordering, concurrency, AND context routing from
those edges — closing the old split where sequential mode had context
threading but no parallelism, and concurrent mode had parallelism but no
threading. All additive; tuple-registered agents and edge-less plans
behave exactly as before.

| Feature | What you get |
|---|---|
| **`depends_on` plan steps** | Every step carries an `id`; a step that consumes an earlier one lists that id. `Supervisor` runs a stable topological order, `AsyncSupervisor` runs a completion-driven scheduler — a step starts the moment its own dependencies finish, not on a wave barrier. Each step is threaded ONLY its direct dependencies' results, so context budgets stop shrinking as plans grow. |
| **`max_parallel=N`** | Concurrency cap on `AsyncSupervisor`, for when you're fighting a tokens-per-minute ceiling. `sequential=True` is now sugar for `max_parallel=1`. |
| **Failure cascade** | A step whose dependency failed after retries is skipped transitively with `SubtaskResult.skipped=True` and an error naming the failed dep. Independent branches keep running; synthesis reports over what survived. No tokens burned downstream of garbage. |
| **`skip_when` conditional steps** | `{"step": <dep id>, "field": <field>, "is": <value>}`, evaluated in Python against the dependency's typed output — no LLM call. Dotted paths supported. Strictly fail-open: a malformed condition runs the step rather than silently dropping work. |
| **Plan sanitization that never fails a run** | Missing/duplicate ids auto-assigned, unknown deps dropped (a planner typo degrades to a root step, not a deadlock), self-deps dropped, cycles broken deterministically. Repairs trigger one re-plan (`max_plan_retries`, default 1). |
| **`Specialist` registry entries** | `Specialist(description=, runner=, depends_on=, when_to_use=, output_schema=)` gives the planner real metadata. Iterable as a 2-tuple, so the old `{"name": ("desc", runner)}` form keeps working alongside it. |

Design notes: [`docs/design/3.3-depends-on-dag.md`](docs/design/3.3-depends-on-dag.md).

## What's new in 3.2 — typed multi-agent pipelines

Structured output stopped being a model-level trick and became something
a whole pipeline can rely on.

| Feature | What you get |
|---|---|
| **Constructor `output_schema`** | `AgentRunner(..., output_schema=MySchema)` runs the full ReAct loop with all its tools, then coerces the final answer into the schema — preferring native function calling, so the provider's constrained decoding fills the fields instead of parsing JSON out of prose. Lands on `completion.output`; `completion.content` still holds the text. Also accepted per-call on `invoke()`, which wins over the constructor. |
| **Typed output survives the Supervisor** | `SubtaskResult.output` carries the validated Pydantic instance, so downstream Python is deterministic instead of re-parsing prose. |
| **Structured findings threading** | A dependent specialist literally sees `STRUCTURED OUTPUT (QuerySchema): {...}` in its context rather than a paraphrase. |
| **Richer `vector_search_tool`** | `max_text_chars` (0 = full passages) and `structured_output` for a JSON `{id, text, vector_score, metadata}` array. |

## What's new in 3.1.4 — reliability hardening

Four fixes for the "the agent said it would do the thing but didn't"
class of failure. All additive; existing code keeps working, and the new
recovery defaults are strictly safer.

| Fix | What changed |
|---|---|
| **Tool-call preamble no longer becomes the answer** | When the model ends a turn with plain text and no tool call (*"I'll look up your scores. Just a second!"*), the runner re-prompts it to act instead of freezing the preamble as the final answer. Bounded by `text_turn_nudges` (default 1; `0` disables). Skipped when no tools are registered — there, prose is the answer. |
| **Proactive "act, don't announce" clause** | Tool-using agents now get a system-prompt clause telling them to call the tool and report what they DID, rather than narrate what they're about to do. Heads off the preamble at the source. |
| **Malformed-JSON tool arguments recover instead of crashing** | A code snippet or Windows path in a tool argument (`re.findall(r'\d+')`, `C:\Users`) is invalid JSON. The framework now repairs the common escape mistakes, and otherwise feeds the error back so the model resends — instead of unwinding the whole run (which surfaced under a Supervisor as `ERROR: Invalid \escape`). `Claude()` was already immune. |
| **Supervisor retries a failed sub-task** | `Supervisor` / `AsyncSupervisor` gained `max_subtask_retries` (default 1). A sub-task that raises is re-dispatched with the prior error appended, instead of quit-on-first-failure. Bounded and informed; set `0` for the old behavior. |

## What's new in 3.1 — power features

Six force-multipliers land in one release. All additive; existing code
keeps working.

| Feature | What you get |
|---|---|
| **Anthropic prompt caching** | `Claude(enable_prompt_cache=True)` marks the system prompt, tool schemas, and (for long chats) stable history segments with `cache_control: ephemeral`. Repeat calls that share those blocks read them from Anthropic's cache -- typically ~90% cheaper on cached input. `TokenUsage` now surfaces `cache_hit_ratio` so you can verify hits. |
| **Parallel tool calls per turn (sync)** | `AgentRunner(bind_tools_natively=True)` binds your tools as native FC tools. When the LLM emits multiple `tool_use` blocks in one turn, they dispatch concurrently on a `ThreadPoolExecutor` (`parallel_tool_workers=8` by default). Async runner already had this; sync now matches. |
| **Semantic long-term memory** | `SemanticMemory(embeddings=...)` embeds every message and retrieves the top-K most relevant older turns for the current query -- keeps recent tail verbatim, injects retrieved older context as a synthetic system message. Beats sliding-window on multi-topic conversations. |
| **`vector_search` RAG tool** | `vector_search_tool(store)` gives your agent a native RAG tool. Backed by `VectorStore` -- in-memory cosine (numpy-accelerated when available), save/load to JSON. `HashEmbeddings` runs offline for dev + tests; `OpenAIEmbeddings` for production. |
| **Agent-to-agent handoffs** | `handoff_tool("researcher")` + `HandoffCoordinator({...}, entry="triage")` let one agent transfer control to a peer mid-run (Swarm-style). Bounded by `max_hops`; conversation history propagates so the target agent starts fully informed. |
| **Evals harness** | `EvalCase` + `EvalRunner` run declarative test cases against any runner, collect per-case latency + tokens + tool-error counts, and produce a pass/fail report. `contains`, `called_tool`, `matches_regex`, `llm_judge`, and more assertion helpers included. CLI: `python -m agentx_dev.Evals run tests/evals/ --config agent.yaml`. |

### 3.1 API reference — everything new at a glance

Grouped by module. All symbols below are exported at the package top level
(`from agentx_dev import X`) unless marked `agentx_dev.X.Y`.

**1. Prompt caching (`agentx_dev.ChatModel`)**

New parameters on `Claude.__init__`:

| Param | Type | Default | What it does |
|---|---|---|---|
| `enable_prompt_cache` | `bool` | `False` | Turn on Anthropic prompt caching. System prompt + tool schemas get `cache_control: ephemeral`. Second call reads them from cache. |
| `cache_history_after` | `int` | `4` | When history has more than N turns, add a third cache breakpoint on the last stable assistant message so long chats also benefit. Max 3 breakpoints used (Anthropic allows 4). |

New `TokenUsage` attributes + property:

| Attribute | What it tracks |
|---|---|
| `total_cache_read_tokens` | Cumulative tokens served from cache (discounted rate). |
| `total_cache_creation_tokens` | Cumulative tokens written into cache (small premium). |
| `last_cache_read_tokens` | Cache read tokens from the most recent call. |
| `last_cache_creation_tokens` | Cache creation tokens from the most recent call. |
| `cache_hit_ratio` *(property)* | `total_cache_read_tokens / total_input_tokens`. Zero when no cache. |

`TokenUsage.record()` gained two keyword args: `cache_read_tokens`, `cache_creation_tokens` (default `0` — providers that don't cache don't need to pass them).

**2. Parallel per-turn tool dispatch (`agentx_dev.Runner.AgentRun`)**

New parameters on `AgentRunner.__init__`:

| Param | Type | Default | What it does |
|---|---|---|---|
| `bind_tools_natively` | `bool` | `False` | Skip the AgentType parser indirection; bind your tools directly as native FC tools so the LLM can call them (and multiple at once per turn). Mutually exclusive with `use_function_calling`. Mirrors the async runner's mode of the same name. |
| `parallel_tool_workers` | `int` | `8` | Max threads in the pool that runs concurrent tool calls when native binding is on. Bounded by `min(workers, num_calls)` per turn. |

Auto-registered when `bind_tools_natively=True`: a synthetic `respond` tool the LLM calls to end the loop with its final answer.

**3. Embeddings + vector store + RAG (`agentx_dev.Embeddings`)**

New module. Exports:

| Symbol | Kind | What it is |
|---|---|---|
| `Embeddings` | ABC | Base class. `embed(texts) -> list[list[float]]`, `embed_one(text)`, `dim: int`. |
| `OpenAIEmbeddings(model="text-embedding-3-small", api_key=None, base_url=None, batch_size=100)` | class | OpenAI-backed. Auto-batches up to 100 texts per request. |
| `HashEmbeddings(dim=256)` | class | Zero-dep fallback. Deterministic char-trigram hash. For tests + dev. |
| `VectorHit(id, text, score, metadata)` | dataclass | One search result. `score` is cosine similarity in `[-1, 1]`. |
| `VectorStore(embeddings)` | class | In-memory store. Methods: `add(texts, ids=None, metadata=None) -> ids`, `search(query, top_k=5, min_score=0.0) -> [VectorHit]`, `delete(ids) -> count`, `clear()`, `save(path)`, `VectorStore.load(path, embeddings=...)`, `len(store)`. numpy-accelerated when available. |
| `SemanticMemory(embeddings, recent_tail=6, top_k=4, min_score=0.15, preserve_system=True)` | class | `BaseMemory` implementation. Every message goes into a `VectorStore`; `get_messages()` returns system + retrieved-context + recent-tail. Call `set_query(text)` before each turn to focus retrieval. Access underlying store via `.store`. |
| `vector_search_tool(store, name="vector_search", default_top_k=5, max_top_k=20, description=None)` | factory | Returns a `StructuredTool` the agent can call. Args schema: `{query, top_k, min_score}`. |

`create_semantic_memory(embeddings=None, recent_tail=6, top_k=4)` — factory in `agentx_dev.Memory`. When `embeddings=None`, defaults to `HashEmbeddings(256)` so tests work with no keys.

**4. Agent-to-agent handoffs (`agentx_dev.Handoffs`)**

New module. Exports:

| Symbol | Kind | What it is |
|---|---|---|
| `HandoffRequest(target, task="", context={})` | dataclass | Sentinel returned by a handoff tool. String form: `"HANDOFF -> <target>: <task>"` — coordinator detects this in `ToolCall.result`. |
| `handoff_tool(target, description=None, tool_name=None)` | factory | Returns a `StructuredTool` named `handoff_to_<target>`. Args schema: `{task, rationale}`. |
| `HandoffCoordinator(agents: dict, entry: str, max_hops=8)` | class | Runs the routing loop. `.run(query, chat_history=None) -> HandoffResult`, `.arun(...)` (async). Chat history propagates across hops. |
| `HandoffResult(completion, hops)` | dataclass | Wraps the final `AgentCompletion` + ordered list of routing hops (`[{from, to, task, hop}, ...]`). Convenience props: `.content`, `.tool_calls`. |
| `MAX_HANDOFF_HOPS = 8` | constant | Default hop cap. |

**5. Evals harness (`agentx_dev.Evals`)**

New module. Exports:

| Symbol | Kind | What it is |
|---|---|---|
| `EvalCase(name, input, assertions=[], tags=[], metadata={})` | dataclass | One test case. |
| `EvalResult(case, completion, passed, duration_sec, ...)` | dataclass | Outcome of one case. Also tracks `input_tokens`, `output_tokens`, `cache_read_tokens`, `tool_error_count`. |
| `EvalReport(results)` | dataclass | Aggregate. Props: `passed`, `failed`, `total`, `pass_rate`, `total_duration_sec`, `total_input_tokens`, `total_output_tokens`, `total_cache_read_tokens`. Methods: `.summary() -> str`, `.to_dict()` for CI. |
| `EvalRunner(runner_factory, verbose=True)` | class | `.run(cases) -> EvalReport`. Builds a FRESH runner per case via `runner_factory` so state doesn't leak. |

Assertion helpers (all return an `(AgentCompletion) -> (passed, message)` callable):

| Helper | Signature | Checks |
|---|---|---|
| `contains(needle, case_sensitive=False)` | | Final answer contains substring. |
| `not_contains(needle, case_sensitive=False)` | | Final answer does NOT contain substring. |
| `matches_regex(pattern, flags=re.IGNORECASE)` | | Final answer matches `re.search`. |
| `called_tool(name)` | | Tool with that name was called at least once. |
| `tool_count(*, min=0, max=None)` | | Total tool calls within bounds. |
| `max_iterations(limit)` | | Steps taken `<= limit`. |
| `llm_judge(judge_model, criterion)` | | Uses another chat model to grade yes/no. |

`AssertionFn` type alias exported for typing custom assertions.

Case loaders (declarative JSON files):

| Function | What it loads |
|---|---|
| `load_case_from_dict(data: dict) -> EvalCase` | Recognizes `expected_substrings`, `forbidden_substrings`, `matches_regex`, `must_call_tools`, `max_iterations`. |
| `load_cases_from_dir(path, pattern="*.eval.json") -> list[EvalCase]` | Walks a directory. Each file is one case dict, a list of dicts, or `{"cases": [...]}`. |

CLI:

```bash
python -m agentx_dev.Evals run tests/evals/ --config agent.yaml [--pattern '*.eval.json'] [--json-out report.json]
```

Exits `0` on all-pass, `1` on any failure. Prints `EvalReport.summary()` to stdout.

**6. Package exports (`agentx_dev`)**

Everything above is re-exported at the top level. New entries in `__all__`:

```
create_semantic_memory,
Embeddings, OpenAIEmbeddings, HashEmbeddings,
VectorStore, VectorHit, SemanticMemory, vector_search_tool,
HandoffRequest, HandoffResult, HandoffCoordinator, handoff_tool,
EvalCase, EvalResult, EvalReport, EvalRunner,
contains, not_contains, matches_regex,
called_tool, tool_count, max_iterations, llm_judge,
load_case_from_dict, load_cases_from_dir,
```

**7. Runnable demo**

`examples/v3_1_features_demo.py` walks every 3.1 feature. Runs offline via
`HashEmbeddings` + mock runners for the RAG / memory / handoffs / evals
sections. Live sections (prompt caching, parallel tools) fall back to GPT
when only `OPENAI_API_KEY` is set; use Claude when `ANTHROPIC_API_KEY` is
set (prompt caching stays Anthropic-only).

---

Quick tour:

```python
from agentx_dev import (
    Claude, AgentRunner, AgentType, Permissions,
    VectorStore, HashEmbeddings, vector_search_tool,
    SemanticMemory, HandoffCoordinator, handoff_tool,
    EvalCase, EvalRunner, contains, called_tool,
)

# Prompt caching -- opt in on the model.
llm = Claude(model="claude-sonnet-4-6", enable_prompt_cache=True)

# RAG -- build a private store and hand the search tool to the agent.
store = VectorStore(embeddings=HashEmbeddings())
store.add(["Postgres uses MVCC for isolation.",
           "SQLite stores the whole DB in one file."])
runner = AgentRunner(
    model=llm, agent=AgentType.ReAct,
    tools=[vector_search_tool(store)],
    permissions=Permissions.read_only(["./"]),
)
result = runner.invoke("How does Postgres handle concurrent writes?")

# Parallel tool calls -- native binding + multi-worker dispatch.
fast = AgentRunner(
    model=llm, agent=AgentType.ReAct,
    tools=[weather_tool, stock_tool, news_tool],
    bind_tools_natively=True, parallel_tool_workers=4,
)

# Handoffs -- specialists that can hand back and forth.
coord = HandoffCoordinator({
    "triage": triage_runner,
    "researcher": researcher_runner,
    "writer": writer_runner,
}, entry="triage")
answer = coord.run("Write a 200-word summary of MVCC.")

# Evals -- catch regressions before shipping.
cases = [
    EvalCase("capital", "What's the capital of France?",
             assertions=[contains("Paris")]),
    EvalCase("math", "137 * 91?",
             assertions=[called_tool("calculator"), contains("12467")]),
]
report = EvalRunner(lambda: build_runner()).run(cases)
print(report.summary())
```

---

## What's new in 3.0.6 — security hardening release

All seven findings from the third-party framework audit are now fixed.
Every change is backwards-compatible: existing code keeps working; the
new defaults are strictly safer.

| Fix | What changed |
|---|---|
| **SSRF guard on `web_fetch`** | Rejects non-public destinations (loopback, RFC1918, 169.254.169.254 cloud-metadata, link-local, multicast) *and* re-validates every redirect hop. `WebTools.py` |
| **`SpawnConfig.auto_spawn_allowed_caps`** | New allow-list gates which capabilities `auto_spawn=True` will silently grant. Prompt-injected tasks can no longer talk the planner into a `["code"]` or `["delete"]` spawn. Recommended: `{"web"}`. |
| **Scrubbed subprocess env** | `run_python` / `run_shell` no longer inherit the full parent env. `print(os.environ)` inside `execute_python` cannot exfiltrate `OPENAI_API_KEY`, `AWS_*`, etc. Opt vars back in with `Permissions(subprocess_env_passthrough=[...])`; use `["*"]` for the pre-3.0.6 behaviour. |
| **HMAC-signed persistent state** | Each `run_python` build generates a per-run HMAC key, kept in parent memory. A tampered or attacker-dropped `.run_python_state.pkl` fails verification and the header starts fresh instead of unpickling attacker `__reduce__` payloads. |
| **`session_id` sanitizer** | `mint_session_dir` / `Permissions.new_session` now reject path separators, `..`, control chars, leading dots. `session_id="../../../tmp/pwn"` no longer escapes `base`. |
| **`permissions.json` mode 0o600** | Config file is now written via `os.open(..., 0o600)`, blocking multi-user hijack where another local user pre-creates the config with `execute_python=True`. |
| **ReDoS guard on `grep`** | 500-char pattern cap + 10s total-scan wall-clock budget. A catastrophic-backtracking regex from an untrusted planner can no longer freeze the agent. |
| **`invoke` / `ainvoke` accept a bare string** | `BaseChatModel.invoke("hi")` now wraps to `[{"role":"user","content":"hi"}]` — same behaviour `with_structured_output(...).invoke("hi")` has always had. Silently forwarding a string to the provider used to produce an OpenAI `Invalid type for 'messages'` 400 that read like a caller-payload problem. Lists and `{"messages": [...]}` dicts pass through unchanged. Fully additive. |
| **`AgentRunner.invoke` / `AsyncAgentRunner.ainvoke` accept a message list** | Parallel fix for the runner. Callers thinking in chat-model shapes can now pass `runner.invoke([{"role":"user","content":"prior"}, {"role":"assistant","content":"ok"}, {"role":"user","content":"NOW"}])` — the last user turn becomes the query and earlier non-system turns become the chat history (merged with any explicit `chat_history=`). Bare strings still work (classic path). `system` messages in the list are dropped because the runner assembles its own from the AgentType template + `system_addendum` + sandbox hint. `stream` / `astream` accept the same shapes. |

## What's new in 3.0

- **Multi-agent orchestration** — `Supervisor` decomposes a task into
  a plan of sub-tasks, dispatches each to the right specialist, and
  synthesizes the results into one answer. Sync and async variants.
- **Dynamic specialist spawning** — `SpawnConfig` lets a Supervisor
  spin up NEW specialists mid-plan when the initial catalog doesn't
  cover a capability, with a capability-overlap guard that refuses
  duplicate spawns and auto-reroutes follow-up dispatches.
- **New default tools** — `grep`, `find_files`, `run_shell` join the
  existing file / code bundle. All noise-dir-skipping, sandbox-checked,
  cross-platform.
- **Web tools** — `web_search`, `web_fetch` (DuckDuckGo + Wikipedia
  fallback, no API keys). Optional disk-cache mode writes full page
  bodies to a file so `run_python` can `open()` them instead of
  re-transmitting HTML.
- **Persistent Python state** — enable
  `Permissions(python_persistent_state=True)` and every `run_python`
  call gets a `state` dict auto-loaded and saved between calls.
  Jupyter-like workflow inside subprocess isolation.
- **Session directories** — `mint_session_dir()` /
  `Permissions.new_session()` give every run its own subdirectory,
  so 25 parallel runs produce 25 non-clobbering outputs.
- **Anti-spiral guards** — the framework catches duplicate tool calls
  (warn at 2, refuse at 5) and force-terminates ReAct loops that
  emit 3 identical `(action, args)` in a row. Models that would have
  burned max_iterations now bail cleanly with a real result.
- **Project guide auto-discovery** — drop an `AGENTX.md` at your repo
  root and the framework auto-points every specialist at it via the
  system prompt. See §"Project guide" below.
- **Prompt template rewrite** — every built-in `AgentType` template
  (ReAct / Instruction-Tuned / Chain-of-Thought / Zero-Shot / Few-Shot)
  now follows a unified shape: HARD RULES → PROCESS checklist →
  ANTI-PATTERNS → OUTPUT CONTRACT → worked examples.

Upgrading from 2.x? Almost everything is additive. The only signature
break is internal to `Supervisor._handle_spawn` (now returns a tuple
for the auto-reroute feature); external `Supervisor.run()` /
`AsyncSupervisor.run()` are unchanged.

---

## Install

Base install (OpenAI provider + all core features):

```bash
pip install agentx-dev
```

Add optional providers / integrations as extras:

```bash
pip install agentx-dev[anthropic]      # enables agentx_dev.Claude
pip install agentx-dev[mcp]            # enables MCPClient (stdio / sse / http)
pip install agentx-dev[otel]           # OpenTelemetry observability adapter
pip install agentx-dev[rag]            # ChromaVectorStore, QdrantVectorStore, PgVectorStore
pip install agentx-dev[excel]          # spreadsheets (.xlsx, .ods) in Media
pip install agentx-dev[all]            # everything above
```

The base install is small: the vector-store clients (`chromadb`, `qdrant-client`, `psycopg`) come with
`[rag]` (or one at a time with `[chroma]`, `[qdrant]`, `[pgvector]`), and pandas with `[excel]`. A class that needs a
missing package says which extra to install. Install into the same Python that runs your code:
`python -m pip install "agentx-dev[rag]"`.

Requires Python **3.10+** (the codebase uses PEP 604 union types).

Set your provider API key as an environment variable:

```bash
export OPENAI_API_KEY=sk-...             # macOS / Linux
export ANTHROPIC_API_KEY=sk-ant-...      # if you installed [anthropic]

$env:OPENAI_API_KEY = "sk-..."           # Windows PowerShell
$env:ANTHROPIC_API_KEY = "sk-ant-..."
```

### The `agentx` command

`pip install agentx-dev` also gives you `agentx`, a terminal coding assistant: run it in a project folder and
ask for changes. It reads files, edits them and runs commands there, and asks you before it edits a file, runs
a command, or deletes or moves anything. Conversations are saved and can be resumed. It has no headless mode
and does not stream the answer token by token yet. For big jobs it can hand the work to a team of helpers (with
your approval, unless you run in auto mode or have allowed it with your own rule), and those helpers can run on
other models you allow (`teamModels` in the settings, or `/team-models`). It also keeps a work log and a
model for each folder (in `.agentx/`) and summarises long conversations; see the guide. It also reads
[skills](docs/guides/skills.md), short guides it opens when its work matches (`/skills` lists them,
`/skills install <github link>` adds more).

```bash
pip install agentx-dev          # Claude also needs: pip install "agentx-dev[anthropic]"
export ANTHROPIC_API_KEY=sk-ant-...   # or OPENAI_API_KEY
agentx                          # in your project folder
```

See the [agentx command guide](docs/guides/cli.md) for the approval modes, settings and rules, the protections
that are always on, and what it does not cover.

---

## Your first agent — 5 lines

```python
from agentx_dev import AgentRunner, AgentType, Claude

runner = AgentRunner(model=Claude(), agent=AgentType.ReAct, tools=[])
result = runner.invoke("What is 12 * 47? Reason step by step.")
print(result.content)
```

That's an agent. No tools yet — the model just thinks and answers. Let's
give it something to do.

---

## Add a custom tool

A tool is a Python function the agent can call. Two flavors:

### Simple tool — one string argument

```python
from agentx_dev import AgentRunner, AgentType, Claude, StandardTool

def get_weather(city: str) -> str:
    """Look up the current weather for a city."""
    # In a real app you'd call a weather API here.
    return f"It's 72°F and sunny in {city}."

weather_tool = StandardTool(
    func=get_weather,
    name="get_weather",
    description="Get the current weather for a city.",
)

runner = AgentRunner(
    model=Claude(),
    agent=AgentType.ReAct,
    tools=[weather_tool],
)
result = runner.invoke("What's the weather in San Francisco?")
print(result.content)
# → "It's 72°F and sunny in San Francisco."
```

### Structured tool — typed arguments

When your tool needs multiple arguments, define them with a Pydantic
schema so the LLM gets the field names and types right:

```python
from pydantic import BaseModel
from agentx_dev import AgentRunner, AgentType, Claude, StructuredTool

class CalcArgs(BaseModel):
    a: float
    b: float
    op: str   # "add" / "sub" / "mul" / "div"

def calculator(a: float, b: float, op: str) -> str:
    """Do basic arithmetic."""
    ops = {"add": a + b, "sub": a - b, "mul": a * b, "div": a / b}
    return f"{a} {op} {b} = {ops[op]}"

calc_tool = StructuredTool(
    func=calculator,
    args_schema=CalcArgs,
    name="calculator",
    description="Perform basic arithmetic (add/sub/mul/div).",
)

runner = AgentRunner(model=Claude(), agent=AgentType.ReAct, tools=[calc_tool])
result = runner.invoke("What is 47 multiplied by 12?")
print(result.content)
# → "47 mul 12 = 564.0"
```

The LLM sees the schema, fills in the right fields, and the framework
validates the arguments before your function runs.

---

## Default tools (file access + Python execution)

Most agents need to read files, write files, and run code. Instead of
hand-rolling those (with sandbox checks, timeouts, etc.), the framework
ships them under a permission system.

### Auto-create a config file on first run

```python
from agentx_dev import AgentRunner, AgentType, Claude, Permissions

runner = AgentRunner(
    model=Claude(),
    agent=AgentType.ReAct,
    permissions=Permissions.from_file(),
)
```

First time you run this, it creates `.agentx/permissions.json` in your
project with **deny-all** defaults:

```json
{
  "_comment": "Edit this file to control which capabilities the agent has...",
  "_docs": "https://github.com/shadrach098/agentx_dev",
  "read_files": false,
  "list_directories": false,
  "write_files": false,
  "edit_files": false,
  "delete_files": false,
  "move_files": false,
  "execute_python": false,
  "allowed_paths": null,
  "python_timeout_sec": 10.0,
  "python_max_output_bytes": 100000,
  "max_file_size_bytes": 10485760
}
```

Open the file, grant the capabilities you want, save:

```json
{
  "read_files": true,
  "list_directories": true,
  "write_files": true,
  "execute_python": true,
  "allowed_paths": ["./workspace"]
}
```

Re-run — the agent now has access to those tools. **No code change.**

### Or configure permissions inline

```python
from agentx_dev import AgentRunner, AgentType, Claude, Permissions

runner = AgentRunner(
    model=Claude(),
    agent=AgentType.ReAct,
    permissions=Permissions(
        read_files=True,
        list_directories=True,
        write_files=True,
        allowed_paths=["./workspace"],   # sandbox
        python_timeout_sec=10,
    ),
)
result = runner.invoke(
    "Create a file at ./workspace/hello.txt with the text 'hi from the agent'"
)
print(result.content)
```

### Permission presets

For common patterns:

```python
Permissions.deny_all()                              # the default
Permissions.read_only(allowed_paths=["./docs"])     # read + list only
Permissions.full_access(["./project"])                # full access in sandbox
```

### What tools you get

Once granted, the agent sees these tools:

| Tool | What it does | Capability |
|---|---|---|
| `read_path` | Read a file (line-numbered slice via `offset`/`limit`) | `read_files` |
| `list_directory` | List a folder | `list_directories` |
| `find_files` | Search by glob (auto-skips `.git`, `node_modules`, `__pycache__`, `.venv`, etc.) | `list_directories` |
| `grep` | Content search — returns `file:line: match`, cross-platform, regex or literal | `read_files` |
| `write_file` | Create / update a file. `if_exists=` controls behavior on collision (`refuse` / `rename` / `overwrite` / `append`) | `write_files` |
| `edit_file` | Replace a substring in a file (exactly one match required) | `edit_files` |
| `delete_path` | Delete a file or directory (`recursive=True` for non-empty dirs) | `delete_files` |
| `move_file` | Move / rename | `move_files` |
| `run_python` | Execute a Python snippet in a fresh subprocess | `execute_python` |
| `run_shell` | Execute a one-shot shell command (bash on Unix, git-bash / PowerShell on Windows) | `execute_shell` |

### Security guarantees

- `allowed_paths` is enforced. `../../etc/passwd` is blocked — the path
  resolver flattens `../` before the subtree check.
- `run_python` runs in a fresh subprocess with a wall-clock timeout and
  an output-byte cap. A runaway script can't lock your agent.
- Denied capabilities → those tools aren't even registered. The agent
  never sees them as options.

### Combine defaults with your own tools

```python
runner = AgentRunner(
    model=Claude(),
    agent=AgentType.ReAct,
    tools=[weather_tool, calc_tool],              # your custom tools
    permissions=Permissions.full_access(["./work"]),  # adds 7 default tools
)
result = runner.invoke(
    "Look up the weather in Paris, then write it to ./work/paris.txt"
)
```

### Persistent state in run_python

Turn on `python_persistent_state=True` and every `run_python` call gets
a `state` dict auto-loaded from `<workspace>/.run_python_state.pkl`.
Model can pass data between calls without re-defining setup:

```python
perms = Permissions(
    read_files=True, write_files=True, execute_python=True,
    allowed_paths=["./workspace"], workspace="./workspace",
    python_persistent_state=True,   # ← enables state dict
)

# Turn 1: setup
# state['files'] = [f for f in Path('./src').rglob('*.py')]

# Turn 2: analysis, no redefinition needed
# print(sum(len(open(f).read().splitlines()) for f in state['files']))
```

Only picklable objects survive round trips. Non-picklable values
(lambdas, threads, live handles) are silently dropped with a stderr
note listing the keys. Clear state with `state.clear()` or delete the
hidden `.run_python_state.pkl`.

### Per-run session directories

Running the same demo multiple times? Batch jobs? Parallel evaluations?
`mint_session_dir` and `Permissions.new_session` isolate each run so
outputs don't clobber each other:

```python
from agentx_dev import mint_session_dir, Permissions

# Mint a fresh subdirectory: ./workspace/run_<UTC timestamp>_<8-char id>/
session_dir = mint_session_dir("./workspace", prefix="run_")

perms = Permissions(
    write_files=True, execute_python=True,
    allowed_paths=[session_dir],
    workspace=session_dir,
)
# Or in one call:
perms = Permissions.new_session(base="./workspace")
```

Every run mints a sibling subdirectory. 25 runs → 25 distinct output
folders. Sortable by timestamp, uniquely suffixed by a random id.

### Web search + fetch (optional)

Not part of DefaultTools — plug them in explicitly when you want the
agent to read the internet:

```python
from agentx_dev import (
    AgentRunner, AgentType, Claude, Permissions,
    web_search_tool, web_fetch_tool,
)

runner = AgentRunner(
    model=Claude(),
    agent=AgentType.ReAct,
    permissions=Permissions.full_access(["./workspace"]),
    tools=[
        web_search_tool(),
        # Optional: cache_dir=... saves the FULL response body to disk,
        # and the tool response includes a "open(cached_path).read()"
        # snippet the model can drop straight into run_python — avoids
        # re-transmitting HTML through the LLM.
        web_fetch_tool(cache_dir="./workspace"),
    ],
)
```

DuckDuckGo with Wikipedia fallback. No API keys, no extra dependencies.

---

## Multi-agent orchestration (Supervisor)

For tasks that decompose into distinct sub-problems, register several
specialists and let a `Supervisor` decide which one handles each step:

```python
from agentx_dev import AgentRunner, AgentType, Claude, Permissions
from agentx_dev.Supervisor import Supervisor

llm = Claude(model="claude-sonnet-4-6")

file_agent = AgentRunner(
    model=llm, agent=AgentType.ReAct,
    permissions=Permissions(
        read_files=True, write_files=True, edit_files=True,
        list_directories=True, delete_files=True,
        allowed_paths=["./workspace"], workspace="./workspace",
    ),
)

python_agent = AgentRunner(
    model=llm, agent=AgentType.ReAct,
    permissions=Permissions(
        read_files=True, write_files=True, execute_python=True,
        allowed_paths=["./workspace"], workspace="./workspace",
    ),
)

supervisor = Supervisor(
    model=llm,
    agents={
        "file_agent":   ("File management inside ./workspace", file_agent),
        "python_agent": ("Python code execution", python_agent),
    },
    max_subtasks=5,
    max_subtask_retries=1,   # retry a failed sub-task once, error fed back
    verbose=True,   # framework prints plan / dispatch / result / final
)

result = supervisor.run(
    "Write a Python script that scrapes example.com for links, run it, "
    "and save the output to ./workspace/links.txt."
)
print(result.content)
```

Flow: ONE LLM call decomposes the task into a plan → each sub-task
dispatches to the named specialist → ONE LLM call synthesizes the
sub-task results into a final answer.

Findings from earlier steps get automatically threaded into later
steps' queries, so a plan like `researcher → writer` sees the
researcher's actual output in the writer's prompt (no manual passing
required).

### Dynamic specialist spawning

`SpawnConfig` lets the Supervisor create NEW specialists mid-plan when
the initial catalog doesn't cover a capability. Great for tasks whose
shape isn't known upfront:

```python
from agentx_dev.Supervisor import Supervisor, SpawnConfig

supervisor = Supervisor(
    model=llm,
    agents={
        # Start with a lean catalog — Supervisor will spawn what it needs.
        "file_agent": ("File management inside ./workspace", file_agent),
    },
    spawn_config=SpawnConfig(
        enabled=True,
        auto_spawn=True,      # False = prompt on stdin (or use approver=)
        allowed_paths=["./workspace"],
        max_spawns=3,         # runaway guard
    ),
)
```

Recognized capability keywords the planner can request:
- `"web"` — installs `web_search` + `web_fetch`.
- `"files_read"` — read / list / find / grep inside the sandbox (read-only).
- `"files"` — read / write / edit / list inside the sandbox.
- `"code"` — `run_python`.
- `"delete"` — grants `delete_files` only (the `delete_path` tool inside the
  sandbox); it adds no read or write access.

Since 3.6 a spawn is not refused because an existing specialist has the
same tools (a helper can carry its own instructions), the planner can
write those instructions inline, and a `capabilities=` / `tools=` ceiling
replaces per-spawn approval. See [Sub-agents](docs/guides/sub-agents.md).

### AsyncSupervisor

Same shape, but sub-tasks run concurrently via `asyncio.gather`:

```python
from agentx_dev.Supervisor import AsyncSupervisor

supervisor = AsyncSupervisor(
    model=llm,
    agents={...},
    sequential=False,   # concurrent (default). sequential=True to thread findings.
)
result = await supervisor.run("...")
```

Pass `sequential=True` if your sub-tasks depend on earlier steps'
output (concurrent execution means no findings-threading between
steps; sequential preserves ordering AND passes findings forward).

---

## Project guide (AGENTX.md)

Drop a file named `AGENTX.md` at your repo root or workspace. Every
AgentRunner auto-discovers it and points the model at it in the
system prompt:

> PROJECT GUIDE: an AGENTX.md file exists at &lt;path&gt;. READ IT FIRST
> via read_path — it documents the conventions this codebase expects
> (tool patterns, sandbox layout, common failure modes, when to spawn
> specialists, how to author custom tools).

Think of it as `CLAUDE.md`-style: instructions written for the model
that lands in your repo, not for humans reading the README. The
framework doesn't inline the file's contents (that would burn tokens
on every call) — it just points at it. Model reads on demand.

The `AGENTX.md` shipped with this repo is a template you can adapt
(or delete + write your own). See the repo root.

---

## Skills

`AGENTX.md` tells the model about your project; **skills** tell it how to do a *kind of work* well, in any
project. A skill is a folder with a `SKILL.md` (a `name`, a one-sentence `description` saying when to use it, then
the guide). The model sees one line per skill and reads the rest only when it calls `use_skill`:

```python
from agentx_dev import AgentRunner, AgentType
from agentx_dev.Skills import Skills

runner = AgentRunner(model=llm, agent=AgentType.ReAct, tools=my_tools,
                     skills=Skills("./my-skills"))      # or skills=True for ~/.agentx/skills
```

```markdown
---
name: release-notes
description: How we write release notes - use when asked to summarise a release.
---
1. Group changes under Added / Changed / Fixed.
2. ...
```

- The built-in skills (files, searching, commands, web, delegation) are created in the folder the first time a
  runner uses it. Your edits and deletions are respected.
- The list and the tool list are fixed when the runner is built, so the prompt stays identical between calls.
- Keep the skills folder outside the paths your agent's write tools can reach: a skill is instructions the agent
  follows.
- In the terminal app, `/skills install <github link or folder>` installs skills after showing what it found.
  Only you can install; the agent cannot.

Full details, limits and the safety rules: [Skills guide](docs/guides/skills.md).

---

## Connect an MCP server

MCP (Model Context Protocol) lets you plug in tool servers without
writing tool code yourself. Want filesystem access via the official
filesystem server? Want database queries via a Postgres MCP server?
Just connect.

### Stdio (local subprocess — most common)

```python
import asyncio
from agentx_dev import AsyncAgentRunner, AgentType, Claude, MCPClient

async def main():
    # Spawn the official filesystem MCP server
    async with MCPClient.connect_stdio(
        "npx", "-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"
    ) as mcp:
        tools = await mcp.list_tools()   # auto-discovers what the server offers

        runner = AsyncAgentRunner(
            model=Claude(),
            agent=AgentType.ReAct,
            tools=tools,
        )
        result = await runner.ainvoke("List the files in /path/to/dir")
        print(result.content)

asyncio.run(main())
```

### HTTP / SSE (remote MCP server)

```python
mcp = await MCPClient.connect_http(
    "https://my-mcp-server.example.com/mcp",
    headers={"Authorization": "Bearer your-token"},
)

mcp = await MCPClient.connect_sse(
    "https://my-mcp-server.example.com/sse",
    headers={"Authorization": "Bearer your-token"},
)
```

### MCP resources + prompts

Beyond tools, MCP servers can expose readable resources and pre-baked
prompt templates:

```python
async with MCPClient.connect_stdio(...) as mcp:
    # List + read resources
    resources = await mcp.list_resources()
    content = await mcp.read_resource("file:///readme.md")

    # Wrap a resource as a tool the agent fetches on demand
    readme_tool = mcp.resource_as_tool("file:///readme.md")

    # Pre-baked prompts
    prompts = await mcp.list_prompts()
    messages = await mcp.get_prompt("summarize", {"text": "..."})
```

---

## Persist conversations across restarts

Without this, every restart loses state. With `Session`, your agent
picks up exactly where it left off:

```python
from agentx_dev import AgentRunner, AgentType, Claude, Permissions, Session

runner = AgentRunner(
    model=Claude(),
    agent=AgentType.ReAct,
    permissions=Permissions.full_access(["./work"]),
)

# First conversation
session = Session.start(runner, metadata={"user_id": "alice"})
session.invoke("Plan a 3-day trip to Paris.")
session.save("./sessions/paris-trip.json")

# ... your program exits ...

# Resume tomorrow — reattach the runner, continue
session = Session.load("./sessions/paris-trip.json", runner=runner)
session.invoke("Add a visit to Versailles on day 2.")
session.save("./sessions/paris-trip.json")
```

What's saved: full history (with provider-native tool_use IDs),
cumulative tool calls, token usage, metadata. The atomic write
guarantees a crashed save can't corrupt an existing valid file.

---

## Send images, PDFs and audio

```python
from agentx_dev import AgentRunner, AgentType, Claude, Media

runner = AgentRunner(model=Claude(), agent=AgentType.ReAct, tools=[])

result = runner.invoke(
    "What does this chart show, and does the report agree?",
    media=["q3_chart.png", "q3_report.pdf"],
)
```

Swap in `GPT(model="gpt-4o")` and nothing else changes. You can also put
media straight into a message for a bare model call:

```python
llm.invoke([{"role": "user", "content": ["What's in this photo?", Media.image("cat.jpg")]}])
```

| | GPT | Claude |
|---|---|---|
| image (file or URL) | yes | yes |
| PDF | file | file or URL |
| CSV, TXT, JSON, Markdown ... | yes, as text | yes, as text |
| Excel (`.xlsx`) | yes, CSV per sheet (pandas, installed with agentx-dev) | same |
| audio (wav / mp3) | audio models | no — `ValueError` |

Paths are relative to where your program runs, not the agent's
workspace. The same `media=` works on `llm.invoke(...)` and
`llm.with_structured_output(Schema).invoke(...)`. See
[Media](docs/media/overview.md).

## Get a typed result back

Force the agent's final answer to fit a Pydantic schema:

```python
from pydantic import BaseModel
from agentx_dev import AgentRunner, AgentType, Claude

class Receipt(BaseModel):
    merchant: str
    total: float
    currency: str = "USD"

runner = AgentRunner(model=Claude(), agent=AgentType.ReAct, tools=[])
result = runner.invoke(
    "Parse this receipt into structured fields: 'Joe's Diner, $12.50 USD'",
    output_schema=Receipt,
)

print(result.output)            # Receipt(merchant="Joe's Diner", total=12.5, currency='USD')
print(result.output.merchant)   # "Joe's Diner"
print(result.output.total)      # 12.5
```

Or skip the agent loop entirely for one-shot extraction:

```python
extractor = Claude().with_structured_output(Receipt)
receipt = extractor.invoke("Joe's Diner, $12.50")
print(receipt.merchant)
```

---

## Stream output token-by-token

```python
# use_function_calling=False is REQUIRED for text_delta: FC mode (the
# auto-detected default since 3.1.7) emits structured tool calls, not
# streaming text, so stream_tokens=True would yield zero text_delta.
runner = AgentRunner(
    model=Claude(), agent=AgentType.ReAct, tools=[weather_tool],
    use_function_calling=False,
)

for event in runner.stream("What's the weather in NYC?", stream_tokens=True):
    if event["type"] == "text_delta":
        print(event["content"], end="", flush=True)
    elif event["type"] == "tool_call":
        print(f"\n→ calling {event['name']}")
    elif event["type"] == "tool_result":
        print(f"\n✓ {event['result']}")
    elif event["type"] == "final":
        print(f"\n\n💬 {event['content']}")
```

---

## Production controls

Set them on the model once and forget. Everything's opt-in.

```python
from agentx_dev import Claude

llm = Claude(model="claude-sonnet-4-6").configure_limits(
    # Don't spend more than $5
    budget_usd=5.0,
    input_price_per_1k=0.003,
    output_price_per_1k=0.015,

    # Don't exceed 5 LLM calls per second
    rate_limit_per_sec=5,

    # Don't retry more than 10 times total over this model's lifetime
    retry_budget=10,
)
```

Crossing the budget raises `CostBudgetExceeded` immediately. The agent
loop stops, no more API calls.

```python
# Per-tool timeouts + circuit breakers
from agentx_dev import AgentRunner, AgentType, ToolRegistry, CircuitBreakerConfig

runner = AgentRunner(model=llm, agent=AgentType.ReAct, tools=[weather_tool])
runner.registry = ToolRegistry(
    runner.registry.tools,
    default_timeout_sec=30,    # no tool runs longer than 30s
    circuit_breaker_config=CircuitBreakerConfig(
        failure_threshold=5,    # 5 fails in a row → trip
        recovery_timeout_sec=60, # stay open for 1 minute
    ),
)
```

Track usage + estimate cost any time:

```python
runner.invoke("...")
runner.invoke("...")
print(llm.usage)
# TokenUsage(calls=2, input=3210, output=587, total=3797)

print(f"spent ${llm.usage.estimate_cost(0.003, 0.015):.4f}")
# spent $0.0184
```

---

## Configure an agent from YAML

When you want to swap models / tools / permissions without code changes:

```yaml
# agent.yaml
model:
  provider: claude          # gpt | claude | custom
  name: claude-sonnet-4-6
  temperature: 0.7

agent_type: ReAct           # ReAct | Chain_of_Thought | Zero_Shot | Few_Shot | Instruction_Tuned

permissions:
  read_files: true
  list_directories: true
  write_files: true
  allowed_paths:
    - ./workspace

tools:
  - module: my_app.tools
    attr: weather_tool

max_iterations: 6
use_function_calling: true
verbose: false
```

Load it:

```python
from agentx_dev import load_agent_from_yaml

runner = load_agent_from_yaml("agent.yaml")
result = runner.invoke("What's in the workspace?")
```

A typo in any field (`read_filez: true`) fails loudly so you know
exactly which line to fix.

---

## End-to-end example: file-editing agent

Putting it together — an agent that reads a file, asks the LLM to
improve it, writes the improved version, and reports what changed:

```python
from agentx_dev import AgentRunner, AgentType, Claude, Permissions

runner = AgentRunner(
    model=Claude(model="claude-sonnet-4-6"),
    agent=AgentType.ReAct,
    permissions=Permissions(
        read_files=True,
        write_files=True,
        edit_files=True,
        allowed_paths=["./workspace"],
    ),
    max_iterations=8,
)

result = runner.invoke(
    "Read ./workspace/draft.md. Improve the writing for clarity and brevity. "
    "Save the improved version to ./workspace/draft_v2.md. "
    "Tell me three specific edits you made."
)

print(result.content)
for tc in result.tool_calls:
    print(f"  {tc.name}({tc.args})")
```

Cost-control variant — same agent, capped at $0.10 with a circuit
breaker, persistable session:

```python
from agentx_dev import (
    AgentRunner, AgentType, Claude, Permissions,
    Session, ToolRegistry, CircuitBreakerConfig,
)

llm = Claude(model="claude-sonnet-4-6").configure_limits(
    budget_usd=0.10,
    input_price_per_1k=0.003,
    output_price_per_1k=0.015,
)

runner = AgentRunner(
    model=llm,
    agent=AgentType.ReAct,
    permissions=Permissions.full_access(["./workspace"]),
    max_iterations=8,
)
runner.registry = ToolRegistry(
    runner.registry.tools,
    default_timeout_sec=30,
    circuit_breaker_config=CircuitBreakerConfig(failure_threshold=3),
)

session = Session.start(runner)
session.invoke("Read ./workspace/draft.md and rewrite it for clarity.")
session.save("./sessions/editing.json")

print(f"Spent: ${llm.usage.estimate_cost(0.003, 0.015):.4f}")
```

---

## Where to go next

- More runnable demos: see [`examples/`](examples/)
  - `file_agent_demo.py` — agent that reads / writes / runs code
  - `function_calling_demo.py` — structured output extraction
  - `mcp_demo.py` — end-to-end MCP server integration
  - `orchestration_demo.py` — the three orchestration patterns side-by-side
    (single agent / orchestrator-as-ReAct / Supervisor with dynamic spawn)
  - `supervisor_codebase_analysis_demo.py` — Supervisor-only, default
    tools only. Analyzes its own source tree to produce a Markdown report,
    exercising the dynamic-spawn machinery and the persistent-state feature.
- Project-scoped instruction sheet: [`AGENTX.md`](AGENTX.md) —
  read for the conventions this codebase expects.
- Skills (guides the agent reads on demand, installable from GitHub):
  [`docs/guides/skills.md`](docs/guides/skills.md)
- The full framework docs: see [`docs/README.md`](docs/README.md)
  (every class, every method, every operational lever)

## Links

- Repo: https://github.com/shadrach098/agentx_dev
- Issues: https://github.com/shadrach098/agentx_dev/issues
