Normandy.Tool behaviour (normandy v2.2.0)

View Source

The contract every tool implements.

A tool is a struct whose module implements this behaviour. The struct holds the tool's configuration (an endpoint, a store, an index name), set by the application when it registers the tool (Normandy.Tools.Toolset.register/3). The model's input never goes into it: the dispatcher validates the input against input_schema/1 (Normandy.Tools.Input.validate/2) and passes it to run/2 as context.input.

Callbacks

  • name/1: the name the model calls the tool by, 1 to 64 letters, digits, _ or -.
  • description/1: what the tool does, for the model.
  • input_schema/1: the JSON Schema object of the input.
  • run/2: runs the tool. context.input is the validated input. Returns {:ok, result} or {:error, reason}; either reaches the model as the tool result.
  • check_context!/2 (optional): checks the agent's tool_context when the agent is built or the tool is registered.
  • native_definition/1 (optional): the provider-defined tool type the tool implements, which an adapter that supports it sends instead of the description and input schema.

use Normandy.Tool

defmodule MyApp.Search do
  defmodule Input do
    use Normandy.Schema

    io_schema "Search input" do
      field(:query, :string, required: true, description: "What to look for")
      field(:limit, :integer, default: 5, minimum: 1, maximum: 20)
    end
  end

  use Normandy.Tool, name: "search", description: "Searches the catalogue", input: Input

  defstruct [:index]

  @impl Normandy.Tool
  def run(%__MODULE__{index: index}, %Normandy.Tools.Context{input: %Input{} = input}),
    do: MyApp.Catalogue.search(index, input.query, input.limit)
end

use Normandy.Tool declares the behaviour, and each option generates one callback: :name → name/1, :description → description/1, :input (a module that uses Normandy.Schema, defined before the use) → input_schema/1. With :input, context.input is a struct of that schema. Leave an option out and write its callback when the value depends on the struct, or to hand-write the JSON Schema map; without :input, context.input is a string-keyed map checked against that map. Do not pass an option and also define its callback.

name/1, description/1, input_schema/1 and run/2 on this module call the tool's own callbacks.

Summary

Callbacks

Optional. Checks the agent's tool_context values (BaseAgentConfig.tool_context) when the tool and the context first meet: BaseAgent.init/1, BaseAgent.register_tool/3 and Normandy.Agents.ConfigTemplate.rebuild/3 (a Turn.Session built from a stored template). Returns :ok or raises ArgumentError, so a misconfiguration fails when the agent is built rather than on the model's first call. A context replaced for one call (as Normandy.A2A.Server does) is not checked here; run/2 still sees it.

Returns what the tool does, as a string for the model.

Returns the JSON Schema object that describes the tool's input, or nil for none.

Returns the name the model calls the tool by, 1 to 64 letters, digits, _ or -.

Optional. Returns the fields of a provider-defined tool the tool implements, such as %{type: "memory_20250818"} for Anthropic's memory tool, or nil for none.

Runs the tool for one call, with the validated input in context.input, and returns {:ok, result} or {:error, reason}.

Functions

Checks values (a tool_context) through the module's check_context!/2; :ok when the module defines none.

The tool's description, from its module's description/1.

Whether module implements this behaviour. Loads it first, so a module this node has not loaded yet (interactive mode) is not taken for a non-tool.

The tool's input JSON Schema, from its module's input_schema/1. nil when the tool has none (an MCP tool whose server sent no schema); such a tool's input is rejected.

The tool's name, from its module's name/1.

The tool's provider-defined fields, from its module's native_definition/1; nil when the module defines none.

Runs the tool through its module's run/2.

Callbacks

check_context!(tool, values)

(optional)
@callback check_context!(tool :: struct(), values :: map()) :: :ok

Optional. Checks the agent's tool_context values (BaseAgentConfig.tool_context) when the tool and the context first meet: BaseAgent.init/1, BaseAgent.register_tool/3 and Normandy.Agents.ConfigTemplate.rebuild/3 (a Turn.Session built from a stored template). Returns :ok or raises ArgumentError, so a misconfiguration fails when the agent is built rather than on the model's first call. A context replaced for one call (as Normandy.A2A.Server does) is not checked here; run/2 still sees it.

description(tool)

@callback description(tool :: struct()) :: String.t()

Returns what the tool does, as a string for the model.

use Normandy.Tool, description: "..." generates it.

input_schema(tool)

@callback input_schema(tool :: struct()) :: map() | nil

Returns the JSON Schema object that describes the tool's input, or nil for none.

The dispatcher validates the model's input against it before run/2; a tool that returns nil has its input rejected. use Normandy.Tool, input: Schema generates it from the schema module.

name(tool)

@callback name(tool :: struct()) :: String.t()

Returns the name the model calls the tool by, 1 to 64 letters, digits, _ or -.

Toolset.register/3 registers the tool under it. use Normandy.Tool, name: "..." generates it.

native_definition(tool)

(optional)
@callback native_definition(tool :: struct()) :: map() | nil

Optional. Returns the fields of a provider-defined tool the tool implements, such as %{type: "memory_20250818"} for Anthropic's memory tool, or nil for none.

Normandy.LLM.ClaudioAdapter sends these fields and the tool's name in place of its description and input schema; the model then calls the tool in the provider's shape, which input_schema/1 must still describe (the dispatcher validates against it). An adapter that does not know provider-defined tools must send the tool's name, description and input schema instead.

run(tool, t)

@callback run(tool :: struct(), Normandy.Tools.Context.t()) ::
  {:ok, term()} | {:error, term()}

Runs the tool for one call, with the validated input in context.input, and returns {:ok, result} or {:error, reason}.

Either reaches the model as the tool result; an error is marked as an error. A return value of any other shape, a raise, an exit or a throw becomes an error result as well, and so does running past the tool's timeout.

Functions

check_context!(tool, values)

@spec check_context!(
  struct(),
  map()
) :: :ok

Checks values (a tool_context) through the module's check_context!/2; :ok when the module defines none.

description(tool)

@spec description(struct()) :: String.t()

The tool's description, from its module's description/1.

implemented_by?(module)

@spec implemented_by?(atom()) :: boolean()

Whether module implements this behaviour. Loads it first, so a module this node has not loaded yet (interactive mode) is not taken for a non-tool.

input_schema(tool)

@spec input_schema(struct()) :: map() | nil

The tool's input JSON Schema, from its module's input_schema/1. nil when the tool has none (an MCP tool whose server sent no schema); such a tool's input is rejected.

name(tool)

@spec name(struct()) :: String.t()

The tool's name, from its module's name/1.

native_definition(tool)

@spec native_definition(struct()) :: map() | nil

The tool's provider-defined fields, from its module's native_definition/1; nil when the module defines none.

run(tool, context)

@spec run(
  struct(),
  Normandy.Tools.Context.t()
) :: {:ok, term()} | {:error, term()}

Runs the tool through its module's run/2.