Liking cljdoc? Tell your friends :D

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

apply-proposal!clj

(apply-proposal! kb proposal)
(apply-proposal! kb {:keys [status batch] :as proposal} {:keys [force?]})

Apply a proposal's batch through vaelii.core/editthe only thing in this namespace that writes, and it is never reached from propose.

Refuses a proposal that is not :ok unless {:force? true} says otherwise, so a batch the critic rejected cannot be applied by accident. ! because the batch's :remove retracts stored knowledge.

Returns {:result <edit result> :violations […] :contradictions […]} — the post-settle signal, with :violations narrowed to what this edit added to the accumulating ledger (a derived conclusion the definitional checks dropped).

Apply a proposal's batch through `vaelii.core/edit` — **the only thing in this
namespace that writes**, and it is never reached from `propose`.

Refuses a proposal that is not `:ok` unless `{:force? true}` says otherwise, so a
batch the critic rejected cannot be applied by accident.  `!` because the batch's
`:remove` retracts stored knowledge.

Returns `{:result <edit result> :violations […] :contradictions […]}` — the
post-settle signal, with `:violations` narrowed to what *this* edit added to the
accumulating ledger (a derived conclusion the definitional checks dropped).
raw docstring

assertion-summaryclj

(assertion-summary sentences {:keys [entries known duplicates]})

What the model produced, in counts: {:proposed :new :known :duplicate} — how many assertions it wrote, how many are new knowledge, how many restate what is already stored, and how many it repeated.

What the model produced, in counts: `{:proposed :new :known :duplicate}` — how many
assertions it wrote, how many are new knowledge, how many restate what is already
stored, and how many it repeated.
raw docstring

check-batchclj

(check-batch kb batch)

Every problem in a batch, as [{:in :add|:remove :index i :entry e :type k :message "…"} …] — empty when the batch is admissible. Adds are checked against the KB as it stands; a removal is checked only for naming an actually stored handle.

The :type is the one assert would have thrown, so a rejection fed back to the model names the same failure the apply would have hit — plus :context-escape, the one problem the chain cannot see (placement-problem), since assert would carry that entry out correctly and into the wrong context.

Every problem in a batch, as
`[{:in :add|:remove :index i :entry e :type k :message "…"} …]` — empty when the
batch is admissible.  Adds are checked against the KB as it stands; a removal is
checked only for naming an actually stored handle.

The `:type` is the one `assert` would have thrown, so a rejection fed back to the
model names the same failure the apply would have hit — plus `:context-escape`, the one
problem the chain cannot see (`placement-problem`), since `assert` would carry that
entry out *correctly* and into the wrong context.
raw docstring

check-entryclj

(check-entry kb entry)

The first problem in one [sentence context opts?] entry as {:type … :message …}, or nil when it is admissible — check-batch narrowed to a single add, plus placement-problem, which the chain has no way to see.

The first problem in one `[sentence context opts?]` entry as `{:type … :message …}`,
or nil when it is admissible — `check-batch` narrowed to a single add, plus
`placement-problem`, which the chain has no way to see.
raw docstring

coinedclj

(coined kb batch)

The vocabulary a proposed batch introduces: {:coined [{:predicate :arity :role :in :index} …] :vocabulary {…}}.

Reported, never rejected — coining a type is how an ontology grows, and which side of that a given proposal falls on is a human's call. Called through here for the same reason check-batch is: one place a caller looks for what a proposal was graded on.

The vocabulary a proposed batch introduces:
`{:coined [{:predicate :arity :role :in :index} …] :vocabulary {…}}`.

Reported, never rejected — coining a type is how an ontology grows, and which side of
that a given proposal falls on is a human's call.  Called through here for the same
reason `check-batch` is: one place a caller looks for what a proposal was graded on.
raw docstring

diff-batchclj

(diff-batch rows proposed)

The selection and the model's edited lines -> the {:add … :remove …} batch.

Diffed by content, exactly as the browser's editor diffs a saved textarea: a line returned unchanged is in both sets and touches nothing, a line rewritten retracts the old handle and asserts the new content, a line dropped retracts, a line invented asserts. So no handle churns for knowledge the model left alone.

The selection and the model's edited lines -> the `{:add … :remove …}` batch.

Diffed **by content**, exactly as the browser's editor diffs a saved textarea: a line
returned unchanged is in both sets and touches nothing, a line rewritten retracts the
old handle and asserts the new content, a line dropped retracts, a line invented
asserts.  So no handle churns for knowledge the model left alone.
raw docstring

