Liking cljdoc? Tell your friends :D

toolnexus.client


add-usageclj/s

(add-usage acc client raw)

Sum token usage across turns. OpenAI prompt/completion/total_tokens; Anthropic input_tokens -> prompt, output_tokens -> completion. Public because §11 translation reports the same Usage from the same provider payloads.

Sum token usage across turns. OpenAI prompt/completion/total_tokens;
Anthropic input_tokens -> prompt, output_tokens -> completion. Public because
§11 translation reports the same Usage from the same provider payloads.
sourceraw docstring

after-llm!clj/s

(after-llm! client response turn)
(after-llm! client response turn model)

§8 afterLLM — observe only (logging, cost, tracing). :response is the provider's decoded payload, so it carries usage. Returns nil; a return value from an observer would be a silent contract nobody could rely on.

§8 `afterLLM` — observe only (logging, cost, tracing). `:response` is the
provider's decoded payload, so it carries `usage`. Returns nil; a return value
from an observer would be a silent contract nobody could rely on.
sourceraw docstring

answer-declinedclj/s

(answer-declined id)
(answer-declined id reason)

§10 Answer for a request the host will NOT grant (addendum A9, shipped in all seven ports beside answer-output). R1: reason is carried only here, because the loop rule branches on ok alone.

§10 `Answer` for a request the host will NOT grant (addendum A9, shipped in
all seven ports beside `answer-output`). R1: `reason` is carried only here,
because the loop rule branches on `ok` alone.
sourceraw docstring

answer-outputclj/s

(answer-output id output)
(answer-output id output is-error)

§10 Answer carrying ONE tool output for a durable resume (ADR 0026: the constructor exists so the map stops being hand-built and there is no key left to get wrong). Recognised payload keys are results then output, in that precedence (addendum A3); this builds the output shape.

A non-string output THROWS. Degrading it to "" is how a wrong answer reaches the model quietly, which is the whole defect this constructor closes.

§10 `Answer` carrying ONE tool output for a durable resume (ADR 0026: the
constructor exists so the map stops being hand-built and there is no key left
to get wrong). Recognised payload keys are `results` then `output`, in that
precedence (addendum A3); this builds the `output` shape.

A non-string `output` THROWS. Degrading it to "" is how a wrong answer
reaches the model quietly, which is the whole defect this constructor closes.
sourceraw docstring

as-answerclj/s

(as-answer answer)

§10 Answer, normalised so a STRING-KEYED map means what it says.

§10 pins Answer's keys because they cross the wire, and that is precisely why a host round-tripping an Answer through JSON — a database column, a webhook body, a queue — hands this port {"id" … "ok" true}. Before this, (:ok answer) read nil from such a map and the run treated a GRANTED answer as DECLINED: no error, no log, the opposite outcome (issue #89, ADR 0026 §5). It is the worst failure mode in that batch precisely because it is silent.

Only the TOP-LEVEL keys are normalised. :data is the host's own payload and is handed to the tool untouched.

§10 `Answer`, normalised so a STRING-KEYED map means what it says.

