Tools are stateless modules that implement the ExAthena.Tool behaviour. The agent loop calls them, and the result is replayed back to the model as a tool-result message.

Builtin tools

ToolPurposePhase: :plan
ExAthena.Tools.ReadRead a file, with optional offset/limit
ExAthena.Tools.GlobFind files by wildcard pattern
ExAthena.Tools.GrepSearch file contents by regex
ExAthena.Tools.WriteCreate/overwrite a file
ExAthena.Tools.EditExact-string replacement in a file
ExAthena.Tools.BashShell execution with timeout
ExAthena.Tools.WebFetchHTTP GET (http/https only, 1 MB cap)
ExAthena.Tools.TodoWriteAgent todo list
ExAthena.Tools.PlanModeRequest phase transition
ExAthena.Tools.SpawnAgentSynchronous sub-agent

Use :all or a list of modules to enable tools:

# All builtins
ExAthena.run("refactor this project", tools: :all)

# A subset
ExAthena.run("find the bug", tools: [
  ExAthena.Tools.Read,
  ExAthena.Tools.Glob,
  ExAthena.Tools.Grep
])

# Configured globally
config :ex_athena, tools: [ExAthena.Tools.Read, ExAthena.Tools.Bash]

Path resolution

The Read, Write, and Edit tools accept absolute paths or paths relative to ctx.cwd. ExAthena.ToolContext.resolve_path/2 rejects path traversal (..) and null bytes before the tool runs.

Writing your own tool

defmodule MyApp.Tools.DescribePage do
  @behaviour ExAthena.Tool

  @impl true
  def name, do: "describe_page"

  @impl true
  def description, do: "Summarise the content of a web page"

  @impl true
  def schema do
    %{
      type: "object",
      properties: %{
        url: %{type: "string", description: "URL to fetch"}
      },
      required: ["url"]
    }
  end

  @impl true
  def execute(%{"url" => url}, _ctx) do
    # … fetch + summarise
    {:ok, "summary of " <> url}
  end
end

# Register it:
ExAthena.run("describe https://example.com", tools: [MyApp.Tools.DescribePage])

Return shapes

ReturnBehaviour
{:ok, result}Stringified and replayed to the model.
{:error, reason}Replayed as an error tool-result; loop continues.
{:halt, reason}Loop stops immediately (emergency brake).

Using ctx.assigns

ExAthena.ToolContext.assigns is a map threaded through every tool call. Use it for data the host app needs during tool execution — project id, conversation id, database ref, user id, pubsub name.

ExAthena.run("…", assigns: %{project_id: 42, user_id: "abc"})

TodoWrite notifier

ExAthena.Tools.TodoWrite optionally calls ctx.assigns[:todo_writer] with the new list — useful for broadcasting to a LiveView:

writer = fn todos -> MyAppWeb.Endpoint.broadcast("todos", "update", todos) end

ExAthena.run("build the feature",
  tools: :all,
  assigns: %{todo_writer: writer})

Phase gating (permissions)

Each builtin has a static "mutating or not" classification. The :plan phase permits only the non-mutating ones; :default permits everything (subject to can_use_tool + hooks); :bypass_permissions skips checks.

See ExAthena.Permissions for the full check order and guides/agent_loop.md for end-to-end examples including permission flows.