GenAgentAnthropic
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:
- Talks HTTP, not a subprocess
- Has no tool use by default (pure text in/text out)
- Tracks conversation history in the session struct (the API is stateless; every request carries the full messages array)
- Is the simplest backend to use for HTTP-only workflows or when you do not want a CLI dependency
Prerequisites
You need an Anthropic API key. Set ANTHROPIC_API_KEY in your
environment, or pass :api_key as a backend option.
Installation
Add gen_agent using the current requirement in the core installation guide.
def deps do
[{:gen_agent_anthropic, "~> 0.5.0"}] # x-release-please-version
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_prompt: "You are a concise, helpful assistant.",
max_output_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.
When extended thinking is enabled, the backend retains the full assistant
content blocks, including signatures and redacted blocks, for the next turn;
the result event still exposes only the answer text.
Set :max_history_turns to a nonnegative integer to retain only that many
completed user/assistant pairs for the next request. The default,
:infinity, keeps the full history; 0 makes each turn independent. You can
also call GenAgent.reset_session(name) between turns to clear the history
without restarting the agent or discarding its application state. Reset
returns {:error, :busy} during an active turn.
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, incomplete response, or unexpected stop returns
{:error, %GenAgent.Backend.Error{}} when using a core version with that
struct. Its :kind distinguishes :refusal, :response_incomplete, and
:unexpected_stop_reason; :raw retains the prior tuple and stop details.
Older core versions still receive the tuple. This text-only backend cannot
complete a paused or tool-use turn.
HTTP failures use the same error struct. A 429 response has
kind: :rate_limited, retryable?: true, and the original Req.Response
in :raw, including headers. The :retry_after field preserves the
unparsed Retry-After header when present. A 200 response with an invalid
body has kind: :invalid_response, distinct from an exception raised by
the injected :http_fn.
Backend options
:api_key-- Anthropic API key. Defaults toSystem.get_env("ANTHROPIC_API_KEY"). Starting without a non-empty key returns{:error, :missing_api_key}(throughGenAgent.start_agent/2,{:error, {:backend_start_failed, :missing_api_key}}), unless a one-arity:http_fnis supplied.:base_url-- API prefix, default"https://api.anthropic.com/v1". The backend appends/messages. HTTPS is required except for loopback HTTP. The configured host receives the API key and conversation content.:api_version--anthropic-versionheader, default"2023-06-01".:headers-- additional headers as a string-keyed map or list of{name, value}pairs. Auth, version, content type, and transport headers cannot be overridden. Values must contain only printable ASCII or horizontal tabs.:request_fields-- extra JSON body fields such as%{temperature: 0.2}. For extended thinking, use a supported model withmax_output_tokens: 2048andrequest_fields: %{thinking: %{type: "enabled", budget_tokens: 1024}}. Backend-owned fields (messages, model, token limit, caching, streaming, and tools) cannot be overridden.:model-- model name. Defaults to"claude-sonnet-4-5".:max_output_tokens-- max tokens per turn. Defaults to1024.:max_tokensremains a deprecated alias.:system_prompt-- system prompt (string).:systemand:instructionsremain deprecated aliases.:cache-- opt in to automatic 5-minute prompt caching. Whentrue, each request includescache_control: %{type: "ephemeral"}. Defaults tofalsebecause single turns and idle conversations can incur cache-write cost without a later hit.Response.usageincludes:cache_creation_input_tokensand:cache_read_input_tokenswhen the API reports them; total input is their sum plus:input_tokens.:max_history_turns-- completed user/assistant pairs retained between turns. Defaults to:infinity;0retains none.:receive_timeout-- HTTP receive timeout in milliseconds. Defaults to60_000. The 60-second default can be short for long reasoning turns.:connect_timeout-- HTTP connect timeout in milliseconds. Defaults tonil(Req's default). Configure it when starting a named Finch pool;:connect_timeoutcannot be combined withfinch: [name: MyFinch].:finch-- per-session Finch options. Usefinch: [size: 100, pool_timeout: 20_000]for a dynamic pool, or start a Finch child with a larger pool and passfinch: [name: MyFinch].:countis also supported for dynamic pools;:pool_timeoutis accepted with either kind. Req's default HTTP/1 pool has 50 connections per host and a 5-second checkout timeout. Named pools set connection options at startup; pool:sizeand:countcannot be combined with:name. Dynamic pools with identical pool configuration are shared across sessions;:sizelimits connections per shard.:http_fn-- a 1-arity function(request_map) -> {:ok, response_map} | {:error, term}that replaces the defaultReq-backed HTTP call. Intended for tests that want to stub out the API.
See GenAgent.Backends.Anthropic for the full module docs.
Unknown keys fail session startup with {:unknown_option, key}; conflicting
values supplied through aliases fail with {:conflicting_options, keys}.
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.