ADK.Tool.LongRunningTool (ADK v0.0.1-alpha.1)

Copy Markdown View Source

An asynchronous tool that executes work in a supervised BEAM process.

Long-running tools spawn a Task under the ADK.RunnerSupervisor, allowing the tool function to send intermediate status updates while the caller awaits completion. This is the BEAM-idiomatic equivalent of Python ADK's LongRunningFunctionTool.

Key differences from Python ADK

Python ADK marks tools with is_long_running = True and relies on async/await coroutines. In Elixir, we use OTP processes:

  • Work runs in a supervised Task (fault-tolerant, isolated)
  • Status updates flow via process messages
  • Timeout is enforced via receive...after
  • Crashes in the tool are caught and returned as {:error, reason}

Usage

tool = ADK.Tool.LongRunningTool.new(:fetch_report,
  description: "Fetch and process a large report",
  func: fn _ctx, %{"url" => url}, send_update ->
    send_update.("Connecting to #{url}...")
    data = fetch_data(url)
    send_update.("Processing #{byte_size(data)} bytes...")
    process(data)
  end,
  parameters: %{
    type: "object",
    properties: %{url: %{type: "string", description: "Report URL"}},
    required: ["url"]
  },
  timeout: 30_000
)

Function signature

The func must accept 3 arguments:

  1. tool_ctxADK.ToolContext.t() (same as regular tools)
  2. argsmap() of tool arguments from the LLM
  3. send_update(String.t() -> :ok) callback to emit status updates

Return value

  • {:ok, result} — success with final result
  • {:ok, %{result: result, status_updates: [String.t()]}} — success with updates captured
  • {:error, reason} — tool error, timeout, or crash

Description annotation

The tool description is automatically annotated with a note telling the LLM not to call the tool again if it has already returned a pending/intermediate status (matching Python ADK's behavior).

Summary

Functions

Create a new long-running tool.

Execute the long-running tool asynchronously.

Types

t()

@type t() :: %ADK.Tool.LongRunningTool{
  description: String.t(),
  func: tool_func(),
  name: String.t(),
  parameters: map(),
  timeout: pos_integer()
}

tool_func()

@type tool_func() :: (ADK.ToolContext.t(), map(), update_fn() -> term())

update_fn()

@type update_fn() :: (String.t() -> :ok)

Functions

new(name, opts)

@spec new(
  atom() | String.t(),
  keyword()
) :: t()

Create a new long-running tool.

Options

  • :description — Human-readable description (automatically annotated with long-running notice)
  • :func — The tool function (tool_ctx, args, send_update) -> result
  • :parameters — JSON Schema map for parameters
  • :timeout — Milliseconds to wait before timing out (default: 60_000)

Examples

iex> tool = ADK.Tool.LongRunningTool.new(:slow_tool,
...>   description: "Does slow work",
...>   func: fn _ctx, _args, _send_update -> "done" end,
...>   parameters: %{type: "object", properties: %{}}
...> )
iex> tool.name
"slow_tool"
iex> String.contains?(tool.description, "long-running operation")
true

run(tool, tool_ctx, args)

@spec run(t(), ADK.ToolContext.t() | nil, map()) :: ADK.Tool.result()

Execute the long-running tool asynchronously.

Spawns a supervised Task under ADK.RunnerSupervisor, collects any status updates the function sends, and awaits the final result within the configured timeout.

Status updates from send_update.(msg) are collected in order. If any updates were sent, the result is wrapped: {:ok, %{result: final_value, status_updates: ["update 1", ...]}}. If no updates were sent, returns {:ok, final_value} directly.

Examples

iex> tool = ADK.Tool.LongRunningTool.new(:fast_tool,
...>   func: fn _ctx, %{"x" => x}, _send_update -> x * 2 end,
...>   parameters: %{},
...>   timeout: 5_000
...> )
iex> {:ok, result} = ADK.Tool.LongRunningTool.run(tool, nil, %{"x" => 21})
iex> result
42