LLM resolution and invocation for SubAgents.
Handles calling LLMs that can be either functions or atoms, with support for
LLM registry lookups for atom-based LLM references (like :haiku or :sonnet).
LLM responses are normalized to a consistent format:
- Plain string responses become
%{content: string, tokens: nil} - Map responses with
:contentkey 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 both functions and atom references.
Calculate total tokens from input and output token counts.
Types
@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
@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}}
@spec resolve(atom() | (map() -> {:ok, term()} | {:error, term()}), map(), map()) :: {:ok, normalized_response()} | {:error, term()}
Resolve and invoke an LLM, handling both 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- Either a function/1 or an atom referencing the registryinput- The LLM input map to pass to the callbackregistry- 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
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}}
@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