The one agent loop, provider- and transport-agnostic.
A Session drives any ReqManagedAgents.Provider to completion: invoke a turn → normalize →
run the return-of-control tools locally via the :handler → resume → repeat until a terminal.
The loop host runs the agent loop — a managed provider server-side, or
ReqManagedAgents.Providers.Local in-process. The provider's mode/0 (:streaming push or
:request_response pull) only changes how a turn's events are acquired; the loop, the result
shape, and the raw-event passthrough are identical across providers.
Pick a provider — ReqManagedAgents.Providers.ClaudeManagedAgents (streaming),
ReqManagedAgents.Providers.BedrockAgentCore (request/response), or
ReqManagedAgents.Providers.Local (in-process) — and:
# synchronous run-to-completion
{:ok, %ReqManagedAgents.SessionResult{terminal: t, stop_reason: r, events: raw}} =
ReqManagedAgents.Session.run(provider, handler: MyTools, prompt: "Hi", ...)
# live, long-lived (stays alive after a terminal; `:notify` gets {:managed_agents_session, %ReqManagedAgents.SessionResult{}})
{:ok, pid} = ReqManagedAgents.Session.start_link(provider, handler: MyTools, notify: self(), ...)
ReqManagedAgents.Session.message(pid, "follow-up")Required opts: :handler (a ReqManagedAgents.Handler module or a 3-arity fn). Optional:
:context, :prompt, :outcome (a %ReqManagedAgents.Outcome{} or a map with the same keys
%{description:, rubric:, max_iterations:} — kicks off a
user.define_outcome graded session instead of a user.message; mutually exclusive with
:prompt, outcome wins; {:error, :outcome_unsupported} on providers without native support),
:timeout, :max_turns, :notify, :name, :telemetry_metadata,
:turn_guard (a 1-arity fun invoked after each turn's usage accumulation with
%{usage: %ReqManagedAgents.Usage{}, turns: n, session_id: id}, returning :cont or {:halt, reason};
on halt the run stops with {:error, {:halted, reason}} and a :terminated result is
notified — usage/turns accumulate within the current request, the same scope as :max_turns;
the guard runs before the max_turns check and wins when both would trip on the same turn;
it fires on terminal-tool re-prompt turns, whose turns counter keeps incrementing normally;
guards must not raise — a raising guard crashes the session and surfaces as
{:error, {exception, stack}} to run/2 rather than producing a {:halt, …}),
:require_terminal_tool + :terminal_tool + :max_reprompts (default 2): an :end_turn
that never called terminal_tool is re-driven with a re-prompt; exhausted re-prompts finish
with stop_reason: :no_terminal_tool. Re-prompt turns count against :max_turns.
For long AgentCore runs set :timeout (the end-to-end run budget, default 600_000 ms)
at or above the server-side budget — a run/2 timeout returns {:error, :timeout} and
tears down the in-flight invoke client-side (the poll task and its HTTP stream are shut
down). The server may still run the already-received invocation to its own limit: the
server-side timeoutSeconds remains the authoritative server budget.
Transport liveness is guarded per turn by :idle_timeout and total
cost by the :timeout_seconds/:max_iterations/:max_tokens per-invocation overrides
(Bedrock AgentCore only).
Provider-specific opts (e.g. :agent_id/:environment_id, :harness_arn/:runtime_session_id,
:session_id to resume) are forwarded to the provider's open/2. :agent/:environment accept
the handle returned by ReqManagedAgents.ensure_agent/3 / ensure_environment/3
(a %ReqManagedAgents.Agent.Handle{} / %ReqManagedAgents.Provisioner.Environment.Handle{}, or a
bare map with the same agent_id:/environment_id: keys) — the handle is unpacked to
:agent_id/:environment_id before open/2 runs, so callers pass the handle instead of
hand-threading the raw id; an explicit :agent_id/:environment_id wins if both are given.
Summary
Functions
Return a child_spec for a live session.
Send a follow-up user message into a running live session.
Post a pre-built raw user event (e.g. Event.tool_confirmation/2, a mid-session
Event.define_outcome/3) into a running live session. The event is pushed verbatim —
no turn accounting or accumulator reset. Streaming providers only:
{:error, :unsupported} on :request_response providers (their input is consumed
by poll_turn/2, there is no out-of-band channel).
Start a long-lived session. Unlike run/2, it stays alive after a terminal for follow-ups.
Functions
Return a child_spec for a live session.
Send a follow-up user message into a running live session.
@spec run( module(), keyword() ) :: {:ok, ReqManagedAgents.SessionResult.t()} | {:error, term()}
Post a pre-built raw user event (e.g. Event.tool_confirmation/2, a mid-session
Event.define_outcome/3) into a running live session. The event is pushed verbatim —
no turn accounting or accumulator reset. Streaming providers only:
{:error, :unsupported} on :request_response providers (their input is consumed
by poll_turn/2, there is no out-of-band channel).
@spec start_link( module(), keyword() ) :: GenServer.on_start()
Start a long-lived session. Unlike run/2, it stays alive after a terminal for follow-ups.