Alloy - a minimal, OTP-native AI agent engine for Elixir

I’ve been building AI agents in Elixir for a while and kept running into the same frustration, every framework either locks you into one provider, ships fifteen dependencies you don’t need, or treats Elixir as a second-class citizen behind Python.

So I built Alloy. It’s a minimal agent engine — the completion → tool-call loop and nothing else. You give it a prompt, a provider, and some tools, and it runs until the job is done.

  {:ok, result} = Alloy.run(
    "Read mix.exs and tell me the version",
    provider: {Alloy.Provider.Anthropic,
      api_key: System.get_env("ANTHROPIC_API_KEY"),
      model: "claude-sonnet-4-6"},
    tools: [Alloy.Tool.Core.Read]
  )

What’s in the box:

  • 8 providers — Anthropic, OpenAI, Google Gemini, Ollama, OpenRouter, xAI, DeepSeek, Mistral. Swap in one line, nothing else changes.
  • 3 dependencies — jason, req, telemetry. That’s it.
  • GenServer agents — Alloy.Agent.Server wraps the loop in a supervised process. Stateful, long-running, message-passing.
  • Multi-agent teams — Alloy.Team gives you delegate, broadcast, and handoff between named agents. Each is its own supervised process — one crashes, the others keep
    going.
  • Streaming — token-by-token from any provider, same interface.
  • Async dispatch — send_message/2 fires non-blocking and broadcasts the result via PubSub. Designed for Phoenix LiveView.
  • Middleware — plug in telemetry, logging, or custom hooks before/after completions and tool calls.
  • Context discovery — drop .md files in .alloy/context/ and they’re auto-injected into the system prompt.

The philosophy is close to pi-agent: what you leave out matters more than what you put in. If an agent needs a capability Alloy doesn’t ship, the agent writes the code itself.

The OTP angle is the part I’m most interested in feedback on. Agents as supervised GenServers isn’t a pattern you find in Python-based frameworks — it’s architecturally native to the BEAM, not bolted on. Fault isolation, message passing, and real concurrency (not coroutines) come for free.

{: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],
        system_prompt: "You are a senior developer."
      ]
    ]
  )

  {:ok, research} = Alloy.Team.delegate(team, :researcher,
    "Find the latest Elixir 1.18 changes")
  {:ok, _} = Alloy.Team.delegate(team, :coder,
    "Update our code based on: #{research.text}")

Available now on Hex as {:alloy, “~> 0.4”}.

Happy to answer questions or take PRs. Particularly interested in feedback on the Team API and whether the provider behaviour interface makes contributing new providers straightforward.

Personally, I think Elixir and OTP is the ideal architecture for agentic systems.

16 Likes