Alloy.Agent.Server (alloy v0.12.4)

Copy Markdown View Source

OTP-backed persistent agent process.

Moved to alloy_agent

This module has moved to AlloyAgent.Server in the alloy_agent package. This copy will be removed in Alloy 0.13.0. Migrate by adding {:alloy_agent, "~> 0.1"} to your deps and changing alias Alloy.Agent.Server to alias AlloyAgent.Server (or alias AlloyAgent).

Why: Alloy is refocusing as a protocol library — the loop, providers, tools, memory behaviour, message types. Runtime concerns (supervised processes, sessions, async dispatch, PubSub, backpressure, default memory stores) belong in the app layer, where OTP already gives you the primitives. alloy_agent is the opt-in runtime for users who want a supervised agent without wiring a GenServer themselves.

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

result()

@type result() :: Alloy.Result.t()

Functions

cancel_request(server, request_id)

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

chat(server, message, opts \\ [])

@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).

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

export_session(server)

@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).

health(server)

@spec health(GenServer.server()) :: map()

Returns a health summary map for the agent process.

messages(server)

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

reset(server)

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

send_message(server, message, opts \\ [])

@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

set_model(server, provider_opts)

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

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Start a supervised, persistent agent process.

stop(server)

@spec stop(GenServer.server()) :: :ok

Stop the agent process.

stream_chat(server, message, on_chunk, opts \\ [])

@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).

usage(server)

@spec usage(GenServer.server()) :: Alloy.Usage.t()

Return accumulated token usage across all turns.