Liking cljdoc? Tell your friends :D

vaelii.impl.llm.anthropic

The real backend: the Anthropic Messages API over raw HTTP.

There is no official Anthropic SDK for Clojure, so raw HTTP is the supported path — and it costs nothing here, because the repo already carries both halves: cheshire for JSON and JDK java.net.http, which vaelii.impl.client already speaks to the vaelii daemon. This namespace mirrors that one: an explicit connection handle, no global state, no new dependency.

Reached only when a caller installs it (vaelii.impl.llm.stub is the default), so a build with no credential and no network never loads a socket.

Request shape notes, because several of them are load-bearing:

  • temperature / top_p / top_k are rejected on this model family — they are never sent, and steering happens in the prompt instead.
  • Thinking is on by default and takes no token budget; depth is set with output_config.effort. :thinking-display "summarized" opts into a readable summary (the default omits the text).
  • A refusal is HTTP 200 with stop_reason: "refusal" and empty or partial content, so parse-response never fabricates a text block and the session loop branches on :stop-reason before touching :content.
  • The generated system prompt is a large stable prefix, so its last block carries a cache_control breakpoint; the user's turn sits after it. The minimum cacheable prefix on claude-opus-5 is 512 tokens — a short prompt simply will not cache, with no error.
  • fallbacks is sent by default so a policy decline is re-served rather than returned as a dead turn. It rides a beta header; pass {:fallbacks nil} to drop both if the org has not enabled it.

Credentials are resolved from the environment, never hardcoded and never logged — see credentials.

The real backend: the Anthropic Messages API over raw HTTP.

There is no official Anthropic SDK for Clojure, so raw HTTP is the supported path —
and it costs nothing here, because the repo already carries both halves: `cheshire`
for JSON and JDK `java.net.http`, which `vaelii.impl.client` already speaks to the
vaelii daemon.  This namespace mirrors that one: an explicit connection handle, no
global state, **no new dependency**.

Reached only when a caller installs it (`vaelii.impl.llm.stub` is the default), so a
build with no credential and no network never loads a socket.

Request shape notes, because several of them are load-bearing:

* **`temperature` / `top_p` / `top_k` are rejected** on this model family — they are
  never sent, and steering happens in the prompt instead.
* **Thinking is on by default** and takes no token budget; depth is set with
  `output_config.effort`.  `:thinking-display "summarized"` opts into a readable
  summary (the default omits the text).
* A **refusal is HTTP 200** with `stop_reason: "refusal"` and empty or partial
  content, so `parse-response` never fabricates a text block and the session loop
  branches on `:stop-reason` before touching `:content`.
* The generated system prompt is a large stable prefix, so its last block carries a
  `cache_control` breakpoint; the user's turn sits after it.  The minimum cacheable
  prefix on `claude-opus-5` is 512 tokens — a short prompt simply will not cache,
  with no error.
* `fallbacks` is sent by default so a policy decline is re-served rather than
  returned as a dead turn.  It rides a beta header; pass `{:fallbacks nil}` to drop
  both if the org has not enabled it.

**Credentials are resolved from the environment, never hardcoded and never logged**
— see `credentials`.
raw docstring

vaelii.impl.llm.correct

Type-level corrections: a proposal that says the right thing in the wrong shape.

Every model asked about a type writes facts about the type symbol(eats penguin fish), (mortal penguin) — where the KB's idiom quantifies over the type's instances. Measured across eight models on the shipped schema this is the dominant remaining error class, and nothing else catches it: naming/problems passes it (the names are all well-formed), wff passes it, the argIsa constraints pass it (open-world — a type symbol carries no type membership, and an untyped argument cannot violate), and it stores. The claim is usually right; only its shape is wrong.

So this rewrites rather than rejects. Each correction carries the sentence it came from, the one to store instead, the alternatives where more than one shape is defensible, and why — because for the most common case the choice between shapes is a semantic judgement no engine can make for the author:

claimshapeinferenceexception possible?
definitional — what a penguin is(genl penguin mortal)free, off the cached closureno
defeasible — what a penguin usually does(set/defaultRule (implies (penguin ?x) (mortal ?x)))forward chainingyes, via exceptWhen

The defeasible shape is the default, on asymmetric risk. A defeasible claim that should have been definitional costs only the chaining it did not need. A definitional claim that should have been defeasible cannot take an exceptWhen at all — a genl edge admits no exception — so the only repair is retracting it and rebuilding the closure. Guessing wrong in that direction is the expensive one, so it is not guessed.

Nothing here mutates: a correction is a proposal about a proposal. What to do with it — show both shapes, show only the rewrite, let the author edit it — is the caller's.

Type-level corrections: a proposal that says the right thing in the wrong shape.

Every model asked about a *type* writes facts **about the type symbol** —
`(eats penguin fish)`, `(mortal penguin)` — where the KB's idiom quantifies over the
type's instances.  Measured across eight models on the shipped schema this is the
dominant remaining error class, and nothing else catches it: `naming/problems` passes
it (the names are all well-formed), `wff` passes it, the argIsa constraints pass it
(open-world — a type symbol carries no type membership, and an untyped argument cannot
violate), and it stores.  The claim is *usually right*; only its shape is wrong.

So this rewrites rather than rejects.  Each correction carries the sentence it came
from, the one to store instead, the **alternatives** where more than one shape is
defensible, and why — because for the most common case the choice between shapes is a
semantic judgement no engine can make for the author:

| claim | shape | inference | exception possible? |
|---|---|---|---|
| definitional — what a penguin *is* | `(genl penguin mortal)` | free, off the cached closure | no |
| defeasible — what a penguin *usually does* | `(set/defaultRule (implies (penguin ?x) (mortal ?x)))` | forward chaining | yes, via `exceptWhen` |

**The defeasible shape is the default, on asymmetric risk.**  A defeasible claim that
should have been definitional costs only the chaining it did not need.  A definitional
claim that should have been defeasible cannot take an `exceptWhen` at all — a `genl`
edge admits no exception — so the only repair is retracting it and rebuilding the
closure.  Guessing wrong in that direction is the expensive one, so it is not guessed.

