VisExtension Center

Using an existing Python project

Connect an existing uv project to Vis without copying its implementation into an extension. The package stays editable in your checkout; a small entry file registers its tools. This is an advanced alternative to the one-file tutorial and automatically prepared packages.

Before you start

You need Vis, uv, a reviewed Python project and a committed uv.lock. Run preparation as the same OS user and with the same runtime and package-directory settings as the gateway. Put uv on the sync command's PATH, or on the gateway's PATH when using /reload --sync.

An ordinary uv sync prepares the project's environment, not Vis's embedded Python. This workflow uses vis-agent python uv sync instead. It runs trusted build backends and installs into the shared Vis package directory. Do not use it merely to try the standard-library tutorial.

Declare the editable project

Keep ordinary Python packaging metadata and business logic in the package, and put a thin entry under the workspace's .vis/extensions/. This complete example uses setuptools with an editable src/ layout:

project/
  einmal/
    pyproject.toml
    uv.lock                       # generated by uv lock
    src/einmal/__init__.py
    tests/test_status.py
  .vis/extensions/einmal_tools.py
# einmal/pyproject.toml
[project]
name = "einmal"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]
# einmal/src/einmal/__init__.py
def status() -> str:
    """Return the integration status."""
    return "ready"
# .vis/extensions/einmal_tools.py
# /// script
# requires-python = ">=3.12"
# dependencies = []
# [tool.vis]
# project = "../../einmal"
# ///
import blockether.vis.extension as vis
from einmal import status

vis.register(vis.Extension(
    name="einmal",
    description="Package example.",
    alias="einmal",
    symbols=[vis.Symbol(status)],
))

project is relative to the entry file, not the working directory; absolute paths also work. It must contain pyproject.toml and uv.lock. Project mode rejects nonempty script dependencies: declare them in pyproject.toml. Do not add the same package's src to source_paths; its editable install already supplies the import root.

The build backend installs the project's own package. Without [build-system], or with uv configured not to package it, preparing dependencies does not install its source. For a local dependency, declare the requirement in project.dependencies and explicitly make its source editable:

[tool.uv.sources]
shared-tools = { path = "../shared-tools", editable = true }

That sibling needs packaging metadata too. A path dependency without editable = true is not a promise of live imports. Published wheels are ordinary installed dependencies, not editable source trees.

Prepare the Vis environment

From project/:

  1. Generate or deliberately update the lock, review it and commit it:

    uv lock --project ./einmal
    
  2. Install the locked project for Vis:

    vis-agent python uv sync --project ./einmal --locked
    
  3. Verify both the import and its source location:

    vis-agent python -c "import einmal; print(einmal.status(), einmal.__file__)"
    

    Expect ready and this checkout's einmal/src/einmal/__init__.py, not a copied module under ~/.vis/python/packages.

  4. Start Vis in project/, or run /reload. On the next turn, inspect doc("status") and call await status(). The tool returns ready. vis-agent extension list checks registration only.

After reviewing dependency changes, /reload --sync can prepare projects declared by configured extensions and retry loading them. It uses the gateway's user, interpreter and package directory, even when assistant shell access is disabled. It respects supplied locks; update a stale lock before retrying. Plain /reload, startup and imports never install manually selected uv projects.

What preparation installs

The command uses the embedded Python and exports the lock with uv export --locked --no-default-groups --format pylock.toml, then installs it with uv pip install --target. It preserves editable local sources, dependency versions, artifact hashes and named indexes while retaining unrelated packages. Python downloads are disabled; the temporary export is removed. uv.lock and the project's .venv are unchanged by this sync.

Default dependency groups and optional extras are not installed. --offline and --no-cache are supported; other uv sync options are rejected. python.index_url supplies the default index without replacing named source indexes. Keep credentials in uv's credential configuration or the sync process's environment, not committed URLs. Installer diagnostics are suppressed because they may contain credentials. Loading an already prepared project does not require uv on the gateway's PATH.

Reload after changes

Change Required action
Edit existing editable Python source or the entry file /reload; no reinstall or gateway restart
Change dependencies or packaging metadata Review/update the lock, sync again, then /reload
Move the checkout Sync from the new location, then /reload
Change Vis runtime, package directory or default index Sync with the intended runtime and settings, then load the extension
Replace compiled extension code Rebuild and install it, then use a fresh Vis process

Readiness checks identify changed inputs: project location, manifest, lock, runtime, interpreter, package directory, default index or installed distribution metadata. Only distributions named in the exported lock are tracked. Editing existing editable Python source or updating an unrelated shared package does not require sync.

Reload switches live sessions at the next turn boundary; in-flight calls can finish with old code. A failed reload retains last-good tools and docs as stale, with source fingerprints and the failure reason in the reload result, doctor and assistant context. Resolve that failure before judging the new API. Reload does not replace the gateway binary or its startup environment, and Python source reload is not native-library reload.

Test the implementation and the tool

Keep ordinary tests in the package:

# einmal/tests/test_status.py
from einmal import status


def test_status():
    assert status() == "ready"

After initial sync, install pytest and run the package tests from project/:

vis-agent python -m pip install pytest
vis-agent python -m pytest einmal/tests/ -q

This uses shared Vis packages, not the project's .venv; sync does not install dev groups. No PYTHONPATH or extra source_paths is needed for this editable package. Use vis-agent python -m pytest, not vis-agent python pytest.

Then make a representative call in Vis. An import or registration does not prove the trusted session worker can use the dependencies. For native libraries, test the actual calculation through the tool; trusted workers support ctypes, but the model's sandbox is a separate confined process.

Understand the shared environment

~/.vis/python/packages is shared across projects and extensions, not isolated per project. Editable installs put .pth files or backend import hooks there instead of copying source. Both the sandbox and trusted workers activate them, but this does not widen sandbox filesystem access. A sandbox import still needs its checkout in an allowed workspace root.

Editable projects read the live checkout, not a frozen source snapshot. A first import can observe edits before reload; cached imports can retain old code. Use /reload as the explicit update step. Ordinary installed dependencies are not cleared from the registration worker's module cache by editable reload.

Build backends and executable .pth lines are trusted code. A failed load cannot roll back shared package changes. Imports in python_execution never install packages, and the shared directory is read-only there.

See also