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 command_hook.ex
Raw

lib/raxol/agent/command_hook.ex

defmodule Raxol.Agent.CommandHook do
@moduledoc """
Pre/post execution hooks for agent commands.
Hooks can inspect, modify, or deny commands before and after execution.
This enables audit logging, permission checks, rate limiting, and other
cross-cutting concerns without modifying individual agent modules.
## Usage
Define a hook module:
defmodule MyAuditHook do
@behaviour Raxol.Agent.CommandHook
@impl true
def pre_execute(command, context) do
Logger.info("Executing: \#{inspect(command.type)}")
{:ok, command}
end
@impl true
def post_execute(command, result, context) do
Logger.info("Result: \#{inspect(result)}")
{:ok, result}
end
end
Register hooks when starting an agent:
Agent.Session.start_link(
id: :my_agent,
app_module: MyAgent,
hooks: [MyAuditHook, MyPermissionHook]
)
## Hook Results
- `{:ok, command}` -- allow (optionally modified)
- `{:deny, reason}` -- block execution, send denial as command result
- `{:ok, result}` -- post-hook, optionally modify result
"""
alias Raxol.Agent.Directive
alias Raxol.Agent.Directive.{Async, SendAgent, Shell}
@type effect :: Directive.t()
@type hook_context :: %{
agent_id: term(),
agent_module: module()
}
@type pre_result :: {:ok, effect()} | {:deny, term()}
@type post_result :: {:ok, term()} | {:error, term()}
@doc """
Called before a command is executed.
Return `{:ok, command}` to allow (optionally modify the command),
or `{:deny, reason}` to block execution.
"""
@callback pre_execute(command :: effect(), context :: hook_context()) ::
pre_result()
@doc """
Called after a command has been executed.
Return `{:ok, result}` to pass through (optionally modify the result).
"""
@callback post_execute(
command :: effect(),
result :: term(),
context :: hook_context()
) :: post_result()
@optional_callbacks post_execute: 3
@doc """
Run a command through a chain of pre-execution hooks.
Hooks are evaluated in order. The first `:deny` short-circuits the chain.
Each hook may modify the command before passing it to the next.
"""
@spec run_pre_hooks([module()], effect(), hook_context()) :: pre_result()
def run_pre_hooks([], command, _context), do: {:ok, command}
def run_pre_hooks([hook | rest], command, context) do
case hook.pre_execute(command, context) do
{:ok, command} -> run_pre_hooks(rest, command, context)
{:deny, _reason} = denied -> denied
end
end
@doc """
Run a result through a chain of post-execution hooks.
Hooks are evaluated in order. Each hook may modify the result.
"""
@spec run_post_hooks([module()], effect(), term(), hook_context()) ::
post_result()
def run_post_hooks([], _command, result, _context), do: {:ok, result}
def run_post_hooks([hook | rest], command, result, context) do
if function_exported?(hook, :post_execute, 3) do
case hook.post_execute(command, result, context) do
{:ok, result} -> run_post_hooks(rest, command, result, context)
{:error, _reason} = error -> error
end
else
run_post_hooks(rest, command, result, context)
end
end
@doc """
Wrap a list of directives, applying pre/post hooks around each execution.
Returns a new list where hookable directives (`Async`, `Shell`,
`SendAgent`) are wrapped to run hooks. Other directives pass through
unchanged.
Denied directives are replaced with an `Async` directive that sends a
`{:command_denied, type, reason}` result back to the agent.
"""
@spec wrap_commands([effect()], [module()], hook_context()) :: [effect()]
def wrap_commands(commands, [], _context), do: commands
def wrap_commands(commands, hooks, context) do
Enum.map(commands, &maybe_wrap(&1, hooks, context))
end
defp maybe_wrap(%Async{} = directive, hooks, context),
do: apply_hooks(directive, hooks, context)
defp maybe_wrap(%Shell{} = directive, hooks, context),
do: apply_hooks(directive, hooks, context)
defp maybe_wrap(%SendAgent{} = directive, hooks, context),
do: apply_hooks(directive, hooks, context)
defp maybe_wrap(effect, _hooks, _context), do: effect
defp apply_hooks(effect, hooks, context) do
case run_pre_hooks(hooks, effect, context) do
{:ok, effect} -> wrap_with_post_hooks(effect, hooks, context)
{:deny, reason} -> build_denial(effect, reason)
end
end
defp build_denial(directive, reason) do
type = effect_type(directive)
Directive.async(fn sender ->
sender.({:command_denied, type, reason})
end)
end
defp effect_type(%Async{}), do: :async
defp effect_type(%Shell{}), do: :shell
defp effect_type(%SendAgent{}), do: :send_agent
defp wrap_with_post_hooks(effect, hooks, context) do
if Enum.any?(hooks, &function_exported?(&1, :post_execute, 3)) do
do_wrap_post(effect, hooks, context)
else
effect
end
end
defp do_wrap_post(%Async{fun: fun} = directive, hooks, context) do
%{directive | fun: wrap_sender_fun(fun, directive, hooks, context)}
end
defp do_wrap_post(effect, _hooks, _context), do: effect
defp wrap_sender_fun(original_fun, effect, hooks, context) do
fn sender ->
original_fun.(build_wrapped_sender(sender, effect, hooks, context))
end
end
defp build_wrapped_sender(sender, effect, hooks, context) do
fn result ->
case run_post_hooks(hooks, effect, result, context) do
{:ok, modified} -> sender.(modified)
{:error, reason} -> sender.({:hook_error, reason})
end
end
end
end