Nothing here mutates: a correction is a proposal about a proposal.  What to do with it
— show both shapes, show only the rewrite, let the author edit it — is the caller's.
raw docstring

vaelii.impl.llm.inventory

The KB's own vocabulary, put in front of the model — and the flag for when the model invents vocabulary anyway.

Measured failure mode. Asked to write type-level common sense about a term, every local model that writes usable s-expressions at all encodes structure into the predicate name rather than into arguments:

(implies (penguin ?x) (lives_in_antarctica ?x))
(implies (penguin ?x) (capable_of_swimming ?x))
(implies (penguin ?x) (thermoregulates_via_blubber_and_feathers ?x))

Each is admissible, novel, and useless: a one-off unary predicate joins no rule and matches no other sentence. (livesIn ?x Antarctica) is the same claim in vocabulary the KB can reason with. Swapping models does not fix it — the strongest model produces the most of it, because it produces the most output.

The check chain cannot catch it. A unary snake_case functor is a well-formed type name by the naming invariants, so (has_black_and_white_feathers ?x) passes naming, groundness, well-formedness, argIsa, disjointness and functionality alike — and it should, since coining a type is a legitimate thing to do. So there are exactly two guards, and they are both here:

  1. Preventioninventory / render put the relevant available vocabulary in the prompt with each predicate's arity and argument types, and the prompt tells the model to reuse it. A model that can see livesIn with args 1:animal 2:place writes (livesIn ?x Antarctica).
  2. Detectioncoined reports every functor in a proposal the KB has never seen, with its arity and naming role, plus a reuse-versus-coin count over the proposal's literals. It reports, never rejects: a reviewer decides, because a genuinely new type is how an ontology grows.

Selection is by relevance, bounded by tokens. Nothing enumerates the KB: the inventory is seeded from the page's term, walks its genl neighbourhood (supertypes nearest first, subtypes, siblings), and takes the predicates that neighbourhood licenses — the argIsa-declared ones whose argument types the term satisfies, plus the ones already used in facts with those terms. Every read is pinned by a term (a cached closure lookup, an argIsa query on a fixed argument, a bounded argument-root walk), so the cost tracks the neighbourhood rather than the knowledge base.

The KB's own vocabulary, put in front of the model — and the flag for when the model
invents vocabulary anyway.

**Measured failure mode.**  Asked to write type-level common sense about a term, every
local model that writes usable s-expressions at all encodes structure into the
*predicate name* rather than into arguments:

    (implies (penguin ?x) (lives_in_antarctica ?x))
    (implies (penguin ?x) (capable_of_swimming ?x))
    (implies (penguin ?x) (thermoregulates_via_blubber_and_feathers ?x))

Each is admissible, novel, and useless: a one-off unary predicate joins no rule and
matches no other sentence.  `(livesIn ?x Antarctica)` is the same claim in vocabulary
the KB can reason with.  Swapping models does not fix it — the strongest model
produces the *most* of it, because it produces the most output.

**The check chain cannot catch it.**  A unary snake_case functor is a well-formed type
name by the naming invariants, so `(has_black_and_white_feathers ?x)` passes naming,
groundness, well-formedness, argIsa, disjointness and functionality alike — and it
should, since coining a type is a legitimate thing to do.  So there are exactly two
guards, and they are both here:

1. **Prevention** — `inventory` / `render` put the *relevant available* vocabulary in
   the prompt with each predicate's arity and argument types, and the prompt tells the
   model to reuse it.  A model that can see `livesIn` with `args 1:animal 2:place`
   writes `(livesIn ?x Antarctica)`.
2. **Detection** — `coined` reports every functor in a proposal the KB has never seen,
   with its arity and naming role, plus a reuse-versus-coin count over the proposal's
   literals.  It **reports, never rejects**: a reviewer decides, because a genuinely
   new type is how an ontology grows.

**Selection is by relevance, bounded by tokens.**  Nothing enumerates the KB: the
inventory is seeded from the page's term, walks its `genl` neighbourhood (supertypes
nearest first, subtypes, siblings), and takes the predicates that neighbourhood
*licenses* — the `argIsa`-declared ones whose argument types the term satisfies, plus
the ones already used in facts with those terms.  Every read is pinned by a term
(a cached closure lookup, an `argIsa` query on a fixed argument, a bounded argument-root
walk), so the cost tracks the neighbourhood rather than the knowledge base.
raw docstring

vaelii.impl.llm.ollama

A local backend: Ollama's chat API over raw HTTP.

Same shape as vaelii.impl.llm.anthropic — the neutral request/response maps of vaelii.impl.llm.protocol, an explicit connection handle, no global state, no new dependency (cheshire for JSON, JDK java.net.http). What differs is everything the transport does:

  • No credential. Ollama serves on a host, not behind an API key, so available? is a reachability probe rather than a credential lookup. That is what makes this backend testable end to end.
  • The context window is the caller's to setoptions.num_ctx, per request. Ollama silently truncates a prompt longer than it, which would quietly drop the user's selection, so the caller sizes the prompt against num-ctx before sending (vaelii.impl.llm.selection/budget-problem); nothing here truncates.
  • Constrained decoding instead of tool calls. :format carries a JSON schema that the sampler is restricted to, so a model with no tools capability still answers in an exact shape. capabilities reads what a model can actually do, and supports-tools? is the gate — sending 42 tool schemas to a completion-only model spends the whole window on something it will never emit.
  • Streaming is newline-delimited JSON, not SSE: one object per token-ish chunk, the last carrying done: true and the run's counts.

Counts come back on every response — prompt_eval_count is the measured prompt size, which is what a budget is checked against after the fact.

A local backend: Ollama's chat API over raw HTTP.

Same shape as `vaelii.impl.llm.anthropic` — the neutral request/response maps of
`vaelii.impl.llm.protocol`, an explicit connection handle, no global state, **no new
dependency** (`cheshire` for JSON, JDK `java.net.http`).  What differs is everything
the transport does:

