ADK.Plugin behaviour (ADK v0.0.1-alpha.1)

Copy Markdown View Source

Global plugin behaviour for intercepting the Runner pipeline.

Unlike callbacks (per-invocation, per-agent), plugins are global — registered at the application level and applied to every Runner.run/5 call. Plugins can inspect/transform the context before execution, and inspect/transform results after.

Behaviour

Implement any subset of callbacks:

  • init/1 — initialize plugin state from config, return {:ok, state}
  • before_run/2 — called before Runner executes, receives {context, plugin_state}, returns {:cont, context, state} or {:halt, result, state}
  • after_run/3 — called after Runner executes, receives {result, context, plugin_state}, returns {result, state}

Per-model and per-tool hooks (stateless)

These hooks are called inline during LLM agent execution. They do not carry plugin state (use ETS or GenServer in init/1 for statefulness across calls):

  • before_model/2 — called before each LLM call; can modify the request or skip the call entirely by returning a canned response
  • after_model/2 — called after each LLM call; can transform the response
  • before_tool/3 — called before each tool execution; can modify args or skip the tool call by returning a canned result
  • after_tool/3 — called after each tool execution; can transform the result
  • on_event/2 — called for each event emitted during execution; observe-only

Example

defmodule MyPlugin do
  @behaviour ADK.Plugin

  @impl true
  def init(config), do: {:ok, config}

  @impl true
  def before_run(context, state) do
    {:cont, context, state}
  end

  @impl true
  def after_run(result, _context, state) do
    {result, state}
  end

  @impl true
  def before_model(context, request) do
    # Inject extra context into every model request
    {:ok, Map.put(request, :extra, "injected")}
  end

  @impl true
  def after_model(_context, response) do
    response
  end

  @impl true
  def before_tool(_context, tool_name, args) do
    IO.puts("Calling tool: #{tool_name}")
    {:ok, args}
  end

  @impl true
  def after_tool(_context, _tool_name, result) do
    result
  end

  @impl true
  def on_event(_context, event) do
    IO.inspect(event, label: "event")
    :ok
  end
end

# Register globally
ADK.Plugin.Registry.start_link([])
ADK.Plugin.register({MyPlugin, my_config: true})

Summary

Callbacks

Called after each LLM model call.

Called after Runner.run executes the agent.

Called after each tool execution.

Called before each LLM model call.

Called before Runner.run executes the agent.

Called before each tool execution.

Initialize plugin state from config. Return {:ok, state}.

Called for each event emitted during execution (observe-only).

Called when an LLM model call fails.

Called when a tool execution fails.

Functions

List all registered plugins as [{module, state}].

Register a plugin globally. Accepts module or {module, config}.

Run after_run hooks for a list of {module, state} tuples.

Run after_model hooks for all registered plugins, threading the result through each.

Run after_tool hooks for all registered plugins, threading the result through each.

Run before_run hooks for a list of {module, state} tuples.

Run before_model hooks for all registered plugins.

Run before_tool hooks for all registered plugins.

Run on_event hooks for all registered plugins.

Run on_model_error hooks for all registered plugins, threading the error through each.

Run on_tool_error hooks for all registered plugins, threading the error through each.

Types

state()

@type state() :: term()

Callbacks

after_model(t, arg2)

(optional)
@callback after_model(ADK.Context.t(), {:ok, map()} | {:error, term()}) ::
  {:ok, map()} | {:error, term()}

Called after each LLM model call.

Receives the raw LLM result and may return a transformed result.

after_run(list, t, state)

(optional)
@callback after_run([ADK.Event.t()], ADK.Context.t(), state()) ::
  {[ADK.Event.t()], state()}

Called after Runner.run executes the agent.

Receives the result (list of events), the context, and plugin state. Return {result, new_state}.

after_tool(t, tool_name, result)

(optional)
@callback after_tool(ADK.Context.t(), tool_name :: String.t(), ADK.Tool.result()) ::
  ADK.Tool.result()

Called after each tool execution.

Receives the tool result and may return a transformed result.

before_model(t, request)

(optional)
@callback before_model(ADK.Context.t(), request :: map()) ::
  {:ok, map()} | {:skip, {:ok, map()} | {:error, term()}}

Called before each LLM model call.

Return {:ok, request} to continue (possibly with a modified request), or {:skip, response} to skip the model call entirely and use the given response.

The response in {:skip, response} should be {:ok, map()} or {:error, term()}.

before_run(t, state)

(optional)
@callback before_run(ADK.Context.t(), state()) ::
  {:cont, ADK.Context.t(), state()} | {:halt, term(), state()}

Called before Runner.run executes the agent.

Return {:cont, context, new_state} to continue or {:halt, result, new_state} to short-circuit.

before_tool(t, tool_name, args)

(optional)
@callback before_tool(ADK.Context.t(), tool_name :: String.t(), args :: map()) ::
  {:ok, map()} | {:skip, ADK.Tool.result()}

Called before each tool execution.

Return {:ok, args} to continue (possibly with modified args), or {:skip, result} to skip the tool and return the given result directly.

init(config)

(optional)
@callback init(config :: term()) :: {:ok, state()}

Initialize plugin state from config. Return {:ok, state}.

on_event(t, t)

(optional)
@callback on_event(ADK.Context.t(), ADK.Event.t()) :: :ok

Called for each event emitted during execution (observe-only).

Always return :ok. Use this for logging, telemetry, or side effects.

on_model_error(t, {})

(optional)
@callback on_model_error(
  ADK.Context.t(),
  {:error, term()}
) :: {:ok, map()} | {:error, term()}

Called when an LLM model call fails.

Return {:ok, response} to recover and use the fake response, or {:error, new_error} to continue the error chain.

on_tool_error(t, tool_name, {})

(optional)
@callback on_tool_error(ADK.Context.t(), tool_name :: String.t(), {:error, term()}) ::
  ADK.Tool.result()

Called when a tool execution fails.

Return {:ok, response} to recover and use the fake response, or {:error, new_error} to continue the error chain.

Functions

list()

@spec list() :: [{module(), state()}]

List all registered plugins as [{module, state}].

register(plugin)

@spec register(module() | {module(), term()}) :: :ok

Register a plugin globally. Accepts module or {module, config}.

run_after(plugins, result, context)

@spec run_after([{module(), state()}], [ADK.Event.t()], ADK.Context.t()) ::
  {[ADK.Event.t()], [{module(), state()}]}

Run after_run hooks for a list of {module, state} tuples.

Returns {result, updated_plugins}.

run_after_model(plugins, ctx, response)

@spec run_after_model(
  [{module(), state()}],
  ADK.Context.t(),
  {:ok, map()} | {:error, term()}
) ::
  {:ok, map()} | {:error, term()}

Run after_model hooks for all registered plugins, threading the result through each.

Plugins that don't implement after_model/2 are skipped.

run_after_tool(plugins, ctx, tool_name, result)

@spec run_after_tool(
  [{module(), state()}],
  ADK.Context.t(),
  String.t(),
  ADK.Tool.result()
) ::
  ADK.Tool.result()

Run after_tool hooks for all registered plugins, threading the result through each.

Plugins that don't implement after_tool/3 are skipped.

run_before(plugins, context)

@spec run_before([{module(), state()}], ADK.Context.t()) ::
  {:cont, ADK.Context.t(), [{module(), state()}]}
  | {:halt, term(), [{module(), state()}]}

Run before_run hooks for a list of {module, state} tuples.

Returns {:cont, context, updated_plugins} or {:halt, result, updated_plugins}.

run_before_model(plugins, ctx, request)

@spec run_before_model([{module(), state()}], ADK.Context.t(), map()) ::
  {:ok, map()} | {:skip, {:ok, map()} | {:error, term()}}

Run before_model hooks for all registered plugins.

Returns {:ok, final_request} if all plugins continue, or {:skip, response} if any plugin short-circuits the model call.

Plugins that don't implement before_model/2 are skipped.

run_before_tool(plugins, ctx, tool_name, args)

@spec run_before_tool([{module(), state()}], ADK.Context.t(), String.t(), map()) ::
  {:ok, map()} | {:skip, ADK.Tool.result()}

Run before_tool hooks for all registered plugins.

Returns {:ok, final_args} if all plugins continue, or {:skip, result} if any plugin short-circuits the tool call.

Plugins that don't implement before_tool/3 are skipped.

run_on_event(plugins, ctx, event)

@spec run_on_event([{module(), state()}], ADK.Context.t(), ADK.Event.t()) :: :ok

Run on_event hooks for all registered plugins.

All plugins that implement on_event/2 are called. Errors are ignored. Always returns :ok.

Plugins that don't implement on_event/2 are skipped.

run_on_model_error(plugins, ctx, error)

@spec run_on_model_error([{module(), state()}], ADK.Context.t(), {:error, term()}) ::
  {:ok, map()} | {:error, term()}

Run on_model_error hooks for all registered plugins, threading the error through each.

If any plugin recovers the error and returns {:ok, response}, subsequent plugins are skipped for the error chain and the error is considered handled.

run_on_tool_error(plugins, ctx, tool_name, error)

@spec run_on_tool_error(
  [{module(), state()}],
  ADK.Context.t(),
  String.t(),
  {:error, term()}
) ::
  ADK.Tool.result()

Run on_tool_error hooks for all registered plugins, threading the error through each.