Just the loop. Any provider. Pure OTP.
Four tools, three dependencies, one behaviour per provider.
Swap models in one line, run agents as supervised GenServers.
Need something it doesn't have? The agent builds it.
{:ok, result} = Alloy.run("Read mix.exs and tell me the version",
provider: {Alloy.Provider.Anthropic, model: "claude-sonnet-4-6"},
tools: [Alloy.Tool.Core.Read]
)
result.text
#=> "The version in mix.exs is 0.4.0"
Three dependencies: jason,
req,
telemetry. That's it.
No framework, no platform, no opinions.
Need a new tool? The agent writes one. Need a new provider? It's ~200 lines.
Send messages. Execute tools. Loop until done. Four core tools — read, write, edit, bash — and a system prompt. What you leave out matters more than what you put in.
Agents run as supervised GenServers. Supervision trees restart crashed agents. Parallel tool execution is real concurrency, not async callbacks.
Delegate, broadcast, handoff. Fault-isolated by default — one agent crashes, the others keep running. Shared context optional.
Token-by-token streaming from any provider. SSE support built in. Works with the agent loop, GenServer, and REPL.
Telemetry, logging, custom hooks. Middleware can halt the loop to enforce spend caps or security policies. Runs at every hook point.
Automatic conversation summarization when approaching token limits. Agents keep working across long sessions without losing context.
Same tools, same conversation. Only the provider line changes. Adding a new provider is ~200 lines implementing one behaviour.
{:ok, result} = Alloy.run("Summarize the README",
provider: {Alloy.Provider.Anthropic, model: "claude-sonnet-4-6"},
tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Bash],
max_turns: 10
)
Every agent is a GenServer. Supervisors restart them on failure. Conversations persist across crashes. State is isolated. This is what OTP was built for.
{:ok, agent} = Alloy.Agent.Server.start_link(
provider: {Alloy.Provider.Anthropic,
model: "claude-sonnet-4-6"},
tools: [Read, Write, Edit, Bash],
system_prompt: "You are a senior Elixir developer."
)
# Stateful conversation
{:ok, r1} = Alloy.Agent.Server.chat(agent,
"What does this project do?")
{:ok, r2} = Alloy.Agent.Server.chat(agent,
"Now refactor the main module")
Spin up teams of specialized agents under a supervisor. Delegate tasks, broadcast to all, or hand off between agents. Fault isolation means one failure doesn't take down the team.
{:ok, team} = Alloy.Team.start_link(
agents: [
researcher: [
provider: {Google, model: "gemini-2.5-flash"},
system_prompt: "You are a research assistant."
],
coder: [
provider: {Anthropic, model: "claude-sonnet-4-6"},
tools: [Read, Write, Edit],
system_prompt: "You are a senior developer."
]
]
)
{:ok, research} = Alloy.Team.delegate(
team, :researcher, "Find the latest Elixir patterns")
{:ok, code} = Alloy.Team.delegate(
team, :coder, "Implement those patterns")
Implement a behaviour. Hand it to the agent. Alloy handles parallel execution, error recovery, and provider-specific wire formatting. Ship with five built-in tools, or bring your own.
defmodule MyApp.Tools.WebSearch do
@behaviour Alloy.Tool
@impl true
def definition do
%{
name: "web_search",
description: "Search the web for current information",
input_schema: %{
type: "object",
properties: %{
query: %{type: "string", description: "Search query"}
},
required: ["query"]
}
}
end
@impl true
def execute(%{"query" => query}, _context) do
results = MyApp.Search.run(query)
{:ok, format_results(results)}
end
end
Every layer does one thing. No hidden state, no implicit wiring, no surprises. Read the source in an afternoon.
Alloy is deliberately narrow. It does one thing well and stays out of your way. Here's how to know if it fits.
Alloy is an engine, not a framework. Other agent frameworks ship 14 dependencies and tell you how to structure your application. Alloy ships 3 and gets out of the way. If an agent needs a capability it doesn't have, it writes the code itself. That's the whole idea.
Alloy is MIT licensed and developed on GitHub. Report bugs, request features, or contribute a provider — every PR makes the engine better for everyone.
Add one line to mix.exs
and you're ready.
def deps do
[
{:alloy, "~> 0.3.0"}
]
end
$ mix deps.get
$ mix alloy --provider anthropic --tools read,bash
Coming soon
Persistence, connectors, a dashboard, and a runtime for 24/7 agents. Built on Alloy. Built on Phoenix. Built on OTP.
Sign up for updates — or just star the repo and watch.