# `ReqManagedAgents.Session`
[🔗](https://github.com/cash-mckeeman/req_managed_agents/blob/v0.12.0/req_managed_agents/lib/req_managed_agents/session.ex#L1)

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),
`:budget` (a `%ReqManagedAgents.Budget{}` or a map with the same keys — a provider-enforced
spending cap on a fresh Claude Managed Agents session, confirmed by the provider before any
prompt is sent; see `ReqManagedAgents.Budget`; `{:error, :budget_unsupported}` on other
providers, an error alongside `:session_id`),
`: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.

# `child_spec`

Return a child_spec for a live session.

# `message`

```elixir
@spec message(pid(), String.t()) :: :ok
```

Send a follow-up user message into a running live session.

# `run`

```elixir
@spec run(
  module(),
  keyword()
) :: {:ok, ReqManagedAgents.SessionResult.t()} | {:error, term()}
```

# `send_event`

```elixir
@spec send_event(pid(), map()) :: :ok | {: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).

# `start_link`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