§10 pins `Answer`'s keys because they cross the wire, and that is precisely
why a host round-tripping an Answer through JSON — a database column, a
webhook body, a queue — hands this port `{"id" … "ok" true}`. Before this,
`(:ok answer)` read nil from such a map and the run treated a GRANTED answer
as DECLINED: no error, no log, the opposite outcome (issue #89, ADR 0026 §5).
It is the worst failure mode in that batch precisely because it is silent.

Only the TOP-LEVEL keys are normalised. `:data` is the host's own payload and
is handed to the tool untouched.
sourceraw docstring

askclj/s

(ask client prompt {:keys [toolkit id on-event] :as ctx})

run, remembering the conversation (§conversation-store) — the id-keyed sugar every other port ships as ask.

(ask client "and the second one?" {:toolkit tk :id "muthu"})

With an :id, the store's history for that id is loaded before the call and the full transcript saved after it, so the next ask with the same id continues the conversation. WITHOUT an id it is a stateless one-shot, identical to run — the same rule as js/src/client.ts, where ask with no id delegates straight to run.

This is deliberately nothing more than run with :conversation-id filled in: the memory mechanics live in ONE place, and a second copy of the load-then-save rule here is exactly the drift the ports exist to prevent.

`run`, remembering the conversation (§conversation-store) — the id-keyed
sugar every other port ships as `ask`.

    (ask client "and the second one?" {:toolkit tk :id "muthu"})

With an `:id`, the store's history for that id is loaded before the call and
the full transcript saved after it, so the next `ask` with the same id
continues the conversation. WITHOUT an id it is a stateless one-shot,
identical to `run` — the same rule as js/src/client.ts, where `ask` with no
id delegates straight to `run`.

This is deliberately nothing more than `run` with `:conversation-id` filled
in: the memory mechanics live in ONE place, and a second copy of the
load-then-save rule here is exactly the drift the ports exist to prevent.
sourceraw docstring

auth-requiredclj/s

(auth-required url)
(auth-required url prompt)

§10 sugar: a kind:"authorization" suspension at url.

§10 sugar: a `kind:"authorization"` suspension at `url`.
sourceraw docstring

before-llm!clj/s

(before-llm! client messages tools turn)

§8 beforeLLM, applied to ONE round trip. Returns [messages tools model] — the hook's replacements when it returned them, the originals otherwise. model is the hook's non-empty :model for THIS turn only (change add-judge-batteries), else the configured model verbatim.

Public because §11 toolnexus.translate must fire this hook exactly once for its single call, and a second copy of the rule in that namespace is exactly the drift the ports exist to prevent.

Ordering (SPEC.md §8 Gap 1, and it IS the spec):

base body -> beforeLLM hook -> :request-params merge -> :body-transform -> marshal -> wire

so this runs BEFORE the body is assembled, and its tools replacement feeds the same empty-list omission (Gap 5) the toolkit's own list does.

§8 `beforeLLM`, applied to ONE round trip. Returns `[messages tools model]` —
the hook's replacements when it returned them, the originals otherwise.
`model` is the hook's non-empty `:model` for THIS turn only (change
add-judge-batteries), else the configured model verbatim.

Public because §11 `toolnexus.translate` must fire this hook exactly once for
its single call, and a second copy of the rule in that namespace is exactly
the drift the ports exist to prevent.

Ordering (SPEC.md §8 Gap 1, and it IS the spec):

  base body -> beforeLLM hook -> :request-params merge -> :body-transform
            -> marshal -> wire

so this runs BEFORE the body is assembled, and its `tools` replacement feeds
the same empty-list omission (Gap 5) the toolkit's own list does.
sourceraw docstring

call-providerclj/s

(call-provider client body)

ONE provider round trip for a CALLER-BUILT body map, through exactly the endpoint, headers, §resilience-policy retry/backoff and llm metric the agent loop uses. §8's :request-params merge and :body-transform apply unchanged.

This is the seam §11 toolnexus.translate sits on: single-turn translation must lose neither resilience nor metrics, and the only way to guarantee that is to share the code rather than copy it.

ONE provider round trip for a CALLER-BUILT body map, through exactly the
endpoint, headers, §resilience-policy retry/backoff and `llm` metric the agent
loop uses. §8's `:request-params` merge and `:body-transform` apply unchanged.

This is the seam §11 `toolnexus.translate` sits on: single-turn translation
must lose neither resilience nor metrics, and the only way to guarantee that
is to share the code rather than copy it.
sourceraw docstring

call-provider*clj/s

(call-provider* client body)

call-provider, returning [decoded-response transmitted-model] — the model the shaped body actually carried (§8/§11: the model reported is the model sent).

`call-provider`, returning `[decoded-response transmitted-model]` — the model
the shaped body actually carried (§8/§11: the model reported is the model sent).
sourceraw docstring

create-clientclj/s