* **No credential.**  Ollama serves on a host, not behind an API key, so
  `available?` is a reachability probe rather than a credential lookup.  That is what
  makes this backend testable end to end.
* **The context window is the caller's to set** — `options.num_ctx`, per request.
  Ollama **silently truncates** a prompt longer than it, which would quietly drop the
  user's selection, so the caller sizes the prompt against `num-ctx` *before* sending
  (`vaelii.impl.llm.selection/budget-problem`); nothing here truncates.
* **Constrained decoding instead of tool calls.**  `:format` carries a JSON schema
  that the sampler is restricted to, so a model with no `tools` capability still
  answers in an exact shape.  `capabilities` reads what a model can actually do, and
  `supports-tools?` is the gate — sending 42 tool schemas to a completion-only model
  spends the whole window on something it will never emit.
* **Streaming is newline-delimited JSON**, not SSE: one object per token-ish chunk,
  the last carrying `done: true` and the run's counts.

Counts come back on every response — `prompt_eval_count` is the **measured** prompt
size, which is what a budget is checked against after the fact.
raw docstring

vaelii.impl.llm.oracle

An outside judge over what the knowledge base concluded: every claim glossed into one English line, handed to a model, and answered agree / disagree / unsure.

This is the other direction, and the trust runs the other way

vaelii.impl.llm.text reads English into the KB, where the danger is a model writing something false into the store and the defence is a reviewer between the two. Here the KB is the one making claims and the model is the one being asked, so nothing a model says can reach the store: this namespace calls no writer, and a verdict is a line in a report. A disagreement is a finding for a person to read, never a retraction, never a defeat class, and never a test failure that edits the KB to make the number go up.

That is the whole of why the judge is worth having. The engine can check that a conclusion follows and that a sentence is well formed; nothing in it can check that the knowledge is true, and a KB full of well-formed nonsense passes every gate this repo has. An outside reader is the only instrument for that, and a model is a reader who will do it for two hundred claims without getting bored.

The claim is glossed, and a claim the KB cannot gloss is not sent

A model handed (genl penguin bird) is judging our notation. vaelii.impl.gloss composes the English from the KB's own comments — the vocabulary documents itself, and the first clause of each comment is already a template — so what the judge sees is the knowledge base's sentence rather than a paraphrase somebody wrote for the prompt. A sentence the KB documents nothing about glosses to :named, which is barely more than the s-expression, so it is left out and counted as skipped: an unanswerable question dressed up as a low score measures the prompt and not the KB.

A derived claim is shown its situation, and never its rule

Fido is awake is not judgeable on its own — nobody knows Fido. Given that Fido is a dog, Fido is awake is: it is the everyday question of whether that is a reasonable thing to say about a dog you have just been told about. So a derived claim carries the facts its justification rests on, glossed the same way.

It does not carry the rule, and that omission is the design. Show the rule and the question becomes does this follow, which is validity — the one thing the engine already guarantees and the one thing an outside judge is not needed for.

Three verdicts, because two would make the number meaningless

Most of this KB is defaults, and a default is not a universal. A judge forced to answer yes or no about an animal is awake will pick one and the disagreement rate will measure the coin. unsure is where a claim that depends on particulars nobody supplied belongs, and the counts are reported apart so a reader can see how much of the answer was a shrug.

What the rate is not is an accuracy: a careful judge marking a default false is telling you the default has exceptions, which the KB already knows and stores as a default for exactly that reason. So each disagreement carries the claim's strength, and the disagreements — not the rate — are the output.

An outside judge over what the knowledge base concluded: every claim glossed into one
English line, handed to a model, and answered *agree / disagree / unsure*.

## This is the other direction, and the trust runs the other way

`vaelii.impl.llm.text` reads English **into** the KB, where the danger is a model
writing something false into the store and the defence is a reviewer between the two.
Here the KB is the one making claims and the model is the one being asked, so nothing
a model says can reach the store: this namespace calls no writer, and a verdict is a
line in a report.  A disagreement is a **finding for a person to read**, never a
retraction, never a defeat class, and never a test failure that edits the KB to make
the number go up.

That is the whole of why the judge is worth having.  The engine can check that a
conclusion follows and that a sentence is well formed; nothing in it can check that
the knowledge is *true*, and a KB full of well-formed nonsense passes every gate this
repo has.  An outside reader is the only instrument for that, and a model is a reader
who will do it for two hundred claims without getting bored.

## The claim is glossed, and a claim the KB cannot gloss is not sent

A model handed `(genl penguin bird)` is judging our notation.  `vaelii.impl.gloss`
composes the English from the KB's **own** comments — the vocabulary documents itself,
and the first clause of each comment is already a template — so what the judge sees is
the knowledge base's sentence rather than a paraphrase somebody wrote for the prompt.
A sentence the KB documents nothing about glosses to `:named`, which is barely more
than the s-expression, so it is left out and counted as skipped: an unanswerable
question dressed up as a low score measures the prompt and not the KB.

## A derived claim is shown its situation, and never its rule

*Fido is awake* is not judgeable on its own — nobody knows Fido.  *Given that Fido is
a dog, Fido is awake* is: it is the everyday question of whether that is a reasonable
thing to say about a dog you have just been told about.  So a derived claim carries
the facts its justification rests on, glossed the same way.

It does **not** carry the rule, and that omission is the design.  Show the rule and
the question becomes *does this follow*, which is validity — the one thing the engine
already guarantees and the one thing an outside judge is not needed for.

## Three verdicts, because two would make the number meaningless

Most of this KB is defaults, and a default is not a universal.  A judge forced to
answer yes or no about *an animal is awake* will pick one and the disagreement rate
will measure the coin.  `unsure` is where a claim that depends on particulars nobody
supplied belongs, and the counts are reported apart so a reader can see how much of
the answer was a shrug.

