One Session loop, any loop host — server-side (Claude Managed Agents, AgentCore) or in-process (Local). Your custom tools execute on your node regardless of which host runs the loop, so your code and data never leave it — the loop host only ever sees each tool's name, description, input schema, and the text result you return.
One loop, three 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 |
| Local (in-process) | ReqManagedAgents.Providers.Local | :request_response — in-process loop over a pluggable chat_fun (default: ReqLLM via the optional req_llm dep); one model call per turn; loop guards for weak-instruction-following local models; reattachable via history: |
Local + routing
Point Local's chat_fun at an OpenAI-compatible gateway lane (base_url + per-run api_key
via model_config) and you get hard data-plane budget enforcement with no coupling to the
gateway's internals. Direct-to-provider chat_funs remain available for dev and tests.
{:ok, result} =
ReqManagedAgents.Session.run(ReqManagedAgents.Providers.Local,
handler: MyTools,
spec: %{system_prompt: "...", tools: tools, terminal_tool: "submit", model_config: nil},
model_config: %{model: "openai:gpt-oss", base_url: lane_url, api_key: granted_key},
prompt: "Go."
)(Providers.Local reads the bare-map spec keys directly and never coerces to
%Agent.Spec{}, so it takes no :name; the managed providers do — see "Provision
once, run anywhere".)
Install
Requires Elixir 1.20 or later.
def deps do
[{:req_managed_agents, "~> 0.12"}]
endUsing the Bedrock AgentCore provider? Add the optional AWS deps (Anthropic-only users can skip these):
def deps do
[
{:req_managed_agents, "~> 0.12"},
{:ex_aws_auth, "~> 1.4"},
{:aws_event_stream, "~> 0.1"}
]
endUsing Providers.Local with the default chat_fun (ReqLLM)? Add:
def deps do
[
{:req_managed_agents, "~> 0.12"},
{:req_llm, "~> 1.10"}
]
endInjected chat_funs (any OpenAI-compatible endpoint via plain Req) need nothing extra.
Configuration
Every config/credential value the library reads funnels through
ReqManagedAgents.Config, in one fixed priority order: an explicit opts
keyword wins, then Application.get_env(:req_managed_agents, key), then a
System.get_env environment variable, then a default. This is the complete
list of keys read anywhere in the library:
| Key | opt | :req_managed_agents app env | ENV var | Default |
|---|---|---|---|---|
| Anthropic API key | :api_key | :api_key | ANTHROPIC_API_KEY | (one auth source required) |
| Anthropic bearer file | :bearer_token_file | :bearer_token_file | ANTHROPIC_BEARER_TOKEN_FILE | (one auth source required) |
| Base URL | :base_url | :base_url | — | "https://api.anthropic.com" |
| Beta header | :beta | :beta | — | "managed-agents-2026-04-01" |
| Files beta header | :files_beta | :files_beta | — | "files-api-2025-04-14" |
| Anthropic API version | :anthropic_version | :anthropic_version | — | "2023-06-01" |
| Receive timeout (ms) | :receive_timeout | :receive_timeout | — | 60_000 |
| Provider profile | :profile | :profile | — | :anthropic |
| AWS access key ID | :aws_access_key_id | :aws_access_key_id | AWS_ACCESS_KEY_ID | (required) |
| AWS secret access key | :aws_secret_access_key | :aws_secret_access_key | AWS_SECRET_ACCESS_KEY | (required) |
| AWS region | :aws_region | :aws_region | AWS_REGION, then AWS_DEFAULT_REGION | "us-east-1" |
| AWS session token | :aws_session_token | :aws_session_token | AWS_SESSION_TOKEN | nil |
Select exactly one auth source. Explicit api_key: nil selects a bearer file even
when an API key is configured; bearer_token_file: nil selects an API key even
when a bearer file is configured. Both clients can coexist in one process.
The bearer file contains JSON {"access_token":"…","expires_at":2000000600},
where expires_at is UTC Unix seconds. Supply it as a regular file and replace
it atomically when rotating credentials. Every JSON, Files and SSE HTTP attempt
reads the current file, including transient retries. Invalid files or tokens
with 30 seconds or less remaining fail before dispatch with
{:error, {:authentication_error, reason}}. Bearer requests do not follow redirects.
Providers.ClaudeManagedAgents.Client.new/1 resolves the Anthropic keys; AWS.SigV4.from_env/1
resolves the AWS keys (it still works called with no args — opts just gives
you an override point without touching the environment).
The core: one loop, the loop host 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. The loop host runs the agent loop —
a managed provider server-side, or Providers.Local in-process. It returns the same result
shape for every provider:
alias ReqManagedAgents.Session
alias ReqManagedAgents.Providers.{ClaudeManagedAgents, BedrockAgentCore}
# `handle` is what `provision/3` returns — see "Provision once, run anywhere" below.
# Claude Managed Agents (streaming) — `agent:`/`environment:` take the handle
# (each lifts the id it needs); no hand-threaded raw ids.
{:ok, %ReqManagedAgents.SessionResult{} = result} =
Session.run(ClaudeManagedAgents,
client: ReqManagedAgents.new(), agent: handle, environment: handle,
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:, …}
result.transcript # client-held history (Local) for reattach, else nil (server-held providers)
# AWS Bedrock AgentCore (request/response) — same handler, same result struct;
# its handle carries a `harness_arn`.
{:ok, %ReqManagedAgents.SessionResult{}} =
Session.run(BedrockAgentCore,
harness_arn: handle.harness_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:provision/3(agent + environment in one call), a local tool handler, and the%SessionResult{}(text, terminal, token usage).examples/bedrock_agent_core.exs— AgentCore Harness: the sameprovision/3→Session.run/2→teardown/2shape, plus 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.
Provision once, run anywhere
Both managed providers speak one vocabulary: build an %ReqManagedAgents.Agent.Spec{}
(a :name is required — Agent.Spec.new/1 rejects a nameless spec), provision it — passing
any environment as the :environment option (an Environment.Spec, or a flat map that coerces
to one; its config is handed verbatim to the provider's wire environment field, no per-key
indexing) — and thread the returned handle into Session.run/2. The provider module is the only
thing you change.
alias ReqManagedAgents.Agent.Spec
spec = %Spec{
name: "billing-support",
system_prompt: "You are a concise billing-support agent. Use tools for customer data.",
model_config: model_config, # provider-specific wire shape — see the table
tools: tools # SCHEMAS only; the implementations stay in your Handler
}
# create-or-reuse, cached in-process per {provider, spec}; `teardown/2` releases it
{:ok, handle} = ReqManagedAgents.provision(provider, spec, environment: env_spec)
# then thread `handle` into Session.run — the connection opts are the one
# per-provider difference (see the table below).What actually differs between the two providers is only this:
| Claude Managed Agents | Bedrock AgentCore | |
|---|---|---|
| mode | :streaming — long-lived SSE, events pushed | :request_response — one synchronous signed invoke per turn |
| credentials | ANTHROPIC_API_KEY + beta header | AWS SigV4 (AWS_*) + an execution-role ARN |
model_config wire | plain model-id string ("claude-haiku-4-5") | %{"bedrockModelConfig" => %{"modelId" => "us.…"}} (cross-region inference profile) |
| provision creates | a versioned agent and an environment (two resources) | one harness folding in model + tools + environment |
| provision handle | %{agent_id:, environment_id:} | %{harness_arn:, harness_id:} |
Session.run connection | agent: handle, environment: handle | harness_arn: handle.harness_arn, runtime_session_id: sid (id ≥33 chars) |
| capabilities | outcomes, server-tool observation, cross-batch tool recovery, resume/reconnect (stream-level, with reconnect/3 event recovery) | session_id: reattach within the session window (no event recovery — a dropped turn just re-invokes) |
:agent/:environment accept a handle (a struct, or a bare map with the same
agent_id:/environment_id: keys) and unpack to the raw ids before the provider opens the
session; an explicit :agent_id/:environment_id still works and wins if both are given.
Each AgentCore turn is one signed invoke; resume re-sends the assistant toolUse + your
toolResult delta. Long runs stream incrementally with no client wall clock — only silence
fails a turn (idle_timeout:, inter-chunk guard, default 300s); cost is bounded server-side via
timeout_seconds:/max_iterations:/max_tokens: (per-invocation overrides of the harness
defaults). Session.run/2's own :timeout must be ≥ the server budget — a client timeout returns
{:error, :timeout} but does NOT cancel the in-flight invoke; the harness keeps executing (and
billing) up to its timeoutSeconds. Events reach Handler.handle_event/2 live either way.
Reattach (0.10): pass session_id: (the id from a prior SessionResult.session_id) instead
of runtime_session_id: to resume an existing AgentCore runtime session within its session
window — resumed?/1 reflects the reattach honestly. There's no event-list surface to replay on
this path, so a custom Provider.reconnect/3 isn't required for it: Session defaults an
unimplemented reconnect/3 to {:ok, conn, [], seen} and proceeds. Beyond-window re-seed is a
planned follow-up. Providers.Local has its own reattach seam: pass history: (a prior
SessionResult.transcript) to open/2 to seed a conversation verbatim; resumed?/1 reflects it
and transcript/1 exposes the grown history. Any provider whose history lives client-side can
implement the optional transcript/1 callback — Session embeds it into SessionResult.transcript
at terminal (nil for server-held providers like Claude Managed Agents and AgentCore).
Provisioning budget (Bedrock AgentCore)
provision/3 takes :timeout (ms, default 365 000) as the budget for the whole call. It
becomes a monotonic deadline at entry, shared by the wait for a same-name harness to finish
deleting, the wait for the new one to reach READY, and the wait for the endpoint an invoke
will reach — and it bounds every request those waits make, so a provision that cannot finish
returns a named {:error, {tag, %WaitContext{}}} inside the budget rather than overrunning
the deadline it is running under. (The best-effort
DeleteHarness that rolls back a harness this call created is the deliberate exception: it
runs after the budget is spent and carries a small fixed budget of its own.)
The budgets are additive, so size them that way. provision and Session.run/2 run in
sequence, and the defaults do not compose: 365 s of provisioning plus run/2's 600 s
default exceeds a 900 s enclosing limit. The constraint is
provision :timeout + Session.run/2 :timeout + margin ≤ your enclosing deadlinewhere the enclosing deadline is whatever kills you from outside — a CI job timeout, an ExUnit
@tag timeout:, a supervisor. Getting this wrong is the failure this budget exists to make
diagnosable: the enclosing timeout fires first, the process is killed mid-wait, and no named
error is ever returned.
A harness reaching READY is not the end of provisioning. The harness and each of its
endpoints carry separate statuses: measured live, a harness was READY in 11 s while its
DEFAULT endpoint took 2 m 31 s, and an invoke against a still-creating endpoint fails. So
provision/3 waits for the endpoint too, polling it only once the harness is READY — so a
harness that never gets there costs zero endpoint calls, and one that does costs a poll per
remaining endpoint interval (at the measured 2 m 31 s and a 5 s cadence, roughly thirty). The
endpoint is :endpoint_name, defaulting to DEFAULT — the endpoint InvokeHarness uses when
no qualifier is given. Its failures carry
their own tags (:endpoint_ready_timeout, :endpoint_failed, :endpoint_terminating,
:endpoint_unknown_status) so an endpoint problem never reads as a harness one, and each names
the endpoint it was waiting on. Size :timeout for both waits.
:endpoint_name is validated at entry against the service's own
[a-zA-Z][a-zA-Z0-9_]{0,47}, returning {:error, {:invalid_opts, :endpoint_name}} before a
harness exists. A mistyped name would otherwise be indistinguishable from an endpoint that has
not appeared yet — both are a 404 — so it would spend the whole budget and then roll back a
harness that is healthy and READY.
Session.run/2 takes the same :endpoint_name and sends it as InvokeHarness's qualifier,
so the endpoint a provision gates on and the endpoint its turns reach are one option rather than
two. It belongs on the connection rather than on the provision handle: the endpoint is a
call-time routing choice, excluded from the content digest, and one harness has many endpoints —
a handle replayed from the provisioner store would otherwise pin whichever one the first
provision happened to ask for.
The :timeout also derives the poll cadence when :ready_poll_ms is not given, so a short
budget still polls repeatedly rather than looking once. Note that the bound covers each
request's receive phase; a retried call also spends Req's backoff sleeps and Finch's connect
timeout outside it, so leave margin rather than sizing to the millisecond.
Layers
ReqManagedAgents.Provider— the behaviour every backend implements (invocation +normalize/1).ReqManagedAgents.Session— the unified, supervised, reconnecting loop driven by yourHandler.ReqManagedAgents.Providers.ClaudeManagedAgents.Client— Claude control-plane HTTP (agents, sessions, events, files).ReqManagedAgents.SSE/ReqManagedAgents.Providers.ClaudeManagedAgents.Stream— the Claude event stream.ReqManagedAgents.Providers.BedrockAgentCore.Client/.Converse/ReqManagedAgents.Provisioner— Bedrock AgentCore wire client, Converse decoding, and Harness provisioning.ReqManagedAgents.Providers.ClaudeManagedAgents.Event/.Consolidate— pure builders, classification, reconnect helpers.ReqManagedAgents.ToolSchema— neutral tool input schemas;ReqManagedAgents.Providers.ClaudeManagedAgents.ToolSchema— custom-tool definitions.ReqManagedAgents.Artifacts/.Artifact/.SessionInfo— name-keyed session-artifact verbs over provider-native stores + the runtime identity handed to handlers.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, :agent_core, :provision, :poll] | elapsed_ms, poll_n | harness_id, status, phase |
[:req_managed_agents, :agent_core, :provision, :stop] | duration_ms, polls | harness_id, phase, result |
[:req_managed_agents, :agent_core, :provision, :exception] | duration_ms | harness_id, phase, kind, result |
[: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 |
The :provision events fire once per poll and once per terminal outcome while a Bedrock
AgentCore harness is being provisioned; phase is :ready, :endpoint or :deleted, and result is
:ok or {:error, tag}. The same fields are also written to the log, so a provisioning
failure is diagnosable without attaching a handler.
All providers run through Session, so the :session events fire regardless of loop host.
:stream :event also fires for both providers as events arrive mid-turn — on Claude,
type is the SSE event type and session_id/usage are set; on Bedrock AgentCore, type is
the Converse envelope key (e.g. "contentBlockDelta") and there is no session_id. The other
:stream events (:connected/:done/:error) are Claude-only. 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.Providers.ClaudeManagedAgents.Client.upload_file(client, %{purpose: "agent", file: "report.csv"})
{:ok, _} = ReqManagedAgents.Providers.ClaudeManagedAgents.Client.attach_file_to_session(client, session_id, %{file_id: file_id, mount_path: "/data/report.csv"})
{:ok, bytes} = ReqManagedAgents.Providers.ClaudeManagedAgents.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.
Artifacts — retrieve what your agent built
An agent writes deliverables into its session sandbox; the file's name is the only
identity the model ever sees. ReqManagedAgents.Artifacts gives one vocabulary over
provider-native session storage — list, fetch, put, delete, name-keyed and
session-scoped:
alias ReqManagedAgents.Artifacts
alias ReqManagedAgents.Providers.BedrockAgentCore.Artifacts, as: AgentCoreSessionStorage
alias ReqManagedAgents.Providers.ClaudeManagedAgents.Artifacts, as: ClaudeFiles
# Claude Managed Agents — the Files API, scoped to one session
store = {ClaudeFiles, ClaudeFiles.store(client, session_id)}
{:ok, artifacts} = Artifacts.list(store) # [%ReqManagedAgents.Artifact{name: "report.md", …}]
{:ok, bytes} = Artifacts.fetch(store, "report.md")
# Bedrock AgentCore — a sessionStorage mount (no VPC), command-backed
store =
{AgentCoreSessionStorage,
AgentCoreSessionStorage.store(ac_client, harness_arn, runtime_session_id, "/mnt/data")}
{:ok, bytes} = Artifacts.fetch(store, "report.md")Handlers receive a %ReqManagedAgents.SessionInfo{} (optional 4th argument to
handle_tool_call/4) carrying the session_id, so a tool can build the store for its
OWN session and fetch what the agent just wrote.
The parity story, honestly: Anthropic offers a provider-hosted blob store (zero infra;
bytes on Anthropic); AWS mounts your storage into the microVM (sessionStorage
needs nothing; EFS/S3 mounts need VPC mode) plus direct shell access
(Providers.BedrockAgentCore.Client.invoke_agent_runtime_command/2 — no model loop, no token cost).
The sessionStorage store handles report-scale artifacts (bytes transit the command
stream as Base64); an S3-mount store (host side = plain S3) is designed for 0.4.
Declare mounts via the :environment provisioning option (an Environment.Spec; its
config is passed verbatim to the provider's wire environment field).
The outputs-dir convention (Claude Managed Agents, established live 2026-07-03): only files the agent writes under
/mnt/session/outputs/become session artifacts — scoped to the session, downloadable, retrievable viaClaudeFiles. Files written elsewhere (e.g./workspace) leave non-downloadable, unscoped residue. The path is exposed asClaudeFiles.outputs_dir/0(+output_path/1for a named file) — interpolate it into your agent's system prompt instead of copying the string.
Environments are images
The Docker mental model maps directly onto the CMA environment lifecycle — with the same rules: a changed spec is a new image, not an in-place update; tags are movable pointers; sessions are the containers that churn; prune is explicit GC.
| Docker | RMA |
|---|---|
| Dockerfile | env spec (canonical map) |
| image digest | spec hash — content-addressed identity |
| repository | base name ("data_analysis") |
repo@digest | provider-side name <base>_<digest8> |
docker build (cached) | ensure_environment/3 — build-if-absent, never rebuilds on a hit |
repo:tag (movable) | Store-backed tag → digest pointer |
docker run | create_session — ephemeral, references an image |
docker image prune | prune_environments/3 — explicit GC, never automatic |
Worked example
alias ReqManagedAgents.Provisioner
alias ReqManagedAgents.Provisioner.Store
store = {Store.File, path: Path.expand("~/.cache/myapp/provisions.json")}
env_spec = %{type: "cloud", packages: %{pip: ["pandas"]}, networking: %{type: "unrestricted"}}
# Build once — next run hits the store and returns the same handle instantly:
{:ok, handle} =
ReqManagedAgents.ensure_environment(client, env_spec, name: "data_analysis", store: store)
# handle is a %ReqManagedAgents.Provisioner.Environment.Handle{} struct
# (dot-access + Jason-encodes to %{environment_id: "env_id_…", name: "data_analysis_3f9a1b2c", digest: "3f9a1b2c"})
# Pin the current image as "prod" (movable pointer; retag freely):
:ok = Provisioner.tag("data_analysis", "prod", handle, store: store)
# Resolve the pinned image later — never falls back; {:error, :unknown_tag} on miss:
{:ok, %{environment_id: _env_id}} = Provisioner.resolve("data_analysis:prod", store: store)
# GC old versions — keep the newest 3 (plus any tagged digest; keep: has no default):
{:ok, %{archived: _old, kept: _live}} =
Provisioner.prune_environments(client, "data_analysis", keep: 3, store: store)Store.File persists handles and tags across OS processes (CLI tools, cron, mix tasks),
with atomic writes and a single-writer assumption. The default is Store.ETS — in-process
only. Values must be JSON-encodable (provision handles always are).
Declared runtimes
Add a runtimes: key to the env spec to have the library produce a bootstrap script
and system-prompt instruction the agent runs on first need:
env_spec = %{
type: "cloud",
packages: %{},
networking: %{type: "unrestricted"},
runtimes: [%{lang: :elixir, version: "1.20.2", via: :mise}]
}
{:ok, handle} = ReqManagedAgents.ensure_environment(client, env_spec, name: "myapp")
# handle.bootstrap == %{script: "…mise install script…", instructions: "…"}Pass handle.bootstrap.instructions into your agent's system prompt. The agent runs
the bootstrap script once via bash before the first command that needs the runtime.
Proven end-to-end: ~11s on ubuntu-24.04 (precompiled OTP from mise; no compile step).
Only via: :mise is supported. The runtimes list is digest-covered — adding or changing
a runtime version produces a new image automatically, no extra machinery.
Agents as managed entities
ensure_agent/3 is the content-addressed cousin of provision/3 ("Provision once, run
anywhere"): same %Agent.Spec{} vocabulary, but Store-backed, digest-named, tag- and
prune-aware, returning a typed handle you splat straight into Session.run/2.
The same content-addressed lifecycle Provisioner.Environments gives environments
applies to agents: ReqManagedAgents.Agent.Spec hashes an agent's identity
(system_prompt, tools, terminal_tool, model_config — name is the base, not
identity content); ensure_agent/3 is build-if-absent, never re-creating on a hit;
tags are movable pointers; prune is explicit GC.
agent_spec = %{
name: "support_bot",
system_prompt: "You triage support tickets.",
tools: [],
model_config: %{model: "claude-opus-4-6"}
}
# Build once — a second call with the same spec returns the same handle, no re-create:
{:ok, agent} = ReqManagedAgents.ensure_agent(client, agent_spec, name: "support_bot", store: store)
# agent is a %ReqManagedAgents.Agent.Handle{} struct
# (dot-access + Jason-encodes to %{agent_id: "agent_id_…", name: "support_bot_3f9a1b2c", digest: "3f9a1b2c"})
# Pin the current version as "prod" (movable; retag freely):
:ok = ReqManagedAgents.tag_agent("support_bot", "prod", agent, store: store)
# Resolve the pinned version later — {:error, :unknown_tag} on miss, never a silent fallback:
{:ok, agent} = ReqManagedAgents.resolve_agent("support_bot:prod", store: store)
# GC old versions — keep the newest 3 (plus any tagged digest; keep: has no default):
{:ok, %{archived: _old, kept: _live}} =
ReqManagedAgents.prune_agents(client, "support_bot", keep: 3, store: store)A 409 on create (a name collision on the provider side) recovers by name instead of
failing — the provider-side name is <base>_<digest8>, so a live agent with that exact
name IS this exact spec.
Pass the ensure_agent/3 handle straight into Session.run/2 alongside an environment
handle — :agent/:environment are unpacked to :agent_id/:environment_id before the
provider opens the session, so callers stop hand-threading raw ids:
{:ok, env} = ReqManagedAgents.ensure_environment(client, env_spec, name: "data_analysis", store: store)
{:ok, result} =
ReqManagedAgents.Session.run(ReqManagedAgents.Providers.ClaudeManagedAgents,
agent: agent,
environment: env,
handler: MyTools,
prompt: "Summarize this quarter's tickets"
)An explicit :agent_id/:environment_id still works and wins if both a handle and an
id are given.
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.Providers.ClaudeManagedAgents.ToolSchema.to_custom_tool/3). A dedicated
adapter package is planned.
License
Apache-2.0.