(create-client opts)

Build a client. Options (idiomatic kebab-case — these never hit the wire):

:base-url required, e.g. "https://api.anthropic.com" :style "openai" | "anthropic" (default "openai") :model required :api-key optional; falls back to OPENAI_API_KEY / ANTHROPIC_API_KEY / OPENROUTER_API_KEY :headers extra request headers :system-prompt prepended to the toolkit's skills prompt :max-turns default 10 :retries transient-failure budget (default 2); retries on 429/500/502/503/504/529 + network. Widen the status set with :retryable-statuses. :retry-base-ms base of the exponential backoff in ms (default 500, as in the other six ports). The wait is base * 2^attempt + jitter[0,100)ms, and a usable Retry-After still wins over it. :retryable-statuses extra HTTP statuses to treat as retryable, ADDED to the default set (429/500/502/503/504/529). It can only widen: a host cannot remove 429 and lose Retry-After handling with it. This sets the DEFAULT classification; :on-error still runs per attempt and has the final say, so :on-error returning :fail overrides a status listed here. Example: a Cloudflare-fronted origin that answers 520–527. :hooks §8 lifecycle middleware — a map of any of {:before-llm :after-llm :before-tool :after-tool}; see before-llm! / execute-tool. Absent => nothing changes. :wait-for (fn [request] answer) — the ONE host slot of §10. :max-part-bytes §1B — reject a content part carrying more than this many DECODED bytes (never the +33% base64 string). Unset => no limit, exactly as before. :on-unsupported-part §8A — "error" | "text", overriding the provenance rule uniformly. Unset => an ATTACHED part the style cannot represent stops the run, a TOOL-DERIVED one degrades to a text placeholder and warns once.

On the name of that slot: §10 says "never await" because await is reserved in JS/Python/C#. In Clojure it is not reserved — it is clojure.core/await, and it exists on BOTH hosts, so (defn await ...) would shadow a core name and (per the spike brief) can make cljgo's static interop scan reject the WHOLE namespace. Same answer, sharper teeth.

Build a client. Options (idiomatic kebab-case — these never hit the wire):

  :base-url       required, e.g. "https://api.anthropic.com"
  :style          "openai" | "anthropic"   (default "openai")
  :model          required
  :api-key        optional; falls back to OPENAI_API_KEY / ANTHROPIC_API_KEY
                  / OPENROUTER_API_KEY
  :headers        extra request headers
  :system-prompt  prepended to the toolkit's skills prompt
  :max-turns      default 10
  :retries        transient-failure budget (default 2); retries on
                  `429`/`500`/`502`/`503`/`504`/`529` + network. Widen the
                  status set with `:retryable-statuses`.
  :retry-base-ms  base of the exponential backoff in ms (default 500, as in
                  the other six ports). The wait is
                  `base * 2^attempt + jitter[0,100)ms`, and a usable
                  `Retry-After` still wins over it.
  :retryable-statuses
                  extra HTTP statuses to treat as retryable, ADDED to the
                  default set (`429`/`500`/`502`/`503`/`504`/`529`). It can
                  only widen: a host cannot remove `429` and lose
                  `Retry-After` handling with it. This sets the DEFAULT
                  classification; `:on-error` still runs per attempt and has
                  the final say, so `:on-error` returning `:fail` overrides a
                  status listed here. Example: a Cloudflare-fronted origin
                  that answers `520`–`527`.
  :hooks          §8 lifecycle middleware — a map of any of
                  {:before-llm :after-llm :before-tool :after-tool}; see
                  `before-llm!` / `execute-tool`. Absent => nothing changes.
  :wait-for       (fn [request] answer) — the ONE host slot of §10.
  :max-part-bytes §1B — reject a content part carrying more than this many
                  DECODED bytes (never the +33% base64 string). Unset => no
                  limit, exactly as before.
  :on-unsupported-part
                  §8A — "error" | "text", overriding the provenance rule
                  uniformly. Unset => an ATTACHED part the style cannot
                  represent stops the run, a TOOL-DERIVED one degrades to a
                  text placeholder and warns once.

