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 [:ir & 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 answer IR) 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 [:ir & 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 answer IR) 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.
(->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).
svar's make-router calls normalize-provider which auto-resolves
:base-url from svar's KNOWN_PROVIDERS table for built-in
providers, so we forward :base-url ONLY when the provider map
has one explicitly (vis-only providers like :github-models,
user overrides, or OAuth-supplied URLs). For known providers
svar fills in the URL itself - stop fighting it.
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`). svar's `make-router` calls `normalize-provider` which auto-resolves `:base-url` from svar's `KNOWN_PROVIDERS` table for built-in providers, so we forward `:base-url` ONLY when the provider map has one explicitly (vis-only providers like `:github-models`, user overrides, or OAuth-supplied URLs). For known providers svar fills in the URL itself - stop fighting it. 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.
(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 the id is already configured). Returns the new fleet or nil on no-op.
Append a provider config to the persisted fleet (no-op when the id is already configured). Returns the new fleet or nil on no-op.
(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.
(answer->ir answer)Lift a final-answer value into canonical IR.
The Markdown-answer pipeline produces exactly two final-answer shapes:
{:answer markdown} - plain-prose answer{:vis/answer-mode :needs-input :answer/text string} - needs-input gateReturns canonical [:ir & blocks]. nil yields [:ir {}].
Anything outside the two canonical shapes is an upstream bug.
Lift a final-answer value into canonical IR.
The Markdown-answer pipeline produces exactly two final-answer shapes:
- `{:answer markdown}` - plain-prose answer
- `{:vis/answer-mode :needs-input :answer/text string}` - needs-input gate
Returns canonical `[:ir & blocks]`. nil yields `[:ir {}]`.
Anything outside the two canonical shapes is an upstream bug.(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 user-images
skipped-images]})Initial provider messages for one turn. Deliberately excludes full prior
dialog transcript: Vis state flows through persisted iterations,
defs, and DB-backed tools. The current user message is tagged as
CURRENT-USER-MESSAGE.
One full previous-turn context block may be prepended so short follow-ups can inspect the prior exchange without replaying the whole session.
:user-images (from attachments/collect-user-images) turns the user
message multimodal: svar image blocks ride ahead of the text block and
an ATTACHED-IMAGES manifest inside the text names each one.
:skipped-images entries appear in the manifest only.
Initial provider messages for one turn. Deliberately excludes full prior dialog transcript: Vis state flows through persisted iterations, defs, and DB-backed tools. The current user message is tagged as `CURRENT-USER-MESSAGE`. One full previous-turn context block may be prepended so short follow-ups can inspect the prior exchange without replaying the whole session. `:user-images` (from `attachments/collect-user-images`) turns the user message multimodal: svar image blocks ride ahead of the text block and an `ATTACHED-IMAGES` manifest inside the text names each one. `:skipped-images` entries appear in the manifest only.
(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
extension reloads should replace this one
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
extension reloads should replace this one
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.(auto-archive-hot-symbols! _environment)Deprecated NOOP. Cross-turn def survival was removed when the
definition_* sidecar tables were dropped; the Python sandbox starts
fresh each turn, so there is nothing to archive.
Deprecated NOOP. Cross-turn def survival was removed when the `definition_*` sidecar tables were dropped; the Python sandbox starts fresh each turn, so there is nothing to archive.
(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.
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.
(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 Python sandbox.
Set `sym` -> `val` in the env's Python sandbox.
(build-system-prompt {:keys [system-prompt]})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.edn / .vis/config.edn /
~/.vis/config.edn, 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.
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.edn` / `.vis/config.edn` / `~/.vis/config.edn`, 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.
(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}`.(cancel! token)Abort the in-flight turn. Flips the cooperative flag AND runs every
registered on-cancel! callback. Each callback is wrapped in its
own try/catch so one bad consumer cannot starve the rest.
Safe to call multiple times — on the second call the flag is
already set, and the callback list has typically drained because
workers disposed themselves.
Abort the in-flight turn. Flips the cooperative flag AND runs every registered `on-cancel!` callback. Each callback is wrapped in its own `try`/`catch` so one bad consumer cannot starve the rest. Safe to call multiple times — on the second call the flag is already set, and the callback list has typically drained because workers disposed themselves.
(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 two 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.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 two 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.
`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.
(channel spec)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.
(coalesce-forms forms)Merge each maximal run of ADJACENT, successful coalescable native op-cards into
a single card. cat/patch coalesce only for the SAME file; format_code
coalesces adjacent per-file acks into one roll-up across files. Every other
form passes through untouched. The ONE projection both channels apply before
rendering an iteration's forms, so repeated tool acks never render as a stack
of look-alike sibling bubbles. Always returns a vector.
Merge each maximal run of ADJACENT, successful coalescable native op-cards into a single card. `cat`/`patch` coalesce only for the SAME file; `format_code` coalesces adjacent per-file acks into one roll-up across files. Every other form passes through untouched. The ONE projection both channels apply before rendering an iteration's forms, so repeated tool acks never render as a stack of look-alike sibling bubbles. Always returns a vector.
(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 spec)Build and validate a command map. Children are NOT validated
recursively; they're checked the first time dispatch! or
render-help walks into them, which keeps dynamic subcommands
from forcing their fn at build time.
Build and validate a command map. Children are NOT validated recursively; they're checked the first time `dispatch!` or `render-help` walks into them, which keeps dynamic subcommands from forcing their fn at build time.
(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).
(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
child]})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)(create-python-context custom-bindings roots-fn)(create-python-context custom-bindings roots-fn network-opts)Create the embedded-GraalPy sandbox context with all available bindings.
custom-bindings — map of symbol->value (tool fns + engine values). Fns are
wired as Python callables; values are marshalled. Returns:
{:python-context <org.graalvm.polyglot.Context> :sandbox-ns :python ; placeholder (Python has one top scope) :initial-ns-keys #{...baseline globals...}}
roots-fn (optional) — a 0-arg fn returning the current allowed root path
strings; when supplied the sandbox gets REAL filesystem access confined to
them. Omitted ⇒ no Python filesystem (IO-NONE).
Create the embedded-GraalPy sandbox context with all available bindings.
`custom-bindings` — map of symbol->value (tool fns + engine values). Fns are
wired as Python callables; values are marshalled. Returns:
{:python-context <org.graalvm.polyglot.Context>
:sandbox-ns :python ; placeholder (Python has one top scope)
:initial-ns-keys #{...baseline globals...}}
`roots-fn` (optional) — a 0-arg fn returning the current allowed root path
strings; when supplied the sandbox gets REAL filesystem access confined to
them. Omitted ⇒ no Python filesystem (IO-NONE).(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
With a single registered backend, omitting :backend works and the
facade tags the returned store map with the chosen backend so all
subsequent facade calls dispatch correctly.
Triggers manifest/scan-extensions! on first call so a freshly
started JVM with no extensions yet required has a chance to load
any backend-providing namespaces from the classpath.
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
With a single registered backend, omitting `:backend` works and the
facade tags the returned store map with the chosen backend so all
subsequent facade calls dispatch correctly.
Triggers `manifest/scan-extensions!` on first call so a freshly
started JVM with no extensions yet required has a chance to load
any backend-providing namespaces from the classpath.(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-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-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.
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).
(discover-extensions!)Public entry point for vis's extension wiring.
First loads the BUILT-IN extensions (load-builtin-extensions! — core
modules like the foundation v/ kernel that register via the same API but
ship in the main jar). Then runs manifest/scan-extensions! (which scans
every META-INF/vis-extension/vis.edn, requires every namespace listed
under each manifest's :nses key, and returns the merged parsed manifests)
and merges those manifests into this namespace's manifest registry. Returns
the count of namespaces declared under :nses across the merged manifests.
Idempotent on every layer.
Public entry point for vis's extension wiring. First loads the BUILT-IN extensions (`load-builtin-extensions!` — core modules like the foundation `v/` kernel that register via the same API but ship in the main jar). Then runs `manifest/scan-extensions!` (which scans every `META-INF/vis-extension/vis.edn`, `require`s every namespace listed under each manifest's `:nses` key, and returns the merged parsed manifests) and merges those manifests into this namespace's manifest registry. Returns the count of namespaces declared under `:nses` across the merged manifests. Idempotent on every layer.
(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.
Human-readable label for a provider id. Never persisted.
(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.
A sub_loop CHILD env BORROWS the parent's DB connection (:owns-db? false) —
disposing the child must NOT close it, or the parent loses its DB mid-turn.
Disposes a vis environment and releases resources. For persistent DBs (created with `:path`), data is preserved. For disposable DBs, all data is deleted. A sub_loop CHILD env BORROWS the parent's DB connection (`:owns-db?` false) — disposing the child must NOT close it, or the parent loses its DB mid-turn.
(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: 2 issues detected - run \bin/vis doctor` for details.when warn/error count > 0; nil otherwise. Caller decides whether to print (skipped when the command being dispatched ISvis doctor`).
Return a single-line string like `⚠ vis: 2 issues detected - run \`bin/vis doctor\` for details.` when warn/error count > 0; nil otherwise. Caller decides whether to print (skipped when the command being dispatched IS `vis doctor`).
(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-overrides)Persisted extension environment overrides from ~/.vis/config.edn
under :environment. Keys are environment variable names as
strings. Values are strings. This does NOT mutate the process
environment; extension code should call extension-env-value when
it wants config-over-env resolution.
Persisted extension environment overrides from `~/.vis/config.edn` under `:environment`. Keys are environment variable names as strings. Values are strings. This does NOT mutate the process environment; extension code should call `extension-env-value` when it wants config-over-env resolution.
(extension-env-status name)Return source and value metadata for an extension-declared env var.
:source is one of :config, :env, or :unset.
Return source and value metadata for an extension-declared env var. `:source` is one of `:config`, `:env`, or `:unset`.
(extension-env-value name)Resolve an extension-declared env var as config override -> real env. Blank/missing values return nil.
Resolve an extension-declared env var as config override -> real env. Blank/missing values return nil.
(extension-id-of-ns ns-sym)Reverse lookup: given a namespace symbol, return the extension id
that registered it under :nses, or nil.
Reverse lookup: given a namespace symbol, return the extension id that registered it under `:nses`, or `nil`.
(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-load-failures)Vec of {:source :extension-load :extension-id :extension-ns :path :class :reason} maps for every extension namespace whose
(require ns) threw during the most recent classpath scan, plus
any vis.edn that failed to parse. Empty vec when every namespace
loaded cleanly.
The shape of each entry matches (:project ctx) :warnings so
callers can splice it straight into environment context.
Vec of `{:source :extension-load :extension-id :extension-ns :path
:class :reason}` maps for every extension namespace whose
`(require ns)` threw during the most recent classpath scan, plus
any vis.edn that failed to parse. Empty vec when every namespace
loaded cleanly.
The shape of each entry matches `(:project ctx) :warnings` so
callers can splice it straight into environment context.(extension-namespaces id)Vector of namespaces declared under :nses for an id. Empty when
the id is unknown.
Vector of namespaces declared under `:nses` for an id. Empty when the id is unknown.
(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 --code.
Walk the AST and return a vector of strings, one per [:code ...] block, in source order. Used by `vis --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
~/.vis/config.edn 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 `~/.vis/config.edn` 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 the canonical display fields off a source map (loop chunk/block, a restored row) — the ONE projection every form builder + the gateway uses instead of hand-listing keys. Drops nils so a merge never stamps empty keys.
Project the canonical display fields off a source map (loop chunk/block, a restored row) — the ONE projection every form builder + the gateway uses instead of hand-listing keys. Drops nils so a merge never stamps empty keys.
Every field carried VERBATIM from the loop to a channel to render a form — the
complete passthrough set, NOT the handful the gateway computes/renames itself
(:stdout/:error are bounded, :silent/:duration_ms are derived; those
stay explicit gateway overrides). Add a new verbatim display field HERE and
->display/<-wire flow it across every boundary.
Grouped: the source the model wrote, the result surfaces (raw + the pre-rendered op-card), the native-tool badge identity (label + colour), the per-form display projections, the tool-call linkage, and the repair/timeout flags channels surface.
Every field carried VERBATIM from the loop to a channel to render a form — the complete passthrough set, NOT the handful the gateway computes/renames itself (`:stdout`/`:error` are bounded, `:silent`/`:duration_ms` are derived; those stay explicit gateway overrides). Add a new verbatim display field HERE and `->display`/`<-wire` flow it across every boundary. Grouped: the source the model wrote, the result surfaces (raw + the pre-rendered op-card), the native-tool badge identity (label + colour), the per-form display projections, the tool-call linkage, and the repair/timeout flags channels surface.
(form<-wire event)Read the canonical display fields back off a gateway WIRE event into a form,
tolerant of snake_case keys and re-keywording the keyword-valued fields. The
single inbound projection the channels use — the mirror of ->display.
Read the canonical display fields back off a gateway WIRE event into a form, tolerant of snake_case keys and re-keywording the keyword-valued fields. The single inbound projection the 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 and
the Telegram tagline): 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 and the Telegram tagline): 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 {:keys [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. :cached is the provider field;
:cached-input / :input-cached are accepted aliases so usage
maps can name the direction explicitly.
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. `:cached` is the provider field; `:cached-input` / `:input-cached` are accepted aliases so usage maps can name the direction explicitly. 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-attach-turn-sync! sid tid {:keys [on-event]})Attach to an ALREADY-submitted turn tid on sid and block until it reaches a
terminal event, returning the same engine-shaped result as submit-turn-sync!.
Creates NO new turn: it drives in-process (TUI) rendering for a turn the gateway
queued and then auto-drains, so a busy-time submission becomes a real gateway
queued record instead of a client-side shadow queue. Optional :on-event fires
for every replay/live event of tid.
Attach to an ALREADY-submitted turn `tid` on `sid` and block until it reaches a terminal event, returning the same engine-shaped result as `submit-turn-sync!`. Creates NO new turn: it drives in-process (TUI) rendering for a turn the gateway queued and then auto-drains, so a busy-time submission becomes a real gateway queued record instead of a client-side shadow queue. Optional `:on-event` fires for every replay/live event of `tid`.
(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-turn! sid tid)Fire the cancellation token of a running turn. Returns
{:status "cancelling"} or {:error ...}.
Fire the cancellation token of a running turn. Returns
`{:status "cancelling"}` or `{:error ...}`.(gateway-close-session! sid)DELETE a session: dispose the live environment, trash the session's draft clones (primary + auto-cloned filesystem roots — only DRAFTS have clones; a trunk workspace's roots are the user's real dirs and are never touched), then delete the session tree. Idempotent. NOTE: this is the DELETE path — merely quitting/closing a session (navigating away, no server call) keeps the draft intact so it can be resumed.
DELETE a session: dispose the live environment, trash the session's draft clones (primary + auto-cloned filesystem roots — only DRAFTS have clones; a trunk workspace's roots are the user's real dirs and are never touched), then delete the session tree. Idempotent. NOTE: this is the DELETE path — merely quitting/closing a session (navigating away, no server call) keeps the draft intact so it can be resumed.
(gateway-context-snapshot sid)The read-only ctx mirror the model sees as its bound session
(ctx-loop/session-snapshot), for an existing session, ENRICHED for
the USER with :session/archived (the GC'd/summarized entities that are
no longer in the model's live ctx). Resolving the env
through lp/env-for rehydrates an evicted session on demand.
nil when the session does not exist.
The read-only ctx mirror the model sees as its bound `session` (`ctx-loop/session-snapshot`), for an existing session, ENRICHED for the USER with `:session/archived` (the GC'd/summarized entities that are no longer in the model's live ctx). Resolving the env through `lp/env-for` rehydrates an evicted session on demand. nil when the session does not exist.
(gateway-create-session! {:keys [channel title external-id workspace-id]})Create a fresh gateway-managed session. Defaults to :api, but in-process
clients such as the TUI can pass :channel :tui and still use the same
gateway turn/event machinery without pretending to be an HTTP client.
Create a fresh gateway-managed session. Defaults to `:api`, but in-process clients such as the TUI can pass `:channel :tui` and still use the same gateway turn/event machinery without pretending to be an HTTP client.
(gateway-current-seq sid)Highest event :seq assigned for sid so far. Subscribing with this
as the cursor yields a live-only stream (empty replay).
Highest event `:seq` assigned for `sid` so far. Subscribing with this as the cursor yields a live-only stream (empty replay).
(gateway-delete-queued-turn! sid tid)Remove a queued turn before it starts. Returns deleted status or an error.
Remove a queued turn before it starts. Returns deleted status or an error.
(gateway-events-since sid cursor)Read-only peek at the replay ring: stored events with :seq > cursor,
oldest first. Lets a page renderer locate the running turn's
turn.started seq so its SSE reconnect can replay the WHOLE in-flight
turn instead of only what happens after connect.
Read-only peek at the replay ring: stored events with `:seq` > cursor, oldest first. Lets a page renderer locate the running turn's `turn.started` seq so its SSE reconnect can replay the WHOLE in-flight turn instead of only what happens after connect.
(gateway-get-turn sid tid)Wire view of one turn record, or nil.
Wire view of one turn record, or nil.
(gateway-list-sessions)(gateway-list-sessions channel)Wire souls for every persisted session.
CROSS-CHANNEL by default (channel = :all): a conversation started
in the web is visible in the TUI and vice-versa. Pass a specific
channel keyword only when a caller genuinely needs a single-channel
slice (e.g. Telegram resolving a chat by external-id).
Wire souls for every persisted session. CROSS-CHANNEL by default (`channel` = `:all`): a conversation started in the web is visible in the TUI and vice-versa. Pass a specific channel keyword only when a caller genuinely needs a single-channel slice (e.g. Telegram resolving a chat by external-id).
(gateway-list-turns sid)Wire views of every turn for sid, newest first: persisted history hydrated
from the engine DB (survives daemon restarts), plus only genuinely live gateway
overlay rows (running/queued or terminal rows not yet visible in persistence).
DEDUP: the gateway's tid is NOT the engine's persisted row id - the engine
mints its own id inside send!. Once the durable row is visible, prefer it:
it owns the iteration trace. Keeping the completed gateway row alongside the
persisted row rendered the last request/response twice after refresh, with the
transient duplicate missing the iterations disclosure.
Wire views of every turn for `sid`, newest first: persisted history hydrated from the engine DB (survives daemon restarts), plus only genuinely live gateway overlay rows (running/queued or terminal rows not yet visible in persistence). DEDUP: the gateway's `tid` is NOT the engine's persisted row id - the engine mints its own id inside `send!`. Once the durable row is visible, prefer it: it owns the iteration trace. Keeping the completed gateway row alongside the persisted row rendered the last request/response twice after refresh, with the transient duplicate missing the iterations disclosure.
(gateway-reconcile-running-turns!)Gateway facade for startup/client resume reconciliation of orphaned running turns.
Gateway facade for startup/client resume reconciliation of orphaned running turns.
(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 the live runtime for a session while keeping persisted data resumable.
This is the gateway facade for local clients that are merely closing a view
(for example a TUI tab or process exit). Use close-session! for DELETE.
Release the live runtime for a session while keeping persisted data resumable. This is the gateway facade for local clients that are merely closing a view (for example a TUI tab or process exit). Use `close-session!` for DELETE.
(gateway-session-model sid)The session's persisted model preference as {:provider :model}
(DB-backed shared store), or nil for the router default.
The session's persisted model preference as `{:provider :model}`
(DB-backed shared store), or nil for the router default.(gateway-session-model-cached sid)Cached variant of session-model for hot render paths. Still part of the
gateway facade: callers do not reach into the session-model store directly.
Cached variant of `session-model` for hot render paths. Still part of the gateway facade: callers do not reach into the session-model store directly.
(gateway-session-workspace sid)Workspace state for a channel surface (the web footer AND the TUI
directory picker): {:id :draft? :root :repo-root :label :fork-ms :filesystem-roots} for the session pinned to sid (soul id), or nil.
:id is the workspace-id every filesystem-root mutation
(add/remove-filesystem-root!) needs — WITHOUT it the TUI picker treats the
session as read-only and C-a silently no-ops. :filesystem-roots is the
normalized [{:trunk :clone :fork-ms}]. Lets the footer announce that the
session — and its extra roots — are isolated drafts. Resolves soul → latest
state → workspace; never throws.
Workspace state for a channel surface (the web footer AND the TUI
directory picker): `{:id :draft? :root :repo-root :label :fork-ms
:filesystem-roots}` for the session pinned to `sid` (soul id), or nil.
`:id` is the workspace-id every filesystem-root mutation
(`add/remove-filesystem-root!`) needs — WITHOUT it the TUI picker treats the
session as read-only and C-a silently no-ops. `:filesystem-roots` is the
normalized `[{:trunk :clone :fork-ms}]`. Lets the footer announce that the
session — and its extra roots — are isolated drafts. Resolves soul → latest
state → workspace; never throws.(gateway-set-session-model! sid provider model)Set (or clear, with blank model) the per-session PROVIDER + MODEL
preference. Every turn submitted for sid routes through it (the engine
reads it at turn start; router-for-model hoists the model, an unknown
name degrades to the default order). Channel-agnostic: web + TUI + embedded
callers all set it here, persisted in the DB and shared across channels.
Set (or clear, with blank model) the per-session PROVIDER + MODEL preference. Every turn submitted for `sid` routes through it (the engine reads it at turn start; `router-for-model` hoists the model, an unknown name degrades to the default order). Channel-agnostic: web + TUI + embedded callers all set it here, persisted in the DB and shared across channels.
(gateway-soul sid)Wire soul for one session: persisted record + live gateway status.
Wire soul for one session: persisted record + live gateway status.
(gateway-start!)(gateway-start! {:keys [port host token-file require-token?]})Start the gateway on the Ring Jetty adapter with virtual threads.
Returns {:port :host :token-file}. Throws when already running.
Safe to call from any host process - the daemon (vis serve), a TUI
run, or an embedded caller.
Start the gateway on the Ring Jetty adapter with virtual threads.
Returns `{:port :host :token-file}`. Throws when already running.
Safe to call from any host process - the daemon (`vis serve`), a TUI
run, or an embedded caller.(gateway-stop!)Stop the gateway server if running. Idempotent.
Stop the gateway server if running. Idempotent.
(gateway-submit-turn! sid
{:keys [request messages idempotency-key model
reasoning-default cancel-token extra-body
turn-features workspace engine-opts attachments]})Submit one turn for sid. Async: starts immediately when idle, otherwise queues.
Returns {:turn record} (plus :idempotent? true on an idempotency
replay) or {:error :session-not-found | :invalid-request, ...}. One engine
turn still runs per session; busy submissions become visible queued records.
Submit one turn for `sid`. Async: starts immediately when idle, otherwise queues.
Returns `{:turn record}` (plus `:idempotent? true` on an idempotency
replay) or `{:error :session-not-found | :invalid-request, ...}`. One engine
turn still runs per session; busy submissions become visible queued records.(gateway-submit-turn-sync! sid {:keys [on-event] :as opts})Submit one turn through the gateway and block until that turn reaches a terminal event.
Accepts the same request keys as submit-turn!; optional :on-event is called
for every replay/live event for the submitted turn. Returns an engine-shaped
result map for in-process clients (CLI/TUI/Telegram) that need a blocking
call without bypassing the canonical gateway machinery.
Submit one turn through the gateway and block until that turn reaches a terminal event. Accepts the same request keys as `submit-turn!`; optional `:on-event` is called for every replay/live event for the submitted turn. Returns an engine-shaped result map for in-process clients (CLI/TUI/Telegram) that need a blocking call without bypassing the canonical gateway machinery.
(gateway-subscribe! sid sub-id sink cursor)Register an SSE sink and return the replay vector (events with
:seq > cursor) ATOMICALLY with the registration, so no event can
fall between replay and live fan-out. The caller serializes replay
writes against live sink calls (see server.clj).
Before capturing replay, HYDRATE any turn currently running in a sibling
process from the cross-process journal (bus/hydrate!) — but only when this
process isn't already tracking a live turn (:current-turn unset), so an
already-mirrored turn isn't re-delivered to existing subscribers. This
materializes the running turn's row + ring HERE, so a watcher joining a turn
in flight elsewhere replays it from turn.started (user bubble + running
frame) instead of catching only the bare deltas after connect.
Register an SSE sink and return the replay vector (events with `:seq` > `cursor`) ATOMICALLY with the registration, so no event can fall between replay and live fan-out. The caller serializes replay writes against live sink calls (see server.clj). Before capturing replay, HYDRATE any turn currently running in a sibling process from the cross-process journal (`bus/hydrate!`) — but only when this process isn't already tracking a live turn (`:current-turn` unset), so an already-mirrored turn isn't re-delivered to existing subscribers. This materializes the running turn's row + ring HERE, so a watcher joining a turn in flight elsewhere replays it from `turn.started` (user bubble + running frame) instead of catching only the bare deltas after connect.
(gateway-transcript sid)Rich persisted transcript rows for sid: turns oldest-first, each carrying
its persisted iteration rows under :iterations. This is the gateway facade
escape hatch for in-process renderers that need full historical trace detail
without reaching around the gateway into persistence directly.
Rich persisted transcript rows for `sid`: turns oldest-first, each carrying its persisted iteration rows under `:iterations`. This is the gateway facade escape hatch for in-process renderers that need full historical trace detail without reaching around the gateway into persistence directly.
(gateway-update-queued-turn! sid tid request)Replace the prompt text for a queued turn. Returns the updated turn or an error.
Replace the prompt text for a queued turn. Returns the updated turn or an error.
(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.
(hide-tool-code? {:keys [error success?] :as form})Should a channel DROP a form's synthesized invocation source (the name(args)
code block) instead of showing it as code? YES for a SUCCESSFUL native tool
call (cat/rg/patch/…) that is NOT python_execution: the op-card
(result-card) already says what ran, so the source is redundant chrome.
python_execution — the model's OWN program — always keeps its code; an
ERRORED form keeps it too (the inline error needs the surrounding source).
The ONE predicate the TUI and web both consult so the code-chrome decision
can't drift between channels.
Should a channel DROP a form's synthesized invocation source (the `name(args)` code block) instead of showing it as code? YES for a SUCCESSFUL native tool call (cat/rg/patch/…) that is NOT `python_execution`: the op-card (`result-card`) already says what ran, so the source is redundant chrome. `python_execution` — the model's OWN program — always keeps its code; an ERRORED form keeps it too (the inline error needs the surrounding source). The ONE predicate the TUI and web both consult so the code-chrome decision can't drift between channels.
(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).
Checks :ext/requires - if the extension declares dependencies, all
listed extension namespaces must already be registered. Throws on
missing dependencies.
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). Checks `:ext/requires` - if the extension declares dependencies, all listed extension namespaces must already be registered. Throws on missing dependencies. 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.
(invoke-symbol-wrapper ext sym-entry args env)Full invocation pipeline for an observed tool symbol entry: before-fn -> fn -> after-fn, with on-error-fn catching :fn errors.
Every hook can override :fn, :args, :env via its return map. :before-fn can return {:result val} to short-circuit. :on-error-fn can return {:result val}, {:error err}, or {:fn :args :env} to retry.
The implementation's final value must be a canonical internal envelope.
The wrapper records channel/provenance from that envelope, then
returns only the payload :result to Python. Failure envelopes are converted
into thrown ex-info so ordinary Python error reporting handles them.
Raw helper symbols (:ext.symbol/raw? true) bypass this function entirely.
Full invocation pipeline for an observed tool symbol entry:
before-fn -> fn -> after-fn, with on-error-fn catching :fn errors.
Every hook can override :fn, :args, :env via its return map.
:before-fn can return {:result val} to short-circuit.
:on-error-fn can return {:result val}, {:error err}, or {:fn :args :env} to retry.
The implementation's final value must be a canonical internal envelope.
The wrapper records channel/provenance from that envelope, then
returns only the payload `:result` to Python. Failure envelopes are converted
into thrown ex-info so ordinary Python error reporting handles them.
Raw helper symbols (`:ext.symbol/raw? true`) bypass this function entirely.(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 2)
and joins them with ·.
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 2) and joins them with ` · `. 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 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:
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 `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.
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.
(limits-label+usage row)Compose "<label> <usage>" for a single row, or "<label>"
when the row has no usage signal. Returns nil when both are
blank/absent.
Compose `"<label> <usage>"` for a single row, or `"<label>"` when the row has no usage signal. Returns nil when both are blank/absent.
(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.
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.
(limits-row-has-signal? row)True when the row has any usage signal worth surfacing. Used to prefer non-zero rows when the visible area is tight.
True when the row has any usage signal worth surfacing. Used to prefer non-zero rows when the visible area is tight.
(list-resources session)Vector of live resource DATA maps for session, dead ones pruned first. This
is what the footer renders and what :session/resources carries into ctx —
ONLY the calling session's resources.
Vector of live resource DATA maps for `session`, dead ones pruned first. This is what the footer renders and what `:session/resources` carries into ctx — ONLY the calling session's resources.
(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 provider config in svar-native syntax from ~/.vis/config.edn.
Load provider config in svar-native syntax from `~/.vis/config.edn`.
(load-config-raw)Load raw config as global ~/.vis/config.edn, then the project-local
.vis/config.edn overlay, then the root vis.edn overlay. Later sources
win; nested maps merge; scalar/vector values replace.
Load raw config as global `~/.vis/config.edn`, then the project-local `.vis/config.edn` overlay, then the root `vis.edn` overlay. Later sources win; nested maps merge; scalar/vector values replace.
(load-python-extensions!)(load-python-extensions! {:keys [dirs]})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.
A file that fails to load is recorded in load-failures (and surfaced
by vis 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.
A file that fails to load is recorded in `load-failures` (and surfaced
by `vis 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.(markdown->ir text)(markdown->ir text {:keys [soft-break]})Parse a Markdown string into canonical answer-IR.
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 [:ir & blocks] directly (no further ->ast
round-trip needed). Empty / nil input yields [:ir {}].
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 answer-IR.
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 `[:ir & blocks]` directly (no further `->ast`
round-trip needed). Empty / nil input yields `[:ir {}]`.
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-fallback-note {:keys [llm-selected llm-fallback? llm-routing-trace]})Faint routing note, present only when the turn fell back to another model:
↳ from <selected-model> — <reason>, retried N×
reason prefers the HTTP status (429) on the fallback event, then the reason
keyword, then the free-form error. Retries count :llm.routing/provider-retry
events in the trace. Returns nil when there was no fallback. Shared so the TUI
can float it on its own faint row while CLI/Telegram fold it inline.
Faint routing note, present only when the turn fell back to another model: ↳ from <selected-model> — <reason>, retried N× `reason` prefers the HTTP status (429) on the fallback event, then the reason keyword, then the free-form error. Retries count `:llm.routing/provider-retry` events in the trace. Returns nil when there was no fallback. Shared so the TUI can float it on its own faint row while CLI/Telegram fold 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, the TUI bubble footer, and the Telegram tagline:
<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, the TUI bubble footer, and the Telegram tagline:
<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.(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-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.(native-tool-form? {tool-name :vis/tool-name})True when form is a NATIVE tool call (cat/rg/patch/…): it carries a
:vis/tool-name and therefore renders as an op-card via result-card.
True when `form` is a NATIVE tool call (cat/rg/patch/…): it carries a `:vis/tool-name` and therefore renders as an op-card via `result-card`.
(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.
(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.(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.
Deprecated misspelling retained as an alias for callers/searches using
'pallete'. Prefer palette.
Deprecated misspelling retained as an alias for callers/searches using 'pallete'. Prefer `palette`.
(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 `[]`.
(provider spec)Build and validate a provider descriptor.
Build and validate a provider descriptor.
(provider-auth-kind pid)How a provider id authenticates: :oauth (interactive flow owned by
the provider extension), :none (local, no credentials), or
:api-key.
How a provider id authenticates: `:oauth` (interactive flow owned by the provider extension), `:none` (local, no credentials), or `:api-key`.
(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-config-with-models preset models)Minimal persistable provider config for a preset + chosen models.
Minimal persistable provider config for a preset + chosen models.
(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 {:name str} model maps.
Preset `:default-models` as persisted `{:name str}` model maps.
(provider-default-model-names provider)Union of model names already on the provider map plus the preset /
provider :default-models, deduped. The list-building below sorts,
so order here is irrelevant.
Union of model names already on the provider map plus the preset / provider `:default-models`, deduped. The list-building below sorts, so order here is irrelevant.
(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.
Placeholder status while a real probe runs in the background.
(provider-limits provider-id)Return a normalized, spec-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 ::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, spec-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 `::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.
Normalized limits report for a provider id; an error report instead of a throw.
Local OpenAI-compatible providers that need no credentials.
Local OpenAI-compatible providers that need no credentials.
(provider-model-options provider)(provider-model-options provider default-models show-all?)Selectable model ids for a provider: live-fetched + defaults,
deduped, 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: live-fetched + defaults,
deduped, 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.
Provider presets not yet in the configured fleet — the 'Add Provider' picker contents.
(provider-status provider)Auth/liveness status for a CONFIGURED provider map. Local providers
are probed for real; an explicit :api-key is trusted; otherwise
the registered extension's status/detect fns answer. Never throws.
Auth/liveness status for a CONFIGURED provider map. Local providers are probed for real; an explicit `:api-key` is trusted; otherwise the registered extension's status/detect fns answer. 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, the TUI through markdown->ir + the IR 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, the TUI through `markdown->ir` + the IR 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.
Status of a REGISTERED provider descriptor via its `:provider/status-fn` (falling back to `:provider/detect-fn`). Never throws.
(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 most recent Python-extension scan:
[{:file <path> :error <message>} ...].
Load failures from the most recent Python-extension scan:
`[{:file <path> :error <message>} ...]`.(reasoning->ir text)Reasoning / thinking text -> canonical answer-IR. 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 answer-IR. 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 number of HIDDEN reasoning rows required before a thinking trace is
COLLAPSED behind a disclosure at all. If the remainder beyond the preview is
smaller than this, the whole trace renders inline — a +1 more toggle that
only reveals one extra line is pure friction. One source of truth shared by
the TUI thinking bubble and the web thinking card.
Minimum number of HIDDEN reasoning rows required before a thinking trace is COLLAPSED behind a disclosure at all. If the remainder beyond the preview is smaller than this, the whole trace renders inline — a `+1 more` toggle that only reveals one extra line is pure friction. One source of truth shared by the TUI thinking bubble and the web thinking card.
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.
(rediscover!)Force a fresh classpath scan, discarding the cached manifests.
Test/REPL utility - production code should use the idempotent
scan-extensions! instead.
Force a fresh classpath scan, discarding the cached manifests. Test/REPL utility - production code should use the idempotent `scan-extensions!` instead.
(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.
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. Call this immediately after `rebuild-router!` so the next `send!` on any cached session picks up the new router.
(register-backend! id ns-sym)Register a persistence backend implementation.
id - keyword identity, e.g. :sqlite.
ns-sym - fully qualified namespace symbol that defines the backend
functions (db-open!, db-close!, db-log!, every
store-*/db-* fn used by this facade). Vars are
resolved lazily via ns-resolve so REPL redefinition
just works.
Idempotent on id. Returns id.
Register a persistence backend implementation.
`id` - keyword identity, e.g. `:sqlite`.
`ns-sym` - fully qualified namespace symbol that defines the backend
functions (`db-open!`, `db-close!`, `db-log!`, every
`store-*`/`db-*` fn used by this facade). Vars are
resolved lazily via `ns-resolve` so REPL redefinition
just works.
Idempotent on `id`. Returns `id`.(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), persistence
backends (:ext/persistance) -- 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`), persistence backends (`:ext/persistance`) -- 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.
:struct_patch). :phase is :after (default — sees & may rewrite the result
envelope), :before (sees & may rewrite the args vector), or :around (MIDDLEWARE
— wraps the call). :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). :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. :struct_patch). `:phase` is :after (default — sees & may rewrite the result envelope), :before (sees & may rewrite the args vector), or :around (MIDDLEWARE — wraps the call). `: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). `: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 restart-fn alive-fn logs-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 :restart-fn :alive-fn :logs-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 :restart-fn
:alive-fn :logs-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! spec)Register one toggle. spec must satisfy :toggle/spec.
Re-registering the same :id is idempotent: metadata MERGES, the
live VALUE in state is preserved (user overrides survive reload).
Returns the canonical registered spec.
Register one toggle. `spec` must satisfy `:toggle/spec`. Re-registering the same `:id` is idempotent: metadata MERGES, the live VALUE in `state` is preserved (user overrides survive reload). Returns the canonical registered spec.
(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-backends)Map of registered backends keyed by id.
Map of registered backends keyed by id.
(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-extension-ids)Sorted vector of every extension id known to the manifest registry.
Sorted vector of every extension id known to the manifest registry.
(registered-extensions-summary)Pure data view of the manifest registry: returns {<id> {:nses [...]}}
for every loaded extension.
Pure data view of the manifest registry: returns `{<id> {:nses [...]}}`
for every loaded extension.(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, Telegram setMyCommands) 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, Telegram `setMyCommands`) use this env-less view. The engine dispatch path itself goes through `active-slashes env` so per-session activation-fn rules still hold.
(registered-startable-resources)Union of every registered extension's :ext/startable-resources — the
declarative resources any channel's Resources UI can start (each with its own
label + proposed options + start-fn).
A startable may declare a :visible-fn (() -> bool) to gate its appearance,
e.g. behind a feature toggle; non-visible ones are dropped here so EVERY
channel's Resources UI (web modal + TUI dialog) hides them uniformly.
Union of every registered extension's `:ext/startable-resources` — the declarative resources any channel's Resources UI can start (each with its own label + proposed options + start-fn). A startable may declare a `:visible-fn` (() -> bool) to gate its appearance, e.g. behind a feature toggle; non-visible ones are dropped here so EVERY channel's Resources UI (web modal + TUI dialog) hides them uniformly.
(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 "ext"
:cmd/doc "Run an extension command."
:cmd/subcommands #(registered-under ["ext"])}
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 "ext"
:cmd/doc "Run an extension command."
:cmd/subcommands #(registered-under ["ext"])}(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
~/.vis/config.edn, preserving unrelated global config keys. Project-local
.vis/config.edn is an overlay and is not edited by this writer. Returns
true when the global file changed.
Remove every persisted provider entry for `provider-id` from `~/.vis/config.edn`, preserving unrelated global config keys. Project-local `.vis/config.edn` is an overlay and is not edited by this writer. Returns true when the global file changed.
(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. Returns true when config changed.
Remove a provider from the persisted fleet AND run the registered extension's logout when present. Returns true when config changed.
(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 | [:ir ...] 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 | [:ir ...] 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. File reads return :anchors
as an ordered {"ln:hash" text} map — the key IS the patch from_anchor
— 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. File reads return `:anchors`
as an ordered `{"ln:hash" text}` map — the key IS the `patch from_anchor`
— so editing resolves straight off this structured data.(render-prompt {:keys [heading usage-note notes] :as opts})Render canonical :ext/prompt-fn text from symbol docstrings + arglists.
Accepts an extension map or any map with:
Returns a prompt string suitable for :ext/prompt-fn.
Render canonical :ext/prompt-fn text from symbol docstrings + arglists.
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 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 help`). Shows the root doc, then a single COMMANDS block listing every immediate subcommand.
(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 -> ~/.vis/config.edn.
Throws when nothing is available.
Resolve provider config: explicit -> `~/.vis/config.edn`. Throws when nothing is available.
(resolve-db-spec)(resolve-db-spec explicit-db-spec)Resolve DB spec: explicit -> VIS_DB_PATH env -> :db-spec from
config.edn -> default sqlite at ~/.vis/vis.mdb.
Resolve DB spec: explicit -> VIS_DB_PATH env -> `:db-spec` from config.edn -> default sqlite at `~/.vis/vis.mdb`.
(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-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.
(restart-resource! session id)Run a resource's :restart-fn (kept registered). Scoped to session. The
restart-fn owns re-registration of any changed DATA (e.g. a new port).
Run a resource's `:restart-fn` (kept registered). Scoped to `session`. The restart-fn owns re-registration of any changed DATA (e.g. a new port).
(result-card {:keys [tool-color-role result-summary result-render]
tool-name :vis/tool-name})Canonical tool-result CARD descriptor — the ONE place the op-card / collapse
decision is made, so the TUI and web AGREE on tool?/label/colour/summary/
collapsible instead of each re-deriving it from the raw form. Given an executed
form map, returns the op-card shape for a NATIVE TOOL result:
{:tool? true
:label "RG" — op-card badge label (tool-label)
:color-role :tool-color/search — badge colour role (keyword-normalized,
since a JSON wire hop stringifies it)
:summary "5 hits in 1 file" — the HEADLINE (:result-summary), nil
when the tool authored none
:body "…markdown…" — the detail body (:result-render), nil
for a summary-only tool (move/delete)
:collapsible? true} — true ⇔ there's a body to fold under
the summary (a chevron/<details>)
nil for a NON-tool form (no :vis/tool-name) — its result rendering stays
channel-specific (raw value / stdout). The badge is whatever the tool's
:summary already produced; no first-line-of-body heuristic.
Canonical tool-result CARD descriptor — the ONE place the op-card / collapse
decision is made, so the TUI and web AGREE on `tool?`/label/colour/summary/
collapsible instead of each re-deriving it from the raw form. Given an executed
form map, returns the op-card shape for a NATIVE TOOL result:
{:tool? true
:label "RG" — op-card badge label (`tool-label`)
:color-role :tool-color/search — badge colour role (keyword-normalized,
since a JSON wire hop stringifies it)
:summary "5 hits in 1 file" — the HEADLINE (`:result-summary`), nil
when the tool authored none
:body "…markdown…" — the detail body (`:result-render`), nil
for a summary-only tool (move/delete)
:collapsible? true} — true ⇔ there's a body to fold under
the summary (a chevron/`<details>`)
`nil` for a NON-tool form (no `:vis/tool-name`) — its result rendering stays
channel-specific (raw value / stdout). The badge is whatever the tool's
`:summary` already produced; no first-line-of-body heuristic.(result-cards form)The op-card descriptor(s) a form renders — the ONE place a channel asks "what cards does this form show?" so the TUI and web never re-derive it differently.
A python block that print()ed several tool results carries a :cards vector of
canonical mini-forms; each becomes its OWN op-card via result-card. Any other
form yields its single result-card (or none). Always a vector — channels just
iterate. Empty when the form has no tool card at all (plain value / stdout).
The op-card descriptor(s) a form renders — the ONE place a channel asks "what cards does this form show?" so the TUI and web never re-derive it differently. A python block that print()ed several tool results carries a `:cards` vector of canonical mini-forms; each becomes its OWN op-card via `result-card`. Any other form yields its single `result-card` (or none). Always a vector — channels just iterate. Empty when the form has no tool card at all (plain value / stdout).
(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 ~/.vis/config.edn:
{: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 `~/.vis/config.edn`:
```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)Walk every registered extension, invoke its :ext/doctor-fn,
return a vec of message maps with :ext auto-injected. The
extension's fn is responsible for stamping :check-id on each
message when it wants per-section grouping in the formatter.
Plan §10: extensions in registration order; messages in fn-return order. Activation-fn ignored: every registered extension's fn runs.
Walk every registered extension, invoke its `:ext/doctor-fn`, return a vec of message maps with `:ext` auto-injected. The extension's fn is responsible for stamping `:check-id` on each message when it wants per-section grouping in the formatter. Plan §10: extensions in registration order; messages in fn-return order. Activation-fn ignored: every registered extension's fn runs.
(save-config! config)(save-config! config source)Persist provider config to ~/.vis/config.edn.
When the first provider (the active provider) changes, the newly
selected provider's optional :provider/on-selected-fn is invoked
after the file write. Hook failures are logged and never prevent
config persistence.
Persist provider config to `~/.vis/config.edn`. When the first provider (the active provider) changes, the newly selected provider's optional `:provider/on-selected-fn` is invoked after the file write. Hook failures are logged and never prevent config persistence.
(save-config-providers! providers)(save-config-providers! providers source)Replace the :providers vec in the GLOBAL config file (project
overlay files are never edited), preserving unrelated keys, then
reload the in-memory config so the running router sees the change.
Returns the persisted vec.
Replace the `:providers` vec in the GLOBAL config file (project overlay files are never edited), preserving unrelated keys, then reload the in-memory config so the running router sees the change. Returns the persisted vec.
(save-extension-env-var! name value)Persist or clear one extension env override in ~/.vis/config.edn.
Blank/nil value removes the override, revealing the process env
value again if one exists. Preserves all other config keys.
Persist or clear one extension env override in `~/.vis/config.edn`. Blank/nil `value` removes the override, revealing the process env value again if one exists. Preserves all other config keys.
(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 lifted via markdown->ir so search hits the same shape
regardless of upstream contract.
Idempotent on canonical input via the markdown->ir 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 lifted via `markdown->ir` so search hits the same shape regardless of upstream contract. Idempotent on canonical input via the `markdown->ir` 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.
(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! python-context sym val)Bind sym -> val in the Python sandbox globals. Clojure fns are wired as
callables; everything else is marshalled.
ASYNC-BY-DEFAULT: a tool fn bound here is also DEFERRED (wrapped by
__vis_deferred__, same as build-agent-context's defer step) so
await tool(...) / gather(tool(...)) work. This matters because extension
and foundation tools are (re)installed via this fn AFTER the context's own
defer pass — without deferring here they'd stay raw/synchronous and the
await the prompt teaches would fail. The compaction verbs
(session_fold/session_drop/__vis_par__) are bound via create-python-context, not
here, so they stay direct. No-op when the async preamble isn't installed
(the printer/parser helper contexts never bind tools).
Bind `sym` -> `val` in the Python sandbox globals. Clojure fns are wired as callables; everything else is marshalled. ASYNC-BY-DEFAULT: a tool fn bound here is also DEFERRED (wrapped by `__vis_deferred__`, same as `build-agent-context`'s defer step) so `await tool(...)` / `gather(tool(...))` work. This matters because extension and foundation tools are (re)installed via this fn AFTER the context's own defer pass — without deferring here they'd stay raw/synchronous and the `await` the prompt teaches would fail. The compaction verbs (`session_fold`/`session_drop`/`__vis_par__`) are bound via `create-python-context`, not here, so they stay direct. No-op when the async preamble isn't installed (the printer/parser helper contexts never bind tools).
(set-session-model! db-info sid provider model)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).
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).(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 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 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, :telegram, ...)
: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, :telegram, ...)
: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-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.
(stop-resource! session id)Run a resource's :stop-fn and unregister it. 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. Returns a result map.
Run a resource's `:stop-fn` and unregister it. 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. 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 and must provide a symbol-specific channel renderer. The model-facing surface is the per-iteration trailer (real Python form values); no per-tool model-side render exists.
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).
:before-fn :after-fn :on-error-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 and must provide
a symbol-specific channel renderer. The model-facing surface is the
per-iteration trailer (real Python form values); no per-tool model-side
render exists.
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`).
:before-fn :after-fn :on-error-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!.
Engine-owned symbols hidden from user live-var listings.
Engine-owned symbols hidden from user live-var listings.
(test-python-extensions!)(test-python-extensions! {:keys [dirs]})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 GraalPy context via the built-in pytest-compat
shim. 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.
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 GraalPy context via the built-in `pytest`-compat
shim. 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.(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]})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), OR forced ON in the current dynamic scope
(*forced-on*). Fail-closed: returns false when id is not registered and
not forced. Hot-path — one set lookup + one atom deref.
Boolean cast of `(value-of id)`, OR forced ON in the current dynamic scope (`*forced-on*`). Fail-closed: returns `false` when `id` is not registered and not forced. Hot-path — one set lookup + 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 — coerce to boolean.
: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` — coerce to boolean.
`: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 persisted toggle values from (:toggles config-map). Silently
skips ids not in the registry so a stale config file from a
previous install can't break boot. Routes through set-value!
so enum entries get validated; individual invalid values are
dropped (logged via the listener) instead of aborting the whole
hydrate.
Bulk-apply persisted toggle values from `(:toggles config-map)`. Silently skips ids not in the registry so a stale config file from a previous install can't break boot. Routes through `set-value!` so enum entries get validated; individual invalid values are dropped (logged via the listener) instead of aborting the whole hydrate.
(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.
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.The canonical set of native-tool op-card BADGE colour roles — the ONE list both
channels colour against (TUI maps each to a lanterna fg, the web to a --tool-*
CSS var). Hand-maintained per-channel maps were free to drift; a guard test in
each channel asserts its map covers every role here, so a new role can't be
added in one channel and silently forgotten in the other.
The canonical set of native-tool op-card BADGE colour roles — the ONE list both channels colour against (TUI maps each to a lanterna fg, the web to a `--tool-*` CSS var). Hand-maintained per-channel maps were free to drift; a guard test in each channel asserts its map covers every role here, so a new role can't be added in one channel and silently forgotten in the other.
(tool-label wire-name)The op-card badge LABEL for a native tool's wire name: the name uppercased,
except the few label-overrides rename. ONE place both channels derive it from
so the TUI badge and the web label never drift. nil for a non-tool form.
The op-card badge LABEL for a native tool's wire name: the name uppercased, except the few `label-overrides` rename. ONE place both channels derive it from so the TUI badge and the web label never drift. nil for a non-tool form.
(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:
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 :info 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.
- :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> :result <value> :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}.
- :cost - Cost map {:input-cost N :output-cost N :total-cost N}.
- :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 a resource from session (does NOT run its stop-fn — caller decides).
Returns true if something was removed.
Drop a resource from `session` (does NOT run its stop-fn — caller decides). Returns true if something 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 the persisted provider entry with provider-id and
save. Returns the new fleet.
Apply `f` to the persisted provider entry with `provider-id` and save. Returns the new fleet.
(update-resource! session id patch)Patch the DATA of an existing resource (e.g. flip :status, refresh
:detail). No-op if unknown. Returns the updated DATA map or nil.
Patch the DATA of an existing resource (e.g. flip `:status`, refresh `:detail`). No-op if unknown. Returns the updated DATA map or nil.
(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).(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 e.g. the OpenAI
Codex verbosity cycle only appears when a Codex provider is actually
configured, and :settings? false toggles (e.g. reasoning-effort, which
has its own Ctrl+R control) 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 e.g. the OpenAI Codex verbosity cycle only appears when a Codex provider is actually configured, and `:settings? false` toggles (e.g. reasoning-effort, which has its own Ctrl+R control) 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-json-pretty x)Pretty-printed (2-space indent) JSON via [[->wire]] — for HUMAN-facing surfaces (the web ctx rail's trailer view), never the wire itself.
Pretty-printed (2-space indent) JSON via [[->wire]] — for HUMAN-facing surfaces (the web ctx rail's trailer view), never the wire itself.
(wire-json-str x)Encode any engine value as a JSON string via [[->wire]].
Encode any engine value as a JSON string via [[->wire]].
(worker-future f)(worker-future name f)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.
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. 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]})Release backend-owned roots and transition the row to :discarded. A :live
backend (trunk) owns no clone, so its shared root is never deleted.
Release backend-owned roots and transition the row to :discarded. A `:live` backend (trunk) owns no clone, so its shared root is never deleted.
(workspace-add-filesystem-root! db-info workspace-id path)Add path to the workspace's extra filesystem roots. Drafts require the same
isolation capabilities for every added root; live workspaces add it live.
Add `path` to the workspace's extra filesystem roots. Drafts require the same isolation capabilities for every added root; live workspaces add it live.
(workspace-apply! db-info {:keys [workspace-id]})Land the draft's since-fork edits into the user's real dirs (trunk),
leaving them uncommitted for the user to review/commit. A draft is about
the WHOLE workspace: this lands the primary clone AND every auto-cloned
filesystem root (each into its own trunk). Vis owns no git lifecycle.
Adds/modifications come from the mtime diff; deletions are files that
existed at the fork but the agent removed in the draft. Returns
{:status :ok :changed [{:status :path :root}] :landed n :workspace ws}.
Land the draft's since-fork edits into the user's real dirs (trunk),
leaving them uncommitted for the user to review/commit. A draft is about
the WHOLE workspace: this lands the primary clone AND every auto-cloned
filesystem root (each into its own trunk). Vis owns no git lifecycle.
Adds/modifications come from the mtime diff; deletions are files that
existed at the fork but the agent removed in the draft. Returns
`{:status :ok :changed [{:status :path :root}] :landed n :workspace ws}`.(workspace-backend backend)Validate and return a workspace backend descriptor.
Required keys: :workspace.backend/id keyword :workspace.backend/capabilities capability set :workspace.backend/available-fn ({:source-root :store-root} -> bool or {:available? bool :reason keyword :details map}) :workspace.backend/fork-fn ({:source-root :store-root :name} -> path) :workspace.backend/discard-fn ({:root} -> nil)
Validate and return a workspace backend descriptor.
Required keys:
:workspace.backend/id keyword
:workspace.backend/capabilities capability set
:workspace.backend/available-fn ({:source-root :store-root} -> bool or
{:available? bool :reason keyword :details map})
:workspace.backend/fork-fn ({:source-root :store-root :name} -> path)
:workspace.backend/discard-fn ({:root} -> nil)(workspace-capabilities-for workspace-or-root)Capability matrix for a workspace (or root path), using the real derived workspace storage location rather than assuming source and destination are on the same filesystem.
Capability matrix for a workspace (or root path), using the real derived workspace storage location rather than assuming source and destination are on the same filesystem.
(workspace-capability-matrix source-root)(workspace-capability-matrix source-root store-root)Describe every registered backend for source-root, including availability
and declared capabilities. This is the public feature-discovery surface.
It never loads extensions: extension discovery owns backend registration.
Describe every registered backend for `source-root`, including availability and declared capabilities. This is the public feature-discovery surface. It never loads extensions: extension discovery owns backend registration.
(workspace-change-root! db-info session-state-id path)Repoint session-state-id's PRIMARY filesystem root to path — the
session now works in a different project directory: shell cwd, relative
path resolution, file tools (cat/patch/write), and search (rg/find_files)
all follow from the next turn, because they resolve through the session's
pinned workspace. Additional filesystem roots CARRY OVER (they are
session-scoped permissions, not root-relative); an entry equal to the new
root is dropped as redundant. Refuses while the session is in a draft —
a draft is a fork of the OLD root, so apply/abandon it first. Returns the
newly pinned trunk workspace (or the current one when path already IS
the root).
Repoint `session-state-id`'s PRIMARY filesystem root to `path` — the session now works in a different project directory: shell cwd, relative path resolution, file tools (cat/patch/write), and search (rg/find_files) all follow from the next turn, because they resolve through the session's pinned workspace. Additional filesystem roots CARRY OVER (they are session-scoped permissions, not root-relative); an entry equal to the new root is dropped as redundant. Refuses while the session is in a draft — a draft is a fork of the OLD root, so apply/abandon it first. Returns the newly pinned trunk workspace (or the current one when `path` already IS the root).
(workspace-create! db-info
{:keys [session-state-id label from required-capabilities]})Create an isolated DRAFT using the strongest available backend and pin it
to :session-state-id. The backend must provide the full draft capability
set; core never silently falls back to a shared root.
The fork PARENT is chosen so apply! lands back where it forked from:
pass :from <parent-workspace> to clone that workspace's :root and
inherit its :repo-root (apply target); otherwise the parent is the
user's real cwd (trunk).
Create an isolated DRAFT using the strongest available backend and pin it to `:session-state-id`. The backend must provide the full draft capability set; core never silently falls back to a shared root. The fork PARENT is chosen so `apply!` lands back where it forked from: pass `:from <parent-workspace>` to clone that workspace's `:root` and inherit its `:repo-root` (apply target); otherwise the parent is the user's real cwd (trunk).
(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); the agent works directly in the repo
until /draft new. Resume returns whatever the session was pinned to
(trunk, or an open draft). Idempotent per session-state.
Find-or-create the session's workspace. The DEFAULT is TRUNK — the user's real cwd (no clone); the agent works directly in the repo until `/draft new`. Resume returns whatever the session was pinned to (trunk, or an open draft). Idempotent per session-state.
(workspace-filesystem-roots ws)Extra filesystem roots configured for ws, normalized to
[{:trunk :clone :fork-ms :backend}].
Extra filesystem roots configured for `ws`, normalized to
`[{:trunk :clone :fork-ms :backend}]`.(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.
True when the current root can create full draft workspaces.
(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.
Canonicalize a workspace root string/File. Blank/nil → nil.
(workspace-register-backend! backend)Register a workspace backend. Idempotent by backend id.
Register a workspace backend. Idempotent by backend id.
(workspace-register-hook! hook-id hook-fn)Register hook-fn for hook-id ∈ {:on-spawn :on-apply :on-discard}.
Synchronous; exceptions swallowed.
Register `hook-fn` for `hook-id` ∈ {:on-spawn :on-apply :on-discard}.
Synchronous; exceptions swallowed.(workspace-registered-backends)Registered workspace backends ordered by descending priority.
Registered workspace backends ordered by descending priority.
(workspace-remove-filesystem-root! db-info workspace-id path)Remove path from the workspace's extra filesystem roots and release any
backend-owned isolated root.
Remove `path` from the workspace's extra filesystem roots and release any backend-owned isolated root.
(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-supports? source-root required)(workspace-supports? source-root store-root required)True when some backend can provide required for the given roots.
True when some backend can provide `required` for the given roots.
(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 |