ExAthena

Hex.pmDocsLicense

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…), 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

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")

Providers

Provider Module Notes
:ollamaExAthena.Providers.Ollama Local Ollama, /api/chat. Native tool-calls on modern models.
:openai_compatibleExAthena.Providers.OpenAICompatible/v1/chat/completions — covers OpenAI, OpenRouter, LM Studio, vLLM, Groq, Together, llama.cpp server mode, etc.
:openaiExAthena.Providers.OpenAICompatible Alias.
:llamacppExAthena.Providers.OpenAICompatible Alias.
:claudeExAthena.Providers.Claude Wraps the claude_code SDK. Preserves hooks, MCP, session resume.
:mockExAthena.Providers.Mock In-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:

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.