An Elixir client for provider-managed agent loops with locally-executed tools. The provider runs the agent loop; your custom tools execute on your node, so your code and data never leave it — the provider only ever sees each tool's name, description, input schema, and the text result you return.

One loop, two backends behind a single Provider behaviour:

ProviderModuleTransport
Anthropic Claude Managed Agents (public beta)ReqManagedAgents.Providers.ClaudeManagedAgents:streaming — long-lived SSE; beta header managed-agents-2026-04-01
AWS Bedrock AgentCore HarnessReqManagedAgents.Providers.BedrockAgentCore:request_response — synchronous SigV4-signed invoke

Install

def deps do
  [{:req_managed_agents, "~> 0.1"}]
end

Using the Bedrock AgentCore provider? Add the optional AWS deps (Anthropic-only users can skip these):

def deps do
  [
    {:req_managed_agents, "~> 0.1"},
    {:ex_aws_auth, "~> 1.4"},
    {:aws_event_stream, "~> 0.1"}
  ]
end

The core: one loop, the provider is a parameter

ReqManagedAgents.Session is the unified loop — invoke a turn → run your return-of-control tools locally → resume → repeat — parameterized by a provider module. It returns the same result shape for every provider:

alias ReqManagedAgents.Session
alias ReqManagedAgents.Providers.{ClaudeManagedAgents, BedrockAgentCore}

# Claude Managed Agents (streaming)
{:ok, %ReqManagedAgents.SessionResult{} = result} =
  Session.run(ClaudeManagedAgents,
    client: ReqManagedAgents.new(), agent_id: agent_id, environment_id: env_id,
    prompt: "…", handler: MyHandler)

result.terminal   # :end_turn | :requires_action | :terminated — uniform across providers
result.text       # the assistant's accumulated text
result.usage      # %ReqManagedAgents.Usage{input_tokens:, output_tokens:, …}

# AWS Bedrock AgentCore (request/response) — same handler, same result struct
{:ok, %ReqManagedAgents.SessionResult{}} =
  Session.run(BedrockAgentCore,
    harness_arn: arn, runtime_session_id: sid,
    prompt: "…", handler: MyHandler)

terminal is the uniform signal to branch on. stop_reason is each provider's raw native value (a map for Claude, e.g. %{"type" => "end_turn"}; a string for Bedrock, e.g. "end_turn") — preserved verbatim, never flattened. The raw events are always in events.

  • Sync: Session.run(provider, opts) blocks until a terminal and returns {:ok, …} / {:error, reason}.
  • Live / supervised: Session.start_link(provider, opts) (reconnecting, multi-turn) + Session.message(pid, text); pass notify: pid to be told when a turn terminates.

Convenience facade (Claude)

For the Claude path, thin sugar over the above:

For the Bedrock path, ReqManagedAgents.AgentCore.invoke_to_completion/1Session.run(BedrockAgentCore, opts).

Writing a handler

Implement ReqManagedAgents.Handlerhandle_tool_call/3 runs your tool locally and returns the text result; the optional handle_event/2 observes raw events as they stream.

defmodule MyHandler do
  @behaviour ReqManagedAgents.Handler

  @impl true
  def handle_tool_call("lookup_customer", %{"email" => email}, _ctx),
    do: {:ok, "Customer #{email}: Pro plan, active."}   # your private code + data

  @impl true
  def handle_event(_ev, _ctx), do: :ok
end

Three runnable, heavily-commented examples ship with the package:

  • examples/claude_managed_agents.exs — the full Claude lifecycle: agent + environment setup, a local tool handler, and the %SessionResult{} (text, terminal, token usage).
  • examples/bedrock_agent_core.exs — AgentCore Harness: provision/3 (idempotent, READY-polled), Session.run/2, teardown/2, and the AWS gotchas (session-id contract, cross-region model profiles, async deletion).
  • examples/provider_agnostic.exs — the core claim: one handler, one loop, two providers, same result shape.

The Claude pattern (setup)

  1. Create a versioned agent once (model, system prompt, custom-tool definitions); store its id.
  2. Create an environment once with Client.create_environment/2 and reuse its id (a session needs an environment_id).
  3. Start a session; the provider drives the loop and emits agent.custom_tool_use. The library runs your tool via the Handler callback and posts the result back. On end_turn, you're done.

The Bedrock AgentCore pattern (setup)

  1. Provision a Harness once — CreateHarness + READY-poll, idempotent and cached — via ReqManagedAgents.provision/3 (Provisioner.ensure/3 under the hood, built on ReqManagedAgents.AgentCore.Client). Store the returned handle; tear down with ReqManagedAgents.teardown/2.
  2. Session.run(BedrockAgentCore, harness_arn: …, runtime_session_id: …, …). Each turn is one synchronous signed invoke; resume re-sends the assistant toolUse + your toolResult delta. (runtimeSessionId must be ≥33 chars.)

Layers

Telemetry

req_managed_agents emits :telemetry events you can attach to:

EventMeasurementsMetadata
[:req_managed_agents, :request, :start | :stop | :exception]durationmethod, path, status
[:req_managed_agents, :agent_core, :request, :start | :stop | :exception]durationoperation, service, method, path, status
[:req_managed_agents, :stream, :connected | :event | :done | :error]session_id, type, usage, reason
[:req_managed_agents, :tool, :start | :stop | :exception]durationtool, session_id, is_error
[:req_managed_agents, :session, :tool_uses]tool_use_countturn, tool_use_ids
[:req_managed_agents, :session, :terminal]terminal

Both providers run through Session, so the :session events fire regardless of backend. Pass telemetry_metadata: %{…} to merge custom tags (e.g. tenant) into every event; library-set keys take precedence. ReqManagedAgents.OpenTelemetry bridges these to OTel GenAI spans.

Files (Claude)

{:ok, %{"id" => file_id}} = ReqManagedAgents.Client.upload_file(client, %{purpose: "agent", file: "report.csv"})
{:ok, _} = ReqManagedAgents.Client.attach_file_to_session(client, session_id, %{file_id: file_id, mount_path: "/data/report.csv"})
{:ok, bytes} = ReqManagedAgents.Client.download_file(client, file_id)

The Files API uses its own beta header (files-api-2025-04-14); download_file/2 returns raw bytes.

Using with Jido

The core is Jido-free. To use Jido Actions as tools, implement handle_tool_call/3 by delegating to Jido.Action.Tool.execute_action/3, and derive the tool definitions with Jido.Action.Tool.to_tool/1 (or ReqManagedAgents.ToolSchema.to_custom_tool/3). A dedicated adapter package is planned.

License

Apache-2.0.