Vis

Using an existing Python project

If you already have a Python project managed by uv, you can expose its functions to Vis without copying the code. Your package stays editable in its checkout. A small extension file registers the tools. This guide covers that setup.

When to use

For a new extension, the one-file tutorial or an automatically prepared package needs less setup.

Before you start

You need Vis and a Python project you trust. Vis includes uv, so there is no separate installation to find on PATH. Run uv commands as vis-agent python uv. The arguments, environment and terminal input/output pass through to uv.

Projects with dependencies or an editable installation use their project environment, normally .venv. Vis loads it in a separate trusted worker. Build backends run with your OS user's permissions. Source-only projects can use shared packages instead.

Declare the editable project

Keep your implementation and packaging metadata in the existing package. Add a small entry file under the workspace's .vis/extensions/ to register it with Vis. This 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

def status_activity(*, phase, result, **_):
    if phase != "success":
        return None
    return vis.ActivityPresentation("Check integration status", f"Status: {result[:160]}")


vis.register_extension(vis.Extension(
    name="einmal",
    description="Package example.",
    alias="einmal",
    symbols=[vis.Symbol(
        status, activity=vis.Activity(
            label="Check integration status", show_start=False, render=status_activity
        )
    )],
))

The local status lookup uses an end-only, human-readable Activity. Keep its presentation at the binding even though the implementation lives in a separate package. See Activity presentation.

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. Set editable = true to import its live source. Published wheels remain ordinary installed dependencies.

Prepare the project environment

From project/:

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

    vis-agent python 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 uv run --project ./einmal --no-sync 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.

Vis also prepares a declared uv project by itself when you open the project or run /reload. It uses the gateway's embedded Python, so compiled dependencies match the worker. Preparation can download dependencies and update uv.lock. Review those changes before you share them.

What preparation installs

Preparation is an ordinary uv sync. Every uv option keeps its usual meaning, and vis-agent python uv sync --help shows uv's own help. --locked refuses lock changes.

Configure uv as usual: pyproject.toml, uv.toml, environment variables or options. Vis's python.index_url sets uv's default index, unless UV_DEFAULT_INDEX or UV_INDEX_URL is set. Keep index credentials in uv's credential configuration, never in committed URLs. When automatic preparation fails, Vis shows uv's exit status, with credentials removed from the output.

The CLI does not force the embedded Python. If you choose an interpreter yourself, use a version that is compatible with the gateway's embedded Python. Project dependencies stay in the project environment. They do not become sandbox imports.

Reload after changes

Change Required action
Edit existing editable Python source or the entry file /reload, with 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 or the project interpreter Sync with a compatible interpreter, then /reload --all
Replace compiled extension code Rebuild and install it, then use a fresh Vis process

/reload loads again only the extensions that changed. This includes new, edited and failed entry files. It also includes changes to source roots, dependencies, the project lock, vis.yml environment values and shared packages. Run /reload --all to load every extension again, for example after a change outside these inputs.

Vis does not run uv for an unchanged extension. For a changed extension, uv sync --check decides if its environment is ready. Editing existing editable Python source does not require another sync.

After vis-agent python uv sync succeeds, Vis records the ready environment in its .venv. The next start then skips uv for that project. Vis checks uv again when pyproject.toml, uv.lock, a local path source or the installed packages change.

Reload switches live sessions at the next turn boundary. Calls that are already running can finish with old code. A failed reload keeps the last good tools and docs and marks them stale. The reload result, doctor and assistant context show the source fingerprints and the failure reason. Fix that failure before you judge the new API. Reload does not replace the gateway binary or its startup environment, and reloading Python source does not reload native libraries.

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 the first sync, run the package tests from project/ with its environment:

vis-agent python uv run --project ./einmal --no-sync --with pytest python -m pytest einmal/tests/ -q

This uses the project's editable install. --with pytest supplies pytest for this command without changing the project's dependencies or lockfile. --no-sync keeps the prepared environment unchanged. No PYTHONPATH or extra source_paths is needed.

vis-agent python itself runs embedded CPython. Its sys.executable and sys._base_executable identify the bundled CPython launcher, so subprocesses can use ordinary -c and -m arguments.

These child processes are standalone Python. They use the working directory and the inherited PYTHONPATH. They do not inherit Vis bindings, shared package paths or other in-memory sys.path changes. When child processes need project dependencies, use the project-environment command above. The subprocess restrictions of the agent sandbox do not change.

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.

Project and shared environments

uv sync installs project dependencies and editable .pth files or backend import hooks into the project's environment, usually .venv. Vis checks the selected environment before starting its trusted worker. The environment must use Vis's embedded Python. This applies to registration and session calls.

The worker does not load shared Vis packages or their startup hooks. If a project dependency is missing, the call fails. It does not fall back to shared installations. Preparing the environment does not install the project into the model sandbox. Vis still supplies its bundled extension SDK. Install the other dependencies of your extension in its project.

