vis - broad host facade.
This is the ONLY namespace extensions, channel adapters, embedded
callers, and tests should import. It deliberately re-exports host,
registry, runtime, persistence, prompt, diagnostic, and sandbox
helpers from com.blockether.vis.internal.*. The internal tree is
not stable; the names exposed here are the host contract.
Canonical runtime language: Session -> Turn -> Iteration -> Block.
A Turn is one user request plus assistant answer inside a Session. New code and documentation should use turn/user-request language.
Primary surfaces:
persistance; human-facing language is Persistence.Not every export is equally high-level. send!, extension maps,
registry builders, read-side persistence helpers, and Markdown export
are the preferred integration surface. Low-level sandbox, parse-repair,
dispatcher, and write-side db helpers are exported because this is a
host facade, but ordinary extensions should avoid depending on them
unless they are implementing host-level behavior.
Binary entry: -main (invoked by clojure -M:vis, AOT'd to a Java
entry class via :gen-class for the GraalVM native-image build).
vis - broad host facade.
This is the ONLY namespace extensions, channel adapters, embedded
callers, and tests should import. It deliberately re-exports host,
registry, runtime, persistence, prompt, diagnostic, and sandbox
helpers from `com.blockether.vis.internal.*`. The internal tree is
not stable; the names exposed here are the host contract.
Canonical runtime language:
Session -> Turn -> Iteration -> Block.
A Turn is one user request plus assistant answer inside a Session.
New code and documentation should use turn/user-request language.
Primary surfaces:
- Session / turn runtime: create!, send!, turn!, by-id,
by-channel, env-for, close!, delete!, set-title!.
- Environment runtime: create-environment, dispose-environment!,
get-router, rebuild-router!, resolve-effective-model.
- Extension contract: extension, symbol, value, render-prompt,
register-extension!, registered-extensions, discovery, reload.
- Registries: command, channel, provider, and backend registration
helpers for host-owned and embedded use.
- Persistence facade: db-* functions and connection helpers. The
implementation namespace / extension slot are spelled
`persistance`; human-facing language is Persistence.
- Prompt / Python-sandbox / formatting / cancellation /
notifications / doctor helpers shared by channels and extensions.
Not every export is equally high-level. `send!`, extension maps,
registry builders, read-side persistence helpers, and Markdown export
are the preferred integration surface. Low-level sandbox, parse-repair,
dispatcher, and write-side db helpers are exported because this is a
host facade, but ordinary extensions should avoid depending on them
unless they are implementing host-level behavior.
Binary entry: -main (invoked by `clojure -M:vis`, AOT'd to a Java
entry class via `:gen-class` for the GraalVM native-image build).(->ast v)Soft-normalize any answer-input value into canonical [:ast & blocks]. Pure, total, idempotent.
Identity-preserving: when the input already satisfies the canonical
invariants (canonical?), the return value is the SAME object.
This keeps downstream System/identityHashCode caches
(format-answer-with-thinking-data, etc.) hot across repeated
render passes — walker output is computed once per canonical IR
identity, not once per equal-but-fresh allocation.
Before canonicalization, Hiccup child positions are walked with
clojure+.walk semantics and non-vector sequential values (notably
lazy seqs from (map ...) inside renderer input) are safely realized to
at most 100 items, then replaced with an explicit … many more
marker when truncated. This avoids persisting Java LazySeq identity
strings and avoids hanging on infinite seqs.
See namespace docstring for the full canonical-form invariants.
Soft-normalize any answer-input value into canonical [:ast & blocks]. Pure, total, idempotent. Identity-preserving: when the input already satisfies the canonical invariants (`canonical?`), the return value is the SAME object. This keeps downstream `System/identityHashCode` caches (`format-answer-with-thinking-data`, etc.) hot across repeated render passes — walker output is computed once per canonical IR identity, not once per equal-but-fresh allocation. Before canonicalization, Hiccup child positions are walked with `clojure+.walk` semantics and non-vector sequential values (notably lazy seqs from `(map ...)` inside renderer input) are safely realized to at most 100 items, then replaced with an explicit `… many more` marker when truncated. This avoids persisting Java LazySeq identity strings and avoids hanging on infinite seqs. See namespace docstring for the full canonical-form invariants.
(->json m)Serialize a value to a JSON TEXT column. Nil in, nil out.
Serialize a value to a JSON TEXT column. Nil in, nil out.
(->kw v)Keyword/string -> TEXT, stripping the leading colon. Nil -> nil.
Keyword/string -> TEXT, stripping the leading colon. Nil -> nil.
(->ref v)Normalize an entity reference to a string ID for SQL. Accepts: UUID, string, or nil. Returns string or nil.
The ONLY way to extract a SQL-ready string from an entity reference -- pass the plain UUID or string directly.
Normalize an entity reference to a string ID for SQL. Accepts: UUID, string, or nil. Returns string or nil. The ONLY way to extract a SQL-ready string from an entity reference -- pass the plain UUID or string directly.
(->svar-model model)(->svar-model _provider-id model)Coerce a model representation to svar-native {:name str}.
Coerce a model representation to svar-native `{:name str}`.
(->svar-provider provider)Coerce a provider map to svar-native shape (:id, :api-key,
:base-url, :api-style, :models, optional :responses-path,
optional :llm-headers, and Vis-owned :network request defaults).
Resolve :base-url from configuration or the provider template
(registered preset, then svar's catalog). A credential callback's
:api-url replaces that URL only when no endpoint is configured or it
matches the preset/catalog default; a custom configured endpoint wins.
Forward the resolved URL because svar cannot recover extension-only presets.
When :api-key is nil, look the provider up in the global
provider registry (registry.clj) and call its
:provider/get-token-fn to resolve a usable token. Each provider
implementation handles its own auth lifecycle (OAuth refresh,
env-var fallback, provider-specific headers, ...) so this fn stays
provider-agnostic and never references a concrete provider ns by
name.
Coerce a provider map to svar-native shape (`:id`, `:api-key`, `:base-url`, `:api-style`, `:models`, optional `:responses-path`, optional `:llm-headers`, and Vis-owned `:network` request defaults). Resolve `:base-url` from configuration or the provider template (registered preset, then svar's catalog). A credential callback's `:api-url` replaces that URL only when no endpoint is configured or it matches the preset/catalog default; a custom configured endpoint wins. Forward the resolved URL because svar cannot recover extension-only presets. When `:api-key` is nil, look the provider up in the global provider registry (registry.clj) and call its `:provider/get-token-fn` to resolve a usable token. Each provider implementation handles its own auth lifecycle (OAuth refresh, env-var fallback, provider-specific headers, ...) so this fn stays provider-agnostic and never references a concrete provider ns by name.
(-main & args)The binary entry; Python workers have their own entrypoint in the runtime.
The binary entry; Python workers have their own entrypoint in the runtime.
(<-json s)Parse a JSON TEXT column. STRINGS-ONLY: keys come back as VERBATIM STRINGS -
no :key-fn keyword re-keywordizing. Whatever needs an internal keyword
shape converts at ONE named adapter, never here.
Parse a JSON TEXT column. STRINGS-ONLY: keys come back as VERBATIM STRINGS - no `:key-fn keyword` re-keywordizing. Whatever needs an internal keyword shape converts at ONE named adapter, never here.
(abbreviate-home path)(abbreviate-home path home)Shorten an absolute path for DISPLAY by replacing the user's home dir with
~, matching the footer/navigator/dialogs. Only rewrites when path is at
or under home (so /etc/x and relative paths stay unchanged). Rendered
descendants always use / separators; nil-safe.
Shorten an absolute path for DISPLAY by replacing the user's home dir with `~`, matching the footer/navigator/dialogs. Only rewrites when `path` is at or under home (so `/etc/x` and relative paths stay unchanged). Rendered descendants always use `/` separators; nil-safe.
(active-extensions environment)Returns the seq of registered extensions whose :ext/activation-fn returns
truthy for environment, in registration order. Single source of truth for
activation; call ONCE at the top of a turn.
Returns the seq of registered extensions whose `:ext/activation-fn` returns truthy for `environment`, in registration order. Single source of truth for activation; call ONCE at the top of a turn.
(active-model)Return the primary model name string, or nil.
Return the primary model name string, or nil.
(active-provider)Return the first (primary) provider from config, or nil.
Return the first (primary) provider from config, or nil.
(active-slashes env)Aggregate :ext/slash-commands from every active extension for env.
Order: extension registration order (stable). Returns a vec.
This is the ONLY source of truth for engine slash dispatch (which
already has a turn env handy). Channels that lack a per-turn env
should use registered-slashes (no activation filtering).
Aggregate `:ext/slash-commands` from every active extension for `env`. Order: extension registration order (stable). Returns a vec. This is the ONLY source of truth for engine slash dispatch (which already has a turn env handy). Channels that lack a per-turn env should use `registered-slashes` (no activation filtering).
(add-config-provider! provider-cfg)(add-config-provider! provider-cfg source)Append a provider config to the persisted fleet (no-op when its id exists).
Append a provider config to the persisted fleet (no-op when its id exists).
(add-python-extension-change-listener! listener-id f)Subscribe f to Python-extension set changes. f receives
{:extensions [<validated ext map> ...] :removed [<ext-name> ...]}
after every (re)load that changed anything: :extensions is the full
freshly-registered set, :removed the names that no longer exist.
Re-registering the same listener-id replaces the old listener.
Returns listener-id.
Subscribe `f` to Python-extension set changes. `f` receives
`{:extensions [<validated ext map> ...] :removed [<ext-name> ...]}`
after every (re)load that changed anything: `:extensions` is the full
freshly-registered set, `:removed` the names that no longer exist.
Re-registering the same `listener-id` replaces the old listener.
Returns `listener-id`.(add-title-listener! session-id listener-fn)Register listener-fn for session-id. The fn is invoked with
the new title (a string) every time the title changes. Multiple
listeners are supported; they fire in unspecified order.
Returns the listener fn so callers can pass it to
remove-title-listener! later.
Register `listener-fn` for `session-id`. The fn is invoked with the new title (a string) every time the title changes. Multiple listeners are supported; they fire in unspecified order. Returns the listener fn so callers can pass it to `remove-title-listener!` later.
(add-title-pending-listener! session-id listener-fn)Register listener-fn for session-id; invoked with a boolean
(true when title generation starts, false when it ends). Returns the
listener fn for later remove-title-pending-listener!.
Register `listener-fn` for `session-id`; invoked with a boolean (true when title generation starts, false when it ends). Returns the listener fn for later `remove-title-pending-listener!`.
(all-provider-limits)Return normalized limits reports for every registered provider in registration order.
Return normalized limits reports for every registered provider in registration order.
(ask-code! opts)One-shot routed svar/ask-code! against the global router.
Plain-text completion + Markdown-code-block extraction — returns the
svar map {:blocks :raw :reasoning :tokens :cost :duration-ms :assistant-message :provider-state}. :blocks is a vec of
{:lang :source} (one entry per Markdown code block); concatenate
yourself with svar.internal.codes/concat-sources if you need a
single string. ask! (JSON-spec) is gone; every Vis caller uses
ask-code!.
One-shot routed `svar/ask-code!` against the global router.
Plain-text completion + Markdown-code-block extraction — returns the
svar map `{:blocks :raw :reasoning :tokens :cost :duration-ms
:assistant-message :provider-state}`. `:blocks` is a vec of
`{:lang :source}` (one entry per Markdown code block); concatenate
yourself with `svar.internal.codes/concat-sources` if you need a
single string. `ask!` (JSON-spec) is gone; every Vis caller uses
`ask-code!`.(assemble-initial-messages
{:keys [stable-prompt-messages initial-user-content previous-turn-context
turn-context user-images skipped-images vision? image-descriptions]
:or {vision? true}})Initial provider messages for one turn.
Prior RESUME entries are emitted as one stable user message per turn (or
materialized fold checkpoint), so adding a turn appends a message instead of
rewriting one monolithic conversation recap. :turn-context is the current
append-only turn/utilization assignment block and rides immediately before
the current user request.
:image-descriptions carries the vision fallback's {label {:text … :model …}}
for images this turn's target cannot see. Pure input: deciding whether that
report is worth paying for belongs to the caller, never to message assembly.
Initial provider messages for one turn.
Prior RESUME entries are emitted as one stable user message per turn (or
materialized fold checkpoint), so adding a turn appends a message instead of
rewriting one monolithic conversation recap. `:turn-context` is the current
append-only turn/utilization assignment block and rides immediately before
the current user request.
`:image-descriptions` carries the vision fallback's `{label {:text … :model …}}`
for images this turn's target cannot see. Pure input: deciding whether that
report is worth paying for belongs to the caller, never to message assembly.(assemble-stable-prompt-messages
environment
{:keys [system-prompt active-extensions session-context] :as opts})Assemble provider-prefix messages.
Send order is explicit and tested:
SYSTEM-PROMPT - CORE_SYSTEM_PROMPT + caller addendum
PROJECT-INSTRUCTIONS - AGENTS.md / CLAUDE.md contents (when present)
TURN-SYSTEM-CONTEXT - turn-scoped runtime capability context. Today
it contains extension prompt fragments; future
message, never append a second extension
context.
Extension fragments are separate from the core system prompt and are not repeated in per-iteration trailers.
Required opts:
:active-extensions - vec from (active-extensions env). Drives
environment, extension prompt, and hint collection.
Optional opts:
:system-prompt - caller addendum appended to CORE.
:session-context - rendered fenced-Python session = {…} block
(standing session state: workspace / env / routing / tools). Embedded
ONCE here as a cached system message; the loop re-emits only the
session[...] = … structural delta in the conversation when it changes
mid-turn.
Assemble provider-prefix messages.
Send order is explicit and tested:
`SYSTEM-PROMPT` - CORE_SYSTEM_PROMPT + caller addendum
`PROJECT-INSTRUCTIONS` - AGENTS.md / CLAUDE.md contents (when present)
`TURN-SYSTEM-CONTEXT` - turn-scoped runtime capability context. Today
it contains extension prompt fragments; future
message, never append a second extension
context.
Extension fragments are separate from the core system prompt and are not
repeated in per-iteration trailers.
Required opts:
`:active-extensions` - vec from `(active-extensions env)`. Drives
environment, extension prompt, and hint collection.
Optional opts:
`:system-prompt` - caller addendum appended to CORE.
`:session-context` - rendered fenced-Python `session = {…}` block
(standing session state: workspace / env / routing / tools). Embedded
ONCE here as a cached system message; the loop re-emits only the
`session[...] = …` structural delta in the conversation when it changes
mid-turn.(audio-transcribe-outcome attachment)What is known about this recording's words RIGHT NOW, without starting anything.
{:transcription "…"} once they exist, {:status "pending"} while the worker
has it, a settled {:status …} when it could not be made, and nil when nobody has
asked yet — which is what a composer paints as a placeholder and re-reads on the
next frame.
What is known about this recording's words RIGHT NOW, without starting anything.
`{:transcription "…"}` once they exist, `{:status "pending"}` while the worker
has it, a settled `{:status …}` when it could not be made, and nil when nobody has
asked yet — which is what a composer paints as a placeholder and re-reads on the
next frame.(audio-transcribe-request! attachments)Start the words for every recording in attachments and answer the rows with
whatever is known NOW — normally pending.
NOTHING waits. This is the call a surface makes the moment files are staged, and the one the turn makes when attachments first land, so the speech is already being made while the human is still typing and while the rest of the turn is assembled.
Start the words for every recording in `attachments` and answer the rows with whatever is known NOW — normally `pending`. NOTHING waits. This is the call a surface makes the moment files are staged, and the one the turn makes when attachments first land, so the speech is already being made while the human is still typing and while the rest of the turn is assembled.
The CLOSED vocabulary of what a surface may be told about a recording whose words
it does not have. A row that carries :transcription carries no status at all.
The CLOSED vocabulary of what a surface may be told about a recording whose words it does not have. A row that carries `:transcription` carries no status at all.
(authenticated-preset-providers)Registered providers that BIND THEMSELVES — the credential lives OUTSIDE the
persisted fleet, so the provider is usable with no Add provider step at all.
Shaped as minimal picker rows ({:id … :models …} carrying the preset's
default catalog models) and appended by picker-fleet.
Two ways in. A MANAGED provider ([[managed?]]) binds because its runtime
issues the credential: there is nothing local to probe and nothing a human
could add. Every other provider binds only when its OWN :provider/detect-fn
(local, no network) finds one — an OAuth token file, a keychain entry.
A provider with neither, or with no default models, is skipped.
Registered providers that BIND THEMSELVES — the credential lives OUTSIDE the
persisted fleet, so the provider is usable with no `Add provider` step at all.
Shaped as minimal picker rows (`{:id … :models …}` carrying the preset's
default catalog models) and appended by [[picker-fleet]].
Two ways in. A MANAGED provider ([[managed?]]) binds because its runtime
issues the credential: there is nothing local to probe and nothing a human
could add. Every other provider binds only when its OWN `:provider/detect-fn`
(local, no network) finds one — an OAuth token file, a keychain entry.
A provider with neither, or with no default models, is skipped.(available-theme-ids)(available-theme-ids extensions)Theme ids from the process registry plus optional unregistered extension
descriptor maps. Normally extensions are already installed into themes
by register-extension!; the argument remains for pure tests/previews.
vis-light/vis-dark are pinned to the top (see theme-id-priority);
all other ids follow, sorted alphabetically.
Theme ids from the process registry plus optional unregistered extension descriptor maps. Normally extensions are already installed into `themes` by `register-extension!`; the argument remains for pure tests/previews. `vis-light`/`vis-dark` are pinned to the top (see `theme-id-priority`); all other ids follow, sorted alphabetically.
(beautify-python code)ruff-format code (cached). nil/blank -> "". Never throws — falls back to
the verbatim source when ruff can't format or isn't available.
ruff-format `code` (cached). nil/blank -> "". Never throws — falls back to the verbatim source when ruff can't format or isn't available.
(bind-and-bump! env sym val)Set sym -> val in the env's sandbox.
Set `sym` -> `val` in the env's sandbox.
(build-system-prompt {:keys [system-prompt workspace-root]})Core system prompt + optional caller addendum + config prompt + SYSTEM.md / APPEND_SYSTEM.md file overrides.
Assembled in send order (later blocks positionally reinforce earlier):
base, then the caller's :system-prompt addendum, then the
:system-prompt pulled from Vis config (~/.vis/config.yml / state.yml /
<project>/vis.yml / .vis/config.yml, deep-merged), then ~/.vis/APPEND_SYSTEM.md, then
<workspace>/.vis/APPEND_SYSTEM.md. The config + file hooks let a project
append house rules without any caller having to pass them.
Full rewrite precedence for the base: <workspace>/.vis/SYSTEM.md >
~/.vis/SYSTEM.md > config :system-prompt map with :replace? true >
CORE_SYSTEM_PROMPT. When a file/config replaces the base, addenda and
append files are still appended after it. workspace-root scopes all project
config and file lookups; an omitted root keeps the caller's workspace binding.
Core system prompt + optional caller addendum + config prompt + SYSTEM.md / APPEND_SYSTEM.md file overrides. Assembled in send order (later blocks positionally reinforce earlier): base, then the caller's `:system-prompt` addendum, then the `:system-prompt` pulled from Vis config (`~/.vis/config.yml` / `state.yml` / `<project>/vis.yml` / `.vis/config.yml`, deep-merged), then `~/.vis/APPEND_SYSTEM.md`, then `<workspace>/.vis/APPEND_SYSTEM.md`. The config + file hooks let a project append house rules without any caller having to pass them. Full rewrite precedence for the base: `<workspace>/.vis/SYSTEM.md` > `~/.vis/SYSTEM.md` > config `:system-prompt` map with `:replace? true` > `CORE_SYSTEM_PROMPT`. When a file/config replaces the base, addenda and append files are still appended after it. `workspace-root` scopes all project config and file lookups; an omitted root keeps the caller's workspace binding.
(by-cmd cmd)Lookup the channel whose :channel/cmd equals cmd. Returns nil
when no channel claims that command.
Lookup the channel whose :channel/cmd equals `cmd`. Returns nil when no channel claims that command.
(by-id id)Return the session record (UUID :id) or nil.
Return the session record (UUID `:id`) or nil.
(cache-env! session-id env)Insert env into the cache under session-id (UUID, or string
normalized via cache-key). Returns {:id <UUID> :environment env}.
Insert `env` into the cache under `session-id` (UUID, or string
normalized via `cache-key`). Returns `{:id <UUID> :environment env}`.(call-env-values env)Resolve ONE call's env delta into {NAME value-or-nil}, where nil means
UNSET that name for this child. A DELTA: it is merged over the project
environment (config/child-environment-values), never a replacement for it,
so a workspace .env still reaches a child whose call names one variable.
A value is either a LITERAL (string/number/boolean) or a SOURCE map — the
same {env|dotenv|keychain|command|literal} shape environment: declares. That
split is not style: this map is an ARGUMENT, so a literal is written into the
session journal and the transcript for good. Literals are for SWITCHES
(NODE_ENV, RUST_LOG, PYTHONHASHSEED); a secret names its source and
only the child ever sees the value.
Every refusal is LOUD and names the key: a name that is not a variable name,
a [[pre-exec-hijack?]] name (which the jail would drop anyway, silently), a
map naming no source, and a source that produced nothing — an explicit
request for ONE variable that resolved to nothing is an error here, not the
quiet :unset a standing declaration is allowed.
Resolve ONE call's `env` delta into `{NAME value-or-nil}`, where nil means
UNSET that name for this child. A DELTA: it is merged over the project
environment (`config/child-environment-values`), never a replacement for it,
so a workspace `.env` still reaches a child whose call names one variable.
A value is either a LITERAL (string/number/boolean) or a SOURCE map — the
same `{env|dotenv|keychain|command|literal}` shape `environment:` declares. That
split is not style: this map is an ARGUMENT, so a literal is written into the
session journal and the transcript for good. Literals are for SWITCHES
(`NODE_ENV`, `RUST_LOG`, `PYTHONHASHSEED`); a secret names its source and
only the child ever sees the value.
Every refusal is LOUD and names the key: a name that is not a variable name,
a [[pre-exec-hijack?]] name (which the jail would drop anyway, silently), a
map naming no source, and a source that produced nothing — an explicit
request for ONE variable that resolved to nothing is an error here, not the
quiet `:unset` a standing declaration is allowed.(cancel! token)(cancel! token reason)Abort the in-flight turn. Flips the cooperative flag and runs every registered
on-cancel! callback exactly once. Each callback is wrapped in its own
try/catch so one bad consumer cannot starve the rest.
The callback list is atomically drained before callbacks run. Repeated stops from a user, watchdog, or shutdown therefore preserve the first reason without re-entering a turn's terminal/unwind path.
reason names the ORIGIN (:client-cancel-turn, :stall-watchdog,
:gateway-shutdown, …). The first one wins and is read back with
[[cancel-reason]]. The one-arity call records :unspecified.
Abort the in-flight turn. Flips the cooperative flag and runs every registered `on-cancel!` callback exactly once. Each callback is wrapped in its own `try`/`catch` so one bad consumer cannot starve the rest. The callback list is atomically drained before callbacks run. Repeated stops from a user, watchdog, or shutdown therefore preserve the first reason without re-entering a turn's terminal/unwind path. `reason` names the ORIGIN (`:client-cancel-turn`, `:stall-watchdog`, `:gateway-shutdown`, …). The first one wins and is read back with [[cancel-reason]]. The one-arity call records `:unspecified`.
(cancellation-atom token)Cooperative flag atom — read with @ at iteration boundaries when
the consumer can return without external help.
Cooperative flag atom — read with `@` at iteration boundaries when the consumer can return without external help.
(cancellation-set-future! token fut)Register a worker Future so cancel! interrupts it. Thin
convenience over on-cancel!: wraps .cancel(true) in a thunk
and discards the returned dispose! (the future's own completion
makes the second cancel a no-op).
Returns the future for convenient threading.
Register a worker `Future` so `cancel!` interrupts it. Thin convenience over `on-cancel!`: wraps `.cancel(true)` in a thunk and discards the returned `dispose!` (the future's own completion makes the second cancel a no-op). Returns the future for convenient threading.
(cancellation-token)Construct a fresh cancellation token.
The token bundles three things every cancellable boundary needs:
::flag — cooperative boolean atom, polled at iteration
boundaries by callers that can return
gracefully.::callbacks — vec of [id thunk] pairs run by cancel! so
any number of in-flight workers (provider
HTTP call, Python eval future, voice recorder)
can register their own hard-cancel hook.::reason — WHO fired the cancel, stamped by cancel! and read
back with cancel-reason. Downstream every cancel
looks identical (a thread interrupt), so without
this stamp a self-inflicted cancel (stall watchdog,
shutdown) is indistinguishable from a user Esc.cancellation-set-future! (legacy single-future API) is kept for
call sites that have not migrated yet; it now routes through the
callback registry too so behaviour stays identical.
Construct a fresh cancellation token.
The token bundles three things every cancellable boundary needs:
- `::flag` — cooperative boolean atom, polled at iteration
boundaries by callers that can return
gracefully.
- `::callbacks` — vec of `[id thunk]` pairs run by `cancel!` so
any number of in-flight workers (provider
HTTP call, Python eval future, voice recorder)
can register their own hard-cancel hook.
- `::reason` — WHO fired the cancel, stamped by `cancel!` and read
back with `cancel-reason`. Downstream every cancel
looks identical (a thread interrupt), so without
this stamp a self-inflicted cancel (stall watchdog,
shutdown) is indistinguishable from a user Esc.
`cancellation-set-future!` (legacy single-future API) is kept for
call sites that have not migrated yet; it now routes through the
callback registry too so behaviour stays identical.(cancellation? e)True if the given throwable was caused by a cancel! call. Channels
should treat this as a normal (cancelled) outcome rather than an
error and avoid showing stack traces.
True if the given throwable was caused by a `cancel!` call. Channels should treat this as a normal (cancelled) outcome rather than an error and avoid showing stack traces.
(cancelled? token)True once cancel! has been called on this token.
True once `cancel!` has been called on this token.
(capability-ensure! id probe)This process's verdict for capability id, probing at most once per answer that
cannot change:
{:capability id :status :ready :detail <whatever the probe returned>} {:capability id :status :unavailable :kind :terminal|:transient :error <message> :cause <throwable>}
probe is a thunk that DOES the real thing and throws when the machine cannot:
link the library, open the device, run the binary. It is called at most once
while a :ready or :terminal verdict stands, and again after a :transient
one. Probes are serialized against each other, because two callers racing to
download the same 13 MB is the failure this replaces.
This process's verdict for capability `id`, probing at most once per answer that
cannot change:
{:capability id :status :ready :detail <whatever the probe returned>}
{:capability id :status :unavailable :kind :terminal|:transient
:error <message> :cause <throwable>}
`probe` is a thunk that DOES the real thing and throws when the machine cannot:
link the library, open the device, run the binary. It is called at most once
while a `:ready` or `:terminal` verdict stands, and again after a `:transient`
one. Probes are serialized against each other, because two callers racing to
download the same 13 MB is the failure this replaces.(capability-fail! id t)Record a TERMINAL failure met somewhere other than the probe, and answer the verdict now in force.
A native runtime rarely breaks where it is provisioned: the library loads from
the static initializer of the first class that touches it, so the linker speaks
from inside the call, long after ensure! answered :ready. Without this, every
later call re-provisions and re-fails; with it, the next ensure! says restart.
A transient failure is NOT recorded — only the JVM's own frozen answers are.
Record a TERMINAL failure met somewhere other than the probe, and answer the verdict now in force. A native runtime rarely breaks where it is provisioned: the library loads from the static initializer of the first class that touches it, so the linker speaks from inside the call, long after `ensure!` answered `:ready`. Without this, every later call re-provisions and re-fails; with it, the next `ensure!` says restart. A transient failure is NOT recorded — only the JVM's own frozen answers are.
(capability-forget-verdicts!)Forget every verdict. The seam a TEST drives — production never calls it, because a remembered verdict is exactly the answer this process can no longer change.
Forget every verdict. The seam a TEST drives — production never calls it, because a remembered verdict is exactly the answer this process can no longer change.
(capability-terminal-error? t)True when t is the JVM refusing to LINK, at any depth: a native library is
missing, or a class that already met a missing library is permanently unusable.
The walk STOPS at the end of the cause chain — a shallow failure must not become
a NullPointerException out of the very code that explains it.
True when `t` is the JVM refusing to LINK, at any depth: a native library is missing, or a class that already met a missing library is permanently unusable. The walk STOPS at the end of the cause chain — a shallow failure must not become a NullPointerException out of the very code that explains it.
(capability-verdict id)The verdict this process already reached for id, or nil when it has never been
asked. NEVER probes: this is what a status line or a doctor check reads, and a
diagnostic that provisions 13 MB of natives to print one line is a bug.
The verdict this process already reached for `id`, or nil when it has never been asked. NEVER probes: this is what a status line or a doctor check reads, and a diagnostic that provisions 13 MB of natives to print one line is a bug.
(channel descriptor)Build and validate a channel descriptor map.
Build and validate a channel descriptor map.
(channel-by-id id)Lookup the channel by :channel/id. Returns nil when absent.
Lookup the channel by :channel/id. Returns nil when absent.
(channel-contributions-for channel-id)(channel-contributions-for channel-id slot)Return registered extension channel contributions for channel-id in
extension registration order. With slot, return only contributions for
that channel slot. Contributions are passive data; the channel owns each
slot's fn arity + return contract.
Return registered extension channel contributions for `channel-id` in extension registration order. With `slot`, return only contributions for that channel slot. Contributions are passive data; the channel owns each slot's fn arity + return contract.
(color k)(color theme-map k)Return RGB vector for palette token k in theme-map (or selected/default theme).
Return RGB vector for palette token `k` in `theme-map` (or selected/default theme).
(command descriptor)Build and validate a command map without realizing dynamic children.
Build and validate a command map without realizing dynamic children.
(configured-providers)The persisted provider fleet (global + project overlay), priority order, catalog metadata applied (base-url/api-style filled in).
The persisted provider fleet (global + project overlay), priority order, catalog metadata applied (base-url/api-style filled in).
(configured-providers-cached)Frame/request-frequency read of configured-providers that never re-runs
the full enumeration on a warm caller. The enumeration behind
config/load-config parses four config files per call — ~200ms on
machines with slow file IO — which stalled every TUI footer frame when it
ran on the render thread (issue #29).
Frame/request-frequency read of `configured-providers` that never re-runs the full enumeration on a warm caller. The enumeration behind `config/load-config` parses four config files per call — ~200ms on machines with slow file IO — which stalled every TUI footer frame when it ran on the render thread (issue #29). - FRESH snapshot → returned as-is (pure atom read). - STALE snapshot → returned immediately; a single-flight background refresh replaces it off-thread. - COLD (first read / just invalidated) → enumerates synchronously ONCE, so callers always get a real fleet, never a nil-because-cold.
(create! channel)(create! channel {:keys [title external-id workspace-id]})Create a brand-new session.
Opts (all optional): :title display title :external-id channel-specific external id :workspace-id pre-spawned workspace to pin the new session to. When omitted, a trunk workspace is auto-minted in create-environment.
Create a brand-new session.
Opts (all optional):
:title display title
:external-id channel-specific external id
:workspace-id pre-spawned workspace to pin the new session to.
When omitted, a trunk workspace is auto-minted in
create-environment.(create-environment router
{:keys [db session channel external-id title workspace-id]})Creates a vis environment (component) for session lifecycle and querying.
The environment holds:
Params:
router - Required. Result of llm/make-router.
opts - Map with :db and optional :session,
:channel, :external-id, :title.
:db accepted forms:
nil - no DB (sandbox-only execution)
:memory - ephemeral in-process SQLite DB
path string - persistent SQLite DB at path
{:path p} - persistent SQLite DB at path
{:datasource ds} - caller-owned DataSource (not closed on dispose)
Returns the vis environment map.
Creates a vis environment (component) for session lifecycle and
querying.
The environment holds:
- Python sandbox context with custom bindings + bindings cache
- DB connection (or shared-mem datasource)
- Router (LLM provider config)
- Extension registry atom
Params:
`router` - Required. Result of `llm/make-router`.
`opts` - Map with `:db` and optional `:session`,
`:channel`, `:external-id`, `:title`.
`:db` accepted forms:
nil - no DB (sandbox-only execution)
:memory - ephemeral in-process SQLite DB
path string - persistent SQLite DB at path
{:path p} - persistent SQLite DB at path
{:datasource ds} - caller-owned DataSource (not closed on dispose)
Returns the vis environment map.(create-python-context custom-bindings roots-fn network-opts stdin)Equip ONE session with the sandbox and answer
{:python-context :sandbox-ns :initial-ns-keys}.
custom-bindings is {symbol value} — a function becomes a host tool, any
other value crosses as data. roots-fn answers the directories the guest may
read and write; without it the interpreter keeps whatever policy the process
already had. network-opts carries the egress rules. stdin is what the
guest reads from sys.stdin, or nil for none.
The order matters and is the same order it has always been: the runtime
first, then the names the block may not overwrite, then the tools, then the
contracts those tools carry, then discovery, then policy — so every __vis_*
name and every seeded module is already BASELINE when the member snapshot is
taken, and the model's live-vars view shows only what its own blocks made.
Equip ONE session with the sandbox and answer
`{:python-context :sandbox-ns :initial-ns-keys}`.
`custom-bindings` is `{symbol value}` — a function becomes a host tool, any
other value crosses as data. `roots-fn` answers the directories the guest may
read and write; without it the interpreter keeps whatever policy the process
already had. `network-opts` carries the egress rules. `stdin` is what the
guest reads from `sys.stdin`, or nil for none.
The order matters and is the same order it has always been: the runtime
first, then the names the block may not overwrite, then the tools, then the
contracts those tools carry, then discovery, then policy — so every `__vis_*`
name and every seeded module is already BASELINE when the member snapshot is
taken, and the model's live-vars view shows only what its own blocks made.(current-config)Return the current provider config. Loads from disk on first call.
Return the current provider config. Loads from disk on first call.
(custom-bindings env)Current custom sandbox bindings {sym -> value}.
Current custom sandbox bindings {sym -> value}.
(db-create-connection! db-spec)Open a persistence connection from db-spec.
Common spec forms: nil - no DB (returns nil) :memory - in-memory ephemeral store (backend-defined) "path/to.db" - file-backed store (backend-defined) {:backend :sqlite :path ...} - explicit backend selection {:backend :sqlite :datasource ds} - caller-owned DataSource
Omitting :backend selects default-backend; the facade tags the returned
store map with the chosen backend so all subsequent facade calls dispatch
correctly.
Open a persistence connection from `db-spec`.
Common spec forms:
nil - no DB (returns nil)
:memory - in-memory ephemeral store (backend-defined)
"path/to.db" - file-backed store (backend-defined)
{:backend :sqlite :path ...} - explicit backend selection
{:backend :sqlite :datasource ds} - caller-owned DataSource
Omitting `:backend` selects `default-backend`; the facade tags the returned
store map with the chosen backend so all subsequent facade calls dispatch
correctly.(db-dispose-shared-connection!)Close the shared connection if one is open. Idempotent.
Close the shared connection if one is open. Idempotent.
(db-error->user-message e)Translate a persistence exception into something a human can act on.
Backend adapters own backend-specific recognition; unknown errors fall
back to (ex-message e).
Translate a persistence exception into something a human can act on. Backend adapters own backend-specific recognition; unknown errors fall back to `(ex-message e)`.
(db-info)Return the process-wide shared DB connection bound to
(config/resolve-db-spec). Thin wrapper over
persistance.core/db-shared-connection! that fills in the default db-spec
so frontend callers stay clear of config resolution.
Return the process-wide shared DB connection bound to `(config/resolve-db-spec)`. Thin wrapper over `persistance.core/db-shared-connection!` that fills in the default db-spec so frontend callers stay clear of config resolution.
(db-list-iteration-attachments-meta db-info iteration-id)(db-list-iterations-attachments-meta db-info iteration-ids)(db-search db-info query opts)Backend-neutral full-text search facade. Delegates to the registered
persistence backend, which RENDERS the neutral query DSL into its native
full-text query and runs it. No caller passes an engine dialect — only the
DSL in search-query-dsl-doc.
query is the DSL — a string (implicit-AND of its words) or a DSL map.
opts:
:owner-table restrict to one owner table (string)
:field restrict to one indexed field (string)
:limit max hits (backend default applies when nil)
Returns a vector of hits sorted by relevance (best first), each
{:owner-table :owner-id :field :snippet :rank}. Backends MUST honor the
DSL; an engine that cannot express a node should degrade it (e.g. :near ->
:all), never reject well-formed DSL. A MALFORMED query (e.g. a lone :not)
may throw — that is a DSL logic error, distinct from un-matchable content.
Backend-neutral full-text search facade. Delegates to the registered
persistence backend, which RENDERS the neutral query DSL into its native
full-text query and runs it. No caller passes an engine dialect — only the
DSL in `search-query-dsl-doc`.
`query` is the DSL — a string (implicit-AND of its words) or a DSL map.
`opts`:
:owner-table restrict to one owner table (string)
:field restrict to one indexed field (string)
:limit max hits (backend default applies when nil)
Returns a vector of hits sorted by relevance (best first), each
`{:owner-table :owner-id :field :snippet :rank}`. Backends MUST honor the
DSL; an engine that cannot express a node should degrade it (e.g. :near ->
:all), never reject well-formed DSL. A MALFORMED query (e.g. a lone :not)
may throw — that is a DSL logic error, distinct from un-matchable content.(db-set-turn-attachment-transcription! db-info
session-turn-soul-id
position
transcription)(db-shared-connection! db-spec)Return the process-wide shared persistence connection for db-spec,
opening it on first call and caching the handle for the lifetime of
the JVM. Subsequent calls return the cached handle regardless of
the db-spec argument - the singleton intentionally pins to the
first spec it saw.
Pair with db-dispose-shared-connection! on process shutdown.
Return the process-wide shared persistence connection for `db-spec`, opening it on first call and caching the handle for the lifetime of the JVM. Subsequent calls return the cached handle regardless of the `db-spec` argument - the singleton intentionally pins to the first spec it saw. Pair with `db-dispose-shared-connection!` on process shutdown.
(db-store-iteration! db-info opts)Same delegating shape as the macro-defined fns, but with input validation kept here so every backend gets the same precondition guarantees for free.
Same delegating shape as the macro-defined fns, but with input validation kept here so every backend gets the same precondition guarantees for free.
(db-sweep-orphaned-running-turns!)(db-sweep-orphaned-running-turns! db)Mark every :running turn as :interrupted. Run at process start
to clean up turns that crashed or were killed mid-write so the next
turn's handover digest renders the right outcome instead of guessing.
Returns the number of turns swept.
Mark every `:running` turn as `:interrupted`. Run at process start to clean up turns that crashed or were killed mid-write so the next turn's handover digest renders the right outcome instead of guessing. Returns the number of turns swept.
(db-update-session-turn! db-info session-turn-id opts)Write a turn's terminal outcome. Same delegating shape as the macro-defined fns, with the DIAGNOSTIC text bound here so every backend gets the same guarantee for free: the write that records HOW a turn ended can never be lost to an unbounded error message (see [[max-persisted-error-chars]]).
Write a turn's terminal outcome. Same delegating shape as the macro-defined fns, with the DIAGNOSTIC text bound here so every backend gets the same guarantee for free: the write that records HOW a turn ended can never be lost to an unbounded error message (see [[max-persisted-error-chars]]).
The built-in theme id used when config has no explicit theme.
The built-in theme id used when config has no explicit theme.
(deregister-cmd! nm)(deregister-cmd! parent nm)Remove a registered command. parent defaults to [] (top-level).
Remove a registered command. `parent` defaults to `[]` (top-level).
(deregister-extension! ns-sym)Drop an extension from the global registry AND reverse every side
effect register-extension! dispatched: deregister each CLI
subcommand, channel, provider, and persistence backend. Returns nil.
Plan caveat: side-effect cleanup on :removed extensions. Used by
Stays available for diagnostic surfaces.
Drop an extension from the global registry AND reverse every side effect `register-extension!` dispatched: deregister each CLI subcommand, channel, provider, and persistence backend. Returns nil. Plan caveat: side-effect cleanup on `:removed` extensions. Used by Stays available for diagnostic surfaces.
(diff-line-kind line)Classify ONE unified-diff line for channel-neutral colouring: :meta (file
headers ---/+++), :hunk (@@), :add (+), :del (-), or :ctx
(context / unchanged). The SINGLE classifier both channels share — the TUI maps
the kind to an ANSI colour, the web to a CSS class — so a diff fence colours
IDENTICALLY in both, from one source of truth (no per-channel copy to drift).
Classify ONE unified-diff line for channel-neutral colouring: `:meta` (file headers `---`/`+++`), `:hunk` (`@@`), `:add` (`+`), `:del` (`-`), or `:ctx` (context / unchanged). The SINGLE classifier both channels share — the TUI maps the kind to an ANSI colour, the web to a CSS class — so a `diff` fence colours IDENTICALLY in both, from one source of truth (no per-channel copy to drift).
(dismiss! id)Drop the entry with the given id, regardless of its :until
deadline. Returns true when an entry was actually removed.
Watchers fire when the value changed.
Drop the entry with the given id, regardless of its `:until` deadline. Returns true when an entry was actually removed. Watchers fire when the value changed.
(dismiss-all!)Drop every notification. Returns the new (empty) vec.
Drop every notification. Returns the new (empty) vec.
(dispatch! root args)(dispatch! root args {:keys [print-fn]})Resolve the command for args against root, parse the residual
tokens against the resolved command's :cmd/args spec, and call
its :cmd/run-fn with [parsed-args residual].
When the command lacks :cmd/run-fn and has subcommands, prints
help for that level via render-command. When --help/-h is
in the residual, also prints help.
Returns: {:status :ok :command cmd :result <whatever run-fn returned>} {:status :help :command cmd :help-text <string>} {:status :no-match :args args}
Resolve the command for `args` against `root`, parse the residual
tokens against the resolved command's `:cmd/args` spec, and call
its `:cmd/run-fn` with `[parsed-args residual]`.
When the command lacks `:cmd/run-fn` and has subcommands, prints
help for that level via `render-command`. When `--help`/`-h` is
in the residual, also prints help.
Returns:
{:status :ok :command cmd :result <whatever run-fn returned>}
{:status :help :command cmd :help-text <string>}
{:status :no-match :args args}(display-label pid)Human-readable label for a provider id. Never persisted.
A REGISTERED provider extension owns its own branding (Anthropic (API Key),
LM Studio, OpenAI) and wins. For every other id — anything a caller wrote
as providers: - id: … in vis.yml — that id IS the author's chosen
spelling, so it is echoed VERBATIM.
Provider ids are authored display values. Return them verbatim when no registered metadata supplies a label.
Human-readable label for a provider id. Never persisted. A REGISTERED provider extension owns its own branding (`Anthropic (API Key)`, `LM Studio`, `OpenAI`) and wins. For every other id — anything a caller wrote as `providers: - id: …` in `vis.yml` — that id IS the author's chosen spelling, so it is echoed VERBATIM. Provider ids are authored display values. Return them verbatim when no registered metadata supplies a label.
(display-model-name m)DISPLAY-ONLY normalization of a model id: path-style ids
(google/gemma-4-12b-qat, org/model as LM Studio / HF name them)
render with the slashes flattened to dashes (google-gemma-4-12b-qat),
so a provider/model label never reads as three ambiguous segments.
The wire/config id keeps its slashes — never feed this back to a
router or provider. nil-safe; non-strings and blanks return nil.
DISPLAY-ONLY normalization of a model id: path-style ids (`google/gemma-4-12b-qat`, `org/model` as LM Studio / HF name them) render with the slashes flattened to dashes (`google-gemma-4-12b-qat`), so a `provider/model` label never reads as three ambiguous segments. The wire/config id keeps its slashes — never feed this back to a router or provider. nil-safe; non-strings and blanks return nil.
(dispose-environment! environment)Disposes a vis environment and releases resources. For persistent DBs
(created with :path), data is preserved. For disposable DBs, all
data is deleted.
Every env owns its DB connection, so disposing one always closes it.
Disposes a vis environment and releases resources. For persistent DBs (created with `:path`), data is preserved. For disposable DBs, all data is deleted. Every env owns its DB connection, so disposing one always closes it.
(doctor-exit-code messages)Compute the doctor exit code from a vec of messages. 0 / 1 / 2 by max level. Plan Q19/d2.
Compute the doctor exit code from a vec of messages. 0 / 1 / 2 by max level. Plan Q19/d2.
(doctor-format-output messages)(doctor-format-output messages {:keys [use-ansi?]})Build the full TTY output from a vec of messages. Empty result
prints a placeholder. Caller routes it to stdout. ANSI is
auto-detected; pass :use-ansi? to override.
Build the full TTY output from a vec of messages. Empty result prints a placeholder. Caller routes it to stdout. ANSI is auto-detected; pass `:use-ansi?` to override.
(doctor-startup-hint)(doctor-startup-hint environment)Return a single-line string like ⚠ vis-agent: 2 issues detected - run \vis-agent doctor` for details.when warn/error count > 0; nil otherwise. Caller decides whether to print (skipped when the command being dispatched ISvis-agent doctor`).
Return a single-line string like `⚠ vis-agent: 2 issues detected - run \`vis-agent doctor\` for details.` when warn/error count > 0; nil otherwise. Caller decides whether to print (skipped when the command being dispatched IS `vis-agent doctor`).
(env-difference running requested)Variable NAMES whose value differs between the env a live process is running with and the one a new start asked for. Both sides are FINGERPRINTS, so this compares digests and answers names — the only thing either side may keep.
Variable NAMES whose value differs between the env a live process is running with and the one a new start asked for. Both sides are FINGERPRINTS, so this compares digests and answers names — the only thing either side may keep.
(env-fingerprint values){NAME "<digest>"} for one resolved delta — its SHAPE without its values.
This is what a status prints and what a REUSED process is compared against,
and both of those are read by a model and written to a log, so the value
itself can never appear: a name set from a keychain must compare equal to
itself and to nothing else. An unset name fingerprints as "unset".
`{NAME "<digest>"}` for one resolved delta — its SHAPE without its values.
This is what a status prints and what a REUSED process is compared against,
and both of those are read by a model and written to a log, so the value
itself can never appear: a name set from a keychain must compare equal to
itself and to nothing else. An unset name fingerprints as "unset".(env-mismatch-refusal id running requested){:message :differing} when a REPL is already running with an env OTHER than
the one this start named, else nil. Every language pack answers this same
refusal, because repl_start must mean ONE thing across languages: a live
REPL is reused, never silently replaced, and an env it was not started with
is a different REPL. Names and digests only — a value never reaches it.
`{:message :differing}` when a REPL is already running with an env OTHER than
the one this start named, else nil. Every language pack answers this same
refusal, because `repl_start` must mean ONE thing across languages: a live
REPL is reused, never silently replaced, and an env it was not started with
is a different REPL. Names and digests only — a value never reaches it.(error-message v)Build an error message string from a Throwable, map, or string.
Build an error message string from a Throwable, map, or string.
(extension spec)Build extension spec and stamp caller namespace for reload/source tracking.
Build extension spec and stamp caller namespace for reload/source tracking.
(extension-aggregate-create! env row)Append one extension-owned aggregate row. Returns the decoded row. Extension id is always filled from current extension callback context.
Append one extension-owned aggregate row. Returns the decoded row. Extension id is always filled from current extension callback context.
(extension-aggregate-get env query)Return one extension-owned aggregate row by query, or nil. Defaults to the latest row when the query is not unique.
Return one extension-owned aggregate row by query, or nil. Defaults to the latest row when the query is not unique.
(extension-aggregate-put! env row)Upsert one singleton extension-owned aggregate row for key/kind/scope. Returns the decoded row.
Upsert one singleton extension-owned aggregate row for key/kind/scope. Returns the decoded row.
(extension-delete-aggregate! env query)Delete extension-owned aggregate rows matching query. Cross-extension delete is impossible through this API because extension id is runtime-filled.
Delete extension-owned aggregate rows matching query. Cross-extension delete is impossible through this API because extension id is runtime-filled.
(extension-env-status name)Source and value metadata for variable name, for EVERY surface that resolves
one: the Python extension host, a Clojure extension, the TUI settings row.
Order: an environment: declaration DECIDES — a declared name resolves from
the source its own declaration names and from nowhere else. Otherwise the
workspace's .env/.env.local answer, and only then the process environment.
A blank value is no value, whatever produced it — an operator's explicit
FOO= means "not this one".
:source is :env, :dotenv, :keychain, :command, :literal or
:unset; :value is nil unless that source produced a non-blank string.
Source and value metadata for variable `name`, for EVERY surface that resolves one: the Python extension host, a Clojure extension, the TUI settings row. Order: an `environment:` declaration DECIDES — a declared name resolves from the source its own declaration names and from nowhere else. Otherwise the workspace's `.env`/`.env.local` answer, and only then the process environment. A blank value is no value, whatever produced it — an operator's explicit `FOO=` means "not this one". `:source` is `:env`, `:dotenv`, `:keychain`, `:command`, `:literal` or `:unset`; `:value` is nil unless that source produced a non-blank string.
(extension-env-value name)Resolved value for name, or nil when no source produced a non-blank one.
Resolved value for `name`, or nil when no source produced a non-blank one.
(extension-list-aggregates env query)List extension-owned aggregate rows. The current extension id is always applied; normal extension code cannot list another extension's rows.
List extension-owned aggregate rows. The current extension id is always applied; normal extension code cannot list another extension's rows.
(extension-source-markers-of ns-sym)Lookup the source markers stored for ns-sym. Returns the marker
map ({:source-paths :source-mtime-max :source-hash-sha256}) or
nil when the extension was never registered (or its markers
computation failed at register time).
Lookup the source markers stored for `ns-sym`. Returns the marker
map (`{:source-paths :source-mtime-max :source-hash-sha256}`) or
nil when the extension was never registered (or its markers
computation failed at register time).(extension-theme-settings)(extension-theme-settings theme-registry)Return a registry in extension :ext/theme compact settings shape.
Return a registry in extension `:ext/theme` compact settings shape.
(extension-update-aggregate! env query f & args)Atomic singleton update. Reads the current content for query, applies f, and writes the returned value as :content. Query must include :key and :kind.
Atomic singleton update. Reads the current content for query, applies f, and writes the returned value as :content. Query must include :key and :kind.
(extract-code input)Walk the AST and return a vector of strings, one per [:code ...] block,
in source order. Used by vis-agent --code.
Walk the AST and return a vector of strings, one per [:code ...] block, in source order. Used by `vis-agent --code`.
(extract-text input)Walk the AST and return concatenated plain-text content of all [:p] blocks (inline content stripped). Used by voice TTS.
Walk the AST and return concatenated plain-text content of all [:p] blocks (inline content stripped). Used by voice TTS.
(find-leaf root args)Walk the tree from root consuming tokens until either:
Returns {:command resolved-cmd :path [name...] :residual [token...]},
or nil when even the root's name doesn't match args[0]. The
residual is everything LEFT after the resolved command name.
Walk the tree from `root` consuming tokens until either:
- a child matches and has no further subcommands
- or no child matches the next token
Returns `{:command resolved-cmd :path [name...] :residual [token...]}`,
or nil when even the root's name doesn't match args[0]. The
residual is everything LEFT after the resolved command name.(find-named root args)Like find-leaf, but matches against the bare arg vector ignoring
the root command's name (the way cli/-main typically gets called).
Useful when the root is implicit and you just want the resolved
subcommand for the given args.
Like `find-leaf`, but matches against the bare arg vector ignoring the root command's name (the way `cli/-main` typically gets called). Useful when the root is implicit and you just want the resolved subcommand for the given args.
(first-run?)True on a genuine FIRST run: no provider configured AND no global machine store
(~/.vis/state.yml) has ever been written. Distinguishes the full welcome
(brand-new user) from a returning user who merely has no provider right now
(e.g. removed their only one).
True on a genuine FIRST run: no provider configured AND no global machine store (`~/.vis/state.yml`) has ever been written. Distinguishes the full welcome (brand-new user) from a returning user who merely has no provider right now (e.g. removed their only one).
(form->display m)Project canonical display fields from a source map, dropping nils.
Project canonical display fields from a source map, dropping nils.
(form-with-display form)Attach the cached ruff rendering of a form's Python source when the form did not
author its own :display-code. Result presentation is always derived locally
from canonical facts and is never attached to the form.
Attach the cached ruff rendering of a form's Python source when the form did not author its own `:display-code`. Result presentation is always derived locally from canonical facts and is never attached to the form.
(form<-wire event)Read the canonical display fields back off a gateway WIRE event into a form,
using the literal wire spelling declared beside each engine key in
display-fields. The single inbound projection channels use — the mirror of
->display.
Read the canonical display fields back off a gateway WIRE event into a form, using the literal wire spelling declared beside each engine key in `display-fields`. The single inbound projection channels use — the mirror of `->display`.
(format-clojure code-str _width)Source is shown as written — no reformatting. Returns code-str
trimmed of trailing whitespace (or unchanged when not a string).
Source is shown as written — no reformatting. Returns `code-str` trimmed of trailing whitespace (or unchanged when not a string).
(format-cost cost)Render a dollar cost as '~$0.006954' (six decimal places, US
locale). Returns nil when cost is nil, zero, negative, or
non-numeric. Accepts either the bare number or a "total_cost"
map. Detailed cost maps render the total first and the breakdown
parenthesized, in order: in, cached, write, out — e.g.
'~$0.006954 (in ~$0.001200, cached ~$0.000400, out ~$0.005354)'.
The parenthesized breakdown renders only when at least two of
those slots carry a positive value; otherwise just the total.
Render a dollar cost as '~$0.006954' (six decimal places, US locale). Returns nil when `cost` is nil, zero, negative, or non-numeric. Accepts either the bare number or a `"total_cost"` map. Detailed cost maps render the total first and the breakdown parenthesized, in order: in, cached, write, out — e.g. '~$0.006954 (in ~$0.001200, cached ~$0.000400, out ~$0.005354)'. The parenthesized breakdown renders only when at least two of those slots carry a positive value; otherwise just the total.
(format-date d)Format a java.util.Date as dd-MM-yyyy HH:mm in local timezone.
Format a `java.util.Date` as `dd-MM-yyyy HH:mm` in local timezone.
(format-duration ms)Human-readable millisecond duration. e.g. 2.3s, 1m 15s. Always
uses Locale/US so the decimal separator is a dot regardless of
the JVM default locale. Coerces the input to long up-front because
callers routinely pass a double from (/ ns 1e6).
Human-readable millisecond duration. e.g. `2.3s`, `1m 15s`. Always uses Locale/US so the decimal separator is a dot regardless of the JVM default locale. Coerces the input to long up-front because callers routinely pass a double from `(/ ns 1e6)`.
(format-error v)Add the standard ERROR: prefix to an error message, idempotent.
Add the standard `ERROR: ` prefix to an error message, idempotent.
(format-iterations n)(format-iterations n {:keys [silent-count]})Render an iteration count as '1 iter' or '3 iters'. Returns nil
when n is nil or non-numeric. Optional :silent-count appends
hidden/silent bookkeeping count, e.g. '3 iters (2 silent)'.
Render an iteration count as '1 iter' or '3 iters'. Returns nil when `n` is nil or non-numeric. Optional `:silent-count` appends hidden/silent bookkeeping count, e.g. '3 iters (2 silent)'.
(format-meta-line result)(format-meta-line result opts)Single-line turn summary for plain-text surfaces (the CLI [...] bracket):
the shared meta-summary-line with the fallback note
folded inline. The TUI instead uses meta-summary-line + meta-fallback-note
directly so it can float the note on its own faint row — same words, same
numbers, just two rows. Returns "" when there's nothing to show.
Single-line turn summary for plain-text surfaces (the CLI `[...]` bracket): the shared `meta-summary-line` with the fallback note folded inline. The TUI instead uses `meta-summary-line` + `meta-fallback-note` directly so it can float the note on its own faint row — same words, same numbers, just two rows. Returns "" when there's nothing to show.
(format-tokens {:strs [input output] :as tokens})Render token counts in the canonical compact grouped form: 'tok <input>→<output> (cached <cached-input>)' when cached input is positive, otherwise 'tok <input>→<output>'.
The arrow reads 'prompt produced completion'. Cached is cached
input tokens, parenthesized because provider APIs report cache
hits inside prompt usage. Reads the canonical string-keyed usage
map ("input" / "output" / "cached" / "cache_created").
Cache visibility: the (cached N) segment renders only when N is
positive. Zero / missing cache info stays hidden so meta lines do
not show noisy (cached 0) decorations.
Returns nil when no known field carries a number.
Render token counts in the canonical compact grouped form: 'tok <input>→<output> (cached <cached-input>)' when cached input is positive, otherwise 'tok <input>→<output>'. The arrow reads 'prompt produced completion'. Cached is cached input tokens, parenthesized because provider APIs report cache hits inside prompt usage. Reads the canonical string-keyed usage map (`"input"` / `"output"` / `"cached"` / `"cache_created"`). Cache visibility: the `(cached N)` segment renders only when N is positive. Zero / missing cache info stays hidden so meta lines do not show noisy `(cached 0)` decorations. Returns nil when no known field carries a number.
(gateway-assign-project! sid pid)Assign a session to a project (nil clears / removes from project). Returns the soul.
Assign a session to a project (nil clears / removes from project). Returns the soul.
(gateway-auth-required?)True when this gateway instance demands the bearer token. OFF by
default on a loopback bind (a localhost single-user daemon — the
token dance is pure friction there); ALWAYS on for a non-loopback
bind; --require-token forces it on loopback too.
True when this gateway instance demands the bearer token. OFF by default on a loopback bind (a localhost single-user daemon — the token dance is pure friction there); ALWAYS on for a non-loopback bind; `--require-token` forces it on loopback too.
(gateway-cancel-current-turn! sid owner-key)Tid-less cancel: kill the turn currently holding sid's :current-turn slot
in the daemon, iff THIS caller submitted it under owner-key (the
idempotency_key it sent). For callers that lost (or never learned) the
gateway turn id. A session is shared, so an unaddressed cancel would kill
whatever another channel happens to be running. Returns the parsed body
({"status" "cancelling", "turn_id" tid}); throws on HTTP error (409 when
the session is idle or the running turn is someone else's).
Tid-less cancel: kill the turn currently holding `sid`'s `:current-turn` slot
in the daemon, iff THIS caller submitted it under `owner-key` (the
`idempotency_key` it sent). For callers that lost (or never learned) the
gateway turn id. A session is shared, so an unaddressed cancel would kill
whatever another channel happens to be running. Returns the parsed body
(`{"status" "cancelling", "turn_id" tid}`); throws on HTTP error (409 when
the session is idle or the running turn is someone else's).(gateway-capabilities)The daemon's capability document, string-keyed, or nil when it cannot answer. The attachment contract a channel admits file drops against comes from here.
The daemon's capability document, string-keyed, or nil when it cannot answer. The attachment contract a channel admits file drops against comes from here.
(gateway-change-root! sid path)Repoint sid's PRIMARY filesystem root to path IN THE DAEMON, returning the
refreshed session-workspace-info (whose :id is the newly pinned workspace).
Repoint `sid`'s PRIMARY filesystem root to `path` IN THE DAEMON, returning the refreshed `session-workspace-info` (whose `:id` is the newly pinned workspace).
(gateway-consume-provider-reset-credit! provider-id account-id idempotency-key)Consume the reset confirmed for this account. The caller keeps the same idempotency key until a recognized outcome is received. No route probing or daemon restart is appropriate for a mutation.
Consume the reset confirmed for this account. The caller keeps the same idempotency key until a recognized outcome is received. No route probing or daemon restart is appropriate for a mutation.
(gateway-cycle-setting! id)Atomically advance one enum setting in the gateway and return its refreshed string-keyed settings row.
Atomically advance one enum setting in the gateway and return its refreshed string-keyed settings row.
(gateway-daemon-status)Admin status of the gateway this process drives — the REMOTE target when one is configured, else the daemon registered for the current DB. Always the daemon's own wire map (STRING keys), including when nothing is running.
Admin status of the gateway this process drives — the REMOTE target when one is configured, else the daemon registered for the current DB. Always the daemon's own wire map (STRING keys), including when nothing is running.
(gateway-daemon-stop!)Stop the daemon registered for this DB, escalating when it stops answering.
POST /v1/admin/stop first; when that is met with silence from a daemon that
still holds its port, signal the pid the registry names ([[kill-registered-daemon!]])
rather than reporting a live orphan and handing the human an lsof. A port held
by a process this registry cannot claim is still reported, never signalled.
Stop the daemon registered for this DB, escalating when it stops answering. `POST /v1/admin/stop` first; when that is met with silence from a daemon that still holds its port, signal the pid the registry names ([[kill-registered-daemon!]]) rather than reporting a live orphan and handing the human an `lsof`. A port held by a process this registry cannot claim is still reported, never signalled.
(gateway-delete-project! pid)(gateway-delete-project! pid {:keys [is-recursive?]})DELETE /v1/projects/:pid. Default: member sessions scatter back to
project-less. With {:is-recursive? true} every member session is deleted
too, and the response names the deleted ids.
DELETE /v1/projects/:pid. Default: member sessions scatter back to
project-less. With `{:is-recursive? true}` every member session is deleted
too, and the response names the deleted ids.(gateway-ensure!)(gateway-ensure! {:keys [port host] :as opts})Return a fresh daemon registry entry for the current DB, auto-starting the
detached gateway if needed. :memory is a programmer error for this client;
headless one-shots stay in-process and should not call here.
Optional :port/:host overrides the bind used WHEN THIS CALL SPAWNS a fresh
daemon (e.g. vis-agent channels web --port); a fresh daemon already registered for
the DB is a singleton and is attached to as-is, so the override is moot there.
Freshness is DEBOUNCED: the full HTTP /healthz probe (via probe-entry?)
runs at most once per entry-probe-ttl-ms. Within that window a cached entry
whose pid is still alive is trusted directly, so the TUI's chatty poll loop
stops paying for a doubled HTTP round-trip (and its JSON/reflection churn) on
every gateway call.
The slow discover/start path is single-flight per canonical DB inside this process. Callers re-check the cache after acquiring that lock, so concurrent startup callbacks share one spawn/wait instead of each waiting for readiness.
A daemon running a DIFFERENT build than this one is also replaced here when
replacing it is free ([[bounce-stale-daemon!]]) - that is how the first vis
started after vis-agent update, or after a rebuild of a dev checkout, comes up
on the new code with nobody stopping anything by hand. That decision comes BEFORE
the compatibility assert: a daemon too old to speak this build's wire protocol is
the one most worth replacing, so the mismatch screen is left for the daemon
somebody is still using.
Return a fresh daemon registry entry for the current DB, auto-starting the detached gateway if needed. `:memory` is a programmer error for this client; headless one-shots stay in-process and should not call here. Optional `:port`/`:host` overrides the bind used WHEN THIS CALL SPAWNS a fresh daemon (e.g. `vis-agent channels web --port`); a fresh daemon already registered for the DB is a singleton and is attached to as-is, so the override is moot there. Freshness is DEBOUNCED: the full HTTP /healthz probe (via `probe-entry?`) runs at most once per `entry-probe-ttl-ms`. Within that window a cached entry whose pid is still alive is trusted directly, so the TUI's chatty poll loop stops paying for a doubled HTTP round-trip (and its JSON/reflection churn) on every gateway call. The slow discover/start path is single-flight per canonical DB inside this process. Callers re-check the cache after acquiring that lock, so concurrent startup callbacks share one spawn/wait instead of each waiting for readiness. A daemon running a DIFFERENT build than this one is also replaced here when replacing it is free ([[bounce-stale-daemon!]]) - that is how the first vis started after `vis-agent update`, or after a rebuild of a dev checkout, comes up on the new code with nobody stopping anything by hand. That decision comes BEFORE the compatibility assert: a daemon too old to speak this build's wire protocol is the one most worth replacing, so the mismatch screen is left for the daemon somebody is still using.
(gateway-ensure-project-for-root! root)(gateway-ensure-project-for-root! root name)POST /v1/projects/actions/ensure — get-or-create the project bound to canonical
workspace root (a project IS a TUI tab set). name seeds a fresh project.
Returns the project.
POST /v1/projects/actions/ensure — get-or-create the project bound to canonical workspace `root` (a project IS a TUI tab set). `name` seeds a fresh project. Returns the project.
(gateway-ensure-serving! path)(gateway-ensure-serving! path opts)Like [[ensure-gateway!]], but tries to GUARANTEE the returned daemon actually
serves path. When [[ensure-gateway!]] attaches to an already-running daemon
that 404s on path (started from a classpath missing the extension that owns
it), respawn a fresh daemon from THIS process — whose classpath, by
construction, carries the route. This is what lets vis-agent channels web
self-heal instead of parking on a /ui that 404s.
Optional opts ({:port :host}) overrides the bind used when THIS call has
to spawn a fresh daemon (the vis-agent channels web --port/--host flags); it is
moot when a fresh daemon is already registered for the DB.
The respawn is NON-DESTRUCTIVE. A blind POST /v1/admin/stop is refcount-blind: it would abort every in-flight turn and kill every session's background resources. So we force-restart the stale daemon ONLY when it is idle — no OTHER clients and no running turn. Otherwise we leave it untouched and surface a clear error. A transport blip on the probe (not a real 404) never triggers a restart. Returns the entry.
Like [[ensure-gateway!]], but tries to GUARANTEE the returned daemon actually
serves `path`. When [[ensure-gateway!]] attaches to an already-running daemon
that 404s on `path` (started from a classpath missing the extension that owns
it), respawn a fresh daemon from THIS process — whose classpath, by
construction, carries the route. This is what lets `vis-agent channels web`
self-heal instead of parking on a `/ui` that 404s.
Optional `opts` (`{:port :host}`) overrides the bind used when THIS call has
to spawn a fresh daemon (the `vis-agent channels web --port/--host` flags); it is
moot when a fresh daemon is already registered for the DB.
The respawn is NON-DESTRUCTIVE. A blind POST /v1/admin/stop is refcount-blind:
it would abort every in-flight turn and kill every session's background
resources. So we force-restart the stale daemon ONLY when it is idle — no OTHER
clients and no running turn. Otherwise we leave it untouched and surface a clear
error. A transport blip on the probe (not a real 404) never triggers a restart.
Returns the entry.(gateway-fleet-subscribe! sink)Watch the FLEET stream — GET /v1/events?scope=fleet — and hand every frame
to sink. One frame per session whose list-visible state changed
(session.status: is_live / is_awaiting_input / current_turn_id) or
that was renamed (session.title_updated). Returns a zero-arg stop fn.
This is what a session LIST subscribes to instead of asking about sessions one
by one: the fleet answers WHICH sessions changed, so a picker holding a
windowed read never polls a row again. There is no replay and no cursor — the
feed is a delta layered on a cold /v1/sessions window, so a reconnect costs
nothing to arrange and a missed frame heals on the next read. sink runs on
the reader thread and must not block it; drops reconnect with the multiplexed
mirror's backoff until the returned fn is called.
Watch the FLEET stream — `GET /v1/events?scope=fleet` — and hand every frame to `sink`. One frame per session whose list-visible state changed (`session.status`: `is_live` / `is_awaiting_input` / `current_turn_id`) or that was renamed (`session.title_updated`). Returns a zero-arg stop fn. This is what a session LIST subscribes to instead of asking about sessions one by one: the fleet answers WHICH sessions changed, so a picker holding a windowed read never polls a row again. There is no replay and no cursor — the feed is a delta layered on a cold `/v1/sessions` window, so a reconnect costs nothing to arrange and a missed frame heals on the next read. `sink` runs on the reader thread and must not block it; drops reconnect with the multiplexed mirror's backoff until the returned fn is called.
(gateway-input-views sid)Pending input Views for sid IN THE DAEMON, oldest first, in
canonical wire shape. The live view.open event is the fast path;
this is how a client that attached LATER still finds the open form instead of
watching a turn that never moves.
Pending input Views for `sid` IN THE DAEMON, oldest first, in canonical wire shape. The live `view.open` event is the fast path; this is how a client that attached LATER still finds the open form instead of watching a turn that never moves.
(gateway-iteration-attachment-bytes sid iid idx)Raw bytes (a byte-array) of ONE outbound artifact — iteration iid, its 0-based
idx in the iteration's ordered attachment list — fetched from the daemon's
attachment byte endpoint, or nil (404 / no bytes). The lazy-fetch companion to
a live iteration.completed attachment descriptor: a client sees {:index :media_type …} on the frame, then pulls the bytes here. HISTORY resolves the
same way (the trace iteration's :id + attachment index).
Raw bytes (a byte-array) of ONE outbound artifact — iteration `iid`, its 0-based
`idx` in the iteration's ordered attachment list — fetched from the daemon's
attachment byte endpoint, or nil (404 / no bytes). The lazy-fetch companion to
a live `iteration.completed` attachment descriptor: a client sees `{:index
:media_type …}` on the frame, then pulls the bytes here. HISTORY resolves the
same way (the trace iteration's `:id` + attachment index).(gateway-list-projects)(gateway-list-projects {:keys [owner archived?]})GET /v1/projects — projects are CROSS-CHANNEL. opts: :owner (string),
:archived? (bool). Returns the :projects vector.
GET /v1/projects — projects are CROSS-CHANNEL. `opts`: :owner (string), :archived? (bool). Returns the :projects vector.
(gateway-list-resources sid)Vector of the session's live resource DATA maps from the daemon's registry
(string-keyed, same shape resources/list-resources returns in-process).
Vector of the session's live resource DATA maps from the daemon's registry (string-keyed, same shape `resources/list-resources` returns in-process).
(gateway-list-resources-cached sid)Footer-frequency read: the session's resource list served from a per-sid cache that NEVER blocks the caller. A stale (or cold) entry kicks a background single-flight refresh and this returns the last-known value immediately (nil before the first success). Keeping the daemon HTTP round-trip OFF the render thread is what stops a busy daemon from stalling every TUI frame.
Footer-frequency read: the session's resource list served from a per-sid cache that NEVER blocks the caller. A stale (or cold) entry kicks a background single-flight refresh and this returns the last-known value immediately (nil before the first success). Keeping the daemon HTTP round-trip OFF the render thread is what stops a busy daemon from stalling every TUI frame.
(gateway-list-sessions opts)The ROWS of one window of the session list, in the gateway's own order. opts names
the cut - see session-window-path.
The ROWS of one window of the session list, in the gateway's own order. `opts` names the cut - see `session-window-path`.
(gateway-list-sessions-page opts)One window of the session list WITH the walk that continues it:
{:sessions rows :next-cursor str-or-nil :has-more bool :total n}.
opts names the cut (session-window-path). A surface that pages - the session picker
:after :next-cursor, so a list of
a thousand sessions is read a screen at a time instead of downloaded whole.One window of the session list WITH the walk that continues it:
`{:sessions rows :next-cursor str-or-nil :has-more bool :total n}`.
`opts` names the cut (`session-window-path`). A surface that pages - the session picker
- holds this window and asks for the next one with `:after` `:next-cursor`, so a list of
a thousand sessions is read a screen at a time instead of downloaded whole.(gateway-live-views sid)The live views session sid is SHOWING in the daemon right now, oldest first,
in canonical wire shape. The view.* events with kind=live are the fast path; this
is how a client that attached MID-RUN paints the whole picture at once instead
of waiting for the next patch to tell it a view exists.
The live views session `sid` is SHOWING in the daemon right now, oldest first, in canonical wire shape. The `view.*` events with `kind=live` are the fast path; this is how a client that attached MID-RUN paints the whole picture at once instead of waiting for the next patch to tell it a view exists.
(gateway-mcp-auth-complete! server flow-id input)Finish a flow with the redirect URL the user pasted back (or a bare code).
Finish a flow with the redirect URL the user pasted back (or a bare code).
(gateway-mcp-auth-logout! server)Forget the gateway's persisted OAuth tokens for a server.
Forget the gateway's persisted OAuth tokens for a server.
(gateway-mcp-auth-poll! server flow-id)Read a flow's verdict without blocking: pending, ok, or error.
Read a flow's verdict without blocking: `pending`, `ok`, or `error`.
(gateway-mcp-auth-start! server)Begin headless OAuth for an HTTP MCP server. Returns the wire flow
(flow_id, kind, url, redirect_uri, expires_at_ms, status).
Begin headless OAuth for an HTTP MCP server. Returns the wire flow (`flow_id`, `kind`, `url`, `redirect_uri`, `expires_at_ms`, `status`).
(gateway-mcp-kill-server! server)Stop a server NOW and hold it down until it is started again. Runtime only — nothing in the user's config changes.
Stop a server NOW and hold it down until it is started again. Runtime only — nothing in the user's config changes.
(gateway-mcp-save-server! server spec)Create or replace a gateway-managed server. spec is the string-keyed wire
spec (transport, command/args/cwd/env, or url/headers, plus the
optional enabled and timeout_ms). The DAEMON validates it, persists it in
its own machine state, and reconnects — nothing is written on this side, so a
TUI attached to a REMOTE gateway adds servers exactly like the app does.
Secrets survive an omitting save: see mcp.core/with-preserved-secrets.
Returns the saved sanitized row.
Create or replace a gateway-managed server. `spec` is the string-keyed wire spec (`transport`, `command`/`args`/`cwd`/`env`, or `url`/`headers`, plus the optional `enabled` and `timeout_ms`). The DAEMON validates it, persists it in its own machine state, and reconnects — nothing is written on this side, so a TUI attached to a REMOTE gateway adds servers exactly like the app does. Secrets survive an omitting save: see `mcp.core/with-preserved-secrets`. Returns the saved sanitized row.
(gateway-mcp-servers)Sanitized MCP inventory (string-keyed rows: name, transport, enabled,
is_connected, is_managed, is_killed, tools, is_authorized, …).
Sanitized MCP inventory (string-keyed rows: `name`, `transport`, `enabled`, `is_connected`, `is_managed`, `is_killed`, `tools`, `is_authorized`, …).
(gateway-mcp-set-server-enabled! server enabled)Persist a server's on/off switch in the gateway's own state.
Persist a server's on/off switch in the gateway's own state.
(gateway-mcp-start-server! server)Release a kill and connect the server again.
Release a kill and connect the server again.
(gateway-mcp-test-server! server spec)Connect a CANDIDATE spec without saving it and return {name, is_connected, tools}. The gateway opens and closes the connection, so a bad command or an
unreachable endpoint is reported before it is ever persisted.
Connect a CANDIDATE spec without saving it and return `{name, is_connected,
tools}`. The gateway opens and closes the connection, so a bad command or an
unreachable endpoint is reported before it is ever persisted.(gateway-mux-subscribe! sid sink cursor)Add sid's sink to the ONE process-wide multiplexed event stream, starting
at cursor (its current-seq for a live-only stream). The connection is
(re)opened only when the session set changes; multiple local listeners for
the SAME session share one cursor and one remote subscription. Returns a
zero-arg cleanup fn. Every sink sees gateway.connected / gateway.disconnected
on connection changes.
Add `sid`'s `sink` to the ONE process-wide multiplexed event stream, starting at `cursor` (its `current-seq` for a live-only stream). The connection is (re)opened only when the session set changes; multiple local listeners for the SAME session share one cursor and one remote subscription. Returns a zero-arg cleanup fn. Every sink sees gateway.connected / gateway.disconnected on connection changes.
(gateway-mux-unsubscribe! sid)(gateway-mux-unsubscribe! sid sub-id)Drop one local listener from the multiplexed stream and reconnect only when the last listener for that sid is gone (or tear the connection down when it was the last watched session).
Drop one local listener from the multiplexed stream and reconnect only when the last listener for that sid is gone (or tear the connection down when it was the last watched session).
(gateway-prepare-speech-model! direction
{:keys [engine-id voice-id on-progress]})Prepare one gateway-owned speech engine and wait until it is ready. direction
is :transcribe or :synthesize; on-progress receives the gateway's
string-keyed model state. No model or native runtime is initialized here.
Prepare one gateway-owned speech engine and wait until it is ready. `direction` is `:transcribe` or `:synthesize`; `on-progress` receives the gateway's string-keyed model state. No model or native runtime is initialized here.
(gateway-provider-auth-cancel! provider-id flow-id)Forget an abandoned flow. Idempotent.
Forget an abandoned flow. Idempotent.
(gateway-provider-auth-complete! provider-id flow-id redirect-url)Finish a pkce flow with the redirect URL the user pasted back.
Finish a `pkce` flow with the redirect URL the user pasted back.
(gateway-provider-auth-poll! provider-id flow-id)Read a device flow's verdict: pending, ok, or error. Never blocks.
Read a `device` flow's verdict: `pending`, `ok`, or `error`. Never blocks.
(gateway-provider-auth-start! provider-id)Begin OAuth for provider-id. Returns the string-keyed wire flow
(flow_id, kind, url, user_code, verification_uri, interval_ms,
instructions) or nil when the daemon refused.
Begin OAuth for `provider-id`. Returns the string-keyed wire flow (`flow_id`, `kind`, `url`, `user_code`, `verification_uri`, `interval_ms`, `instructions`) or nil when the daemon refused.
(gateway-provider-auth-submit-key! provider-id flow-id api-key)Finish an api-key flow: hand the key the user typed to the DAEMON, which
persists it in ITS OWN config. The calling process never writes the
credential — same boundary as OAuth.
Finish an `api-key` flow: hand the key the user typed to the DAEMON, which persists it in ITS OWN config. The calling process never writes the credential — same boundary as OAuth.
(gateway-provider-logout! provider-id)Clear provider-id's persisted credentials IN THE DAEMON.
Clear `provider-id`'s persisted credentials IN THE DAEMON.
(gateway-provider-model-options provider-id show-all?)GET /v1/providers/:id/models — the LIVE model catalog resolved DAEMON-side,
where the gateway owns OAuth token resolution. A thin client NEVER builds a
token-resolving svar router to list models; it asks the daemon, which runs
the svar/models! probe (and any token refresh) against its own credential.
Returns the engine-shaped {:models [id …] :hidden-count n}.
GET /v1/providers/:id/models — the LIVE model catalog resolved DAEMON-side,
where the gateway owns OAuth token resolution. A thin client NEVER builds a
token-resolving svar router to list models; it asks the daemon, which runs
the `svar/models!` probe (and any token refresh) against its own credential.
Returns the engine-shaped `{:models [id …] :hidden-count n}`.(gateway-provider-remove! provider-id)DELETE /v1/providers/:id — drop provider-id from the fleet IN THE DAEMON,
credential included. Removal is the daemon's to do because it owns BOTH the
config file and the token file: a row dropped while its credential stays on
disk comes straight back as an authenticated preset. Idempotent — is_removed
is false when the id was not in the persisted fleet.
DELETE /v1/providers/:id — drop `provider-id` from the fleet IN THE DAEMON, credential included. Removal is the daemon's to do because it owns BOTH the config file and the token file: a row dropped while its credential stays on disk comes straight back as an authenticated preset. Idempotent — `is_removed` is false when the id was not in the persisted fleet.
(gateway-reconcile-running-turns!)Clients do not sweep. Only the daemon may reconcile its own startup orphans.
Clients do not sweep. Only the daemon may reconcile its own startup orphans.
(gateway-register-contributed-sse! stream-id close!)Count a contributed SSE connection in the gateway's existing client lifecycle.
Count a contributed SSE connection in the gateway's existing client lifecycle.
(gateway-register-routes! id contribution)Imperative escape hatch: register (or replace, by id) a route
contribution from an embedded/REPL caller. Extensions should prefer
the declarative :gateway.slot/http-routes channel-contribution slot
— the gateway pulls it with no registration call at all.
Imperative escape hatch: register (or replace, by `id`) a route contribution from an embedded/REPL caller. Extensions should prefer the declarative `:gateway.slot/http-routes` channel-contribution slot — the gateway pulls it with no registration call at all.
(gateway-release-session! sid)Release a session VIEW when the owning channel exits: tell the daemon to
stop the session's background resources (background shell children, REPLs) and drop
its live runtime, then release the process-level client lease. This is NOT
a per-session delete (the transcript stays resumable) and never sends daemon
shutdown; the daemon stops itself only when refcount AND running-turn-count
hit zero. Best-effort and never daemon-spawning — if no fresh daemon is
registered there is nothing to release against.
Release a session VIEW when the owning channel exits: tell the daemon to stop the session's background resources (background `shell` children, REPLs) and drop its live runtime, then release the process-level client lease. This is NOT a per-session delete (the transcript stays resumable) and never sends daemon shutdown; the daemon stops itself only when refcount AND running-turn-count hit zero. Best-effort and never daemon-spawning — if no fresh daemon is registered there is nothing to release against.
(gateway-release-session-runtime! sid)Release a session's live RUNTIME on the daemon WITHOUT touching the process
client lease: stop its background resources (background shell children, managed REPLs)
and drop its loop/env, keeping the transcript resumable. Used when ONE view of
a session closes (e.g. a single TUI tab) while the owning process stays
connected — so the whole-process refcount lease is left intact and the daemon
is never nudged toward self-reap while other tabs remain open. Best-effort and
never daemon-spawning — nothing to release against when no fresh daemon is
registered.
Release a session's live RUNTIME on the daemon WITHOUT touching the process client lease: stop its background resources (background `shell` children, managed REPLs) and drop its loop/env, keeping the transcript resumable. Used when ONE view of a session closes (e.g. a single TUI tab) while the owning process stays connected — so the whole-process refcount lease is left intact and the daemon is never nudged toward self-reap while other tabs remain open. Best-effort and never daemon-spawning — nothing to release against when no fresh daemon is registered.
(gateway-reorder-project-sessions! pid session-ids)Persist a project's manual session order in one gateway call. Loose named sessions are adopted atomically; guests owned by another project are not moved.
Persist a project's manual session order in one gateway call. Loose named sessions are adopted atomically; guests owned by another project are not moved.
(gateway-resource-logs sid rid)Captured output lines for a background via its daemon-side logs-fn, or nil.
Captured output lines for a background via its daemon-side logs-fn, or nil.
(gateway-router-diagnostics)The WHOLE provider dialog in ONE gateway call.
GET /v1/router already carries every provider's status and limits, so a
client that wants both for N providers reads it once instead of firing 2×N
per-provider probes. Keyed by provider-id keyword:
{:openai {:status {"is_authenticated" …} :limits {…}}} — :status stays
VERBATIM snake_case strings (same shape provider-status returns) and
:limits is restored to the engine shape provider-limits returns, so both
values drop straight into the callers those two functions already have.
The WHOLE provider dialog in ONE gateway call.
`GET /v1/router` already carries every provider's `status` and `limits`, so a
client that wants both for N providers reads it once instead of firing 2×N
per-provider probes. Keyed by provider-id keyword:
`{:openai {:status {"is_authenticated" …} :limits {…}}}` — `:status` stays
VERBATIM snake_case strings (same shape `provider-status` returns) and
`:limits` is restored to the engine shape `provider-limits` returns, so both
values drop straight into the callers those two functions already have.(gateway-router-fleet)GET /v1/router — the unified router dialog payload assembled by the gateway:
{"providers" [{"id" … "label" … "base_url" … "models" [...] "status" {"is_authenticated" …} "limits" {…}} …]}. Returned VERBATIM with
snake_case STRING keys — NO keyword restoration. Consumers read the string
keys directly ((get status "is_authenticated")).
GET /v1/router — the unified router dialog payload assembled by the gateway:
`{"providers" [{"id" … "label" … "base_url" … "models" [...]
"status" {"is_authenticated" …} "limits" {…}} …]}`. Returned VERBATIM with
snake_case STRING keys — NO keyword restoration. Consumers read the string
keys directly (`(get status "is_authenticated")`).(gateway-search-session-ids query)GET /v1/sessions/actions/search?q= — soul-id STRINGS whose transcript (user request +
assistant text) matches query. Blank query → []. The heavy assistant text
never crosses the wire; callers union these ids into a local title filter.
GET /v1/sessions/actions/search?q= — soul-id STRINGS whose transcript (user request + assistant text) matches `query`. Blank query → []. The heavy assistant text never crosses the wire; callers union these ids into a local title filter.
(gateway-search-session-matches query)GET /v1/sessions/actions/search?q= — like search-session-ids but each hit is
TAGGED with WHERE it matched, RANKED by the server, and carries up to a handful
of snippets:
[{:id str :rank 0-3 :in-title? bool :in-request? bool :in-reply? bool :in-thinking? bool :request-snippet str :reply-snippet str :hits [{:side :request|:reply|:thinking :snippet str :at ms}]}].
:in-title? = the session's own name matched; :in-request? = the user's own
request; :in-reply? = the assistant's answer; :in-thinking? = only its
reasoning aside. The vector arrives in the gateway's own order — running
sessions first, then FRESHEST first, the same order its session list is in —
and is painted in it; :rank (0 best) says WHERE the query hit and a surface
never re-orders. Blank query → []. Heavy assistant text never crosses the
wire.
GET /v1/sessions/actions/search?q= — like `search-session-ids` but each hit is
TAGGED with WHERE it matched, RANKED by the server, and carries up to a handful
of snippets:
`[{:id str :rank 0-3 :in-title? bool :in-request? bool :in-reply? bool
:in-thinking? bool :request-snippet str :reply-snippet str
:hits [{:side :request|:reply|:thinking :snippet str :at ms}]}]`.
`:in-title?` = the session's own name matched; `:in-request?` = the user's own
request; `:in-reply?` = the assistant's answer; `:in-thinking?` = only its
reasoning aside. The vector arrives in the gateway's own order — running
sessions first, then FRESHEST first, the same order its session list is in —
and is painted in it; `:rank` (0 best) says WHERE the query hit and a surface
never re-orders. Blank query → []. Heavy assistant text never crosses the
wire.(gateway-session-artifacts sid)Every durable artifact sid has produced, string-keyed and in gateway order,
or nil when the daemon cannot answer. nil is UNAVAILABLE — a channel must
paint it differently from an index that is genuinely empty.
Every durable artifact `sid` has produced, string-keyed and in gateway order, or nil when the daemon cannot answer. nil is UNAVAILABLE — a channel must paint it differently from an index that is genuinely empty.
(gateway-session-model-cached sid)Footer-frequency read of the session's model pref served from a per-sid
cache that NEVER blocks the caller (issue #29, gateway leg: this used to
be a live session-model HTTP round-trip per footer frame). A stale (or
cold) entry kicks a background single-flight refresh and this returns the
last-known value immediately (nil before the first success).
set-session-model! writes through, so a pick made in THIS client shows
on the very next frame.
Footer-frequency read of the session's model pref served from a per-sid cache that NEVER blocks the caller (issue #29, gateway leg: this used to be a live `session-model` HTTP round-trip per footer frame). A stale (or cold) entry kicks a background single-flight refresh and this returns the last-known value immediately (nil before the first success). `set-session-model!` writes through, so a pick made in THIS client shows on the very next frame.
(gateway-session-slashes sid)(gateway-session-slashes sid channel)GET the gateway-owned slash catalog for sid and channel. The first call may
initialize Python extensions in the gateway, so it uses the cold-load timeout.
GET the gateway-owned slash catalog for `sid` and `channel`. The first call may initialize Python extensions in the gateway, so it uses the cold-load timeout.
(gateway-set-router-default! provider-id model)PATCH /v1/router — tag the PRIMARY provider/model pair (the router root every
turn starts on). Returns {:provider-id … :model …}.
PATCH /v1/router — tag the PRIMARY provider/model pair (the router root every
turn starts on). Returns `{:provider-id … :model …}`.(gateway-set-router-fallback!)(gateway-set-router-fallback! provider-id model)PATCH /v1/router — tag the FALLBACK provider/model pair: the router's second
root, on a provider the primary does NOT use (the daemon refuses the primary's
own with a 400). Zero args, or a nil provider, CLEARS the tag. Returns the
resulting {:provider-id … :model …}, or nil once cleared.
PATCH /v1/router — tag the FALLBACK provider/model pair: the router's second
root, on a provider the primary does NOT use (the daemon refuses the primary's
own with a 400). Zero args, or a nil provider, CLEARS the tag. Returns the
resulting `{:provider-id … :model …}`, or nil once cleared.(gateway-set-session-model! sid provider model)PATCH the session's model pref in the daemon. Writes the returned pref
straight through into the session-model-cached snapshot so the footer
chip flips on the very next frame instead of waiting out the cache TTL.
PATCH the session's model pref in the daemon. Writes the returned pref straight through into the `session-model-cached` snapshot so the footer chip flips on the very next frame instead of waiting out the cache TTL.
(gateway-start!)(gateway-start! {:keys [port host token-file require-token? db managed?]})Start the gateway on the Jetty 12 core adapter with virtual threads.
Returns {:port :host :token-file}. Throws when already running.
Safe to call from any host process - the daemon (vis-agent gateway start), a TUI
run, or an embedded caller.
Start the gateway on the Jetty 12 core adapter with virtual threads.
Returns `{:port :host :token-file}`. Throws when already running.
Safe to call from any host process - the daemon (`vis-agent gateway start`), a TUI
run, or an embedded caller.(gateway-stop!)Stop the gateway server if running. Idempotent.
Stop the gateway server if running. Idempotent.
(gateway-stop-resource! sid rid)Run the resource's stop-fn in the daemon and unregister it. Returns the
daemon's stop result map ({:result "stopped"|"unknown"|… :id …}).
Run the resource's stop-fn in the daemon and unregister it. Returns the
daemon's stop result map (`{:result "stopped"|"unknown"|… :id …}`).(gateway-synthesize-speech! sid text {:keys [engine-id voice-id on-progress]})Ask the gateway-owned speech engine to synthesize text and return a temporary
WAV file owned by the caller. Progress is streamed for asynchronous jobs. A nil sid needs no conversation.
Ask the gateway-owned speech engine to synthesize `text` and return a temporary WAV file owned by the caller. Progress is streamed for asynchronous jobs. A nil `sid` needs no conversation.
(gateway-toggle-setting! id)Atomically flip one boolean setting in the gateway and return its refreshed string-keyed settings row. The gateway owns both persistence and live runtime fan-out; clients must not mutate a process-local toggle registry instead.
Atomically flip one boolean setting in the gateway and return its refreshed string-keyed settings row. The gateway owns both persistence and live runtime fan-out; clients must not mutate a process-local toggle registry instead.
(gateway-transcribe-audio! sid audio-path {:keys [engine-id on-progress]})Upload a WAV to the gateway-owned transcription engine, stream its progress,
and return the transcript. audio-path is read by this client only; Sherpa and
its model live solely in the gateway process. A nil sid needs no conversation.
Upload a WAV to the gateway-owned transcription engine, stream its progress, and return the transcript. `audio-path` is read by this client only; Sherpa and its model live solely in the gateway process. A nil `sid` needs no conversation.
(gateway-transcript sid)Every turn of sid, hydrated. UNBOUNDED — the whole session is listed AND
hydrated, which on a long session is seconds of work and megabytes of JSON.
Prefer transcript-page for anything interactive.
Every turn of `sid`, hydrated. UNBOUNDED — the whole session is listed AND hydrated, which on a long session is seconds of work and megabytes of JSON. Prefer `transcript-page` for anything interactive.
(gateway-transcript-html sid)The gateway-rendered STANDALONE HTML transcript for sid — the canonical
transcript->html, the HTML sibling of transcript-md. Returns the string,
or nil on a non-2xx.
The gateway-rendered STANDALONE HTML transcript for `sid` — the canonical `transcript->html`, the HTML sibling of `transcript-md`. Returns the string, or nil on a non-2xx.
(gateway-transcript-md sid)The gateway-rendered user/assistant dialog Markdown for sid — the canonical
transcript->md :dialog. Returns the string, or nil on a non-2xx.
The gateway-rendered user/assistant dialog Markdown for `sid` — the canonical `transcript->md :dialog`. Returns the string, or nil on a non-2xx.
(gateway-transcript-page sid {:keys [limit offset]})A WINDOW of sid's transcript — the paging counterpart of transcript.
opts: :limit window size (nil = the whole transcript), :offset 0-based
start in the OLDEST-FIRST list (nil = the NEWEST :limit turns). The gateway
also caps a window in BYTES, so the reply's offset can come back HIGHER
than the one asked for — page from the RETURNED offset, never from your own
arithmetic.
Returns the canonical wire map {"turns" [...] "total" n "offset" n "has_more" bool} (oldest-first turns).
A WINDOW of `sid`'s transcript — the paging counterpart of `transcript`.
`opts`: `:limit` window size (nil = the whole transcript), `:offset` 0-based
start in the OLDEST-FIRST list (nil = the NEWEST `:limit` turns). The gateway
also caps a window in BYTES, so the reply's `offset` can come back HIGHER
than the one asked for — page from the RETURNED `offset`, never from your own
arithmetic.
Returns the canonical wire map `{"turns" [...] "total" n "offset" n
"has_more" bool}` (oldest-first turns).(gateway-turn-trace sid tid)Canonical wire iterations of ONE persisted turn (nil when the id is unknown to the daemon).
Canonical wire iterations of ONE persisted turn (nil when the id is unknown to the daemon).
(gateway-unregister-contributed-sse! stream-id)Remove a contributed SSE connection and re-run managed-idle shutdown policy.
Remove a contributed SSE connection and re-run managed-idle shutdown policy.
(gateway-view-action! sid view-id action)Apply one operator action to the DAEMON-side View view-id of sid.
action is the closed View map: {:action :submit :values …},
{:action :cancel}, {:action :select :node-id … :item-ids …}, or
{:action :interrupt :note …}. Kind is resolved by the daemon from the View,
never encoded into this route. Returns the engine's canonical action outcome.
Apply one operator action to the DAEMON-side View `view-id` of `sid`.
`action` is the closed View map: `{:action :submit :values …}`,
`{:action :cancel}`, `{:action :select :node-id … :item-ids …}`, or
`{:action :interrupt :note …}`. Kind is resolved by the daemon from the View,
never encoded into this route. Returns the engine's canonical action outcome.(get-locals _environment)User-defined sandbox vars surface. Live-vars introspection is cosmetic-off for the Python engine (the agent uses its own Python scope + stdlib), so this returns an empty map. Kept as a stable seam for the trailer/renderer callers.
User-defined sandbox vars surface. Live-vars introspection is cosmetic-off for the Python engine (the agent uses its own Python scope + stdlib), so this returns an empty map. Kept as a stable seam for the trailer/renderer callers.
(get-resource session id)DATA map for session+id, or nil.
DATA map for `session`+`id`, or nil.
(get-router)Get or create the shared LLM router.
Honors :router opts from ~/.vis/config.edn (:rate-limit,
:network, :budget, ...). Without that block svar's built-in
defaults apply. See config/router-opts for the supported keys.
Get or create the shared LLM router. Honors `:router` opts from `~/.vis/config.edn` (`:rate-limit`, `:network`, `:budget`, ...). Without that block svar's built-in defaults apply. See `config/router-opts` for the supported keys.
(humanize-fact-key k)Human-facing label for a fact/entity key. A turn_<N> key reads as
Turn <N> for DISPLAY. Every other key is shown with
underscores/hyphens normalized to SPACES and the first letter capitalized
(api_key -> Api key, clj_eval_render -> Clj eval render).
DISPLAY ONLY — the stored key stays verbatim, so restore still
matches. Canonical across the context panel and every channel (TUI, web).
Fact/entity keys are model-authored strings (strings-only boundary),
so (str k) is total here — no keyword branch.
Human-facing label for a fact/entity key. A `turn_<N>` key reads as `Turn <N>` for DISPLAY. Every other key is shown with underscores/hyphens normalized to SPACES and the first letter capitalized (`api_key` -> `Api key`, `clj_eval_render` -> `Clj eval render`). DISPLAY ONLY — the stored key stays verbatim, so restore still matches. Canonical across the context panel and every channel (TUI, web). Fact/entity keys are model-authored strings (strings-only boundary), so `(str k)` is total here — no keyword branch.
(init!)Redirect System/out and System/err to the log file. Lanterna uses tty-in / tty-out for terminal I/O. Call from the TUI entry point.
Redirect System/out and System/err to the log file. Lanterna uses tty-in / tty-out for terminal I/O. Call from the TUI entry point.
(init-cli!)Logging init for non-TUI processes. Same redirects as init! but without the shutdown hook (CLI commands run to completion and exit).
Logging init for non-TUI processes. Same redirects as init! but without the shutdown hook (CLI commands run to completion and exit).
(install-extension! environment ext)Register a validated extension into environment (per-env registration,
distinct from the global-registry register-extension! defined earlier
in this file).
If an extension with the same :ext/name is already registered,
it is replaced (not duplicated). Enables hot-swap via
reload-extension! (removed for GraalVM native-image compatibility).
Returns environment for chaining.
Register a validated extension into `environment` (per-env registration, distinct from the global-registry `register-extension!` defined earlier in this file). If an extension with the same `:ext/name` is already registered, it is replaced (not duplicated). Enables hot-swap via `reload-extension!` (removed for GraalVM native-image compatibility). Returns `environment` for chaining.
(limits-compact-cells rows)Compact a group of rows into
{:prefix <shared plan name or nil> :cells [{:row <row> :text "5h 100%"}]}.
When every row's label ends in a window suffix and they all carry the SAME
plan name, that name is hoisted OUT of the cells into :prefix, so a
three-window plan reads "OpenCode Go 5h 100% · 7d 100% · 30d 99%" instead
of spelling "OpenCode Go" three times and "left" three times. Otherwise
:prefix is nil and each cell keeps its own label.
Channels render the pieces themselves (the TUI footer joins with / and
stamps a reset on one cell) but never re-derive them.
Compact a group of rows into
`{:prefix <shared plan name or nil> :cells [{:row <row> :text "5h 100%"}]}`.
When every row's label ends in a window suffix and they all carry the SAME
plan name, that name is hoisted OUT of the cells into `:prefix`, so a
three-window plan reads "OpenCode Go 5h 100% · 7d 100% · 30d 99%" instead
of spelling "OpenCode Go" three times and "left" three times. Otherwise
`:prefix` is nil and each cell keeps its own label.
Channels render the pieces themselves (the TUI footer joins with ` / ` and
stamps a reset on one cell) but never re-derive them.(limits-dynamic-summary limits)(limits-dynamic-summary limits max-rows)Compact one-line summary of the most informative :dynamic :limits
rows for a provider's normalized limits report.
Picks rows with signal first, falls back to all rows when nothing
has signal yet (so a fresh, all-zero report still surfaces SOMETHING
rather than collapsing to empty). Takes up to max-rows (default 3 — the
widest plan family shipped is 5h + 7d + 30d), compacts them through
compact-limit-cells so the plan name is written once, and joins the cells
with ·:
OpenCode Go 5h 100% · 7d 100% · 30d 99%
Returns nil when there's nothing to render.
Compact one-line summary of the most informative `:dynamic :limits` rows for a provider's normalized limits report. Picks rows with signal first, falls back to all rows when nothing has signal yet (so a fresh, all-zero report still surfaces SOMETHING rather than collapsing to empty). Takes up to `max-rows` (default 3 — the widest plan family shipped is 5h + 7d + 30d), compacts them through `compact-limit-cells` so the plan name is written once, and joins the cells with ` · `: OpenCode Go 5h 100% · 7d 100% · 30d 99% Returns nil when there's nothing to render.
(limits-format-number n)Render a numeric usage/limit/remaining value with a single-decimal
suffix when the value is non-integral, else as a clean integer.
Locale/ROOT keeps the JVM locale from injecting a comma decimal
separator next to English suffix text.
Render a numeric usage/limit/remaining value with a single-decimal suffix when the value is non-integral, else as a clean integer. `Locale/ROOT` keeps the JVM locale from injecting a comma decimal separator next to English suffix text.
(limits-format-usage {:keys [used limit remaining is-unlimited] :as row})Render the usage/remaining portion of a row as a short string, choosing the most informative shape the row's numbers allow:
is-unlimited flag -> "unlimited"Returns nil only when the row carries no usage signal at all, so
callers can (when usage ...) to skip empty cells.
Render the usage/remaining portion of a row as a short string, choosing the most informative shape the row's numbers allow: - explicit `is-unlimited` flag -> "unlimited" - percentage-style row -> "47% left" - used + limit + remaining -> "3/5 used (2 left)" - used + limit -> "3/5 used" - remaining + limit -> "2/5 left" - remaining only -> "2 left" - used only -> "3 used" - none of the above -> nil Returns nil only when the row carries no usage signal at all, so callers can `(when usage ...)` to skip empty cells.
(limits-generic-label row)Human label for a dynamic-limit row. Hand-rolled overrides for the
widely-known plan rows; fallback derives a label from :label or
:id, trimming the redundant Quota / Quota (%) suffixes the
raw provider rows ship with. :id is coerced via ->kw so the
overrides match whether the report came in-process (keyword ids) or
across the gateway wire (string ids).
Human label for a dynamic-limit row. Hand-rolled overrides for the widely-known plan rows; fallback derives a label from `:label` or `:id`, trimming the redundant ` Quota` / ` Quota (%)` suffixes the raw provider rows ship with. `:id` is coerced via `->kw` so the overrides match whether the report came in-process (keyword ids) or across the gateway wire (string ids).
(limits-label+usage row)Compact cell for a SINGLE row — its own short label plus its compact usage
("Codex 5h 47%"), or the label alone when the row carries no usage signal.
generic-limit-label bottoms out at "Limit", so a row always renders.
Compact cell for a SINGLE row — its own short label plus its compact usage
("Codex 5h 47%"), or the label alone when the row carries no usage signal.
`generic-limit-label` bottoms out at "Limit", so a row always renders.(limits-percentage-row? {:keys [id kind limit remaining]})True when the row is best displayed as a percent-remaining (the
provider reports a 0-100 percentage rather than raw token counts).
The ID allowlist covers the Codex / Z.ai plan windows; the
:rate + :limit 100 heuristic catches generic percentage rows
(the Anthropic Claude windows). id/kind are coerced via ->kw
so a report that crossed the gateway wire (string values) matches
the same as an in-process one (keyword values).
True when the row is best displayed as a percent-remaining (the provider reports a 0-100 percentage rather than raw token counts). The ID allowlist covers the Codex / Z.ai plan windows; the `:rate` + `:limit 100` heuristic catches generic percentage rows (the Anthropic Claude windows). `id`/`kind` are coerced via `->kw` so a report that crossed the gateway wire (string values) matches the same as an in-process one (keyword values).
(limits-prioritize-rows rows)Stable reorder of limit rows: rolling plan windows first, SHORTEST window
leading (5h before 7d, never the other way round because the weekly bucket
happens to be tighter today), then everything else by limit-row-pressure
so whatever is blocking leads any truncated rendering.
Stable reorder of limit rows: rolling plan windows first, SHORTEST window leading (5h before 7d, never the other way round because the weekly bucket happens to be tighter today), then everything else by `limit-row-pressure` so whatever is blocking leads any truncated rendering.
(limits-row-exhausted? {:keys [is-unlimited remaining limit used]})True when a metered row has nothing left: not unlimited, a numeric
:remaining at (or below) zero, and enough context to know that the
zero is a WALL — either a positive :limit or positive :used.
A brand new all-zero row ({:remaining 0 :limit 0}) is NOT exhausted;
it simply has not been filled in yet.
True when a metered row has nothing left: not unlimited, a numeric
`:remaining` at (or below) zero, and enough context to know that the
zero is a WALL — either a positive `:limit` or positive `:used`.
A brand new all-zero row (`{:remaining 0 :limit 0}`) is NOT exhausted;
it simply has not been filled in yet.(limits-row-has-signal? row)True when the row has usage or reset signal worth surfacing. Used to prefer informative rows when the visible area is tight. A reset timestamp is signal even when the provider reports zero remaining and omits a limit: that's exactly when the user needs to know when credits come back.
True when the row has usage or reset signal worth surfacing. Used to prefer informative rows when the visible area is tight. A reset timestamp is signal even when the provider reports zero remaining and omits a limit: that's exactly when the user needs to know when credits come back.
(limits-row-pressure {:keys [is-unlimited remaining limit] :as row})Sort key ranking a row by how much it constrains the user RIGHT NOW:
:is-unlimited)Without this, a provider that reports its unlimited buckets first
(GitHub Copilot lists chat, completions, then premium_interactions)
summarises as "Chat unlimited · Completions unlimited" while the ONE
bucket that actually rejects requests sits silently at 0 remaining.
Sort key ranking a row by how much it constrains the user RIGHT NOW: 0. exhausted metered rows (0 left, requests are being rejected) 1. metered rows, tightest remaining fraction first 2. rows with no tank at all (`:is-unlimited`) Without this, a provider that reports its unlimited buckets first (GitHub Copilot lists `chat`, `completions`, then `premium_interactions`) summarises as "Chat unlimited · Completions unlimited" while the ONE bucket that actually rejects requests sits silently at 0 remaining.
(list-resources session)Vector of live resource DATA maps for session, dead ones pruned and every
health-capable status refreshed first (parallel, hard-timeout probes).
This is what the footer renders and what repl_status answers from — ONLY
the calling session's resources. Nothing about a resource rides in ctx.
Vector of live resource DATA maps for `session`, dead ones pruned and every health-capable `status` refreshed first (parallel, hard-timeout probes). This is what the footer renders and what `repl_status` answers from — ONLY the calling session's resources. Nothing about a resource rides in ctx.
(llm-text! {:keys [messages system prompt reasoning temperature routing]
:as opts})Fast helper LLM call for extensions.
Uses svar routing (:routing {:optimize :cost}) instead of Vis-side model
name heuristics. The call still goes through svar/ask-code! because Vis no
longer uses the retired ask! structured-output path; :lang "text",
:reasoning :off, and :code-tail-pointer? true make the return a plain
text string under :text. Callers may pass either :messages or :system +
:prompt.
Fast helper LLM call for extensions.
Uses svar routing (`:routing {:optimize :cost}`) instead of Vis-side model
name heuristics. The call still goes through `svar/ask-code!` because Vis no
longer uses the retired `ask!` structured-output path; `:lang "text"`,
`:reasoning :off`, and `:code-tail-pointer? true` make the return a plain
text string under :text. Callers may pass either :messages or :system +
:prompt.(load-config)(load-config require-providers?)Load the validated YAML config and adapt its finite schema keys to internal
keyword-keyed domain maps. load-config-raw retains the original string keys.
This is also the ${NAME} interpolation boundary. It is done HERE and not in
load-config-raw on purpose: the raw loaders are the read half of every
read-modify-write into ~/.vis/state.yml, so resolving there would write the
plaintext secret straight back to disk. save-config! runs
restore-env-refs as the matching guard for values that still reach a write
through this keywordized view. Pass false for require-providers? when the
caller resolves runtime-only providers separately; preserve its settings even
without a persisted provider fleet.
Load the validated YAML config and adapt its finite schema keys to internal
keyword-keyed domain maps. `load-config-raw` retains the original string keys.
This is also the `${NAME}` interpolation boundary. It is done HERE and not in
`load-config-raw` on purpose: the raw loaders are the read half of every
read-modify-write into `~/.vis/state.yml`, so resolving there would write the
plaintext secret straight back to disk. `save-config!` runs
`restore-env-refs` as the matching guard for values that still reach a write
through this keywordized view. Pass false for `require-providers?` when the
caller resolves runtime-only providers separately; preserve its settings even
without a persisted provider fleet.(load-config-raw)Load raw config as the deep-merge of four YAML sources — later sources win, nested maps merge, scalar/vector values replace:
~/.vis/config.yml (or .yaml / vis.yml / vis.yaml) — hand-written
global base~/.vis/state.yml — machine-written global store (OAuth tokens, TUI-added
providers); wins over the hand-written base<workspace>/vis.yml (or vis.yaml) — visible project root, the committed
team config<workspace>/.vis/config.yml (or .yaml) — hidden project overlay; the
nested overlay wins over the root file (personal beats committed)Project tiers follow workspace/cwd, bound to the session's workspace, not
the gateway launch directory. Unbound CLI/bootstrap callers use invocation cwd.
Memoized against source paths and their mtime+size (see config-raw-cache).
Load raw config as the deep-merge of four YAML sources — later sources win, nested maps merge, scalar/vector values replace: 1. `~/.vis/config.yml` (or `.yaml` / `vis.yml` / `vis.yaml`) — hand-written global base 2. `~/.vis/state.yml` — machine-written global store (OAuth tokens, TUI-added providers); wins over the hand-written base 3. `<workspace>/vis.yml` (or `vis.yaml`) — visible project root, the committed team config 4. `<workspace>/.vis/config.yml` (or `.yaml`) — hidden project overlay; the nested overlay wins over the root file (personal beats committed) Project tiers follow `workspace/cwd`, bound to the session's workspace, not the gateway launch directory. Unbound CLI/bootstrap callers use invocation cwd. Memoized against source paths and their mtime+size (see `config-raw-cache`).
(load-python-extensions!)(load-python-extensions! {:keys [dirs sync-projects?]})Scan the Python extension dirs (default: ~/.vis/extensions and
<cwd>/.vis/extensions) and (re)load every *.py file. Idempotent:
when no file changed since the last scan this is a cheap no-op. On any
change the whole set is torn down and rebuilt (contexts are ~40ms warm
on the shared engine) — deterministic ordering, no partial states.
Change is measured over each extension's WHOLE import root, never its entry
file alone — a package module the entry imports is part of the extension —
and every root is FROZEN at load (freeze-root!), one freeze per root per
pass.
A file that fails to load is recorded in load-failures (and surfaced
by vis-agent doctor) — it never crashes the host.
Returns {:loaded n :failed n :changed? bool}.
Scan the Python extension dirs (default: `~/.vis/extensions` and
`<cwd>/.vis/extensions`) and (re)load every `*.py` file. Idempotent:
when no file changed since the last scan this is a cheap no-op. On any
change the whole set is torn down and rebuilt (contexts are ~40ms warm
on the shared engine) — deterministic ordering, no partial states.
Change is measured over each extension's WHOLE import root, never its entry
file alone — a package module the entry imports is part of the extension —
and every root is FROZEN at load (`freeze-root!`), one freeze per root per
pass.
A file that fails to load is recorded in `load-failures` (and surfaced
by `vis-agent doctor`) — it never crashes the host.
Returns `{:loaded n :failed n :changed? bool}`.(loaded-python-extensions)Snapshot of the currently loaded Python extensions:
{<canonical-path> {:sha ... :ext-name ...}} (context handle elided).
Snapshot of the currently loaded Python extensions:
`{<canonical-path> {:sha ... :ext-name ...}} ` (context handle elided).(make-progress-tracker)(make-progress-tracker {:keys [on-update]})Create a phased progress tracker.
Returns {:on-chunk fn :get-timeline fn}. The :on-chunk fn
accepts the loop's phased chunks and returns nil. The
:get-timeline fn returns the accumulated timeline vec
(oldest-iteration first).
on-update (when supplied) is called (on-update timeline chunk)
after every chunk update, so the consumer can re-render
incrementally.
Create a phased progress tracker.
Returns `{:on-chunk fn :get-timeline fn}`. The `:on-chunk` fn
accepts the loop's phased chunks and returns nil. The
`:get-timeline` fn returns the accumulated timeline vec
(oldest-iteration first).
`on-update` (when supplied) is called `(on-update timeline chunk)`
after every chunk update, so the consumer can re-render
incrementally.(manifest-initializers)The qualified symbol of every initializer, in manifest order.
The qualified symbol of every initializer, in manifest order.
(markdown->ast text)(markdown->ast text {:keys [soft-break]})Parse a Markdown string into canonical transient Markdown tree.
Idempotent: when the input is already canonical IR, returns it
unchanged (identical? preserved — cache-friendly).
This is the SINGLE entry point for turning Markdown source into IR. Used by:
Returns canonical [:ast & blocks] directly (no further ->ast
round-trip needed). Empty / nil input yields [:ast {}].
Implementation: commonmark-java parser + GFM tables / strikethrough
extensions, then a faithful Node→IR walker. Soft line breaks collapse
to a single space; hard line breaks become [:br].
opts (2-arity) currently understands {:soft-break :hard}, which
lifts every bare newline to [:br] instead of a space — used for
line-oriented user/pasted input so the rendered bubble keeps the
exact line structure the user typed. Default keeps prose semantics.
Parse a Markdown string into canonical transient Markdown tree.
Idempotent: when the input is already canonical IR, returns it
unchanged (`identical?` preserved — cache-friendly).
This is the SINGLE entry point for turning Markdown source into IR.
Used by:
- the final-answer pipeline (model's plain-prose Markdown answer)
- thinking text from the model
- user-typed messages from the TUI input box
Returns canonical `[:ast & blocks]` directly (no further `->ast`
round-trip needed). Empty / nil input yields `[:ast {}]`.
Implementation: commonmark-java parser + GFM tables / strikethrough
extensions, then a faithful Node→IR walker. Soft line breaks collapse
to a single space; hard line breaks become `[:br]`.
`opts` (2-arity) currently understands `{:soft-break :hard}`, which
lifts every bare newline to `[:br]` instead of a space — used for
line-oriented user/pasted input so the rendered bubble keeps the
exact line structure the user typed. Default keeps prose semantics.(meta-cost cost)Humanized dollar cost — "~$0.0070" / "~$1.23". nil for zero / missing. Extra decimals for sub-cent turns so they don't round down to "$0".
Humanized dollar cost — "~$0.0070" / "~$1.23". nil for zero / missing. Extra decimals for sub-cent turns so they don't round down to "$0".
(meta-fallback-note {:keys [llm-selected llm-fallback? llm-routing-trace]})Faint routing note when a turn fell back or retried a provider:
↳ from <selected-model> — <reason>, retried N×, prompt cache lost, session now on <model>
reason prefers the HTTP status (429) on a fallback event, then the reason
keyword, then the free-form error. Retry-only traces use their final retry
event, so a provider failure is never rendered as merely retried N×.
A :scope :session-pick event means the rescue also MOVED the session's pick, so
the model chip in every surface now names a provider the human never chose: the
note is the one line that says why it changed (issue #154). It is never the
anchor for from, which stays this turn's own route.
A route that CHANGED provider or model also lost prompt-cache continuity: the peer never saw the cache the pinned route built, so every following request re-sends the whole context. That silent 4× cost step is the reported half of issue #154 the numbers never showed, so the note says it in words on the turn it happens.
A :llm.routing/model-fallback moved the MODEL inside one provider — Anthropic's
safety classifier declining this request is the switch Vis raises that way. Nothing
about the credential or the provider failed, so it reads as its own reason and never
as a retry, but the cache is gone all the same: an Anthropic cache belongs to ONE
model.
Returns nil when the trace has neither a fallback nor retries. Shared so the
TUI can float it on its own faint row while the CLI folds it inline.
Faint routing note when a turn fell back or retried a provider: ↳ from <selected-model> — <reason>, retried N×, prompt cache lost, session now on <model> `reason` prefers the HTTP status (429) on a fallback event, then the reason keyword, then the free-form error. Retry-only traces use their final retry event, so a provider failure is never rendered as merely `retried N×`. A `:scope :session-pick` event means the rescue also MOVED the session's pick, so the model chip in every surface now names a provider the human never chose: the note is the one line that says why it changed (issue #154). It is never the anchor for `from`, which stays this turn's own route. A route that CHANGED provider or model also lost prompt-cache continuity: the peer never saw the cache the pinned route built, so every following request re-sends the whole context. That silent 4× cost step is the reported half of issue #154 the numbers never showed, so the note says it in words on the turn it happens. A `:llm.routing/model-fallback` moved the MODEL inside one provider — Anthropic's safety classifier declining this request is the switch Vis raises that way. Nothing about the credential or the provider failed, so it reads as its own reason and never as a retry, but the cache is gone all the same: an Anthropic cache belongs to ONE model. Returns nil when the trace has neither a fallback nor retries. Shared so the TUI can float it on its own faint row while the CLI folds it inline.
(meta-summary-line result)(meta-summary-line {:keys [tokens cost duration-ms] :as result}
{:keys [model prefix suffix]})The canonical, humanized turn-summary MAIN line, shared verbatim by the CLI bracket and the TUI bubble footer:
<provider/model> · <in→out (cached)> · ~$cost · <duration>
Zero-usage and zero-cost slots are dropped (no "0→0", no "$0"), so a turn
that produced nothing reads as just the model + time. Does NOT include the
fallback note — that is meta-fallback-note, which single-line surfaces fold
in via format-meta-line and the TUI floats on a second row.
opts keeps the legacy override hooks: {:model <string|false> :prefix [...] :suffix [...]} — :model false suppresses the model slot, a string overrides
it; prefix/suffix are extra slots spliced in around the standard ones.
The canonical, humanized turn-summary MAIN line, shared verbatim by the CLI
bracket and the TUI bubble footer:
<provider/model> · <in→out (cached)> · ~$cost · <duration>
Zero-usage and zero-cost slots are dropped (no "0→0", no "$0"), so a turn
that produced nothing reads as just the model + time. Does NOT include the
fallback note — that is `meta-fallback-note`, which single-line surfaces fold
in via `format-meta-line` and the TUI floats on a second row.
`opts` keeps the legacy override hooks: `{:model <string|false> :prefix [...]
:suffix [...]}` — `:model false` suppresses the model slot, a string overrides
it; prefix/suffix are extra slots spliced in around the standard ones.(meta-tokens tokens)Humanized token slot — "11.5k→35", with " (cached 4.1k)" only when the cached-input count is positive. Returns nil for a ZERO-usage turn (no input AND no output) so a failed / empty provider call never renders a bare "0→0".
Humanized token slot — "11.5k→35", with " (cached 4.1k)" only when the cached-input count is positive. Returns nil for a ZERO-usage turn (no input AND no output) so a failed / empty provider call never renders a bare "0→0".
(model-name model)Extract the model name string from a model (string or {:name str}).
Extract the model name string from a model (string or `{:name str}`).
(model-pricing model)Per-model price table entry (USD per MILLION tokens) for model, looked up
by exact model name in svar's MODEL_PRICING — {:input :output :cache-read :cached-input …} — or nil when the model isn't priced. Read-only view over
the same table estimate-token-cost bills against, so channel pickers show
the price that actually gets charged.
Per-model price table entry (USD per MILLION tokens) for `model`, looked up
by exact model name in svar's `MODEL_PRICING` — `{:input :output :cache-read
:cached-input …}` — or nil when the model isn't priced. Read-only view over
the same table `estimate-token-cost` bills against, so channel pickers show
the price that actually gets charged.(model-routing-status displayed-provider displayed-model)(model-routing-status router displayed-provider displayed-model)Live routing health for the model a channel is DISPLAYING (displayed-provider
displayed-model — the per-session pick or the config default the picker
shows).svar opens a circuit breaker on a provider after repeated transient failures
(5xx / 'Overloaded' 529 / dropped streams) and routes turns to the next
AVAILABLE provider so work keeps flowing. The displayed model is computed
from config ORDER and is NOT breaker-aware, so during an outage the picker
says opus while turns actually land on zai. This reconciles the two: when
the displayed provider's breaker is open/half-open, it reports what svar is
actually serving so the channel can surface
⚠ <displayed> overloaded — routing to <serving>.
Returns nil when the displayed provider is healthy, else
{:overloaded-provider <kw> :overloaded-model <str> :serving-provider <kw> :serving-model <str>}. serving-* is nil if every
provider is down.
Live routing health for the model a channel is DISPLAYING (`displayed-provider`
+ `displayed-model` — the per-session pick or the config default the picker
shows).
svar opens a circuit breaker on a provider after repeated transient failures
(5xx / 'Overloaded' 529 / dropped streams) and routes turns to the next
AVAILABLE provider so work keeps flowing. The displayed model is computed
from config ORDER and is NOT breaker-aware, so during an outage the picker
says `opus` while turns actually land on `zai`. This reconciles the two: when
the displayed provider's breaker is open/half-open, it reports what svar is
actually serving so the channel can surface
`⚠ <displayed> overloaded — routing to <serving>`.
Returns nil when the displayed provider is healthy, else
`{:overloaded-provider <kw> :overloaded-model <str>
:serving-provider <kw> :serving-model <str>}`. `serving-*` is nil if every
provider is down.(non-blank x)x trimmed to a string, or nil when it is nil, empty or all whitespace.
`x` trimmed to a string, or nil when it is nil, empty or all whitespace.
(normalize-reasoning text)Canonical normalization for model reasoning / thinking text before it is
rendered as a trace. Reasoning streams carry whitespace-padded blank rows
(trailing spaces/tabs the model emits) and paragraph-style double newlines
that make the compact thinking block look ragged (glm-5.2 especially). Strip
per-line trailing whitespace, collapse every run of newlines down to ONE line
break, then give the trace BREATHING ROOM: a line that ENDS A SENTENCE
(./!/?/…, optionally closed by a quote/paren/bracket) and is
followed by more text gets a blank line after it, so consecutive sentences
read as separate paragraphs instead of a wall. Finally trim. Shared by every
channel so the TUI bubble and the web thinking card normalize identically.
Canonical normalization for model reasoning / thinking text before it is rendered as a trace. Reasoning streams carry whitespace-padded blank rows (trailing spaces/tabs the model emits) and paragraph-style double newlines that make the compact thinking block look ragged (glm-5.2 especially). Strip per-line trailing whitespace, collapse every run of newlines down to ONE line break, then give the trace BREATHING ROOM: a line that ENDS A SENTENCE (`.`/`!`/`?`/`…`, optionally closed by a quote/paren/bracket) and is followed by more text gets a blank line after it, so consecutive sentences read as separate paragraphs instead of a wall. Finally trim. Shared by every channel so the TUI bubble and the web thinking card normalize identically.
(normalize-status status)Map runtime status keywords to the schema CHECK constraint values. Allowed: running, done, error, interrupted.
Map runtime status keywords to the schema CHECK constraint values. Allowed: running, done, error, interrupted.
(notifications)Vec of currently-active notifications, oldest first. Implicitly
prunes expired entries before returning so a paint loop reading
this never has to re-check :until deadlines.
Vec of currently-active notifications, oldest first. Implicitly prunes expired entries before returning so a paint loop reading this never has to re-check `:until` deadlines.
(notify! text & {:keys [level ttl-ms] :or {level :info ttl-ms DEFAULT_TTL_MS}})Push a new notification. Returns the entry's id (uuid string) so
the caller can later dismiss! it manually.
Options:
:level one of #{:info :success :warn :error} - default :info.
Channels use the level for visual treatment (color,
emoji prefix, etc.).
:ttl-ms lifespan in ms. Default 3000. nil = sticky / no auto
expiry; the notification stays until dismiss!d.
The notification is appended to the in-memory vec and every registered watcher is fired with the new full vec.
Push a new notification. Returns the entry's id (uuid string) so
the caller can later `dismiss!` it manually.
Options:
:level one of #{:info :success :warn :error} - default :info.
Channels use the level for visual treatment (color,
emoji prefix, etc.).
:ttl-ms lifespan in ms. Default 3000. nil = sticky / no auto
expiry; the notification stays until `dismiss!`d.
The notification is appended to the in-memory vec and every
registered watcher is fired with the new full vec.(now-ms)Milliseconds since the epoch — the engine's one wall clock.
Milliseconds since the epoch — the engine's one wall clock.
(on-cancel! token thunk)Register a no-arg thunk to fire the moment cancel! is invoked
on token. Returns a dispose! thunk the caller MUST invoke when
the cancellable work finishes normally — otherwise callbacks
accumulate for the token's lifetime.
If cancel! has already fired on this token, thunk runs
synchronously here and dispose! is a no-op. This matches the
contract every consumer wants: registering AFTER cancellation
must still cancel, not silently swallow the request.
Replaces the atom-watch pattern earlier eval boundaries hand-rolled: one shared callback list, no per-consumer add-watch / remove-watch plumbing, no risk of leaving a watch on the flag after the worker finishes.
Register a no-arg `thunk` to fire the moment `cancel!` is invoked on `token`. Returns a `dispose!` thunk the caller MUST invoke when the cancellable work finishes normally — otherwise callbacks accumulate for the token's lifetime. If `cancel!` has already fired on this token, `thunk` runs synchronously here and `dispose!` is a no-op. This matches the contract every consumer wants: registering AFTER cancellation must still cancel, not silently swallow the request. Replaces the atom-watch pattern earlier eval boundaries hand-rolled: one shared callback list, no per-consumer add-watch / remove-watch plumbing, no risk of leaving a watch on the flag after the worker finishes.
(op-presentation op)Engine-owned presentation metadata for a tool's :op keyword:
{:tag ...}. Tool wrappers merge this into their :info/:metadata
so channels read canonical keys.
Badge LABEL is derived from :tag by the channel, not stored here.
Color / glyph / layout remain pure channel concerns.
Engine-owned presentation metadata for a tool's `:op` keyword:
`{:tag ...}`. Tool wrappers merge this into their `:info`/`:metadata`
so channels read canonical keys.
Badge LABEL is derived from `:tag` by the channel, not stored here.
Color / glyph / layout remain pure channel concerns.(op-tag op-keyword)Return the :observation | :mutation tag for op-keyword. Unknown
ops fail closed; every symbol must declare :tag inline on its
vis/symbol entry.
Return the `:observation | :mutation` tag for `op-keyword`. Unknown ops fail closed; every symbol must declare `:tag` inline on its `vis/symbol` entry.
Default palette. Kept as a named var for channels that only need colours.
Default palette. Kept as a named var for channels that only need colours.
(parse-args arg-specs raw-args)Parse raw-args against arg-specs. Returns a map of
{arg-name value}. Positional specs are matched in order; flag
specs by --name; boolean flags need no value. Unknown flags
are silently dropped so commands can layer their own loose flags.
Parse `raw-args` against `arg-specs`. Returns a map of
`{arg-name value}`. Positional specs are matched in order; flag
specs by `--name`; boolean flags need no value. Unknown flags
are silently dropped so commands can layer their own loose flags.(parse-block-display form-source)Return the model's authored source as ONE verbatim :code segment.
The engine is full-Python: the source is Python and we keep it VERBATIM here —
exactly what the model wrote, no splitting, no classification. This is the
canonical segment (tests + the model's own context depend on it being the raw
bytes). Channels that want a beautified view call prettify-python at paint
time; the IR stays raw.
Pure helper. Never throws. Blank / nil input returns [].
Return the model's authored source as ONE verbatim `:code` segment. The engine is full-Python: the source is Python and we keep it VERBATIM here — exactly what the model wrote, no splitting, no classification. This is the canonical segment (tests + the model's own context depend on it being the raw bytes). Channels that want a beautified view call `prettify-python` at paint time; the IR stays raw. Pure helper. Never throws. Blank / nil input returns `[]`.
(picker-fleet)The provider fleet a model picker should render: the persisted
configured-providers first, then authenticated-preset-providers
(authenticated-but-unconfigured OAuth providers whose creds live outside
config) appended. This is what channel model pickers enumerate so
authenticated providers are selectable even before they're saved into the
fleet.
Failure-isolated by construction: the base fleet reads through the
never-nil configured-providers-cached (no per-open 4-file parse, no
render-thread stall), and the authenticated-preset enumeration degrades to
empty on any error. A transient hiccup therefore drops the OAuth extras at
worst — it NEVER throws and NEVER blanks the picker of already-configured
providers.
The provider fleet a model picker should render: the persisted `configured-providers` first, then `authenticated-preset-providers` (authenticated-but-unconfigured OAuth providers whose creds live outside config) appended. This is what channel model pickers enumerate so authenticated providers are selectable even before they're saved into the fleet. Failure-isolated by construction: the base fleet reads through the never-nil `configured-providers-cached` (no per-open 4-file parse, no render-thread stall), and the authenticated-preset enumeration degrades to empty on any error. A transient hiccup therefore drops the OAuth extras at worst — it NEVER throws and NEVER blanks the picker of already-configured providers.
(prepare-session-jail! {:keys [session-id jail-policy-fn]})Bind the language surface's live session env to the managed-process contract. Missing session identity or policy fails closed before a language handler can start a REPL or project test process. Safe and idempotent per dispatch.
Bind the language surface's live session env to the managed-process contract. Missing session identity or policy fails closed before a language handler can start a REPL or project test process. Safe and idempotent per dispatch.
(provider descriptor)Build and validate a provider descriptor.
Build and validate a provider descriptor.
(provider-auth-kind pid)(provider-auth-kind pid provider)How a provider authenticates: :command (an api_key_command mints the
credential — never prompt), :oauth (the registered extension declares an
interactive :provider/auth-fn), :managed (the extension owns configuration
and exposes no interactive flow), :none (local, no credentials), or :api-key.
Ownership never overrides authentication: a managed provider with auth-fn is
OAuth-capable while remaining automatically bound and absent from Add Provider.
The 1-arity classifies by id alone and therefore can never see a command-minted
provider; pass the configured provider map when that answer decides whether to
prompt a human.
How a provider authenticates: `:command` (an `api_key_command` mints the credential — never prompt), `:oauth` (the registered extension declares an interactive `:provider/auth-fn`), `:managed` (the extension owns configuration and exposes no interactive flow), `:none` (local, no credentials), or `:api-key`. Ownership never overrides authentication: a managed provider with `auth-fn` is OAuth-capable while remaining automatically bound and absent from Add Provider. The 1-arity classifies by id alone and therefore can never see a command-minted provider; pass the configured provider map when that answer decides whether to prompt a human.
(provider-base-url provider)Resolve base-url for a provider: explicit field on the provider map first (so user-supplied URLs win), then the merged catalog.
Resolve base-url for a provider: explicit field on the provider map first (so user-supplied URLs win), then the merged catalog.
(provider-by-id id)Lookup a provider by :provider/id. Returns nil when absent.
Lookup a provider by `:provider/id`. Returns nil when absent.
(provider-command-minted? provider)True when the credential is minted BY THE MACHINE: config carries an
api_key_command, so the helper mints (and rotates) the token on every
request and there is nothing for a human to type or paste.
True when the credential is minted BY THE MACHINE: config carries an `api_key_command`, so the helper mints (and rotates) the token on every request and there is nothing for a human to type or paste.
(provider-config-with-models preset models)Persistable provider config carrying the provider's complete catalog.
Persistable provider config carrying the provider's complete catalog.
(provider-configured?)True when at least one provider is configured (global or project config).
The single predicate entry points use to branch onboarding vs normal start —
never trips the resolve-config throw.
True when at least one provider is configured (global or project config). The single predicate entry points use to branch onboarding vs normal start — never trips the `resolve-config` throw.
(provider-default-model-configs preset)Preset :default-models as persisted model maps. A bare-string entry
becomes {:name str}; a MAP entry is carried through verbatim (name
normalized) so a provider can declare :context / :output-limit / … for
a model svar's pinned catalog doesn't know yet — no svar release, no
enrich hook. ->svar-model whitelists which of those keys svar honors, so
extra keys are harmless.
Preset `:default-models` as persisted model maps. A bare-string entry
becomes `{:name str}`; a MAP entry is carried through verbatim (name
normalized) so a provider can declare `:context` / `:output-limit` / … for
a model svar's pinned catalog doesn't know yet — no svar release, no
enrich hook. `->svar-model` whitelists which of those keys svar honors, so
extra keys are harmless.(provider-default-model-names provider)Union of model names already on the provider map plus the preset /
provider :default-models, deduped. Config order leads: the models a
user wrote in vis.yml come first and model-options keeps them there.
Union of model names already on the provider map plus the preset / provider `:default-models`, deduped. Config order leads: the models a user wrote in vis.yml come first and `model-options` keeps them there.
(provider-fetch-models provider)List models for a vis provider via svar/models!.
Returns vec of chat model id strings, or nil on failure. Filters out TTS / embedding / speech / image and provider-excluded models.
Routing through svar means the call automatically picks up
provider-specific OAuth headers (anthropic-version,
anthropic-beta for the Anthropic Claude subscription;
chatgpt-account-id for OpenAI Codex; bare Bearer for everyone
else).
provider is a vis-shaped provider map. We coerce to svar shape
(resolving OAuth tokens via the provider's :provider/get-token-fn
when :api-key is absent) and ask svar.
List models for a vis provider via `svar/models!`. Returns vec of chat model id strings, or nil on failure. Filters out TTS / embedding / speech / image and provider-excluded models. Routing through svar means the call automatically picks up provider-specific OAuth headers (`anthropic-version`, `anthropic-beta` for the Anthropic Claude subscription; `chatgpt-account-id` for OpenAI Codex; bare Bearer for everyone else). `provider` is a vis-shaped provider map. We coerce to svar shape (resolving OAuth tokens via the provider's `:provider/get-token-fn` when `:api-key` is absent) and ask svar.
(provider-ids)Set of configured provider :id keywords.
Set of configured provider `:id` keywords.
(provider-initial-limits provider)Placeholder limits report while the real fetch runs.
Placeholder limits report while the real fetch runs.
(provider-initial-status provider)Placeholder status while a real probe runs in the background. A credential gap is decided synchronously — it is a pure read of the config already in hand, plus at most one cached credential-command probe — so the card never flashes an authenticated verdict it is about to retract.
Placeholder status while a real probe runs in the background. A credential gap is decided synchronously — it is a pure read of the config already in hand, plus at most one cached credential-command probe — so the card never flashes an authenticated verdict it is about to retract.
(provider-key-detect book plan-tag)Lookup priority for one plan:
:api-key for this plan.{:api-key str :source kw} or nil. Never throws.:source is :config, :env-var or :auth-file, so a status report can
tell the user WHERE the key came from.
Lookup priority for one plan:
1. TUI/config provider `:api-key` for this plan.
2. The plan's env-var chain.
3. The book's file slice for this plan.
Returns `{:api-key str :source kw}` or nil. Never throws.
`:source` is `:config`, `:env-var` or `:auth-file`, so a status report can
tell the user WHERE the key came from.(provider-key-entries book limits-fn)One :ext/providers entry per plan in the book, in plan order.
limits-fn is (fn [plan-tag] (fn [] report)) because a quota endpoint -
or the absence of one - is the vendor's own business.
One `:ext/providers` entry per plan in the book, in plan order. `limits-fn` is `(fn [plan-tag] (fn [] report))` because a quota endpoint - or the absence of one - is the vendor's own business.
(provider-limits provider-id)Return a normalized, contract-validated limits report for one provider id.
The provider's optional :provider/limits-fn supplies the dynamic
portion. This host wrapper backfills static svar metadata and always
returns a valid contract-provider/report envelope, even when the
provider-specific implementation is absent, missing, throws, or returns
malformed data.
Providers that only have static svar catalog metadata still return a
usable :ok report so callers can surface RPM / TPM without needing a
registered runtime extension.
Return a normalized, contract-validated limits report for one provider id. The provider's optional `:provider/limits-fn` supplies the dynamic portion. This host wrapper backfills static svar metadata and always returns a valid `contract-provider/report` envelope, even when the provider-specific implementation is absent, missing, throws, or returns malformed data. Providers that only have static svar catalog metadata still return a usable `:ok` report so callers can surface RPM / TPM without needing a registered runtime extension.
(provider-limits-safe provider)Normalized limits report for a provider id; an error report instead
of a throw, and never slower than limits-probe-timeout-ms.
A late probe is deliberately NOT cancelled: provider-limits memoizes the
report it is still computing, so abandoning this read leaves the next one warm.
Normalized limits report for a provider id; an error report instead of a throw, and never slower than `limits-probe-timeout-ms`. A late probe is deliberately NOT cancelled: `provider-limits` memoizes the report it is still computing, so abandoning this read leaves the next one warm.
Local OpenAI-compatible providers that need no credentials.
Local OpenAI-compatible providers that need no credentials.
(provider-managed? provider-id)True when the extension that registered provider-id owns its binding and
configuration. Managed providers bind as soon as their extension loads and stay
out of Add Provider. Authentication is independent: :provider/auth-fn may still
obtain a provider-owned credential on first use.
True when the extension that registered `provider-id` owns its binding and configuration. Managed providers bind as soon as their extension loads and stay out of Add Provider. Authentication is independent: `:provider/auth-fn` may still obtain a provider-owned credential on first use.
(provider-model-options provider)(provider-model-options provider default-models show-all?)Selectable model ids for a provider: configured models first IN vis.yml
ORDER, then live-fetched + preset defaults deduped and sorted, env
default pinned first. When show-all? is false, dated snapshot
variants (gpt-4o-2024-08-06) are hidden.
Returns {:models [id ...] :hidden-count n} - channels render
their own 'show all' affordance from :hidden-count.
Selectable model ids for a provider: configured models first IN vis.yml
ORDER, then live-fetched + preset defaults deduped and sorted, env
default pinned first. When `show-all?` is false, dated snapshot
variants (gpt-4o-2024-08-06) are hidden.
Returns `{:models [id ...] :hidden-count n}` - channels render
their own 'show all' affordance from `:hidden-count`.(provider-model-visible? provider-id model-id)True when svar's provider-scoped model filters allow this model id.
True when svar's provider-scoped model filters allow this model id.
Providers whose credentials come from an interactive OAuth flow and live OUTSIDE config.edn (keychain / token files owned by the provider extension).
Providers whose credentials come from an interactive OAuth flow and live OUTSIDE config.edn (keychain / token files owned by the provider extension).
(provider-persisted-config provider)Convert an in-memory provider entry to the durable on-disk shape.
Convert an in-memory provider entry to the durable on-disk shape.
(provider-presets)All known provider presets, sorted for the 'Add Provider' picker.
All known provider presets, sorted for the 'Add Provider' picker.
(provider-presets-available)Provider presets not yet in the configured fleet — the 'Add Provider' picker contents.
A MANAGED provider is never offered here: it carries no credential a human
supplies and it binds itself, so an Add provider row for it could only ask
for a key that every seam below must refuse.
Provider presets not yet in the configured fleet — the 'Add Provider' picker contents. A MANAGED provider is never offered here: it carries no credential a human supplies and it binds itself, so an `Add provider` row for it could only ask for a key that every seam below must refuse.
(provider-status provider)Auth/liveness status for a CONFIGURED provider map, with one explicit
:auth-state every channel paints:
:verified — a live provider check accepted the credential,:rejected — the credential/config was explicitly refused,:degraded — the credential remains usable but its live check failed,:unverified — a credential exists but no live check can prove it (or
no credential exists yet).:is-authenticated remains the independent usability bit: a degraded or
unverified entry can still route only when it is true; rejection forces it false.
Never throws.
Auth/liveness status for a CONFIGURED provider map, with one explicit `:auth-state` every channel paints: - `:verified` — a live provider check accepted the credential, - `:rejected` — the credential/config was explicitly refused, - `:degraded` — the credential remains usable but its live check failed, - `:unverified` — a credential exists but no live check can prove it (or no credential exists yet). `:is-authenticated` remains the independent usability bit: a degraded or unverified entry can still route only when it is true; rejection forces it false. Never throws.
(provider-status-md provider)(provider-status-md provider status limits)The provider status + limits report as MARKDOWN — one rich canonical form every channel renders natively: the web through its markdown pipeline and the TUI through its transient Markdown layout walker. The same facts as [[status-text]], structured instead of flat.
The provider status + limits report as MARKDOWN — one rich canonical form every channel renders natively: the web through its markdown pipeline and the TUI through its transient Markdown layout walker. The same facts as [[status-text]], structured instead of flat.
(provider-status-of-registered provider)Status of a REGISTERED provider descriptor via its :provider/status-fn
(falling back to :provider/detect-fn). Never throws, and never runs longer
than probe-timeout-ms: a callback that is still going by then answers
{:is-authenticated false :error "…timed out…"} — an honest verdict the
surface can paint — instead of parking the thread that asked.
Status of a REGISTERED provider descriptor via its `:provider/status-fn`
(falling back to `:provider/detect-fn`). Never throws, and never runs longer
than `probe-timeout-ms`: a callback that is still going by then answers
`{:is-authenticated false :error "…timed out…"}` — an honest verdict the
surface can paint — instead of parking the thread that asked.(provider-status-text provider)(provider-status-text provider status limits)Multi-line human status + limits report for a configured provider. The single source for the TUI 'Show Status + Limits' dialog and the web status view.
Multi-line human status + limits report for a configured provider. The single source for the TUI 'Show Status + Limits' dialog and the web status view.
(provider-template pid)Preset descriptor for a provider id, merged from a provider extension's metadata and svar's catalog. Returns nil for unknown or intentionally removed ids.
Preset descriptor for a provider id, merged from a provider extension's metadata and svar's catalog. Returns nil for unknown or intentionally removed ids.
(provider-url-host url)Extract host from URL for display. 'https://llm.blockether.com/v1' -> 'llm.blockether.com'.
Extract host from URL for display. 'https://llm.blockether.com/v1' -> 'llm.blockether.com'.
(python-extension-load-failures)Load failures from the latest scan. Each row names the file, error, retained extension, stale? status, loaded/requested source fingerprints and readiness changes.
Load failures from the latest scan. Each row names the file, error, retained extension, stale? status, loaded/requested source fingerprints and readiness changes.
(reasoning->ast text)Reasoning / thinking text -> canonical transient Markdown tree. The SINGLE shared entry
point for rendering a model's thinking trace (TUI thinking bubble AND the web
thinking card), so both channels paint the SAME structure. Reasoning is
line-oriented (a trace, not flowing prose): normalize via normalize-reasoning
then lift every bare newline to a HARD break ([:br]) via {:soft-break :hard}. Without the hard break a bold heading collapses onto its body line
(the TUI **heading** body bug); with it the heading keeps its own line,
matching the web ticker's marked({:breaks true}).
Reasoning / thinking text -> canonical transient Markdown tree. The SINGLE shared entry
point for rendering a model's thinking trace (TUI thinking bubble AND the web
thinking card), so both channels paint the SAME structure. Reasoning is
line-oriented (a trace, not flowing prose): normalize via `normalize-reasoning`
then lift every bare newline to a HARD break (`[:br]`) via `{:soft-break
:hard}`. Without the hard break a bold heading collapses onto its body line
(the TUI `**heading** body` bug); with it the heading keeps its own line,
matching the web ticker's `marked({:breaks true})`.Minimum HIDDEN reasoning rows required before a thinking trace collapses. One or two surplus rows stay inline because a disclosure would add more friction than space savings.
Minimum HIDDEN reasoning rows required before a thinking trace collapses. One or two surplus rows stay inline because a disclosure would add more friction than space savings.
(reasoning-effort-configurable? resolved-model)True when a model accepts a CALLER-selected reasoning effort.
svar decides this, not Vis: :reasoning-effort? is stamped on every model
the router normalizes, from the WIRE that model rides. :reasoning? only
says the model thinks — GitHub Copilot's Gemini/Grok tiers think but are
:server-managed on the OpenAI-compatible wire, and Z.ai GLM thinking is
binary, so neither accepts a depth and neither may show a depth control.
Copilot's Claude tier rides the native Anthropic wire and DOES take one.
True when a model accepts a CALLER-selected reasoning effort. svar decides this, not Vis: `:reasoning-effort?` is stamped on every model the router normalizes, from the WIRE that model rides. `:reasoning?` only says the model thinks — GitHub Copilot's Gemini/Grok tiers think but are `:server-managed` on the OpenAI-compatible wire, and Z.ai GLM thinking is binary, so neither accepts a depth and neither may show a depth control. Copilot's Claude tier rides the native Anthropic wire and DOES take one.
Canonical reasoning PREVIEW height shared by every channel. Up to this many
rows/lines of a thinking trace stay visible; the remainder folds behind a
+N more disclosure (web) / ▸ THINKING +N more toggle (TUI). One
source of truth so the TUI bubble and the web card clamp reasoning to the
SAME height.
Canonical reasoning PREVIEW height shared by every channel. Up to this many rows/lines of a thinking trace stay visible; the remainder folds behind a `+N more` disclosure (web) / `▸ THINKING +N more` toggle (TUI). One source of truth so the TUI bubble and the web card clamp reasoning to the SAME height.
(rebuild-router! config)Rebuild the router from the given config. Used when provider settings change.
Forwards :router opts so live config edits (e.g. tuning
:same-provider-delays-ms) take effect on the next set-provider!
without restarting the JVM.
Rebuild the router from the given config. Used when provider settings change. Forwards `:router` opts so live config edits (e.g. tuning `:same-provider-delays-ms`) take effect on the next `set-provider!` without restarting the JVM.
(refresh-cached-routers! router)Reseat :router on every cached env's environment map.
create-environment snapshots the router into
(:router env) at construction time, and the iteration loop calls
(svar/ask-code! (:router environment) ...) - not the global
router-atom. So when a frontend changes provider
config and rebuilds the global router, every long-lived env in the
cache (TUI keeps one for the whole session) keeps talking to the
previous model until disposed.
Provider kickoff hooks run against each session before its new snapshot is
seated, covering providers added or reconfigured while that session is live.
A failed kickoff aborts the reseat instead of installing incomplete metadata.
Call this immediately after rebuild-router! so the next send! on any cached
session picks up the new router.
Reseat `:router` on every cached env's environment map. `create-environment` snapshots the router into `(:router env)` at construction time, and the iteration loop calls `(svar/ask-code! (:router environment) ...)` - not the global `router-atom`. So when a frontend changes provider config and rebuilds the global router, every long-lived env in the cache (TUI keeps one for the whole session) keeps talking to the *previous* model until disposed. Provider kickoff hooks run against each session before its new snapshot is seated, covering providers added or reconfigured while that session is live. A failed kickoff aborts the reseat instead of installing incomplete metadata. Call this immediately after `rebuild-router!` so the next `send!` on any cached session picks up the new router.
(register-channel! spec)Register a channel in the global registry. Idempotent on :channel/id - re-registering replaces the prior spec. Returns the validated channel.
Register a channel in the global registry. Idempotent on :channel/id - re-registering replaces the prior spec. Returns the validated channel.
(register-cmd! spec)Register a command in the global registry. Idempotent on
[:cmd/parent :cmd/name] - re-registering replaces the prior
entry, useful for REPL-driven development. Returns the validated
command map.
Register a command in the global registry. Idempotent on `[:cmd/parent :cmd/name]` - re-registering replaces the prior entry, useful for REPL-driven development. Returns the validated command map.
(register-extension! ext)Register an extension in the global process-level registry.
This is THE single entry point for everything an extension
contributes to vis. Whatever the extension declares -- Python sandbox
symbols (:ext.engine/symbols), CLI commands (:ext/cli), channels
(:ext/channels), LLM providers (:ext/providers) -- gets routed here and dispatched into
the matching sub-registry as a side effect.
Also computes source-file markers (paths, max-mtime, sha256) and stores them in a sidecar atom read by the tool-envelope emitter (UI extension provenance label).
Idempotent on :ext/name. Returns the validated extension.
Register an extension in the global process-level registry. This is THE single entry point for everything an extension contributes to vis. Whatever the extension declares -- Python sandbox symbols (`:ext.engine/symbols`), CLI commands (`:ext/cli`), channels (`:ext/channels`), LLM providers (`:ext/providers`) -- gets routed here and dispatched into the matching sub-registry as a side effect. Also computes source-file markers (paths, max-mtime, sha256) and stores them in a sidecar atom read by the tool-envelope emitter (UI extension provenance label). Idempotent on `:ext/name`. Returns the validated extension.
(register-op-hook! {:keys [op phase owner] hook-fn :fn :or {phase :after}})Register a cross-cutting hook on operation :op (its op-keyword, e.g.
:patch). :phase is :after (default — sees & may rewrite the result
envelope), :before (sees & may rewrite the args vector), :around (MIDDLEWARE
— wraps the call), or :gate (the op is ASKED, never wrapped — see gate-ops;
a gate op forces this phase whatever the caller declared, because the op
decides the shape). :fn is, for :after, (fn [env op-kw args result] ->
result-envelope); for :before, (fn [env op-kw args] -> args-vector); for
:around, (fn [env op-kw args next] -> result) where next runs the inner call
and may be invoked zero+ times (skip / retry) or wrapped in try/catch (recover
— this is how an op is made NOT to fail); for :gate, (fn [env op-kw ctx] ->
nil | reason). :owner (an ext keyword) makes the registration idempotent
across :reloads — re-registering the same owner+phase for an op REPLACES the
prior one. Returns the op-keyword.
Register a cross-cutting hook on operation `:op` (its op-keyword, e.g. :patch). `:phase` is :after (default — sees & may rewrite the result envelope), :before (sees & may rewrite the args vector), :around (MIDDLEWARE — wraps the call), or :gate (the op is ASKED, never wrapped — see `gate-ops`; a gate op forces this phase whatever the caller declared, because the op decides the shape). `:fn` is, for :after, (fn [env op-kw args result] -> result-envelope); for :before, (fn [env op-kw args] -> args-vector); for :around, (fn [env op-kw args next] -> result) where `next` runs the inner call and may be invoked zero+ times (skip / retry) or wrapped in try/catch (recover — this is how an op is made NOT to fail); for :gate, (fn [env op-kw ctx] -> nil | reason). `:owner` (an ext keyword) makes the registration idempotent across `:reload`s — re-registering the same owner+phase for an op REPLACES the prior one. Returns the op-keyword.
(register-provider! spec)Register a provider in the global registry. Idempotent on
:provider/id - re-registering replaces the previous descriptor.
Returns the validated provider.
Register a provider in the global registry. Idempotent on `:provider/id` - re-registering replaces the previous descriptor. Returns the validated provider.
(register-resource! session resource)(register-resource! session
resource
{:keys [stop-fn alive-fn logs-fn health-fn] :as fns})Register (or replace) a resource UNDER session. resource is the DATA map
(needs at least :id, unique within the session; :kind defaults to
:resource). fns carries the live lifecycle thunks {:stop-fn :alive-fn :logs-fn :health-fn} (all optional — a resource with no :stop-fn
reports can_stop false). Returns the stored DATA map.
Register (or replace) a resource UNDER `session`. `resource` is the DATA map
(needs at least `:id`, unique within the session; `:kind` defaults to
`:resource`). `fns` carries the live lifecycle thunks `{:stop-fn
:alive-fn :logs-fn :health-fn}` (all optional — a resource with no `:stop-fn`
reports `can_stop false`). Returns the stored DATA map.(register-theme! theme-map)(register-theme! id theme-map)Add or replace one theme in the process registry.
Arity 1 expects a full theme map with :name. Arity 2 accepts either a
full theme map or a compact settings map such as {"PADDING" "0px"}.
Add or replace one theme in the process registry.
Arity 1 expects a full theme map with `:name`. Arity 2 accepts either a
full theme map or a compact settings map such as `{"PADDING" "0px"}`.(register-themes! theme-map)Add every entry from an extension-style theme map.
{"THEME_NAME" {"PADDING" "0px"}}
Values may be compact settings maps or full theme maps.
Add every entry from an extension-style theme map.
{"THEME_NAME" {"PADDING" "0px"}}
Values may be compact settings maps or full theme maps.(register-toggle! contribution)Register one toggle that satisfies the contract-owned contribution shape.
Re-registering the same :id is idempotent: metadata MERGES, the
live VALUE in state is preserved (user overrides survive reload).
Returns the normalized contribution.
Register one toggle that satisfies the contract-owned contribution shape. Re-registering the same `:id` is idempotent: metadata MERGES, the live VALUE in `state` is preserved (user overrides survive reload). Returns the normalized contribution.
(register-toggles! specs)Convenience: register a sequence of specs in order, returns the vec of canonical specs.
Convenience: register a sequence of specs in order, returns the vec of canonical specs.
(registered-channels)All globally registered channels as a vector.
All globally registered channels as a vector.
(registered-commands)Return all registered commands as a vector, in registration order.
Return all registered commands as a vector, in registration order.
(registered-slashes)Walk every globally registered extension and return the union of
their :ext/slash-commands specs. Activation-fn filtering is NOT
applied here — channels that surface slash UX before a session is
running (TUI palette overlay) use this
env-less view. The engine dispatch path itself goes through
active-slashes env so per-session activation-fn rules still hold.
Walk every globally registered extension and return the union of their `:ext/slash-commands` specs. Activation-fn filtering is NOT applied here — channels that surface slash UX before a session is running (TUI palette overlay) use this env-less view. The engine dispatch path itself goes through `active-slashes env` so per-session activation-fn rules still hold.
(registered-toggles)Vec of every registered toggle's normalized spec, in registration insertion order. Stable for the TUI settings dialog.
Vec of every registered toggle's normalized spec, in registration insertion order. Stable for the TUI settings dialog.
(registered-under parent-path)Return the vector of registered commands whose :cmd/parent equals
parent-path (a vector of names). Use this from a parent command's
:cmd/subcommands slot - typically as a 0-arg fn so newly
registered children appear immediately:
{:cmd/name "extension"
:cmd/doc "Run an extension command."
:cmd/subcommands #(registered-under ["extension"])}
Return the vector of registered commands whose `:cmd/parent` equals
`parent-path` (a vector of names). Use this from a parent command's
`:cmd/subcommands` slot - typically as a 0-arg fn so newly
registered children appear immediately:
{:cmd/name "extension"
:cmd/doc "Run an extension command."
:cmd/subcommands #(registered-under ["extension"])}(reload-python-extensions!)(reload-python-extensions! opts)Force a full reload of every Python extension (even when no file
changed). Same return shape as load-python-extensions!. Live
sessions pick the new tool bindings up at the next turn boundary.
Force a full reload of every Python extension (even when no file changed). Same return shape as `load-python-extensions!`. Live sessions pick the new tool bindings up at the next turn boundary.
(remove-config-provider! provider-id)(remove-config-provider! provider-id source)Remove every persisted provider entry for provider-id from the string-keyed
machine config, preserving unrelated keys.
A FALLBACK tag naming that provider goes with it. Unlike default_provider,
which degrades to the fleet's first provider, the fallback root is never
implicit: a tag left behind names nobody, is invisible to every UI, and
silently resurrects the moment that provider is authenticated again.
Remove every persisted provider entry for `provider-id` from the string-keyed machine config, preserving unrelated keys. A FALLBACK tag naming that provider goes with it. Unlike `default_provider`, which degrades to the fleet's first provider, the fallback root is never implicit: a tag left behind names nobody, is invisible to every UI, and silently resurrects the moment that provider is authenticated again.
(remove-provider! provider-id)(remove-provider! provider-id source)Remove a provider from the persisted fleet AND run the registered
extension's logout when present. Invalidates the fleet snapshot.
Returns true when config changed. Extension-managed providers are rejected
before logout or any config mutation with :type :provider/managed.
Remove a provider from the persisted fleet AND run the registered extension's logout when present. Invalidates the fleet snapshot. Returns true when config changed. Extension-managed providers are rejected before logout or any config mutation with `:type :provider/managed`.
(remove-python-extension-change-listener! listener-id)Remove a listener registered with [[add-change-listener!]]. Returns nil.
Remove a listener registered with [[add-change-listener!]]. Returns nil.
(remove-title-listener! session-id listener-fn)Deregister a previously added listener. Idempotent.
Deregister a previously added listener. Idempotent.
(remove-title-pending-listener! session-id listener-fn)Deregister a previously added pending listener. Idempotent.
Deregister a previously added pending listener. Idempotent.
(render input flavor)(render input flavor opts)Render any answer input into a flavor.
Input: string | Hiccup vector | [:ast ...] AST | sequential of mixed Flavor: :html | :markdown | :plain Opts: {:context #{:answer :thinking :status :error} :max-length int - hard cap; truncate at paragraph boundary}
Render any answer input into a flavor.
Input: string | Hiccup vector | [:ast ...] AST | sequential of mixed
Flavor: :html | :markdown | :plain
Opts: {:context #{:answer :thinking :status :error}
:max-length int - hard cap; truncate at paragraph boundary}(render-command cmd path)Render multi-section help for a single command: USAGE / DESCRIPTION / SUBCOMMANDS / ARGUMENTS / FLAGS / EXAMPLES.
Empty sections are omitted. path is the command-name chain
leading up to and including this command - used for the USAGE line
when :cmd/usage isn't set.
Render multi-section help for a single command: USAGE / DESCRIPTION / SUBCOMMANDS / ARGUMENTS / FLAGS / EXAMPLES. Empty sections are omitted. `path` is the command-name chain leading up to and including this command - used for the USAGE line when `:cmd/usage` isn't set.
(render-form-value _src v)THE model-facing string for one tool/form VALUE: the canonical STRUCTURED serialization of the result, and nothing else. Tools return maps or vectors and the model reads them as DATA — there is NO per-tool rendering, no hash-gutter file views, no rg grouping.
:op (the call head) is stripped from maps since the call is already
visible in the assistant replay; src is accepted for call-site
compatibility but no longer affects the output. Structural results carry
plain 1-based line/end_line numbers, so editing resolves straight off
this structured data.
THE model-facing string for one tool/form VALUE: the canonical STRUCTURED serialization of the result, and nothing else. Tools return maps or vectors and the model reads them as DATA — there is NO per-tool rendering, no hash-gutter file views, no rg grouping. `:op` (the call head) is stripped from maps since the call is already visible in the assistant replay; `src` is accepted for call-site compatibility but no longer affects the output. Structural results carry plain 1-based `line`/`end_line` numbers, so editing resolves straight off this structured data.
(render-prompt {:keys [heading usage-note notes] :as opts})Render canonical :ext/prompt-fn text for an extension's symbols.
A prompt fragment states ROUTING and POLICY only: when this extension is the
right approach, and what it refuses. It NEVER restates a signature, an
argument name, a return shape or an example call — that text is the symbol's
own :ext.symbol/description, reached on demand with doc(name). A fragment
is pushed into EVERY request; a docstring is pulled once, so a signature
copied up here is paid for on every turn and drifts from the one that runs.
Accepts an extension map or any map with:
Returns a prompt string suitable for :ext/prompt-fn.
Render canonical `:ext/prompt-fn` text for an extension's symbols.
A prompt fragment states ROUTING and POLICY only: when this extension is the
right approach, and what it refuses. It NEVER restates a signature, an
argument name, a return shape or an example call — that text is the symbol's
own `:ext.symbol/description`, reached on demand with `doc(name)`. A fragment
is pushed into EVERY request; a docstring is pulled once, so a signature
copied up here is paid for on every turn and drifts from the one that runs.
Accepts an extension map or any map with:
- :ext/description or :heading
- :ext.engine/alias optional {:alias 'v}
- :ext.engine/symbols vector of symbol + value entries
- :usage-note optional extra note added to the heading
- :notes optional string or seq of extra lines appended verbatim
Returns a prompt string suitable for :ext/prompt-fn.(render-tree root)Top-level overview rendered when the binary is invoked with no
arguments (or via vis-agent help). Shows the root doc, then a single
COMMANDS block listing every immediate subcommand.
Top-level overview rendered when the binary is invoked with no arguments (or via `vis-agent help`). Shows the root doc, then a single COMMANDS block listing every immediate subcommand.
(repository-inventory root)(repository-inventory root opts)Return a lightweight, cached inventory of Git roots below root.
Unlike snapshot, this performs no per-repository Git status work and is
suitable for extension discovery. Known VCS metadata, cache, vendor, and
build directories are skipped. The default scan is bounded at 64
repositories.
Shape: {:root <abs-root> :count 2 :repositories [{:path <relative-path> :root <abs-root>} ...] :truncated? false}
Return a lightweight, cached inventory of Git roots below `root`.
Unlike `snapshot`, this performs no per-repository Git status work and is
suitable for extension discovery. Known VCS metadata, cache, vendor, and
build directories are skipped. The default scan is bounded at 64
repositories.
Shape:
{:root <abs-root>
:count 2
:repositories [{:path <relative-path> :root <abs-root>} ...]
:truncated? false}(reset-themes!)Reset process registry to built-in themes. Test/dev helper.
Reset process registry to built-in themes. Test/dev helper.
(resolve-config)(resolve-config explicit-config)Resolve provider config: explicit -> merged YAML config. Throws when nothing is available.
Resolve provider config: explicit -> merged YAML config. Throws when nothing is available.
(resolve-db-spec)(resolve-db-spec explicit-db-spec)Resolve DB spec: explicit -> JVM property -> environment -> validated YAML -> default.
Resolve DB spec: explicit -> JVM property -> environment -> validated YAML -> default.
(resolve-default-selection cfg fleet)PURE: the valid PRIMARY provider/model pair cfg's tags name within fleet.
Explicit config wins; an untagged config falls back to the first provider and
its first model, so a fleet always HAS a primary root while it has a provider.
Pure because every surface must resolve the tag the way the router does — channels hold the config they just read, and a channel that re-read global state here would answer for a different machine's config in a test and for a stale one in a race.
PURE: the valid PRIMARY provider/model pair `cfg`'s tags name within `fleet`. Explicit config wins; an untagged config falls back to the first provider and its first model, so a fleet always HAS a primary root while it has a provider. Pure because every surface must resolve the tag the way the router does — channels hold the config they just read, and a channel that re-read global state here would answer for a different machine's config in a test and for a stale one in a race.
(resolve-effective-model router)(resolve-effective-model router _routing-overrides)Best-effort root model descriptor from router config.
The returned map carries :name (model id, e.g. "gpt-4o") AND
:provider (provider id keyword, e.g. :openai) so every caller
can persist BOTH alongside the model. Earlier versions returned
just the model map and the provider id was silently dropped on
the way to the DB - leaving the meta layer with no way to render
provider/model.
Best-effort root model descriptor from router config. The returned map carries `:name` (model id, e.g. "gpt-4o") AND `:provider` (provider id keyword, e.g. `:openai`) so every caller can persist BOTH alongside the model. Earlier versions returned just the model map and the provider id was silently dropped on the way to the DB - leaving the meta layer with no way to render `provider/model`.
(resolve-model-info router provider-id model-name)Resolved model map for the model a SESSION actually routes to.
resolve-effective-model answers a different question — the router's GLOBAL
root — and a channel that asks it about a session's capabilities describes
the wrong model whenever the session picked something else (which is the
normal case: Ctrl+T and the web picker both write a per-session preference).
provider-id/model-name come from that preference; either may be nil, and
the first provider/model that matches what IS given wins. Falls back to the
root model so a session with no preference still gets an answer.
Resolved model map for the model a SESSION actually routes to. `resolve-effective-model` answers a different question — the router's GLOBAL root — and a channel that asks it about a session's capabilities describes the wrong model whenever the session picked something else (which is the normal case: Ctrl+T and the web picker both write a per-session preference). `provider-id`/`model-name` come from that preference; either may be nil, and the first provider/model that matches what IS given wins. Falls back to the root model so a session with no preference still gets an answer.
(resolve-subcommands cmd)Return the static vector of subcommands, calling the dynamic fn
when needed. Returns [] when the command has no children.
Return the static vector of subcommands, calling the dynamic fn when needed. Returns `[]` when the command has no children.
(resource-logs session id)Captured output lines for session+id, via the resource's :logs-fn thunk.
Returns a vector of line strings (newest last), or nil when the resource has
no logs-fn (can_logs false) or is unknown. Shell backgrounds expose their
ring buffer; managed language REPLs can expose launcher logs.
Captured output lines for `session`+`id`, via the resource's `:logs-fn` thunk. Returns a vector of line strings (newest last), or nil when the resource has no logs-fn (`can_logs false`) or is unknown. Shell backgrounds expose their ring buffer; managed language REPLs can expose launcher logs.
(result-card {:keys [op] :as form})Canonical result CARD descriptor derived only from the form's :stdout:
{:op grep — optional form metadata
:body …markdown… — local projection of printed output
:collapsible? true}
nil means the form printed nothing. A label or operation can never manufacture successful output.
Canonical result CARD descriptor derived only from the form's `:stdout`:
{:op `grep` — optional form metadata
:body …markdown… — local projection of printed output
:collapsible? true}
nil means the form printed nothing. A label or operation can never manufacture
successful output.(router-initialized?)True once the shared router has been built (via get-router/rebuild-router!).
Lets a frontend defer the FIRST build to lazy first-use instead of forcing it
at startup — so OAuth token fetches (Copilot/Codex) never run at TUI boot.
True once the shared router has been built (via `get-router`/`rebuild-router!`). Lets a frontend defer the FIRST build to lazy first-use instead of forcing it at startup — so OAuth token fetches (Copilot/Codex) never run at TUI boot.
(router-opts config)Extracts svar/make-router opts from a Vis config map.
Reads the :router block from the merged YAML config:
{:router
{:rate-limit {:same-provider-delays-ms [2000 3000 6000]
:fallback-after-ms 30000
:respect-retry-after? true
:fallback-provider? true}
:network {:timeout-ms 300000 :idle-timeout-ms 45000}
:budget {:max-tokens 1000000 :max-cost 5.0}}}
Returns {} when no :router block is present so svar's built-in
defaults win. Unknown keys are dropped — only the keys svar's
make-router knows about flow through.
See com.blockether.svar.internal.router/make-router for the
authoritative key reference.
Extracts `svar/make-router` opts from a Vis config map.
Reads the `:router` block from the merged YAML config:
```clojure
{:router
{:rate-limit {:same-provider-delays-ms [2000 3000 6000]
:fallback-after-ms 30000
:respect-retry-after? true
:fallback-provider? true}
:network {:timeout-ms 300000 :idle-timeout-ms 45000}
:budget {:max-tokens 1000000 :max-cost 5.0}}}
```
Returns `{}` when no `:router` block is present so svar's built-in
defaults win. Unknown keys are dropped — only the keys svar's
`make-router` knows about flow through.
See `com.blockether.svar.internal.router/make-router` for the
authoritative key reference.(run-doctor-checks environment)Run host-owned diagnostics, including speech, then every registered extension's checks.
Run host-owned diagnostics, including speech, then every registered extension's checks.
(runtime-config v)Adapt an already-validated string-keyed YAML map to Vis' internal domain maps.
Only the finite keys in runtime-keywords become keywords. User-defined map keys
remain strings, and parsing/validation never uses this adapter.
Adapt an already-validated string-keyed YAML map to Vis' internal domain maps. Only the finite keys in `runtime-keywords` become keywords. User-defined map keys remain strings, and parsing/validation never uses this adapter.
(save-config! config)(save-config! config source)Persist configuration to ~/.vis/state.yml using the string-keyed YAML contract.
Callers may supply internal keyword-keyed domain maps; validation always runs on
the exact string-keyed map that is written.
This REPLACES the whole store. Anything that reads the store in order to change
PART of it goes through update-machine-config! instead — a bare
read-modify-write here silently drops whatever another writer stored in between.
Persist configuration to `~/.vis/state.yml` using the string-keyed YAML contract. Callers may supply internal keyword-keyed domain maps; validation always runs on the exact string-keyed map that is written. This REPLACES the whole store. Anything that reads the store in order to change PART of it goes through `update-machine-config!` instead — a bare read-modify-write here silently drops whatever another writer stored in between.
(save-config-providers! providers)(save-config-providers! providers source)Replace the provider vector in the global string-keyed config while preserving unrelated keys, then refresh runtime provider state.
Replace the provider vector in the global string-keyed config while preserving unrelated keys, then refresh runtime provider state.
(save-toggles! snapshot)Persist a {id value} feature-toggle snapshot into the MACHINE store, folding
NOTHING else in.
Every flip used to hand save-config! the MERGED config (load-config-raw),
which copied the hand-written ~/.vis/config.yml tier and the project's
committed vis.yml — filesystem grants included — into state.yml, where the
machine tier then WON over the very files they came from: a project's grants
became global for every other repository, and a later edit of the hand-written
file silently stopped taking effect. Read the machine tier, replace one key,
write it back. No-op (and false) when the block is already identical.
Persist a `{id value}` feature-toggle snapshot into the MACHINE store, folding
NOTHING else in.
Every flip used to hand `save-config!` the MERGED config (`load-config-raw`),
which copied the hand-written `~/.vis/config.yml` tier and the project's
committed `vis.yml` — filesystem grants included — into `state.yml`, where the
machine tier then WON over the very files they came from: a project's grants
became global for every other repository, and a later edit of the hand-written
file silently stopped taking effect. Read the machine tier, replace one key,
write it back. No-op (and false) when the block is already identical.(search-text v)Universal plain-text projection for full-text search / clipboard /
logging. Accepts canonical IR, a markdown string, or anything
->ast can coerce; returns a single concatenated string suitable
for FT5 indexing or substring matching.
IR-side rendering: all prose/list/quote/table text collapses to spaces;
:code/:c bodies are included verbatim (often the highest-signal text
for search).
Strings are parsed via markdown->ast so search sees the rendered shape
regardless of upstream contract.
Idempotent on parsed input via the markdown->ast shortcut.
Universal plain-text projection for full-text search / clipboard / logging. Accepts canonical IR, a markdown string, or anything `->ast` can coerce; returns a single concatenated string suitable for FT5 indexing or substring matching. IR-side rendering: all prose/list/quote/table text collapses to spaces; `:code`/`:c` bodies are included verbatim (often the highest-signal text for search). Strings are parsed via `markdown->ast` so search sees the rendered shape regardless of upstream contract. Idempotent on parsed input via the `markdown->ast` shortcut.
(session->markdown db-info session-ref)(session->markdown db-info session-ref opts)Project a full session as a Markdown document on top of the IR pipeline.
Project a full session as a Markdown document on top of the IR pipeline.
(session-model-of db-info sid)The preference for session sid as {:provider :model}, or nil for the
router default. Prefers the immediate in-memory value, else the DB. Use on
the routing path (engine, gateway).
The preference for session `sid` as `{:provider :model}`, or nil for the
router default. Prefers the immediate in-memory value, else the DB. Use on
the routing path (engine, gateway).(session-model-of-cached db-info sid)Like model-of but DISPLAY-oriented: when no pending value exists, a recent
DB value is served from a tiny TTL cache so callers can read it every frame
without a DB hit.
Like `model-of` but DISPLAY-oriented: when no pending value exists, a recent DB value is served from a tiny TTL cache so callers can read it every frame without a DB hit.
(session-process-spawn! session-id argv directory)(session-process-spawn! session-id
argv
directory
{:keys [loopback-port env] :as opts})THE managed-language launch contract. Resolve session-id atomically, derive
its REPL/test policy, merge this call's environment delta, and spawn through
[[spawn!]]. Unknown, disposed, or failing sessions are denied before spawn.
Options additionally accept :loopback-port, :env, and every [[spawn!]]
option. The returned value is a java.lang.Process.
THE managed-language launch contract. Resolve `session-id` atomically, derive its REPL/test policy, merge this call's environment delta, and spawn through [[spawn!]]. Unknown, disposed, or failing sessions are denied before spawn. Options additionally accept `:loopback-port`, `:env`, and every [[spawn!]] option. The returned value is a `java.lang.Process`.
(set-provider! provider)Set the single active provider config. Persists to disk, updates
in-memory state, rebuilds the global router, and reseats cached
session envs. provider is a svar-native provider map
{:id :base-url :api-key :models [...]}. Replaces an existing
provider with the same :id or appends a new entry.
Set the single active provider config. Persists to disk, updates
in-memory state, rebuilds the global router, and reseats cached
session envs. `provider` is a svar-native provider map
`{:id :base-url :api-key :models [...]}`. Replaces an existing
provider with the same `:id` or appends a new entry.(set-python-binding! session sym val)Bind sym -> val in session's globals.
A FUNCTION becomes a host tool: the name is registered for this session in
[[python-host]] and installed by the runtime as a deferred callable, so
await tool(...) and gather(tool(...), …) work exactly like the tools the
context was built with. A dotted name publishes one declared method through a
capability namespace instead of exposing the extension's raw object. Anything
else is DATA and crosses as JSON.
Bind `sym` -> `val` in `session`'s globals. A FUNCTION becomes a host tool: the name is registered for this session in [[python-host]] and installed by the runtime as a deferred callable, so `await tool(...)` and `gather(tool(...), …)` work exactly like the tools the context was built with. A dotted name publishes one declared method through a capability namespace instead of exposing the extension's raw object. Anything else is DATA and crosses as JSON.
(set-session-model! db-info sid provider model)(set-session-model! db-info sid provider model reason)Set (or clear, with blank model) the PROVIDER + MODEL preference for session
sid. Takes effect IMMEDIATELY for reads; the DB write is debounced so
rapid cycling coalesces to one write. Returns {:provider :model} (or nil).
reason names why a writer that is NOT the human moved the pick (the engine's
:authentication-fallback rescue); it rides the broadcast so a surface can say
why the chip changed under the user's hands. nil for a manual pick. An
idempotent write neither schedules storage nor announces a change.
Set (or clear, with blank model) the PROVIDER + MODEL preference for session
`sid`. Takes effect IMMEDIATELY for reads; the DB write is debounced so
rapid cycling coalesces to one write. Returns `{:provider :model}` (or nil).
`reason` names why a writer that is NOT the human moved the pick (the engine's
`:authentication-fallback` rescue); it rides the broadcast so a surface can say
why the chip changed under the user's hands. nil for a manual pick. An
idempotent write neither schedules storage nor announces a change.(set-title! id title)Host-driven title change. Resolves the live env (if any) so the
in-memory atom + listener fan-out stay in sync; falls back to a
plain DB write when no env is live for this session (e.g.
vis-agent sessions rename ops).
Host-driven title change. Resolves the live env (if any) so the in-memory atom + listener fan-out stay in sync; falls back to a plain DB write when no env is live for this session (e.g. `vis-agent sessions` rename ops).
(shutdown!)Flush and stop all telemere handlers. Call after the TUI screen stops.
Flush and stop all telemere handlers. Call after the TUI screen stops.
(slash-by-path env path)Return the slash spec whose full path = path, or nil. path is a
non-empty vec of names like ["workspace" "apply"]. When
multiple specs share the path (per-channel partitioning), returns
the first registered.
Return the slash spec whose full path = `path`, or nil. `path` is a non-empty vec of names like `["workspace" "apply"]`. When multiple specs share the path (per-channel partitioning), returns the first registered.
(slash-children env)(slash-children env parent)Return the vec of slash specs whose :slash/parent = parent vec.
parent defaults to [] (top-level commands).
Return the vec of slash specs whose `:slash/parent` = `parent` vec. `parent` defaults to `[]` (top-level commands).
(slash-dispatch env ctx text)Dispatch slash text against env. ctx carries channel-side knobs:
:channel/id keyword, required (:tui, :cli, ...)
:session/id session-soul UUID (optional unless slash
declares :slash/requires #{:session})
:workspace/id workspace UUID (optional unless required)
:db-info persistence handle (always passed when
present in the channel env)
:reply! (fn [ir-or-string]) for channels that want
an immediate side-effect surface
:publish! (fn [event]) bus for cross-channel events
Return shapes documented at the namespace docstring.
Dispatch slash `text` against `env`. `ctx` carries channel-side knobs:
:channel/id keyword, required (:tui, :cli, ...)
:session/id session-soul UUID (optional unless slash
declares `:slash/requires #{:session}`)
:workspace/id workspace UUID (optional unless required)
:db-info persistence handle (always passed when
present in the channel env)
:reply! (fn [ir-or-string]) for channels that want
an immediate side-effect surface
:publish! (fn [event]) bus for cross-channel events
Return shapes documented at the namespace docstring.(slash-palette channel)(slash-palette channel extra)THE canonical typed-/ palette for a channel. Registered slash leaves,
channel-native entries, and prompt templates become complete {:name :doc}
rows. Registered/channel-native names always win.
THE canonical typed-`/` palette for a channel. Registered slash leaves,
channel-native entries, and prompt templates become complete `{:name :doc}`
rows. Registered/channel-native names always win.(slash-parse text)Tokenise a slash text into {:path :args :raw} or nil.
This function performs PURE tokenisation. It does NOT consult any
slash registry; the engine resolves the longest matching prefix at
dispatch time once it has the env. This keeps parse pure and
testable in isolation.
Tokenise a slash `text` into `{:path :args :raw}` or nil.
This function performs PURE tokenisation. It does NOT consult any
slash registry; the engine resolves the longest matching prefix at
dispatch time once it has the env. This keeps `parse` pure and
testable in isolation.(stable-prompt-text messages)Join stable prompt message contents for token budgeting and debug bindings only. Provider sends the original message vector; this is not a send path.
Join stable prompt message contents for token budgeting and debug bindings only. Provider sends the original message vector; this is not a send path.
(state-path)Machine-owned RMW config store ~/.vis/state.yml (YAML). Vis read-modify-writes
this exact file — OAuth tokens, TUI-added providers, extension env overrides — so
it is kept SEPARATE from the hand-written ~/.vis/config.yml tier: the RMW cycle
must never fold (and thus clobber) a user's hand-written YAML.
Machine-owned RMW config store `~/.vis/state.yml` (YAML). Vis read-modify-writes this exact file — OAuth tokens, TUI-added providers, extension env overrides — so it is kept SEPARATE from the hand-written `~/.vis/config.yml` tier: the RMW cycle must never fold (and thus clobber) a user's hand-written YAML.
(stop-resource! session id)Atomically claim a resource and run its :stop-fn.
THE single stop path — the agent tool and the footer both land here, always
scoped to session so no session can stop another's resource. A successful
stop leaves the claimed generation unregistered. If its callback throws, that
generation is restored only when the id is still vacant, preserving both a
retry handle and any replacement registered during teardown. Returns a result
map.
Atomically claim a resource and run its `:stop-fn`. THE single stop path — the agent tool and the footer both land here, always scoped to `session` so no session can stop another's resource. A successful stop leaves the claimed generation unregistered. If its callback throws, that generation is restored only when the id is still vacant, preserving both a retry handle and any replacement registered during teardown. Returns a result map.
(symbol v)(symbol v opts)Build a function symbol entry FROM A CLOJURE VAR.
The 3-arg form (symbol sym-name f opts) is a test-friendly direct
constructor: pass the sandbox-visible symbol, the implementation fn, and
an opts map whose :doc / :arglists are read directly from opts
instead of var meta. Production code uses the var form.
The var supplies :symbol (var name), :fn (the var's value), :doc and
:arglists (read from var metadata - i.e. the underlying defn's
docstring + arglists). Pass it as #'my-tool.
Observed tools return canonical internal envelope maps. Declare :activity
beside every observed binding: {:headline "Read file" :show-start false :render callback}.
Activity is for people: use understandable sentence-case English, not identifiers
or all-caps sentences. Quick reads and patches use :show-start false to show only
the end result; slow operations keep the default true to show running progress.
This hides presentation, never internal timing, failure or cancellation tracking. The
optional callback receives invocation details and a bounded, redacted public
result, and returns a canonical Activity presentation. Tools can instead call
publish-activity! while running. No generic result presentation is generated;
the engine owns identity, state, timing, errors and file-change evidence.
Raw helpers pass :raw? true and return plain values directly, with no
envelope enforcement, channel sink, or tool metadata.
Optional opts:
:symbol - override the Python sandbox name (default: var name).
:doc-fn - compute doc lazily from (sym v) when the var
lacks a docstring (third-party vars only).
:raw? - true for plain composable helpers.
:tag - REQUIRED :observation | :mutation for observed
tools (unless :raw? true).
:params - options-dict key vocabulary [{:name "paths" :required? true} {:name "ranges"}], rendered by doc(name). REQUIRED of every
tool whose call ends in an options dict — a **kwargs
signature states nothing a caller can act on.
:before-fn :after-fn :on-error-fn :ticker-fn
Observed tool functions return canonical internal envelope maps. The wrapper records the envelope, then returns only its payload to Python; failure envelopes are converted into thrown ex-info so Python reports normal errors.
:doc and :arglists ALWAYS come from var metadata — the previous
test-only (symbol sym-name f opts) 3-arg form is RETIRED. Tests
that want to register an inline fn must defn it first and pass
#'the-fn.
See docs/src/extensions/hooks.md for hook semantics.
Build a function symbol entry FROM A CLOJURE VAR.
The 3-arg form `(symbol sym-name f opts)` is a test-friendly direct
constructor: pass the sandbox-visible symbol, the implementation fn, and
an opts map whose `:doc` / `:arglists` are read directly from opts
instead of var meta. Production code uses the var form.
The var supplies `:symbol` (var name), `:fn` (the var's value), `:doc` and
`:arglists` (read from var metadata - i.e. the underlying defn's
docstring + arglists). Pass it as `#'my-tool`.
Observed tools return canonical internal envelope maps. Declare `:activity`
beside every observed binding: `{:headline "Read file" :show-start false :render callback}`.
Activity is for people: use understandable sentence-case English, not identifiers
or all-caps sentences. Quick reads and patches use `:show-start false` to show only
the end result; slow operations keep the default true to show running progress.
This hides presentation, never internal timing, failure or cancellation tracking. The
optional callback receives invocation details and a bounded, redacted public
result, and returns a canonical Activity presentation. Tools can instead call
`publish-activity!` while running. No generic result presentation is generated;
the engine owns identity, state, timing, errors and file-change evidence.
Raw helpers pass `:raw? true` and return plain values directly, with no
envelope enforcement, channel sink, or tool metadata.
Optional opts:
:symbol - override the Python sandbox name (default: var name).
:doc-fn - compute doc lazily from `(sym v)` when the var
lacks a docstring (third-party vars only).
:raw? - true for plain composable helpers.
:tag - REQUIRED `:observation | :mutation` for observed
tools (unless `:raw? true`).
:params - options-dict key vocabulary `[{:name "paths" :required? true}
{:name "ranges"}]`, rendered by `doc(name)`. REQUIRED of every
tool whose call ends in an options dict — a `**kwargs`
signature states nothing a caller can act on.
:before-fn :after-fn :on-error-fn :ticker-fn
Observed tool functions return canonical internal envelope maps. The
wrapper records the envelope, then returns only its payload to Python; failure
envelopes are converted into thrown ex-info so Python reports normal errors.
`:doc` and `:arglists` ALWAYS come from var metadata — the previous
test-only `(symbol sym-name f opts)` 3-arg form is RETIRED. Tests
that want to register an inline fn must `defn` it first and pass
`#'the-fn`.
See `docs/src/extensions/hooks.md` for hook semantics.(sync-active-extension-symbols! environment)(sync-active-extension-symbols! environment active-extensions)Make the Python sandbox's callable globals match active extension state.
install-extension! keeps every extension row in :extensions, but only
active extensions contribute callable symbols. Called after per-env
installation and again at turn start so :ext/activation-fn changes become
real tool availability, not just prompt visibility.
The Python sandbox is FLAT globals (no namespaces/aliases/macros): active extensions putMember their symbols straight into the top scope; deactivated extensions have theirs removed (putMember nil). Symbol names are snake-ified by env/set-python-binding!.
Make the Python sandbox's callable globals match active extension state. `install-extension!` keeps every extension row in `:extensions`, but only active extensions contribute callable symbols. Called after per-env installation and again at turn start so `:ext/activation-fn` changes become real tool availability, not just prompt visibility. The Python sandbox is FLAT globals (no namespaces/aliases/macros): active extensions putMember their symbols straight into the top scope; deactivated extensions have theirs removed (putMember nil). Symbol names are snake-ified by env/set-python-binding!.
Host-owned globals refreshed with the standing context and hidden from user live vars.
Host-owned globals refreshed with the standing context and hidden from user live vars.
(test-python-extensions!)(test-python-extensions! {:keys [dirs sys-path]})Discover and run every Python test (test_*.py / *_test.py) across the
extension dirs (default: ~/.vis/extensions and <cwd>/.vis/extensions),
each in its own TRUSTED session driving real pytest, which the sandbox
installs on first use. Tests import the extension's own package through the
sys.path sugar,
exactly like extension.py does.
Returns {:files n :ok? bool :passed n :failed n :errored n :skipped n :tests [{:file :nodeid :outcome :message}] :results [{:file :ok? :tests …}]}.
Counts are DERIVED from :tests (the flat per-test list) — the single source
of truth. Never throws: a file that blows up at import is one :errored
result, not a crash.
:sys-path adds extra import roots (the project's own declared src layout)
below each test's own dirs, so a test that imports the package under test
resolves it the way the project's packaging metadata says it should.
Discover and run every Python test (`test_*.py` / `*_test.py`) across the
extension dirs (default: `~/.vis/extensions` and `<cwd>/.vis/extensions`),
each in its own TRUSTED session driving real `pytest`, which the sandbox
installs on first use. Tests import the extension's own package through the
`sys.path` sugar,
exactly like `extension.py` does.
Returns `{:files n :ok? bool :passed n :failed n :errored n :skipped n
:tests [{:file :nodeid :outcome :message}] :results [{:file :ok? :tests …}]}`.
Counts are DERIVED from `:tests` (the flat per-test list) — the single source
of truth. Never throws: a file that blows up at import is one `:errored`
result, not a crash.
`:sys-path` adds extra import roots (the project's own declared `src` layout)
below each test's own dirs, so a test that imports the package under test
resolves it the way the project's packaging metadata says it should.(theme id)Return registered theme by id (string or keyword). Unknown ids fall back to
default-theme.
Return registered theme by id (string or keyword). Unknown ids fall back to `default-theme`.
(theme->web-css-vars {:keys [palette settings]})CSS custom-property map ("--bg" -> "#rrggbb") for a theme map - the palette-named tokens, the derived bg/fg mixes, and the luminance-delta-normalized border hairlines.
CSS custom-property map ("--bg" -> "#rrggbb") for a theme map -
the palette-named tokens, the derived bg/fg mixes, and the
luminance-delta-normalized border hairlines.(theme-registry)Return current immutable theme registry map.
Return current immutable theme registry map.
(toggle-add-listener! f)Register a no-arg-or-event listener fn. Returns a dispose! thunk
the caller invokes when their consumer goes away (channel close,
extension reload, ...).
Register a no-arg-or-event listener fn. Returns a `dispose!` thunk the caller invokes when their consumer goes away (channel close, extension reload, ...).
(toggle-choices id)Vec of legal choices for an :enum toggle. Empty when id is
unregistered or registered as :boolean.
Vec of legal choices for an `:enum` toggle. Empty when `id` is unregistered or registered as `:boolean`.
(toggle-cycle-value! id)Advance an :enum toggle one step through its registered
:choices. Wraps at the end. Throws on boolean toggles.
Advance an `:enum` toggle one step through its registered `:choices`. Wraps at the end. Throws on boolean toggles.
(toggle-enabled? id)Boolean cast of (value-of id). Fail-closed: returns false when id is not
registered. Hot-path — one atom deref.
Boolean cast of `(value-of id)`. Fail-closed: returns `false` when `id` is not registered. Hot-path — one atom deref.
(toggle-reset-to-default! id)Drop the user override for id so resolution falls back to the
registered default. Notifies listeners when the effective value
changes.
Drop the user override for `id` so resolution falls back to the registered default. Notifies listeners when the effective value changes.
(toggle-set-enabled! id value)Boolean alias of set-value! for the TUI dialog — keeps the
common toggle-flip call sites readable. Refuses :enum toggles
so an accidental boolean-flip on a multi-value toggle surfaces
loudly; use cycle-value! / set-value! for those.
Boolean alias of `set-value!` for the TUI dialog — keeps the common toggle-flip call sites readable. Refuses `:enum` toggles so an accidental boolean-flip on a multi-value toggle surfaces loudly; use `cycle-value!` / `set-value!` for those.
(toggle-set-value! id value)Set id to value and notify listeners. Returns the new value.
Validation matches the registered :type:
:boolean — must already BE a boolean; anything else throws, so a
truthy string can never mean its own opposite. set-enabled!
casts, coerce-config-value and wire-value parse.
:enum — must be one of :choices; otherwise throws
:vis.toggles/invalid-value so the bug surfaces
at the call site instead of later in render.
Set `id` to `value` and notify listeners. Returns the new value.
Validation matches the registered `:type`:
`:boolean` — must already BE a boolean; anything else throws, so a
truthy string can never mean its own opposite. `set-enabled!`
casts, `coerce-config-value` and `wire-value` parse.
`:enum` — must be one of `:choices`; otherwise throws
`:vis.toggles/invalid-value` so the bug surfaces
at the call site instead of later in render.(toggle-spec id)Lookup the registered spec for id, or nil.
Lookup the registered spec for `id`, or nil.
(toggle-type id):boolean / :enum / nil for unknown.
`:boolean` / `:enum` / nil for unknown.
(toggle-value id)Resolve the live value for id. Lookup order:
state,nil if the toggle isn't registered.Returns the raw value (boolean for :boolean toggles, any value
from :choices for :enum toggles). enabled? is the
boolean-cast convenience for the common boolean path.
Resolve the live value for `id`. Lookup order: 1. live override in `state`, 2. registered default, 3. `nil` if the toggle isn't registered. Returns the raw value (boolean for `:boolean` toggles, any value from `:choices` for `:enum` toggles). `enabled?` is the boolean-cast convenience for the common boolean path.
(toggles-for-channel channel)visible-toggles further scoped to channel via toggle-for-channel?.
Channels render THIS instead of visible-toggles so each Settings UI
only shows controls it actually honours.
`visible-toggles` further scoped to `channel` via `toggle-for-channel?`. Channels render THIS instead of `visible-toggles` so each Settings UI only shows controls it actually honours.
(toggles-hydrate-from-config! config-map)Bulk-apply values from the string-keyed YAML toggles map. Keyword-keyed
internal maps remain accepted for callers that do not originate at YAML.
Bulk-apply values from the string-keyed YAML `toggles` map. Keyword-keyed internal maps remain accepted for callers that do not originate at YAML.
(toggles-snapshot)Return a map {id value} of EVERY persistable toggle's effective
value, intended for serialisation. Skips toggles whose
:persist? is false. Boolean toggles are coerced to boolean;
enum toggles surface their raw choice value. Orphans from a
previously-installed extension are dropped. Keys are SORTED so the
serialised block is stable and diff-friendly, never a hash jumble.
Return a map `{id value}` of EVERY persistable toggle's effective
value, intended for serialisation. Skips toggles whose
`:persist?` is false. Boolean toggles are coerced to boolean;
enum toggles surface their raw choice value. Orphans from a
previously-installed extension are dropped. Keys are SORTED so the
serialised block is stable and diff-friendly, never a hash jumble.(turn! environment messages)(turn! environment messages opts)Runs one session turn on an RLM environment using iterative LLM code evaluation.
Params:
environment - RLM environment from create-environment.
messages - Vector of message maps. Always a vector, e.g.:
[(svar/user <prompt-text>)]
[(svar/user <prompt-text> (svar/image <b64> <mime-type>))]
opts - Map, optional:
high or max.
Catalog-gated and threaded unchanged through every iteration.Returns: Map with:
:error or :cancelled).Runs one session turn on an RLM environment using iterative LLM code evaluation.
Params:
`environment` - RLM environment from create-environment.
`messages` - Vector of message maps. Always a vector, e.g.:
[(svar/user <prompt-text>)]
[(svar/user <prompt-text> (svar/image <b64> <mime-type>))]
`opts` - Map, optional:
- :spec - Output spec for structured answers.
- :model - Override config's default model.
- :max-context-tokens - Token budget for context.
- :debug? - Enable verbose debug logging (default: false). Logs iteration details,
code evaluation, LLM responses at :debug level with :rlm-phase context.
- :reasoning-default - Optional base reasoning effort for reasoning-capable models.
Accepts :low/:medium/:high or low/medium/high strings. Adaptive escalation still applies.
- :reasoning-effort - Exact provider-native effort string, `high` or `max`.
Catalog-gated and threaded unchanged through every iteration.
- :extra-body - Optional provider-specific request-body params merged into the
upstream LLM call after auto max_tokens + reasoning translation.
Returns:
Map with:
- :trace - Vector of iteration trace entries, each containing:
{:iteration N
:response <llm-response-text>
:blocks [{:id 0 :code <code-str> :stdout <printed-text> :error nil
:envelope {:started-at-ms 10 :finished-at-ms 15 ...}}
...]}
- :iteration-count - Number of iterations used.
- :duration-ms - Turn duration in milliseconds.
- :tokens - Token usage map {"input" N "output" N "total" N} (canonical string keys).
- :cost - Cost map {"input_cost" N "output_cost" N "total_cost" N} (canonical string keys).
- :confidence - Confidence level (:high/:medium/:low) from final iteration.
- :reasoning - String summary of how the answer was derived (from LLM's FINAL call).
- :status - Only present on failure (`:error` or `:cancelled`).(unknown-flags arg-specs raw-args)Return a vector of --flag tokens in raw-args that are NOT declared
in arg-specs. Walks the args the same way parse-args does so that
a string-typed flag's VALUE (e.g. bar in --out bar) is never
misclassified as an unknown flag. Boolean flags don't consume their
next token. Universal --help / -h are always considered known.
Used by dispatch! to refuse unknown flags and surface the list of
accepted flags via render-command. Pure; no side effects.
Unknown flags are reported only by their leading token; we don't know whether the user intended them to take a value, so the walker conservatively advances by one token after each unknown.
Return a vector of `--flag` tokens in `raw-args` that are NOT declared in `arg-specs`. Walks the args the same way `parse-args` does so that a string-typed flag's VALUE (e.g. `bar` in `--out bar`) is never misclassified as an unknown flag. Boolean flags don't consume their next token. Universal `--help` / `-h` are always considered known. Used by `dispatch!` to refuse unknown flags and surface the list of accepted flags via `render-command`. Pure; no side effects. Unknown flags are reported only by their leading token; we don't know whether the user intended them to take a value, so the walker conservatively advances by one token after each unknown.
(unregister-op-hooks-for-owner! owner)Remove EVERY op-hook registered by owner (all ops + phases). Driven by
deregister-extension! so an extension's hooks die with it; also callable
directly to dynamically tear an extension's hooks down.
Remove EVERY op-hook registered by `owner` (all ops + phases). Driven by `deregister-extension!` so an extension's hooks die with it; also callable directly to dynamically tear an extension's hooks down.
(unregister-resource! session id)Drop the resource generation current when this call began from session
(does NOT run its stop-fn — caller decides). A concurrent replacement is never
removed. Returns true only when the captured generation was removed.
Drop the resource generation current when this call began from `session` (does NOT run its stop-fn — caller decides). A concurrent replacement is never removed. Returns true only when the captured generation was removed.
(unregister-theme! id)Remove a theme id. Built-ins reset to their built-in value instead of being removed, so the registry always keeps light and dark available.
Remove a theme id. Built-ins reset to their built-in value instead of being removed, so the registry always keeps light and dark available.
(unwatch-notifications! key)Remove the watcher registered under key. Returns true when one
was actually removed.
Remove the watcher registered under `key`. Returns true when one was actually removed.
(update-config-provider! provider-id f)(update-config-provider! provider-id f source)Apply f to one persisted provider and save the resulting fleet.
Apply `f` to one persisted provider and save the resulting fleet.
(update-machine-config! f)(update-machine-config! f source)Read-modify-write the machine store ~/.vis/state.yml under a lock: apply f
to the raw string-keyed map (an absent store arrives as {}) and persist the
result through save-config!.
THE write pattern for everything Vis persists about itself. Every writer here rewrites the WHOLE map, so the read and the write have to be ONE critical section: a provider added while a toggle flipped used to write back the toggle block the other writer had just replaced. The file was never corrupt — the update was simply lost, silently, and the loser only found out much later.
The lock lives in a sibling state.yml.lock because the store itself is
REPLACED by an atomic move, and a lock taken on a replaced inode guards
nothing.
Returns the written map, or nil when f answered nil or changed nothing (no
write, no provider-selected event). A lock the filesystem refuses (a network
mount) degrades to the plain read-modify-write rather than to no write at all.
Read-modify-write the machine store `~/.vis/state.yml` under a lock: apply `f`
to the raw string-keyed map (an absent store arrives as `{}`) and persist the
result through `save-config!`.
THE write pattern for everything Vis persists about itself. Every writer here
rewrites the WHOLE map, so the read and the write have to be ONE critical
section: a provider added while a toggle flipped used to write back the toggle
block the other writer had just replaced. The file was never corrupt — the
update was simply lost, silently, and the loser only found out much later.
The lock lives in a sibling `state.yml.lock` because the store itself is
REPLACED by an atomic move, and a lock taken on a replaced inode guards
nothing.
Returns the written map, or nil when `f` answered nil or changed nothing (no
write, no provider-selected event). A lock the filesystem refuses (a network
mount) degrades to the plain read-modify-write rather than to no write at all.(update-resource! session id patch)Patch the DATA of the resource generation current when this call began (e.g.
flip :status, refresh :detail). No-op if unknown or if that generation was
replaced while the patch was being prepared. Returns the updated DATA map or
nil.
Patch the DATA of the resource generation current when this call began (e.g. flip `:status`, refresh `:detail`). No-op if unknown or if that generation was replaced while the patch was being prepared. Returns the updated DATA map or nil.
(utf8 s)s as UTF-8 bytes — the one charset every vis wire format names.
`s` as UTF-8 bytes — the one charset every vis wire format names.
(validate-args arg-specs parsed)Validate parsed args against spec. Returns nil on success, or an error string describing the missing required arguments.
Validate parsed args against spec. Returns nil on success, or an error string describing the missing required arguments.
(value v)(value v opts-or-val)(value sym-name val opts)Build a value symbol entry FROM A CLOJURE VAR - a plain constant/data binding.
The var supplies :symbol (var name), :val (the var's value, unless :val
is provided in opts to override - used by macro-shim entries), and :doc
(from var metadata, i.e. the defn's docstring).
(def ^{:doc "Maximum retry attempts."} max-retries 3) (vis/value #'max-retries)
Opts: :symbol - override the Python sandbox name (default: var name). :val - explicit value override (rare; for macro shims that bind a marker map instead of the var's own value).
Build a value symbol entry FROM A CLOJURE VAR - a plain constant/data binding.
The var supplies `:symbol` (var name), `:val` (the var's value, unless `:val`
is provided in opts to override - used by macro-shim entries), and `:doc`
(from var metadata, i.e. the defn's docstring).
(def ^{:doc "Maximum retry attempts."} max-retries 3)
(vis/value #'max-retries)
Opts:
:symbol - override the Python sandbox name (default: var name).
:val - explicit value override (rare; for macro shims that bind a
marker map instead of the var's own value).(verbosity-configurable? resolved-model)True when a model accepts a caller-selected answer verbosity.
Also svar's call: :verbosity-style is stamped from the wire, so every
provider on the OpenAI Responses endpoint (Codex AND GitHub Copilot's GPT
tier) gets the knob and nothing else does. Never test a provider id here.
True when a model accepts a caller-selected answer verbosity. Also svar's call: `:verbosity-style` is stamped from the wire, so every provider on the OpenAI Responses endpoint (Codex AND GitHub Copilot's GPT tier) gets the knob and nothing else does. Never test a provider id here.
(virtual-threads-available?)True when this JVM exposes Java virtual-thread APIs. Reflection keeps source compatible with older runtimes.
True when this JVM exposes Java virtual-thread APIs. Reflection keeps source compatible with older runtimes.
(visible-toggles)registered-toggles filtered to what settings UIs should SHOW —
provider-specific knobs declare a :visible-fn so a knob only appears when
the provider that owns it is actually configured, and :settings? false
toggles (e.g. reasoning-effort and verbosity, which have their own Ctrl+R /
Ctrl+X controls) stay out of the Settings dialog entirely.
State ops always work on the FULL registry; visibility is a presentation
concern only.
`registered-toggles` filtered to what settings UIs should SHOW — provider-specific knobs declare a `:visible-fn` so a knob only appears when the provider that owns it is actually configured, and `:settings? false` toggles (e.g. reasoning-effort and verbosity, which have their own Ctrl+R / Ctrl+X controls) stay out of the Settings dialog entirely. State ops always work on the FULL registry; visibility is a presentation concern only.
(watch-notifications! key f)Register a watcher under key. The fn f is called with the
full active-notifications vec on every push / dismiss / clear.
Replacing an existing watcher under the same key is fine -
typical pattern is (watch! :tui-screen render-bump).
Watchers run synchronously on the mutating thread, AFTER the atom swap. They MUST be cheap (microseconds) - anything heavier should bounce work onto a future.
Register a watcher under `key`. The fn `f` is called with the full active-notifications vec on every push / dismiss / clear. Replacing an existing watcher under the same key is fine - typical pattern is `(watch! :tui-screen render-bump)`. Watchers run synchronously on the mutating thread, AFTER the atom swap. They MUST be cheap (microseconds) - anything heavier should bounce work onto a future.
(web-css-root theme-or-id)A :root{...} CSS block for a theme id (or theme map): every shared
var from theme->web-css-vars plus color-scheme, ready to serve
AFTER the static stylesheet so it overrides the baked-in defaults.
A `:root{...}` CSS block for a theme id (or theme map): every shared
var from `theme->web-css-vars` plus `color-scheme`, ready to serve
AFTER the static stylesheet so it overrides the baked-in defaults.(wire->engine x)Recursively convert decoded wire data into the engine's keyword-keyed shape — the mirror of [[->wire]]. Only KEYS are converted (via [[engine-key]]); values are data and are never re-typed.
Recursively convert decoded wire data into the engine's keyword-keyed shape — the mirror of [[->wire]]. Only KEYS are converted (via [[engine-key]]); values are data and are never re-typed.
(wire->wire x)Recursively convert an engine value into JSON-encodable data.
Recursively convert an engine value into JSON-encodable data.
(wire-canonical x)THE canonical gateway value shape — snake_case STRING map keys, exactly
what a remote client holds after parse-json ∘ json-str. In-process and
remote consumers therefore read the same role-labelled messages and typed
content blocks.
Invariant: (canonical x) equals (parse-json (json-str x)).
THE canonical gateway value shape — snake_case STRING map keys, exactly what a remote client holds after `parse-json` ∘ `json-str`. In-process and remote consumers therefore read the same role-labelled messages and typed content blocks. Invariant: `(canonical x)` equals `(parse-json (json-str x))`.
(wire-json-pretty x)Pretty-print canonical wire JSON for human-facing views.
Pretty-print canonical wire JSON for human-facing views.
(wire-json-str x)Encode any engine value as a JSON string via [[->wire]].
Encode any engine value as a JSON string via [[->wire]].
(wire-key k)Keyword/symbol map key -> snake_case string. A boolean-style foo? key
becomes is_foo (already-is- prefixed keys just drop the ?). String
keys (fact keys, scope strings, file paths) pass VERBATIM - rewriting
them could corrupt user data that legitimately contains hyphens. ANY
other key (the number/boolean/nil keys a decoded JSON or Python value can
carry into a tool result) is rendered to its JSON key spelling: JSON has
no non-string keys, and leaving one unrendered makes the whole event
unencodable - which kills the transport, not just the field.
Keyword/symbol map key -> snake_case string. A boolean-style `foo?` key becomes `is_foo` (already-`is-` prefixed keys just drop the `?`). String keys (fact keys, scope strings, file paths) pass VERBATIM - rewriting them could corrupt user data that legitimately contains hyphens. ANY other key (the number/boolean/nil keys a decoded JSON or Python value can carry into a tool result) is rendered to its JSON key spelling: JSON has no non-string keys, and leaving one unrendered makes the whole event unencodable - which kills the transport, not just the field.
(worker-future f)(worker-future name f)(worker-future name f {:keys [platform?]})Run f on a cancellable worker Future. Uses a virtual thread when the JVM
supports it, otherwise falls back to a named daemon platform thread.
Pass {:platform? true} for work that can pin a virtual-thread carrier in
native or uninterruptible code. A platform worker costs one thread but cannot
starve the virtual-thread scheduler that runs ordinary lightweight work.
The returned value implements java.util.concurrent.Future plus Clojure
deref/realized? protocols so legacy future call sites can migrate without
losing timeout/cancellation behavior.
Run `f` on a cancellable worker Future. Uses a virtual thread when the JVM
supports it, otherwise falls back to a named daemon platform thread.
Pass `{:platform? true}` for work that can pin a virtual-thread carrier in
native or uninterruptible code. A platform worker costs one thread but cannot
starve the virtual-thread scheduler that runs ordinary lightweight work.
The returned value implements java.util.concurrent.Future plus Clojure
deref/realized? protocols so legacy `future` call sites can migrate without
losing timeout/cancellation behavior.(worker-runtime)Runtime probe for worker execution. :worker-helper is stable metadata for
diagnostics; :virtual-threads? reports whether new worker tasks will use
Java virtual threads.
Runtime probe for worker execution. `:worker-helper` is stable metadata for diagnostics; `:virtual-threads?` reports whether new worker tasks will use Java virtual threads.
(workspace-abandon! db-info {:keys [workspace-id reason]})Transition a workspace to :discarded and release the backend-owned clones it holds: its primary clone plus every private per-root draft clone.
Transition a workspace to :discarded and release the backend-owned clones it holds: its primary clone plus every private per-root draft clone.
(workspace-apply! db-info {:keys [workspace-id]})Land a draft's changes into their real roots: the primary clone into
:repo-root, plus every extra root minted with the copy-and-apply draft
policy into its own trunk. copy-only / not-allowed roots never land.
Land a draft's changes into their real roots: the primary clone into `:repo-root`, plus every extra root minted with the `copy-and-apply` draft policy into its own trunk. `copy-only` / `not-allowed` roots never land.
(workspace-capability-matrix workspace-or-root)Availability of each draft backend for workspace-or-root, checked against
the real derived storage location rather than assuming source and store share
a filesystem: [{:backend :worktree …} {:backend :rift …}], each with
:available? and, when unavailable, :reason/:details. A diagnostics
surface — draft-backend-for is the selection.
Availability of each draft backend for `workspace-or-root`, checked against
the real derived storage location rather than assuming source and store share
a filesystem: `[{:backend :worktree …} {:backend :rift …}]`, each with
`:available?` and, when unavailable, `:reason`/`:details`. A diagnostics
surface — `draft-backend-for` is the selection.(workspace-change-root! db-info session-state-id path)Repoint session-state-id's primary workspace root to path. Refuses while
the session is in an isolated workspace.
Repoint `session-state-id`'s primary workspace root to `path`. Refuses while the session is in an isolated workspace.
(workspace-create! db-info
{:keys [session-state-id label from clean?
filesystem-roots]})Create an isolated DRAFT with the backend draft-backend-for selects for the
fork parent — a linked Git worktree on a fresh vis/<label> branch, or a Rift
copy-on-write clone — and pin it to :session-state-id. Core never silently
falls back to a shared root: with drafts switched off this throws
:workspace/drafts-disabled, with no capable backend
:workspace/capability-unavailable.
Pass :from <parent-workspace> to clone that workspace's :root and inherit
its :repo-root; otherwise the parent is the user's real cwd (trunk).
apply! copies files back to repo-root; approve! merges into that
repository's default branch, which may have a different checkout.
By default the draft carries the parent's pending work: a Rift clone copies
it, a worktree checks HEAD out and replays the uncommitted diff plus untracked
files into it. :clean? true hands back the committed state instead — Rift
resets the clone (recording the omissions in its marker), a worktree simply
skips the replay. The baseline is captured after that seeding so it is not
read as an agent edit. A project that is not Git-managed has no committed
state to seed from, so a clean draft is refused there before anything is
cloned.
Create an isolated DRAFT with the backend `draft-backend-for` selects for the fork parent — a linked Git worktree on a fresh `vis/<label>` branch, or a Rift copy-on-write clone — and pin it to `:session-state-id`. Core never silently falls back to a shared root: with drafts switched off this throws `:workspace/drafts-disabled`, with no capable backend `:workspace/capability-unavailable`. Pass `:from <parent-workspace>` to clone that workspace's `:root` and inherit its `:repo-root`; otherwise the parent is the user's real cwd (trunk). `apply!` copies files back to repo-root; `approve!` merges into that repository's default branch, which may have a different checkout. By default the draft carries the parent's pending work: a Rift clone copies it, a worktree checks HEAD out and replays the uncommitted diff plus untracked files into it. `:clean? true` hands back the committed state instead — Rift resets the clone (recording the omissions in its marker), a worktree simply skips the replay. The baseline is captured after that seeding so it is not read as an agent edit. A project that is not Git-managed has no committed state to seed from, so a clean draft is refused there before anything is cloned.
(workspace-create-dir! parent name)Create a single child directory name under existing directory parent.
Returns the canonical path of the (possibly already-existing) child. Throws
when parent is not a directory or name is not a single safe path segment.
name may not contain a separator, be blank, or be ./...
Create a single child directory `name` under existing directory `parent`. Returns the canonical path of the (possibly already-existing) child. Throws when `parent` is not a directory or `name` is not a single safe path segment. `name` may not contain a separator, be blank, or be `.`/`..`.
(workspace-create-trunk-at! db-info root)Mint a TRUNK workspace rooted at root (an arbitrary directory), not
pinned to any session. Lets a channel open a session under a directory
OTHER than the one vis was launched from — a tab in another project.
Returns the workspace row (with :id) to pass as :workspace-id when
creating the session.
Mint a TRUNK workspace rooted at `root` (an arbitrary directory), not pinned to any session. Lets a channel open a session under a directory OTHER than the one vis was launched from — a tab in another project. Returns the workspace row (with `:id`) to pass as `:workspace-id` when creating the session.
(workspace-cwd)Resolve the current workspace cwd. In production the channel
wrapper binds *workspace-root* per turn, so the process-cwd
fallback only fires from REPL / test / one-off CLI paths that have
no session context.
Resolve the current workspace cwd. In production the channel wrapper binds `*workspace-root*` per turn, so the process-cwd fallback only fires from REPL / test / one-off CLI paths that have no session context.
(workspace-display-label workspace)(workspace-display-label db-info workspace session)Human-facing label for workspace. Order: explicit :label →
pinned session title → clone name (:branch) → id prefix.
Human-facing label for `workspace`. Order: explicit `:label` → pinned session title → clone name (`:branch`) → id prefix.
(workspace-ensure-workspace! db-info {:keys [session-state-id]})Find-or-create the session's workspace. The DEFAULT is TRUNK — the user's real cwd (no clone). Resume returns whatever workspace the session was pinned to. Idempotent per session-state.
Find-or-create the session's workspace. The DEFAULT is TRUNK — the user's real cwd (no clone). Resume returns whatever workspace the session was pinned to. Idempotent per session-state.
(workspace-focus! db-info workspace-id)Stamp last_focused_at_ms and upsert the per-repo repo_focus
pointer. Returns the updated workspace record.
Stamp `last_focused_at_ms` and upsert the per-repo `repo_focus` pointer. Returns the updated workspace record.
(workspace-for-session db-info session-state-id)Workspace pinned to session-state-id, or nil.
Workspace pinned to `session-state-id`, or nil.
(workspace-get db-info workspace-id)Return the workspace with workspace-id, or nil.
Return the workspace with `workspace-id`, or nil.
(workspace-isolation-supported?)(workspace-isolation-supported? root)True when the current root can create full draft workspaces under the
current draft_backend setting.
True when the current root can create full draft workspaces under the current `draft_backend` setting.
(workspace-last-focused db-info repo-id)Workspace id from repo_focus for repo-id, or nil.
Workspace id from `repo_focus` for `repo-id`, or nil.
(workspace-list-active db-info repo-id)Active workspaces for repo-id, newest first.
Active workspaces for `repo-id`, newest first.
(workspace-list-active-with-sessions db-info repo-id)Like list-active but each entry is the {:workspace :session-state}
pair, sorted by last_focused_at_ms DESC NULLS LAST, then
created_at DESC.
Like `list-active` but each entry is the `{:workspace :session-state}`
pair, sorted by `last_focused_at_ms` DESC NULLS LAST, then
`created_at` DESC.(workspace-list-finished db-info repo-id)Discarded workspaces for repo-id, newest first.
Discarded workspaces for `repo-id`, newest first.
(workspace-normalize-root root)Canonicalize a workspace root string/File. Blank/nil → nil. Expand a leading
~ or ~/ against the current user's home directory.
Canonicalize a workspace root string/File. Blank/nil → nil. Expand a leading `~` or `~/` against the current user's home directory.
(workspace-register-hook! hook-id hook-fn)Register hook-fn for hook-id ∈ {:on-spawn :on-apply :on-approve :on-discard}.
Synchronous; exceptions swallowed.
Register `hook-fn` for `hook-id` ∈ {:on-spawn :on-apply :on-approve :on-discard}.
Synchronous; exceptions swallowed.(workspace-root env-or-root)Extract a canonical :workspace/root from an env map or raw root value.
Extract a canonical :workspace/root from an env map or raw root value.
(workspace-set-label! db-info {:keys [workspace-id label]})Set the workspace's human-friendly :label. Empty/nil clears it.
Set the workspace's human-friendly `:label`. Empty/nil clears it.
(workspace-status db-info workspace-id)Enrich a workspace record with live status. Stamps
:workspace/root, :workspace/sandbox?, :workspace/exists?, :workspace/changed
(count of since-fork edits) and :workspace/dirty?. No git.
Enrich a workspace record with live status. Stamps `:workspace/root`, `:workspace/sandbox?`, `:workspace/exists?`, `:workspace/changed` (count of since-fork edits) and `:workspace/dirty?`. No git.
(workspace-subdirs path)Child directory names (non-hidden) of path, case-insensitively sorted.
Empty vec when path is blank, not a directory, or unreadable.
Child directory names (non-hidden) of `path`, case-insensitively sorted. Empty vec when `path` is blank, not a directory, or unreadable.
(workspace-trunk-info)(workspace-trunk-info root)The user's real cwd (trunk). No git read; just the launch dir.
The user's real cwd (trunk). No git read; just the launch dir.
(workspace-with-session db-info workspace-id)Hydrate workspace-id with its pinned session_state. Returns
{:workspace <ws> :session-state <ss>}.
Hydrate `workspace-id` with its pinned `session_state`. Returns
`{:workspace <ws> :session-state <ss>}`.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 |