Packages

A behaviour and supervision framework for long-running LLM agent processes, modeled as OTP state machines.

Current section

Files

Jump to
gen_agent README.md
Raw

README.md

# GenAgent

[![CI](https://github.com/genagent/gen_agent/actions/workflows/ci.yml/badge.svg)](https://github.com/genagent/gen_agent/actions/workflows/ci.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/gen_agent.svg)](https://hex.pm/packages/gen_agent)
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/gen_agent)

A behaviour and supervision framework for long-running LLM agent processes,
modeled as OTP state machines.

This repository contains six independently versioned Hex packages. Install
only the packages an application needs; core has no dependency on a CLI
wrapper or HTTP client.

| Package | Source | Purpose |
| --- | --- | --- |
| [`gen_agent`](https://hex.pm/packages/gen_agent) | `.` | Agent behaviour, lifecycle, and supervision |
| [`gen_agent_claude`](https://hex.pm/packages/gen_agent_claude) | [`integrations/claude`](https://github.com/genagent/gen_agent/tree/main/integrations/claude) | Claude CLI backend |
| [`gen_agent_codex`](https://hex.pm/packages/gen_agent_codex) | [`integrations/codex`](https://github.com/genagent/gen_agent/tree/main/integrations/codex) | Codex CLI backend |
| [`gen_agent_anthropic`](https://hex.pm/packages/gen_agent_anthropic) | [`integrations/anthropic`](https://github.com/genagent/gen_agent/tree/main/integrations/anthropic) | Anthropic HTTP backend |
| [`gen_agent_openai`](https://hex.pm/packages/gen_agent_openai) | [`integrations/openai`](https://github.com/genagent/gen_agent/tree/main/integrations/openai) | OpenAI HTTP backend |
| [`gen_agent_ensemble`](https://hex.pm/packages/gen_agent_ensemble) | [`extensions/ensemble`](https://github.com/genagent/gen_agent/tree/main/extensions/ensemble) | Multi-agent strategies |

The [`claude_wrapper`](https://github.com/genagent/claude_wrapper_ex) and
[`codex_wrapper`](https://github.com/genagent/codex_wrapper_ex) repositories
remain independent.

If you are deciding between calling a wrapper directly, running a GenAgent,
or enqueuing an Oban job, see [Choosing a layer](guides/choosing_a_layer.md).

Each agent is a `:gen_statem` process wrapping a persistent LLM session.
Every interaction is a prompt-response turn, and the implementation decides
what happens between turns.

> It is a GenServer but every call is a prompt.

GenAgent handles the mechanics of turns. Implementations handle the
semantics of turns.

## Installation

```elixir
def deps do
  [
    {:gen_agent, "~> 0.8.0"} # x-release-please-version
  ]
end
```

Add at least one backend using its own installation guide:
[Claude](https://github.com/genagent/gen_agent/tree/main/integrations/claude),
[Codex](https://github.com/genagent/gen_agent/tree/main/integrations/codex),
[Anthropic](https://github.com/genagent/gen_agent/tree/main/integrations/anthropic),
or [OpenAI](https://github.com/genagent/gen_agent/tree/main/integrations/openai).

## Quick start

Define an implementation module by using the `GenAgent` behaviour:

```elixir
defmodule MyApp.Coder do
  use GenAgent

  defmodule State do
    defstruct [:path, responses: []]
  end

  @impl true
  def init_agent(opts) do
    path = Keyword.fetch!(opts, :cwd)

    backend_opts = [
      cwd: path,
      system_prompt: "You are a coding assistant."
    ]

    {:ok, backend_opts, %State{path: path}}
  end

  @impl true
  def handle_response(_ref, response, state) do
    {:noreply, %{state | responses: state.responses ++ [response.text]}}
  end
end
```

Start the agent under the supervision tree and interact with it by name:

```elixir
{:ok, _pid} = GenAgent.start_agent(MyApp.Coder,
  name: "my-coder",
  backend: GenAgent.Backends.Claude,
  cwd: "/path/to/project"
)

# Synchronous prompt.
{:ok, response} = GenAgent.ask("my-coder", "What does lib/foo.ex do?")
IO.puts(response.text)

# Async prompt.
{:ok, ref} = GenAgent.tell("my-coder", "Add tests for lib/foo.ex")
GenAgent.poll("my-coder", ref)
#=> {:ok, :pending} while the turn runs, then {:ok, :completed, response}

# Or have the outcome sent to you as a message.
{:ok, ref} = GenAgent.tell_with_completion("my-coder", "Run the tests")

receive do
  {:gen_agent, :completion, "my-coder", ^ref, {:ok, response}} -> IO.puts(response.text)
  {:gen_agent, :completion, "my-coder", ^ref, {:error, reason}} -> IO.inspect(reason)
end

# Push an external event into handle_event/2.
GenAgent.notify("my-coder", {:ci_failed, "test_auth"})

GenAgent.stop("my-coder")
```

## One interface for Claude and Codex

Use the same callback module and `GenAgent` API for both providers. Select
the backend when starting each agent:

`gen_agent` owns the agent process, prompt tasks, callbacks, and queue.
`gen_agent_claude` and `gen_agent_codex` implement the backend contract,
translate provider events, and retain each provider's session identifier.
Those backends call `claude_wrapper` and `codex_wrapper`, respectively;
the wrappers build CLI arguments, parse output, and select a runner for
the external process. Add both backend packages to one application when
agents need both CLIs. Select a backend per agent, then use the same
`GenAgent.ask/3`, `tell/3`, `poll/3`, and interruption APIs for either.

```elixir
defmodule MyApp.Assistant do
  use GenAgent

  @impl true
  def init_agent(opts) do
    {:ok, [cwd: Keyword.fetch!(opts, :cwd)], %{}}
  end

  @impl true
  def handle_response(_ref, _response, state), do: {:noreply, state}
end

for {name, backend} <- [
      {"claude", GenAgent.Backends.Claude},
      {"codex", GenAgent.Backends.Codex}
    ] do
  {:ok, _pid} = GenAgent.start_agent(MyApp.Assistant,
    name: name, backend: backend, cwd: "/path/to/project"
  )
end

{:ok, response} = GenAgent.ask("claude", "Explain this project")
{:ok, ref} = GenAgent.tell("codex", "Explain this project")
GenAgent.poll("codex", ref) # {:ok, :pending} while queued or running
```

Either agent supports `ask`, `tell`, and `poll`; the interaction style is
independent of the provider. Install both backend packages and configure
their CLIs before starting them. Backend-specific options and capabilities
can differ: return the appropriate options from `init_agent/1` when adding
model settings, instructions, or permissions. Each agent keeps its own
provider session.

For a runnable OTP application that starts Claude and Codex behind one
entry point, see the [local GenAgent app example](examples/gen_agent_app/README.md).

## State model

An agent is a state machine with two states:

```
idle --- ask/tell ---------------------------> processing
idle --- notify -> handle_event/2 -> {:prompt, ..} -> processing
idle --- notify -> handle_event/2 -> {:noreply, ..} -> idle (no turn)
                                  |
                                  v
idle <--- handle_response --- processing (turn done)
```

A notification only starts a turn when `handle_event/2` returns
`{:prompt, text, state}`; `{:noreply, state}` leaves the agent idle.

- **:idle** -- waiting for work. On enter, drains the mailbox (queued
  prompts) in FIFO order.
- **:processing** -- a prompt is in flight. One at a time. New prompts
  queue.
- **Self-chaining** -- `handle_response/3` can return `{:prompt, text, state}`
  to immediately dispatch another turn without going through the mailbox.
  Useful for multi-step work that the agent drives itself.
- **Halting** -- `handle_response/3`, `handle_error/3`, `handle_event/2`,
  or `pre_turn/2` can return `{:halt, state}` to go idle but freeze the
  mailbox. `GenAgent.halt/1` requests the same pause externally, after
  any active turn completes. A halted agent ignores queued prompts until
  `GenAgent.resume/1`
  is called.
- **Watchdog** -- a `:state_timeout` kills any turn that runs longer than
  the `:watchdog_ms` deadline (positive integer milliseconds or `:infinity`,
  default 10 minutes). `:max_tell_results` (default 100) and
  `:max_tell_result_bytes` (default 8 MiB) bound the completed tell results
  retained for `poll/2`. Both are non-negative integers; zero retains none.
  The byte budget uses `:erlang.external_size/1` for each ref and result,
  including results also sent by `tell_with_completion/4`. The oldest
  results are evicted first; a result larger than the budget is not retained.

Halting does not terminate the agent. It remains registered with its final
state and backend session. An external owner can read `GenAgent.status(name)`
after completion, persist the state it needs, then call `GenAgent.stop(name)`
to release resources. Use `stop/2` with the owning supervisor for a
caller-owned agent. Calling `stop/1` or `stop/2` for that same agent from one
of its callbacks, including `handle_stream_event/2` in the prompt task,
returns `{:error, :self_stop}` because waiting for its exit would block
cleanup. If final state and cached results are not needed, a
`post_run/1` callback can spawn a separate process to stop a global agent:

```elixir
def post_run(_state) do
  name = GenAgent.current_name()
  spawn(fn -> GenAgent.stop(name) end)
  :ok
end
```

An ordinary `ask/3` sent to a halted agent waits until `resume/1`, and its
watchdog does not bound that wait. `tell/3` and `tell_with_completion/4`
accept requests while halted, but `poll/3` reports `{:ok, :pending}` until
they run. Check `status/2` or `runtime_snapshot/2` to distinguish a halt
from an active turn. Callers that cannot resume the agent can use
`tell_with_completion/5` with `on_halt: :fail`.

## Core callbacks

Only `init_agent/1` and `handle_response/3` are required. `use GenAgent`
supplies defaults for the rest, except `handle_info/2`, which stays
undefined so unexpected messages are logged and discarded. A "decision"
is `{:noreply, state}`, `{:prompt, text, state}` (text must be a binary),
or `{:halt, state}`.

| Callback | Runs in | Valid returns | Use it for |
|---|---|---|---|
| `init_agent/1` | agent | `{:ok, backend_opts, state}` or `{:error, reason}` | Backend options and initial state |
| `handle_response/3` | agent | decision | A turn succeeded: record it, chain, or halt |
| `handle_error/3` | agent | decision | A turn failed: stay idle, retry, or halt |
| `handle_event/2` | agent | decision | Events from `notify/2` and `notify_ack/3` |
| `handle_info/2` | agent | decision | Timers, monitors, and other OTP messages |
| `handle_stream_event/2` | prompt task | next state (any term) | Mid-turn backend events |
| `terminate_agent/2` | agent | any term (ignored) | Cleanup on shutdown |

`pre_run/1`, `pre_turn/2`, `post_turn/3`, and `post_run/1` are covered
under [Lifecycle hooks](#lifecycle-hooks). The `GenAgent` moduledoc has the
full callback contract table, including invalid returns and logging.

**Errors.** `handle_error/3` receives backend errors, a terminal `:error`
event, a stream that ends without a terminal event, a prompt-task crash
(`{:task_crashed, reason}`), `:timeout`, `:interrupted`, and
`:task_supervisor_unavailable`. It also receives overload or `pre_turn/2`
rejections of generated prompts. It returns a decision, so `{:prompt, text,
state}` retries; there is no built-in retry cap. A raise or invalid return
from `handle_error/3`, `handle_event/2`, or `handle_info/2` is logged and
keeps the previous state; the same failure in `handle_response/3` stops the
agent.

**Events.** An event handled while idle runs immediately. During a turn it
is buffered and delivered in arrival order after the decision callback and
`post_turn/3`, using the resulting state. See the keyless
[log-triage example](examples/log_triage/README.md) for an OTP Logger handler
that forwards crash reports and batches deferred notifications into agent turns.

**Stream events.** `handle_stream_event/2` runs in the prompt task. Its
state starts as the agent state at dispatch and is threaded through the
stream inside that task; the agent process does not see intermediate
values. The final value is merged only when the stream finishes normally,
which includes a terminal `:error` event or EOF without a terminal event.
A task crash, interruption, or watchdog kill discards it, and the agent
continues from its pre-stream state. A crash inside the callback is a task
crash.

**Terminate.** `terminate_agent/2` runs in the agent process on shutdown
for cleanup; its return value is ignored. It does not run for an
untrappable kill.

### Request refs and acknowledgments

`interrupt_request/3` cancels the active turn only if `ref` is still the
current request, so it cannot hit a successor turn. It returns
`{:ok, :accepted}`, `{:error, :not_current}`, `{:error, :idle}`, or
`{:error, :not_found}`. Use `cancel_request/3` for a queued tell.

```elixir
try do
  with {:ok, ref} <- GenAgent.tell("my-coder", "Run the tests", 5_000) do
    interruption =
      case GenAgent.interrupt_request("my-coder", ref, 5_000) do
        {:ok, :accepted} = result -> result
        {:error, reason} when reason in [:not_current, :idle] ->
          GenAgent.cancel_request("my-coder", ref, 5_000)
        {:error, reason} -> {:error, reason}
      end

    %{ref: ref, interruption: interruption}
  end
catch
  :exit, reason -> {:error, {:agent_call_failed, reason}}
end
```

`{:ok, :accepted}` reports the agent's decision and BEAM task cancellation,
not that a provider process has stopped. `{:error, :not_current}` can also
mean the request has finished or is queued. `:idle` includes a tell queued
while halted, so both results fall back to `cancel_request/3`, which may
itself return `{:error, :current}` or `{:error, :already_finished}`.
The example returns the ref and admission result without waiting for completion.
Read the eventual outcome with `GenAgent.poll/3`, or use
`tell_with_completion/4` when you want ref-tagged completion messages.
Each call has a finite timeout, and the catch handles timeout or agent-death
exits. A call timeout does not prove rejection; check agent state before retrying.

`notify_ack/3` waits for admission, not for model work. While a turn is
running, `:ok` means the event was retained for delivery after the turn.

```elixir
case GenAgent.notify_ack("my-coder", {:ci_failed, "test_auth"}, 5_000) do
  :ok -> :accepted
  {:error, {:overloaded, info}} -> {:rejected, info}
  {:error, :not_found} -> :no_such_agent
  {:error, :draining} -> :shutting_down
end
```

The acknowledgment is in-memory only. A prompt that `handle_event/2` later
generates can still be rejected, and `handle_error/3` receives that
overload. `notify/2` is a best-effort cast that returns `:ok` regardless.

## Lifecycle hooks

In addition to the core callbacks, v0.2 adds four optional lifecycle
hooks for fine-grained control over what happens around each turn and
around the agent's full run:

| Hook | When it fires | Typical use |
|---|---|---|
| `pre_run/1` | Once, after `init_agent/1`, before the first turn | Slow async setup: clone a repo, create a worktree, fetch secrets |
| `pre_turn/2` | Before each prompt dispatch | Prompt augmentation, rate limiting, `:skip`/`:halt` as a gate |
| `post_turn/3` | After each turn, post-decision | State-mutating side effects: commit per turn, record usage |
| `post_run/1` | On clean `{:halt, state}` from a decision callback or `pre_turn/2`, or an external `halt/1` | Completion actions: open a PR, post a summary |

All four are optional with default no-op implementations. The guiding
principle is **telemetry first, callbacks for state mutation** --
observational use cases (log tokens, emit metrics) should use the
existing telemetry events; callbacks exist specifically for hooks that
need to mutate `agent_state` or block the next transition.

See the [Workspace pattern guide][workspace-guide] for a complete
example exercising all four hooks in sequence around a git workspace.

[workspace-guide]: https://hexdocs.pm/gen_agent/workspace.html

## Backends

GenAgent ships with a `GenAgent.Backend` behaviour and no built-in backend.
Pick one of the sibling packages or write your own:

| Backend | Package | Transport |
|---|---|---|
| Claude (Anthropic) | `gen_agent_claude` | `claude` CLI via `claude_wrapper` |
| Codex (OpenAI) | `gen_agent_codex` | `codex` CLI via `codex_wrapper` |
| Anthropic HTTP | `gen_agent_anthropic` | direct HTTP API via `req` |
| OpenAI HTTP | `gen_agent_openai` | Responses API via `req` |

A backend owns its session lifecycle, translates the LLM-specific event
stream into the normalized `GenAgent.Event` values the state machine
consumes, and carries any state it needs (session id, message history) in
an opaque session term.

The contract has three required callbacks (`start_session/1`, `prompt/2`,
`terminate_session/1`). `prompt/3` and `checkpoint_session/2` let CLI
backends acknowledge a session ID during an active turn, so a later failure
or interruption does not discard it. `update_session/2` and
`resume_session/2` and `reset_session/1` remain optional. See
`GenAgent.Backend` for details.

The four bundled adapters normalize turn failures as
`GenAgent.Backend.Error` when this core version is present. Its `:kind`,
`:retryable?`, `:status`, and `:retry_after` fields support shared retry
policies; `:raw` retains the provider's original reason. A retry is always
the application's decision.

See the [Backends guide](guides/backends.md) for backend implementation rules and a capability and option comparison.
For the tradeoffs between wrappers, GenAgent, and Oban workers, see
[Choosing a layer](guides/choosing_a_layer.md).

## Public API

| Function | What it does |
|---|---|
| `start_agent/2` | Start an agent under the supervision tree. |
| `current_name/0` | Read the registered name from an agent callback. |
| `ask/3` | Synchronous prompt. Blocks until the turn finishes. |
| `tell/3` | Async prompt. Returns a ref for `poll/3`. |
| `tell_with_completion/4` | Async prompt with a request-scoped completion message. |
| `poll/3` | Check on a previously-issued `tell/3`. |
| `notify/2` | Push an external event into `handle_event/2`. |
| `notify_ack/3` | Wait for an in-memory notification admission result. |
| `interrupt/1` | Cancel an in-flight turn. |
| `interrupt_request/3` | Acknowledge cancellation only if the active request ref matches. |
| `cancel_request/3` | Remove a queued tell by its exact request ref. |
| `halt/1` | Pause dispatch after the active turn, without stopping the agent. |
| `resume/1` | Unhalt an agent and drain its mailbox. |
| `reset_session/2` | Clear supported backend conversation context between turns while retaining agent state. |
| `status/2` | Read the agent's current state. |
| `runtime_snapshot/2` | Read bounded runtime metadata and pending-input counts. |
| `drain/2` | Refuse new work, finish the active turn, and wait for the agent to exit. |
| `stop/1` | Terminate the agent. |
| `child_spec/2` | Build an agent child spec for a caller-owned supervisor. |
| `stop/2` | Terminate an agent under a caller-owned supervisor. |
| `whereis/1` | Look up an agent's pid. |

Names resolve through a `Registry`, so callers address agents by name
(any term). Agents use `restart: :temporary`: a crashed or stopped agent
must be started explicitly, and its previous state is not restored.
Callbacks can use `GenAgent.current_name()` in both the agent process and
the prompt task; no duplicate `agent_name` option is needed.

`runtime_snapshot/2` reports the coordinator's current phase, halted, pending-halt and draining
flag, queued prompt and buffered notification counts, pending self-chain,
and active request ref, origin and elapsed/watchdog time. It contains no
prompt, callback state, backend session or event payload. It is a
point-in-time observation, not durable state or permission to dispatch.
The older `status/2` API remains available; its `agent_state` is the
server's latest retained state, not a live read of an in-flight task.

Use `GenAgent.drain(name, timeout)` during an orderly shutdown. It rejects
new asks, tells, and acknowledged notifications with `{:error, :draining}`;
queued asks and completion recipients receive the same error. The active
turn finishes its callbacks before the agent and backend session terminate.
`drain/2` returns `:ok` only after that cleanup and process exit. The
default timeout is `:infinity`; a caller timeout does not cancel a drain
already accepted by the agent. `stop/1` remains immediate.

## Request completion messages

Use `tell_with_completion/4` when a caller needs the exact request ref
and an asynchronous terminal outcome:

```elixir
{:ok, ref} = GenAgent.tell_with_completion("my-coder", "Run the tests")

receive do
  {:gen_agent, :completion, "my-coder", ^ref, {:ok, response}} ->
    IO.puts(response.text)

  {:gen_agent, :completion, "my-coder", ^ref, {:error, reason}} ->
    IO.inspect(reason)
end
```

The optional third argument selects a recipient pid; it defaults to the
caller. The agent registers that recipient when it accepts the request,
so a fast response or `pre_turn/2` skip can send the completion before
the call returns. Match on the ref, which works with
`interrupt_request/3` while active or `cancel_request/3` while queued.
Pending-queue overload returns an error without a ref or completion
message. Accepted queued requests deliver after
their turn, or deliver `{:error, :cancelled}` when cancelled before
dispatch. Backend errors, gate skip/halt/invalid, interruption, and
watchdog expiry deliver error outcomes. For started turns, the message
is sent after turn decision and `post_turn/3` callbacks. It is sent at
most once for each accepted request.

Delivery does not depend on the bounded poll-result cache. It is an
in-memory BEAM send, not durable delivery: a dead recipient loses its
message, and agent death before a terminal outcome leaves the request
uncertain. Monitor the agent if its death matters; a replacement under
the same name uses new refs. A crashing decision callback stops the
agent before completion. Neither a completion nor an agent monitor
proves that an external provider process has settled.

## Pending input bounds

Each agent admits at most 1,000 pending prompts and 1,000 deferred
notifications by default, with a separate 1,048,576-byte payload cap
for each queue. Configure `:max_pending_prompts`,
`:max_pending_prompt_bytes`, `:max_pending_notifications`, and
`:max_pending_notification_bytes` in `start_agent/2` or `child_spec/2`.
Values must be non-negative integers; zero disables the corresponding
pending queue. Byte use is the sum of `:erlang.external_size/1` for
queued prompt strings or event terms. The active prompt and events
handled immediately while idle are outside these pending caps.

`ask/3` and `tell/3` return `{:error, {:overloaded, info}}` before
acceptance when a pending prompt does not fit. A rejected `tell/3`
returns no pollable ref. `info` identifies the queue, count or byte
limit, current count and bytes, incoming bytes, and configured maxima.
`notify_ack/3` returns `:ok` after the event is handled or retained, or
the same typed overload error if admission fails. It acknowledges
in-memory processing, not durable delivery. Existing `notify/2` is a
best-effort cast: its immediate `:ok` does not mean the event fit. Use
the rejection telemetry event to observe cast overloads.

Deferred event callbacks may generate prompts when a turn finishes.
Those prompts use the bounded pending-prompt queue; on overload,
`handle_error/3` receives the reason. Response follow-ups and error retries
use one reserved slot outside the prompt count cap, but must fit the prompt
byte cap. Error retries of ask and tell turns retain their original caller
and ref; response follow-ups are independent turns. A halted agent
retains admitted work until `resume/1`; interruption or completion
releases queue capacity as work is drained. These limits bound accepted
internal storage, not arbitrary messages already waiting in the BEAM
process mailbox before admission is processed. Stream-output capture
and external provider cancellation have their own contracts.

## Event capture bounds

Each turn retains at most 1,000 normalized events and 1,048,576 bytes of
event terms by default. Override these positive-integer limits with
`:max_events_per_turn` and `:max_event_bytes_per_turn` in `start_agent/2`
or `child_spec/2`. Byte usage is the sum of `:erlang.external_size/1`
for retained events, not total process memory.

The default `event_retention: :compact` delivers every normalized event to
`handle_stream_event/2` and continues to the terminal event. If the log
limit is reached, `Response.events` holds the unchanged prefix and
`Response.event_coverage` reports the observed, retained, and omitted
counts plus the first omission reason. `Response.terminal` keeps the full
terminal event separately; `Response.text`, usage, and session ID are built
from the full stream. Check coverage before using `Response.events` as a
complete record. Full response text, terminal payloads, callback state, and
provider decoder buffers can still grow beyond the event-log budget.

Set `event_retention: :lossless` to require the previous complete-log
contract. An event that exceeds either limit then ends the turn with
`{:error, {:event_capture_overflow, diagnostics}}`; the rejected event is
not delivered to `handle_stream_event/2`. The stream enumerable is halted,
but settlement of provider subprocesses or remote work remains the
backend's responsibility.

## Supervision

The package starts a fixed supervision tree on application boot:

```
GenAgent.Supervisor
  GenAgent.Registry          (Registry, keys: :unique)
  GenAgent.TaskSupervisor    (Task.Supervisor)
  GenAgent.AgentSupervisor   (DynamicSupervisor)
    <your agents under here>
```

To own agent and prompt-task lifetimes in your application, start a
`Task.Supervisor` before a `DynamicSupervisor` in your supervision tree:

```elixir
children = [
  {Task.Supervisor, name: MyApp.AgentTasks},
  {DynamicSupervisor, name: MyApp.Agents, strategy: :one_for_one}
]

{:ok, _owner} = Supervisor.start_link(children, strategy: :rest_for_one)

spec =
  GenAgent.child_spec(MyAgent,
    name: "worker-1",
    backend: MyBackend,
    task_supervisor: MyApp.AgentTasks
  )

{:ok, _agent} = DynamicSupervisor.start_child(MyApp.Agents, spec)
{:ok, response} = GenAgent.ask("worker-1", "Hello")
:ok = GenAgent.stop("worker-1", MyApp.Agents)
```

For an agent that starts with your application, a module that uses
`GenAgent` can also be a static supervisor child:

```elixir
children = [
  {Task.Supervisor, name: MyApp.AgentTasks},
  {MyApp.Watcher,
   name: "watcher",
   backend: MyBackend,
   task_supervisor: MyApp.AgentTasks}
]

{:ok, owner} = Supervisor.start_link(children, strategy: :rest_for_one)
{:ok, response} = GenAgent.ask("watcher", "Hello")
:ok = GenAgent.stop("watcher", owner)
```

`use GenAgent` provides an overridable `child_spec/1` for this tuple form.
It delegates to `GenAgent.child_spec/2`, so the options and validation are
the same. The child ID is its registered name. A static agent also has
`restart: :temporary`: after a stop or crash, the supervisor removes it
and does not restart it. Start a new agent explicitly if needed; its
previous state is not restored.

`child_spec/2` requires an explicit, running task supervisor; it never
silently uses the global one. Both globally and caller-owned agents use
`GenAgent.Registry`, so names must be unique across them and normal
name-based calls work for either. `stop/1` targets only the global agent
supervisor; pass the owning supervisor to `stop/2`.
The agent child is temporary and is never automatically replayed after
a crash. With `:rest_for_one`, failure of the task supervisor also stops
the agent supervisor. On ordinary shutdown, the agent supervisor stops
first so its agents can cancel in-flight tasks while their task supervisor
is still running. An application must reconstruct state and consumed work
from its own durable records; supervision alone does not provide recovery.
The [Chaos Lab](https://github.com/genagent/gen_agent/tree/main/examples/chaos_lab) example is a keyless, executable check of
the supervision contract.

Each prompt turn runs as a Task under its selected `Task.Supervisor`. A
crashed task delivers `:DOWN` to the owning agent, which turns it into an
`{:error, {:task_crashed, reason}}` response for the caller -- it does not
take down the agent process.
If the selected task supervisor is unavailable before a task can start,
the request fails with `:task_supervisor_unavailable`, reaches
`handle_error/3`, and leaves the agent idle. A callback's immediate retry
is discarded in this case; a later request can run after the supervisor
returns. When this happens during a caller-owned retry, the original ask or
poll receives the final `:task_supervisor_unavailable` error.

The prompt task belongs to its agent: it is stopped when the agent exits,
including abrupt exits that bypass termination callbacks. Interruption and
the watchdog also stop the active prompt task. Stopping a BEAM task does
not establish that a provider's subprocess or remote request has stopped;
external cancellation and resource cleanup belong to the backend and its
transport.

## Patterns

Eleven common topologies are documented as ex_doc guides shipped with
the package. Each guide is a complete worked example you can read,
copy, and adapt -- they are **not** installed as public API modules:

- **[Switchboard][sb]** -- human-managed named agent fleet with
  non-blocking send/poll/inbox, the base for manager-driven UIs
- **[Research][rs]** -- one agent self-chaining through phases
- **[Debate][db]** -- two agents pushing each other via cross-notify
- **[Pipeline][pl]** -- linear stage chain, one-way notify
- **[Supervisor][sv]** -- coordinator + dynamic workers (fan-out/in)
- **[Pool][pool]** -- reusable worker pool with round-robin dispatch
- **[Watcher][wc]** -- reactive event-driven agent, idle until triggered
- **[Heartbeat][hb]** -- periodic agent driven by timer events
- **[Checkpointer][cp]** -- human-in-the-loop review workflow
- **[Retry][rt]** -- caller-owned handle_error retries for transient failures
- **[Workspace][ws]** -- all four lifecycle hooks around a git workspace

Start with the [patterns overview][overview] for a "choose your
pattern" decision tree.

[overview]: https://hexdocs.pm/gen_agent/overview.html
[sb]: https://hexdocs.pm/gen_agent/switchboard.html
[rs]: https://hexdocs.pm/gen_agent/research.html
[db]: https://hexdocs.pm/gen_agent/debate.html
[pl]: https://hexdocs.pm/gen_agent/pipeline.html
[sv]: https://hexdocs.pm/gen_agent/supervisor.html
[pool]: https://hexdocs.pm/gen_agent/pool.html
[wc]: https://hexdocs.pm/gen_agent/watcher.html
[hb]: https://hexdocs.pm/gen_agent/heartbeat.html
[cp]: https://hexdocs.pm/gen_agent/checkpointer.html
[rt]: https://hexdocs.pm/gen_agent/retry.html
[ws]: https://hexdocs.pm/gen_agent/workspace.html

## Telemetry

GenAgent emits telemetry events for observability:

```
[:gen_agent, :turn, :start]      # content-free, %{agent, ref, origin}
[:gen_agent, :turn, :stop]       # content-free, duration_ms
[:gen_agent, :turn, :error]      # content-free, duration_ms, reason_kind
[:gen_agent, :turn, :rejected]   # declined before dispatch; no matching start
[:gen_agent, :prompt, :start]    # %{agent, ref}
[:gen_agent, :prompt, :stop]     # %{agent, ref, duration}
[:gen_agent, :prompt, :error]    # %{agent, ref, reason}
[:gen_agent, :event, :received]  # %{agent, event}
[:gen_agent, :state, :changed]   # %{agent, from, to}
[:gen_agent, :mailbox, :queued]  # %{agent, depth}
[:gen_agent, :input, :rejected]  # %{agent, reason: {:overloaded, info}}
[:gen_agent, :halted]            # %{agent}
[:gen_agent, :terminated]        # %{agent, reason}; callback-driven exits only
```

Enough to build a communication graph, track latency, alert on stuck
agents. Attach handlers with `:telemetry.attach/4`. Use the `:turn`
events for ordinary metrics: older prompt/event events can carry
prompts, raw errors, or agent state. `GenAgent.Telemetry` documents
measurements, units, ordering, and data handling.

## What GenAgent does not do

- **Prescribe agent behavior.** No retry logic, no STATUS line conventions,
  no summary format. That is all implementation concern.
- **Prescribe inter-agent communication.** Agents can `notify/2` each other
  by name, but the message format is up to you.
- **Manage persistence.** If you want to persist agent state across
  restarts, do it in `terminate_agent/2` and `init_agent/1`.
- **Manage pools.** One agent = one session = one process. If you want a
  pool, start multiple and route to them.
- **Track costs or budgets.** Usage data is in `GenAgent.Response.usage`.
  Do what you want with it.

## Testing

```bash
mix test
mix format --check-formatted
mix credo --strict
mix dialyzer
```

These commands check the root package. Run `scripts/quality.sh` to validate
all six packages; see the [contributor guide](https://github.com/genagent/gen_agent/blob/main/CONTRIBUTING.md)
for setup and release conventions.

`GenAgent.Backends.Mock` ships with the core package so applications can
test their own agent callbacks without a provider account or CLI. Return
`scripts:` from `init_agent/1`, one script per turn:

```elixir
def init_agent(opts) do
  events = [GenAgent.Event.new(:result, %{text: "done"})]
  {:ok, [scripts: [events]], %{observer: Keyword.fetch!(opts, :observer)}}
end

{:ok, _pid} = GenAgent.start_agent(MyAgent,
  name: "test-worker",
  backend: GenAgent.Backends.Mock,
  observer: self()
)

assert {:ok, response} = GenAgent.ask("test-worker", "work")
assert response.text == "done"
assert GenAgent.Backends.Mock.history("test-worker") == ["work"]
```

Use `GenAgent.Backends.Mock.gate/2` when a test needs to inspect an active
turn or queue before releasing it. See the module documentation for the
release message and supported script shapes.

## License

MIT. See [LICENSE](LICENSE).