Packages

AI agent framework for Elixir built on OTP. TEA-based agents with crash isolation, inter-agent messaging, team supervision, and real SSE streaming to Anthropic, OpenAI, Ollama, and more.

Current section

Files

Jump to
raxol_agent lib raxol agent.ex
Raw

lib/raxol/agent.ex

defmodule Raxol.Agent do
@moduledoc """
Thin layer over `Raxol.Core.Runtime.Application` for building AI agents.
Agents implement the same TEA callbacks (`init/1`, `update/2`, `view/1`)
as any Raxol app. Input comes from LLMs, tools, or other agents instead
of a keyboard. OTP provides supervision, crash isolation, and hot reload.
`view/1` is optional -- headless agents skip rendering entirely.
## Example
defmodule MyAgent do
use Raxol.Agent
def init(_context) do
%{findings: [], status: :idle}
end
def update({:agent_message, _from, {:analyze, file}}, model) do
{%{model | status: :working}, Directive.async(fn sender ->
result = do_analysis(file)
sender.({:analysis_done, result})
end)}
end
def update({:command_result, {:analysis_done, result}}, model) do
{%{model | findings: [result | model.findings], status: :idle}, []}
end
end
"""
defmacro __using__(_opts) do
quote do
@behaviour Raxol.Core.Runtime.Application
import Raxol.Core.Renderer.View, except: [view: 1]
alias Raxol.Agent.Directive
alias Raxol.Core.Events.Event
def init(_), do: %{}
def update(_, state), do: {state, []}
def view(_model), do: nil
def subscribe(_), do: []
def subscriptions(_), do: []
def handle_event(_), do: nil
def handle_tick(model), do: {model, []}
def handle_message(_, model), do: {model, []}
def terminate(_, _), do: :ok
@doc """
Returns compaction config for automatic session history compaction.
Override to customize token limits. Return `nil` to disable compaction.
Agents that store message history in `model.history` get automatic
compaction after each action in `Agent.Process`.
## Default Config
%{max_tokens: 8_000, preserve_recent: 4, summary_max_tokens: 1_000}
"""
def compaction_config do
%{max_tokens: 8_000, preserve_recent: 4, summary_max_tokens: 1_000}
end
@doc """
Returns a list of `Raxol.Agent.CommandHook` modules for this agent.
Hooks intercept command execution with pre/post semantics.
Return `[]` to disable hooks (default).
"""
def command_hooks, do: []
@doc """
Returns a list of `Raxol.Agent.Sandbox` structs declaring the
isolation dimensions that gate this agent's directives.
Defaults to `[]` (no sandboxing). When non-empty, consumers
using `Raxol.Agent.effective_hooks/1` get
`Raxol.Agent.SandboxHook` prepended automatically to the
`command_hooks/0` chain.
"""
def sandbox, do: []
@doc """
Returns the `Raxol.Agent.ThreadLog` adapter (`{module, config}`
or bare module) for durable audit, or `nil` to disable.
When non-nil, consumers wire
`Raxol.Agent.ThreadLogRouter.attach/3` at agent startup so
every policy decision and sandbox denial lands as a durable
event keyed on the agent's id.
"""
def thread_log, do: nil
@doc """
Returns the cross-session memory provider for this agent, or `nil`.
Override to return a `Raxol.Agent.Memory` provider module (e.g.
`Raxol.Agent.Memory.Store.Ets`). When set, memory actions are exposed
automatically and the agent should put
`Raxol.Agent.Memory.provider_context(provider, agent_id)` under
`context[:memory]` when invoking a Strategy or `Raxol.Agent.Stream`.
Defaults to `nil` (memory disabled).
"""
def memory_provider, do: nil
@doc """
Returns a list of memory providers to stack for this agent.
Each entry is a provider module or `{module, opts}` pair. When non-empty,
the runtime composes them via `Raxol.Agent.Memory.stack_context/3` so the
built-in store and external providers run together. Defaults to `[]`
(use the single `memory_provider/0`).
"""
def memory_providers, do: []
@doc """
Returns the procedural-memory (skills) provider for this agent, or `nil`.
Override to return a `Raxol.Agent.Skills.Store`-compatible module. When
set, the skill actions (`skills_list`, `skill_view`, `skill_manage`) are
exposed automatically and the agent should put
`Raxol.Agent.Skills.provider_context(provider)` under `context[:skills]`
when invoking a Strategy or `Raxol.Agent.Stream`. Defaults to `nil`
(skills disabled).
"""
def skills_provider, do: nil
@doc """
Returns the self-improvement config for this agent, or `nil` to disable.
When set, the runtime calls `Raxol.Agent.SelfImprove.after_turn/3` after
recording each turn. A qualifying turn (succeeded, at least
`min_tool_calls` tool calls) spawns a background reviewer that writes
durable memory and `created_by: :agent` skills on an auxiliary model.
%{enabled: true, backend: module, model: term, min_tool_calls: 5}
Defaults to `nil` (self-improvement disabled).
"""
def self_improve, do: nil
@doc """
Returns a list of Action modules available to this agent.
Used by `Agent.Process` with a Strategy to determine which
Actions can be invoked. Override to expose Actions as tools.
Defaults to the memory actions when `memory_provider/0` is set and the
skill actions when `skills_provider/0` is set, otherwise `[]`.
"""
def available_actions do
memory_on? = memory_provider() || not Enum.empty?(memory_providers())
memory = if memory_on?, do: Raxol.Agent.Actions.Memory.actions(), else: []
skills = if skills_provider(), do: Raxol.Agent.Actions.Skills.actions(), else: []
memory ++ skills
end
defoverridable init: 1,
update: 2,
view: 1,
subscribe: 1,
subscriptions: 1,
handle_event: 1,
handle_tick: 1,
handle_message: 2,
terminate: 2,
compaction_config: 0,
command_hooks: 0,
sandbox: 0,
thread_log: 0,
memory_provider: 0,
memory_providers: 0,
skills_provider: 0,
self_improve: 0,
available_actions: 0
@doc false
def async(fun), do: Directive.async(fun)
@doc false
def shell(command, opts \\ []), do: Directive.shell(command, opts)
@doc false
def send_agent(target, message), do: Directive.send_agent(target, message)
@doc "Run an action synchronously. Returns `{:ok, result}` or `{:error, reason}`."
@spec run_action(module(), map(), map()) ::
{:ok, term()} | {:error, term()}
def run_action(action_module, params, context \\ %{}) do
action_module.call(params, context)
end
@doc "Run an action asynchronously. Result arrives as `{:command_result, {:action_result, module, result}}`."
@spec run_action_async(module(), map(), map()) :: Directive.Async.t()
def run_action_async(action_module, params, context \\ %{}) do
Directive.async(fn sender ->
case action_module.call(params, context) do
{:ok, result} ->
sender.({:action_result, action_module, result})
{:ok, result, _commands} ->
sender.({:action_result, action_module, result})
{:error, reason} ->
sender.({:action_error, action_module, reason})
end
end)
end
@doc "Run a pipeline asynchronously. Result arrives as `{:command_result, {:pipeline_result, result}}`."
@spec run_pipeline_async([module()], map(), map()) :: Directive.Async.t()
def run_pipeline_async(steps, params, context \\ %{}) do
Directive.async(fn sender ->
case Raxol.Agent.Action.Pipeline.run(steps, params, context) do
{:ok, result, _commands} ->
sender.({:pipeline_result, result})
{:error, {step, reason}} ->
sender.({:pipeline_error, step, reason})
end
end)
end
end
end
# --- Module-level helpers (callable without `use Raxol.Agent`) ----------
@doc """
Return the effective hook chain for `agent_module`: prepends
`Raxol.Agent.SandboxHook` to `agent_module.command_hooks/0` when
the agent declares a non-empty `sandbox/0`.
Consumers wiring an agent into a runtime (Symphony's
`Runners.RaxolAgent`, the Agent.Session helper, etc) should pass
the result here as the hook list rather than reading
`command_hooks/0` directly.
Falls back to `[]` when `agent_module` doesn't export either
callback (e.g. an agent built without `use Raxol.Agent`).
"""
@spec effective_hooks(module()) :: [module()]
def effective_hooks(agent_module) when is_atom(agent_module) do
base = safe_call(agent_module, :command_hooks, [], [])
sandboxes = safe_call(agent_module, :sandbox, [], [])
case sandboxes do
[] -> base
_list -> [Raxol.Agent.SandboxHook | base]
end
end
@doc """
Return the normalized `Raxol.Agent.ThreadLog` adapter tuple from
`agent_module.thread_log/0`, or `nil` when the agent doesn't
declare one. Useful for `Raxol.Agent.ThreadLogRouter.attach/3`.
"""
@spec thread_log_adapter(module()) :: {module(), map()} | nil
def thread_log_adapter(agent_module) when is_atom(agent_module) do
agent_module
|> safe_call(:thread_log, [], nil)
|> Raxol.Agent.ThreadLog.normalize()
end
defp safe_call(module, fun, args, default) do
if function_exported?(module, fun, length(args)) do
apply(module, fun, args)
else
default
end
end
end