862 lines
34 KiB
Markdown
862 lines
34 KiB
Markdown
# 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.
|