Eai.Hook behaviour (eai v1.0.0)

Copy Markdown

Behaviour + macro for user-level hook modules.

Events

There are four event types, two for tool calls and two for LLM HTTP requests:

EventScopeWhen
:preToolBefore tool execution
:postToolAfter tool execution
:llm_preLLMBefore each LLM HTTP request
:llm_postLLMAfter each LLM HTTP response

All events share the same interest/3 + verdict/3 / verdict/4 callbacks. The tool_name for LLM events is always "LLM_REQUEST".

Usage

defmodule MyHook do
  use Eai.Hook, priority: 10

  # ── Tool hooks ──────────────────────────────────────────

  @impl true
  def interest(:pre, tool_name, _payload),
    do: String.contains?(tool_name, "WriteToSession")
  def interest(_event, _tool, _payload), do: false

  @impl true
  def verdict(:pre, _tool, payload) do
    if payload.args |> hd() |> String.contains?("rm -rf") do
      {:block, "blocked dangerous command"}
    else
      :ok
    end
  end

  @impl true
  def verdict(:post, _tool, _payload, result), do: :ok

  # ── LLM hooks ───────────────────────────────────────────

  @impl true
  def interest(:llm_pre, "LLM_REQUEST", _payload), do: true
  def interest(:llm_post, "LLM_REQUEST", _payload), do: true

  @impl true
  def verdict(:llm_pre, _tool, _payload), do: :ok

  @impl true
  def verdict(:llm_post, _tool, _payload, result) do
    case result do
      {:error, _reason, _msgs} ->
        # handle error, e.g. log or rollback
        :ok
      _ -> :ok
    end
  end
end

LLM payload shape

llm_pre and llm_post receive:

%{
  messages: [Eai.Message.t()],
  pty_session_id: String.t(),
  chat_session_id: String.t(),
  opts: map()
}

llm_post additionally receives result as the 4th argument:

{:ok, reply_text, full_history} | {:error, reason, partial_history}

The post-hook can return {:modify, modified_result} to replace the return value that bubbles up to the Chat GenServer.

Callbacks

  • interest/3 — return true to opt-in. Keep fast: no I/O.
  • verdict/3 — pre-hook (:pre, :llm_pre): :ok | {:block, reason} | {:modify, new_value}

  • verdict/4 — post-hook (:post, :llm_post): :ok | {:block, reason} | {:modify, new_value}

Registered automatically

@before_compile injects register/0 which puts this module into :persistent_term under :eai_hooks. Called by Eai.Hub.Pipeline.register/1.

Summary

Callbacks

Return true if this hook wants to handle (event, tool_name, payload).

Pre-hook verdict: may allow, block, or modify the incoming value.

Post-hook verdict: may allow, block, or modify the result.

Functions

Injects @priority, @behaviour Eai.Hook, and @before_compile Eai.Hook into the calling module.

Callbacks

interest(event, tool_name, payload)

@callback interest(
  event :: :pre | :post | :llm_pre | :llm_post,
  tool_name :: String.t(),
  payload :: map()
) :: boolean()

Return true if this hook wants to handle (event, tool_name, payload).

verdict(event, tool_name, payload)

@callback verdict(
  event :: :pre | :llm_pre,
  tool_name :: String.t(),
  payload :: map()
) :: :ok | {:block, String.t()} | {:modify, any()}

Pre-hook verdict: may allow, block, or modify the incoming value.

For :pre (tool): payload has %{mod:, fun:, args:}. :modify expects [new_args]. For :llm_pre (LLM): payload has %{messages:, pty_session_id:, chat_session_id:, opts:}. :modify expects a map with the same keys (messages etc. can be changed).

verdict(event, tool_name, payload, result)

@callback verdict(
  event :: :post | :llm_post,
  tool_name :: String.t(),
  payload :: map(),
  result :: any()
) :: :ok | {:block, String.t()} | {:modify, any()}

Post-hook verdict: may allow, block, or modify the result.

For :post (tool): result is the tool's raw output. :modify returns a replacement result. For :llm_post (LLM): result is {:ok, reply, history} or {:error, reason, history}. :modify returns a replacement triple.

Functions

__using__(opts)

(macro)

Injects @priority, @behaviour Eai.Hook, and @before_compile Eai.Hook into the calling module.

Priority is a positive integer. Lower = runs first in the pre-pipeline, higher priority = runs last. Users own the numbering space entirely.