Files
AIPA/docs/ARCHITECTURE.md
T
2026-04-03 09:01:01 -07:00

862 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AIPA Architecture Reference
**AI Personal Agency** — a multi-agent orchestration system where a Principal
(human user) issues directives that are routed, executed, audited, and
synthesized by a named hierarchy of LLM agents.
---
## Table of Contents
1. [Repository Layout](#1-repository-layout)
2. [System Overview](#2-system-overview)
3. [Request Lifecycle](#3-request-lifecycle)
4. [Agent Hierarchy](#4-agent-hierarchy)
5. [Configuration System](#5-configuration-system)
6. [Provider System](#6-provider-system)
7. [Orchestration Modules](#7-orchestration-modules)
8. [Tool-Calling System](#8-tool-calling-system)
9. [Streaming Output](#9-streaming-output)
10. [Terminal UI](#10-terminal-ui)
11. [Memory and Persistence](#11-memory-and-persistence)
12. [Key Data Structures](#12-key-data-structures)
13. [Function Reference](#13-function-reference)
14. [Environment Variables](#14-environment-variables)
15. [Extension Guide](#15-extension-guide)
---
## 1. Repository Layout
```
AIPA/
├── config/
│ ├── agents.yaml # Agent definitions, providers, roles
│ └── app.yaml # App identity, paths, logging, infrastructure
├── agents/
│ ├── prompts/ # System prompt Markdown files (one per named agent)
│ │ ├── miranda_chief_of_staff.md
│ │ ├── vera_auditor.md
│ │ ├── evelyn_director_of_personnel.md
│ │ ├── atlas_research_lead.md
│ │ ├── cole_operations_lead.md
│ │ ├── clio_analysis_lead.md
│ │ └── iris_interface_director.md
│ ├── registry/
│ │ └── agent_registry.md # Living roster maintained by Evelyn
│ └── specs/
│ └── evelyn_spec_iris_v1.md # Agent recruitment specifications
├── orchestration/ # All runnable Python code lives here
│ ├── orchestrator.py # Entry point; session REPL; provider clients; pipeline
│ ├── config.py # Unified config loader (app.yaml + agents.yaml + .env)
│ ├── ui.py # Rich terminal UI (Iris-authored)
│ ├── tools.py # Tool registry and agent-management tools
│ ├── task_store.py # JSON task persistence per session
│ ├── requirements.txt # Python dependencies
│ └── .venv/ # Virtual environment
├── docs/
│ ├── standing_brief.md # Miranda's cross-session working memory (mutable)
│ ├── standing_brief_template.md # Blank template for /reset-brief
│ └── archive/
│ └── standing_brief_archive.md
├── data/
│ └── tasks/ # session_<id>.json files written by task_store.py
├── logs/ # Runtime logs (if file logging enabled)
└── .env # API keys — never committed
```
---
## 2. System Overview
AIPA is a **single-process, async Python** application. There is no network
server, message queue, or database in the default configuration — everything
runs as in-process Python objects connected by `asyncio`.
```
┌─────────────────────────────────────────────────────────┐
│ Principal (human at terminal) │
└────────────────────────┬────────────────────────────────┘
│ directive (text)
┌─────────────────────────────────────────────────────────┐
│ Session REPL (orchestrator.py: principal_session) │
│ Slash-command dispatch → run_directive() │
└────────────────────────┬────────────────────────────────┘
┌──────────▼──────────┐
│ Miranda (CoS) │ orchestrator role
│ routes / responds │ stateful, thinking mode
└──┬──────────────────┘
│ TASK BRIEF blocks (if leads needed)
┌────────┼────────┬──────────────┐
▼ ▼ ▼ ▼
Atlas Cole Clio (other leads)
Research Ops Analysis dispatched in parallel
│ │ │
└────────┴────────┘
│ outputs collected
Miranda synthesizes → deliverable
Vera audits (independent, stateless)
Principal sees deliverable + audit memo
```
---
## 3. Request Lifecycle
`Session.run_directive(directive)` in `orchestrator.py` implements the full
pipeline. Steps, in order:
| Step | What happens | Key call |
|------|-------------|----------|
| 1 | Miranda receives the directive and decides: respond directly or issue task briefs | `call_agent_async(orchestrator, cos_prompt, stream=True)` |
| 2 | Task briefs are parsed from Miranda's response | `parse_task_briefs(cos_response, task_id)` |
| 3 | If leads are needed: tasks are persisted then dispatched concurrently | `dispatch_all_leads(leads, tasks)``asyncio.gather` |
| 4 | Miranda synthesizes all lead outputs into a single deliverable | `call_agent_async(orchestrator, synthesis_prompt, stream=True)` |
| 5 | Vera audits the deliverable (if `session.auto_audit` is True) | `run_auditor(auditor, deliverable, task_id)` |
| 6 | Deliverable and audit memo are displayed; session log updated | `ui.print_deliverable` / `ui.print_audit_memo` |
**Direct response path** (Step 2 finds no task briefs): Miranda's Step 1
response *is* the deliverable. Steps 3 and 4 are skipped.
**Task ID format:** `T-YYYYMMDD-NNN-A`, `T-YYYYMMDD-NNN-B`, …
The counter resets each session; the letter suffix enumerates tasks within
one directive.
---
## 4. Agent Hierarchy
### System roles
Roles are logical names. The actual agent assigned to each role is set in
`config/agents.yaml` under `roles:` and is read at startup — no code change
needed to reassign a role.
| Role key | Default agent | Description |
|----------|--------------|-------------|
| `orchestrator` | `miranda` | Primary interface to the Principal; routes all work and synthesizes outputs |
| `auditor` | `vera` | Stateless independent reviewer; reports only to the Principal |
| `recruiter` | `evelyn` | Agent generator; creates and maintains agent definitions |
Code always references `config.ORCHESTRATOR_AGENT`, `config.AUDITOR_AGENT`,
`config.RECRUITER_AGENT` — never hardcoded names.
### Named agents (leads)
Every agent in `agents.yaml` that is not assigned to a system role is a
**lead agent** and is available for task dispatch.
| Agent | Title | Color | Provider | Mode |
|-------|-------|-------|----------|------|
| Miranda | Chief of Staff | cyan | vastblueai_thinking | thinking (0.6) |
| Vera | Auditor | yellow | vastblueai_thinking | thinking (0.6) |
| Evelyn | Director of Personnel & Systems | white | vastblueai | non-thinking (0.7) |
| Atlas | Director of Research | green | vastblueai_thinking | thinking (0.6) |
| Cole | Director of Operations | blue | vastblueai | non-thinking (0.7) |
| Clio | Director of Analysis | magenta | vastblueai_thinking | thinking (0.6) |
| Iris | Director of Interface & Experience | bright_cyan | vastblueai | non-thinking (0.7) |
### Worker agents (runtime-generated)
Individual worker agents (RSCH-###, OPS-###, ANLY-###, etc.) are generated
at runtime inside Lead prompts. They are not defined in `agents.yaml` and
have no static prompt files — their prompts are composed by their Lead as
part of the task execution.
### Stateful vs stateless
`stateful: true` — the agent's `history` list grows across calls within a
session. Miranda and all leads are stateful.
`stateful: false` — history is discarded between calls. Only the last user
message and the immediately preceding assistant reply are sent. Vera is
stateless by design (each audit is independent).
`AgentState.messages_for_call()` enforces this: returns `self.history` if
stateful, `self.history[-2:]` if not.
---
## 5. Configuration System
### Loading order
`orchestration/config.py` runs at import time:
1. Loads `.env` into `os.environ` (via `python-dotenv`)
2. Reads `config/app.yaml` → application identity, paths, logging, session defaults
3. Reads `config/agents.yaml` → providers, agent definitions, roles
4. Derives all module-level constants
5. Environment variables with `AIPA_` prefix override any value from the YAML files
### `config/app.yaml` — key sections
| Section | Purpose |
|---------|---------|
| `app` | Name, version, environment, log level |
| `paths` | All file paths (prompts dir, standing brief, task dir, etc.) |
| `logging` | Format (rich/plain/json), file logging settings |
| `session` | `log_retention`, `lead_timeout`, `auto_audit`, `debug_output` |
| `memory` | Standing brief toggle; future vector/DB store config |
| `infrastructure` | API server, cache, queue placeholders (all disabled by default) |
Path resolution: relative paths are resolved against `AIPA_ROOT` if set,
otherwise against the repository root. Absolute paths are used as-is.
### `config/agents.yaml` — key sections
**`roles`** — maps logical role names to agent names:
```yaml
roles:
orchestrator: miranda
auditor: vera
recruiter: evelyn
```
**`default_provider`** — fallback provider for any agent without an explicit
`provider:` field.
**`providers`** — named provider definitions:
```yaml
providers:
vastblueai:
type: openai_compatible
base_url: http://10.250.50.54:9292/v1
api_key: local
default_model: "qwen3.5-35-a3b"
extra_body:
enable_thinking: false
```
Provider types: `anthropic`, `openai`, `openai_compatible`, `ollama`.
**`agents`** — named agent entries:
```yaml
agents:
evelyn:
title: Director of Personnel & Systems
color: white
prompt_file: evelyn_director_of_personnel.md
provider: vastblueai
model: "" # empty = use provider default_model
temperature: 0.7
max_tokens: 16384
stateful: true
tools: # optional — enables tool-calling loop
- list_agents
- upsert_agent_definition
```
### Reloading config at runtime
`config.reload_agents_config()` re-reads `agents.yaml` from disk and
refreshes all derived module-level variables (`AGENT_CONFIGS`,
`LEAD_AGENT_NAMES`, `ORCHESTRATOR_AGENT`, etc.). The `/reload` slash command
calls this then rebuilds all session agents.
### Module-level constants (config.py)
| Constant | Type | Description |
|----------|------|-------------|
| `APP_NAME` | str | Application name |
| `APP_VERSION` | str | Version string |
| `APP_ENV` | str | `development` / `production` / `test` |
| `PROMPTS_DIR` | Path | Absolute path to `agents/prompts/` |
| `REGISTRY_PATH` | Path | Absolute path to `agents/registry/agent_registry.md` |
| `STANDING_BRIEF_PATH` | Path | Absolute path to the standing brief |
| `BRIEF_TEMPLATE_PATH` | Path | Absolute path to the blank template |
| `ARCHIVE_DIR` | Path | Absolute path to `docs/archive/` |
| `ARCHIVE_PATH` | Path | `ARCHIVE_DIR / "standing_brief_archive.md"` |
| `LOGS_DIR` | Path | Absolute path to `logs/` |
| `DATA_DIR` | Path | Absolute path to `data/` |
| `TASKS_DIR` | Path | Absolute path to `data/tasks/` |
| `SESSION_LOG_RETENTION` | int | Max session log entries in standing brief |
| `LEAD_TIMEOUT_SECONDS` | int | Per-lead API call timeout |
| `VERA_AUTO_AUDIT` | bool | Default auto-audit setting |
| `DEBUG_PRINT_AGENT_OUTPUTS` | bool | Print raw outputs when True |
| `ORCHESTRATOR_AGENT` | str | Agent name holding the orchestrator role |
| `AUDITOR_AGENT` | str | Agent name holding the auditor role |
| `RECRUITER_AGENT` | str | Agent name holding the recruiter role |
| `AGENT_CONFIGS` | dict | `{name: {prompt_file, temperature, max_tokens, stateful, tools}}` |
| `LEAD_AGENT_NAMES` | set | All agents not in a system role |
| `ACTIVE_PROVIDER` | str | `default_provider` value from agents.yaml |
### Config helper functions
| Function | Returns | Description |
|----------|---------|-------------|
| `agent_provider_name(agent)` | str | Named provider for an agent (falls back to default) |
| `agent_provider_type(agent)` | str | Provider type string |
| `agent_api_key(agent)` | str | API key from env; warns if missing |
| `agent_base_url(agent)` | str\|None | Base URL for the agent's provider |
| `agent_extra_body(agent)` | dict | Extra request body fields (e.g. `enable_thinking`) |
| `agent_model(agent)` | str | Model name; falls back to provider `default_model` |
| `agent_title(agent)` | str | Human-readable title from agents.yaml |
| `agent_color(agent)` | str | Rich color name from agents.yaml |
| `agent_tools(agent)` | list[str] | Tool names assigned to the agent |
| `reload_agents_config()` | None | Re-reads agents.yaml, refreshes all derived state |
| `ensure_runtime_dirs()` | None | Creates logs/, data/, archive/, tasks/ if missing |
---
## 6. Provider System
### Class hierarchy
```
ProviderClient (base — raises NotImplementedError if called)
└── OpenAIClient (openai + openai_compatible providers)
(AnthropicClient) (commented out — uncomment to enable)
(OllamaClient) (commented out — uncomment to enable)
```
`build_client(agent_name)` is the factory. It reads the agent's provider type
from config and constructs the appropriate client.
### OpenAIClient
Wraps `AsyncOpenAI` from the `openai` package. Covers all OpenAI-compatible
endpoints (OpenAI, Groq, Together, Mistral, LM Studio, llama.cpp, Ollama /v1,
the local `vastblueai` server).
Key behaviour:
- `extra_body` is forwarded to every API call. For `vastblueai` this carries
`{"enable_thinking": true|false}` to control Qwen3 thinking mode.
- `call_async_streaming()` uses `stream=True` and yields
`('thinking', text)` chunks when `delta.reasoning_content` is present (Qwen3 /
DeepSeek R1 style) and `('content', text)` chunks for regular output.
- `call_async_with_tools()` makes a non-streaming call with OpenAI function-call
tool schemas and returns a `_ToolMessage`.
### Adding a new provider
1. Add a provider definition to `config/agents.yaml` under `providers:`.
2. If the endpoint is OpenAI-compatible, no code change is needed — just set
`type: openai_compatible` and `base_url`.
3. For a native API (Anthropic, Ollama native): uncomment or write a new
`ProviderClient` subclass in `orchestrator.py`, then add a branch to
`build_client()`.
---
## 7. Orchestration Modules
### orchestrator.py
Entry point and pipeline engine. Run directly:
```bash
cd orchestration
python orchestrator.py # interactive Principal session
python orchestrator.py --dry-run # trace routing without API calls
python orchestrator.py --agent evelyn # direct session with one agent
```
**Key functions:**
| Function | Description |
|----------|-------------|
| `build_client(agent_name)` | Factory: returns the correct ProviderClient |
| `build_agent(name)` | Constructs AgentState from config + client |
| `load_system_prompt(prompt_file)` | Reads and extracts the system prompt section from a .md file |
| `load_standing_brief()` | Reads the standing brief (falls back to template) |
| `save_standing_brief(content)` | Writes the standing brief to disk |
| `call_agent_async(agent, message, ...)` | Core caller: routes to tool loop or streaming |
| `_run_tool_loop(agent, ...)` | Agentic tool-calling loop for tool-enabled agents |
| `parse_task_briefs(response, prefix)` | Extracts TASK BRIEF blocks from orchestrator response |
| `dispatch_to_lead(lead, task, ...)` | Calls one lead with one task; updates task_store |
| `dispatch_all_leads(leads, tasks, ...)` | Concurrently dispatches all leads via asyncio.gather |
| `run_auditor(auditor, deliverable, ...)` | Calls Vera with a deliverable; returns audit memo |
| `update_standing_brief(orchestrator, summary)` | Calls Miranda to produce an updated brief |
| `principal_session(dry_run)` | Main REPL loop |
| `direct_agent_session(agent_name, ...)` | One-agent REPL (--agent flag) |
**Slash commands** are registered with `@command(["/name"], "description")`.
Each handler is `async def cmd_*(session, args) -> bool` where `False` exits
the REPL. Handlers are looked up at runtime by `_dispatch_command(name)`.
Available commands:
| Command | Description |
|---------|-------------|
| `/help` | Show command list |
| `/status` | Session stats, settings, and log |
| `/brief` | Show current standing brief |
| `/reset-brief` | Replace standing brief with blank template |
| `/agents` | Show agent roster with provider/model details |
| `/history` | Show session directive log |
| `/thinking` | Display stored thinking output from last response |
| `/audit` | Re-run Vera's audit on the last deliverable |
| `/debug` | Toggle raw agent output |
| `/tasks` | Show dispatched tasks and their status |
| `/autoaudit` | Toggle automatic Vera audit |
| `/clear` | Clear terminal |
| `/reload` | Reload agents.yaml and rebuild session agent roster |
| `/quit`, `/exit`, `/q` | Close session and update standing brief |
**Prompt extraction:** `load_system_prompt()` searches for a
`## System Prompt` section in the .md file and returns everything up to
`## Access Configuration` or end-of-file. Falls back to stripping header
metadata if no section is found.
### config.py
See [Section 5](#5-configuration-system).
### ui.py
See [Section 10](#10-terminal-ui).
### tools.py
See [Section 8](#8-tool-calling-system).
### task_store.py
Writes one JSON file per session under `data/tasks/session_<id>.json`.
| Function | Description |
|----------|-------------|
| `record_tasks(tasks, session_id, directive)` | Create initial records after parse_task_briefs |
| `update_task(task, session_id)` | Update status/output/error after any state change |
| `load_session(session_id)` | Load the full session record from disk |
| `list_sessions()` | List all sessions with summary metadata, newest first |
Task record fields: `task_id`, `directive`, `assigned_to`, `brief`, `status`
(`pending`|`in_progress`|`complete`|`error`), `output`, `error`,
`created_at`, `updated_at`.
---
## 8. Tool-Calling System
### Architecture
Tool-enabled agents run through `_run_tool_loop()` instead of a single
streaming call. The loop:
1. Calls the provider with tool schemas attached (`call_async_with_tools`)
2. If the response contains `tool_calls`: executes each tool, appends results
to history, displays the invocation via `ui.print_tool_call()`, loops
3. When the response has no `tool_calls`: streams the final content response
normally and exits
This implements the standard OpenAI function-calling agentic pattern.
### tools.py
**Registry:** `_REGISTRY: dict[str, dict]` — maps tool name to `{fn, schema}`.
**Registering a tool:**
```python
@tool(
description="One-sentence description shown to the LLM.",
parameters={
"type": "object",
"properties": {
"my_param": {"type": "string", "description": "..."},
},
"required": ["my_param"],
},
)
def my_tool(my_param: str) -> dict:
return {"result": my_param.upper()}
```
**Public API:**
| Function | Description |
|----------|-------------|
| `get_schemas(names)` | Returns OpenAI-format tool schema list for named tools |
| `call(name, arguments_json)` | Execute a tool by name; returns JSON string |
**Built-in agent-management tools:**
| Tool name | Description |
|-----------|-------------|
| `list_agents` | All agents with config summary |
| `read_agent_config` | Full config for one agent by name |
| `upsert_agent_definition` | Create or update an agent in agents.yaml |
| `read_agent_prompt` | Read a prompt .md file by agent name |
| `write_agent_prompt` | Write/overwrite a prompt .md file |
| `list_providers` | All provider definitions |
| `list_tools` | All registered tools with descriptions |
`upsert_agent_definition` uses `ruamel.yaml` for comment-preserving
round-trips. Changes take effect after `/reload`.
### Assigning tools to an agent
Add a `tools:` list to the agent's entry in `agents.yaml`:
```yaml
agents:
evelyn:
...
tools:
- list_agents
- upsert_agent_definition
- read_agent_prompt
- write_agent_prompt
```
Tool names must match registered function names in `tools.py`.
`config.agent_tools(agent)` returns the list; `config.AGENT_CONFIGS[name]["tools"]`
holds it directly.
---
## 9. Streaming Output
All agent calls default to `stream=True`. The flow:
1. `call_agent_async()` detects `agent.tools` is empty → calls
`ui.stream_agent_output(agent_name, client.call_async_streaming(...), mode=...)`
2. `call_async_streaming()` on `OpenAIClient` connects with `stream=True` and
yields `('thinking', text)` and `('content', text)` tuples as chunks arrive.
Thinking tokens come from `delta.reasoning_content` (Qwen3 / DeepSeek R1).
3. `stream_agent_output()` in `ui.py` consumes the async generator and renders
output live using Rich's `Live` display.
**Stream modes** (passed as `stream_mode` to `call_agent_async`):
| Mode | Used for | Behaviour |
|------|----------|-----------|
| `"deliverable"` | Miranda's routing/synthesis responses | Full panel with thinking + content |
| `"audit"` | Vera's audit memos | Yellow thinking identity; verdict-aware border colour |
| `"background"` | Standing brief update | Compact transient spinner; no panel |
`_last_stream_rendered` is set to `True` after `stream_agent_output` completes,
causing the subsequent `print_deliverable()` / `print_audit_memo()` to become
no-ops (avoiding double rendering).
`ui.get_last_thinking()` returns the full thinking text from the most recent
response, accessible via `/thinking`.
---
## 10. Terminal UI
`orchestration/ui.py` is authored and maintained by Iris.
### Visual language
| Agent / concept | Color | Notes |
|-----------------|-------|-------|
| Miranda | cyan | Primary orchestrator |
| Vera | yellow | Auditor, independent track |
| Atlas | green | Research |
| Cole | blue | Operations |
| Clio | magenta | Analysis |
| Evelyn | white | Personnel & systems |
| Iris | bright_cyan | Interface |
| System messages | dim white | Infrastructure notes |
| Errors | bold red | Failures and blockers |
| Warnings | bold yellow | Flags and cautions |
| Task IDs | dim cyan | Always dim, never distracting |
The Rich `Theme` is built dynamically from `agents.yaml` color values so
that adding or recoloring an agent in config is reflected automatically.
### Key UI functions
| Function | Description |
|----------|-------------|
| `stream_agent_output(agent_name, gen, mode)` | Live streaming render with thinking panel |
| `print_agent_panel(agent_name, content)` | Static Markdown panel for an agent |
| `print_deliverable(content, task_id)` | Orchestrator's synthesized output |
| `print_audit_memo(content, task_id)` | Vera's memo with verdict-based border |
| `print_tool_call(agent, tool, args_json, result_json)` | Inline tool invocation display |
| `print_thinking_expansion(text)` | Full thinking text in a dim panel |
| `lead_dispatch_progress(tasks)` | Context manager: live per-task progress bars |
| `print_session_header()` | Opening banner |
| `print_session_footer(count)` | Closing rule |
| `print_status(...)` | `/status` command output |
| `print_agents(configs, provider)` | `/agents` roster table |
| `get_directive()` | Styled `Principal ` input prompt |
| `get_last_thinking()` | Returns stored thinking from last response |
### Thinking panel behaviour
During the thinking phase a dim panel shows a rolling 300-character window
of the thinking text with an overflow indicator (`+N chars earlier`) when
it exceeds the window. When the first content token arrives the panel
collapses to a single summary line: `▸ Thinking [N chars · L lines]`.
### Audit verdict border
`_print_audit_memo_panel()` (and the audit path in `stream_agent_output`)
scan the content for a `VERDICT:` line and map its value to a border color:
- `PASS` → green
- `PASS WITH NOTES` → warning (bold yellow)
- `FLAG` → flag (bold yellow)
- `REJECT` → red
- (none found) → agent color (yellow)
---
## 11. Memory and Persistence
### Standing Brief
The standing brief (`docs/standing_brief.md`) is Miranda's cross-session
working memory. At session startup it is appended to Miranda's system prompt:
```python
orchestrator.system_prompt += "\n\n---\n\n## Standing Brief (current)\n\n" + brief
```
When the session closes (`/quit`), Miranda is called to produce a fully
updated brief incorporating the session summary, and the result is written
back to disk. Old session log entries beyond `SESSION_LOG_RETENTION` are
archived to `docs/archive/standing_brief_archive.md`.
### Task Store
`task_store.py` writes JSON files to `data/tasks/`. Each directive that
dispatches leads produces one session file. The file is updated after every
task state transition (`pending``in_progress``complete`/`error`).
This allows an interrupted session to be reconstructed and provides an audit
trail of all work dispatched.
### Agent Memory Store
Placeholder configuration exists in `app.yaml` under `memory.store` for
future vector/DB-backed agent memory (SQLite, PostgreSQL, Chroma, Pinecone).
Currently disabled (`enabled: false`); not implemented in code.
---
## 12. Key Data Structures
### AgentState (orchestrator.py)
```python
@dataclass
class AgentState:
name: str # agent key (e.g. "miranda")
system_prompt: str # loaded from prompt file + optional brief injection
model: str # resolved model name
temperature: float
max_tokens: int
stateful: bool
client: ProviderClient # provider-specific LLM client
history: list[dict] # message history [{"role":..., "content":...}]
tools: list[str] # tool names from agents.yaml
```
`messages_for_call()` returns `history` (stateful) or `history[-2:]` (stateless).
### Task (orchestrator.py)
```python
@dataclass
class Task:
task_id: str # e.g. "T-20260402-001-A"
directive: str # the lead's portion of the orchestrator prompt
assigned_to: str # agent name
brief: str # full TASK BRIEF text sent to the lead
status: str # pending | in_progress | complete | error
output: str # lead's response text
error: str # exception message on failure
```
### _ToolMessage (orchestrator.py)
```python
@dataclass
class _ToolMessage:
content: str # text content from the LLM
tool_calls: list | None # openai ToolCall objects, or None
```
### Tool history messages
When a tool is called, two messages are appended to `agent.history`:
```python
# Assistant message with tool call
{"role": "assistant", "content": "", "tool_calls": [
{"id": "call_abc", "type": "function",
"function": {"name": "list_agents", "arguments": "{}"}}
]}
# Tool result
{"role": "tool", "tool_call_id": "call_abc", "content": "[{...}]"}
```
---
## 13. Function Reference
### orchestrator.py — full index
| Function / Class | Location | Purpose |
|-----------------|----------|---------|
| `ProviderClient` | class | Abstract base client |
| `OpenAIClient` | class | OpenAI / openai_compatible provider |
| `build_client(agent_name)` | fn | Provider client factory |
| `AgentState` | dataclass | Runtime agent state |
| `Task` | dataclass | Task record |
| `_ToolMessage` | dataclass | Tool-call response |
| `load_system_prompt(prompt_file)` | fn | Reads .md, extracts system prompt section |
| `load_standing_brief()` | fn | Reads brief or falls back to template |
| `save_standing_brief(content)` | fn | Writes brief to disk |
| `build_agent(name)` | fn | Constructs AgentState from config |
| `new_task_id()` | fn | Generates next task ID |
| `call_agent_async(agent, msg, ...)` | async fn | Core call; routes to tool loop or stream |
| `_run_tool_loop(agent, ...)` | async fn | Agentic tool-calling loop |
| `parse_task_briefs(response, prefix)` | fn | Regex-extracts TASK BRIEF blocks |
| `dispatch_to_lead(lead, task, ...)` | async fn | Calls one lead; updates progress and store |
| `dispatch_all_leads(leads, tasks, ...)` | async fn | Concurrent lead dispatch |
| `run_auditor(auditor, deliverable, ...)` | async fn | Vera audit call |
| `update_standing_brief(orch, summary)` | async fn | Miranda brief update call |
| `Session` | class | Full session state and pipeline |
| `Session.run_directive(directive)` | async method | Full 5-step pipeline |
| `Session.close()` | async method | Brief update + footer |
| `command(names, desc)` | decorator | Registers slash command handler |
| `principal_session(dry_run)` | async fn | Main REPL |
| `direct_agent_session(name, ...)` | async fn | Single-agent REPL |
| `main()` | fn | CLI entry point |
### tools.py — full index
| Function / decorator | Purpose |
|---------------------|---------|
| `@tool(description, parameters)` | Register a function as a tool |
| `get_schemas(names)` | Return OpenAI-format schemas for named tools |
| `call(name, arguments_json)` | Execute tool by name; return JSON string |
| `_load_agents_yaml()` | ruamel.yaml load for comment-preserving round-trip |
| `_save_agents_yaml(ryaml, data)` | Write back agents.yaml with comments intact |
| `list_agents()` | Tool: all agent configs |
| `read_agent_config(agent_name)` | Tool: one agent's full config |
| `upsert_agent_definition(agent_name, ...)` | Tool: create or update agent in YAML |
| `read_agent_prompt(agent_name)` | Tool: read prompt .md by agent name |
| `write_agent_prompt(prompt_file, content)` | Tool: write/overwrite prompt .md |
| `list_providers()` | Tool: all provider definitions |
| `list_tools()` | Tool: all registered tools |
---
## 14. Environment Variables
All variables use the `AIPA_` prefix. Set them in `.env` (loaded automatically)
or as shell environment variables (override `.env`).
### Required (if using cloud providers)
| Variable | Provider | Description |
|----------|----------|-------------|
| `ANTHROPIC_API_KEY` | anthropic | Anthropic API key |
| `OPENAI_API_KEY` | openai | OpenAI API key |
| `GROQ_API_KEY` | groq | Groq API key |
| `TOGETHER_API_KEY` | together | Together AI key |
| `MISTRAL_API_KEY` | mistral | Mistral API key |
| `ANYSCALE_API_KEY` | anyscale | Anyscale key |
Local providers (`vastblueai`, `lmstudio`, `llamacpp`, `ollama`) use
`api_key: local` in agents.yaml and need no env variable.
### Application overrides
| Variable | Default | Description |
|----------|---------|-------------|
| `AIPA_ROOT` | (repo root) | Relocate all mutable data paths |
| `AIPA_APP_NAME` | AIPA | Application display name |
| `AIPA_ENV` | development | Environment name |
| `AIPA_LOG_LEVEL` | INFO | Log level |
| `AIPA_LOG_FORMAT` | rich | Terminal format (rich/plain/json) |
| `AIPA_LOG_FILE_ENABLED` | false | Enable file logging |
| `AIPA_LOG_FILE_PATH` | logs/aipa.log | Log file path |
| `AIPA_PROMPTS_DIR` | agents/prompts | Prompts directory |
| `AIPA_STANDING_BRIEF_PATH` | docs/standing_brief.md | Standing brief path |
| `AIPA_ARCHIVE_DIR` | docs/archive | Archive directory |
| `AIPA_TASKS_DIR` | data/tasks | Task JSON directory |
| `AIPA_SESSION_LOG_RETENTION` | 10 | Sessions kept in brief before archiving |
| `AIPA_LEAD_TIMEOUT` | 120 | Lead API call timeout (seconds) |
| `AIPA_AUTO_AUDIT` | true | Enable Vera auto-audit |
| `AIPA_DEBUG` | false | Print raw agent outputs |
| `AIPA_MEMORY_BRIEF_ENABLED` | true | Enable standing brief |
| `AIPA_APP_CONFIG` | config/app.yaml | Override app.yaml path |
| `AIPA_AGENTS_CONFIG` | config/agents.yaml | Override agents.yaml path |
---
## 15. Extension Guide
### Add a new named agent
1. Write a system prompt file in `agents/prompts/<name>_<role>.md`.
Structure: metadata block, then `## System Prompt` section.
2. Add the agent to `config/agents.yaml` under `agents:` with all required
fields (`title`, `color`, `prompt_file`, `provider`, `model`, `temperature`,
`max_tokens`, `stateful`).
3. The agent becomes available for lead dispatch immediately on next startup
(or after `/reload`). No code changes needed.
Alternatively, have Evelyn create the agent during a session — she has
`upsert_agent_definition` and `write_agent_prompt` tools.
### Add a new provider
1. Add an entry to `config/agents.yaml` under `providers:`.
2. For OpenAI-compatible endpoints: set `type: openai_compatible` and
`base_url`. No code change needed.
3. For native APIs: implement a `ProviderClient` subclass in `orchestrator.py`
and add a branch to `build_client()`.
4. Add API key env variable to `.env` and reference it via `api_key_env:`.
### Add a new tool
1. Write the function in `orchestration/tools.py` (or a new module that
imports and calls `@tool()`).
2. Decorate it with `@tool(description=..., parameters={...})`.
3. Add the tool name to the `tools:` list of any agent that should have
access in `agents.yaml`.
4. Run `/reload` or restart the session.
Tools receive `**kwargs` parsed from the LLM's JSON arguments. Always return
a JSON-serialisable value (dict, list, str, etc.). The `call()` dispatcher
JSON-encodes the return value automatically.
### Reassign a system role
Edit the `roles:` section in `config/agents.yaml`:
```yaml
roles:
orchestrator: new_agent_name
```
Restart the session (or `/reload`). No code changes needed.
### Extend the tool-calling loop
`_run_tool_loop()` in `orchestrator.py` is the central loop. To add
parallel tool execution, streaming tool results, or loop limits, edit
that function directly.
### Add a slash command
```python
@command(["/mycommand", "/mc"], "Brief description shown in /help")
async def cmd_mycommand(session: Session, args: str) -> bool:
# args is everything after the command name
# Return True to continue the REPL, False to exit
ui.print_system(f"You typed: {args}")
return True
```
Commands self-register at import time via `COMMANDS` list; no other
wiring needed.