Agents
The core building blocks: agents, providers, tools, memory, and guardrail hooks.
New here? Start with the Elixir overview & installation.
The agent loop
AgentSea.Agent is a GenServer that owns its conversation state and runs the loop: call the provider → if it requests tools, run them concurrently and feed the results back → repeat until the model answers or max_iterations is hit.
config = %AgentSea.Agent.Config{
name: :assistant,
model: "claude-opus-4-8",
provider: {AgentSea.Providers.Anthropic, []},
system_prompt: "You are concise and precise.",
temperature: 0.2,
max_iterations: 10
}
{:ok, agent} = AgentSea.Agent.start_link(config)
{:ok, response} = AgentSea.Agent.run(agent, "What is OTP?")
response.content # the answer
AgentSea.Agent.history(agent) # conversation so farProviders
Providers implement the AgentSea.Provider behaviour (complete/2 and an optional stream/2). The Anthropic provider lives in agentsea_providers and reads its key from ANTHROPIC_API_KEY (or a :api_key option):
provider: {AgentSea.Providers.Anthropic, api_key: System.fetch_env!("ANTHROPIC_API_KEY")}Because it's just a behaviour, you can write your own — or route across several providers with the gateway.
Tools
A tool is a module implementing AgentSea.Tool. When the model requests it, the agent runs it under a supervised Task — a crash becomes an {:error, _} the model sees, never a dead agent.
defmodule MyApp.Tools.Weather do
@behaviour AgentSea.Tool
@impl true
def name, do: "get_weather"
@impl true
def description, do: "Get the current weather for a city."
@impl true
def schema, do: [city: [type: :string, required: true]]
@impl true
def run(%{"city" => city}, _ctx), do: {:ok, "It is sunny in #{city}."}
end
# then:
config = %AgentSea.Agent.Config{
name: :assistant,
model: "claude-opus-4-8",
provider: {AgentSea.Providers.Anthropic, []},
tools: [MyApp.Tools.Weather]
}For ad-hoc or dynamically-discovered tools (e.g. from an MCP server), pass an AgentSea.Tool.Spec — a function-backed tool — in the same tools list:
%AgentSea.Tool.Spec{
name: "now",
description: "Return the current UTC time.",
run: fn _args, _ctx -> {:ok, DateTime.utc_now() |> DateTime.to_iso8601()} end
}Memory
AgentSea.Memory is a behaviour with three adapters: a windowed Buffer, an LLM-compacting Summary store, and a Vector store for semantic recall.
# Rolling window
{:ok, _} = AgentSea.Memory.Buffer.start_link(max_messages: 20)
AgentSea.Memory.Buffer.save("conv-1", messages)
AgentSea.Memory.Buffer.load("conv-1")
# Compact older turns into an LLM summary past a threshold
AgentSea.Memory.Summary.start_link(
provider: {AgentSea.Providers.Anthropic, []},
model: "claude-haiku-4-5",
keep_recent: 6,
threshold: 20
)Guardrail hooks
Agents accept optional input_guard / output_guardhooks — any function returning {:ok, content} (possibly rewritten) or {:block, reason}. That's exactly the shape of AgentSea.Guardrails.run/2, so a pipeline drops straight in:
alias AgentSea.Guardrail.{MaxLength, Blocklist, PIIRedactor}
config = %AgentSea.Agent.Config{
name: :assistant,
model: "claude-opus-4-8",
provider: {AgentSea.Providers.Anthropic, []},
input_guard:
&AgentSea.Guardrails.run(&1, [
{MaxLength, max: 4000},
{Blocklist, terms: ["ignore previous instructions"]}
]),
output_guard: &AgentSea.Guardrails.run(&1, [PIIRedactor])
}A blocked input short-circuits to {:error, {:guardrail, :input, reason}} before the model is ever called. See agentsea_guardrails for the built-ins (and an LLMGuard moderator).
Structured output
Need a typed result instead of prose? agentsea_structured extracts a validated Ecto struct, feeding validation errors back to the model and retrying:
{:ok, %Person{name: "Ada Lovelace", age: 36}} =
AgentSea.Structured.extract(Person, "Ada Lovelace, 36",
provider: {AgentSea.Providers.Anthropic, []},
model: "claude-opus-4-8"
)