Cancellation token - leaf module.
The cancellation token is a tiny two-atom record that lets a UI
thread (TUI, REPL caller) cooperatively abort an
in-flight turn! AND interrupt the worker future hosting the
blocking provider call. The cooperative side is checked at every
iteration boundary; the future side hard-cancels any HTTP call
that has already started.
Public API:
(cancellation-token) - fresh token
(cancellation-atom token) - cooperative flag atom (pass to turn!)
(cancellation-set-future! token fut) - register the worker future
(cancel! token) - set flag + interrupt registered future
(cancel! token reason) - same, stamping WHO cancelled
(cancel-reason token) - the recorded origin of the cancel
(cancelled? token) - true once cancel! has been called
(cancellation? throwable) - true if exception was caused by cancel!
(preserve-interrupt! t) - re-arm the interrupt flag a catch-all ate
This namespace has zero side effects at load time and depends only on Java interop - channels and the runtime can require it directly without pulling in the rest of the SDK.
Cancellation token - leaf module. The cancellation token is a tiny two-atom record that lets a UI thread (TUI, REPL caller) cooperatively abort an in-flight `turn!` AND interrupt the worker future hosting the blocking provider call. The cooperative side is checked at every iteration boundary; the future side hard-cancels any HTTP call that has already started. Public API: `(cancellation-token)` - fresh token `(cancellation-atom token)` - cooperative flag atom (pass to `turn!`) `(cancellation-set-future! token fut)` - register the worker future `(cancel! token)` - set flag + interrupt registered future `(cancel! token reason)` - same, stamping WHO cancelled `(cancel-reason token)` - the recorded origin of the cancel `(cancelled? token)` - true once `cancel!` has been called `(cancellation? throwable)` - true if exception was caused by `cancel!` `(preserve-interrupt! t)` - re-arm the interrupt flag a catch-all ate This namespace has zero side effects at load time and depends only on Java interop - channels and the runtime can require it directly without pulling in the rest of the SDK.
Explicit, durable session goals. Only the slash/SDK user path creates or replaces an objective. The model can mark that exact objective complete or blocked, but cannot resume, replace, cancel or increase its budget. Every mutation is a CAS on the persisted revision; the lifecycle version also rejects stale model work. Completion is a model declaration supported by evidence, not an independent judge.
Explicit, durable session goals. Only the slash/SDK user path creates or replaces an objective. The model can mark that exact objective complete or blocked, but cannot resume, replace, cancel or increase its budget. Every mutation is a CAS on the persisted revision; the lifecycle version also rejects stale model work. Completion is a model declaration supported by evidence, not an independent judge.
Canonical iteration-entry shape — the single source of truth shared by
the LIVE progress tracker (internal/progress) and the RESUME projection
(channel-tui/chat).
Background: the live-vs-resume split was the root cause of every TUI regression. The live tracker accumulated chunks into one map shape; the resume path rebuilt a different map shape from persisted rows. This ns pins ONE shape both paths populate.
form envelope — one block = one form record (the engine :forms
BLOB), carrying :code, :stdout, :error, and its
settled Activity snapshot. Printed stdout is the single
success surface; op cards / render-fns are gone.
{:position n ;; 0-based display position of the iteration :scope "tN/iM" ;; BLOCK-level scope, never /fK :thinking string-or-nil ;; reasoning text for this iteration :code "<block source>" :forms [<form> ...] ;; form envelopes (engine :forms BLOB) :status :ok|:error|:running|:cancelled|:timeout :duration-ms long :error error-map-or-nil}
Canonical iteration-entry shape — the single source of truth shared by
the LIVE progress tracker (`internal/progress`) and the RESUME projection
(`channel-tui/chat`).
Background: the live-vs-resume split was the root cause of every TUI
regression. The live tracker accumulated chunks into one map shape; the
resume path rebuilt a *different* map shape from persisted rows. This ns
pins ONE shape both paths populate.
## Vocabulary
form envelope — one block = one form record (the engine `:forms`
BLOB), carrying `:code`, `:stdout`, `:error`, and its
settled Activity snapshot. Printed stdout is the single
success surface; op cards / render-fns are gone.
## Canonical iteration-entry
{:position n ;; 0-based display position of the iteration
:scope "tN/iM" ;; BLOCK-level scope, never /fK
:thinking string-or-nil ;; reasoning text for this iteration
:code "<block source>"
:forms [<form> ...] ;; form envelopes (engine :forms BLOB)
:status :ok|:error|:running|:cancelled|:timeout
:duration-ms long
:error error-map-or-nil}Persistent, channel-NEUTRAL per-session model preference.
ONE source of truth — session_soul.llm_pref_provider + llm_pref_model
in the DB — for every channel (web gateway + TUI), so a session routes
through the same PROVIDER + MODEL wherever it's opened and the choice
survives restarts. Provider + model (not just a model name) mirrors how a
turn records its route and disambiguates a model name shared by >1 provider.
The engine reads it at turn start (prepare-turn-context in loop.clj) as
the default route when the caller passes none; router-for-model hoists the
chosen model (the provider follows, since it's the one carrying that model).
DEBOUNCED WRITE-BACK: set-model! updates an in-memory value IMMEDIATELY
(footer + engine see it at once) and coalesces the DB write, so cycling the
model (TUI Ctrl+T) many times in a row produces a SINGLE write. Reads prefer
the pending in-memory value, falling back to the DB.
Values are {:provider <id-string-or-nil> :model <name>} or nil. Keyed by
the session-soul id (the gateway's sid and the engine env's :session-id).
Persistent, channel-NEUTRAL per-session model preference.
ONE source of truth — `session_soul.llm_pref_provider` + `llm_pref_model`
in the DB — for every channel (web gateway + TUI), so a session routes
through the same PROVIDER + MODEL wherever it's opened and the choice
survives restarts. Provider + model (not just a model name) mirrors how a
turn records its route and disambiguates a model name shared by >1 provider.
The engine reads it at turn start (`prepare-turn-context` in loop.clj) as
the default route when the caller passes none; `router-for-model` hoists the
chosen model (the provider follows, since it's the one carrying that model).
DEBOUNCED WRITE-BACK: `set-model!` updates an in-memory value IMMEDIATELY
(footer + engine see it at once) and coalesces the DB write, so cycling the
model (TUI Ctrl+T) many times in a row produces a SINGLE write. Reads prefer
the pending in-memory value, falling back to the DB.
Values are `{:provider <id-string-or-nil> :model <name>}` or nil. Keyed by
the session-soul id (the gateway's `sid` and the engine env's `:session-id`).Streaming progress tracker - leaf module.
Channels (TUI, CLI agent) consume the iteration loop's
PHASED chunks via this tracker. Every chunk carries a :phase
keyword that tells the tracker what to do with it; the tracker
accumulates the chunks into a per-iteration timeline that the
channel re-renders incrementally.
Phases (every chunk has exactly one):
:reasoning LLM is streaming reasoning text. Updates the
iteration entry's :thinking field.
:tool-preview LLM is streaming the block's code. Carries the cumulative source separately from reasoning/content; the first real form replaces this ephemeral slot.
:form-start One block is about to evaluate. Carries
:position and :code. The tracker writes the
code immediately so channels can show the
currently-running block before the result lands.
:form-result One block finished evaluating. Carries
:position, :code, :stdout/:error,
and :envelope timestamps. The tracker writes
the completed form record into :forms at the
chunk's display index. Chunks tagged :silent?
keep their flag so channels can toggle visibility.
:form-activity One block's bounded Activity snapshot — what it has DONE
beside what it will return. Running and settled revisions are
full REPLACEMENTS into the form slot :position names, so a
dropped running frame costs a repaint and never a wrong picture.
:iteration-final Iteration is complete. Carries :final (nil
when the turn isn't done yet) and :done?
(true when this iteration produced the
turn-terminal answer). The block chunk has
already streamed; this is the trim
"iteration done" marker.
:iteration-error Iteration aborted before forms could run
(e.g. LLM call failed). Carries :thinking
and :error.
:provider-retry-reset
Provider stream failed before code eval and Vis is
retrying the provider call. Clears stale live
reasoning/content for this attempt and keeps a retry
recap in :provider-fallbacks.
Public API:
(make-progress-tracker) - fresh tracker, no callback
(make-progress-tracker {:on-update}) - invokes (on-update timeline chunk)
on every chunk
Returns {:on-chunk fn :get-timeline fn}. Pass the :on-chunk fn
under :hooks {:on-chunk ...} of sessions/send!. Each timeline
entry has the shape:
{:iteration N :thinking str-or-nil :forms [{:code str :comment str-or-nil :render-segments [{:kind ...} ...] ;; source classification :stdout str-or-nil ;; canonical printed output :result-kind :tool|:value|:error :result-detail map-or-nil ;; tool metadata :error map-or-nil :duration-ms int :success? bool :silent? bool :started-at-ms int-or-nil :activity map-or-nil ;; bounded tool-call trace} ...] :provider-fallbacks [map ...] ;; routed provider fallback notices :activity nil-or-keyword ;; live coarse phase (:provider-call/:response-parse) :elided-form-idxs #{int ...} ;; original loop indices hidden from :forms :error nil-or-iteration-error :final nil-or-{:answer :iteration-count :status} :done? bool}
The pre-existing :events interleaving log was removed: it lived
only in memory (never persisted), and resumed bubbles re-render
from this single flat layout. One layout path is enough.
Streaming progress tracker - leaf module.
Channels (TUI, CLI agent) consume the iteration loop's
PHASED chunks via this tracker. Every chunk carries a `:phase`
keyword that tells the tracker what to do with it; the tracker
accumulates the chunks into a per-iteration timeline that the
channel re-renders incrementally.
Phases (every chunk has exactly one):
:reasoning LLM is streaming reasoning text. Updates the
iteration entry's `:thinking` field.
:tool-preview LLM is streaming the block's code. Carries the
cumulative source separately from reasoning/content;
the first real form replaces this ephemeral slot.
:form-start One block is about to evaluate. Carries
`:position` and `:code`. The tracker writes the
code immediately so channels can show the
currently-running block before the result lands.
:form-result One block finished evaluating. Carries
`:position`, `:code`, `:stdout`/`:error`,
and `:envelope` timestamps. The tracker writes
the completed form record into `:forms` at the
chunk's display index. Chunks tagged `:silent?`
keep their flag so channels can toggle visibility.
:form-activity One block's bounded Activity snapshot — what it has DONE
beside what it will return. Running and settled revisions are
full REPLACEMENTS into the form slot `:position` names, so a
dropped running frame costs a repaint and never a wrong picture.
:iteration-final Iteration is complete. Carries `:final` (nil
when the turn isn't done yet) and `:done?`
(true when this iteration produced the
turn-terminal answer). The block chunk has
already streamed; this is the trim
"iteration done" marker.
:iteration-error Iteration aborted before forms could run
(e.g. LLM call failed). Carries `:thinking`
and `:error`.
:provider-retry-reset
Provider stream failed before code eval and Vis is
retrying the provider call. Clears stale live
reasoning/content for this attempt and keeps a retry
recap in `:provider-fallbacks`.
Public API:
`(make-progress-tracker)` - fresh tracker, no callback
`(make-progress-tracker {:on-update})` - invokes `(on-update timeline chunk)`
on every chunk
Returns `{:on-chunk fn :get-timeline fn}`. Pass the `:on-chunk` fn
under `:hooks {:on-chunk ...}` of `sessions/send!`. Each timeline
entry has the shape:
{:iteration N
:thinking str-or-nil
:forms [{:code str
:comment str-or-nil
:render-segments [{:kind ...} ...] ;; source classification
:stdout str-or-nil ;; canonical printed output
:result-kind :tool|:value|:error
:result-detail map-or-nil ;; tool metadata
:error map-or-nil
:duration-ms int
:success? bool
:silent? bool
:started-at-ms int-or-nil
:activity map-or-nil ;; bounded tool-call trace} ...]
:provider-fallbacks [map ...] ;; routed provider fallback notices
:activity nil-or-keyword ;; live coarse phase (:provider-call/:response-parse)
:elided-form-idxs #{int ...} ;; original loop indices hidden from :forms
:error nil-or-iteration-error
:final nil-or-{:answer :iteration-count :status}
:done? bool}
The pre-existing `:events` interleaving log was removed: it lived
only in memory (never persisted), and resumed bubbles re-render
from this single flat layout. One layout path is enough.Session-title subsystem, lifted out of the loop namespace: the three listener
registries (per-session value, global, and the pending/spinner channel), the
single set-title-with-broadcast! mutation point, and the async auto-title
side-channel (an off-surface ask! that names a session on its first
real turn). A LEAF — depends only on persistance + svar + runtime-settings,
never back on the loop.
Session-title subsystem, lifted out of the loop namespace: the three listener registries (per-session value, global, and the pending/spinner channel), the single `set-title-with-broadcast!` mutation point, and the async auto-title side-channel (an off-surface `ask!` that names a session on its first real turn). A LEAF — depends only on persistance + svar + runtime-settings, never back on the loop.
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 |