Toolnexus.Client (toolnexus v0.9.4)

Copy Markdown View Source

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

t()

@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

ask(client, prompt, toolkit, opts \\ [])

@spec ask(t(), String.t(), term(), keyword() | String.t()) ::
  Toolnexus.Client.RunResult.t()

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{}.

conversation_store(client)

@spec conversation_store(t()) :: struct()

§8 Gap 4. The client's conversation store — the exact instance passed in :store, else the default in-memory store the client created.

create(opts)

@spec create(keyword() | map()) :: t()

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 — a Toolnexus.Client.ConversationStore struct (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/stream forbidden) — §8 Gap 1
  • :body_transform(body -> body) run LAST after the merge — §8 Gap 1
  • :http_options — extra Req options for the LLM path (§8 Gap 2)

metrics(client)

@spec metrics(t()) :: String.t()

Prometheus text exposition of cumulative metrics (§8). Empty-but-valid before activity.

run(client, prompt, toolkit, opts \\ [])

@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.

stream(client, prompt, toolkit, opts \\ [])

@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.