Pure helpers for the model-facing context snapshot.
The mutable session state lives outside this namespace; these functions only advance the turn/iteration cursor, project form envelopes for persisted result messages, and compute utilization metadata.
Pure helpers for the model-facing context snapshot. The mutable session state lives outside this namespace; these functions only advance the turn/iteration cursor, project form envelopes for persisted result messages, and compute utilization metadata.
(advance-iter ctx _form-results-vec)Advance the cursor so the next iter starts at "iter" (current+1),
"next_form" 1. Prior tool outputs are carried by append-only message
history, not by mutable context state. CTX is STRING-KEYED end to end —
it is bound into Python as the context dict and nippy-persisted, and
both surfaces are strings-only.
Advance the cursor so the next iter starts at "iter" (current+1), "next_form" 1. Prior tool outputs are carried by append-only message history, not by mutable context state. CTX is STRING-KEYED end to end — it is bound into Python as the `context` dict and nippy-persisted, and both surfaces are strings-only.
(block->envelope block position cursor)(block->envelope block _position cursor head-tag-resolver)Project one loop-side block {:code :result :error :stdout} plus its
1-based position and the engine cursor into the form envelope shape
(one block = one form, 1:1):
{:scope :tag :src :duration-ms :result :error :stdout}
:src carries the block's source text. :tag is derived from the source via
classify-form-tag. :result is included only when the block has
one (engine convention: drop on default/nil). :error is included
only when the block errored. :stdout (what the block PRINTED) is the
single display surface — channels paint it, and the model reads it back.
:duration-ms is derived from the loop's block envelope so persisted TUI
replays keep the same footer timing as live progress bubbles.
Every result is walked through realize-value so any IDeref (Var,
atom) is derefed and lazy seqs land as data, never as
#:vis{:ref :expr} placeholders left over from persistence flattening
unrealized seqs.
Project one loop-side block `{:code :result :error :stdout}` plus its
1-based position and the engine cursor into the form envelope shape
(one block = one form, 1:1):
{:scope :tag :src :duration-ms :result :error :stdout}
`:src` carries the block's source text. `:tag` is derived from the source via
`classify-form-tag`. `:result` is included only when the block has
one (engine convention: drop on default/nil). `:error` is included
only when the block errored. `:stdout` (what the block PRINTED) is the
single display surface — channels paint it, and the model reads it back.
`:duration-ms` is derived from the loop's block envelope so persisted TUI
replays keep the same footer timing as live progress bubbles.
Every result is walked through `realize-value` so any `IDeref` (Var,
atom) is derefed and lazy seqs land as data, never as
`#:vis{:ref :expr}` placeholders left over from persistence flattening
unrealized seqs.(blocks->forms blocks cursor)(blocks->forms blocks {:keys [turn iter]} head-tag-resolver)Map a loop-side blocks vec into a vec of engine envelopes. :cursor
is {:turn :iter} of THIS iter; each block gets a 1-based form
position by its index in the vec.
3-arity passes head-tag-resolver (see classify-form-tag) through
to every block->envelope call so extension-declared mutation tools
(patch, git/commit!, any symbol with inline :tag on its
vis/symbol entry) classify correctly without the engine
hard-coding their symbol set.
Map a loop-side blocks vec into a vec of engine envelopes. `:cursor`
is `{:turn :iter}` of THIS iter; each block gets a 1-based form
position by its index in the vec.
3-arity passes `head-tag-resolver` (see `classify-form-tag`) through
to every `block->envelope` call so extension-declared mutation tools
(`patch`, `git/commit!`, any symbol with inline `:tag` on its
`vis/symbol` entry) classify correctly without the engine
hard-coding their symbol set.(classify-form-tag src)(classify-form-tag src head-tag-resolver)Classify a form-source string as :observation or :mutation.
1-arity: pure, engine-only. Returns :mutation when the head is a
member of core-mutation-heads; everything else is :observation.
Use this from contexts that have no access to the extension
registry (tests, pure tools).
2-arity: takes head-tag-resolver, an optional fn
(fn [^Symbol head]) -> :mutation | :observation | nil. The
resolver wins when it returns a non-nil tag; on nil the engine
falls back to core-mutation-heads. The integration layer in
loop.clj builds the resolver from extension/op-tag so every
extension-declared tool (patch, git/commit!, anything new an
extension ships) classifies correctly without the engine hard-
coding its symbol.
Classify a form-source string as `:observation` or `:mutation`. 1-arity: pure, engine-only. Returns `:mutation` when the head is a member of `core-mutation-heads`; everything else is `:observation`. Use this from contexts that have no access to the extension registry (tests, pure tools). 2-arity: takes `head-tag-resolver`, an optional fn `(fn [^Symbol head]) -> :mutation | :observation | nil`. The resolver wins when it returns a non-nil tag; on nil the engine falls back to `core-mutation-heads`. The integration layer in `loop.clj` builds the resolver from `extension/op-tag` so every extension-declared tool (`patch`, `git/commit!`, anything new an extension ships) classifies correctly without the engine hard- coding its symbol.
(compact-src src)One-line, length-capped form source — the auto-summary listing AND
the renderer's src line on pre-turn <results> pins share it.
One-line, length-capped form source — the auto-summary listing AND the renderer's src line on pre-turn `<results>` pins share it.
Soft per-call request-size guardrail surfaced to the model as
session_utilization.auto_compress_above. ~72% of a typical 200k window,
leaving headroom for the model's own output. Compared against PROVIDER-reported
input tokens (no local tokenizer) — see utilization.
Soft per-call request-size guardrail surfaced to the model as `session_utilization.auto_compress_above`. ~72% of a typical 200k window, leaving headroom for the model's own output. Compared against PROVIDER-reported input tokens (no local tokenizer) — see `utilization`.
(empty-ctx)(empty-ctx session-id)A minimal CTX scaffold with all model-facing keys filled by empty / default values. Useful as the starting point for scenario replays.
Includes the engine-ephemeral key "engine_warnings" so the rest of
the system can swap! it without nil-puncturing. Stripped at
persistence boundaries via strip-ephemeral.
CTX is STRING-KEYED end to end: it is bound into Python as the context
dict (strings-only boundary) and nippy-persisted (strings-only DB), so
there is no keyword->string projection anywhere between.
A minimal CTX scaffold with all model-facing keys filled by empty / default values. Useful as the starting point for scenario replays. Includes the engine-ephemeral key `"engine_warnings"` so the rest of the system can swap! it without nil-puncturing. Stripped at persistence boundaries via `strip-ephemeral`. CTX is STRING-KEYED end to end: it is bound into Python as the `context` dict (strings-only boundary) and nippy-persisted (strings-only DB), so there is no keyword->string projection anywhere between.
(enter-turn ctx turn-pos)Idempotent turn-start sync. Sets :session/turn to turn-pos,
resets :session/scope to {:turn turn-pos :iter 1 :next-form 1},
clears :engine/blockers, then runs gc-pass.
Safe to call repeatedly with the same turn-pos (no-op
semantically); safe to call when ctx was loaded fresh from DB at
turn-pos > 1.
THIS is what the integration layer (vis loop) calls at the start
of every turn. Single chokepoint for engine turn-state advance —
no advance-turn alias, no auto-incrementing variant. The integration
layer always knows the target turn-pos (DB tracks
session_turn_soul.position), so the engine takes it as an explicit
arg.
Idempotent turn-start sync. Sets `:session/turn` to `turn-pos`,
resets `:session/scope` to `{:turn turn-pos :iter 1 :next-form 1}`,
clears `:engine/blockers`, then runs `gc-pass`.
Safe to call repeatedly with the same `turn-pos` (no-op
semantically); safe to call when ctx was loaded fresh from DB at
turn-pos > 1.
THIS is what the integration layer (vis loop) calls at the start
of every turn. Single chokepoint for engine turn-state advance —
no `advance-turn` alias, no auto-incrementing variant. The integration
layer always knows the target turn-pos (DB tracks
`session_turn_soul.position`), so the engine takes it as an explicit
arg.(finalize-turn ctx _form-scope _args)Finalize a turn: the loop ships :answer to the channel and the engine
returns ctx unchanged so the turn can settle.
Finalize a turn: the loop ships `:answer` to the channel and the engine returns ctx unchanged so the turn can settle.
(form-head-name src)Return the head call NAME (a string) of src — a Python source string —
or nil when src is not a name(...) call form. Leading comments and
blank lines are skipped. Reading the head name (rather than scanning the
raw source) avoids false positives — a "patch(x)" inside a string can't
match. Used by classify-form-tag.
Return the head call NAME (a string) of `src` — a Python source string — or nil when `src` is not a `name(...)` call form. Leading comments and blank lines are skipped. Reading the head name (rather than scanning the raw source) avoids false positives — a `"patch(x)"` inside a string can't match. Used by `classify-form-tag`.
(gc-pass ctx)Passthrough. Tasks/facts/archive are gone — there is nothing to GC.
Kept so the turn-lifecycle chokepoint (enter-turn) and any external
callers stay valid.
Passthrough. Tasks/facts/archive are gone — there is nothing to GC. Kept so the turn-lifecycle chokepoint (`enter-turn`) and any external callers stay valid.
(malformed-scope? s)True if s is a string but does not parse as ::cs/scope-form.
True if `s` is a string but does not parse as `::cs/scope-form`.
EXACT set of session_* keys the model is meant to see. This is the
SINGLE definition of 'model-facing'; session-view selects on it so engine
bookkeeping cannot leak into the rendered <context> or bound context
dict. "session_utilization" is derived (from "engine_utilization"),
so it is folded in by session-view rather than listed here.
EXACT set of `session_*` keys the model is meant to see. This is the SINGLE definition of 'model-facing'; `session-view` selects on it so engine bookkeeping cannot leak into the rendered `<context>` or bound `context` dict. `"session_utilization"` is derived (from `"engine_utilization"`), so it is folded in by `session-view` rather than listed here.
(model-form-envelope r)Project one executed-form envelope onto the MODEL contract the
trailer stores: :scope :src :result :error (+ engine forensic
fields), WITHOUT the channel sink slice, the host failure/metadata
chains, or :tag. The mutation/observation tag stays load-bearing
on LIVE op envelopes and on the persisted rows — but NOTHING reads
it from the trailer (the observation-prune that once did was
removed), and the model can see what a form does from :src. The
full envelopes stay on the progress chunks and the persisted
session_turn_iteration.forms rows — channels and the persisted
forms rows keep total fidelity; the trailer is what rides every
prompt.
Empty payloads are DROPPED, not rendered: "result": None /
[] / {} and empty :error maps say nothing the form's absence
of an error/result doesn't already say — the :src stays, the dead
field goes.
Project one executed-form envelope onto the MODEL contract the
trailer stores: `:scope :src :result :error` (+ engine forensic
fields), WITHOUT the channel sink slice, the host failure/metadata
chains, or `:tag`. The mutation/observation tag stays load-bearing
on LIVE op envelopes and on the persisted rows — but NOTHING reads
it from the trailer (the observation-prune that once did was
removed), and the model can see what a form does from `:src`. The
full envelopes stay on the progress chunks and the persisted
`session_turn_iteration.forms` rows — channels and the persisted
forms rows keep total fidelity; the trailer is what rides every
prompt.
Empty payloads are DROPPED, not rendered: `"result": None` /
`[]` / `{}` and empty `:error` maps say nothing the form's absence
of an error/result doesn't already say — the `:src` stays, the dead
field goes.(parse-scope-form s)Parse a tN/iM (or legacy tN/iM/fK) scope into {:turn :iter :form} or nil
if malformed. :form is nil for an iteration-level scope. Pure value-or-nil.
Parse a `tN/iM` (or legacy `tN/iM/fK`) scope into `{:turn :iter :form}` or nil
if malformed. `:form` is nil for an iteration-level scope. Pure value-or-nil.(scope-compare a b)Total order on form-scope strings by (turn, iter, form). Returns int. Compares parsed segments; malformed scopes sort before all valid ones to make their presence obvious in render.
Total order on form-scope strings by (turn, iter, form). Returns int. Compares parsed segments; malformed scopes sort before all valid ones to make their presence obvious in render.
(session-view ctx)(session-view ctx _warnings)THE single projection from engine-internal ctx to the model-facing
session_* view.
Both consumers derive from this, so the rendered <context> block and the
Python context dict are the same map by construction:
ctx-renderer/render-ctx serializes this viewctx-loop/session-snapshot binds this view as read-only contextKeeps ONLY model-facing-keys (so engine bookkeeping never leaks) and
projects "engine_utilization" → "session_utilization". The second
arity takes legacy warnings for call-site compatibility but ignores them.
Pure; STRING keys in and out.
THE single projection from engine-internal ctx to the model-facing `session_*` view. Both consumers derive from this, so the rendered `<context>` block and the Python `context` dict are the same map by construction: - `ctx-renderer/render-ctx` serializes this view - `ctx-loop/session-snapshot` binds this view as read-only `context` Keeps ONLY `model-facing-keys` (so engine bookkeeping never leaks) and projects `"engine_utilization"` → `"session_utilization"`. The second arity takes legacy `warnings` for call-site compatibility but ignores them. Pure; STRING keys in and out.
(strip-ephemeral ctx)Remove every "engine_*" key from a ctx. Call before Nippy-snapshotting
to persistence so transient mutator state (warnings, pending satisfy
requests) does not leak into the durable record.
Remove every `"engine_*"` key from a ctx. Call before Nippy-snapshotting to persistence so transient mutator state (warnings, pending satisfy requests) does not leak into the durable record.
(utilization request-tokens window-tokens turn-tokens fold-cap)Pure: the "session_utilization" map the model reads to see how much
of the context window the LAST request consumed. Keys are spelled out
so they can't be misread:
last_request_tokens input size of the most recent model call
model_input_limit HARD per-call ceiling (provider rejects above)
saturation last-request / model-input-limit, as a rounded
percentage — how FULL the per-call window is
headroom_tokens tokens still free before the ceiling
(model_input_limit - last request); the
actionable 'can I keep going or must I fold?'
auto_compress_above soft guardrail threshold for request size
turn_total_tokens cumulative input this turn (billing, NOT a
per-call limit — may exceed the limit safely)
Returns nil until a request has actually been measured (req <= 0), so
the first iter of a turn shows nothing rather than a bogus 0%.
Pure: the `"session_utilization"` map the model reads to see how much
of the context window the LAST request consumed. Keys are spelled out
so they can't be misread:
last_request_tokens input size of the most recent model call
model_input_limit HARD per-call ceiling (provider rejects above)
saturation last-request / model-input-limit, as a rounded
percentage — how FULL the per-call window is
headroom_tokens tokens still free before the ceiling
(model_input_limit - last request); the
actionable 'can I keep going or must I fold?'
auto_compress_above soft guardrail threshold for request size
turn_total_tokens cumulative input this turn (billing, NOT a
per-call limit — may exceed the limit safely)
Returns nil until a request has actually been measured (req <= 0), so
the first iter of a turn shows nothing rather than a bogus 0%.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 |