ReqManagedAgents

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.

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:

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.