Alloy

Hex.pmCIDocsLicense

Model-agnostic agent harness for Elixir.

Alloy gives you the agent loop: send messages to any LLM, execute tool calls, loop until done. Swap providers with one line. Run agents as supervised GenServers. Build multi-agent teams with fault isolation. Zero framework lock-in.

{:ok, result} = Alloy.run("Read mix.exs and tell me the version",
provider: {Alloy.Provider.OpenAI, api_key: System.get_env("OPENAI_API_KEY"), model: "gpt-5.2"},
tools: [Alloy.Tool.Core.Read]
)
result.text #=> "The version is 0.1.0"

Why Alloy?

Most agent frameworks target Python and single-script usage. Alloy targets what happens after — when you need agents running in production with supervision, fault isolation, concurrency, and multi-agent orchestration.

Installation

Add alloy to your dependencies in mix.exs:

def deps do
[
{:alloy, "~> 0.1.0"}
]
end

Quick Start

Simple completion

{:ok, result} = Alloy.run("What is 2+2?",
provider: {Alloy.Provider.Anthropic, api_key: "sk-ant-...", model: "claude-sonnet-4-6"}
)
result.text #=> "4"

Agent with tools

{:ok, result} = Alloy.run("Read mix.exs and summarize the dependencies",
provider: {Alloy.Provider.Google, api_key: "...", model: "gemini-2.5-flash"},
tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Bash],
max_turns: 10
)

Swap providers in one line

# The same tools and conversation work with any provider
opts = [tools: [Alloy.Tool.Core.Read], max_turns: 10]
# Anthropic
Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"}} | opts])
# OpenAI
Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.OpenAI, api_key: "...", model: "gpt-5.2"}} | opts])
# Local (Ollama — no API key needed)
Alloy.run("Read mix.exs", [{:provider, {Alloy.Provider.Ollama, model: "llama4"}} | opts])

Supervised GenServer agent

{:ok, agent} = Alloy.Agent.Server.start_link(
provider: {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"},
tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Edit, Alloy.Tool.Core.Bash],
system_prompt: "You are a senior Elixir developer."
)
{:ok, response} = Alloy.Agent.Server.chat(agent, "What does this project do?")
{:ok, response} = Alloy.Agent.Server.chat(agent, "Now refactor the main module")

Multi-agent teams

{:ok, team} = Alloy.Team.start_link(
agents: [
researcher: [
provider: {Alloy.Provider.Google, api_key: "...", model: "gemini-2.5-flash"},
system_prompt: "You are a research assistant."
],
coder: [
provider: {Alloy.Provider.Anthropic, api_key: "...", model: "claude-sonnet-4-6"},
tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Write, Alloy.Tool.Core.Edit],
system_prompt: "You are a senior developer."
]
]
)
# Delegate tasks to specific agents
{:ok, research} = Alloy.Team.delegate(team, :researcher, "Find the latest Elixir release notes")
{:ok, code} = Alloy.Team.delegate(team, :coder, "Write a GenServer that #{research.text}")

Providers

ProviderModuleAPI Key Env VarExample Model
AnthropicAlloy.Provider.AnthropicANTHROPIC_API_KEYclaude-sonnet-4-6
OpenAIAlloy.Provider.OpenAIOPENAI_API_KEYgpt-5.2
Google GeminiAlloy.Provider.GoogleGEMINI_API_KEYgemini-2.5-flash
OllamaAlloy.Provider.Ollama(none — local)llama4
OpenRouterAlloy.Provider.OpenRouterOPENROUTER_API_KEYanthropic/claude-sonnet-4-6
xAI (Grok)Alloy.Provider.XAIXAI_API_KEYgrok-3
DeepSeekAlloy.Provider.DeepSeekDEEPSEEK_API_KEYdeepseek-chat
MistralAlloy.Provider.MistralMISTRAL_API_KEYmistral-large-latest

Adding a provider is ~200 lines implementing the Alloy.Provider behaviour.

CLI

Alloy ships with an interactive REPL:

# Interactive mode
mix alloy
mix alloy --provider gemini --tools read,bash
# One-shot mode
mix alloy -p "What is the capital of France?"
mix alloy -p "Read mix.exs" --tools read --provider openai

Built-in Tools

ToolModuleDescription
readAlloy.Tool.Core.ReadRead files from disk
writeAlloy.Tool.Core.WriteWrite files to disk
editAlloy.Tool.Core.EditSearch-and-replace editing
bashAlloy.Tool.Core.BashExecute shell commands
scratchpadAlloy.Tool.Core.ScratchpadPersistent key-value notepad

Custom tools

defmodule MyApp.Tools.WebSearch do
@behaviour Alloy.Tool
@impl true
def definition do
%{
name: "web_search",
description: "Search the web for information",
input_schema: %{
type: "object",
properties: %{query: %{type: "string", description: "Search query"}},
required: ["query"]
}
}
end
@impl true
def execute(%{"query" => query}, _context) do
# Your implementation here
{:ok, "Results for: #{query}"}
end
end

Architecture

Alloy.run/2 One-shot agent loop (pure function)
Alloy.Agent.Server GenServer wrapper (stateful, supervisable)
Alloy.Team Multi-agent supervisor (delegate, broadcast, handoff)
Alloy.Agent.Turn Single turn: call provider → execute toolsreturn
Alloy.Provider Behaviour: translate wire format ↔ Alloy.Message
Alloy.Tool Behaviour: definition + execute
Alloy.Middleware Pipeline: logger, telemetry, custom hooks
Alloy.Context.Compactor Automatic conversation summarization
Alloy.Scheduler Cron/heartbeat for recurring agent runs

License

MIT — see LICENSE.