Extension subsystem: spec, builders, hook execution, the global registry, parse-error rescue, and manifest namespace catalog.
An extension is the SINGLE entry point for everything a third-party
bundle contributes to vis. Whatever surfaces it populates - Python
sandbox symbols, CLI commands, channels, providers, persistence
backends - it does so by listing them in the matching :ext/<surface>
slot, and register-extension! dispatches each slot to its concrete
sub-registry. The same data feeds:
:ext.engine/symbols:ext/hooks checksChannel and provider registries live in internal.registry (the
sub-registry that lights up when this module dispatches their
contributions). Backend dispatch lives in internal.persistance.
Classpath manifest scanning lives in internal.manifest. This
module is the only consumer of all four.
Extension subsystem: spec, builders, hook execution, the global registry, parse-error rescue, and manifest namespace catalog. An extension is the SINGLE entry point for everything a third-party bundle contributes to vis. Whatever surfaces it populates - Python sandbox symbols, CLI commands, channels, providers, persistence backends - it does so by listing them in the matching `:ext/<surface>` slot, and `register-extension!` dispatches each slot to its concrete sub-registry. The same data feeds: - the active-extensions list every iteration consults - the system-prompt block rendered from `:ext.engine/symbols` - the per-iteration `:ext/hooks` checks - the parse-error rescue chain - the manifest id/namespace catalog used for extension metadata Channel and provider registries live in `internal.registry` (the sub-registry that lights up when this module dispatches their contributions). Backend dispatch lives in `internal.persistance`. Classpath manifest scanning lives in `internal.manifest`. This module is the only consumer of all four.
Extension map currently executing on an extension callback thread. Bound by symbol wrappers so extension-owned helper APIs can fill the caller's stable extension identity without accepting user-supplied ids.
Extension map currently executing on an extension callback thread. Bound by symbol wrappers so extension-owned helper APIs can fill the caller's stable extension identity without accepting user-supplied ids.
Zero-based index of the top-level form currently evaluating, bound
per-form by run-python-code so the render sink writer can stamp
:form-idx on every entry.
The render sink atom itself is iteration-scoped (one channel-sink
per run-python-code invocation, fed by every tool call in the
block, which runs as one whole-block coroutine).
Zero-based index of the top-level form currently evaluating, bound per-form by `run-python-code` so the render sink writer can stamp `:form-idx` on every entry. The render sink atom itself is iteration-scoped (one channel-sink per `run-python-code` invocation, fed by every tool call in the block, which runs as one whole-block coroutine).
Sandbox symbol currently executing, when a symbol callback is active.
Sandbox symbol currently executing, when a symbol callback is active.
Optional per-eval sink for observable tool lifecycle events. Bound by tests and UI/progress adapters that need to know a tool started before its fn returns. The sink receives plain event maps.
Optional per-eval sink for observable tool lifecycle events. Bound by tests and UI/progress adapters that need to know a tool started before its fn returns. The sink receives plain event maps.
(active-protected-globs environment)Aggregate protected path rules from active extensions in extension order.
Each active extension may declare :ext/protected-paths as
(fn [env] -> [{:glob string :access :read-only|:read-write|:none :hint string} ...]). Rule order is preserved within
each extension; the foundation editing core resolves first-match-wins
per extension and most-restrictive-wins globally. Returned rows are
enriched with :extension/name for diagnostics.
Aggregate protected path rules from active extensions in extension order.
Each active extension may declare `:ext/protected-paths` as
`(fn [env] -> [{:glob string :access :read-only|:read-write|:none
:hint string} ...])`. Rule order is preserved within
each extension; the foundation editing core resolves first-match-wins
per extension and most-restrictive-wins globally. Returned rows are
enriched with `:extension/name` for diagnostics.(builtin-sandbox-bindings env-thunk){sym -> fn} bindings for EVERY registered built-in extension
(ext-builtin?), merged into the Python sandbox globals alongside the engine
verbs at sandbox-context creation. env-thunk (0-arg) resolves the live
environment at call time, so these can be wired before the env map exists.
Loads built-ins first (idempotent) so registration is guaranteed before we
read the registry. Later extensions win on key collisions, but built-ins are
disjoint by construction (kernel tools vs engine verbs).
Each value is the plain wrapped tool fn; env_python/create-python-context
installs it as a Python callable (ProxyExecutable). Per-tool docstrings are
surfaced through the sandbox's own doc/apropos introspection.
`{sym -> fn}` bindings for EVERY registered built-in extension
(`ext-builtin?`), merged into the Python sandbox globals alongside the engine
verbs at sandbox-context creation. `env-thunk` (0-arg) resolves the live
environment at call time, so these can be wired before the env map exists.
Loads built-ins first (idempotent) so registration is guaranteed before we
read the registry. Later extensions win on key collisions, but built-ins are
disjoint by construction (kernel tools vs engine verbs).
Each value is the plain wrapped tool fn; `env_python/create-python-context`
installs it as a Python callable (ProxyExecutable). Per-tool docstrings are
surfaced through the sandbox's own `doc`/`apropos` introspection.Hook-task lifetime policies. Controls how long an emitted hook-task
lingers in :session/tasks after the hint stops firing.
:session Default. Task survives across turns and is GC'd by the
standard TTL machinery (TTL-TASK-DONE/
TTL-TASK-CANCELLED turns after terminal status). Right
for cross-turn concerns like
:vis.foundation/session-title whose work product (the
session title) is itself session-scoped.
:turn Ephemeral. Task is dropped from :session/tasks at
advance-turn regardless of status. If the originating
hint condition still holds, the next iter recreates the
task; if not, it stays gone. Right for transient signals
like :vis.foundation/context-pressure whose advisory
value evaporates the moment the next request's input
size drops below threshold. Prevents the cargo-cult
pattern where a stale :done :validated? false task
keeps showing up in the CTX render for 6 turns and the
model keeps re-emitting (task-set! … :done) to silence
it.
:iteration Hyper-transient. Task is dropped at advance-iter
(every iter boundary), not just turn boundary. Right
for hints whose firing condition is recomputed from
per-iter state (e.g. a one-iter retry-shape warning,
an in-flight tool-call status banner). The next iter's
hook fire is the single source of truth; if the
condition still holds the task re-materialises
immediately.
Hook-task lifetime policies. Controls how long an emitted hook-task
lingers in `:session/tasks` after the hint stops firing.
:session Default. Task survives across turns and is GC'd by the
standard TTL machinery (`TTL-TASK-DONE`/
`TTL-TASK-CANCELLED` turns after terminal status). Right
for cross-turn concerns like
`:vis.foundation/session-title` whose work product (the
session title) is itself session-scoped.
:turn Ephemeral. Task is dropped from `:session/tasks` at
`advance-turn` regardless of status. If the originating
hint condition still holds, the next iter recreates the
task; if not, it stays gone. Right for transient signals
like `:vis.foundation/context-pressure` whose advisory
value evaporates the moment the next request's input
size drops below threshold. Prevents the cargo-cult
pattern where a stale `:done :validated? false` task
keeps showing up in the CTX render for 6 turns and the
model keeps re-emitting `(task-set! … :done)` to silence
it.
:iteration Hyper-transient. Task is dropped at `advance-iter`
(every iter boundary), not just turn boundary. Right
for hints whose firing condition is recomputed from
per-iter state (e.g. a one-iter retry-shape warning,
an in-flight tool-call status banner). The next iter's
hook fire is the single source of truth; if the
condition still holds the task re-materialises
immediately.Canonical namespaced lifecycle phases accepted by :ext/hooks.
Canonical namespaced lifecycle phases accepted by `:ext/hooks`.
(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.
(ctx-contributions environment active-extensions)Return merged structured ctx contributions for active extensions.
Each active extension may declare :ext/ctx-fn as (fn [env] -> map).
The contribution CONTRACT is STRING-KEYED: top-level keys are the
folded session_* strings ("session_env", "session_workspace", ...)
and values are string-keyed all the way down — the merged map crosses the
Python boundary as the model's session dict, which throws on any keyword
key/value. This fn only aggregates (deep-merge); producers own the keys.
Exceptions and non-map returns are logged and ignored so bad optional
context never blocks a turn.
Return merged structured `ctx` contributions for active extensions. Each active extension may declare `:ext/ctx-fn` as `(fn [env] -> map)`. The contribution CONTRACT is STRING-KEYED: top-level keys are the folded `session_*` strings (`"session_env"`, `"session_workspace"`, ...) and values are string-keyed all the way down — the merged map crosses the Python boundary as the model's `session` dict, which throws on any keyword key/value. This fn only aggregates (deep-merge); producers own the keys. Exceptions and non-map returns are logged and ignored so bad optional context never blocks a turn.
(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.
(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.
(envelope-failure? envelope)True when envelope is an :envelope and :success? is
false (i.e. failure path with a structured :error). Returns
false for non-envelopes.
True when `envelope` is an `:envelope` and `:success?` is false (i.e. failure path with a structured `:error`). Returns false for non-envelopes.
(envelope-success? envelope)True when envelope is an :envelope and :success? is
true. Use this instead of raw (:success? e) in renderers and
guards — it (a) reads as English and (b) returns false for non-
envelopes (defensive against shape drift).
True when `envelope` is an `:envelope` and `:success?` is true. Use this instead of raw `(:success? e)` in renderers and guards — it (a) reads as English and (b) returns false for non- envelopes (defensive against shape drift).
(ex->op-error t & [{:keys [form-source hint]}])Convert an arbitrary Throwable to a structured :error map.
Output shape: {:message <one-line headline, required> :trace <preformatted multi-line string, optional> :hint <recovery suggestion, optional> :block {:source :phase :row :col :opened-loc?, optional}}
Throwables reaching here are transport / spec / wrapping failures —
Python eval errors are mapped to op-error shape inside the engine via
env-python/map-polyglot-error, so the block :phase is :preflight.
Optional opts:
:form-source the verbatim source the form was built from;
embedded in :block.source so the model sees its
own input echoed back.
:hint override / pre-supply a recovery hint string.
Convert an arbitrary `Throwable` to a structured `:error` map.
Output shape:
{:message <one-line headline, required>
:trace <preformatted multi-line string, optional>
:hint <recovery suggestion, optional>
:block {:source :phase :row :col :opened-loc?, optional}}
Throwables reaching here are transport / spec / wrapping failures —
Python eval errors are mapped to op-error shape inside the engine via
`env-python/map-polyglot-error`, so the block `:phase` is `:preflight`.
Optional opts:
:form-source the verbatim source the form was built from;
embedded in `:block.source` so the model sees its
own input echoed back.
:hint override / pre-supply a recovery hint string.(ext-builtin? ext)True when this extension is a BUILT-IN: its symbols bind BARE into the
sandbox ns (no alias), alongside the engine verbs. See
builtin-sandbox-bindings.
True when this extension is a BUILT-IN: its symbols bind BARE into the sandbox ns (no alias), alongside the engine verbs. See `builtin-sandbox-bindings`.
(extension spec)Build and validate an extension. The canonical constructor.
See docs/src/extensions/extension-spec.md for the full key list.
Build and validate an extension. The canonical constructor. See docs/src/extensions/extension-spec.md for the full key list.
(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-info ext)Canonical extension info map.
Merges author-declared extension metadata with source markers: {:namespace :alias? :doc? :kind? :version? :author? :owner? :license? :registry-id? :source-paths :source-mtime-max :source-hash-sha256}
This is the single info shape used by ctx :extensions and tool-result enrichment.
Canonical extension info map.
Merges author-declared extension metadata with source markers:
{:namespace :alias? :doc? :kind? :version? :author? :owner?
:license? :registry-id? :source-paths :source-mtime-max
:source-hash-sha256}
This is the single info shape used by ctx :extensions and tool-result enrichment.(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).(failure {:keys [error throwable] :as args})Construct a failing tool-result envelope. :throwable auto-builds
an :error map via normalize-error. Explicit :error
(already structured) wins.
Construct a failing tool-result envelope. `:throwable` auto-builds an `:error` map via `normalize-error`. Explicit `:error` (already structured) wins.
(helper v)(helper v opts)Build a raw callable helper entry FROM A CLOJURE VAR.
Helpers are bound as plain values in Python, not observed tools: no envelope
validation, no channel renderer. Use for composable host helper functions
such as snapshot, not for user-observable tool calls.
Build a raw callable helper entry FROM A CLOJURE VAR. Helpers are bound as plain values in Python, not observed tools: no envelope validation, no channel renderer. Use for composable host helper functions such as `snapshot`, not for user-observable tool calls.
(hook-lifetime? lifetime)True when lifetime is one of the canonical hook-task lifetimes.
True when `lifetime` is one of the canonical hook-task lifetimes.
(hook-phase? phase)True when phase is a canonical namespaced hook phase.
True when `phase` is a canonical namespaced hook phase.
(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.(load-extension! ns-sym)Dynamically load extension namespace and return extensions it registered.
Dynamically load extension namespace and return extensions it registered.
(merge-into-metadata envelope extra)Merge extra into the :metadata slot of an already-valid
envelope, re-check the contract, and preserve metadata. Used by the
extension wrapper to stamp extension/source info onto tool-like
returns.
Merge `extra` into the `:metadata` slot of an already-valid envelope, re-check the contract, and preserve metadata. Used by the extension wrapper to stamp extension/source info onto tool-like returns.
(native-tool-call-shapes active-extensions)(native-tool-call-shapes active-extensions env)Map wire-name → the symbol's :call synthesis shape (a shape map or a
(fn [input] -> source)) for every ACTIVE native tool that declares one.
tool-call->python-source consults this to build the Python a tool call runs;
a tool with no :call is ABSENT here and falls back to the generic
name({input}) form. Single source — the shape lives WITH its symbol, so a new
module's tool needs NO engine edit.
Map wire-name → the symbol's `:call` synthesis shape (a shape map or a
`(fn [input] -> source)`) for every ACTIVE native tool that declares one.
`tool-call->python-source` consults this to build the Python a tool call runs;
a tool with no `:call` is ABSENT here and falls back to the generic
`name({input})` form. Single source — the shape lives WITH its symbol, so a new
module's tool needs NO engine edit.(native-tool-color-roles active-extensions)Map wire-name → :color-role keyword (read / search / edit / …) for every
declared native tool — the per-tool result-badge colour. NOT active-filtered
(mirrors renderers).
Map wire-name → `:color-role` keyword (read / search / edit / …) for every declared native tool — the per-tool result-badge colour. NOT active-filtered (mirrors renderers).
(native-tool-handlers active-extensions)(native-tool-handlers active-extensions env)Map wire-name → :handler (fn [env input] -> result) for every ACTIVE native
tool that declares one. A handler tool is dispatched DIRECTLY in Clojure by the
loop (no synthesized Python); one without runs the legacy synthesized-Python
path. Inactive (:active-fn false) tools are excluded — unadvertised ⇒
undispatchable.
Map wire-name → `:handler` `(fn [env input] -> result)` for every ACTIVE native tool that declares one. A handler tool is dispatched DIRECTLY in Clojure by the loop (no synthesized Python); one without runs the legacy synthesized-Python path. Inactive (`:active-fn` false) tools are excluded — unadvertised ⇒ undispatchable.
(native-tool-renderers active-extensions)Map wire-name → :render fn for every declared native tool. A :render takes
the tool's RESULT and returns a {:summary :body} op-card both the TUI and web
paint — a clean card, never a raw args+result dump. NOT active-filtered: a
result that already ran must still render even if its toggle flipped after.
Map wire-name → `:render` fn for every declared native tool. A `:render` takes
the tool's RESULT and returns a `{:summary :body}` op-card both the TUI and web
paint — a clean card, never a raw args+result dump. NOT active-filtered: a
result that already ran must still render even if its toggle flipped after.(native-tool-renderers-by-op active-extensions)Map op-STRING (the value stamp-public-result-op writes into a result's :op,
e.g. "cat") -> {:render :color-role}. Resolves the op-card of a TOOL RESULT
the model print()ed in Python, where the only origin handle is :op. Mirrors
native-tool-renderers (NOT active-filtered: a printed result must still render
even if its toggle flipped).
Map op-STRING (the value `stamp-public-result-op` writes into a result's `:op`,
e.g. "cat") -> `{:render :color-role}`. Resolves the op-card of a TOOL RESULT
the model print()ed in Python, where the only origin handle is `:op`. Mirrors
`native-tool-renderers` (NOT active-filtered: a printed result must still render
even if its toggle flipped).(native-tool-schemas active-extensions)(native-tool-schemas active-extensions env)The model-facing :tools surface: {:name :description :schema} for every
ACTIVE native tool, in extension/symbol order. Single source — schema lives
WITH its symbol.
The model-facing `:tools` surface: `{:name :description :schema}` for every
ACTIVE native tool, in extension/symbol order. Single source — schema lives
WITH its symbol.(native-tool-tags active-extensions)Map wire-name → :tag (:observation | :mutation) for every declared native
tool that carries a :tag. The tag is the AUTHORITATIVE observation/mutation
classification declared INLINE on each (vis/symbol …) opts map (never a
hardcoded name list). Used by the loop to decide whether an all-observation
iteration may run its calls concurrently. NOT active-filtered (mirrors
native-tool-color-roles): the classification of a tool never depends on
whether its toggle is currently on.
Map wire-name → `:tag` (`:observation | :mutation`) for every declared native tool that carries a `:tag`. The tag is the AUTHORITATIVE observation/mutation classification declared INLINE on each `(vis/symbol …)` opts map (never a hardcoded name list). Used by the loop to decide whether an all-observation iteration may run its calls concurrently. NOT active-filtered (mirrors `native-tool-color-roles`): the classification of a tool never depends on whether its toggle is currently on.
(native-tools-for active-extensions)(native-tools-for active-extensions env)Every native tool on active-extensions' symbols — ONE walk, the single source
the schemas / handlers / renderers / colours all project from, NORMALIZED to
{:name :description :schema :handler :render :color-role :active?}.
A symbol IS a native tool IFF it declares :native-tool? true (which REQUIRES
:schema). Every field is FLAT on the symbol — no legacy :native-tool map.
:description defaults to the symbol's :doc so the schema description and the
docstring are ONE source. :name is the :name wire-name override else the
symbol name (so exists? advertises file_exists). :active? is the
:active-fn against env (default true). env may be nil.
Every native tool on `active-extensions`' symbols — ONE walk, the single source
the schemas / handlers / renderers / colours all project from, NORMALIZED to
`{:name :description :schema :handler :render :color-role :active?}`.
A symbol IS a native tool IFF it declares `:native-tool? true` (which REQUIRES
`:schema`). Every field is FLAT on the symbol — no legacy `:native-tool` map.
`:description` defaults to the symbol's `:doc` so the schema description and the
docstring are ONE source. `:name` is the `:name` wire-name override else the
symbol name (so `exists?` advertises `file_exists`). `:active?` is the
`:active-fn` against `env` (default true). `env` may be nil.(normalize-error t)Build a structured :error map from a Throwable.
Required :message; optional :trace (preformatted string
including header + frames). :hint and :block are tool/engine-
supplied via merge-into-metadata style updates after
construction.
Build a structured `:error` map from a Throwable. Required `:message`; optional `:trace` (preformatted string including header + frames). `:hint` and `:block` are tool/engine- supplied via `merge-into-metadata` style updates after construction.
(normalize-metadata metadata)Fill timing keys on the :metadata map when absent. Returns a
metadata map (NOT an envelope). The envelope wraps the result of
this fn under :metadata.
Timing keys (always populated): :started-at-ms :finished-at-ms :duration-ms
Callers may pass richer maps (tool / extension / source metadata, tool-specific :paths / :hit-count / :command); this helper only normalizes the shared timing surface.
Fill timing keys on the `:metadata` map when absent. Returns a metadata map (NOT an envelope). The envelope wraps the result of this fn under `:metadata`. Timing keys (always populated): :started-at-ms :finished-at-ms :duration-ms Callers may pass richer maps (tool / extension / source metadata, tool-specific :paths / :hit-count / :command); this helper only normalizes the shared timing surface.
(normalize-prompt-text text)Normalize model-facing prompt text.
Removes source indentation from multiline literals, trims leading/trailing blank lines, trims trailing horizontal whitespace, and collapses runs of blank lines to a single blank line.
Normalize model-facing prompt text. Removes source indentation from multiline literals, trims leading/trailing blank lines, trims trailing horizontal whitespace, and collapses runs of blank lines to a single blank line.
(normalize-trace t)Convert a Throwable's stack into the preformatted, babashka-style
single-string ::op.error/trace. First line is
<ClassName>: <message> (matches babashka error-handler header);
subsequent lines are filtered frames (one per line, `class/method
Frames in noisy-frame? (clojure.lang reflection,
java.lang.reflect, jdk.internal.reflect) are dropped to keep the
trace LLM-friendly. Capped at max-trace-frames lines after the
header.
Convert a Throwable's stack into the preformatted, babashka-style single-string `::op.error/trace`. First line is `<ClassName>: <message>` (matches babashka error-handler header); subsequent lines are filtered frames (one per line, `class/method - file:line`). Frames in `noisy-frame?` (clojure.lang reflection, java.lang.reflect, jdk.internal.reflect) are dropped to keep the trace LLM-friendly. Capped at `max-trace-frames` lines after the header.
(op-batch-hint-threshold op-keyword)The per-tool high-fan-out batch-hint threshold for op-keyword, or nil
when the tool declared no override (callers fall back to the default).
Never throws on unknown ops — the hint is advisory, not load-bearing.
The per-tool high-fan-out batch-hint threshold for `op-keyword`, or nil when the tool declared no override (callers fall back to the default). Never throws on unknown ops — the hint is advisory, not load-bearing.
(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.
(op-tag-index)Read-only snapshot of the canonical op-keyword -> tag map. Lets
call-sites that only hold a Python-snake call HEAD (the
classify-form-tag resolver in loop.clj, which reads the head off
the model's source) fold each registered op to its Python name and
recover the tag — there is no vis/symbol handle at that point.
Never throws; an unknown head simply misses the folded view.
Read-only snapshot of the canonical op-keyword -> tag map. Lets call-sites that only hold a Python-snake call HEAD (the `classify-form-tag` resolver in `loop.clj`, which reads the head off the model's source) fold each registered op to its Python name and recover the tag — there is no `vis/symbol` handle at that point. Never throws; an unknown head simply misses the folded view.
Closed set of operation tags a tool can declare. The two values map to the observation/mutation half of the OODA loop. The prior granular enum collapses into these two:
:observation reads state without changing it — cat, ls, exists?, locators, rg, env queries, registry lookups
:mutation mutates state — patch, write, append, mkdir, touch, delete, move, copy.
Channels that want to color tools by tag look it up themselves; the engine never carries presentation in the tool envelope.
Closed set of operation tags a tool can declare. The two values
map to the observation/mutation half of the OODA loop. The prior
granular enum collapses into these two:
:observation reads state without changing it — cat,
ls, exists?, locators, rg, env
queries, registry lookups
:mutation mutates state — patch, write, append,
mkdir, touch, delete, move, copy.
Channels that want to color tools by tag look it up themselves;
the engine never carries presentation in the tool envelope.(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-extensions! environment register-fn!)Install all globally registered extensions into an environment.
Topologically sorts by :ext/requires so dependencies are registered before dependents. Throws on missing dependencies or cycles.
Called by create-environment automatically. Returns environment.
Install all globally registered extensions into an environment. Topologically sorts by :ext/requires so dependencies are registered before dependents. Throws on missing dependencies or cycles. Called by `create-environment` automatically. Returns environment.
(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-reload-hook! id f)Register a zero-arg f to run on /reload. Idempotent per id —
re-registering replaces. Hooks must be cheap and safe to call at any
time; a throwing hook is reported, never fatal.
Register a zero-arg `f` to run on `/reload`. Idempotent per `id` — re-registering replaces. Hooks must be cheap and safe to call at any time; a throwing hook is reported, never fatal.
(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-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.
(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.(resolve-markers ns-syms)Resolve every namespace in ns-syms to its source on the classpath
and compute aggregate markers.
Returns {:source-paths ["..."] ;; sorted entry locators :source-mtime-max long ;; -1 if nothing resolved :source-hash-sha256 "hex"} ;; nil if nothing resolved
Always returns a map (never throws). Per-namespace failures are logged at :warn and skipped; an extension whose nses partially resolve still gets markers from the parts that did.
Resolve every namespace in `ns-syms` to its source on the classpath
and compute aggregate markers.
Returns
{:source-paths ["..."] ;; sorted entry locators
:source-mtime-max long ;; -1 if nothing resolved
:source-hash-sha256 "hex"} ;; nil if nothing resolved
Always returns a map (never throws). Per-namespace failures are
logged at :warn and skipped; an extension whose nses partially
resolve still gets markers from the parts that did.(resolve-markers-for-extension ext-or-manifest)Resolve source markers from manifest :nses or extension :ext/source-nses.
Resolve source markers from manifest `:nses` or extension `:ext/source-nses`.
(run-reload-hooks!)Run every registered reload hook. Returns {id {:ok? bool :error msg}}.
Run every registered reload hook. Returns `{id {:ok? bool :error msg}}`.
(sandbox-shims)Every Python sandbox SHIM contributed across all registered extensions, in
registration order (built-ins first). env-python/build-agent-context
installs each into the model sandbox Context at creation time — wiring the
shim's host :shim/bindings onto the globals, then eval'ing its
:shim/preamble — turning a host / JVM capability into an importable Python
module. Loads built-ins first (idempotent) so the registry is populated
before we read it.
Every Python sandbox SHIM contributed across all registered extensions, in registration order (built-ins first). `env-python/build-agent-context` installs each into the model sandbox Context at creation time — wiring the shim's host `:shim/bindings` onto the globals, then eval'ing its `:shim/preamble` — turning a host / JVM capability into an importable Python module. Loads built-ins first (idempotent) so the registry is populated before we read it.
(sandbox-symbol-docs)Map {sandbox-symbol -> doc-text} for every engine-bound symbol across the
registered extensions, keyed by the :ext.symbol/symbol as it is bound in
the Python sandbox. doc-text prefers the curated :ext.symbol/description
(the model-facing prose the prompt catalog renders) and falls back to the
tool var's :ext.symbol/doc docstring.
env_python/build-agent-context seeds the sandbox __vis_docs__ table from
this so in-sandbox doc(name) returns the tool's real description instead of
a bare name (callable). Loads built-ins first (idempotent) so the registry
is populated before we read it. Symbols absent here simply have no doc entry.
Map `{sandbox-symbol -> doc-text}` for every engine-bound symbol across the
registered extensions, keyed by the `:ext.symbol/symbol` as it is bound in
the Python sandbox. `doc-text` prefers the curated `:ext.symbol/description`
(the model-facing prose the prompt catalog renders) and falls back to the
tool var's `:ext.symbol/doc` docstring.
`env_python/build-agent-context` seeds the sandbox `__vis_docs__` table from
this so in-sandbox `doc(name)` returns the tool's real description instead of
a bare `name (callable)`. Loads built-ins first (idempotent) so the registry
is populated before we read it. Symbols absent here simply have no doc entry.(sha256-hex s)Hex SHA-256 of a string (UTF-8). The ONE string-digest helper — loop's replay-dedup key and the source-marker hashing share it instead of re-rolling MessageDigest + hex folds.
Hex SHA-256 of a string (UTF-8). The ONE string-digest helper — loop's replay-dedup key and the source-marker hashing share it instead of re-rolling MessageDigest + hex folds.
(slash-path slash-spec)Canonical full path vec of a slash spec: parent ++ [name]. Used as the
lookup key in internal/slash.clj.
Canonical full path vec of a slash spec: parent ++ [name]. Used as the lookup key in `internal/slash.clj`.
(success args)Construct a successful tool-result envelope. See envelope-of for
the call shape. Returns a :envelope map (flat, all metadata
under op/*).
Construct a successful tool-result envelope. See `envelope-of` for the call shape. Returns a `:envelope` map (flat, all metadata under `op/*`).
(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.(symbol-active? e env)Whether a symbol ENTRY is LIVE for env this iteration. Default true; an
:active-fn (fn [env] -> bool) makes it dynamic (e.g. a sub-toggle). env may
be nil — toggle-style predicates ignore it.
Whether a symbol ENTRY is LIVE for `env` this iteration. Default true; an `:active-fn (fn [env] -> bool)` makes it dynamic (e.g. a sub-toggle). `env` may be nil — toggle-style predicates ignore it.
(symbol-bound? e)Whether a symbol ENTRY is ENGINE-BOUND (bound into the GraalPy env as a Python
verb). Default true; :engine-bound? false (native-tool-only verbs, e.g. skill)
opts out — they never become a Python name; their :handler executes directly.
Whether a symbol ENTRY is ENGINE-BOUND (bound into the GraalPy env as a Python verb). Default true; `:engine-bound? false` (native-tool-only verbs, e.g. skill) opts out — they never become a Python name; their `:handler` executes directly.
(tool-result? x)True when x is a valid :envelope map. Renamed conceptually;
name kept for caller compatibility.
True when `x` is a valid `:envelope` map. Renamed conceptually; name kept for caller compatibility.
(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.
(validate! ext)Normalize and assert that an extension map conforms to ::extension.
Normalizes :ext/prompt-fn (string -> fn) before checking the spec
when the key is present. Throws with spec explain-data on violation.
Normalize and assert that an extension map conforms to ::extension. Normalizes `:ext/prompt-fn` (string -> fn) before checking the spec when the key is present. Throws with spec explain-data on violation.
(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).(wrap-extension ext env)Wrap all function symbols in an extension into invocation fns.
Returns a map of {sym -> (fn [& args] result)} where each fn
closes over the extension, symbol entry, and environment, then
routes through invoke-symbol-wrapper.
All stdout/stderr from extension calls is redirected to the log file so nothing bleeds into the TUI.
Value symbols are returned as {sym -> value}.
Returns every extension symbol.
Wrap all function symbols in an extension into invocation fns.
Returns a map of {sym -> (fn [& args] result)} where each fn
closes over the extension, symbol entry, and environment, then
routes through `invoke-symbol-wrapper`.
All stdout/stderr from extension calls is redirected to the log
file so nothing bleeds into the TUI.
Value symbols are returned as {sym -> value}.
Returns every extension symbol.(wrap-extension-thunked ext env-thunk)Like wrap-extension but resolves the environment LAZILY via env-thunk
(a 0-arg fn) at CALL time instead of closing over a concrete env. Interns
BUILT-IN extension symbols into the sandbox at Python-context creation —
BEFORE the environment map exists — mirroring how doc/apropos defer
through environment-atom. Same wrapping/IO-redirect as wrap-extension.
Like `wrap-extension` but resolves the environment LAZILY via `env-thunk` (a 0-arg fn) at CALL time instead of closing over a concrete `env`. Interns BUILT-IN extension symbols into the sandbox at Python-context creation — BEFORE the environment map exists — mirroring how `doc`/`apropos` defer through `environment-atom`. Same wrapping/IO-redirect as `wrap-extension`.
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 |