The model's read surface over a KB — generated, not hand-written.
vaelii.impl.serve/ops is already an allowlisted, EDN-typed map of vaelii.core
calls: the exact surface the browser and the daemon reach a KB through. So the
tool schemas are derived from its read subset rather than transcribed, and the
tool calls are dispatched back through the same table. A read added to serve/ops
becomes a tool with no edit here; a read renamed there cannot rot a copy here,
because there is no copy.
The model never writes. write-ops names every mutating op, and anything
resolving to a ! var is treated as one whatever the table says, so the exposed set
is reads only. The model's output is a proposed batch, reviewed and applied by a
human (vaelii.impl.llm.session) — there is no write tool and no write path.
Argument shapes come from the vaelii.core var's own :arglists (minus the leading
kb) and its docstring, so a signature change is picked up on the next build. JSON
carries no symbols, so a sentence / context / term argument is a string holding an
EDN s-expression — "(dog ?x)", "CxWell" — read back with
clojure.edn/read-string (never read-string: EDN has no reader-eval, so a model's
output cannot evaluate code).
The model's read surface over a KB — **generated**, not hand-written. `vaelii.impl.serve/ops` is already an allowlisted, EDN-typed map of `vaelii.core` calls: the exact surface the browser and the daemon reach a KB through. So the tool schemas are derived from its **read subset** rather than transcribed, and the tool calls are dispatched back through the same table. A read added to `serve/ops` becomes a tool with no edit here; a read renamed there cannot rot a copy here, because there is no copy. **The model never writes.** `write-ops` names every mutating op, and anything resolving to a `!` var is treated as one whatever the table says, so the exposed set is reads only. The model's *output* is a proposed batch, reviewed and applied by a human (`vaelii.impl.llm.session`) — there is no write tool and no write path. Argument shapes come from the `vaelii.core` var's own `:arglists` (minus the leading `kb`) and its docstring, so a signature change is picked up on the next build. JSON carries no symbols, so a sentence / context / term argument is a **string holding an EDN s-expression** — `"(dog ?x)"`, `"CxWell"` — read back with `clojure.edn/read-string` (never `read-string`: EDN has no reader-eval, so a model's output cannot evaluate code).
(call kb tname input)(call kb tname input {:keys [max-result-chars] :or {max-result-chars 4000}})Run one tool call against kb and return {:ok true :result "…"} or
{:ok false :error "…"} — the string a tool_result block carries back.
input is the provider's JSON object (string keys). The longest signature the
input fully satisfies decides the call, so an op with several shapes (why-not
takes a handle or a sentence and a context) dispatches to the one the model
actually supplied. Dispatch goes through serve/ops, the same table the schemas
were generated from.
An argument the chosen signature does not take is refused, never dropped. The
shapes nest — query takes (goal), (goal, context), (goal, context, opts) —
so an input of goal and opts with no context satisfies only the first, and passing
it on would answer facts-only with the depth discarded, which reads exactly like a
goal no rule can reach. That is opts/check!'s failure one level out: an argument
nothing reads takes the default in silence. So the refusal names the shape the
input selected and the shapes the op has, and the model supplies the argument in
between.
Never throws: a bad argument, an unknown tool, or a refusal from the KB comes back
as {:ok false :error "…"}, which is what a tool_result block wants — the model
reads the error and tries again.
opts: :max-result-chars (default 4000) bounds what a broad read can push into
the context window.
Run one tool call against `kb` and return `{:ok true :result "…"}` or
`{:ok false :error "…"}` — the string a `tool_result` block carries back.
`input` is the provider's JSON object (string keys). The **longest signature the
input fully satisfies** decides the call, so an op with several shapes (`why-not`
takes a handle *or* a sentence and a context) dispatches to the one the model
actually supplied. Dispatch goes through `serve/ops`, the same table the schemas
were generated from.
**An argument the chosen signature does not take is refused, never dropped.** The
shapes nest — `query` takes `(goal)`, `(goal, context)`, `(goal, context, opts)` —
so an input of goal *and* opts with no context satisfies only the first, and passing
it on would answer facts-only with the depth discarded, which reads exactly like a
goal no rule can reach. That is `opts/check!`'s failure one level out: an argument
nothing reads takes the default in silence. So the refusal names the shape the
input selected and the shapes the op has, and the model supplies the argument in
between.
Never throws: a bad argument, an unknown tool, or a refusal from the KB comes back
as `{:ok false :error "…"}`, which is what a `tool_result` block wants — the model
reads the error and tries again.
`opts`: `:max-result-chars` (default 4000) bounds what a broad read can push into
the context window.(op-of tname)The op keyword a tool name came from, or nil if it names no exposed read.
The op keyword a tool name came from, or nil if it names no exposed read.
(read-ops)The op keywords the model may call: serve/ops minus the writes, minus anything
that does not resolve to a non-variadic vaelii.core var. Sorted, so the generated
tool list is byte-stable and the prompt cache survives a rebuild.
The op keywords the model may call: `serve/ops` minus the writes, minus anything that does not resolve to a non-variadic `vaelii.core` var. Sorted, so the generated tool list is byte-stable and the prompt cache survives a rebuild.
(schema op)The tool schema for one op keyword.
The tool schema for one op keyword.
(schemas)(schemas {:keys [only exclude]})Every exposed read as a tool schema, in read-ops order. opts:
:only a set of op keywords to keep (default: all reads) :exclude a set of op keywords to drop
Every exposed read as a tool schema, in `read-ops` order. `opts`: :only a set of op keywords to keep (default: all reads) :exclude a set of op keywords to drop
(tool-name op)The tool name for an op keyword — :find-sentexes -> kb_find_sentexes,
:ask? -> kb_ask_p.
The tool name for an op keyword — `:find-sentexes` -> `kb_find_sentexes`, `:ask?` -> `kb_ask_p`.
The ops in serve/ops the model may not reach. A new mutating op must be
listed here — the exposed tool set is (keys serve/ops) minus this, so an omission
would hand the model a write. Ops resolving to a ! var are excluded
independently, which catches the ones spelled with the warning already.
That second mechanism is a backstop, not the rule: ! means irreversible
(docs/api.md), and a write that merely mutates is spelled without one. So every
mutating op is listed here explicitly, including the ones the ! sweep cannot see —
:edit and :edit-with-consequences both store, and neither carries the suffix.
:preview stores nothing but belongs here all the same: it applies its batch and
rolls it back, so it holds the process's single writer, advances the handle counter
and moves the chain and settle statistics — serve and access both file it with
the writes for that reason. :clear-caches mutates the process's measurement
state, which is nothing to hand a model that is being measured through it.
The ops in `serve/ops` the model may not reach. A new mutating op **must** be listed here — the exposed tool set is `(keys serve/ops)` minus this, so an omission would hand the model a write. Ops resolving to a `!` var are excluded independently, which catches the ones spelled with the warning already. That second mechanism is a backstop, not the rule: `!` means *irreversible* (`docs/api.md`), and a write that merely mutates is spelled without one. So every mutating op is listed here explicitly, including the ones the `!` sweep cannot see — `:edit` and `:edit-with-consequences` both store, and neither carries the suffix. `:preview` stores nothing but belongs here all the same: it applies its batch and rolls it back, so it holds the process's single writer, advances the handle counter and moves the chain and settle statistics — `serve` and `access` both file it with the writes for that reason. `:clear-caches` mutates the process's measurement state, which is nothing to hand a model that is being measured through it.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |