Packages
Provider-agnostic Elixir client for agent runtimes — one Session loop, any loop host: server-side (Anthropic Claude Managed Agents, AWS Bedrock AgentCore) or in-process (Local, over any OpenAI-compatible chat endpoint). Your tools run locally.
Current section
Files
Jump to
Current section
Files
examples/claude_managed_agents.exs
# Claude Managed Agents: the full lifecycle, end to end.
#
# The headline pattern of this library: **the provider runs the agent loop;
# your custom tools execute here, on your node**. Anthropic only ever sees each
# tool's name, description, input schema, and the text result you return —
# your code and data never leave this process.
#
# What this script does:
#
# 1. provisions a versioned agent + its environment in one call
# (`provision/3` — model + system prompt + custom-tool schema, plus the
# environment the session runs in)
# 2. asks it a question that requires the `lookup_customer` tool
# 3. Claude runs the loop server-side; when it calls the tool, control
# returns HERE — `Demo.Handler.handle_tool_call/3` runs locally — and the
# text result is posted back to resume the loop
# 4. prints the final `%ReqManagedAgents.SessionResult{}`: assistant text,
# terminal, and token usage
#
# Run it:
#
# ANTHROPIC_API_KEY=sk-ant-... mix run examples/claude_managed_agents.exs
#
# Cost: one short Haiku-class conversation (a fraction of a cent on the
# default model below). Agents and environments are free to create and
# persist until deleted.
defmodule Demo.Handler do
@moduledoc false
@behaviour ReqManagedAgents.Handler
# Called whenever the agent invokes one of YOUR custom tools. This is the
# return-of-control seam: the function body is private to your node — query
# your database, call internal services, whatever. Only the returned text
# goes back to the provider. Return `{:error, text}` to post a tool failure
# (the agent sees it flagged `is_error` and can recover).
@impl true
def handle_tool_call("lookup_customer", %{"email" => email}, _ctx) do
# Pretend this is your private DB. Claude never sees this code or the row.
{:ok, "Customer #{email}: Pro plan, active, last invoice $49.00 on 2026-05-01."}
end
# Optional: observe every streamed event as the loop runs (assistant
# messages, tool activity, lifecycle). Great for logging/UX; return :ok.
@impl true
def handle_event(%{"type" => "agent.message"} = ev, _ctx) do
IO.puts("assistant> #{inspect(ev["content"])}")
:ok
end
def handle_event(_ev, _ctx), do: :ok
end
{:ok, _} = Application.ensure_all_started(:req_managed_agents)
alias ReqManagedAgents.Agent.Spec
alias ReqManagedAgents.Providers.ClaudeManagedAgents
# The control-plane client. Reads ANTHROPIC_API_KEY from the environment by
# default; see `ReqManagedAgents.Client.new/1` for explicit options.
client = ReqManagedAgents.new()
# ── 1. Provision the agent + environment (idempotent; cached per {provider, spec}) ──
#
# The same shape as the Bedrock example: build an `%Agent.Spec{}` — name,
# system prompt, and the SCHEMAS of your custom tools (never their
# implementations) — and hand it to `ReqManagedAgents.provision/3`, with the
# environment passed as the `:environment` option (an `Environment.Spec`, or a
# flat map that coerces to one — its `config` is passed verbatim to the wire
# environment field). For Claude Managed Agents the model config is a plain
# model-id string. `provision/3` creates the versioned agent resource and its
# environment, returning ONE handle carrying both ids. Store it in a real app.
agent_spec = %Spec{
name: "billing-support",
system_prompt: "You are a concise billing-support agent. Use tools for customer data.",
model_config: "claude-haiku-4-5",
tools: [
%{
type: "custom",
name: "lookup_customer",
description:
"Look up a customer by email and return plan, status, and last invoice. " <>
"Always call this for customer data; never guess.",
input_schema: %{
"type" => "object",
"properties" => %{"email" => %{"type" => "string"}},
"required" => ["email"]
}
}
]
}
env_spec = %{type: "cloud", networking: %{type: "unrestricted"}}
{:ok, handle} =
ReqManagedAgents.provision(ClaudeManagedAgents, agent_spec,
client: client,
environment: env_spec
)
# handle == %{agent_id: "agent_id_…", environment_id: "env_id_…"}
IO.puts("provisioned agent #{handle.agent_id}")
# ── 2. Run a session to completion ──────────────────────────────────────────
#
# `Session.run/2` blocks until the agent reaches a terminal state and returns
# `{:ok, %ReqManagedAgents.SessionResult{}}`. The provision handle carries both
# ids; `:agent`/`:environment` accept it directly (each lifts the id it needs),
# so you never hand-thread raw ids.
#
# For a long-lived, supervised, reconnecting session (a chat), use
# `ReqManagedAgents.start_session/1` + `ReqManagedAgents.Session.message/2`
# instead — same opts, same handler. `ReqManagedAgents.run_to_completion/1` is
# the Claude convenience alias for `Session.run(ClaudeManagedAgents, opts)`.
{:ok, result} =
ReqManagedAgents.Session.run(ClaudeManagedAgents,
client: client,
agent: handle,
environment: handle,
prompt: "What plan is jane@acme.com on, and when was she last billed?",
handler: Demo.Handler
)
# ── 3. The result: one canonical shape, whatever the provider ───────────────
#
# result.terminal — :end_turn | :requires_action | :terminated
# result.text — the assistant's accumulated text
# result.usage — %ReqManagedAgents.Usage{input_tokens:, output_tokens:, ...}
# result.turns — how many loop turns the provider ran
# result.custom_tool_uses — every return-of-control tool call it made
# result.events — the raw provider events, if you need them
IO.puts("""
terminal: #{result.terminal}
turns: #{result.turns}
tokens: #{result.usage.input_tokens} in / #{result.usage.output_tokens} out
tools: #{Enum.map_join(result.custom_tool_uses, ", ", & &1.name)}
#{result.text}
""")