vis-agent
The Python SDK for Vis: author extensions and control local or remote engines.
One distribution, vis-agent, exposes two entry points: blockether.vis.extension
for extension authors and blockether.vis.engine for engine clients. Models live
in blockether.vis.views and blockether.vis.activity. The lightweight
blockether.vis root initializes no host and starts no process or connection.
blockether is an implicit PEP 420 namespace; the SDK does not own its __init__.py.
The SDK does not install or alias the unrelated top-level vis package.
SDK records, including Response, Event, Session, Turn, View and Activity
projections, are frozen, slotted dataclasses. Event envelope fields (type,
session_id, seq, cursor, turn_id) are named and validated on both transports.
Event.view and Event.activity expose validated records with immutable nested
collections. Event.data holds the remaining dynamic payload and remains mutable;
frozen records do not promise deep immutability for arbitrary endpoint data.
Extension tools can return frozen/slotted dataclasses; the runtime serializes
their declared fields, including nested records, without requiring __dict__.
The SDK ships PEP 561 py.typed metadata for its inline annotations.
The remaining endpoint dictionaries are not yet a fully typed model API.
pip install vis-agent
from dataclasses import dataclass
import blockether.vis.extension as vis
@dataclass(frozen=True, slots=True)
class Greeting:
text: str
def greet(name: str) -> Greeting:
"""Greet one person and return a typed result."""
vis.publish_activity(
vis.ActivityPresentation(
"Greeting", "Preparing reply", (vis.ActivityProgress("Working"),)
)
)
return Greeting(f"Hello, {name}!")
def greeting_activity(phase, result, **_) -> vis.ActivityPresentation:
return vis.ActivityPresentation(
"Greeting",
phase,
(vis.ActivityText(result.text if phase == "success" else "Preparing reply"),),
)
vis.register(
vis.Extension(
name="greeter",
description="Greeting tools.",
alias="greeter",
symbols=[vis.Symbol(greet, activity=vis.Activity(render=greeting_activity))],
)
)
Extension, Symbol, SlashCommand, OpHook, NetworkFilter and Provider
are frozen, slotted declarations. Constructing them performs no host IO. Only
vis.register(extension) registers the file and resolves its declared environment;
call it once. Collections are snapshotted, and marker dictionaries are not accepted.
vis is the imported module, not a singleton or an extension base class.
| API | Purpose |
|---|---|
log(level, message) |
Diagnostics, not model context. |
notify(text) |
One-way notification to the human. |
ask(title, fields) |
Wait for a typed human-input result. |
Activity / publish_activity(presentation) |
This tool invocation's progress and presentation; the engine owns timing and outcome. |
Providers
Provider presets and callback results use typed records too:
import os
import blockether.vis.extension as vis
def credential() -> vis.ProviderCredential | None:
token = os.environ.get("EXAMPLE_API_KEY")
return vis.ProviderCredential(token) if token else None
def status() -> vis.ProviderStatus:
return vis.ProviderStatus(
is_authenticated=bool(os.environ.get("EXAMPLE_API_KEY")),
source="env-var",
)
vis.register(
vis.Extension(
name="provider-example",
description="An OpenAI-compatible provider.",
env=["EXAMPLE_API_KEY"],
providers=[
vis.Provider(
id="example",
label="Example AI",
preset=vis.ProviderPreset(
base_url="https://gateway.example.com/v1",
api_style="openai",
default_models=["example-model"],
),
get_token_fn=credential,
status_fn=status,
)
],
)
)
Load this file as an extension, then add/select example in Vis. Credentials are
read passively; return None when absent. Set is_managed=True only when the
extension owns automatic binding/configuration; it does not disable authentication.
Refresh, login/logout, limits and model enrichment use the same provider boundary.
See provider callbacks and limits
for their signatures and typed results. No constructor invokes a callback.
One file, two hosts
Host operations are declared in
python-host.json
and bundled with the other canonical documents in the SDK. Contract reading and
validation are private implementation details, not a separate Python package.
Inside a Vis session the engine seeds those ops and they reach the live agent:
state is the extension's durable state, vis.ask opens a dialog on whichever
surface the human is using, vis.shell runs in the agent's sandbox.
Installed in an ordinary Python environment there is no agent, so
blockether.vis._outside supplies the host operations:
| op | outside |
|---|---|
state, log, notify, shell, secrets, host_env |
done locally — a JSON file under ~/.vis/outside, stderr lines, a real subprocess, a process-local vault |
ask |
prompted in the TERMINAL: the same field tree, the same validators, the same Answer |
jailed_shell, jailed_shell_session |
refused by name — a jail is a property of the agent's process boundary, and nothing out here can enforce one |
So an extension file imports, type-checks, unit-tests and runs on a laptop or in CI, and the code that ships is the code that was tested.
Answering without a human
import blockether.vis.extension as vis
vis.outside.answer_with({"env": "staging", "token": "hunter2"})
answer = vis.ask("Deploy", [vis.select("env", ["staging", "prod"])])
assert answer["env"] == "staging"
VIS_OUTSIDE_ANSWERS (a JSON object) primes the same values from the
environment, and VIS_OUTSIDE_NONINTERACTIVE=1 makes an unanswerable ask return
undeliverable — exactly what a session with no surface mounted returns — instead
of blocking a build.
Other environment knobs: VIS_OUTSIDE_HOME moves the state file and the shell
logs (default ~/.vis/outside).
Testing live extensions
vis.testing.LiveRecorder is the shared in-memory host for extension tests. It
records extension envelopes without publishing fixture views into a real session,
materializes open/patch/state/close, and exposes focus and close for simulated
surface actions. Provider-specific tests keep only their provider snapshots and
assertions; vis.testing.assert_tree compares terminal view goldens at the exact
leaf that changed.
View and Activity are different contracts
View is a human interaction: an input form or a live surface with the existing
view.open, view.patch and view.close lifecycle. Extension authors keep using
vis.ask and the live builders. Remote and local clients use the same typed
InputView, LiveView, LivePatch, InputResult and LiveResult records from
blockether.vis.views. session.input_views() and session.live_views() return
records, not unvalidated dictionaries. session.answer(view_id, values) submits a
form; session.view_action(view_id, action, **values) validates the closed action
shape before transport IO. Malformed incoming views raise ProtocolError.
Input projections carry the engine's created_at, not channel routing or validator
callbacks. An InputResult contains only the close reason: submitted values and
secret handles go to the waiting extension, not the event stream. SSE omits the
already streamed live picture, so LiveResult.view is optional; retain the open
View and apply its patches if your application needs a materialized display.
Activity is engine-observed execution evidence, not an interactive View and not
model context. An extension can set vis.Activity(presenter=..., label=...) on a
symbol or vis.method; it cannot forge lifecycle identity, timing or outcomes.
The tool's observation/mutation tag still determines its effect classification.
Omitting Activity metadata preserves the engine's default presentation.
# The same records are returned by GatewayClient and LocalEngine.
with session.events() as events:
for event in events:
if event.activity is not None:
print(event.activity.state, event.activity.counts)
if event.view is not None:
print(event.type, event.view.kind, event.view.view_id)
blockether.vis.activity.ActivityProjection represents the complete replacement
in a block.activity event; it is not a delta to merge. Rows, counts, omission
accounting, evidence and resources are named records. from_wire validates the
canonical schema and semantic bounds; to_wire produces plain JSON-compatible
data. View records provide the same conversion boundary.
The normative specifications are the packaged View contract, View schema, Activity contract and Activity schema. SDK, engine, TUI and Companion test against fixtures owned by that contract package, not separately maintained examples. Retired Activity envelopes are rejected; there is no compatibility renderer or alternate import alias.
Remote gateway client
import os
from blockether.vis.engine import GatewayClient
with GatewayClient(
"https://gateway.example.com", token=os.environ["VIS_TOKEN"]
) as client:
session = client.create_session(title="Python API")
turn = session.send("Describe this project")
result = turn.wait(timeout=120)
The client uses an explicit origin; it does not discover credentials, download Vis, start a gateway or stop a user's server. Closing releases its client lease, not sessions. Idle clients renew their lease every 30 seconds using the canonical bounded keepalive request. A failed renewal invalidates the client; close it and create a new instance explicitly. Mutations are never replayed to regain a lease. This client is not an embedded/local engine.
create_session,list_sessions(one cursor page),session(id).- Session:
read,update,delete,send,input_views,live_views,answer,view_action,events,turns,artifacts,transcript,upload,download_attachment. - Turn:
read,wait,cancel. Failed/cancelled turns are returned with their status; a wait timeout does not cancel the operation. - Dedicated methods cover non-streaming SDK operations: for example
get_models(),get_provider_status(provider_id),get_projects()andget_settings(). There is no publiccall. Each method's docstring names its canonical operation; JSON operations are annotatedJSONValue; binary/text operations returnResponse(status, content, headers), and empty responses returnNone. Endpoint payloads without a canonical schema remain JSON rather than invented models. Session SSE usessession.events().speech_events(job_id),voice_events(job_id)and theirsession_speech_events(sid, job_id)/session_voice_events(sid, job_id)counterparts expose all four job streams as typedJobEventsnapshots. Job streams have no cursor: reconnect rereads state, identical snapshots are suppressed, andis_doneterminates the iterator. Buffered methods refuse SSE. events(cursor=0, reconnects=3, retry_delay=0.2)is a context-managed SSE iterator returning namedEventrecords withtypeandseq. Replay suppresses duplicates and honors asubscription.readycursor reset after restart. Exhausting the reconnect budget raisesTransportError.GatewayErrorexposes HTTPstatusandcode;ProtocolErrorreports incompatible/malformed protocol data;VisTimeoutis aTransportError. Mutations are never retried automatically. Keep the idempotency key if you explicitly retry a submission whose outcome is unknown.
Use one calling thread per client. Always close event iterators. Requests and SSE idle reads use the client's timeout (30 seconds by default); turns have a separate wait deadline. TLS verification stays enabled and redirects are refused.
Owned local engine
from blockether.vis.engine import LocalEngine
with LocalEngine(executable="/path/to/vis-agent", root="/path/to/project") as engine:
session = engine.create_session(title="Local Python API")
print(session.read())
Supply a compatible Vis executable that implements sdk-stdio. The wheel does not
bundle or download that executable, and older binaries without this command cannot
be used. LocalEngine starts a private engine process, not an HTTP gateway and not
an in-process JVM. Linux and macOS are supported by this transport; Windows is not.
It reuses the remote client's dedicated methods, Session, Turn and Event types.
Local session and speech/voice job events poll the same canonical resources over
stdio instead of opening SSE connections. Polling has a bounded idle timeout and
validates an event before advancing its cursor. Closing a stream never cancels a job.
Use one calling thread per engine. Request timeouts close the owned process to
prevent a late response being mistaken for a later request. startup_timeout
defaults to 120 seconds; individual requests default to 30 seconds.
The database is temporary and discarded on close. Engine sessions are not durable across context-manager exits. Project configuration is inherited; agent execution still needs a configured provider. No gateway discovery or user-server shutdown is performed.
The integration suite runs both transports against real Vis processes and a
loopback model double, covering extension execution, input/live View, Activity,
completion, cancellation and cleanup. Select it with VIS_TEST_LOCAL_COMMAND.
No real-model credentials are required. Release verification and any remaining
external gates are recorded in PLAN.md; unit tests do not prove a linked binary.
Repository SDK checks
This checkout ships .vis/extensions/sdk_checks.py as the sdk extension. After
/reload, call its namespaced tool from a Vis session:
report = sdk.check(
root=".",
python="/path/to/verification-venv/bin/python",
engine_command="/path/to/staged/vis-agent",
)
print(report.is_pass, report.is_engine_checked)
for step in report.steps:
print(step.name, step.exit_code, step.duration_ms)
The selected Python needs pytest, build, ruff and twine. Checks cover source
lint/format/tests, direct versus sdist-rebuilt wheels, exact packaged source and
canonical contracts, strict distribution metadata, documented examples/file links,
and tests of a disposable wheel installation outside the checkout. The check
publishes ordered Activity progress and returns frozen CheckReport/CheckResult
records with bounded per-process output tails. It stops at the first failure and
cleans up its own processes and temporary environments, including on interruption.
Omit engine_command for local/package checks; then is_engine_checked is false
and opt-in engine tests are skipped. An explicit command exercises real HTTP/stdio
with an isolated engine and local model double—not your live gateway. A staged
native wrapper tests its bundled Python runtime as well. This tool never builds
native images, spends real-model tokens, commits, tags or publishes.
Distribution and publishing
The SDK builds a wheel from its sdist and is tested after installation outside
the checkout. CI covers CPython 3.11–3.14 and PyPy 3.11 on Linux/macOS;
the engine's embedded interpreter is independently owned by vis-python-runtime.
The pinned runtime v0.5.0 is published on GitHub with Linux/macOS x64/arm64 assets.
It is not a second Python SDK or a PyPI alias.
.github/workflows/python-publish.yml is an explicit, version-checked publishing
gate, dependent on distribution and real-engine tests. Publishing requires the
protected pypi environment and a trusted publisher for vis-agent. Building
a wheel or adding this workflow does not publish the SDK to PyPI.
Where the real documentation lives
vis.ask, the field builders, vis.Extension/vis.register, hooks, providers and network
filters are documented where they are defined, in blockether/vis/extension.py, and in the
Vis docs (doc("extending") inside a session). Canonical JSON documents live in
vis-contract; this package implements their host and gateway contracts.
Apache-2.0. Part of the Vis repository: packages/vis-agent.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vis_agent-0.1.43.tar.gz.
File metadata
- Download URL: vis_agent-0.1.43.tar.gz
- Upload date:
- Size: 130.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eca4da89fed7bf0b18016a828b4b4b252220fc748d0b95c9f5a984618485a934
|
|
| MD5 |
70ccc41a01208fd4a1885d37d27d48d3
|
|
| BLAKE2b-256 |
cbb034261d2610fcf44a4525a2d85357b97293b2acd71edfc1390b8af2c218fe
|
Provenance
The following attestation bundles were made for vis_agent-0.1.43.tar.gz:
Publisher:
python-publish.yml on Blockether/vis
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vis_agent-0.1.43.tar.gz -
Subject digest:
eca4da89fed7bf0b18016a828b4b4b252220fc748d0b95c9f5a984618485a934 - Sigstore transparency entry: 2753272066
- Sigstore integration time:
-
Permalink:
Blockether/vis@548183a79fd7a98368a3085c93df1f1705d04c35 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Blockether
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@548183a79fd7a98368a3085c93df1f1705d04c35 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file vis_agent-0.1.43-py3-none-any.whl.
File metadata
- Download URL: vis_agent-0.1.43-py3-none-any.whl
- Upload date:
- Size: 104.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
deb662ed93e347dfb9934d2864e8916b9fae0475fdd6dc534ad20a81f0ffe65b
|
|
| MD5 |
e083d6491819dfb9179c73fac65f1e74
|
|
| BLAKE2b-256 |
7d60539cd282c453b8597b3ab69c057111e5ba796b7d0a60bdbe5a1e715d5fdc
|
Provenance
The following attestation bundles were made for vis_agent-0.1.43-py3-none-any.whl:
Publisher:
python-publish.yml on Blockether/vis
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vis_agent-0.1.43-py3-none-any.whl -
Subject digest:
deb662ed93e347dfb9934d2864e8916b9fae0475fdd6dc534ad20a81f0ffe65b - Sigstore transparency entry: 2753272071
- Sigstore integration time:
-
Permalink:
Blockether/vis@548183a79fd7a98368a3085c93df1f1705d04c35 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Blockether
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@548183a79fd7a98368a3085c93df1f1705d04c35 -
Trigger Event:
workflow_dispatch
-
Statement type: