Packages

Elixir port of Google's Agent Development Kit (ADK) — agent orchestration, sessions, tools, LLM abstraction, plugins, and telemetry. Transport-agnostic.

Current section

Files

Jump to
adk_ex lib adk plugin manager.ex
Raw

lib/adk/plugin/manager.ex

defmodule ADK.Plugin.Manager do
@moduledoc """
Chains plugin callbacks. First non-nil return wins (short-circuit).
All `run_*` functions accept `nil` as the first argument, acting as a
no-op when no plugin manager is configured. This avoids nil checks at
every call site.
"""
alias ADK.Plugin
@type t :: %__MODULE__{plugins: [Plugin.t()]}
defstruct plugins: []
@doc """
Creates a new plugin manager from a list of plugins.
Validates that plugin names are unique.
"""
@spec new([Plugin.t()]) :: {:ok, t()} | {:error, String.t()}
def new(plugins) do
names = Enum.map(plugins, & &1.name)
dupes = names -- Enum.uniq(names)
if dupes == [] do
{:ok, %__MODULE__{plugins: plugins}}
else
{:error, "Duplicate plugin names: #{Enum.join(Enum.uniq(dupes), ", ")}"}
end
end
# -- Runner-level callbacks --
@doc "Runs on_user_message plugins. May modify user content."
def run_on_user_message(nil, ctx, _content), do: {nil, ctx}
def run_on_user_message(%__MODULE__{} = mgr, ctx, content) do
run_chain(mgr.plugins, :on_user_message, ctx, fn plugin, acc_ctx ->
plugin.on_user_message.(acc_ctx, content)
end)
end
@doc "Runs before_run plugins. May short-circuit with Content."
def run_before_run(nil, ctx), do: {nil, ctx}
def run_before_run(%__MODULE__{} = mgr, ctx) do
run_chain(mgr.plugins, :before_run, ctx, fn plugin, acc_ctx ->
plugin.before_run.(acc_ctx)
end)
end
@doc "Runs after_run plugins. Notification only, no short-circuit."
def run_after_run(nil, _ctx), do: :ok
def run_after_run(%__MODULE__{} = mgr, ctx) do
Enum.each(mgr.plugins, fn plugin ->
if plugin.after_run, do: plugin.after_run.(ctx)
end)
:ok
end
@doc "Runs on_event plugins. May modify event."
def run_on_event(nil, ctx, _event), do: {nil, ctx}
def run_on_event(%__MODULE__{} = mgr, ctx, event) do
run_chain(mgr.plugins, :on_event, ctx, fn plugin, acc_ctx ->
plugin.on_event.(acc_ctx, event)
end)
end
# -- Agent-level callbacks --
@doc "Runs before_agent plugins. May short-circuit with Content."
def run_before_agent(nil, cb_ctx), do: {nil, cb_ctx}
def run_before_agent(%__MODULE__{} = mgr, cb_ctx) do
run_chain(mgr.plugins, :before_agent, cb_ctx, fn plugin, acc_ctx ->
plugin.before_agent.(acc_ctx)
end)
end
@doc "Runs after_agent plugins. May replace output with Content."
def run_after_agent(nil, cb_ctx), do: {nil, cb_ctx}
def run_after_agent(%__MODULE__{} = mgr, cb_ctx) do
run_chain(mgr.plugins, :after_agent, cb_ctx, fn plugin, acc_ctx ->
plugin.after_agent.(acc_ctx)
end)
end
# -- Model-level callbacks --
@doc "Runs before_model plugins. May short-circuit with LlmResponse."
def run_before_model(nil, cb_ctx, _request), do: {nil, cb_ctx}
def run_before_model(%__MODULE__{} = mgr, cb_ctx, request) do
run_chain(mgr.plugins, :before_model, cb_ctx, fn plugin, acc_ctx ->
plugin.before_model.(acc_ctx, request)
end)
end
@doc "Runs after_model plugins. May replace LlmResponse."
def run_after_model(nil, cb_ctx, _response), do: {nil, cb_ctx}
def run_after_model(%__MODULE__{} = mgr, cb_ctx, response) do
run_chain(mgr.plugins, :after_model, cb_ctx, fn plugin, acc_ctx ->
plugin.after_model.(acc_ctx, response)
end)
end
@doc "Runs on_model_error plugins. May recover with LlmResponse."
def run_on_model_error(nil, cb_ctx, _request, _error), do: {nil, cb_ctx}
def run_on_model_error(%__MODULE__{} = mgr, cb_ctx, request, error) do
run_chain(mgr.plugins, :on_model_error, cb_ctx, fn plugin, acc_ctx ->
plugin.on_model_error.(acc_ctx, request, error)
end)
end
# -- Tool-level callbacks --
@doc "Runs before_tool plugins. May short-circuit with result map."
def run_before_tool(nil, tool_ctx, _tool, _args), do: {nil, tool_ctx}
def run_before_tool(%__MODULE__{} = mgr, tool_ctx, tool, args) do
run_chain(mgr.plugins, :before_tool, tool_ctx, fn plugin, acc_ctx ->
plugin.before_tool.(acc_ctx, tool, args)
end)
end
@doc "Runs after_tool plugins. May replace result map."
def run_after_tool(nil, tool_ctx, _tool, _args, _result), do: {nil, tool_ctx}
def run_after_tool(%__MODULE__{} = mgr, tool_ctx, tool, args, result) do
run_chain(mgr.plugins, :after_tool, tool_ctx, fn plugin, acc_ctx ->
plugin.after_tool.(acc_ctx, tool, args, result)
end)
end
@doc "Runs on_tool_error plugins. May recover with result map."
def run_on_tool_error(nil, tool_ctx, _tool, _error), do: {nil, tool_ctx}
def run_on_tool_error(%__MODULE__{} = mgr, tool_ctx, tool, error) do
run_chain(mgr.plugins, :on_tool_error, tool_ctx, fn plugin, acc_ctx ->
plugin.on_tool_error.(acc_ctx, tool, error)
end)
end
# -- Internal --
# Iterates plugins in order. For each plugin with a non-nil callback for
# the given key, calls the callback function. First non-nil value wins.
defp run_chain(plugins, callback_key, ctx, callback_fn) do
Enum.reduce_while(plugins, {nil, ctx}, fn plugin, {_val, acc_ctx} ->
if Map.get(plugin, callback_key) do
apply_callback(callback_fn, plugin, acc_ctx)
else
{:cont, {nil, acc_ctx}}
end
end)
end
defp apply_callback(callback_fn, plugin, acc_ctx) do
case callback_fn.(plugin, acc_ctx) do
{nil, updated_ctx} -> {:cont, {nil, updated_ctx}}
{value, updated_ctx} -> {:halt, {value, updated_ctx}}
end
end
end