ExAgent

An agent framework for Elixir — structured output, tool-calling and streaming for LLMs, powered by the BEAM. Built the Elixir way: recursion, behaviours, Ecto changesets, concurrent tool execution, supervision and telemetry.

Why

Python agent libraries are delightful when types + validation + an agentic loop work together. ExAgent brings that ergonomics (type-derived tool schemas, structured output with retry, model-agnostic agents) to Elixir, while leaning on BEAM strengths: cheap concurrency for tools, supervision/durability, :telemetry, and streaming that plugs straight into LiveView.

Features

Quick start

def deps do
[{:exagent, "~> 0.1.0"}]
end
alias ExAgent
agent = ExAgent.new(model: "test", instructions: "Be concise.")
{:ok, %{output: text}} = ExAgent.run(agent, "Hello!")

Set OPENAI_API_KEY before using openai:* models.

Tools with derived schemas

defmodule MyApp.Tools do
use ExAgent.Tools
@doc "Get the weather for a city."
deftool get_weather(ctx, city :: String.t(), days :: integer()) do
{:ok, "#{city}: sunny"}
end
end
agent = ExAgent.new(model: "openai:gpt-4o", tools: MyApp.Tools.tools())

Structured output

defmodule WeatherReport do
use Ecto.Schema
embedded_schema do
field :city, :string
field :temp_c, :float
field :condition, Ecto.Enum, values: [:sunny, :rainy, :cloudy]
end
def changeset(s, a) do
s |> Ecto.Changeset.cast(a, [:city, :temp_c, :condition])
|> Ecto.Changeset.validate_required([:city, :temp_c])
end
end
agent = ExAgent.new(model: "anthropic:claude-3-5-haiku", output: WeatherReport)
{:ok, %{output: %WeatherReport{city: "Madrid", temp_c: 22.0, condition: :sunny}}} =
ExAgent.run(agent, "It's 22 and sunny in Madrid")

Streaming

ExAgent.run_stream(agent, "count to five")
|> Stream.each(fn
{:delta, t} -> IO.write(t)
{:result, %{usage: u}} -> IO.puts("\n#{u.output_tokens} tokens")
end)
|> Stream.run()

Persistence / durable runs

The framework is DB-free: it doesn't own a database or job queue. What it does provide is best-effort message-history serialization, so you can persist a conversation anywhere (Postgres/Redis/ETS/file) and resume it later:

alias ExAgent.Message
json = Message.to_json(result.messages) # store this
{:ok, history} = Message.from_json(json) # load it back later
ExAgent.run(agent, "follow up", message_history: history)

For crash-safe, resumable runs, wrap ExAgent.run in an Oban job in your app — see examples/durable_oban.exs for a copy-paste recipe (idempotency keys, checkpoints, retries). Approval workflows can be coordinated in your app around persisted history. Durability is an application concern, so the library doesn't force Oban/Postgres on you.

Models

Resolve from a string or pass a struct:

ExAgent.new(model: "openai:gpt-4o")
ExAgent.new(model: "openrouter:deepseek/deepseek-v4-flash")
ExAgent.new(model: "anthropic:claude-3-5-haiku-20241022")
# Z.AI's Anthropic-compatible endpoint (GLM models), needs ZAI_API_KEY:
ExAgent.new(model: "zai:glm-4.5-air")

Examples

Status

Early, feature-complete MVP for the core agent loop. Implemented & verified against live providers; see the test suite (run mix test).

Notes for host apps

License

MIT