Unified LLM client — the host loop (SPEC §8, §10).
Give it a plain base_url + a style ("openai" | "anthropic") and it runs the whole
tool-calling agent loop against a toolkit — either a plain list of %Toolnexus.Tool{} or
any struct/map with :tools and :prompt fields (the skills prompt).
client = Toolnexus.Client.create(base_url: "...", style: "openai", model: "gpt-4o-mini")
result = Toolnexus.Client.run(client, "add 2 and 3", tools)
Summary
Functions
Stateful ask. With an :id (or a bare conversation-id string as the 4th argument), the
client's store remembers the conversation: load transcript → run → save. Without an id,
a stateless one-shot identical to run/4. With :on_text, streams and forwards text
deltas while still returning the final %RunResult{}.
§8 Gap 4. The client's conversation store — the exact instance passed in :store,
else the default in-memory store the client created.
Create a client. Options (SPEC §8 ClientOptions)
Prometheus text exposition of cumulative metrics (§8). Empty-but-valid before activity.
Run the agent loop: system prompt → LLM call → execute tool calls (concurrently, results
fed back in call order) → repeat to :max_turns. Returns a %RunResult{}.
Streaming variant: returns an Enumerable of event maps —
%{type: "text", delta}, %{type: "tool_call", id, name, args},
%{type: "tool_result", id, name, output, is_error}, %{type: "usage", usage},
%{type: "pending", request} (§10), %{type: "done", result}.
Types
@type t() :: %Toolnexus.Client{ api_key: term(), base_url: term(), body_transform: term(), deadline: term(), headers: term(), hooks: term(), http_options: term(), max_turns: term(), model: term(), on_metric: term(), registry: term(), request_params: term(), retries: term(), retry_base_ms: term(), store: term(), style: term(), system_prompt: term(), timeout_ms: term(), wait_for: term() }
Functions
Stateful ask. With an :id (or a bare conversation-id string as the 4th argument), the
client's store remembers the conversation: load transcript → run → save. Without an id,
a stateless one-shot identical to run/4. With :on_text, streams and forwards text
deltas while still returning the final %RunResult{}.
§8 Gap 4. The client's conversation store — the exact instance passed in :store,
else the default in-memory store the client created.
Create a client. Options (SPEC §8 ClientOptions):
:base_url,:style("openai"|"anthropic"),:model,:api_key(env fallback:OPENROUTER_API_KEY/OPENAI_API_KEY/ANTHROPIC_API_KEY),:headers:system_prompt— prepended to the toolkit's skills prompt (joined with"\n\n"):max_turns(default 10):hooks— map with:before_llm,:after_llm,:before_tool,:after_tool:retries(default 2),:retry_base_ms(default 500),:timeout_ms(whole-run deadline):store— aToolnexus.Client.ConversationStorestruct (default: in-memory):on_metric—(event_map -> any)semantic observability sink:wait_for— §10 suspension resolver,(Request -> Answer):request_params— map shallow-merged into every LLM body AFTER base keys (caller wins;messages/tools/streamforbidden) — §8 Gap 1:body_transform—(body -> body)run LAST after the merge — §8 Gap 1:http_options— extraReqoptions for the LLM path (§8 Gap 2)
Prometheus text exposition of cumulative metrics (§8). Empty-but-valid before activity.
@spec run(t(), String.t(), term(), keyword()) :: Toolnexus.Client.RunResult.t()
Run the agent loop: system prompt → LLM call → execute tool calls (concurrently, results
fed back in call order) → repeat to :max_turns. Returns a %RunResult{}.
opts: :history — a prior transcript to continue.
@spec stream(t(), String.t(), term(), keyword()) :: Enumerable.t()
Streaming variant: returns an Enumerable of event maps —
%{type: "text", delta}, %{type: "tool_call", id, name, args},
%{type: "tool_result", id, name, output, is_error}, %{type: "usage", usage},
%{type: "pending", request} (§10), %{type: "done", result}.
With an :id it is stateful like ask: history is loaded before streaming and the
transcript is saved back to the store on the terminal done event.