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/:
-
Generate or deliberately update the lock, review it and commit it:
uv lock --project ./einmal -
Install the locked project for Vis:
vis-agent python uv sync --project ./einmal --locked -
Verify both the import and its source location:
vis-agent python -c "import einmal; print(einmal.status(), einmal.__file__)"Expect
readyand this checkout'seinmal/src/einmal/__init__.py, not a copied module under~/.vis/python/packages. -
Start Vis in
project/, or run/reload. On the next turn, inspectdoc("status")and callawait status(). The tool returnsready.vis-agent extension listchecks 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
- Extension design — tool boundaries and test expectations.
- Installing and sharing extensions — other layouts and distribution.
- Extension troubleshooting — imports, readiness and stale results.
