LemonAgent.Types.AgentTool (lemon_agent v0.1.0)

View Source

A tool the agent loop can call: a schema the model sees plus the function that runs when it calls it.

This is the unit every tool in the platform reduces to, whether it is a built-in, an MCP tool adapted by LemonMcp.ToolAdapter, or one contributed at runtime by an app the platform does not know about (see LemonAgent.ToolRegistry).

The shape

  • :name — what the model calls. Stable, lowercase, snake_case; it is matched against the model's tool call after whitespace normalisation, and it is the key everything else in the platform uses. Renaming one invalidates prompts and cached tool schemas, so treat it as an identity.
  • :description — what the model reads to decide whether to call it. This is prompt text, and it is the main lever on whether the tool gets used correctly; say what the tool does, when to use it and what its limits are.
  • :parameters — a JSON Schema object ("type" => "object" with "properties" and "required"), passed to the provider as-is. String keys, not atoms.
  • :label — a short human-readable name for UI display. Never shown to the model.
  • :execute — the function below.

The execute function

It is called as execute.(tool_call_id, params, signal, on_update):

  • tool_call_id — this invocation's id, for correlating events.
  • params — the arguments the model produced, decoded from JSON, so string keys and model-supplied values. Validate them; a model will eventually send a missing field or the wrong type, and a clear {:error, message} teaches it to retry correctly.
  • signal — an abort signal (see LemonAgent.AbortSignal), or nil. Long-running tools should check it and return early; ignoring it means a cancelled run keeps working.
  • on_update — a one-argument callback taking a partial AgentToolResult, or nil. Calling it pushes a streaming update to the UI. It is fire-and-forget and always returns :ok.

Return an AgentToolResult, {:ok, result} (equivalent), or {:error, reason}, which the loop turns into an error result the model can read and react to. Anything else is treated as an error.

The loop wraps execution in try, so a raise or throw becomes an error result rather than killing the run — but the model then gets an exception message instead of an explanation. Handle your own failures: a tool that is not configured should say so in its result (as XApi.Tools.PostToX does when its credentials are missing), not raise.

The module convention

A tool lives in a module exposing tool/1 (options) and tool/2 (cwd plus options) that build the struct. Both are expected — consumers that have a working directory call tool/2, those that do not call tool/1 — and the usual definition is def tool(_cwd, opts), do: tool(opts). Building the struct on each call rather than at compile time is what lets the description and parameters reflect current configuration.

defmodule MyApp.Tools.Greet do
  alias LemonAgent.Types.{AgentTool, AgentToolResult}
  alias LemonAi.Types.TextContent

  @spec tool(keyword()) :: AgentTool.t()
  def tool(_opts \\ []) do
    %AgentTool{
      name: "greet",
      label: "Greet",
      description: "Greet someone by name.",
      parameters: %{
        "type" => "object",
        "properties" => %{
          "name" => %{"type" => "string", "description" => "Who to greet"}
        },
        "required" => ["name"]
      },
      execute: &execute(&1, &2, &3, &4)
    }
  end

  def tool(_cwd, opts), do: tool(opts)

  def execute(_id, %{"name" => name}, _signal, _on_update) when is_binary(name) do
    %AgentToolResult{content: [%TextContent{text: "Hello, " <> name <> "!"}]}
  end

  def execute(_id, _params, _signal, _on_update) do
    {:error, "Missing required parameter: name"}
  end
end

Summary

Types

execute_fn()

@type execute_fn() :: (tool_call_id :: String.t(),
                 params :: map(),
                 signal :: reference() | nil,
                 on_update :: on_update() | nil ->
                   LemonAgent.Types.AgentToolResult.t()
                   | {:ok, LemonAgent.Types.AgentToolResult.t()}
                   | {:error, term()})

on_update()

@type on_update() :: (LemonAgent.Types.AgentToolResult.t() -> :ok)

t()

@type t() :: %LemonAgent.Types.AgentTool{
  description: String.t(),
  execute: execute_fn(),
  label: String.t(),
  name: String.t(),
  parameters: map()
}