When an extension declares a uv project or has its own pyproject.toml, the first session in its Vis project prepares the missing environment before registration. /reload prepares dependency changes. A preparation failure is reported. Vis does not silently use shared packages instead. Extensions without a declared project use ~/.vis/python/packages and the shared sync workflow.

Source-only projects also use shared packages when uv selects no dependency beyond the bundled Vis SDK. Vis does not create or read .venv for these projects. Declare your import directories in tool.vis.source_paths. A project that needs an editable installation keeps its own environment, even without other dependencies. To prepare all extension projects of your project in shared packages, see Use shared packages for project extensions.

Automatic preparation ignores UV_PROJECT_ENVIRONMENT and uses the workspace's .venv. Manual validation honors that variable. If a selected environment is incomplete or uses another Python, Vis reports an error. Run the exact sync command in that error, then /reload. The command selects Vis's embedded Python and blocks interpreter downloads.

Shared storage also serves the model sandbox, extensions without a declared project, and standalone vis-agent python --shared commands. Sandbox imports still need their checkout in an allowed workspace root. Editable installs do not widen access.

Editable projects read the live checkout, not a frozen source snapshot. A first import can see edits before a reload. Cached imports can keep old code. Use /reload as the explicit update step. An editable reload does not clear ordinary installed dependencies from the module cache of the registration worker.

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

Install a project into shared packages

To make a uv project's libraries available in python_execution, explicitly sync it into Vis's shared packages.

Shared installs can change versions used by every shared consumer. Only sync projects you trust: build backends run with your OS user's permissions.

vis-agent python --shared uv sync --project ./einmal

# Include every dependency group and optional extra:
vis-agent python --shared uv sync --project ./einmal --all-groups --all-extras

# Require the existing lockfile to remain unchanged:
vis-agent python --shared uv sync --project ./einmal --locked

This installs into ~/.vis/python/packages (or VIS_PYTHON_PACKAGES) using Vis's embedded Python. uv resolves the project and its sources, honors uv.lock, and includes default dependency groups. The project and local editable dependencies remain editable. You do not create or manage an exported requirements file.

Run /reload after installation to refresh Vis workers. To run a shared package from the CLI, including inside a project, use vis-agent python --shared -m your_package. Shared sync does not create .venv. Extensions without a project use these packages. Source-only projects without dependencies beyond Vis also use them. Other projects keep their own environment.

Shared sync keeps unrelated packages, including packages that you installed from groups that you no longer select. All shared consumers use the same installed versions.

Shared packages hold one editable install for each project name. A shared sync of a second checkout, such as a test copy, replaces it. Vis sessions of the first checkout then import the code of the second checkout. Shared sync prints a warning that names the previous directory. To restore the first checkout, sync it again:

vis-agent python --shared uv sync --project ./einmal

Use --group, --extra, --no-dev and related selection flags to choose dependencies. --frozen skips lock updates, and --no-install-project installs dependencies without the project itself. Configure private indices through uv project configuration, UV_* environment variables, or Vis's python.index_url.

To decide if a setup script must sync, use --check. It exits with 0 when every locked package and the editable project are installed at the locked version. Otherwise it lists the planned installs and exits with 1. The check ignores unrelated shared packages. Add --offline to keep it local. --dry-run lists the planned installs and changes nothing.

vis-agent python --shared uv sync --project . --check --offline || vis-agent python --shared uv sync --project .

vis-agent python --shared uv sync --help lists supported options. Shared sync does not support --exact, --active, or choosing another interpreter. Without --shared, vis-agent python uv ... remains ordinary upstream uv.

Use shared packages for project extensions

Some projects install their dependencies only with vis-agent python --shared uv sync and also run their tests with vis-agent python --shared. By default, the extensions of such a project still get their own .venv. Then a broken .venv stops every project extension. To use the shared packages for the extensions too, set this in the project's vis.yml:

python:
  environment: shared

With this setting, Vis prepares each uv project of the project's extensions like vis-agent python --shared uv sync. It does not create or read .venv. The trusted worker imports the shared packages in ~/.vis/python/packages (or VIS_PYTHON_PACKAGES).

  • When you open the project or run /reload, Vis first checks offline that the shared packages satisfy the version ranges of the extension project. Another lock can install a newer version in the same range, and the check accepts it. Vis installs the locked versions only when a range is not satisfied.
  • When Vis prepares extensions, it does not install or check vis-agent in the shared packages. The extensions always use the bundled SDK.
  • An unmanaged scan only checks. If a range is not satisfied, the error names the packages that uv must change. It also names the vis-agent python --shared uv sync --project command to run before /reload.
  • The setting applies only to uv projects inside the project directory. Installed extension packages and global extensions keep their own environments.

The default value is project. All shared install limits also apply here: every shared consumer uses the same versions and the same editable checkout.

See also