edit-summaryclj

(edit-summary rows proposed {:keys [add remove]})

What the model did to the selection, in counts: {:selected :returned :unchanged :removed :added}.

Worth reporting separately from the batch because a dropped line is a retraction — the model deleting a line it was asked to leave alone is legal, well-formed, and invisible to the critic, so the one thing a reviewer must not miss is how many sentexes the proposal removes. A content diff cannot tell a rewrite from a delete-plus-add, so these are counts of what the diff is, not a guess at intent.

What the model did to the selection, in counts:
`{:selected :returned :unchanged :removed :added}`.

Worth reporting separately from the batch because **a dropped line is a retraction**
— the model deleting a line it was asked to leave alone is legal, well-formed, and
invisible to the critic, so the one thing a reviewer must not miss is how many
sentexes the proposal removes.  A content diff cannot tell a rewrite from a
delete-plus-add, so these are counts of what the diff *is*, not a guess at intent.
raw docstring

new-assertionsclj

(new-assertions kb context items)
(new-assertions kb context items ->entry)

The read sentences -> {:entries [[sentence context opts?] …] :known […] :duplicates […]}.

Two kinds of non-news are separated out rather than proposed: a sentence the KB already stores in this context (re-asserting is a no-op find-or-create, so it is noise in a diff), and one the model wrote twice. Both are counted for the reviewer, because a proposal that is 80% restatement is a different thing from one that is not.

->entry builds the entry for a kept item and defaults to the page path's, which carries nothing but strength. The reading path passes its own, because a candidate read out of text carries a span into provenance — and asking is this already stored is the same question on both paths, so it is asked in one place rather than twice.

The read sentences -> `{:entries [[sentence context opts?] …] :known […] :duplicates […]}`.

Two kinds of non-news are separated out rather than proposed: a sentence the KB
**already stores** in this context (re-asserting is a no-op find-or-create, so it is
noise in a diff), and one the model wrote **twice**.  Both are counted for the reviewer,
because a proposal that is 80% restatement is a different thing from one that is not.

`->entry` builds the entry for a kept item and defaults to the page path's, which carries
nothing but strength.  The reading path passes its own, because a candidate read out of
text carries a **span** into provenance — and asking *is this already stored* is the same
question on both paths, so it is asked in one place rather than twice.
raw docstring

parse-assertionsclj

(parse-assertions text)

A model's answer -> {:sentences [{:sentence :strength} …] :notes "…" :problems […]}, or {:errors ["…" …]} when nothing readable came back.

The JSON envelope is the contract here (decoding is constrained to it), with a markdown fence stripped first — models fence unprompted, including while decoding under a schema that cannot express one — and bare lines read as the fallback. Unlike the edit path, a line the parser cannot read is a problem, not a failure: this answer only adds, so the readable assertions stand on their own and the unreadable ones are reported beside them.

A model's answer -> `{:sentences [{:sentence :strength} …] :notes "…" :problems […]}`,
or `{:errors ["…" …]}` when nothing readable came back.

The JSON envelope is the contract here (decoding is constrained to it), with a
markdown fence stripped first — models fence unprompted, including while decoding under
a schema that cannot express one — and bare lines read as the fallback.  Unlike the edit
path, a line the parser cannot read is a **problem, not a failure**: this answer only
adds, so the readable assertions stand on their own and the unreadable ones are reported
beside them.
raw docstring

parse-batchclj

(parse-batch text)

Pull the proposed batch out of a model's answer.

Reads the last fenced block (a model often shows a wrong draft before the final one), falling back to the whole text when there is no fence, with clojure.edn/read-string — never read-string, so a model's output cannot evaluate code. Returns {:batch {:add […] :remove […]}} or {:error "…"}.

Pull the proposed batch out of a model's answer.

Reads the **last** fenced block (a model often shows a wrong draft before the final
one), falling back to the whole text when there is no fence, with
`clojure.edn/read-string` — never `read-string`, so a model's output cannot evaluate
code.  Returns `{:batch {:add […] :remove […]}}` or `{:error "…"}`.
raw docstring

parse-candidatesclj

(parse-candidates text)

A model's answer -> {:candidates [{:sentence :segment :confidence :strength} …] :untranslated […] :notes "…" :problems […]}, or {:errors […]} when nothing readable came back.

