AshAgent

Hex.pmLicense: MIT

Pre-1.0 Release - API may change between minor versions. Pin to specific versions in production.

Production AI agents for Elixir. AshAgent builds on Ash Framework to give you durable state, authorization, and declarative agent definitions—without locking you into any specific LLM provider.

Installation

def deps do
  [
    {:ash_agent, "~> 0.1.0"}
  ]
end

Quick Start

1. Define an Agent Resource

defmodule MyApp.Assistant do
  use Ash.Resource,
    domain: MyApp.Agents,
    extensions: [AshAgent.Resource]

  agent do
    client "anthropic:claude-sonnet-4-20250514"

    instruction ~p"""
    You are a helpful assistant for {{ company_name }}.
    """

    instruction_schema Zoi.object(%{
      company_name: Zoi.string(description: "Name of the company the assistant represents")
    }, coerce: true)

    input_schema Zoi.object(%{
      message: Zoi.string(description: "The user's question or request")
    }, coerce: true)

    output_schema Zoi.object(%{
      content: Zoi.string(description: "The assistant's helpful response")
    }, coerce: true)
  end

  code_interface do
    define :call, args: [:context]
    define :stream, args: [:context]
  end
end

2. Configure Your Domain

defmodule MyApp.Agents do
  use Ash.Domain

  resources do
    resource MyApp.Assistant
  end
end

3. Call Your Agent

AshAgent uses a context-based API for building conversations:

# Build context with instruction and user message
context =
  [
    MyApp.Assistant.instruction(company_name: "Acme Corp"),
    MyApp.Assistant.user(message: "Hello!")
  ]
  |> MyApp.Assistant.context()

# Call the agent
{:ok, result} = MyApp.Assistant.call(context)
result.output.content
#=> "Hello! How can I help you today?"

# For multi-turn conversations, reuse the context from the result
new_context =
  [
    result.context,
    MyApp.Assistant.user(message: "What's the weather?")
  ]
  |> MyApp.Assistant.context()

{:ok, result2} = MyApp.Assistant.call(new_context)

Streaming Responses

context =
  [
    MyApp.Assistant.instruction(company_name: "Acme Corp"),
    MyApp.Assistant.user(message: "Tell me a story")
  ]
  |> MyApp.Assistant.context()

{:ok, stream} = MyApp.Assistant.stream(context)

Enum.each(stream, fn chunk ->
  IO.write(chunk.content)
end)

Agentic Loops

Build autonomous agents that loop until a task is complete. Use Zoi.union with discriminated types to define the possible outputs—each variant has its own required fields:

defmodule MyApp.LoopAgent do
  use Ash.Resource,
    domain: MyApp.Agents,
    extensions: [AshAgent.Resource]

  agent do
    client "anthropic:claude-sonnet-4-20250514"

    instruction ~p"""
    Help the user by searching for information when needed.
    Choose one of the response types based on your next action.
    """

    input_schema Zoi.object(%{
      message: Zoi.string(description: "The user's question")
    }, coerce: true)

    output_schema Zoi.union([
      Zoi.object(%{
        intent: Zoi.literal("search"),
        query: Zoi.string(description: "Search query to find information")
      }, coerce: true),
      Zoi.object(%{
        intent: Zoi.literal("done"),
        answer: Zoi.string(description: "Final answer to the user's question")
      }, coerce: true)
    ])
  end
end

# The agentic loop
defmodule MyApp.AgentRunner do
  def run(question) do
    context = [MyApp.LoopAgent.user(message: question)] |> MyApp.LoopAgent.context()
    loop(context)
  end

  defp loop(context) do
    {:ok, result} = MyApp.LoopAgent.call(context)

    case result.output do
      %{intent: "done", answer: answer} ->
        {:ok, answer}

      %{intent: "search", query: query} ->
        search_result = perform_search(query)
        new_context = [result.context, MyApp.LoopAgent.user(message: search_result)]
                      |> MyApp.LoopAgent.context()
        loop(new_context)
    end
  end

  defp perform_search(query), do: "Results for: #{query}"
end

# Run it
{:ok, answer} = MyApp.AgentRunner.run("What's the weather in Tokyo?")

The Zoi.union converts to JSON Schema anyOf, clearly showing the LLM valid output shapes. Each variant is type-safe—query is required for "search", answer is required for "done".

Generated Functions

AshAgent generates these functions on your agent module:

DSL Reference

agent Section

Option Type Required Description
client string/atom Yes LLM provider and model (e.g., "anthropic:claude-sonnet-4-20250514")
instruction string/template Depends System instruction template. Use ~p sigil for Liquid templates. Required unless provider declares :prompt_optional.
instruction_schema Zoi schema No Zoi schema for instruction template arguments
input_schema Zoi schema Yes Zoi schema for user message validation
output_schema Zoi schema Yes Zoi schema for output validation and structured output enforcement
provider atom No LLM provider (:req_llm default, :baml, or custom module)
hooks module No Module implementing AshAgent.Runtime.Hooks behaviour
token_budget integer No Maximum tokens for agent execution
budget_strategy:halt or :warn No How to handle budget limits (default: :warn)

Structured output is handled automatically by the provider—Zoi schemas are compiled to JSON Schema and passed to the LLM API.

Provider Options

AshAgent supports multiple LLM providers through an abstraction layer.

ReqLLM (Default)

agent do
  provider :req_llm
  client "anthropic:claude-sonnet-4-20250514", temperature: 0.7, max_tokens: 1000
  instruction "You are a helpful assistant."
  input_schema Zoi.object(%{
    message: Zoi.string(description: "The user's message")
  }, coerce: true)
  output_schema Zoi.object(%{
    content: Zoi.string(description: "The assistant's response")
  }, coerce: true)
end

BAML (Optional)

For structured outputs via ash_baml:

agent do
  provider :baml
  client :my_client, function: :ChatAgent
  instruction "Prompt defined in BAML"
  input_schema Zoi.object(%{
    message: Zoi.string(description: "The user's message")
  }, coerce: true)
  output_schema MyBamlTypes.ChatReply
end

Custom Providers

Register custom providers in config:

config :ash_agent,
  providers: [
    custom: MyApp.CustomProvider
  ]

Generated Actions

AshAgent automatically generates two actions on your resource:

These integrate with Ash's action system, enabling authorization policies, preparations, and all standard Ash action features.

Telemetry

AshAgent emits telemetry events for observability:

Development

mix test
mix format
mix credo --strict
mix dialyzer

License

MIT License - see LICENSE for details.

Related Packages

AshAgent is part of the AshAgent Stack ecosystem:

Links