ExAgent (ExAgent v1.2.0)

Copy Markdown View Source

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.PubSub

Features

  • 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 :: Type annotations and @doc strings (no hand-written schemas).
  • Structured output — any Ecto embedded_schema becomes 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.Model behaviour.
  • External tools (MCP) — consume any Model Context Protocol server's tools.
  • Observable:telemetry events plus app-level ExAgent.Event envelopes.
  • Offline-first testing — a deterministic ExAgent.Models.Test model 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"}]
end

The 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]}

ExAgent does not shadow OTP's Agent unless you alias it as Agent.

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

output_type()

@type output_type() :: :text | module()

result()

@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()
}

t()

@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

new(opts)

@spec new(keyword()) :: t()

Build an agent from options. Use :output for the output spec.

run(agent, prompt, opts \\ [])

@spec run(t(), String.t(), keyword()) :: {:ok, result()} | {:error, term()}

Run the agent against prompt, returning {:ok, result} or {:error, _}.

Options:

  • :deps — dependency value threaded into RunContext.
  • :message_history — prior Message.t() list to continue from.
  • :model_settings — per-run settings overrides.

run!(agent, prompt, opts \\ [])

@spec run!(t(), String.t(), keyword()) :: term()

Run synchronously and return the output value directly, raising on error.

run_stream(agent, prompt, opts \\ [])

@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.