Current section

Files

Jump to
alloy lib alloy.ex
Raw

lib/alloy.ex

defmodule Alloy do
@moduledoc """
Model-agnostic agent harness for Elixir.
Alloy provides the minimal agent loop: send messages to any LLM,
execute tool calls, loop until done. Zero framework dependencies.
## Quick Start
{:ok, result} = Alloy.run("What is 2+2?",
provider: {Alloy.Provider.Anthropic, api_key: "sk-ant-..."},
system_prompt: "You are helpful."
)
result.text #=> "4"
## With Tools
{:ok, result} = Alloy.run("Read mix.exs and tell me the version",
provider: {Alloy.Provider.Anthropic, api_key: "sk-ant-..."},
tools: [Alloy.Tool.Core.Read],
max_turns: 10
)
## Continuing a Conversation
{:ok, result} = Alloy.run("Now edit that file",
provider: {Alloy.Provider.OpenAI, api_key: "sk-..."},
tools: [Alloy.Tool.Core.Read, Alloy.Tool.Core.Edit],
messages: previous_result.messages
)
## Options
- `:provider` - `{module, config_keyword_list}` or just `module` (required)
- `:tools` - list of modules implementing `Alloy.Tool` (default: `[]`)
- `:system_prompt` - system prompt string (default: `nil`)
- `:messages` - existing conversation history (default: `[]`)
- `:max_turns` - maximum agent loop iterations (default: `25`)
- `:max_tokens` - context window budget for compaction (default: `200_000`)
- `:middleware` - list of `Alloy.Middleware` modules (default: `[]`)
- `:working_directory` - base path for file tools (default: `"."`)
- `:context` - arbitrary map passed to tools and middleware (default: `%{}`)
- `:max_pending` - max queued async `send_message/3` requests while one is running (default: `0`)
"""
alias Alloy.Agent.{Config, Server, State, Turn}
alias Alloy.{Message, Result}
@type result :: Result.t()
@doc """
Send a message to a running agent without blocking the caller.
Non-blocking fire-and-forget. Returns `{:ok, request_id}` immediately.
Results are broadcast via PubSub. See `Alloy.Agent.Server.send_message/3`
for full documentation.
"""
@spec send_message(GenServer.server(), String.t(), keyword()) ::
{:ok, binary()} | {:error, :busy | :queue_full | :no_pubsub}
def send_message(server, message, opts \\ [])
defdelegate send_message(server, message, opts), to: Server
@doc """
Cancel an async request by `request_id`.
"""
@spec cancel_request(GenServer.server(), binary()) :: :ok | {:error, :not_found}
defdelegate cancel_request(server, request_id), to: Server
@doc """
Run the agent loop with a message and options.
The first argument can be a string (converted to a user message)
or ignored if `:messages` option provides conversation history.
Returns `{:ok, result}` on completion or `{:error, result}` on failure.
"""
@spec run(String.t() | nil, keyword()) :: {:ok, result()} | {:error, result()}
def run(message \\ nil, opts) do
config = Config.from_opts(opts)
messages = build_messages(message, opts)
state = %{State.init(config, messages) | status: :running}
try do
final_state = Turn.run_loop(state)
result = Result.from_state(final_state)
case final_state.status do
:completed -> {:ok, result}
:max_turns -> {:ok, result}
_ -> {:error, result}
end
after
State.cleanup(state)
end
end
defp build_messages(nil, opts) do
Keyword.get(opts, :messages, [])
end
defp build_messages(text, opts) when is_binary(text) do
existing = Keyword.get(opts, :messages, [])
existing ++ [Message.user(text)]
end
end