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:
| Provider | Module | Transport |
|---|---|---|
| Anthropic Claude Managed Agents (public beta) | ReqManagedAgents.Providers.ClaudeManagedAgents | :streaming — long-lived SSE; beta header managed-agents-2026-04-01 |
| AWS Bedrock AgentCore Harness | ReqManagedAgents.Providers.BedrockAgentCore | :request_response — synchronous SigV4-signed invoke |
Install
def deps do
[{:req_managed_agents, "~> 0.1"}]
endUsing 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"}
]
endThe 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); passnotify: pidto be told when a turn terminates.
Convenience facade (Claude)
For the Claude path, thin sugar over the above:
ReqManagedAgents.run_to_completion/1≡Session.run(ClaudeManagedAgents, opts)ReqManagedAgents.start_session/1≡Session.start_link(ClaudeManagedAgents, opts)ReqManagedAgents.new/1— a control-plane client.
For the Bedrock path, ReqManagedAgents.AgentCore.invoke_to_completion/1 ≡
Session.run(BedrockAgentCore, opts).
Writing a handler
Implement ReqManagedAgents.Handler — handle_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
endThree 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)
- Create a versioned agent once (model, system prompt, custom-tool definitions); store its id.
- Create an environment once with
Client.create_environment/2and reuse its id (a session needs anenvironment_id). - Start a session; the provider drives the loop and emits
agent.custom_tool_use. The library runs your tool via theHandlercallback and posts the result back. Onend_turn, you're done.
The Bedrock AgentCore pattern (setup)
- Provision a Harness once — CreateHarness + READY-poll, idempotent and cached — via
ReqManagedAgents.provision/3(Provisioner.ensure/3under the hood, built onReqManagedAgents.AgentCore.Client). Store the returned handle; tear down withReqManagedAgents.teardown/2. Session.run(BedrockAgentCore, harness_arn: …, runtime_session_id: …, …). Each turn is one synchronous signed invoke; resume re-sends the assistanttoolUse+ yourtoolResultdelta. (runtimeSessionIdmust be ≥33 chars.)
Layers
ReqManagedAgents.Provider— the behaviour every backend implements (invocation +normalize/1).ReqManagedAgents.Session— the unified, supervised, reconnecting loop driven by yourHandler.ReqManagedAgents.Client— Claude control-plane HTTP (agents, sessions, events, files).ReqManagedAgents.SSE/.Stream— the Claude event stream.ReqManagedAgents.AgentCore.Client/.Converse/ReqManagedAgents.Provisioner— Bedrock AgentCore wire client, Converse decoding, and Harness provisioning.ReqManagedAgents.Event/.Consolidate— pure builders, classification, reconnect helpers.ReqManagedAgents.ToolSchema— custom-tool schema construction.ReqManagedAgents.SessionResult/.TurnResult/.Usage/.ToolUse/.ToolResult— the canonical result vocabulary shared by every provider.
Telemetry
req_managed_agents emits :telemetry events you can attach to:
| Event | Measurements | Metadata |
|---|---|---|
[:req_managed_agents, :request, :start | :stop | :exception] | duration | method, path, status |
[:req_managed_agents, :agent_core, :request, :start | :stop | :exception] | duration | operation, service, method, path, status |
[:req_managed_agents, :stream, :connected | :event | :done | :error] | — | session_id, type, usage, reason |
[:req_managed_agents, :tool, :start | :stop | :exception] | duration | tool, session_id, is_error |
[:req_managed_agents, :session, :tool_uses] | tool_use_count | turn, 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.