lib/planck/agent/tool.ex

defmodule Planck.Agent.Tool do
  @moduledoc """
  An executable tool for use in agent turns.

  Extends `Planck.AI.Tool` with an `execute_fn` — the function called when the
  LLM requests a tool invocation. The AI tool schema (name, description, parameters)
  is extracted when building `Planck.AI.Context`; `execute_fn` stays agent-side.
  """

  @typedoc """
  The function invoked when the LLM requests this tool.

  Receives the calling `agent_id`, the tool call `id` (an opaque string from
  the provider, used to correlate results), and `args` (the JSON-decoded
  arguments map). Must return `{:ok, result}` or `{:error, reason}` where both
  values are strings — they are placed directly into the model's context as
  tool result text. Exceptions and exit signals are caught by the agent and
  converted to error strings automatically.
  """
  @type execute_fn ::
          (agent_id :: String.t(), id :: String.t(), args :: map() ->
             {:ok, String.t()} | {:error, String.t()})

  @typedoc """
  A fully-specified tool: schema fields understood by the LLM plus the
  `execute_fn` that runs when the model calls it.

  - `:name` — identifier the model uses to call the tool; must be unique within
    an agent's tool set
  - `:description` — natural-language description sent to the model; quality
    here directly affects how reliably the model uses the tool
  - `:parameters` — JSON Schema object describing the accepted arguments
  - `:execute_fn` — the function called with the agent id, tool call id, and decoded args
  """
  @type t :: %__MODULE__{
          name: String.t(),
          description: String.t(),
          parameters: map(),
          execute_fn: execute_fn()
        }

  @enforce_keys [:name, :description, :parameters, :execute_fn]
  defstruct [:name, :description, :parameters, :execute_fn]

  @doc """
  Build a `Planck.Agent.Tool` from keyword options.

  ## Examples

      iex> Tool.new(
      ...>   name: "read_file",
      ...>   description: "Read a file",
      ...>   parameters: %{"type" => "object", "properties" => %{"path" => %{"type" => "string"}}, "required" => ["path"]},
      ...>   execute_fn: fn _agent_id, _id, %{"path" => path} -> File.read(path) end
      ...> )
      %Planck.Agent.Tool{name: "read_file", ...}
  """
  @spec new(keyword()) :: t()
  def new(opts) do
    %__MODULE__{
      name: opts[:name],
      description: opts[:description],
      parameters: opts[:parameters],
      execute_fn: opts[:execute_fn]
    }
  end

  @doc """
  Convert to a `Planck.AI.Tool` for use in `Planck.AI.Context` (drops `execute_fn`).
  """
  @spec to_ai_tool(t()) :: Planck.AI.Tool.t()
  def to_ai_tool(%__MODULE__{} = tool) do
    alias Planck.AI.Tool, as: AITool

    AITool.new(
      name: tool.name,
      description: tool.description,
      parameters: tool.parameters
    )
  end
end