PtcRunner.SubAgent.LLMResolver (PtcRunner v0.10.1)

Copy Markdown View Source

LLM resolution and invocation for SubAgents.

Handles calling LLMs that can be:

  • Strings (model aliases like "haiku" or full IDs like "openrouter:anthropic/claude-haiku-4.5")
  • Functions (direct callback functions)
  • Atoms (registry lookups for atom-based LLM references like :haiku)

LLM responses are normalized to a consistent format:

  • Plain string responses become %{content: string, tokens: nil}
  • Map responses with :content key preserve tokens if present

Summary

Types

Normalized LLM response with content, optional token counts, and optional tool calls.

Functions

Normalize an LLM response to a consistent format.

Resolve and invoke an LLM, handling strings, functions, and atom references.

Calculate total tokens from input and output token counts.

Types

normalized_response()

@type normalized_response() ::
  %{content: String.t() | nil, tokens: map() | nil}
  | %{content: String.t() | nil, tokens: map() | nil, tool_calls: [map()]}

Normalized LLM response with content, optional token counts, and optional tool calls.

For tool calling mode, the response may include tool_calls instead of or in addition to content.

Functions

normalize_response(response)

@spec normalize_response(String.t() | map()) :: normalized_response()

Normalize an LLM response to a consistent format.

Examples

iex> PtcRunner.SubAgent.LLMResolver.normalize_response("hello")
%{content: "hello", tokens: nil}

iex> PtcRunner.SubAgent.LLMResolver.normalize_response(%{content: "hello"})
%{content: "hello", tokens: nil}

iex> PtcRunner.SubAgent.LLMResolver.normalize_response(%{content: "hello", tokens: %{input: 10, output: 5}})
%{content: "hello", tokens: %{input: 10, output: 5}}

resolve(llm, input, registry)

@spec resolve(
  String.t() | atom() | (map() -> {:ok, term()} | {:error, term()}),
  map(),
  map()
) ::
  {:ok, normalized_response()} | {:error, term()}

Resolve and invoke an LLM, handling strings, functions, and atom references.

Normalizes the LLM response to always return a map with :content and :tokens keys. This provides a consistent interface for callers regardless of whether the LLM callback returns a plain string or a map with token information.

Parameters

  • llm - A model string (alias or full ID), function/1, or atom referencing the registry
  • input - The LLM input map to pass to the callback
  • registry - Map of atom to LLM callback for atom-based LLM references

Returns

  • {:ok, %{content: String.t(), tokens: map() | nil}} - Normalized response on success

  • {:error, reason} - Error tuple with reason on failure

Examples

# String model reference (resolved via PtcRunner.LLM.callback)
# PtcRunner.SubAgent.LLMResolver.resolve("haiku", %{...}, %{})

iex> llm = fn %{messages: [%{content: _}]} -> {:ok, "result"} end
iex> PtcRunner.SubAgent.LLMResolver.resolve(llm, %{messages: [%{content: "test"}]}, %{})
{:ok, %{content: "result", tokens: nil}}

iex> llm = fn _ -> {:ok, %{content: "result", tokens: %{input: 10, output: 5}}} end
iex> PtcRunner.SubAgent.LLMResolver.resolve(llm, %{messages: []}, %{})
{:ok, %{content: "result", tokens: %{input: 10, output: 5}}}

iex> registry = %{haiku: fn %{messages: _} -> {:ok, "response"} end}
iex> PtcRunner.SubAgent.LLMResolver.resolve(:haiku, %{messages: [%{content: "test"}]}, registry)
{:ok, %{content: "response", tokens: nil}}

total_tokens(tokens)

@spec total_tokens(map()) :: non_neg_integer()

Calculate total tokens from input and output token counts.

Examples

iex> PtcRunner.SubAgent.LLMResolver.total_tokens(%{input: 10, output: 5})
15

iex> PtcRunner.SubAgent.LLMResolver.total_tokens(%{input: 0, output: 0})
0

iex> PtcRunner.SubAgent.LLMResolver.total_tokens(%{})
0