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
| Tool | Purpose | Phase: :plan |
|---|---|---|
ExAthena.Tools.Read | Read a file, with optional offset/limit | ✅ |
ExAthena.Tools.Glob | Find files by wildcard pattern | ✅ |
ExAthena.Tools.Grep | Search file contents by regex | ✅ |
ExAthena.Tools.Write | Create/overwrite a file | ❌ |
ExAthena.Tools.Edit | Exact-string replacement in a file | ❌ |
ExAthena.Tools.Bash | Shell execution with timeout | ❌ |
ExAthena.Tools.WebFetch | HTTP GET (http/https only, 1 MB cap) | ✅ |
ExAthena.Tools.TodoWrite | Agent todo list | ❌ |
ExAthena.Tools.PlanMode | Request phase transition | ✅ |
ExAthena.Tools.SpawnAgent | Synchronous 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
| Return | Behaviour |
|---|---|
{: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.