Psychonauts
Phase Machine
Section titled “Phase Machine”Each psychonaut is a GenServer (Agent.Server). The phase field drives transitions via handle_continue(:enter_phase, state):
:idle --> :streaming --> :executing_tools --> :steering_check --> :streaming (loop) \-> :steering_check --> :done (no tool calls, task complete) \-> :error (max turns exceeded)Phase transitions are automatic — each phase completes and triggers the next via {:noreply, state, {:continue, :enter_phase}}. The agent loops until:
- The LLM responds without tool calls (task complete ->
:done) - Turn limit exceeded (
max_turns, default 50 ->:error) - Recursion depth exceeded (
max_depth, default 5 ->:done) - Conversation exceeds 200 messages ->
:done
Agent.State
Section titled “Agent.State”The complete runtime state for a psychonaut:
%Annihilation.Agent.State{ id: "agent_abc123", # Unique agent identifier identity: %AgentIdentity{}, # Name, description, model model: "claude-sonnet-4-20250514", # LLM model provider: Annihilation.Agent.Provider.Anthropic, # Provider module system_prompt: "You are...", # System prompt tools: [Shell, FileRead], # Available tool modules messages: [%Message{}, ...], # Full conversation history current_deltas: nil, # Delta accumulator during streaming phase: :idle, # Current phase error_reason: nil, # Error details when phase == :error bead_id: nil, # Current bead being worked on burst_id: nil, # Current burst ID session_id: nil, # Session for JSONL logging pipeline_stage: nil, # Current pipeline stage index recursion_depth: 0, # Current recursion depth max_depth: 5, # Max allowed recursion depth max_turns: 50, # Max LLM turns before forced stop parent_id: nil, # Parent agent ID (if recursive child) turn_count: 0, # Current turn count config: %{}, # Provider-specific config (api_key, etc.) inherited_leases: [], # File leases from parent (read-only) lease_permissions: :full, # :full or :read_only active_leases: [] # Currently held file leases}Provider Abstraction
Section titled “Provider Abstraction”The Provider behaviour abstracts LLM APIs. The system never sees provider-specific formats.
@callback stream(messages, tools, config) :: {:ok, Enumerable.t()} | {:error, term()}@callback format_messages(messages) :: [map()]@callback format_tools(tools) :: [map()]Anthropic Provider
Section titled “Anthropic Provider”The built-in Provider.Anthropic communicates with the Anthropic Messages API using HTTP/2 streaming via Mint:
- Connects via
Mint.HTTP.connect(:https, "api.anthropic.com", 443) - Builds
Stream.resource/3for lazy delta processing - SSE parser (
SSEParser) converts raw bytes intoDeltastructs DeltaAccumulatorcollects deltas into completeMessagestructs- System prompt extracted from messages and passed as separate
systemparameter - Tool calls formatted as
tool_usecontent blocks
Config keys: :api_key, :model (default claude-sonnet-4-20250514), :max_tokens (default 8192), :system, :temperature.
Mock Provider
Section titled “Mock Provider”Provider.Mock returns scripted responses for testing. No network calls.
Tool System
Section titled “Tool System”Every tool implements Annihilation.Agent.Tool:
@callback name() :: String.t()@callback description() :: String.t()@callback parameters_schema() :: map() # JSON Schema@callback execute(args :: map(), context :: map()) :: {:ok, term()} | {:error, String.t()}The context map contains agent_id, bead_id, burst_id, project_root, and tool_call_id.
Tool Registry
Section titled “Tool Registry”Agent.Tool.Registry is an ETS-based GenServer that maps tool names to modules. On startup, it auto-registers all built-in tools. Tools are resolved at call time from the agent’s tools list.
Validation-as-Feedback
Section titled “Validation-as-Feedback”Tool errors are never fatal. They become %ToolResult{is_error: true} and are sent back to the LLM, which self-corrects:
- Parameter validation failure -> error result with schema info
- Tool crash -> rescued -> error result with exception message
- Tool not found -> error result listing available tools
- Invalid JSON arguments -> error result with raw arguments
Built-in Tools
Section titled “Built-in Tools”| Tool | Module | Description |
|---|---|---|
shell | Tool.Shell | Execute shell commands (integrates with CommandGuard + TraumaGuard) |
file_read | Tool.FileRead | Read file contents |
file_write | Tool.FileWrite | Write/edit files (integrates with LeaseManager) |
ask_user | Tool.AskUser | Reach for the Tether with a question |
echo | Tool.Echo | Return input as output (testing/debugging) |
peek_file | Tool.PeekFile | Quick file preview (first/last N lines) |
peek_dir | Tool.PeekDir | Directory listing with metadata |
recurse | Tool.Recurse | Spawn a single recursive sub-agent |
recurse_fan_out | Tool.RecurseFanOut | Concurrent fan-out with optional reduce |
search_sessions | Tool.SearchSessions | FTS5 search over past session transcripts |
search_skills | Tool.SearchSkills | Find relevant skills from the catalog |
create_skill | Tool.CreateSkill | Add a skill to the catalog |
check_messages | Tool.CheckMessages | Read inter-agent mailbox |
list_psychonauts | Tool.ListPsychonauts | List active psychonaut agents |
whois | Tool.Whois | Look up agent details by ID |
set_pipeline | Tool.SetPipeline | Override pipeline for current bead |
create_pipeline | Tool.CreatePipeline | Create a new pipeline definition |
read_diary | Tool.ReadDiary | Read diary entries from past bursts |
propose_playbook_delta | Tool.ProposePlaybookDelta | Propose changes to playbook rules |
Recursion
Section titled “Recursion”Psychonauts can spawn child agents for focused subtasks via Agent.Recurse:
Single Call
Section titled “Single Call”Agent.Recurse.call(parent_state, context, query, opts)# -> {:ok, result_text} | {:error, reason}The parent blocks until the child completes, crashes, or times out (default 5 minutes). Children inherit the parent’s bead, session, burst context, and file leases (as read-only).
Fan-Out
Section titled “Fan-Out”Agent.Recurse.fan_out(parent_state, tasks, opts)# tasks = [%{task: "analyze X", context: "..."}, ...]# -> {:ok, %{result: text, metadata: %{total_partitions: N, ...}}}Spawns N children concurrently (max concurrency: 5 by default). Supports an optional :reduce_prompt that spawns a synthesis agent to combine results.
Guardrails
Section titled “Guardrails”- Depth limit:
max_depth(default 5). At max depth,recurseandrecurse_fan_outtools are removed from the child’s tool list. - Semaphore:
RecursionSemaphorelimits global concurrent children to 16. Uses a waiter queue when at capacity. - Timeout: Per-child timeout (default 5 minutes). Timed-out children are terminated.
- Lease inheritance: Children inherit parent’s file leases as
:read_only. They cannot acquire exclusive leases.
Session Logging
Section titled “Session Logging”Child lifecycle events (child_spawned, child_completed, child_failed) are logged to the parent’s session JSONL for full audit trail.
Agent Messaging
Section titled “Agent Messaging”Psychonauts can communicate via Agent.Mailbox, an ETS-based message router:
# Send a direct messageMailbox.send_message(from_id, to_id, "subject", "body", priority: :high)
# Broadcast to all active agentsMailbox.broadcast_message(from_id, "subject", "body")
# Check inbox (via check_messages tool)Mailbox.get_unread(agent_id)Messages have priority ordering (:high > :normal) and statuses (:unread -> :read -> :acknowledged). Event notifications are broadcast on "agent:mail:#{id}" for real-time awareness.