On the name of that slot: §10 says "never `await`" because `await` is
reserved in JS/Python/C#. In Clojure it is not reserved — it is
`clojure.core/await`, and it exists on BOTH hosts, so `(defn await ...)`
would shadow a core name and (per the spike brief) can make cljgo's static
interop scan reject the WHOLE namespace. Same answer, sharper teeth.
sourceraw docstring

create-in-process-clientclj/s

(create-in-process-client {:keys [model generate] :as opts})

A client backed by a model running IN THIS PROCESS — no server, no socket, and no HTTP shapes to construct.

This is a second constructor, not a second seam: it builds on the same :http-client transport, so the tool-calling loop, MCP servers, skills, sub-agents, hooks, metrics and the completion gate behave identically.

(create-in-process-client {:model "my-local" :generate (fn [req] ;; req = {:messages [...] :tools [...] :model "my-local" :body {...}} {:content "hello"})}) ;; or {:tool-calls [{:name "add" :arguments {:a 2 :b 3}}]}

:generate returns ONE assistant message; :usage is optional. There is no :base-url, :api-key or :style — there is no wire to configure.

NOTE this port has no streaming entry point (:on-event is a sink on the non-streaming loop), so unlike the other six there is nothing here to refuse.

A client backed by a model running IN THIS PROCESS — no server, no socket, and no
HTTP shapes to construct.

This is a second constructor, not a second seam: it builds on the same
`:http-client` transport, so the tool-calling loop, MCP servers, skills,
sub-agents, hooks, metrics and the completion gate behave identically.

  (create-in-process-client
    {:model "my-local"
     :generate (fn [req]
                 ;; req = {:messages [...] :tools [...] :model "my-local" :body {...}}
                 {:content "hello"})})
                 ;; or {:tool-calls [{:name "add" :arguments {:a 2 :b 3}}]}

`:generate` returns ONE assistant message; `:usage` is optional. There is no
`:base-url`, `:api-key` or `:style` — there is no wire to configure.

NOTE this port has no streaming entry point (`:on-event` is a sink on the
non-streaming loop), so unlike the other six there is nothing here to refuse.
sourceraw docstring

in-memory-storeclj/s

(in-memory-store)

§conversation-store — the shipped default: per-client, process lifetime. A store is exactly two operations, :get and :save, so a host can swap in a file or a database without this namespace knowing anything about it.

§conversation-store — the shipped default: per-client, process lifetime.
A store is exactly two operations, `:get` and `:save`, so a host can swap in
a file or a database without this namespace knowing anything about it.
sourceraw docstring

in-process-http-clientclj/s

(in-process-http-client generate)

Turns a semantic generate into the shipped :http-client seam: the host returns ONE assistant message and this builds the provider envelope.

Public (ADR 0030) so a caller OTHER than create-in-process-client — the agent runtime's :in-process option, in particular — can build the exact same :http-client from a generate function with zero duplicated logic. create-in-process-client itself is unchanged: it still calls this.

Turns a semantic `generate` into the shipped `:http-client` seam: the host returns
ONE assistant message and this builds the provider envelope.

Public (ADR 0030) so a caller OTHER than `create-in-process-client` — the
agent runtime's `:in-process` option, in particular — can build the exact
same `:http-client` from a `generate` function with zero duplicated logic.
`create-in-process-client` itself is unchanged: it still calls this.
sourceraw docstring

limitsclj/s

The §8 RunResult.limit vocabulary — which limit stopped an incomplete run. Identical strings in every port.

maxTurns the turn budget was exhausted contentPart a content part could not be sent (§8A) timeout the whole-run :timeout-ms deadline expired

The §8 `RunResult.limit` vocabulary — which limit stopped an `incomplete`
run. Identical strings in every port.

  maxTurns     the turn budget was exhausted
  contentPart  a content part could not be sent (§8A)
  timeout      the whole-run `:timeout-ms` deadline expired
