(answer-markdown answer)Disposable text projection of a final answer. Canonical typed content remains structured; this projection exists only for legacy text-only model context and will not be transported as a second answer shape.
Disposable text projection of a final answer. Canonical typed content remains structured; this projection exists only for legacy text-only model context and will not be transported as a second answer shape.
(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!`.(assign-project! session-id project-id)Assign the session soul to project-id (nil clears / removes from project).
Assign the session soul to `project-id` (nil clears / removes from project).
(auth-cooldown-metrics)Observability snapshot of the per-provider auth cooldown: the window length and the providers still excluded, with the epoch-ms the exclusion lifts and how many fallbacks landed inside the window.
Observability snapshot of the per-provider auth cooldown: the window length and the providers still excluded, with the epoch-ms the exclusion lifts and how many fallbacks landed inside the window.
(auth-refresh-metrics)Observability snapshot of the OAuth-refresh circuit breaker. Returns the
rolling window, the trip threshold, the per-provider count of forced
refreshes still inside the window, and the set of providers at the refresh
limit (breaker OPEN). Surfaced by the gateway /metrics endpoint so an
auth-refresh flap is visible at a glance instead of needing a vis.log
grep.
Observability snapshot of the OAuth-refresh circuit breaker. Returns the rolling window, the trip threshold, the per-provider count of forced refreshes still inside the window, and the set of providers at the refresh limit (breaker OPEN). Surfaced by the gateway `/metrics` endpoint so an auth-refresh flap is visible at a glance instead of needing a `vis.log` grep.
(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.
(by-id id)Return the session record (UUID :id) or nil.
Return the session record (UUID `:id`) or nil.
In-process env cache.
Keyed by java.util.UUID session-soul-id. Under the 1:1 session ↔
workspace invariant this key is isomorphic to (:workspace/id env)
— one cache entry = one session = one workspace = one Python sandbox
lineage. Lookups normalize incoming strings to UUID via cache-key
so string-id callers keep working alongside the UUID key.
In-process env cache. Keyed by `java.util.UUID` session-soul-id. Under the 1:1 session ↔ workspace invariant this key is isomorphic to `(:workspace/id env)` — one cache entry = one session = one workspace = one Python sandbox lineage. Lookups normalize incoming strings to UUID via `cache-key` so string-id callers keep working alongside the UUID key.
(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}`.(child-workspace! db-info parent-ws)Spawn the child's workspace. An isolation backend available for the parent's
root → a CoW clone of the parent's workspace (workspace/create! {:from parent-ws}): isolated writes, and workspace/apply! later lands the
since-fork diff back into the parent root. Else (Windows / non-POSIX, or no
backend) → a trunk row at the parent's root (create-trunk-at!): SHARED
files, no clone (safety = disjoint :files). Returns the workspace row.
Spawn the child's workspace. An isolation backend available for the parent's
root → a CoW clone of the parent's workspace (`workspace/create! {:from
parent-ws}`): isolated writes, and `workspace/apply!` later lands the
since-fork diff back into the parent root. Else (Windows / non-POSIX, or no
backend) → a trunk row at the parent's root (`create-trunk-at!`): SHARED
files, no clone (safety = disjoint `:files`). Returns the workspace row.(create! channel)(create! channel {:keys [title external-id workspace-id prewarm?]})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 prewarm?]})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.(custom-bindings env)Current custom sandbox bindings {sym -> value}.
Current custom sandbox bindings {sym -> value}.
(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-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.
(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.
(ensure-project-for-root! root)(ensure-project-for-root! owner-id root name)Get-or-create the project bound to canonical workspace root (a project IS a
tab set). Race-safe: on a UNIQUE(owner_id, workspace_root) collision from a
creator the insert throws and we re-read. name seeds a freshly created
project (falls back to the root path).
Get-or-create the project bound to canonical workspace `root` (a project IS a tab set). Race-safe: on a UNIQUE(owner_id, workspace_root) collision from a creator the insert throws and we re-read. `name` seeds a freshly created project (falls back to the root path).
(final-answer-gate-error environment iteration blocks)(final-answer-gate-error environment iteration blocks answer-value)(final-answer-gate-error environment
iteration
blocks
answer-value
active-extensions)(final-answer-gate-error environment
iteration
blocks
answer-value
active-extensions
extra-ctx)Dispatch :turn.answer/validate extension hooks against the
candidate answer. Returns nil when every hook accepts,
otherwise a single string surfaced as the rejected answer's
validation error.
A final answer is plain prose with no tool calls, so it inherently
never shares an iteration with extension/tool calls: the model uses
one iteration to observe tool output, then a later iteration replies
with the answer. Extensions that need an additional veto (e.g.
user-facing safety / format gates) still get their
:turn.answer/validate hook fired here.
active-extensions is passed by the turn loop so activation is
computed once per turn; direct callers may omit it and provide
:extensions on the environment.
Dispatch `:turn.answer/validate` extension hooks against the candidate answer. Returns nil when every hook accepts, otherwise a single string surfaced as the rejected answer's validation error. A final answer is plain prose with no tool calls, so it inherently never shares an iteration with extension/tool calls: the model uses one iteration to observe tool output, then a later iteration replies with the answer. Extensions that need an additional veto (e.g. user-facing safety / format gates) still get their `:turn.answer/validate` hook fired here. `active-extensions` is passed by the turn loop so activation is computed once per turn; direct callers may omit it and provide `:extensions` on the environment.
(gateway-runtime-metrics)Bounded process/runtime gauges for the gateway metrics endpoint. Values are sampled on demand; no profiler or background allocation is required.
Bounded process/runtime gauges for the gateway metrics endpoint. Values are sampled on demand; no profiler or background allocation is required.
(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-project-by-root root)(get-project-by-root owner-id root)Project bound to canonical workspace root for owner-id (default
"local"), or nil.
Project bound to canonical workspace `root` for `owner-id` (default "local"), 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.
(handle-iteration-exception! e ctx)Error path for the main-loop try/catch around run-iteration.
Infrastructure failures are terminal for the turn; model/format/code
failures still return {::iteration-error ...} for RLM self-correction.
Error path for the main-loop try/catch around `run-iteration`.
Infrastructure failures are terminal for the turn; model/format/code
failures still return `{::iteration-error ...}` for RLM self-correction.(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.
(iteration-loop environment
user-request
{:keys [system-prompt session-turn-id max-context-tokens hooks
cancel-atom cancel-token reasoning-default routing
extra-body reasoning-effort turn-features
allow-copilot-claude-deep? workspace-overrides]})The core iteration loop. Runs assemble -> ask LLM -> execute -> persist
until the model emits :answer or the user cancels.
The core iteration loop. Runs assemble -> ask LLM -> execute -> persist until the model emits `:answer` or the user cancels.
(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.(mark-policy-reload!)Invalidate every cached env's frozen security-policy snapshot. Registered as
a /reload hook: live sessions recycle at their next turn so vis.yml edits
to network domains / filesystem roots actually take effect. Returns nil.
Invalidate every cached env's frozen security-policy snapshot. Registered as a `/reload` hook: live sessions recycle at their next turn so vis.yml edits to network domains / filesystem roots actually take effect. Returns nil.
(markdown-answer? v)True for the canonical final-answer VALUE: {:answer string}.
The answer is the plain prose the model replies with; answer-fn wraps that
string into this {:answer string} shape. The only other accepted value is
the needs-input-answer? map.
True for the canonical final-answer VALUE: `{:answer string}`.
The answer is the plain prose the model replies with; `answer-fn` wraps that
string into this `{:answer string}` shape. The only other accepted value is
the `needs-input-answer?` map.(model-pricing model)Per-model price table entry (USD per MILLION tokens) for model, looked up
by exact model name in svar's MODEL_PRICING — {:input :output :cache-read :cached-input …} — or nil when the model isn't priced. Read-only view over
the same table estimate-token-cost bills against, so channel pickers show
the price that actually gets charged.
Per-model price table entry (USD per MILLION tokens) for `model`, looked up
by exact model name in svar's `MODEL_PRICING` — `{:input :output :cache-read
:cached-input …}` — or nil when the model isn't priced. Read-only view over
the same table `estimate-token-cost` bills against, so channel pickers show
the price that actually gets charged.(model-routing-status displayed-provider displayed-model)(model-routing-status router displayed-provider displayed-model)Live routing health for the model a channel is DISPLAYING (displayed-provider
displayed-model — the per-session pick or the config default the picker
shows).svar opens a circuit breaker on a provider after repeated transient failures
(5xx / 'Overloaded' 529 / dropped streams) and routes turns to the next
AVAILABLE provider so work keeps flowing. The displayed model is computed
from config ORDER and is NOT breaker-aware, so during an outage the picker
says opus while turns actually land on zai. This reconciles the two: when
the displayed provider's breaker is open/half-open, it reports what svar is
actually serving so the channel can surface
⚠ <displayed> overloaded — routing to <serving>.
Returns nil when the displayed provider is healthy, else
{:overloaded-provider <kw> :overloaded-model <str> :serving-provider <kw> :serving-model <str>}. serving-* is nil if every
provider is down.
Live routing health for the model a channel is DISPLAYING (`displayed-provider`
+ `displayed-model` — the per-session pick or the config default the picker
shows).
svar opens a circuit breaker on a provider after repeated transient failures
(5xx / 'Overloaded' 529 / dropped streams) and routes turns to the next
AVAILABLE provider so work keeps flowing. The displayed model is computed
from config ORDER and is NOT breaker-aware, so during an outage the picker
says `opus` while turns actually land on `zai`. This reconciles the two: when
the displayed provider's breaker is open/half-open, it reports what svar is
actually serving so the channel can surface
`⚠ <displayed> overloaded — routing to <serving>`.
Returns nil when the displayed provider is healthy, else
`{:overloaded-provider <kw> :overloaded-model <str>
:serving-provider <kw> :serving-model <str>}`. `serving-*` is nil if every
provider is down.(needs-input-answer? v)True for explicit clarification/needs-input answer payloads.
Foundation exposes this through (needs-input ...); the loop
keeps the predicate data-shaped instead of depending on foundation
namespaces so the core runtime has no extension cycle.
True for explicit clarification/needs-input answer payloads. Foundation exposes this through `(needs-input ...)`; the loop keeps the predicate data-shaped instead of depending on foundation namespaces so the core runtime has no extension cycle.
(parallel-sub-loops! parent-env specs)Run several sub-loop!s CONCURRENTLY on Clojure futures, bounded by
MAX-PARALLEL-SUBLOOPS (a Semaphore), and return their results as a vector in
INPUT ORDER. specs is a seq of {:prompt :subctx :models} maps (the model
passes a list of dicts; keys arrive keyword-snake at the GraalPy boundary).
All children share the parent's ONE db-info + depth-cap; the fast rift clone /
merge-back steps serialize on workspace-mutation-lock while the expensive
child LLM turns overlap. A child that throws does NOT sink the batch — its slot
becomes a {:status "failed" :error …} result so the coordinator can see the
failure and merge the rest. The sandbox denies Python threads, so concurrency
lives Clojure-side on the shared GraalVM Engine (forks are safe mid-eval).
Run several `sub-loop!`s CONCURRENTLY on Clojure futures, bounded by
`MAX-PARALLEL-SUBLOOPS` (a Semaphore), and return their results as a vector in
INPUT ORDER. `specs` is a seq of `{:prompt :subctx :models}` maps (the model
passes a list of dicts; keys arrive keyword-snake at the GraalPy boundary).
All children share the parent's ONE db-info + depth-cap; the fast rift clone /
merge-back steps serialize on `workspace-mutation-lock` while the expensive
child LLM turns overlap. A child that throws does NOT sink the batch — its slot
becomes a `{:status "failed" :error …}` result so the coordinator can see the
failure and merge the rest. The sandbox denies Python threads, so concurrency
lives Clojure-side on the shared GraalVM Engine (forks are safe mid-eval).Monotonic /reload epoch. Every /reload bumps it (via a reload hook).
A cache entry stamped with an older epoch is recycled at its NEXT turn
boundary so its immutable security-policy snapshot rebuilds from the
freshly-reloaded vis.yml. This is the sanctioned way /reload replaces the
frozen network-domain / filesystem-root policy: the snapshot drives the
GraalPy context, egress proxy, and process jail at env-creation time, so it
can only change by rebuilding the env — never by an in-place reseat.
Monotonic `/reload` epoch. Every `/reload` bumps it (via a reload hook). A cache entry stamped with an older epoch is recycled at its NEXT turn boundary so its immutable security-policy snapshot rebuilds from the freshly-reloaded vis.yml. This is the sanctioned way `/reload` replaces the frozen network-domain / filesystem-root policy: the snapshot drives the GraalPy context, egress proxy, and process jail at env-creation time, so it can only change by rebuilding the env — never by an in-place reseat.
(projects)(projects opts)List projects (cross-channel). opts: :owner-id (default "local"),
:include-archived?. Each carries a live :session-count.
List projects (cross-channel). `opts`: :owner-id (default "local"), :include-archived?. Each carries a live :session-count.
(reap-idle-envs!)One reaper sweep: dispose + evict cached session envs idle past
env-idle-ttl-ms (or, under heap pressure past env-heap-watermark-pct,
EVERY idle env this sweep — TTL ignored), then — if the cache still exceeds
env-cache-max — force-evict the least-recently-active idle entries until
back under the cap. Every eviction is lock-guarded (a running turn is
skipped). Returns the number of entries evicted. Safe to call directly
(tests / manual sweeps).
One reaper sweep: dispose + evict cached session envs idle past `env-idle-ttl-ms` (or, under heap pressure past `env-heap-watermark-pct`, EVERY idle env this sweep — TTL ignored), then — if the cache still exceeds `env-cache-max` — force-evict the least-recently-active idle entries until back under the cap. Every eviction is lock-guarded (a running turn is skipped). Returns the number of entries evicted. Safe to call directly (tests / manual sweeps).
(rebuild-router! config)Rebuild the router from the given config. Used when provider settings change.
Forwards :router opts so live config edits (e.g. tuning
:same-provider-delays-ms) take effect on the next set-provider!
without restarting the JVM.
Rebuild the router from the given config. Used when provider settings change. Forwards `:router` opts so live config edits (e.g. tuning `:same-provider-delays-ms`) take effect on the next `set-provider!` without restarting the JVM.
(refresh-cached-routers! router)Reseat :router on every cached env's environment map.
create-environment snapshots the router into
(:router env) at construction time, and the iteration loop calls
(svar/ask-code! (:router environment) ...) - not the global
router-atom. So when a frontend changes provider
config and rebuilds the global router, every long-lived env in the
cache (TUI keeps one for the whole session) keeps talking to the
previous model until disposed.
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.
(reorder-project-sessions! project-id session-ids)Atomically adopt any loose named session into project-id, then persist the
manual order. Guests owned by another project are never stolen.
Atomically adopt any loose named session into `project-id`, then persist the manual order. Guests owned by another project are never stolen.
(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`.
(retry-sub-loop! parent-env spec n)DECORATOR (not a composite): re-run the SAME spec until its child SUCCEEDS,
up to n total attempts (default 2). Returns the first successful result,
else the last failure — stamped with the :attempts made. (Contrast
selector-sub-loops!, which tries DIFFERENT alternatives.)
DECORATOR (not a composite): re-run the SAME `spec` until its child SUCCEEDS, up to `n` total attempts (default 2). Returns the first successful result, else the last failure — stamped with the `:attempts` made. (Contrast `selector-sub-loops!`, which tries DIFFERENT alternatives.)
(router-for-model router prefs)Return a router variant whose provider/model ORDER reflects a model PREFERENCE,
so svar's router picks + falls back accordingly — WE don't pick one model, we
express the preference and let the inner router decide (no svar change: it
already routes by the router's order). prefs is a model name OR an ORDERED
coll of names; matching models are hoisted to the front in preference order
(within each provider AND across providers), and the rest of the router follows
UNCHANGED as fallback. Blank/unknown prefs → the router as-is (child inherits the
parent's order). Coordinator: sub_loop(prompt, subctx, {"models": ["haiku", "sonnet"]}) (or a single "model") — try the cheap one first, fall back.
Vector order alone is DECORATION to svar, which selects by provider :priority
and then by the provider's :root model name. So the hoist is also written into
both fields: a matched provider's :root becomes its preferred model and the
whole fleet is renumbered from its new position. Without that, a coordinator's
models list changed the turn card and the cost row while every child turn
still ran the default provider's root model.
Return a router variant whose provider/model ORDER reflects a model PREFERENCE,
so svar's router picks + falls back accordingly — WE don't pick one model, we
express the preference and let the inner router decide (no svar change: it
already routes by the router's order). `prefs` is a model name OR an ORDERED
coll of names; matching models are hoisted to the front in preference order
(within each provider AND across providers), and the rest of the router follows
UNCHANGED as fallback. Blank/unknown prefs → the router as-is (child inherits the
parent's order). Coordinator: `sub_loop(prompt, subctx, {"models": ["haiku",
"sonnet"]})` (or a single `"model"`) — try the cheap one first, fall back.
Vector order alone is DECORATION to svar, which selects by provider `:priority`
and then by the provider's `:root` model name. So the hoist is also written into
both fields: a matched provider's `:root` becomes its preferred model and the
whole fleet is renumbered from its new position. Without that, a coordinator's
`models` list changed the turn card and the cost row while every child turn
still ran the default provider's root model.(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.
(run-iteration environment
messages
&
[{:keys [routing iteration reasoning-level reasoning-effort
resolved-model on-chunk extra-body llm-headers
active-extensions answer-validation-context]}])Runs a single RLM iteration: ask! -> check final -> execute code. Returns map with :thinking :blocks :final-result :api-usage etc.
Runs a single RLM iteration: ask! -> check final -> execute code. Returns map with :thinking :blocks :final-result :api-usage etc.
(run-turn! env user-request loop-opts)Store turn -> iteration-loop -> update turn -> return result.
Derives :prior-outcome (one of :complete, :cancelled, :error)
from the loop result and
persists it on the session_turn_state row. The next turn's
<system_state> digest reads it.
BEFORE the LLM round-trip, every turn is passed through
slash/dispatch. When the user-message resolves
to a registered slash, the turn is fully handled by a synthetic
iteration (tag :user-slash) and the LLM is never called. The
transcript still shows the user message + the slash envelope.
A slash NO extension claims (:reason :unknown) gets one more
chance as a PROMPT TEMPLATE (.vis/prompts/*.md, ~/.vis/prompts,
provider-contributed templates like /<name>): when a
template matches, the expanded text runs as a NORMAL LLM turn.
Registered slashes always win over templates.
Store turn -> iteration-loop -> update turn -> return result. Derives `:prior-outcome` (one of `:complete`, `:cancelled`, `:error`) from the loop result and persists it on the `session_turn_state` row. The next turn's `<system_state>` digest reads it. BEFORE the LLM round-trip, every turn is passed through `slash/dispatch`. When the user-message resolves to a registered slash, the turn is fully handled by a synthetic iteration (`tag :user-slash`) and the LLM is never called. The transcript still shows the user message + the slash envelope. A slash NO extension claims (`:reason :unknown`) gets one more chance as a PROMPT TEMPLATE (`.vis/prompts/*.md`, `~/.vis/prompts`, provider-contributed templates like `/<name>`): when a template matches, the expanded text runs as a NORMAL LLM turn. Registered slashes always win over templates.
(selector-sub-loops! parent-env specs):selector composite (a.k.a. fallback) — try specs IN ORDER until one
child SUCCEEDS, then STOP. Serial. Returns the vector of results tried, in
order: the failed alternatives followed by the first success (the last
result), or — if every alternative failed — all of them (all failures).
Mirrors the BT selector: any-succeed. (Unlike retry, the alternatives are
DIFFERENT specs.)
`:selector` composite (a.k.a. fallback) — try `specs` IN ORDER until one child SUCCEEDS, then STOP. Serial. Returns the vector of results tried, in order: the failed alternatives followed by the first success (the last result), or — if every alternative failed — all of them (all failures). Mirrors the BT selector: any-succeed. (Unlike `retry`, the alternatives are DIFFERENT specs.)
(sequence-sub-loops! parent-env specs):sequence composite — run specs IN ORDER, each only after the prior
SUCCEEDS, SHORT-CIRCUITING on the first failure. Serial by nature (each child
may depend on the last). Returns the vector of results ACTUALLY RUN, in order:
all of them when every child succeeded, or up to and INCLUDING the first
failure when it stopped early (that last result carries the failure). Mirrors
the BT sequence: all-succeed, fail-fast.
`:sequence` composite — run `specs` IN ORDER, each only after the prior SUCCEEDS, SHORT-CIRCUITING on the first failure. Serial by nature (each child may depend on the last). Returns the vector of results ACTUALLY RUN, in order: all of them when every child succeeded, or up to and INCLUDING the first failure when it stopped early (that last result carries the failure). Mirrors the BT sequence: all-succeed, fail-fast.
(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-title! id title)Host-driven title change. Resolves the live env (if any) so the
in-memory atom + listener fan-out stay in sync; falls back to a
plain DB write when no env is live for this session (e.g.
vis-agent sessions rename ops).
Host-driven title change. Resolves the live env (if any) so the in-memory atom + listener fan-out stay in sync; falls back to a plain DB write when no env is live for this session (e.g. `vis-agent sessions` rename ops).
(sub-loop! parent-env {:keys [prompt subctx models system-prompt]})Run a CHILD agentic loop for prompt over subctx (the model-supplied focused
slice; see subctx->seed-ctx). Forks a child session env (own ctx-atom seeded
from subctx, own forked Context on the shared Engine, own workspace per
the parent root's isolation backend, reusing the parent's SINGLE DB
connection + depth-cap),
optionally on a cheaper PROPOSED model preference list models
(router-for-model — always a vector, svar falls back). Runs run-turn!,
merges the child's workspace diff back (rift path), then ALWAYS tears the child
down — env disposed, rift clone trashed (both via guard, failures logged) so
nothing leaks across parallel/retry. Returns:
{:task_id <focus> :status <string> :answer :changed_files}
Throws :vis/subloop-depth-exceeded past MAX-SUBLOOP-DEPTH.
Run a CHILD agentic loop for `prompt` over `subctx` (the model-supplied focused
slice; see `subctx->seed-ctx`). Forks a child session env (own ctx-atom seeded
from subctx, own forked Context on the shared Engine, own workspace per
the parent root's isolation backend, reusing the parent's SINGLE DB
connection + depth-cap),
optionally on a cheaper PROPOSED model preference list `models`
(`router-for-model` — always a vector, svar falls back). Runs `run-turn!`,
merges the child's workspace diff back (rift path), then ALWAYS tears the child
down — env disposed, rift clone trashed (both via `guard`, failures logged) so
nothing leaks across `parallel`/`retry`. Returns:
{:task_id <focus> :status <string> :answer :changed_files}
Throws `:vis/subloop-depth-exceeded` past `MAX-SUBLOOP-DEPTH`.(subctx->seed-ctx _subctx)Seed ctx for a sub_loop child's ctx-atom from the model-supplied subctx.
Child contexts start from an empty engine ctx. PURE. Kept as the named seed site.
Seed ctx for a sub_loop child's ctx-atom from the model-supplied `subctx`. Child contexts start from an empty engine ctx. PURE. Kept as the named seed site.
(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!.
(sync-cached-extension-symbols!)Synchronize extension bindings in every idle cached session immediately.
A Settings change is process-wide while each session owns a persistent Python context. Busy contexts retain their started tool surface and are synchronized at the next turn boundary. Returns the refreshed count.
Synchronize extension bindings in every idle cached session immediately. A Settings change is process-wide while each session owns a persistent Python context. Busy contexts retain their started tool surface and are synchronized at the next turn boundary. Returns the refreshed count.
(turn! environment messages)(turn! environment messages opts)Runs one session turn on an RLM environment using iterative LLM code evaluation.
Params:
environment - RLM environment from create-environment.
messages - Vector of message maps. Always a vector, e.g.:
[(svar/user <prompt-text>)]
[(svar/user <prompt-text> (svar/image <b64> <mime-type>))]
opts - Map, optional:
high or max.
Catalog-gated and threaded unchanged through every iteration.Returns: Map with:
:error or :cancelled).Runs one session turn on an RLM environment using iterative LLM code evaluation.
Params:
`environment` - RLM environment from create-environment.
`messages` - Vector of message maps. Always a vector, e.g.:
[(svar/user <prompt-text>)]
[(svar/user <prompt-text> (svar/image <b64> <mime-type>))]
`opts` - Map, optional:
- :spec - Output spec for structured answers.
- :model - Override config's default model.
- :max-context-tokens - Token budget for context.
- :debug? - Enable verbose debug logging (default: false). Logs iteration details,
code evaluation, LLM responses at :debug level with :rlm-phase context.
- :reasoning-default - Optional base reasoning effort for reasoning-capable models.
Accepts :low/:medium/:high or low/medium/high strings. Adaptive escalation still applies.
- :reasoning-effort - Exact provider-native effort string, `high` or `max`.
Catalog-gated and threaded unchanged through every iteration.
- :extra-body - Optional provider-specific request-body params merged into the
upstream LLM call after auto max_tokens + reasoning translation.
Returns:
Map with:
- :trace - Vector of iteration trace entries, each containing:
{:iteration N
:response <llm-response-text>
:blocks [{:id 0 :code <code-str> :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} (canonical string keys).
- :cost - Cost map {"input_cost" N "output_cost" N "total_cost" N} (canonical string keys).
- :confidence - Confidence level (:high/:medium/:low) from final iteration.
- :reasoning - String summary of how the answer was derived (from LLM's FINAL call).
- :status - Only present on failure (`:error` or `:cancelled`).(validate-iteration-blocks! blocks)Fail fast if a stored/evaluated block lost mandatory envelope. Tool-result envelopes enforce their nested info separately; this spec enforces the outer block-level eval envelope for every executed block.
Fail fast if a stored/evaluated block lost mandatory envelope. Tool-result envelopes enforce their nested info separately; this spec enforces the outer block-level eval envelope for every executed block.
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 |