v1.1.0 release - Contributors, Sponsors and Enquiries are most welcome 😌

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.

elixir
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 far

Providers

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):

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

elixir
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:

elixir
%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.

elixir
# 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:

elixir
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:

elixir
{:ok, %Person{name: "Ada Lovelace", age: 36}} =
  AgentSea.Structured.extract(Person, "Ada Lovelace, 36",
    provider: {AgentSea.Providers.Anthropic, []},
    model: "claude-opus-4-8"
  )