What the rate is **not** is an accuracy: a careful judge marking a default false is
telling you the default has exceptions, which the KB already knows and stores as a
default for exactly that reason.  So each disagreement carries the claim's strength,
and the disagreements — not the rate — are the output.
raw docstring

vaelii.impl.llm.page

The page-scoped prompt: the unit of work is the term the reader is looking at.

vaelii.impl.llm.selection prompts about lines the reader picked and asks for them back edited. This namespace prompts about a term page/term?q=penguin — and asks for knowledge the KB does not have yet:

  1. what the page already says about the term, as bare sentences,
  2. the vocabulary that term's genl neighbourhood licenses (vaelii.impl.llm.inventory) — arity and argument types included,
  3. the reader's free-text instruction ("flesh out the capabilities of this").

Three things differ from the edit path, each because it was measured:

  • The context is dropped. A page is already about one context, so the caller supplies it and the model writes bare sentences. That removes a whole class of answer the model gets wrong for no gain — and it shortens every line it writes.
  • Decoding is constrained (output-schema, Ollama's format). On generation this rescues models that otherwise answer in markdown prose; on the edit path the same parameter silently drops lines, so the two contracts are deliberately different and are not unified.
  • The content is type-level. Common sense about a kind is a genl edge or a rule, not a fact about an individual, so the prompt asks for those shapes and shows them.

The prompt's one job beyond the shape is to stop the model coining vocabulary — see vaelii.impl.llm.inventory, which is where both guards against that live.

The page-scoped prompt: **the unit of work is the term the reader is looking at.**

`vaelii.impl.llm.selection` prompts about lines the reader picked and asks for them
back edited.  This namespace prompts about a *term page* — `/term?q=penguin` — and asks
for knowledge the KB does not have yet:

1. what the page already says about the term, as bare sentences,
2. the vocabulary that term's `genl` neighbourhood licenses
   (`vaelii.impl.llm.inventory`) — arity and argument types included,
3. the reader's free-text instruction ("flesh out the capabilities of this").

Three things differ from the edit path, each because it was measured:

* **The context is dropped.**  A page is already about one context, so the caller
  supplies it and the model writes bare sentences.  That removes a whole class of
  answer the model gets wrong for no gain — and it shortens every line it writes.
* **Decoding is constrained** (`output-schema`, Ollama's `format`).  On generation this
  *rescues* models that otherwise answer in markdown prose; on the edit path the same
  parameter silently drops lines, so the two contracts are deliberately different and
  are not unified.
* **The content is type-level.**  Common sense about a *kind* is a `genl` edge or a
  rule, not a fact about an individual, so the prompt asks for those shapes and shows
  them.

The prompt's one job beyond the shape is to stop the model coining vocabulary — see
`vaelii.impl.llm.inventory`, which is where both guards against that live.
raw docstring

vaelii.impl.llm.prompt

The system prompt, generated from the live KB.

A hand-written copy of the ontology in a prompt string rots the moment someone drops a new <Context>.txt into resources/kb/. So every section here is read back out of the KB it is about: the context topology from contexts / context-up, the type hierarchy from types / genls, the predicate documentation from the (comment <term> "…") sentexes the vocabulary documents itself with (vaelii.impl.core-context/comment-of), the argument types from the stored argIsa sentexes, the disjointness from disjoint / disjointMetatype, and the algebraic metadata from props. The naming invariants are the one static section, because they are mechanical rules rather than content.

The result is a large stable prefix: byte-identical across turns for an unchanged KB (every section is sorted, nothing carries a clock or an id), which is what prompt caching needs. The volatile part — the user's request — lives in the message turn after the cache breakpoint, never in here.

The system prompt, **generated from the live KB**.

A hand-written copy of the ontology in a prompt string rots the moment someone
drops a new `<Context>.txt` into `resources/kb/`.  So every section here is read
back out of the KB it is about: the context topology from `contexts` /
`context-up`, the type hierarchy from `types` / `genls`, the predicate
documentation from the `(comment <term> "…")` sentexes the vocabulary documents
itself with (`vaelii.impl.core-context/comment-of`), the argument types from the stored
`argIsa` sentexes, the disjointness from `disjoint` / `disjointMetatype`, and the
algebraic metadata from `props`.  The naming invariants are the one static section,
because they are mechanical rules rather than content.

The result is a **large stable prefix**: byte-identical across turns for an
unchanged KB (every section is sorted, nothing carries a clock or an id), which is
what prompt caching needs.  The volatile part — the user's request — lives in the
message turn after the cache breakpoint, never in here.
raw docstring

vaelii.impl.llm.protocol

The pluggable-model seam: one protocol, two methods, and a provider-neutral request/response shape.

Mirrors vaelii.impl.solve/Solver — a protocol plus a deterministic stub as the default, with the real backend reached only when a caller installs it. So the suite, and a build with no API key and no network, run the whole pipeline against vaelii.impl.llm.stub and never open a socket.

The shapes are the seam. A provider takes a request map and answers a response map; neither mentions HTTP, JSON, or any vendor field, so the session loop (vaelii.impl.llm.session) is written once and runs against either provider.

Request:

{:model      "claude-opus-5"     ; provider-resolved when absent
 :system     [{:text "…" :cache? true} …]   ; blocks, in order
 :messages   [{:role "user"|"assistant" :content <string | [block …]}]
 :tools      [<tool schema> …]      ; vaelii.impl.llm.tools/schemas
 :max-tokens 8192
 :effort     "low"|"medium"|"high"|"xhigh"|"max"}

:cache? on a system block asks the provider to mark it as a cache breakpoint; the generated system prompt is a large stable prefix and the user's turn is the volatile part after it, which is exactly the shape prompt caching wants.

Response:

{:stop-reason  "end_turn"|"tool_use"|"refusal"|"max_tokens"|…
 :stop-details {…}                ; populated only on a refusal
 :content      [{:type :text     :text "…"}
                {:type :tool-use :id "…" :name "…" :input {…}}
                {:type :thinking :text "…"}]
 :model        "…"
 :usage        {…}}

A refusal is a successful answer with no content — the caller must read :stop-reason before reading :content, so a provider never fabricates a text block to keep indexing code happy. Assistant content is echoed back verbatim in the next request's :messages, so a provider may carry vendor-specific blocks through :content untouched as long as it can read its own back.

The pluggable-model seam: one protocol, two methods, and a provider-neutral
request/response shape.

Mirrors `vaelii.impl.solve/Solver` — a protocol plus a deterministic stub as the
default, with the real backend reached only when a caller installs it.  So the
suite, and a build with no API key and no network, run the whole pipeline against
`vaelii.impl.llm.stub` and never open a socket.

**The shapes are the seam.**  A provider takes a `request` map and answers a
`response` map; neither mentions HTTP, JSON, or any vendor field, so the session
loop (`vaelii.impl.llm.session`) is written once and runs against either provider.

Request:

    {:model      "claude-opus-5"     ; provider-resolved when absent
     :system     [{:text "…" :cache? true} …]   ; blocks, in order
     :messages   [{:role "user"|"assistant" :content <string | [block …]}]
     :tools      [<tool schema> …]      ; vaelii.impl.llm.tools/schemas
     :max-tokens 8192
     :effort     "low"|"medium"|"high"|"xhigh"|"max"}

`:cache?` on a system block asks the provider to mark it as a cache breakpoint;
the generated system prompt is a large stable prefix and the user's turn is the
volatile part after it, which is exactly the shape prompt caching wants.

Response:

    {:stop-reason  "end_turn"|"tool_use"|"refusal"|"max_tokens"|…
     :stop-details {…}                ; populated only on a refusal
     :content      [{:type :text     :text "…"}
                    {:type :tool-use :id "…" :name "…" :input {…}}
                    {:type :thinking :text "…"}]
     :model        "…"
     :usage        {…}}

A **refusal is a successful answer with no content** — the caller must read
`:stop-reason` before reading `:content`, so a provider never fabricates a text
block to keep indexing code happy.  Assistant content is echoed back verbatim in
the next request's `:messages`, so a provider may carry vendor-specific blocks
through `:content` untouched as long as it can read its own back.
raw docstring

vaelii.impl.llm.provider

Which backend a turn runs against — the selection seam.

Stands where vaelii.impl.asp.solver stands for the ASP backends: a keyword names a backend, the backend is lazily resolved so choosing one is what loads it, and an unreachable backend falls back to the deterministic default rather than throwing. vaelii.impl.llm.stub is that default, exactly as vaelii.impl.solve/local-solver is for contradictions — so a build with no credential, no Ollama and no network runs the whole pipeline and opens no socket.

Three kinds:

:stub deterministic, offline, scriptable — the default :ollama a local Ollama (vaelii.impl.llm.ollama), no credential :anthropic the Messages API (vaelii.impl.llm.anthropic), credential required

Select with VAELII_LLM_PROVIDER or -Dvaelii.llm.provider; a caller that already knows what it wants passes the kind (or a built provider) directly.

Resolution is lazy for a reason beyond load time: anthropic/available? may shell out to the ant CLI, and ollama/available? opens a socket. Neither should happen because a namespace was required.

Which backend a turn runs against — the selection seam.

Stands where `vaelii.impl.asp.solver` stands for the ASP backends: a keyword names a
backend, the backend is **lazily resolved** so choosing one is what loads it, and an
unreachable backend falls back to the deterministic default rather than throwing.
`vaelii.impl.llm.stub` is that default, exactly as `vaelii.impl.solve/local-solver`
is for contradictions — so a build with no credential, no Ollama and no network runs
the whole pipeline and opens no socket.

Three kinds:

  :stub       deterministic, offline, scriptable — the default
  :ollama     a local Ollama (`vaelii.impl.llm.ollama`), no credential
  :anthropic  the Messages API (`vaelii.impl.llm.anthropic`), credential required

Select with `VAELII_LLM_PROVIDER` or `-Dvaelii.llm.provider`; a caller that already
knows what it wants passes the kind (or a built provider) directly.

Resolution is lazy for a reason beyond load time: `anthropic/available?` may shell
out to the `ant` CLI, and `ollama/available?` opens a socket.  Neither should happen
because a namespace was required.
raw docstring

vaelii.impl.llm.score

Scoring a set of candidate entries against a hand-written one.

vaelii.impl.llm.text produces candidates and vaelii.core/check-edit says whether each is admissible. Neither says whether it is right, and nothing in the engine can: the whole reason the reading direction needs a reviewer is that every check passes on a well-formed translation of a claim the text did not make. So the only honest measure is against knowledge somebody wrote by hand, and this is the arithmetic for that.

The gold set is read out of a KB, never transcribed

A second copy of the fables' sentexes in a scoring fixture would drift from the ones the suite actually loads, and a score against a stale gold set is worse than no score. So the gold is a set of handles in a loaded KB, and a candidate matches when vaelii.core/handle-of finds it under one of them.

That also means the comparison uses the engine's own canonical form rather than a reimplementation of it: a rule whose variables are named differently, whose antecedents arrive in another order, or whose symmetric arguments are the other way round is the same sentence to handle-of and is therefore the same sentence here (docs/canonicalization.md). Nothing about matching is this namespace's opinion.

Two scores, because the constants are unrecoverable

A fable introduces its characters by kind — a lion, a mouse — so the names in the formal version (LionA, MouseA) are the modeller's, and no reader of the text could produce them. A strict score therefore reads zero on stories whose structure was recovered perfectly, which measures the naming convention rather than the reading.

So score reports both, and the pair is the finding:

  • strict — the candidate matched a gold handle as written;
  • aligned — the same comparison after renaming the candidate's introduced individuals onto the gold's, one-for-one, by the types each is asserted to have (alignment). A renaming is a bijection or it is not applied, so alignment can never merge two characters into one to score better.

Nothing here writes: handle-of is find-only, and the alignment is arithmetic over sentences.

Scoring a set of candidate entries against a hand-written one.

`vaelii.impl.llm.text` produces candidates and `vaelii.core/check-edit` says whether each
is *admissible*.  Neither says whether it is **right**, and nothing in the engine can:
the whole reason the reading direction needs a reviewer is that every check passes on a
well-formed translation of a claim the text did not make.  So the only honest measure is
against knowledge somebody wrote by hand, and this is the arithmetic for that.

## The gold set is read out of a KB, never transcribed

A second copy of the fables' sentexes in a scoring fixture would drift from the ones the
suite actually loads, and a score against a stale gold set is worse than no score.  So
the gold is a set of **handles** in a loaded KB, and a candidate matches when
`vaelii.core/handle-of` finds it under one of them.

That also means the comparison uses the engine's **own** canonical form rather than a
reimplementation of it: a rule whose variables are named differently, whose antecedents
arrive in another order, or whose symmetric arguments are the other way round is the same
sentence to `handle-of` and is therefore the same sentence here (docs/canonicalization.md).
Nothing about matching is this namespace's opinion.

## Two scores, because the constants are unrecoverable

A fable introduces its characters by kind — *a lion*, *a mouse* — so the names in the
formal version (`LionA`, `MouseA`) are the modeller's, and no reader of the text could
produce them.  A strict score therefore reads zero on stories whose structure was
recovered perfectly, which measures the naming convention rather than the reading.

So `score` reports both, and the pair is the finding:

* **strict** — the candidate matched a gold handle as written;
* **aligned** — the same comparison after renaming the candidate's *introduced*
  individuals onto the gold's, one-for-one, by the types each is asserted to have
  (`alignment`).  A renaming is a bijection or it is not applied, so alignment can never
  merge two characters into one to score better.

Nothing here writes: `handle-of` is find-only, and the alignment is arithmetic over
sentences.
raw docstring

vaelii.impl.llm.selection

The selection-scoped prompt: the unit of work is a set of handles, not the KB.

vaelii.impl.llm.prompt renders the whole vocabulary — every context, type and predicate — and vaelii.impl.llm.tools renders every read as a tool schema. Both are fixed costs that grow with the KB, and against the schema-only starter (no individuals, no facts) they already come to ~8,800 tokens before the user has said anything. A KB heading for 100M sentexes cannot pay that per request, and a model with no tools capability cannot spend half of it at all.

So this namespace prompts about what the reader selected:

  1. the selected sentexes as the editor's own [sentence context] lines,
  2. a vocabulary card computed only from the terms those lines mention — each term's comment, its argIsa constraints, its place in the genl hierarchy, and its metadata,
  3. the reader's instruction.

Every read is pinned by a term the selection actually contains (comment-of, an argIsa query on a fixed predicate, a genl closure lookup), so the prompt is O(selection) and flat in KB size. Ten sentexes cost the same in a KB of ten as in a KB of a hundred million.

The model rewrites lines; it does not write. Its answer is the edited line set, which vaelii.impl.llm.session/propose-edit diffs against the selection by content to produce the {:add … :remove …} batch — the same diff the browser's editor does on Save, so an unchanged line touches nothing.

Nothing here truncates. A selection too big for the context window is a clean refusal (budget-problem), because the alternative is Ollama silently dropping the front of the reader's own selection.

The selection-scoped prompt: **the unit of work is a set of handles, not the KB.**

`vaelii.impl.llm.prompt` renders the whole vocabulary — every context, type and
predicate — and `vaelii.impl.llm.tools` renders every read as a tool schema.  Both
are fixed costs that grow with the KB, and against the schema-only starter (no
individuals, no facts) they already come to ~8,800 tokens before the user has said
anything.  A KB heading for 100M sentexes cannot pay that per request, and a model
with no `tools` capability cannot spend half of it at all.

So this namespace prompts about **what the reader selected**:

1. the selected sentexes as the editor's own `[sentence context]` lines,
2. a vocabulary card computed *only* from the terms those lines mention — each
   term's `comment`, its `argIsa` constraints, its place in the genl hierarchy, and
   its metadata,
3. the reader's instruction.

Every read is pinned by a term the selection actually contains (`comment-of`, an
`argIsa` query on a fixed predicate, a genl closure lookup), so the prompt is
**O(selection)** and flat in KB size.  Ten sentexes cost the same in a KB of ten as
in a KB of a hundred million.

**The model rewrites lines; it does not write.**  Its answer is the edited line set,
which `vaelii.impl.llm.session/propose-edit` diffs against the selection by content
to produce the `{:add … :remove …}` batch — the same diff the browser's editor does
on Save, so an unchanged line touches nothing.

**Nothing here truncates.**  A selection too big for the context window is a clean
refusal (`budget-problem`), because the alternative is Ollama silently dropping the
front of the reader's own selection.
raw docstring

vaelii.impl.llm.session

The turn loop: propose → validate → repair → an edit batch.

The model never writes. Its output is {:add [[sentence context opts?] …] :remove [handle …]} — the exact shape vaelii.core/edit takes, and the exact shape the browser's textarea editor already produces. So a proposal lands in the existing editor as a reviewable diff: no new write path, no new trust boundary, and no way for a model turn to reach storage. Applying is a separate, explicit call (apply-proposal!), which is where the ! lives.

The well-formedness checker is the critic. check-batch is vaelii.core/check-editassert's own check chain run over each proposed entry for its answer rather than its effect, storing nothing, reporting each failure with the same :type keyword assert would have thrown (:naming, :not-ground, :not-range-restricted, :not-well-formed, :not-stratified, :arg-type, :disjoint, :functional). That is a deterministic grader rather than a model-judged one, which is what makes the repair loop terminate on a fact rather than on an opinion — and sharing the writer's own chain is what keeps the two from drifting, so the model is never graded more leniently than it will be applied.

The loop is bounded twice. :max-repairs caps how many times a rejected batch is fed back, and :max-turns caps total provider turns including tool calls, so neither a stubborn model nor a tool-calling one can spin. Running out of repairs is a reported outcome (:status :invalid with the rejections), not an exception.

The turn loop: propose → validate → repair → an edit batch.

**The model never writes.**  Its output is `{:add [[sentence context opts?] …]
:remove [handle …]}` — the exact shape `vaelii.core/edit` takes, and the exact shape
the browser's textarea editor already produces.  So a proposal lands in the existing
editor as a reviewable diff: no new write path, no new trust boundary, and no way
for a model turn to reach storage.  Applying is a separate, explicit call
(`apply-proposal!`), which is where the `!` lives.

**The well-formedness checker is the critic.**  `check-batch` is
`vaelii.core/check-edit` — `assert`'s own check chain run over each proposed entry
for its answer rather than its effect, storing nothing, reporting each failure with
the same `:type` keyword `assert` would have thrown (`:naming`, `:not-ground`,
`:not-range-restricted`, `:not-well-formed`, `:not-stratified`, `:arg-type`,
`:disjoint`, `:functional`).  That is a deterministic grader rather than a
model-judged one, which is what makes the repair loop terminate on a fact rather
than on an opinion — and sharing the writer's own chain is what keeps the two from
drifting, so the model is never graded more leniently than it will be applied.

**The loop is bounded twice.**  `:max-repairs` caps how many times a rejected batch
is fed back, and `:max-turns` caps total provider turns including tool calls, so
neither a stubborn model nor a tool-calling one can spin.  Running out of repairs is
a reported outcome (`:status :invalid` with the rejections), not an exception.
raw docstring

vaelii.impl.llm.stub

The default provider: deterministic, offline, no credential.

Standing in the same place vaelii.impl.solve/local-solver stands — the stub that makes the seam usable before (and without) a real backend. lein test runs the whole pipeline against it, so the suite needs no API key and opens no socket, and a deployment with no credential degrades to a provider that proposes nothing rather than to an exception.

Behaviour is scripted, so a test drives the session loop exactly: :script is the sequence of turns to hand back, one per complete/stream call, and :default answers every call past the end of it. Each entry is a full response map, or one of three shorthands:

"some text" a plain text answer {:batch {:add […] :remove […]}} text holding that batch in a fenced edn block {:lines [[sentence context] …]} the selection path's line set (:json? true for the JSON envelope shape instead) {:assertions [sentence …]} the page path's bare sentences, in the JSON envelope it decodes under (:lines? true for the bare-line shape a model ignoring format writes) {:candidates [[sentence seg] …]} the reading path's candidates, each naming the document sentence it came from (:untranslated, :notes) {:verdicts [[item verdict] …]} the judging path's answer, one verdict per numbered claim (true / false / unsure, optional note) {:tool "kb_sentexes_matching" :input {…}} a tool-use turn (:id optional)

With no :script the provider answers every turn with an empty batch — valid, applies to nothing, and never varies. That default is the whole-KB path's answer; on the selection path (session/propose-edit) it reads as unparseable, which is the safe outcome — the only line set meaning "change nothing" is the reader's selection itself, and a provider that never saw it cannot write one.

The default provider: deterministic, offline, no credential.

Standing in the same place `vaelii.impl.solve/local-solver` stands — the stub that
makes the seam usable before (and without) a real backend.  `lein test` runs the
whole pipeline against it, so the suite needs no API key and opens no socket, and a
deployment with no credential degrades to a provider that proposes nothing rather
than to an exception.

Behaviour is **scripted**, so a test drives the session loop exactly: `:script` is
the sequence of turns to hand back, one per `complete`/`stream` call, and `:default`
answers every call past the end of it.  Each entry is a full response map, or one of
three shorthands:

  "some text"                    a plain text answer
  {:batch {:add […] :remove […]}}  text holding that batch in a fenced `edn` block
  {:lines [[sentence context] …]}  the selection path's line set (`:json? true` for
                                   the JSON envelope shape instead)
  {:assertions [sentence …]}       the page path's bare sentences, in the JSON envelope
                                   it decodes under (`:lines? true` for the bare-line
                                   shape a model ignoring `format` writes)
  {:candidates [[sentence seg] …]} the reading path's candidates, each naming the
                                   document sentence it came from (`:untranslated`,
                                   `:notes`)
  {:verdicts [[item verdict] …]}   the judging path's answer, one verdict per numbered
                                   claim (`true` / `false` / `unsure`, optional note)
  {:tool "kb_sentexes_matching" :input {…}}  a tool-use turn (`:id` optional)

With no `:script` the provider answers every turn with an empty batch — valid,
applies to nothing, and never varies.  That default is the *whole-KB* path's answer;
on the selection path (`session/propose-edit`) it reads as unparseable, which is the
safe outcome — the only line set meaning "change nothing" is the reader's selection
itself, and a provider that never saw it cannot write one.
raw docstring

vaelii.impl.llm.text

The document-scoped prompt: text in, candidates out — never knowledge in.

vaelii.impl.gloss composes English out of the KB and says why that direction is the dangerous one: nothing in the engine can check that a sentence means what a text said. This namespace is the other direction, and it has to answer that argument rather than ignore it, because every check the engine has passes on a correct-looking translation of the wrong claim — naming, well-formedness, argument types and disjointness all read the sentence. So what is built here is a candidate generator with a reviewer between it and the store, and every decision below follows from that:

  • a candidate is a [sentence context opts] entry, which is the shape vaelii.core/edit already takes and the browser's editor already parses — so a proposal lands as a reviewable diff and there is no second write path;
  • a candidate carries the span of the text it came from, so an accepted sentence is auditable back to the sentence that produced it;
  • a candidate is :default, never :monotonic unless the reader says so — a translated guess asserted as known-true would defeat hand-written defaults;
  • what the pipeline could not translate is part of the answer (coverage), because a reader who is shown only the two-thirds that worked reads it as a reader that understood the document.

Where the boundary is. Nothing here is in vaelii.core, and nothing here writes. Like web / serve / llm, this is an application over the engine: the engine's contribution is the critic (check-edit), the vocabulary (the taxonomy and the declarations), the provenance side map, and the equality partition — all of them public reads that existed already. See docs/reading.md.

Resolution is the problem; parsing is not

A pipeline that coins has_black_and_white_feathers for every sentence produces fragmentation rather than knowledge, and no naming check refuses it (docs/naming.md says so in as many words). So the document's own words are resolved against the vocabulary the KB already has before a model is asked anything:

  • spellings turns each run of the document's words into the symbols a KB term could be spelled as — prepared for winter into preparedForWinter and prepared_for_winter, Fido into Fido — and known asks the KB which of them it has. Generating and asking runs the opposite way from inverting the KB's vocabulary into the words each term is written with, which is the one read here that would grow with the KB;
  • resolve-in walks the document longest-run-first and non-overlapping, so a compound predicate is not shredded into the words its name is made of;
  • every resolved term is the equality partition's representative, so a word spelled at a retired name resolves to the term that name was merged into.

What resolves becomes the vocabulary card (document-inventory), which is the prevention half of the fragmentation guard; the detection half is vaelii.impl.llm.inventory/coined, unchanged and shared with the other three paths.

The document-scoped prompt: **text in, candidates out — never knowledge in.**

`vaelii.impl.gloss` composes English *out of* the KB and says why that direction is the
dangerous one: nothing in the engine can check that a sentence means what a text said.
This namespace is the other direction, and it has to answer that argument rather than
ignore it, because every check the engine has passes on a correct-looking translation of
the wrong claim — naming, well-formedness, argument types and disjointness all read the
*sentence*.  So what is built here is a **candidate generator with a reviewer between it
and the store**, and every decision below follows from that:

* a candidate is a `[sentence context opts]` entry, which is the shape
  `vaelii.core/edit` already takes and the browser's editor already parses — so a
  proposal lands as a reviewable diff and there is no second write path;
* a candidate carries the **span** of the text it came from, so an accepted sentence is
  auditable back to the sentence that produced it;
* a candidate is `:default`, never `:monotonic` unless the reader says so — a translated
  guess asserted as known-true would defeat hand-written defaults;
* what the pipeline **could not** translate is part of the answer (`coverage`), because a
  reader who is shown only the two-thirds that worked reads it as a reader that
  understood the document.

**Where the boundary is.**  Nothing here is in `vaelii.core`, and nothing here writes.
Like `web` / `serve` / `llm`, this is an application over the engine: the engine's
contribution is the critic (`check-edit`), the vocabulary (the taxonomy and the
declarations), the provenance side map, and the equality partition — all of them public
reads that existed already.  See docs/reading.md.

## Resolution is the problem; parsing is not

A pipeline that coins `has_black_and_white_feathers` for every sentence produces
fragmentation rather than knowledge, and no naming check refuses it (docs/naming.md says
so in as many words).  So the document's own words are resolved against the vocabulary
the KB already has **before** a model is asked anything:

* `spellings` turns each run of the document's words into the symbols a KB term could be
  spelled as — *prepared for winter* into `preparedForWinter` and `prepared_for_winter`,
  *Fido* into `Fido` — and `known` asks the KB which of them it has.  Generating and
  asking runs the *opposite* way from inverting the KB's vocabulary into the words each
  term is written with, which is the one read here that would grow with the KB;
* `resolve-in` walks the document longest-run-first and non-overlapping, so a compound
  predicate is not shredded into the words its name is made of;
* every resolved term is the equality partition's `representative`, so a word spelled at
  a retired name resolves to the term that name was merged into.

What resolves becomes the vocabulary card (`document-inventory`), which is the
prevention half of the fragmentation guard; the detection half is
`vaelii.impl.llm.inventory/coined`, unchanged and shared with the other three paths.
raw docstring

vaelii.impl.llm.tools

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)", "WellContext" — 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)"`, `"WellContext"` — read back with
`clojure.edn/read-string` (never `read-string`: EDN has no reader-eval, so a model's
output cannot evaluate code).
raw docstring

vaelii.impl.llm.verdict

What a reviewer needs to know about one proposed line, gathered in one place.

A proposal is judged on four independent axes, and they are independent in the strong sense: a line can be admissible and still wrong-shaped, refused and still worth keeping once rewritten, admissible and shaped right and still quietly fragmenting the vocabulary. Prose about a batch buries all four; a reviewer reads a gutter.

:problems what the KB itself says — vaelii.core/check-edit, typed :correction what shape it should have been in — vaelii.impl.llm.correct :coined what vocabulary it invents — vaelii.impl.llm.inventory/coined :confidence how sure the correction is, which is a fifth thing only in the sense that a rewrite the engine cannot decide is a decision handed back

Each already exists; what did not is one call that runs all of them over one batch and lines the answers up by entry, so a caller renders a row rather than joining three reports by index. Nothing here stores, checks a rewrite, or applies one: this is a reading of a proposal, and the proposal is still a proposal afterwards.

What a reviewer needs to know about one proposed line, gathered in one place.

A proposal is judged on **four independent axes**, and they are independent in the
strong sense: a line can be admissible and still wrong-shaped, refused and still worth
keeping once rewritten, admissible and shaped right and still quietly fragmenting the
vocabulary.  Prose about a batch buries all four; a reviewer reads a gutter.

  :problems    what the KB itself says — `vaelii.core/check-edit`, typed
  :correction  what shape it should have been in — `vaelii.impl.llm.correct`
  :coined      what vocabulary it invents — `vaelii.impl.llm.inventory/coined`
  :confidence  how sure the correction is, which is a fifth thing only in the sense
               that a rewrite the engine cannot decide is a decision handed back

Each already exists; what did not is one call that runs all of them over one batch and
lines the answers up **by entry**, so a caller renders a row rather than joining three
reports by index.  Nothing here stores, checks a rewrite, or applies one: this is a
reading of a proposal, and the proposal is still a proposal afterwards.
raw 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