An agent framework for Elixir — structured output, tool-calling, streaming, stateful agents, multi-agent sessions and durable persistence, powered by the BEAM.
ExAgent is layered and opt-in: use just the one-shot core, or stack on the
stateful runtime, persistence and coordination as you need them. It is built the
Elixir way — recursion, behaviours, Ecto changesets, cheap concurrency for tools,
supervision/durability, :telemetry, and events that plug straight into LiveView.
Layer 3 ExAgent.Session coordinated multi-agent turns + shared state
Layer 2 ExAgent.Store snapshots: resume after crash / restart
Layer 1 ExAgent.Server a supervised, stateful, event-emitting agent
Layer 0 ExAgent.run/3 the one-shot model ⇄ tools loop
──────────────────────── events (ExAgent.Event) over ExAgent.PubSubFeatures
- One-shot agentic loop — a model ⇄ tools recursion built as idiomatic Elixir.
- Type-derived tool schemas — define tools as plain functions; JSON Schema is
generated from
name :: Typeannotations and@docstrings (no hand-written schemas). - Structured output — any Ecto
embedded_schemabecomes the output spec; JSON Schema is derived from the schema and its changeset validations, validated with retry-on-failure. - Streaming — text deltas as a lazy stream for typewriter/chat UIs.
- Supervised stateful agents — keep history, accumulate usage, thread stateful models across runs, and emit versioned events over PubSub (LiveView-ready).
- Durable snapshots & resume — checkpoint after every run, rehydrate on restart; ETS by default, Postgres for multi-node durability (your DB, your repo).
- Multi-agent sessions — coordinated turns over shared state with pluggable
turn policies (
round_robin,initiative, or your own). - Orchestration — delegation (agent-as-tool) and hand-off between participants.
- Robustness & safety — context compaction, usage/cost limits, and per-tool
permissions (
allow/ask/deny). - Model-agnostic — OpenAI, OpenRouter, Anthropic and Z.AI built in; bring your
own by implementing the
ExAgent.Modelbehaviour. - External tools (MCP) — consume any Model Context Protocol server's tools.
- Observable —
:telemetryevents plus app-levelExAgent.Eventenvelopes. - Offline-first testing — a deterministic
ExAgent.Models.Testmodel drives the full loop with no API key and no network.
Requirements
- Elixir 1.17+
- Erlang/OTP 25+
Installation
Add :exagent to your list of dependencies in mix.exs:
def deps do
[{:exagent, "~> 1.0"}]
endThe library starts its own supervised ExAgent.Finch HTTP pool, a Registry
(ExAgent.PubSub.Local), a Task.Supervisor, an ExAgent.Store.ETS table and
an ExAgent.AgentSupervisor, so it works out of the box. Tune the Finch pool
with:
config :exagent, :finch_pools, %{:default => [size: 32]}
ExAgentdoes not shadow OTP'sAgentunless you alias it asAgent.
Quick start
The fastest way to try ExAgent is with Mix.install/2 (Livebook or a script) —
using the built-in ExAgent.Models.Test model, no API key needed:
Mix.install([
{:exagent, "~> 1.0"}
])
agent = ExAgent.new(model: "test", instructions: "Be concise.")
{:ok, %{output: text}} = ExAgent.run(agent, "Hello!")Point it at a real provider with a "provider:model" string:
agent = ExAgent.new(model: "openai:gpt-4o", instructions: "Be concise.")
{:ok, %{output: text}} = ExAgent.run(agent, "Hello!")
Summary
Functions
Build an agent from options. Use :output for the output spec.
Run the agent against prompt, returning {:ok, result} or {:error, _}.
Run synchronously and return the output value directly, raising on error.
Run the agent, returning a lazy stream of events as the model generates.
Types
@type output_type() :: :text | module()
@type result() :: %{ output: term(), messages: [ExAgent.Message.t()], new_messages: [ExAgent.Message.t()], usage: ExAgent.Message.Usage.t(), run_step: non_neg_integer(), model: ExAgent.Model.model() }
@type t() :: %ExAgent{ capabilities: [module() | struct()], instructions: [ExAgent.Message.Part.System.t()], max_steps: pos_integer(), model: ExAgent.Model.model(), name: String.t() | nil, output_retries: non_neg_integer(), output_type: output_type(), settings: ExAgent.ModelSettings.t(), tool_timeout: pos_integer(), tools: [ExAgent.Tool.t()], usage_limits: ExAgent.UsageLimits.t() | nil }
Functions
Build an agent from options. Use :output for the output spec.
Run the agent against prompt, returning {:ok, result} or {:error, _}.
Options:
:deps— dependency value threaded intoRunContext.:message_history— priorMessage.t()list to continue from.:model_settings— per-run settings overrides.
Run synchronously and return the output value directly, raising on error.
@spec run_stream(t(), String.t(), keyword()) :: Enumerable.t()
Run the agent, returning a lazy stream of events as the model generates.
This is the streaming variant. It yields:
{:delta, binary}— incremental output text (one per model text chunk),{:result, map}— the final result once the stream completes.
It is text-focused: best suited to chat / response-streaming UIs. Tool calls
emitted mid-stream are not re-executed in this path (use run/3 for full
agentic tool loops). The same per-run options as run/3 apply.