The JSON envelope is the contract (decoding is constrained to vaelii.impl.llm.text/output-schema), with a fence stripped first. Unlike the edit path there is no fallback to bare lines: a bare sentence carries no segment number, and a candidate with no segment has no span — which is most of what this path is for. A line the parser cannot read is a problem, not a failure, as on the page path: this answer only adds, so the readable candidates stand on their own.

A model's answer -> `{:candidates [{:sentence :segment :confidence :strength} …]
:untranslated […] :notes "…" :problems […]}`, or `{:errors […]}` when nothing readable
came back.

The JSON envelope is the contract (decoding is constrained to
`vaelii.impl.llm.text/output-schema`), with a fence stripped first.  Unlike the edit path
there is no fallback to bare lines: a bare sentence carries no segment number, and a
candidate with no segment has no span — which is most of what this path is for.  A line
the parser cannot read is a **problem, not a failure**, as on the page path: this answer
only adds, so the readable candidates stand on their own.
raw docstring

parse-linesclj

(parse-lines text)

A model's answer -> {:rows [{:key [sentence context] :entry […]} …] :notes "…"}, or {:errors ["…" …]}.

Parsed defensively, because the answer shape is not something every model can be held to: a markdown fence is stripped (models add one unprompted, including while decoding under a JSON schema), a JSON envelope is read if one arrives, and otherwise — the normal case, and the contract — the text is read as the editor's own lines.

A model's answer -> `{:rows [{:key [sentence context] :entry […]} …] :notes "…"}`,
or `{:errors ["…" …]}`.

Parsed **defensively**, because the answer shape is not something every model can be
held to: a markdown fence is stripped (models add one unprompted, including while
decoding under a JSON schema), a JSON envelope is read if one arrives, and otherwise
— the normal case, and the contract — the text is read as the editor's own lines.
raw docstring

placement-problemclj

(placement-problem entry)

The problem with an entry whose sentence would file itself somewhere other than the context the entry names, or nil.

(ist Ctx S) is find-or-create in Ctx, so an entry [(ist CriedWolfContext (dog Sneaky)) LionMouseContext] stores in CriedWolfContext and the context column a reviewer reads is a lie. Nothing in the check chain objects — the sentence is well-formed and every name in it is legal — so this is checked here, where the entry and its intended context are both in hand.

It matters most on the two paths that promise the context is the caller's and not the model's (propose-page, propose-text), where it is the only way that promise can be broken; it is refused on the other two as well, because a line whose displayed context contradicts where it lands is a bad line to show a reviewer whoever wrote it.

Only a top-level ist. A rule consequent may legitimately be one — that is how a rule says where its conclusions are placed (docs/contexts.md) — and it is written out in a line the reviewer reads, so it is visible rather than hidden.

The problem with an entry whose sentence would file itself **somewhere other than the
context the entry names**, or nil.

`(ist Ctx S)` is find-or-create in `Ctx`, so an entry
`[(ist CriedWolfContext (dog Sneaky)) LionMouseContext]` stores in `CriedWolfContext`
and the context column a reviewer reads is a lie.  Nothing in the check chain objects —
the sentence is well-formed and every name in it is legal — so this is checked here,
where the entry and its intended context are both in hand.

It matters most on the two paths that promise the context is **the caller's** and not
the model's (`propose-page`, `propose-text`), where it is the only way that promise can
be broken; it is refused on the other two as well, because a line whose displayed
context contradicts where it lands is a bad line to show a reviewer whoever wrote it.

Only a **top-level** `ist`.  A rule *consequent* may legitimately be one — that is how a
rule says where its conclusions are placed (docs/contexts.md) — and it is written out in
a line the reviewer reads, so it is visible rather than hidden.
raw docstring

proposeclj

(propose kb
         {:keys [message provider system prompt-opts tool-opts model max-tokens
                 effort thinking-display max-repairs max-turns on-event]
          :or {max-repairs 2 max-turns 24}})

Ask provider for an edit batch and return a proposal — never applied.

This is the entry point an application (the browser's panel, the CLI) calls. opts:

:message the user's request (required) :provider a vaelii.impl.llm.protocol/Provider (default: the offline stub) :system override the generated system prompt (default: generated from kb) :prompt-opts passed to vaelii.impl.llm.prompt/system-prompt :tool-opts passed to vaelii.impl.llm.tools/schemas (:only / :exclude) :model :max-tokens :effort :thinking-display forwarded to the provider :max-repairs rejected batches fed back before giving up (default 2) :max-turns total provider turns, tool calls included (default 24) :on-event when present, the provider streams and every event is handed here — what an SSE endpoint consumes

Returns:

{:status :ok | :invalid | :unparseable | :refused | :exhausted :batch {:add […] :remove […]} ; present on :ok and :invalid :edn "…" ; the batch pretty-printed, for the editor :rejections [{:in :index :entry :type :message} …] :coined [{:predicate :arity :role :in :index} …] ; vocabulary it invented :vocabulary {:literals :reused :coined :coined-types :coined-relations} :text the model's final prose :attempts how many batches it proposed :turns how many provider turns it took :tool-calls how many read tools it ran :messages the full conversation, for a follow-up turn}

:ok means every entry passed the same checks assert runs — not that a human agreed with it. :refused means the provider's safety classifiers declined and the content is empty or partial; :exhausted means the turn cap was reached with no final answer.

Ask `provider` for an edit batch and return a **proposal** — never applied.

This is the entry point an application (the browser's panel, the CLI) calls.
`opts`:

  :message      the user's request (required)
  :provider     a `vaelii.impl.llm.protocol/Provider` (default: the offline stub)
  :system       override the generated system prompt (default: generated from `kb`)
  :prompt-opts  passed to `vaelii.impl.llm.prompt/system-prompt`
  :tool-opts    passed to `vaelii.impl.llm.tools/schemas` (`:only` / `:exclude`)
  :model :max-tokens :effort :thinking-display   forwarded to the provider
  :max-repairs  rejected batches fed back before giving up (default 2)
  :max-turns    total provider turns, tool calls included (default 24)
  :on-event     when present, the provider streams and every event is handed here —
                what an SSE endpoint consumes

Returns:

  {:status     :ok | :invalid | :unparseable | :refused | :exhausted
   :batch      {:add […] :remove […]}   ; present on :ok and :invalid
   :edn        "…"                    ; the batch pretty-printed, for the editor
   :rejections [{:in :index :entry :type :message} …]
   :coined     [{:predicate :arity :role :in :index} …]  ; vocabulary it invented
   :vocabulary {:literals :reused :coined :coined-types :coined-relations}
   :text       the model's final prose
   :attempts   how many batches it proposed
   :turns      how many provider turns it took
   :tool-calls how many read tools it ran
   :messages   the full conversation, for a follow-up turn}

`:ok` means every entry passed the same checks `assert` runs — not that a human
agreed with it.  `:refused` means the provider's safety classifiers declined and the
content is empty or partial; `:exhausted` means the turn cap was reached with no
final answer.
raw docstring

propose-editclj

(propose-edit kb
              {:keys [handles message provider num-ctx max-repairs max-turns
                      prompt-opts model max-tokens on-event]
               fmt :format
               :or {max-repairs 2 max-turns 6 num-ctx 8192}})

Ask a model to edit a selection of sentexes and return a proposal — never applied. This is what a browser selection panel calls.

opts:

:handles the selected sentex handles, in page order (required) :message the reader's instruction (required) :provider a Provider (default: the offline stub) :num-ctx the context window to size the request against (default 8192) :max-repairs rejected answers fed back before giving up (default 2) :max-turns total provider turns (default 6) :prompt-opts passed to vaelii.impl.llm.selection/vocabulary-card :format a JSON schema to constrain decoding with — an optimization for a model it demonstrably helps, not the contract, which is the editor's line format. selection/output-schema is the one to pass. :model :max-tokens :on-event forwarded to the provider

Returns:

{:status :ok | :invalid | :unparseable | :refused | :exhausted | :too-large | :empty-selection :batch {:add […] :remove […]} ; present on :ok and :invalid :edn "…" ; the batch, for apply-proposal! :lines "[…]\n[…]" ; the edited lines, for the editor textarea :summary {:selected :returned :unchanged :removed :added} ; what it did :rejections [{:in :index :entry :type :message} …] :coined [{:predicate :arity :role :in :index} …] ; vocabulary it invented :vocabulary {:literals :reused :coined :coined-types :coined-relations} :notes what the model was unsure of :selection [{:handle :line} …] ; what it was actually shown :budget {:prompt :reserved :total :num-ctx :headroom} ; estimated tokens :usage {:input-tokens :output-tokens …} ; measured, by the host :elapsed-ms wall clock for the whole loop :attempts :turns :messages}

:too-large means the request does not fit the window — reported with the numbers and nothing sent, because the alternative is the host silently truncating the reader's own selection. It is checked before the first turn and before every repair turn, since a repair carries the whole conversation back. :empty-selection means no handle still names a stored sentex.

Ask a model to edit **a selection of sentexes** and return a proposal — never
applied.  This is what a browser selection panel calls.

`opts`:

  :handles      the selected sentex handles, in page order (required)
  :message      the reader's instruction (required)
  :provider     a `Provider` (default: the offline stub)
  :num-ctx      the context window to size the request against (default 8192)
  :max-repairs  rejected answers fed back before giving up (default 2)
  :max-turns    total provider turns (default 6)
  :prompt-opts  passed to `vaelii.impl.llm.selection/vocabulary-card`
  :format       a JSON schema to constrain decoding with — an **optimization** for a
                model it demonstrably helps, not the contract, which is the editor's
                line format.  `selection/output-schema` is the one to pass.
  :model :max-tokens :on-event   forwarded to the provider

Returns:

  {:status     :ok | :invalid | :unparseable | :refused | :exhausted
               | :too-large | :empty-selection
   :batch      {:add […] :remove […]}   ; present on :ok and :invalid
   :edn        "…"                    ; the batch, for `apply-proposal!`
   :lines      "[…]\n[…]"            ; the edited lines, for the editor textarea
   :summary    {:selected :returned :unchanged :removed :added}  ; what it did
   :rejections [{:in :index :entry :type :message} …]
   :coined     [{:predicate :arity :role :in :index} …]  ; vocabulary it invented
   :vocabulary {:literals :reused :coined :coined-types :coined-relations}
   :notes      what the model was unsure of
   :selection  [{:handle :line} …]      ; what it was actually shown
   :budget     {:prompt :reserved :total :num-ctx :headroom}   ; estimated tokens
   :usage      {:input-tokens :output-tokens …}                ; measured, by the host
   :elapsed-ms wall clock for the whole loop
   :attempts :turns :messages}

`:too-large` means the request does not fit the window — reported with the numbers
and **nothing sent**, because the alternative is the host silently truncating the
reader's own selection.  It is checked before the first turn *and before every repair
turn*, since a repair carries the whole conversation back.  `:empty-selection` means
no handle still names a stored sentex.
raw docstring

propose-pageclj

(propose-page kb
              {:keys [term context message provider num-ctx max-repairs
                      max-turns max-assertions prompt-opts model max-tokens
                      on-event]
               :or {max-repairs 1 max-turns 4 num-ctx 8192 max-assertions 24}
               :as opts})

Ask a model for new knowledge about the term on a page and return a proposal — never applied. This is what a term page's panel calls when the reader types something open-ended ("flesh out the capabilities of this").

opts:

:term the page's term, a symbol (required) :message the reader's instruction (required) :context the context new assertions are filed in. Defaults to the context most of the term's own sentexes are in, else UniverseContext; the choice is reported back as :context :provider a Provider (default: the offline stub) :num-ctx the context window to size the request against (default 8192) :max-assertions how many assertions to ask for, and the output room reserved for them (default 24) :max-repairs rejected answers fed back before giving up (default 1 — an additive answer's good assertions survive a rejection, and a second round costs a whole generation) :max-turns total provider turns (default 4) :prompt-opts passed to vaelii.impl.llm.page/user-turn:max-lines for the page's own content, :max-predicates / :max-types / :max-tokens for the vocabulary card :format the JSON schema decoding is constrained to. Defaults to vaelii.impl.llm.page/output-schema, which is the contract on this path; pass :format nil to send none :model :max-tokens :on-event forwarded to the provider

Returns the same shape propose-edit does, so one panel handles both:

{:status :ok | :invalid | :unparseable | :refused | :exhausted | :too-large | :no-term :batch {:add [[sentence context opts?] …] :remove []} ; generation never removes :edn "…" ; the batch, for apply-proposal! :lines "[…]\n[…]" ; the entries, for the editor textarea :summary {:proposed :new :known :duplicate} :rejections [{:in :index :entry :type :message} …] :coined [{:predicate :arity :role :in :index} …] :vocabulary {:literals :reused :coined :coined-types :coined-relations} :problems ["…"] ; lines the parser could not read :notes what the model was unsure of :term :context ; what it wrote about, and where it lands :page [{:handle :line} …] ; the page content it was shown :budget {:prompt :reserved :total :num-ctx :headroom} ; estimated tokens :usage {:input-tokens :output-tokens …} ; measured, by the host :first-assertion-ms ; time to the model's first complete assertion :elapsed-ms :attempts :turns :messages}

:coined is the one report that matters most here and the only guard against vocabulary fragmentation, since the check chain admits a coined unary predicate by design — see vaelii.impl.llm.inventory.

Ask a model for **new knowledge about the term on a page** and return a proposal —
never applied.  This is what a term page's panel calls when the reader types something
open-ended ("flesh out the capabilities of this").

`opts`:

  :term         the page's term, a symbol (required)
  :message      the reader's instruction (required)
  :context      the context new assertions are filed in.  Defaults to the context most
                of the term's own sentexes are in, else `UniverseContext`; the choice is
                reported back as `:context`
  :provider     a `Provider` (default: the offline stub)
  :num-ctx      the context window to size the request against (default 8192)
  :max-assertions  how many assertions to ask for, and the output room reserved for
                them (default 24)
  :max-repairs  rejected answers fed back before giving up (default 1 — an additive
                answer's good assertions survive a rejection, and a second round costs
                a whole generation)
  :max-turns    total provider turns (default 4)
  :prompt-opts  passed to `vaelii.impl.llm.page/user-turn` — `:max-lines` for the page's
                own content, `:max-predicates` / `:max-types` / `:max-tokens` for the
                vocabulary card
  :format       the JSON schema decoding is constrained to.  Defaults to
                `vaelii.impl.llm.page/output-schema`, which is **the contract on this
                path**; pass `:format nil` to send none
  :model :max-tokens :on-event   forwarded to the provider

Returns the same shape `propose-edit` does, so one panel handles both:

  {:status     :ok | :invalid | :unparseable | :refused | :exhausted | :too-large
               | :no-term
   :batch      {:add [[sentence context opts?] …] :remove []}   ; generation never removes
   :edn        "…"                    ; the batch, for `apply-proposal!`
   :lines      "[…]\n[…]"            ; the entries, for the editor textarea
   :summary    {:proposed :new :known :duplicate}
   :rejections [{:in :index :entry :type :message} …]
   :coined     [{:predicate :arity :role :in :index} …]
   :vocabulary {:literals :reused :coined :coined-types :coined-relations}
   :problems   ["…"]                  ; lines the parser could not read
   :notes      what the model was unsure of
   :term :context                       ; what it wrote about, and where it lands
   :page       [{:handle :line} …]      ; the page content it was shown
   :budget     {:prompt :reserved :total :num-ctx :headroom}   ; estimated tokens
   :usage      {:input-tokens :output-tokens …}                ; measured, by the host
   :first-assertion-ms                  ; time to the model's first complete assertion
   :elapsed-ms :attempts :turns :messages}

`:coined` is the one report that matters most here and the only guard against
vocabulary fragmentation, since the check chain admits a coined unary predicate by
design — see `vaelii.impl.llm.inventory`.
raw docstring

propose-textclj

(propose-text kb
              {:keys [text context source instruction provider num-ctx
                      max-repairs max-turns max-candidates prompt-opts model
                      max-tokens on-event]
               :or {max-repairs 1 max-turns 4 num-ctx 8192 max-candidates 40}
               :as opts})

Read English and answer with candidate sentexes — never applied, and never asserted.

This is the reading direction, and the whole of it is a proposal: nothing in the engine can check that a sentence means what a text said (vaelii.impl.gloss states the argument and docs/reading.md answers it), so what comes back is a review queue with the engine's own critic already run over it.

opts:

:text the document (required) :context the context candidates are filed in (required) — never the model's to write, exactly as on the page path :source what to record as provenance's :source (default: :text) :instruction what the reader wants out of the document, if anything :provider a Provider (default: the offline stub) :num-ctx the context window to size the request against (default 8192) :max-candidates how many to ask for, and the output room reserved (default 40) :max-repairs unreadable answers fed back before giving up (default 1) :max-turns total provider turns (default 4) :prompt-opts passed to vaelii.impl.llm.text/user-turn (:max-relations, :max-types, :max-tokens) :format the JSON schema decoding is constrained to. Defaults to vaelii.impl.llm.text/output-schema, the contract here; nil sends none :model :max-tokens :on-event forwarded to the provider

Returns propose-edit's shape, so the same panel handles it, plus the fields only a document has:

{:status :ok | :invalid | :unparseable | :refused | :exhausted | :too-large | :no-text :batch {:add [[sentence context {:provenance {…}}] …] :remove []} ; always applicable :repairs [{:index :entry :problem :correction} …] ; refused, with the verdict :corrections [{:from :to :alternatives :why …} …] ; admissible and still wrong-shaped :coverage {:segments :covered :uncovered [{:index :span :text :reason} …]} :queue [{:index :entry :flagged? :confidence :segment} …] ; the order to review in :candidates [{:sentence :segment :confidence :strength} …] ; every claim as read :segments [{:text :span} …] ; what the spans point into :resolved [{:surface :term :segment :span} …] ; words that already named something :summary {:proposed :new :known :duplicate :applicable :repairs :corrections} :lines :edn :rejections :coined :vocabulary :notes :problems :budget :usage :elapsed-ms :attempts :turns :messages}

:status :ok means the batch is applicable and says nothing about whether the reading is right — that is what :queue and a person are for. :invalid means nothing survived the critic at all. Every candidate is :default unless it claimed otherwise, because a translated guess at :monotonic would defeat hand-written defaults.

:candidates is every claim the model made, before the winnowing that drops what the KB already stores and what the critic refused — which is what a score has to be taken over (vaelii.impl.llm.score), since a reading judged only on what survived would be judged against the KB it was read into.

Read **English** and answer with candidate sentexes — never applied, and never asserted.

This is the reading direction, and the whole of it is a proposal: nothing in the engine
can check that a sentence means what a text said (`vaelii.impl.gloss` states the argument
and docs/reading.md answers it), so what comes back is a review queue with the engine's
own critic already run over it.

`opts`:

  :text         the document (required)
  :context      the context candidates are filed in (required) — never the model's to
                write, exactly as on the page path
  :source       what to record as provenance's `:source` (default: `:text`)
  :instruction  what the reader wants out of the document, if anything
  :provider     a `Provider` (default: the offline stub)
  :num-ctx      the context window to size the request against (default 8192)
  :max-candidates  how many to ask for, and the output room reserved (default 40)
  :max-repairs  unreadable answers fed back before giving up (default 1)
  :max-turns    total provider turns (default 4)
  :prompt-opts  passed to `vaelii.impl.llm.text/user-turn` (`:max-relations`,
                `:max-types`, `:max-tokens`)
  :format       the JSON schema decoding is constrained to.  Defaults to
                `vaelii.impl.llm.text/output-schema`, **the contract here**; `nil` sends none
  :model :max-tokens :on-event   forwarded to the provider

Returns `propose-edit`'s shape, so the same panel handles it, plus the fields only a
document has:

  {:status      :ok | :invalid | :unparseable | :refused | :exhausted | :too-large
                | :no-text
   :batch       {:add [[sentence context {:provenance {…}}] …] :remove []} ; always applicable
   :repairs     [{:index :entry :problem :correction} …] ; refused, with the verdict
   :corrections [{:from :to :alternatives :why …} …]     ; admissible and still wrong-shaped
   :coverage    {:segments :covered :uncovered [{:index :span :text :reason} …]}
   :queue       [{:index :entry :flagged? :confidence :segment} …] ; the order to review in
   :candidates  [{:sentence :segment :confidence :strength} …]     ; every claim as read
   :segments    [{:text :span} …]                  ; what the spans point into
   :resolved    [{:surface :term :segment :span} …] ; words that already named something
   :summary     {:proposed :new :known :duplicate :applicable :repairs :corrections}
   :lines :edn :rejections :coined :vocabulary :notes :problems
   :budget :usage :elapsed-ms :attempts :turns :messages}

`:status :ok` means the batch is applicable and says nothing about whether the reading is
*right* — that is what `:queue` and a person are for.  `:invalid` means nothing survived
the critic at all.  Every candidate is `:default` unless it claimed otherwise, because a
translated guess at `:monotonic` would defeat hand-written defaults.

`:candidates` is every claim the model made, before the winnowing that drops what the KB
already stores and what the critic refused — which is what a *score* has to be taken over
(`vaelii.impl.llm.score`), since a reading judged only on what survived would be judged
against the KB it was read into.
raw docstring

read-sentenceclj

(read-sentence text)

One s-expression's text -> the sentence, or nil when it is not one. Read with clojure.edn/read-string — never read-string — so model output cannot evaluate code, and retried unescaped for text lifted out of a JSON string.

One s-expression's text -> the sentence, or nil when it is not one.  Read with
`clojure.edn/read-string` — never `read-string` — so model output cannot evaluate code,
and retried unescaped for text lifted out of a JSON string.
raw docstring

repair-assertions-promptclj

(repair-assertions-prompt problems)

The turn handed back after a rejected or unreadable generation: the problems verbatim, and the same contract as the first turn.

The turn handed back after a rejected or unreadable generation: the problems verbatim,
and the same contract as the first turn.
raw docstring

repair-candidates-promptclj

(repair-candidates-prompt problems)

The turn handed back after an unreadable answer: the problems verbatim, and the same contract as the first turn.

The turn handed back after an unreadable answer: the problems verbatim, and the same
contract as the first turn.
raw docstring

repair-lines-promptclj

(repair-lines-prompt problems)

The turn handed back after a rejected or unreadable answer on the selection path: the problems verbatim, and the same instruction as the first turn — return the whole edited line set, not a patch.

The turn handed back after a rejected or unreadable answer on the selection path:
the problems verbatim, and the same instruction as the first turn — return the whole
edited line set, not a patch.
raw docstring

repair-promptclj

(repair-prompt rejections)

The turn handed back to the model after a rejected batch: the typed rejections, verbatim, and nothing else to argue with.

The turn handed back to the model after a rejected batch: the typed rejections,
verbatim, and nothing else to argue with.
raw docstring

scanclj

(scan state delta)

Fold one streamed text delta into the scanner state, returning [state' ["(…)" …]] — every s-expression whose closing paren arrived in this delta.

Deliberately shape-blind: it counts parentheses and skips backslash escapes, so the same scanner reads sentences out of the JSON envelope (where each is a string value) and out of a bare-line answer. A quoted Clojure string holding an unbalanced parenthesis would confuse it — which costs a progress event and nothing more, since the batch is built by parsing the finished text.

Fold one streamed text delta into the scanner state, returning
`[state' ["(…)" …]]` — every s-expression whose closing paren arrived in this delta.

Deliberately shape-blind: it counts parentheses and skips backslash escapes, so the
same scanner reads sentences out of the JSON envelope (where each is a string value) and
out of a bare-line answer.  A quoted Clojure string holding an *unbalanced* parenthesis
would confuse it — which costs a progress event and nothing more, since the batch is
built by parsing the finished text.
raw docstring

scan-initclj

The initial state of the incremental s-expression scanner.

The initial state of the incremental s-expression scanner.
raw docstring

split-admissibleclj

(split-admissible kb entries)

Every candidate entry sorted into what a reviewer can apply and what they cannot: {:add […] :repairs [{:entry :problem :correction :index} …]}.

This is where a document path stops behaving like the other three. There, a rejected entry makes the whole batch :invalid and a reviewer is handed something they cannot apply; here a document routinely yields thirty claims of which two are malformed, and failing all thirty on account of two would make the pipeline useless. So the batch that comes back is always applicable, and each rejected candidate arrives explicitly as a repair with the checker's own verdict attached — plus :correction, the shape vaelii.impl.llm.correct would store instead, for the commonest failure of all: the right claim about a type's instances written as a fact about the type symbol.

Every candidate entry sorted into what a reviewer can apply and what they cannot:
`{:add […] :repairs [{:entry :problem :correction :index} …]}`.

This is where a document path stops behaving like the other three.  There, a rejected
entry makes the whole batch `:invalid` and a reviewer is handed something they cannot
apply; here a document routinely yields thirty claims of which two are malformed, and
failing all thirty on account of two would make the pipeline useless.  So the batch that
comes back is **always applicable**, and each rejected candidate arrives explicitly as a
repair with the checker's own verdict attached — plus `:correction`, the shape
`vaelii.impl.llm.correct` would store instead, for the commonest failure of all: the right
claim about a type's instances written as a fact about the type symbol.
raw docstring

unescapeclj

(unescape s)

A JSON string's escapes undone, in one pass. The incremental scanner reads s-expressions straight out of the raw response text, so a sentence carrying a Clojure string arrives with the JSON layer's backslashes still in it.

A JSON string's escapes undone, in one pass.  The incremental scanner reads
s-expressions straight out of the raw response text, so a sentence carrying a Clojure
string arrives with the JSON layer's backslashes still in it.
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