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

  • Agent loop — recursive User → Model ⇄ CallTools → End.
  • Model-agnostic providers — OpenAI, OpenRouter (OpenAI-Chat format), Anthropic + Z.AI/GLM (native Messages API), and an offline TestModel.
  • Toolsdeftool macro derives JSON Schema from :: type annotations + @doc; runs in parallel (Task.async_stream) with per-tool timeout & retry budget.
  • Structured output — any embedded_schema becomes the output spec; JSON Schema is derived and validated with a changeset, with retry-on-failure.
  • Streamingrun_stream/3 returns a lazy Stream of {:delta, text} / {:result, map} over real SSE (OpenAI + Anthropic).
  • Capabilities — composable middleware (before_model_request, after_tool_execute, …) via a behaviour with no-op defaults.
  • Production bits — supervised ExAgent.Finch pool, typed RequestError, UsageLimits safety net, and :telemetry events.

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

  • examples/demo.exs — offline loop with the TestModel (no API key).
  • examples/openrouter.exs — live tool-calling via OpenRouter.
  • examples/zai_anthropic.exs — live native Anthropic format via Z.AI.
  • examples/structured_output.exs — live structured output via Ecto.
  • examples/streaming.exs — live SSE streaming.

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

  • This library starts a supervised ExAgent.Finch HTTP pool in its Application, so it works out of the box. Tune pool size with config :exagent, :finch_pools, %{:default => [size: 32]}.
  • ExAgent does not shadow OTP's Agent unless you alias it as Agent. If you use both in the same file, keep the full name or choose a different alias.

License

MIT