sourceraw docstring

make-answerclj/s

(make-answer id ok)
(make-answer id ok data)
(make-answer id ok data reason)

§10 Answer. R1: reason is populated ONLY when ok == false; the loop rule branches on ok alone and never reads it.

§10 `Answer`. R1: `reason` is populated ONLY when ok == false; the loop rule
branches on `ok` alone and never reads it.
sourceraw docstring

make-requestclj/s

(make-request kind prompt)
(make-request kind prompt opts)

§10 Request. opts may carry :url, :data (incl. R2's data.schema) and :expiresAt (RFC3339).

§10 `Request`. `opts` may carry :url, :data (incl. R2's `data.schema`) and
:expiresAt (RFC3339).
sourceraw docstring

new-request-idclj/s

(new-request-id)

A unique correlation key for one suspension.

A unique correlation key for one suspension.
sourceraw docstring

pending-ofclj/s

(pending-of result)

The Request iff this ToolResult is a suspension, else nil.

The `Request` iff this ToolResult is a suspension, else nil.
sourceraw docstring

provider-errorclj/s

(provider-error status body retry-after)

The typed §8 provider failure: an ex-info whose DATA carries :status, :body (fully redacted) and :retry-after (the RAW Retry-After header verbatim, absent when the response sent none), and whose MESSAGE carries the capped, redacted body.

:retry-after is deliberately NOT pre-parsed: an HTTP-date, fractional or out-of-range value is information the response really supplied, and a numeric field would have to drop it. The library's waiting rule (retry-after-ms, delay-seconds only, falling back to backoff) is separate and unchanged.

Before this, a host could only regex the message — and the message was the raw body, account identifiers and all (#91/#92, ADR 0027).

The typed §8 provider failure: an ex-info whose DATA carries `:status`,
`:body` (fully redacted) and `:retry-after` (the RAW `Retry-After` header
verbatim, absent when the response sent none), and whose MESSAGE carries
the capped, redacted body.

`:retry-after` is deliberately NOT pre-parsed: an HTTP-date, fractional or
out-of-range value is information the response really supplied, and a numeric
field would have to drop it. The library's waiting rule (`retry-after-ms`,
delay-seconds only, falling back to backoff) is separate and unchanged.

Before this, a host could only regex the message — and the message was the raw
body, account identifiers and all (#91/#92, ADR 0027).
sourceraw docstring

runclj/s

(run client prompt {:keys [toolkit history on-event conversation-id]})

Run the agent loop. ctx = {:toolkit tk :history [...] :on-event f}.

Returns a RunResult: {:text :messages :tool-calls :tool-call-count :turns :usage {:prompt-tokens :completion-tokens :total-tokens} :model :status ("done"|"pending"|"incomplete") :limit? :pending?}

:on-event is an optional synchronous sink for the §8 event vocabulary this non-streaming loop can honestly produce — tool_call, tool_result, pending (emitted BEFORE wait-for runs, so a channel handler can push the link in real time), usage, done. There are no text deltas here: this loop buffers the whole response, and faking deltas out of a buffered body would be a lie. Real SSE streaming is a separate seam (koine.stream/sse-post exists and is proven on both hosts) and is NOT implemented in this namespace.

Run the agent loop. `ctx` = {:toolkit tk :history [...] :on-event f}.

Returns a RunResult:
  {:text :messages :tool-calls :tool-call-count :turns
   :usage {:prompt-tokens :completion-tokens :total-tokens}
   :model :status ("done"|"pending"|"incomplete") :limit? :pending?}

`:on-event` is an optional synchronous sink for the §8 event vocabulary this
non-streaming loop can honestly produce — `tool_call`, `tool_result`,
`pending` (emitted BEFORE `wait-for` runs, so a channel handler can push the
link in real time), `usage`, `done`. There are no `text` deltas here: this
loop buffers the whole response, and faking deltas out of a buffered body
would be a lie. Real SSE streaming is a separate seam (`koine.stream/sse-post`
exists and is proven on both hosts) and is NOT implemented in this namespace.
sourceraw docstring

statusesclj/s

The §8 RunResult status vocabulary — THREE values, and the ONLY three a run can return:

done the model produced a final answer pending a §10 durable suspension; the Request is on :pending incomplete a limit stopped the run, and :limit NAMES which

IT IS NOT THE SAME VOCABULARY as toolnexus.agents.runtime/statuses, which has SEVEN values for a TaskResult and includes "timeout". Two different sets on two fields both spelled status is the collision behind #92.1, and the fix is to name both rather than rename either (D5): a host switching on a status must know WHICH vocabulary it holds. A §8 run never returns "timeout" — a whole-run deadline throws timeout-error.

RESIDUAL GAP, stated rather than implied (addendum A7): these constants pin the VALUES; nothing pins the invariant that a third vocabulary can never land on a third field called status. Tracked as a follow-up conformance row.

The §8 `RunResult` status vocabulary — THREE values, and the ONLY three a
`run` can return:

  done         the model produced a final answer
  pending      a §10 durable suspension; the Request is on `:pending`
  incomplete   a limit stopped the run, and `:limit` NAMES which

IT IS NOT THE SAME VOCABULARY as `toolnexus.agents.runtime/statuses`, which
has SEVEN values for a TaskResult and includes `"timeout"`. Two different
sets on two fields both spelled `status` is the collision behind #92.1, and
the fix is to name both rather than rename either (D5): a host switching on a
status must know WHICH vocabulary it holds. A §8 run never returns
`"timeout"` — a whole-run deadline throws `timeout-error`.

RESIDUAL GAP, stated rather than implied (addendum A7): these constants pin
the VALUES; nothing pins the invariant that a third vocabulary can never land
on a third field called `status`. Tracked as a follow-up conformance row.
sourceraw docstring

suspendclj/s

(suspend request)
(suspend request output)

A ToolResult whose metadata.pending is a Request IS a suspension (§0.12). execute's signature is untouched — suspension is data on the existing result, not a new return type.

A `ToolResult` whose `metadata.pending` is a `Request` IS a suspension
(§0.12). `execute`'s signature is untouched — suspension is data on the
existing result, not a new return type.
sourceraw docstring

timeout-errorclj/s

(timeout-error timeout-ms elapsed-ms)

The typed whole-run deadline failure. :timeout-ms is the WHOLE-RUN deadline §8 specifies, and the message NAMES the budget that stopped the run.

The typed whole-run deadline failure. `:timeout-ms` is the WHOLE-RUN deadline
§8 specifies, and the message NAMES the budget that stopped the run.
sourceraw docstring

user-messageclj/s

(user-message prompt)

§1B — the ONE place a caller's prompt becomes a message in this port.

run is fixed 3-arity (ctx is required), so a new arity is impossible and the dispatch is on the VALUE: a string is the pre-0.17 path and is stored verbatim, byte-identical; a sequence is a ContentPart list and is stored as the canonical transcript shape, which content/build-wire encodes on the way out.

§1B — the ONE place a caller's prompt becomes a message in this port.

`run` is fixed 3-arity (`ctx` is required), so a new arity is impossible and
the dispatch is on the VALUE: a string is the pre-0.17 path and is stored
verbatim, byte-identical; a sequence is a ContentPart list and is stored as the
canonical transcript shape, which `content/build-wire` encodes on the way out.
sourceraw docstring

wire-optsclj/s

(wire-opts client)

The §1B/§8A shaping options every wire build shares.

The §1B/§8A shaping options every wire build shares.
sourceraw docstring

zero-usageclj/s

The empty §8 Usage. Idiomatic kebab-case — Usage is a RETURN value, it never crosses the wire.

The empty §8 Usage. Idiomatic kebab-case — Usage is a RETURN value, it never
crosses the wire.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close