OTP-backed persistent agent process.
Wraps the stateless Turn.run_loop/1 in a GenServer so the agent
can hold conversation history across multiple calls, be supervised,
and run concurrently with other agents.
Usage
{:ok, pid} = Alloy.Agent.Server.start_link(
provider: {Alloy.Provider.Anthropic, api_key: "sk-ant-...", model: "claude-opus-4-6"},
tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Bash],
system_prompt: "You are a helpful assistant."
)
{:ok, r1} = Alloy.Agent.Server.chat(pid, "List the files in this project")
{:ok, r2} = Alloy.Agent.Server.chat(pid, "Now read mix.exs")
IO.puts(r2.text)
Alloy.Agent.Server.stop(pid)Options
All options from Alloy.run/2 are accepted at start time, plus:
:name- Register the process under a name (optional)
Supervision
children = [
{Alloy.Agent.Server, [
name: :my_agent,
provider: {Alloy.Provider.Anthropic, api_key: System.get_env("ANTHROPIC_API_KEY"), model: "claude-opus-4-6"}
]}
]
Supervisor.start_link(children, strategy: :one_for_one)
Summary
Functions
Cancel an async request by request_id.
Send a message and wait for the agent to finish its full loop.
Returns a specification to start this module under a supervisor.
Export the current conversation as a serializable Session struct.
Returns a health summary map for the agent process.
Return the full conversation message history.
Clear conversation history. Config and tools are preserved.
Send a message to the agent without blocking the caller.
Switch the provider (and its config) mid-session.
Start a supervised, persistent agent process.
Stop the agent process.
Send a message with streaming. Calls on_chunk for each text delta.
Returns the same result shape as chat/3.
Return accumulated token usage across all turns.
Types
@type result() :: Alloy.Result.t()
Functions
@spec cancel_request(GenServer.server(), binary()) :: :ok | {:error, :not_found}
Cancel an async request by request_id.
If the request is currently running, the active task is terminated. If the request is queued, it is removed from the queue.
When cancelled, the server broadcasts an {:agent_response, result} payload
with status: :error, error: :cancelled, and the matching :request_id.
@spec chat(GenServer.server(), String.t(), keyword()) :: {:ok, result()} | {:error, result()}
Send a message and wait for the agent to finish its full loop.
Blocks until the model reaches end_turn (including all tool calls).
Conversation history is preserved for subsequent calls.
Options
:timeout- GenServer call timeout in milliseconds (default:30_000).
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec export_session(GenServer.server()) :: Alloy.Session.t()
Export the current conversation as a serializable Session struct.
Note: if an async Turn is in progress via send_message/3, the exported
session reflects the state before that Turn started (a pre-Turn snapshot).
@spec health(GenServer.server()) :: map()
Returns a health summary map for the agent process.
@spec messages(GenServer.server()) :: [Alloy.Message.t()]
Return the full conversation message history.
Note: if an async Turn is in progress via send_message/3, this returns a
snapshot of the conversation before that Turn started. The in-flight
assistant response will appear only after the Turn completes.
@spec reset(GenServer.server()) :: :ok | {:error, :busy}
Clear conversation history. Config and tools are preserved.
Returns {:error, :busy} if an async Turn is currently running via send_message/3.
@spec send_message(GenServer.server(), String.t(), keyword()) :: {:ok, binary()} | {:error, :busy | :queue_full | :no_pubsub}
Send a message to the agent without blocking the caller.
Returns {:ok, request_id} immediately. The agent runs its full Turn loop
in a supervised Task, then broadcasts the result via PubSub to
"agent:<id>:responses" as {:agent_response, result} where result
includes a :request_id field matching the returned ID.
Backpressure behavior:
- If no Turn is running, the request starts immediately.
- If a Turn is running and
:max_pending > 0, the request is queued. - If the queue is full, returns
{:error, :queue_full}. - If
:max_pending == 0, returns{:error, :busy}while running.
Returns {:error, :no_pubsub} if the agent was started without a :pubsub
option — without PubSub there is no way to receive results.
Requirements
PubSub must be configured on the agent. Add pubsub: MyApp.PubSub to the
agent start options.
Options
:request_id- supply your own correlation ID (binary). Defaults to a random URL-safe ID.
Example
{:ok, agent} = Alloy.Agent.Server.start_link(
provider: {...},
pubsub: MyApp.PubSub
)
Phoenix.PubSub.subscribe(MyApp.PubSub, "agent:#{session_id}:responses")
{:ok, req_id} = Alloy.Agent.Server.send_message(agent, "Summarise the logs")
receive do
{:agent_response, %{request_id: ^req_id, text: text}} -> IO.puts(text)
end
@spec set_model( GenServer.server(), keyword() ) :: :ok | {:error, :busy}
Switch the provider (and its config) mid-session.
Accepts provider_opts in the same format as the :provider option in
start_link/1. Conversation history, tools, system prompt, and all
other config fields are preserved.
Examples
Server.set_model(pid, provider: {Alloy.Provider.Anthropic, api_key: key, model: "claude-haiku-4-5"})
Server.set_model(pid, provider: Alloy.Provider.OpenAI)Returns {:error, :busy} if an async Turn is currently running via send_message/3.
@spec start_link(keyword()) :: GenServer.on_start()
Start a supervised, persistent agent process.
@spec stop(GenServer.server()) :: :ok
Stop the agent process.
@spec stream_chat(GenServer.server(), String.t(), (String.t() -> :ok), keyword()) :: {:ok, result()} | {:error, result()}
Send a message with streaming. Calls on_chunk for each text delta.
Returns the same result shape as chat/3.
Options
:timeout- GenServer call timeout in milliseconds (default:30_000).
@spec usage(GenServer.server()) :: Alloy.Usage.t()
Return accumulated token usage across all turns.