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

34 KiB
Raw Blame History

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
  2. System Overview
  3. Request Lifecycle
  4. Agent Hierarchy
  5. Configuration System
  6. Provider System
  7. Orchestration Modules
  8. Tool-Calling System
  9. Streaming Output
  10. Terminal UI
  11. Memory and Persistence
  12. Key Data Structures
  13. Function Reference
  14. Environment Variables
  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:

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_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:

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:

  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:

@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:

  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:

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 (pendingin_progresscomplete/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

  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:

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.