ExAgent.Capability behaviour (ExAgent v2.0.0)

View Source

Composable middleware that observes/transforms an agent run at well-defined points. This is ExAgent's "capabilities" spine, modelled the Elixir way: a behaviour whose callbacks default to no-ops via use ExAgent.Capability, so a capability overrides only the hooks it cares about.

A capability is a module or a configured struct. That value is passed as the first argument to its callbacks, keeping configuration explicit without global state. Hooks compose in list order.

Hooks (all optional)

  • before_model_request(cap, state) — return (possibly modified) state. Set state.request_messages to alter what's sent to the model without touching the canonical history (e.g. keep a sliding window, redact PII). Select state.model, state.settings and state.params.function_tools here. Selected tools are prepared and become this request's executable set; the wider agent inventory does not authorize omitted tools.
  • after_model_request(cap, state) — observe/modify state after the model responded (state already includes the new response + merged usage).
  • before_tool_execute(cap, ctx, tool_call) — observe or transform arguments, revalidated before authority. ID, name, kind and metadata are immutable for every Model; context includes call identity/retry fields.
  • after_tool_execute(cap, ctx, tool_call, result) — observe a tool result.

Model after-hooks transform the effective response before tool admission; their output still passes validation, limits and ancestor authority. Before-tool exceptions report :not_executed; after-tool failures retain confirmed effects. Hooks are trusted host code, not a sandbox. Run identity, scope handles, prepared validators and accounting fields are internal and must not be replaced.

In Model hooks, tool_retries, output_retries_used, run_step, tool_calls, max_steps and agent.output_retries are observable, runtime-owned values. Writes to these six fields are ignored: each callback's result restores the confirmed values before the next capability and before admission/persistence/IO. Configure limits before starting the run; callbacks cannot replenish budgets. Other valid agent/model/settings/tool selections and response/projection transformations remain supported, including selected tools' max_retries. This applies to actual runs, not generic maps passed to <code>ExAgent.Capabilities</code>. It is a behavioral change for the pending major; invalid callback results are not repaired into valid runs, and historical persisted counters are not migrated.

Example

defmodule MyApp.RedactPII do
  use ExAgent.Capability

  @impl true
  def before_model_request(_cap, state) do
    redacted = redact(state.messages)
    %{state | request_messages: redacted}
  end
end

ExAgent.new(model: "test", capabilities: [MyApp.RedactPII])

Summary

Types

state()

@type state() :: map()

Callbacks

after_model_request(cap, state)

(optional)
@callback after_model_request(cap :: module(), state()) :: state()

after_tool_execute(cap, map, struct, term)

(optional)
@callback after_tool_execute(cap :: module(), map(), struct(), term()) :: term()

before_model_request(cap, state)

(optional)
@callback before_model_request(cap :: module(), state()) :: state()

before_tool_execute(cap, map, struct)

(optional)
@callback before_tool_execute(cap :: module(), map(), struct()) :: struct()