GenAgentAnthropic

CI Hex.pm Docs

The package source and new issues live in genagent/gen_agent/integrations/anthropic. The former repository retains historical releases and discussions.

HTTP-direct Anthropic backend for GenAgent, built on Req.

Provides GenAgent.Backends.Anthropic, which talks directly to the Anthropic Messages API and translates the response into the normalized GenAgent.Event values the state machine consumes.

Unlike the CLI-backed backends (gen_agent_claude, gen_agent_codex), this backend:

Prerequisites

You need an Anthropic API key. Set ANTHROPIC_API_KEY in your environment, or pass :api_key as a backend option.

Installation

def deps do
  [
    {:gen_agent, "~> 0.3.0"},
    {:gen_agent_anthropic, "~> 0.2.0"}
  ]
end

Quick start

defmodule MyApp.Assistant do
  use GenAgent

  defmodule State do
    defstruct responses: []
  end

  @impl true
  def init_agent(_opts) do
    backend_opts = [
      system: "You are a concise, helpful assistant.",
      max_tokens: 512
    ]

    {:ok, backend_opts, %State{}}
  end

  @impl true
  def handle_response(_ref, response, state) do
    {:noreply, %{state | responses: state.responses ++ [response.text]}}
  end
end

{:ok, _pid} = GenAgent.start_agent(MyApp.Assistant,
  name: "my-assistant",
  backend: GenAgent.Backends.Anthropic
)

{:ok, response} = GenAgent.ask("my-assistant", "Explain OTP gen_statem in one sentence.")
IO.puts(response.text)

Session continuation

The Anthropic API is stateless -- every request carries the full messages array. This backend tracks the conversation history on the session struct so multi-turn conversations work transparently:

# Turn 1: fresh conversation
{:ok, r1} = GenAgent.ask("my-assistant", "Remember the number 42")
# Turn 2: backend sends the full history including turn 1
{:ok, r2} = GenAgent.ask("my-assistant", "What number did I ask you to remember?")
# r2.text == "42"

Conversation history lives in session.messages as an in-order list of %{role: ..., content: ...} maps with atom keys. The user message is included in the API request. The assistant message is appended when the terminal :result event carries non-blank text. If that text is empty or only whitespace, the unanswered user message is removed. Refusals and incomplete responses also leave prior history intact, so a later turn does not resend a failed prompt.

end_turn and stop_sequence are successful stops. Their terminal event data includes :stop_reason and, when the API provides it, :stop_details; callers can read them through response.terminal.data. A refusal returns {:error, {:refusal, stop_details}}. A max_tokens or model_context_window_exceeded stop returns {:error, {:response_incomplete, %{stop_reason: reason, stop_details: details}}}. Other stops are rejected as {:error, {:unexpected_stop_reason, reason}} because this text-only backend cannot complete a paused or tool-use turn.

Backend options

See GenAgent.Backends.Anthropic for the full module docs.

Why no tool use?

This backend is deliberately minimal: text in, text out. Anthropic's Messages API supports tool use, but adding it means a richer event surface, tool schema definitions, and roundtripping tool results -- all of which is better served by the Claude CLI backend (gen_agent_claude), which gets that flow from Claude Code itself.

If you want tool-using agents with Anthropic as the provider, reach for gen_agent_claude. If you want a thin HTTP client for single-turn or multi-turn text exchanges, this is the right backend.

Testing your agent

Return :http_fn in the backend options from your agent's init_agent/1 (for example, {:ok, Keyword.take(opts, [:http_fn]), initial_state}). Then application tests can use a canned response with no API key or HTTP calls:

body = %{
  "id" => "msg_test", "model" => "test-model", "stop_reason" => "end_turn",
  "content" => [%{"type" => "text", "text" => "hi"}],
  "usage" => %{"input_tokens" => 1, "output_tokens" => 1}
}

{:ok, _pid} = GenAgent.start_agent(MyApp.Assistant,
  name: "test-assistant",
  backend: GenAgent.Backends.Anthropic,
  http_fn: fn %{body: _request_body} -> {:ok, body} end
)

{:ok, %GenAgent.Response{text: "hi"}} = GenAgent.ask("test-assistant", "hello")
:ok = GenAgent.stop("test-assistant")

The stub receives a request map and returns the decoded response body. See the keyless primitive examples for callback assertions.

Testing

mix test

Unit tests stub the HTTP layer via the :http_fn backend option, so no tokens are burned during mix test.

Live tests (tagged :live) hit the real API and require ANTHROPIC_API_KEY in the environment:

mix test --only live

License

MIT. See LICENSE.