Hex.pm Docs License

Provider-agnostic agent loop for Elixir. Drop-in replacement for the Claude Code SDK that runs on Ollama, OpenAI-compatible endpoints (OpenAI, OpenRouter, LM Studio, vLLM, Groq, Together, llama.cpp server…), Google Gemini, or Anthropic Claude itself — with the same tools, hooks, permissions, and streaming semantics across every provider.

Status (v0.4): the operational-harness release. Builds on v0.3's loop kernel with file-based memory (AGENTS.md/CLAUDE.md), Claude Code-style skills (SKILL.md with progressive disclosure), a five-stage compaction pipeline with reactive recovery on context-window errors, 14 hook events with {:inject, msg} / {:transform, prompt} returns, five permission modes (:plan / :default / :accept_edits / :trusted / :bypass_permissions), structured tool results with rich UI payloads, custom agent definitions with optional git-worktree isolation, sidechain transcripts, and append-only session storage with file-checkpointing

  • /rewind. Backed by 248 tests.

See the v0.4.0 changelog for the full list and migration notes (one breaking change: 6 builtin tools now return {:ok, text, ui} 3-tuples).

Why

If you're using claude_code today and want to switch to a local Ollama model — or route per-task to OpenAI-compatible endpoints, or try Groq behind the same Elixir code — you don't want to rewrite every orchestrator. ExAthena is that abstraction layer. Pick a provider, run the same call, get back the same shape.

Install

The one-liner (Igniter auto-installs + writes sensible config):

mix igniter.install ex_athena

Or manually — add to mix.exs:

def deps do
  [
    {:ex_athena, "~> 0.4"},
    # optional — only needed for the Claude provider:
    {:claude_code, "~> 0.36"}
  ]
end

…then run mix ex_athena.install once to wire up defaults, or configure manually (see Configuration).

Quick start

# config/config.exs
config :ex_athena, default_provider: :ollama
config :ex_athena, :ollama, base_url: "http://localhost:11434", model: "llama3.1"

# anywhere
{:ok, response} = ExAthena.query("Tell me a joke")
IO.puts(response.text)

# streaming
ExAthena.stream("Explain quantum computing", fn event ->
  case event.type do
    :text_delta -> IO.write(event.data)
    :stop -> IO.puts("\n[done]")
    _ -> :ok
  end
end)

Swap the provider by changing one option:

ExAthena.query("hi", provider: :openai_compatible, model: "gpt-4o-mini")
ExAthena.query("hi", provider: :claude, model: "claude-opus-4-5")
ExAthena.query("hi", provider: :ollama, model: "qwen2.5-coder")
ExAthena.query("hi", provider: :gemini, model: "gemini-2.5-flash")

Providers

ProviderModuleNotes
:ollamaExAthena.Providers.ReqLLMLocal Ollama, /api/chat. Native tool-calls on modern models.
:openai_compatibleExAthena.Providers.ReqLLM/v1/chat/completions — covers OpenAI, OpenRouter, LM Studio, vLLM, Groq, Together, llama.cpp server mode, etc.
:openaiExAthena.Providers.ReqLLMAlias for :openai_compatible.
:llamacppExAthena.Providers.ReqLLMAlias for local llama.cpp server.
:claudeExAthena.Providers.ReqLLMAnthropic Claude via req_llm.
:geminiExAthena.Providers.ReqLLMGoogle Gemini via AI Studio (routed through req_llm's Google adapter). Native tool calls + streaming. See setup guide.
:mockExAthena.Providers.MockIn-memory test double.

Pass a custom module that implements ExAthena.Provider directly if you have an endpoint that doesn't fit the above.

Configuration

config :ex_athena,
  default_provider: :ollama

config :ex_athena, :ollama,
  base_url: "http://localhost:11434",
  model: "llama3.1"

config :ex_athena, :openai_compatible,
  base_url: "https://api.openai.com/v1",
  api_key: System.get_env("OPENAI_API_KEY"),
  model: "gpt-4o-mini"

config :ex_athena, :claude,
  api_key: System.get_env("ANTHROPIC_API_KEY"),
  model: "claude-opus-4-5"

Resolution is tiered — per-call opts always beat app env:

ExAthena.query("…",
  provider: :openai_compatible,          # overrides default_provider
  base_url: "https://openrouter.ai/api/v1",  # overrides :openai_compatible, base_url
  api_key: System.get_env("OPENROUTER_API_KEY"),
  model: "anthropic/claude-opus-4.1")

Tool calls

ExAthena.ToolCalls handles both protocols and auto-falls-back between them:

  • Native — OpenAI tool_calls arrays and Anthropic tool_use blocks. Parsed into canonical ExAthena.Messages.ToolCall structs.
  • TextTagged~~~tool_call {json} fenced blocks embedded in assistant prose, for models without native support.

The agent loop (Phase 2) will pick the protocol based on the provider's declared capabilities, and fall back when the model gets it wrong.

What's in v0.4

The "1.6% reasoning, 98.4% harness" upgrade — built around the Claude Code paper's observation that production agent value comes from the operational scaffolding, not the loop itself.

File-based context. Drop an AGENTS.md (or CLAUDE.md) at the project root and ex_athena prepends it as user-context on every turn. Drop a SKILL.md with YAML frontmatter under .exathena/skills/<name>/ and its description joins the system-prompt catalog at ~50 tokens; the body loads only when the model writes [skill: <name>]. See the memory + skills guide.

Five-stage compaction. The default Compactor.Pipeline runs cheapest-first: budget reduction (truncate huge tool results) → snip (drop stale ones) → microcompact (collapse runs of identical calls) → context collapse (read-time-only projection) → LLM summary. When a provider returns "context too long" the pipeline forces every stage and retries.

14-event hook surface. Every transition in the loop is observable + interceptable: SessionStart/End, UserPromptSubmit, ChatParams, Stop, StopFailure, all the *ToolUse* variants, PermissionRequest/Denied, Subagent*, three compaction events, Notification. Hooks can {:inject, msg} to add context or {:transform, prompt} to rewrite the user's message. See hooks reference.

Five permission modes. Add :accept_edits (auto-allow file edits, still prompt for bash) and :trusted (skip prompts; with optional respect_denylist: false for full YOLO) on top of the existing :plan / :default / :bypass_permissions. The denylist always wins, including in bypass — that's locked in a doctest. See permissions.

Subagents v2. Define custom agents in .exathena/agents/<name>.md with frontmatter (tools, permissions, mode, isolation); spawn by name via agent: "explore". Optional git-worktree isolation creates an isolated checkout per subagent (with safety checks + graceful fallback). Sidechain transcripts persist the full subagent conversation to disk so the parent only spends tokens on the final text. Three builtin definitions ship: general, explore, plan. See agents + subagents.

Storage + checkpoint. ExAthena.Sessions.Store is an append-only event log behaviour with two stores: in-memory (default) and ETS-buffered JSONL with periodic flush. Sessions emit :user_message / :assistant_message / :tool_result events; Session.resume/2 rebuilds the message history from any store. File-checkpoint snapshots fire before every Edit / Write, and Checkpoint.rewind/3 restores files + truncates the session log to a chosen UUID. See sessions + checkpoints.

Guides

License

Apache 2.0 — see LICENSE.