# 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_.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_.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/_.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.