Skip to content

Tools

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, String.t() | map()} | {:error, String.t()}

The context map passed to execute/2 contains:

  • :agent_id — the psychonaut’s unique identifier
  • :bead_id — current bead identifier
  • :burst_id — current burst identifier
  • :project_root — absolute path to the project root
  • :tool_call_id — ID for this tool invocation (for streaming updates)

Agent.Tool.Registry is an ETS-based GenServer that maps tool names to modules. Built-in tools are auto-registered on startup. The registry supports:

# Look up a tool by name from allowed tools list
ToolRegistry.lookup("shell", agent_tools)
# Get tool definitions for provider API
ToolRegistry.definitions(agent_tools)
# -> [%{name: "shell", description: "...", parameters: %{...}}, ...]

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 string
ToolRiskDescription
file_readsafeRead file contents. Returns the full file content as a string.
file_writemoderateWrite/create files. Integrates with LeaseManager for cooperative locking. Acquires exclusive lease before writing, releases after.
peek_filesafeQuick file preview. Returns first/last N lines without reading the entire file.
peek_dirsafeDirectory listing with file sizes and modification times.
ToolRiskDescription
shelldangerousExecute shell commands. Three-tier security via CommandGuard + TraumaGuard.

Shell command classification:

CommandGuard.classify("ls -la") # -> :safe
CommandGuard.classify("git reset --hard") # -> :caution
CommandGuard.classify("rm -rf /") # -> :danger
# Trauma escalation (experience-based)
TraumaGuard.classify_with_trauma("npm install", trauma_records)
# -> {:danger, "Matches past incident: npm install corrupted node_modules"}

Danger patterns (blocked until Tether approval): rm -rf /, DROP DATABASE, git push --force, mkfs., dd if=, chmod -R 777, curl|bash

Caution patterns (logged, Tether notified): git reset --hard, rm -r, DROP, DELETE FROM, sudo, npm publish

ToolRiskDescription
ask_usersafeReach for the Tether with a question. Blocks until answered or timed out.
ToolRiskDescription
recursemoderateSpawn a single recursive sub-agent. Parent blocks until child completes.
recurse_fan_outmoderateConcurrent map-style decomposition. Optional reduce step for synthesis.
# Single recursive call
%{"query" => "Analyze the authentication module", "context" => "..."}
# Fan-out with reduce
%{
"tasks" => [
%{"task" => "Analyze module A", "context" => "..."},
%{"task" => "Analyze module B", "context" => "..."}
],
"reduce_prompt" => "Synthesize findings from all analyses"
}

Guardrails: depth limit (default 5), global semaphore (16 slots), per-child timeout (5 min), recursive tools removed at max depth.

ToolRiskDescription
search_sessionssafeFTS5 search over past session transcripts. Supports boolean operators, phrase matching, prefix matching.
search_skillssafeFind relevant skills from the catalog, ranked by Thompson sampling.
create_skillsafeAdd a reusable skill template to the catalog.
read_diarysafeRead diary entries from past bursts.
propose_playbook_deltasafePropose changes (add/update/deprecate) to playbook rules.
ToolRiskDescription
check_messagessafeRead unread messages from the inter-agent mailbox.
list_psychonautssafeList all active psychonaut agents with their status.
whoissafeLook up agent details by ID.
ToolRiskDescription
set_pipelinemoderateOverride the pipeline for the current bead. Goes through grounding queue.
create_pipelinemoderateCreate a new pipeline definition. Goes through grounding queue.
ToolRiskDescription
echosafeReturn input as output. Useful for testing and debugging.

Stateless pure functions for three-tier command classification:

CommandGuard.classify(command)
# -> :safe | :caution | :danger

Danger takes priority over caution when a command matches both tiers.

Experience-based escalation. Matches commands against a list of TraumaRecord structs:

%Annihilation.Security.TraumaRecord{
pattern: "npm install", # Plain string or ~r/ regex
context: "npm install corrupted node_modules on 2026-02-15",
severity: :danger
}

Even :safe commands are escalated to :danger if they match a trauma record. Pattern types:

  • Plain strings: case-insensitive substring match
  • Regex (~r/ prefix): compiled and matched at runtime

Cooperative file locking via File.LeaseManager:

# Acquire exclusive lease before writing
LeaseManager.acquire(path, agent_id, :exclusive)
# -> :ok | {:conflict, %{agent_id: "other", lease_type: :exclusive, expires_at: ...}}
# Release after writing
LeaseManager.release(path, agent_id)

Lease types:

  • :exclusive — one writer, no other agent can write
  • :shared_read — multiple readers allowed

Features:

  • TTL: 5 minutes default, renewable
  • Automatic cleanup: 30-second sweep for expired leases
  • Crash recovery: process monitoring releases leases on agent crash
  • Burst cleanup: clear_all/0 called on burst drain

Recursive children inherit parent leases as :read_only — they cannot acquire exclusive leases.

defmodule MyTool do
@behaviour Annihilation.Agent.Tool
def name, do: "my_tool"
def description, do: "Does something useful"
def parameters_schema do
%{
"type" => "object",
"properties" => %{
"input" => %{"type" => "string", "description" => "The input"}
},
"required" => ["input"]
}
end
def execute(%{"input" => input}, _context) do
{:ok, "Processed: #{input}"}
end
end

Place in .annihilation/tools/my_tool.ex for auto-discovery.