Extension subsystem: spec, builders, hook execution, the global registry, and parse-error rescue.
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 persistence
backend table lives in internal.persistance. The one ordered distribution manifest invokes
each extension's explicit registration function.
Extension subsystem: spec, builders, hook execution, the global registry, and parse-error rescue. 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 Channel and provider registries live in `internal.registry`; the persistence backend table lives in `internal.persistance`. The one ordered distribution manifest invokes each extension's explicit registration function.
Invocation-scoped presentation writer. No invocation coordinates cross into Python.
Invocation-scoped presentation writer. No invocation coordinates cross into Python.
Live session environment for the extension callback currently executing.
Live session environment for the extension callback currently executing.
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).
Invocation whose body is running, used only as observed parentage.
Invocation whose body is running, used only as observed parentage.
Sandbox symbol currently executing, when a symbol callback is active.
Sandbox symbol currently executing, when a symbol callback is active.
Evaluation-scoped Activity identity, sequence allocators, and form anchor.
Evaluation-scoped Activity identity, sequence allocators, and form anchor.
Optional per-eval sink for immutable tool lifecycle events. The sink is a presentation observer: a failure is logged and cannot change tool behavior.
Optional per-eval sink for immutable tool lifecycle events. The sink is a presentation observer: a failure is logged and cannot change tool behavior.
(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.Canonical lifecycle phase keywords accepted by :ext/hooks.
Canonical lifecycle phase keywords 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.
(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-python-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-python-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.(extension spec)Build and validate an extension, capturing shim source alongside host bindings.
See docs/src/extensions/extension-spec.md for the full key list.
Build and validate an extension, capturing shim source alongside host bindings. See docs/src/extensions/extension-spec.md for the full key list.
(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-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.
(gate-hooked? op-kw)Whether any gate hook is registered for op-kw — the short-circuit that keeps
an engine with no guard installed paying one map lookup per operation.
Whether any gate hook is registered for `op-kw` — the short-circuit that keeps an engine with no guard installed paying one map lookup per operation.
(gate-op s)The gate op keyword s names, or nil when it names no gate. Both spellings
resolve: the author's "fs_access" and the engine's :fs/access.
The gate op keyword `s` names, or nil when it names no gate. Both spellings resolve: the author's `"fs_access"` and the engine's `:fs/access`.
Gate ops, keyed by op keyword and valued by the spelling a hook author writes
(vis.OpHook(["fs_access"], guard) in Python, {:op :fs/access} in Clojure).
:fs/access is asked with {:operation :path} for every path the host file
tools touch — cat, grep, patch, ls (foundation/editing/core). It does
not reach the Python interpreter: open(p, 'w') inside a block is bounded by
the sandbox roots, not by a gate hook.
Gate ops, keyed by op keyword and valued by the spelling a hook author writes
(`vis.OpHook(["fs_access"], guard)` in Python, `{:op :fs/access}` in Clojure).
`:fs/access` is asked with `{:operation :path}` for every path the host file
tools touch — `cat`, `grep`, `patch`, `ls` (`foundation/editing/core`). It does
not reach the Python interpreter: `open(p, 'w')` inside a block is bounded by
the sandbox roots, not by a gate hook.(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.
(invoke-operation op-kw env f args)Invoke host operation f through the declarative :around hooks for op-kw,
then thread its result through the :after hooks. This is the non-model-tool
entry point for operations such as a TUI Git commit or a draft approval; hook
lifecycle remains owned by extension registration.
Invoke host operation `f` through the declarative :around hooks for `op-kw`, then thread its result through the :after hooks. This is the non-model-tool entry point for operations such as a TUI Git commit or a draft approval; hook lifecycle remains owned by extension registration.
(invoke-symbol-wrapper ext sym-entry args env)Run one observed tool invocation and emit paired Activity lifecycle events.
Identity and wrapper-entry order are allocated before hooks. The terminal is emitted only after recovery, hooks, envelope validation, and public-value conversion determine exactly what Python receives. Nested wrappers inherit the actual parent invocation id; concurrent terminal order follows observation. The enclosing execution budget is parked through the complete invocation and reset in full after the last concurrent or nested call returns or throws.
Run one observed tool invocation and emit paired Activity lifecycle events. Identity and wrapper-entry order are allocated before hooks. The terminal is emitted only after recovery, hooks, envelope validation, and public-value conversion determine exactly what Python receives. Nested wrappers inherit the actual parent invocation id; concurrent terminal order follows observation. The enclosing execution budget is parked through the complete invocation and reset in full after the last concurrent or nested call returns or throws.
(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.
(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-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.(publish-activity! presentation)Replace headline, summary, content and sections; false outside an observed invocation.
Replace headline, summary, content and sections; false outside an observed invocation.
(register-extension! ext)Register an extension in the global process-level registry.
This is THE single entry point for everything an extension
contributes to vis. Whatever the extension declares -- Python sandbox
symbols (:ext.engine/symbols), CLI commands (:ext/cli), channels
(:ext/channels), LLM providers (:ext/providers) -- gets routed here and dispatched into
the matching sub-registry as a side effect.
Also computes source-file markers (paths, max-mtime, sha256) and stores them in a sidecar atom read by the tool-envelope emitter (UI extension provenance label).
Idempotent on :ext/name. Returns the validated extension.
Register an extension in the global process-level registry. This is THE single entry point for everything an extension contributes to vis. Whatever the extension declares -- Python sandbox symbols (`:ext.engine/symbols`), CLI commands (`:ext/cli`), channels (`:ext/channels`), LLM providers (`:ext/providers`) -- gets routed here and dispatched into the matching sub-registry as a side effect. Also computes source-file markers (paths, max-mtime, sha256) and stores them in a sidecar atom read by the tool-envelope emitter (UI extension provenance label). Idempotent on `:ext/name`. Returns the validated extension.
(register-extensions! environment register-fn!)Install all globally registered extensions into an environment in registry order.
Called by create-environment automatically. Returns environment.
Install all globally registered extensions into an environment in registry order. 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.
:patch). :phase is :after (default — sees & may rewrite the result
envelope), :before (sees & may rewrite the args vector), :around (MIDDLEWARE
— wraps the call), or :gate (the op is ASKED, never wrapped — see gate-ops;
a gate op forces this phase whatever the caller declared, because the op
decides the shape). :fn is, for :after, (fn [env op-kw args result] ->
result-envelope); for :before, (fn [env op-kw args] -> args-vector); for
:around, (fn [env op-kw args next] -> result) where next runs the inner call
and may be invoked zero+ times (skip / retry) or wrapped in try/catch (recover
— this is how an op is made NOT to fail); for :gate, (fn [env op-kw ctx] ->
nil | reason). :owner (an ext keyword) makes the registration idempotent
across :reloads — re-registering the same owner+phase for an op REPLACES the
prior one. Returns the op-keyword.
Register a cross-cutting hook on operation `:op` (its op-keyword, e.g. :patch). `:phase` is :after (default — sees & may rewrite the result envelope), :before (sees & may rewrite the args vector), :around (MIDDLEWARE — wraps the call), or :gate (the op is ASKED, never wrapped — see `gate-ops`; a gate op forces this phase whatever the caller declared, because the op decides the shape). `:fn` is, for :after, (fn [env op-kw args result] -> result-envelope); for :before, (fn [env op-kw args] -> args-vector); for :around, (fn [env op-kw args next] -> result) where `next` runs the inner call and may be invoked zero+ times (skip / retry) or wrapped in try/catch (recover — this is how an op is made NOT to fail); for :gate, (fn [env op-kw ctx] -> nil | reason). `:owner` (an ext keyword) makes the registration idempotent across `:reload`s — re-registering the same owner+phase for an op REPLACES the prior one. Returns the op-keyword.
(register-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.
(render-prompt {:keys [heading usage-note notes] :as opts})Render canonical :ext/prompt-fn text for an extension's symbols.
A prompt fragment states ROUTING and POLICY only: when this extension is the
right approach, and what it refuses. It NEVER restates a signature, an
argument name, a return shape or an example call — that text is the symbol's
own :ext.symbol/description, reached on demand with doc(name). A fragment
is pushed into EVERY request; a docstring is pulled once, so a signature
copied up here is paid for on every turn and drifts from the one that runs.
Accepts an extension map or any map with:
Returns a prompt string suitable for :ext/prompt-fn.
Render canonical `:ext/prompt-fn` text for an extension's symbols.
A prompt fragment states ROUTING and POLICY only: when this extension is the
right approach, and what it refuses. It NEVER restates a signature, an
argument name, a return shape or an example call — that text is the symbol's
own `:ext.symbol/description`, reached on demand with `doc(name)`. A fragment
is pushed into EVERY request; a docstring is pulled once, so a signature
copied up here is paid for on every turn and drifts from the one that runs.
Accepts an extension map or any map with:
- :ext/description or :heading
- :ext.engine/alias optional {:alias 'v}
- :ext.engine/symbols vector of symbol + value entries
- :usage-note optional extra note added to the heading
- :notes optional string or seq of extra lines appended verbatim
Returns a prompt string suitable for :ext/prompt-fn.(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)Resolve source markers from :ext/source-nses — the declaring namespace the
vis/extension macro stamps on every extension.
Resolve source markers from `:ext/source-nses` — the declaring namespace the `vis/extension` macro stamps on every extension.
(run-gate-hooks op-kw env ctx)Ask the gate hooks registered for op-kw whether the operation ctx describes
may proceed. Each hook is (fn [env op-kw ctx] -> nil | reason). Returns nil to
ALLOW, or {:reason <sentence> :owner <ext>} to REFUSE — see gate-ops for the
contract the mechanism carries so the guard author does not have to.
Ask the gate hooks registered for `op-kw` whether the operation `ctx` describes
may proceed. Each hook is `(fn [env op-kw ctx] -> nil | reason)`. Returns nil to
ALLOW, or `{:reason <sentence> :owner <ext>}` to REFUSE — see `gate-ops` for the
contract the mechanism carries so the guard author does not have to.(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 evaluating the Python source
captured by extension, so later resource edits cannot change one side of that
boundary alone. 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 evaluating the Python source captured by `extension`, so later resource edits cannot change one side of that boundary alone. 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 comes from symbol-doc-text.
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.
NOTE: this keys by the BARE symbol, so it only serves BUILT-IN (unaliased)
extensions bound eagerly at context creation. Aliased extensions bind their
<alias>_<name> symbols LATER (per turn) and seed __vis_docs__ themselves
through symbol-doc-text — see loop/sync-active-extension-symbols!.
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` comes from `symbol-doc-text`.
`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.
NOTE: this keys by the BARE symbol, so it only serves BUILT-IN (unaliased)
extensions bound eagerly at context creation. Aliased extensions bind their
`<alias>_<name>` symbols LATER (per turn) and seed `__vis_docs__` themselves
through `symbol-doc-text` — see `loop/sync-active-extension-symbols!`.(sandbox-symbol-keys)Map {sandbox-symbol -> keys-line} for every engine-bound callable whose
contract lives inside an options dict, from symbol-keys-line. The
requiredness twin of sandbox-symbol-signatures: the signature says HOW the
verb is called, this says WHICH keys the dict must carry. Seeded into the
sandbox as __vis_keys__ by env-python/build-agent-context, and per turn by
aliased extensions through env-python/set-python-binding-keys!.
Map `{sandbox-symbol -> keys-line}` for every engine-bound callable whose
contract lives inside an options dict, from `symbol-keys-line`. The
requiredness twin of `sandbox-symbol-signatures`: the signature says HOW the
verb is called, this says WHICH keys the dict must carry. Seeded into the
sandbox as `__vis_keys__` by `env-python/build-agent-context`, and per turn by
aliased extensions through `env-python/set-python-binding-keys!`.(sandbox-symbol-signatures)Map {sandbox-symbol -> python-parameter-list} for every engine-bound
callable across the registered extensions, from symbol-signature. The
signature twin of sandbox-symbol-docs, seeded into the sandbox by
env-python/build-agent-context and keyed the same way: by the BARE symbol,
so aliased extensions (which bind per turn) seed their own through
env-python/set-python-binding-signature!.
Map `{sandbox-symbol -> python-parameter-list}` for every engine-bound
callable across the registered extensions, from `symbol-signature`. The
signature twin of `sandbox-symbol-docs`, seeded into the sandbox by
`env-python/build-agent-context` and keyed the same way: by the BARE symbol,
so aliased extensions (which bind per turn) seed their own through
`env-python/set-python-binding-signature!`.(session-provider-kickoff-llm-headers environment active-extensions provider)Run active extensions' :session_provider_kickoff hooks for provider.
A hook receives {:phase :session_provider_kickoff :environment environment :provider provider} and returns nil or
{:llm-headers {nonblank-string nonblank-string}}. Contributions merge in
registry order, with later hooks owning the same header. Hook exceptions and
malformed returns fail the kickoff so required transport metadata is never
silently omitted.
Run active extensions' `:session_provider_kickoff` hooks for `provider`.
A hook receives `{:phase :session_provider_kickoff :environment environment
:provider provider}` and returns nil or
`{:llm-headers {nonblank-string nonblank-string}}`. Contributions merge in
registry order, with later hooks owning the same header. Hook exceptions and
malformed returns fail the kickoff so required transport metadata is never
silently omitted.(shim-src shim)Python source paired with a shim's host bindings by extension.
Constructed extensions retain their source even if the classpath resource changes
before another sandbox starts. Reconstruct from the raw descriptor to adopt new
source. Raw descriptors read :shim/source from the classpath, also supporting
doctor checks before construction. Missing resources fail loudly. Native images
embed the same resources through -H:IncludeResources=vis-shims/.*.
Python source paired with a shim's host bindings by `extension`. Constructed extensions retain their source even if the classpath resource changes before another sandbox starts. Reconstruct from the raw descriptor to adopt new source. Raw descriptors read `:shim/source` from the classpath, also supporting doctor checks before construction. Missing resources fail loudly. Native images embed the same resources through `-H:IncludeResources=vis-shims/.*`.
(slash-path slash-spec)Canonical full path vector of a slash declaration.
Canonical full path vector of a slash declaration.
(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. Declare :activity
beside every observed binding: {:headline "Read file" :show-start false :render callback}.
Activity is for people: use understandable sentence-case English, not identifiers
or all-caps sentences. Quick reads and patches use :show-start false to show only
the end result; slow operations keep the default true to show running progress.
This hides presentation, never internal timing, failure or cancellation tracking. The
optional callback receives invocation details and a bounded, redacted public
result, and returns a canonical Activity presentation. Tools can instead call
publish-activity! while running. No generic result presentation is generated;
the engine owns identity, state, timing, errors and file-change evidence.
Raw helpers pass :raw? true and return plain values directly, with no
envelope enforcement, channel sink, or tool metadata.
Optional opts:
:symbol - override the Python sandbox name (default: var name).
:doc-fn - compute doc lazily from (sym v) when the var
lacks a docstring (third-party vars only).
:raw? - true for plain composable helpers.
:tag - REQUIRED :observation | :mutation for observed
tools (unless :raw? true).
:params - options-dict key vocabulary [{:name "paths" :required? true} {:name "ranges"}], rendered by doc(name). REQUIRED of every
tool whose call ends in an options dict — a **kwargs
signature states nothing a caller can act on.
:before-fn :after-fn :on-error-fn :ticker-fn
Observed tool functions return canonical internal envelope maps. The wrapper records the envelope, then returns only its payload to Python; failure envelopes are converted into thrown ex-info so Python reports normal errors.
:doc and :arglists ALWAYS come from var metadata — the previous
test-only (symbol sym-name f opts) 3-arg form is RETIRED. Tests
that want to register an inline fn must defn it first and pass
#'the-fn.
See docs/src/extensions/hooks.md for hook semantics.
Build a function symbol entry FROM A CLOJURE VAR.
The 3-arg form `(symbol sym-name f opts)` is a test-friendly direct
constructor: pass the sandbox-visible symbol, the implementation fn, and
an opts map whose `:doc` / `:arglists` are read directly from opts
instead of var meta. Production code uses the var form.
The var supplies `:symbol` (var name), `:fn` (the var's value), `:doc` and
`:arglists` (read from var metadata - i.e. the underlying defn's
docstring + arglists). Pass it as `#'my-tool`.
Observed tools return canonical internal envelope maps. Declare `:activity`
beside every observed binding: `{:headline "Read file" :show-start false :render callback}`.
Activity is for people: use understandable sentence-case English, not identifiers
or all-caps sentences. Quick reads and patches use `:show-start false` to show only
the end result; slow operations keep the default true to show running progress.
This hides presentation, never internal timing, failure or cancellation tracking. The
optional callback receives invocation details and a bounded, redacted public
result, and returns a canonical Activity presentation. Tools can instead call
`publish-activity!` while running. No generic result presentation is generated;
the engine owns identity, state, timing, errors and file-change evidence.
Raw helpers pass `:raw? true` and return plain values directly, with no
envelope enforcement, channel sink, or tool metadata.
Optional opts:
:symbol - override the Python sandbox name (default: var name).
:doc-fn - compute doc lazily from `(sym v)` when the var
lacks a docstring (third-party vars only).
:raw? - true for plain composable helpers.
:tag - REQUIRED `:observation | :mutation` for observed
tools (unless `:raw? true`).
:params - options-dict key vocabulary `[{:name "paths" :required? true}
{:name "ranges"}]`, rendered by `doc(name)`. REQUIRED of every
tool whose call ends in an options dict — a `**kwargs`
signature states nothing a caller can act on.
:before-fn :after-fn :on-error-fn :ticker-fn
Observed tool functions return canonical internal envelope maps. The
wrapper records the envelope, then returns only its payload to Python; failure
envelopes are converted into thrown ex-info so Python reports normal errors.
`:doc` and `:arglists` ALWAYS come from var metadata — the previous
test-only `(symbol sym-name f opts)` 3-arg form is RETIRED. Tests
that want to register an inline fn must `defn` it first and pass
`#'the-fn`.
See `docs/src/extensions/hooks.md` for hook semantics.(symbol-active? entry env)Whether a symbol entry is active for env.
Whether a symbol entry is active for `env`.
(symbol-doc-text entry)Model-facing doc text for ONE symbol ENTRY: the compact :description (falling
back to the implementation docstring), then the raw-result contract whenever the
entry declares one. Returns nil without prose — a handle with no description has
no page. This is the single source doc(name) answers from.
PROSE ONLY. How the handle is CALLED is structure, not text: symbol-signature
renders the call line and symbol-keys-line the required keys, and
doc-corpus/entry-text prints both above this document.
Model-facing doc text for ONE symbol ENTRY: the compact `:description` (falling back to the implementation docstring), then the raw-result contract whenever the entry declares one. Returns nil without prose — a handle with no description has no page. This is the single source `doc(name)` answers from. PROSE ONLY. How the handle is CALLED is structure, not text: `symbol-signature` renders the call line and `symbol-keys-line` the required keys, and `doc-corpus/entry-text` prints both above this document.
(symbol-entry parts opts)Build a symbol entry from ALREADY-RESOLVED parts instead of a Clojure var.
The constructor for hosts that have no var to point at — the Python
extension bridge, where :fn, :doc and :arglists are derived from a
Python function. parts is {:symbol :fn :doc :arglists} (plus optional
:source) and opts is EXACTLY the symbol opts map, so a Python-declared
symbol runs the SAME validation and can never skip the :description /
:result contract.
Build a symbol entry from ALREADY-RESOLVED parts instead of a Clojure var.
The constructor for hosts that have no var to point at — the Python
extension bridge, where `:fn`, `:doc` and `:arglists` are derived from a
Python function. `parts` is `{:symbol :fn :doc :arglists}` (plus optional
`:source`) and `opts` is EXACTLY the `symbol` opts map, so a Python-declared
symbol runs the SAME validation and can never skip the `:description` /
`:result` contract.(symbol-keys-line entry)Keys: language · code (REQUIRED) · id — the options-dict vocabulary from
:ext.symbol/params, in DECLARED order (authors lead with what a caller cannot
omit). The signature of a dict-shaped tool ends in **kwargs, which names
nothing; this line is where its required keys are stated. nil when the entry
declares no params.
STRUCTURE, never prose: env-python ships it to the sandbox as __vis_keys__
and doc-corpus/entry-text prints it under the call line. apropos filters
names only, so this structure cannot affect discovery; keeping it out of the
prose also avoids a second signature-shaped contract.
`Keys: language · code (REQUIRED) · id` — the options-dict vocabulary from `:ext.symbol/params`, in DECLARED order (authors lead with what a caller cannot omit). The signature of a dict-shaped tool ends in `**kwargs`, which names nothing; this line is where its required keys are stated. nil when the entry declares no params. STRUCTURE, never prose: `env-python` ships it to the sandbox as `__vis_keys__` and `doc-corpus/entry-text` prints it under the call line. `apropos` filters names only, so this structure cannot affect discovery; keeping it out of the prose also avoids a second signature-shaped contract.
(symbol-signature entry)Python parameter list for ONE symbol entry — what stands between the
parentheses of the signature the sandbox reports for it, e.g.
"language=None, **kwargs". A portable symbol contract wins, then the :call
shape (the declared keyword->positional contract); implementation arglists are
the fallback. nil when the entry declares nothing a caller could act on.
env-python ships these to the sandbox as __vis_sigs__, where a deferred
tool hangs its parameters off __wrapped__ so inspect.signature(tool) and
help(tool) answer with them instead of with the async trampoline's own
(*a, **k).
Python parameter list for ONE symbol entry — what stands between the parentheses of the signature the sandbox reports for it, e.g. `"language=None, **kwargs"`. A portable symbol contract wins, then the `:call` shape (the declared keyword->positional contract); implementation arglists are the fallback. nil when the entry declares nothing a caller could act on. `env-python` ships these to the sandbox as `__vis_sigs__`, where a deferred tool hangs its parameters off `__wrapped__` so `inspect.signature(tool)` and `help(tool)` answer with them instead of with the async trampoline's own `(*a, **k)`.
(tool-failure-on-error err _env _f _args)Observed-symbol error hook. Preserve the exception's message and diagnostic trace; the wrapper supplies the declared tool identity and classification.
Observed-symbol error hook. Preserve the exception's message and diagnostic trace; the wrapper supplies the declared tool identity and classification.
(tool-result? x)True when x is a valid tool-result envelope.
True when `x` is a valid tool-result envelope.
(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 validate an extension declaration. String prompt contributions become functions before ordinary predicate validation.
Normalize and validate an extension declaration. String prompt contributions become functions before ordinary predicate validation.
(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).(with-context {:keys [ext env] sym :symbol} & body)Install THE extension context around body and evaluate it.
There is exactly ONE context, and it is the session environment map.
Everything else an extension callback can read ambiently is DERIVED from it
here — the extension identity (*current-extension*, *current-symbol*)
and the workspace view (workspace/*workspace-root*,
workspace/*filesystem-roots*) — so a callback can never see half a
session. EVERY extension callback site enters through this macro (symbol
calls, turn hooks, ctx / prompt / activation callbacks, Python adapters);
nothing else binds those vars. Sites that bound only a subset used to hand
vis.ask / vis.shell a session-less environment.
Opts {:ext ext :symbol sym :env environment}: a nil or EMPTY :env, and a
nil :ext, INHERIT the ambient one, so a nested callback stays in its
caller's session and a caller that hands a hook no environment at all cannot
silently drop the session out from under it. :symbol is never inherited —
only a symbol call has one.
Install THE extension context around `body` and evaluate it.
There is exactly ONE context, and it is the session `environment` map.
Everything else an extension callback can read ambiently is DERIVED from it
here — the extension identity (`*current-extension*`, `*current-symbol*`)
and the workspace view (`workspace/*workspace-root*`,
`workspace/*filesystem-roots*`) — so a callback can never see half a
session. EVERY extension callback site enters through this macro (symbol
calls, turn hooks, ctx / prompt / activation callbacks, Python adapters);
nothing else binds those vars. Sites that bound only a subset used to hand
`vis.ask` / `vis.shell` a session-less environment.
Opts `{:ext ext :symbol sym :env environment}`: a nil or EMPTY `:env`, and a
nil `:ext`, INHERIT the ambient one, so a nested callback stays in its
caller's session and a caller that hands a hook no environment at all cannot
silently drop the session out from under it. `:symbol` is never inherited —
only a symbol call has one.(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 |