34 KiB
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
- Repository Layout
- System Overview
- Request Lifecycle
- Agent Hierarchy
- Configuration System
- Provider System
- Orchestration Modules
- Tool-Calling System
- Streaming Output
- Terminal UI
- Memory and Persistence
- Key Data Structures
- Function Reference
- Environment Variables
- 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:
- Loads
.envintoos.environ(viapython-dotenv) - Reads
config/app.yaml→ application identity, paths, logging, session defaults - Reads
config/agents.yaml→ providers, agent definitions, roles - Derives all module-level constants
- 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:
roles:
orchestrator: miranda
auditor: vera
recruiter: evelyn
default_provider — fallback provider for any agent without an explicit
provider: field.
providers — named provider definitions:
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:
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_bodyis forwarded to every API call. Forvastblueaithis carries{"enable_thinking": true|false}to control Qwen3 thinking mode.call_async_streaming()usesstream=Trueand yields('thinking', text)chunks whendelta.reasoning_contentis 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
- Add a provider definition to
config/agents.yamlunderproviders:. - If the endpoint is OpenAI-compatible, no code change is needed — just set
type: openai_compatibleandbase_url. - For a native API (Anthropic, Ollama native): uncomment or write a new
ProviderClientsubclass inorchestrator.py, then add a branch tobuild_client().
7. Orchestration Modules
orchestrator.py
Entry point and pipeline engine. Run directly:
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.
ui.py
See Section 10.
tools.py
See Section 8.
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:
- Calls the provider with tool schemas attached (
call_async_with_tools) - If the response contains
tool_calls: executes each tool, appends results to history, displays the invocation viaui.print_tool_call(), loops - 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:
@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:
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:
call_agent_async()detectsagent.toolsis empty → callsui.stream_agent_output(agent_name, client.call_async_streaming(...), mode=...)call_async_streaming()onOpenAIClientconnects withstream=Trueand yields('thinking', text)and('content', text)tuples as chunks arrive. Thinking tokens come fromdelta.reasoning_content(Qwen3 / DeepSeek R1).stream_agent_output()inui.pyconsumes the async generator and renders output live using Rich'sLivedisplay.
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→ greenPASS 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:
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)
@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)
@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)
@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:
# 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
- Write a system prompt file in
agents/prompts/<name>_<role>.md. Structure: metadata block, then## System Promptsection. - Add the agent to
config/agents.yamlunderagents:with all required fields (title,color,prompt_file,provider,model,temperature,max_tokens,stateful). - 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
- Add an entry to
config/agents.yamlunderproviders:. - For OpenAI-compatible endpoints: set
type: openai_compatibleandbase_url. No code change needed. - For native APIs: implement a
ProviderClientsubclass inorchestrator.pyand add a branch tobuild_client(). - Add API key env variable to
.envand reference it viaapi_key_env:.
Add a new tool
- Write the function in
orchestration/tools.py(or a new module that imports and calls@tool()). - Decorate it with
@tool(description=..., parameters={...}). - Add the tool name to the
tools:list of any agent that should have access inagents.yaml. - Run
/reloador 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:
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
@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.