Ask the human, and block until the answer settles or is cancelled.
deflive(self, envelope_json:str) -> str:
Open, patch, read or close a live view.
state accepts timeout_ms (0..86400000) and after_seq (nonnegative).
A positive timeout blocks until the sequence differs or the view closes.
An unchanged timeout returns is_open=True, timed_out=True, without view.
Ordinary state reads and changed waits return the current view with seq.
A visible headline and one-line summary; only content waits behind disclosure.
summary_format="markdown" opts into inline Markdown with HTTP(S) links.
Unmarked or "inline" summaries are literal text. The 512-byte, one-line
limit includes Markdown source; images, HTML and block layout are not shown.
Human-facing symbol presentation; the engine owns identity, timing and outcome.
Declare Activity on every exported callable, including each object method.
Write understandable English for people, not Python identifiers or object reprs.
Labels and headlines use sentence case ("Read file", "Run tests"), preserving
proper names and acronyms. Summaries explain the target, useful counts or outcome.
Use render or publish_activity for selected content; return values stay independent.
show_start=False makes a fast operation end-only: no running row or start callback.
Use it for quick reads, patches and local lookups. Keep show_start=True for work
people wait for, such as tests, network requests or transfers. Internal start/end
tracking still preserves ordering, timing, errors and cancellation. Published
content is retained but stays hidden until an end-only invocation settles.
The final presentation must stand alone: name the target and outcome, including
empty results or failures reported as return values. A normally returned failed
workflow is still a failed workflow; "Completed" alone is not enough. Label
partial lists and excerpts, and retain useful counts, errors and changes.
render is an optional synchronous callback receiving phase, args, kwargs,
result and error as keyword arguments; it returns an ActivityPresentation
(or None to keep the current presentation). It runs on success and failure,
and on start only when show_start=True. Use publish_activity for intermediate
stages of long-running tools. Rendering failures never change returns or errors.
Arguments:
presenter: Presentation category; leave "generic" for ordinary tools.
label: Optional nonblank, single-line label of at most 96 characters.
render: Synchronous callback accepting phase, args, kwargs, result
and error keyword arguments. Return an ActivityPresentation or None.
show_start: Whether readers see a running row before the call settles.
Raises:
TypeError:show_start is not boolean or render is not synchronous/callable.
ValueError: The presenter is unknown or the label is invalid.
Replace headline, summary, content and sections without authoring lifecycle.
Headline and summary stay visible when collapsed. Content uses typed text,
Markdown, table, code, diff, attachment and progress blocks. Sections group
multiple results with one blank line between them. To clear content, publish
the same headline/summary with empty content. None from render keeps the last
snapshot. Returns False outside an invocation or when publication is rejected.
@dataclass(frozen=True, slots=True, kw_only=True)
classExtension:
Group tools and host capabilities in one immutable declaration.
Arguments:
name: Human-readable, nonblank extension name.
description: Nonblank description of what the extension does.
alias: Python namespace for exported symbols; required with symbols.
symbols:Symbol declarations for functions or object namespaces.
version: Optional extension version string.
kind: Optional extension kind understood by the host.
env: Environment variable names resolved at host registration.
Collection inputs are copied into tuples. Constructing an Extension is pure;
it does not run callbacks or register anything. In an application, pass it to
blockether.vis.engine.Agent or its register_extension method. In an installed
extension entrypoint, call this module's register_extension once instead.
The application callback bridge rejects host-only fields before connecting.
Raises:
ValueError: A required name, alias, callback or environment name is invalid.
TypeError: A field has the wrong type or a collection contains something
other than its corresponding SDK declaration.
Register one typed declaration and resolve its declared environment in this context.
Construction is pure; registration is the sole host boundary. A failure before
completion leaves the context unregistered. No constructor registers itself.
Declare a dataclass's public list or tuple field as its sandbox sequence.
Apply this outside @dataclass. The returned class is unchanged except for
metadata: original methods are neither called nor transported. Each subclass
must opt in separately. Values are checked when returned by an extension;
the backing field must hold a built-in list or tuple, not a lazy iterable.
Sandbox records iterate over the received field and support length, truth
testing, integer indices and slices. String keys still read named fields.
Only the received items are exposed; iteration never fetches another page.
Describe a public method on an object exported through Symbol.
Arguments:
fn: Method to annotate; omit it to use @method(...).
tag:"observation" for reads or "mutation" for state-changing work.
is_hidden: Hide the method from discovery without removing the callable.
activity: This method's human-facing presentation. Declare it on each
exported method, not on the containing object namespace.
Returns:
The original method, or a decorator returning it. Calling your method
directly still uses normal Python behavior; this decorator adds metadata.
Raises:
ValueError: The tag is unsupported or the decorated value is not callable.
TypeError: The Activity declaration has the wrong type.
fromblockether.vis.extensionimportActivity,Symbol,methodclassGreeter:@method(activity=Activity(label="Greet person",show_start=False))defhello(self,name:str)->str:"Return a greeting for the named person."returnf"Hello, {name}!"greeter=Greeter()tool=Symbol(greeter,name="greeter")assertgreeter.hello("Ada")=="Hello, Ada!"asserttool.contract["members"][0]["name"]=="greeter.hello"
@dataclass(frozen=True, slots=True)
classSymbol:
Expose a function or an object's public methods as typed tools.
Arguments:
fn: Function, or an object whose public methods form a namespace.
name: Override the function name; required for objects and must then be
a public Python identifier.
tag: Default operation classification: "observation" or "mutation".
is_hidden: Hide the symbol from discovery without removing the callable.
activity: Human-facing presentation for a function; declare one on every
exported callable. For an object, leave this unset and put an Activity
on every exported method instead.
Construction validates the declaration and derives its contract without
calling the function or starting Vis. Function docstrings and type annotations
become tool documentation, not runtime argument validation. Export narrow
objects: public methods and nested namespace objects are traversed, not just
methods bearing the decorator.
Raises:
ValueError: A callable lacks a docstring, or the name, operation tag,
Activity placement or namespace is invalid, including cycles and
repeated object references.
TypeError: A declaration field has an unsupported type.
Fresh portable tool description; no callable, default values or host access.
Namespace members carry their full public names. Strings in Annotated
describe meaning; unresolved annotations remain explicit, never evaluated.
This is documentation, not runtime argument or result validation.
@dataclass(frozen=True, slots=True)
classTypeSpec:
An inert Python type description; references bound recursive records.
Generated reference text, not captured CLI output or an operation result.
HelpDocument(tool:str, text:str)
tool: str
text: str
classCatalog:
Read-only snapshot of public Symbols; construction and lookup perform no IO.
Pass the same symbols to Catalog and Extension. Rebuild after changing declarations.
This is an adapter, not a registry, dispatcher or runtime type validator.
Endpoint defaults, not credentials. Opaque API payload keys remain unchanged.
extra carries additional router settings as JSON, never overriding named fields.
Use enrich_models_fn for typed model metadata beyond default model names.
Return fresh host data; declared optional fields are omitted, not null.
@dataclass(frozen=True, slots=True)
classProvider:
Pure provider declaration; register_extension adapts callbacks to the host.
Credential reads are passive; only auth_fn may initiate login. Callbacks are
synchronous and may run without a session. Refresh accepts zero arguments or
one rejected token (None if unknown), chosen without retrying callback errors.
Enrichment/selection inputs remain JSON mappings owned by the router/config,
not a second SDK schema for those domains. Callback outputs use typed records.
Form heading, or live heading(id, text, level=1..6).
defparagraph(text, live_text=None, **spec):
Form prose, or live paragraph(id, text) with inline Markdown.
defreveal(handle):
defforget(handle):
classInterrupted(builtins.Exception):
The live view this handle drives is no longer open.
Raised by the next push after the human stopped watching — Escape in the
terminal, Stop in the app — so an unattended loop ends by itself. A loop
that would rather finish its own work reads view.is_interrupted instead
and decides.
note is the comment the person left with the stop, when they left one: the
reason it is being stopped, in their words.
Interrupted(view_id, reason=None, note=None)
view_id
reason
note
classTextNode(_Node):
A paragraph, heading or code block, replaced in place.
Append complete lines, optionally styled by severity. Redact before writing.
Each argument is a retained line, not a raw byte fragment. A tone applies
only to this call; omitted tones are plain. Controls are displayed literally.
defclear(self):
Inherited methods and attributes
Log(view, node_id, type_name)
node_id
type
classTable(_KeyedNode):
A node holding items the extension addresses by id: it can drop them.
A live view the human WATCHES, driven by the extension that opened it.
vis.live(...) mounts one and answers this handle. Nodes are addressed by
id — view['jobs'], or view.node('jobs') — and each answers the typed
handle its own type declares. The view-level shortcuts (view.status(...),
view.log(...), view.row(...)) resolve to the one node of that type and
raise naming the candidate ids when the view holds several, so an ambiguous
call fails where it was written instead of quietly patching the wrong table.
Pushes are BATCHED: ops buffer and cross on the next push after flush_ms,
when a coalesced push fills, and always before the view is read or closed.
with view.batch(): groups one logical picture explicitly, including structural
add/drop operations. Repeated writes to the same row or node collapse into the
last one, so a per-row progress counter costs one wire row per tick rather than
one per write.
Closing is the point: close() answers either the structured verdict or the
compact model_result the extension chose. Used as a context manager the
view closes itself — completed on the way out, and failed carrying the
error when the body raised, because a run that died mid-way still owes the
model what happened.
LiveView(request, flush_ms=None)
view_id
@contextmanager
defbatch(self):
Send one complete picture for a related group of node changes.
Node handles still coalesce exactly as usual, but neither the leading edge nor
structural add/drop operations cross the host seam until the outermost batch
ends. Reads and close remain explicit flush points.
defflush(self):
Send everything buffered. Called for you before any read or close.
defstate(self):
What the view looks like right now, as the surfaces paint it.
defsleep(self, seconds):
Block until the view changes, ends, or seconds elapse.
One host wait replaces periodic state reads. An unchanged timeout returns
no view payload. Returns True on change or close, False on timeout.
Nonpositive durations do not flush or call the host.
is_interrupted
True once the human stopped watching.
Asks the engine at most once per flush window, so a compute loop can
poll it every iteration and still cost one host call per tick.
reason
Why the view ended, or None while open or after a compact result.
is_from_human
True when a PERSON ended it, rather than the run itself or a deadline.
A view is always stoppable — nothing is asked of the human, so nothing is
left unanswered by stopping it — and this is how the run finds out that is
what happened.
note
The comment the human left with their stop, or None.
The stop always lands; the note says WHY in their own words, and the same
words reach the model in the verdict.
result
The structured verdict or compact model result, once ended.
End the view and answer the result the model reads.
model_result is an optional compact string returned instead of the
full structured verdict. The finished picture and close metadata remain
in the durable artifact and on human-facing close events.
selection_snapshots are finished pictures keyed by a selectable table and
its selected rows. They are sealed only into the artifact record, so a
reopened run can still switch rows without keeping its extension alive.
Closing twice is a no-op answering the first result: a finally that
closes what an interrupt already closed must not overwrite the reason
the human chose.
deflive(title, nodes, **options):
Open a live view and answer the handle that drives it.
View options: description, source, session_id, channel_ids, plus flush_ms
for the batching window. EVERY key is a snake_case string, exactly as
vis.ask documents. There is no cancellable flag: a human can always stop
watching, and the verdict says they did (is_from_human) and why (note).
plus flush_ms for the batching window. EVERY key is a snake_case string,
exactly as vis.ask documents.
The view is mounted at once and nothing blocks — use it as a context
manager so it closes itself:
with vis.live('Deploy', [vis.steps('plan', steps=[...])]) as view:
view['plan'].set('build', tone='running')
Closing answers the verdict: is_completed, reason, the finished picture
as data, and whatever summary the extension chose to end with.
An operator action. Accepted presses increment clicks in view.state().
No callback or code crosses the wire. The producer decides how to respond.
Disabled buttons and completed receipts cannot be activated.
defstatus(node_id, text=None, **spec):
defprogress(node_id, **spec):
defstat(node_id, stats=None, **spec):
defsteps(node_id, steps=None, **spec):
defoutput(node_id, **spec):
Retained output with an independent disclosure, collapsed by default.
default_expanded=True opens an active log initially. Updates preserve the
local choice; completion starts a collapsed receipt. Hiding never clears lines.
window_lines bounds only the hot window, not the durable record.
write(..., tone="warn") styles complete lines with a LogTone; omitted
tones remain plain. Seeded line_tones align one-for-one with lines.
Redact before writing. ANSI controls are visible text, never executed.