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.
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.(apply-proposal! kb proposal)(apply-proposal! kb {:keys [status batch] :as proposal} {:keys [force?]})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).
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).(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.(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.(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.(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.(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.(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.(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.(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.(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 "…"}`.(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.(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.(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.
(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.(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.(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`.(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.(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.
(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.
(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.
(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.
(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.
(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.
The initial state of the incremental s-expression scanner.
The initial state of the incremental s-expression scanner.
(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.(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.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |