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
- Your team already has a Python library whose functions Vis should call. Declare the project and register those functions from a small entry file.
- You change that library often. Source edits need only
/reload, not a reinstall. - You want to test the library and the Vis tool separately. See Test the implementation and the tool.
- The agent's own Python code should import your library. Install the project into shared packages.
- Your project already installs everything into shared packages, and a second
.venvonly causes failures. Use shared packages for project extensions.
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/:
-
Generate or deliberately update the lock, review it and commit it:
vis-agent python 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 uv run --project ./einmal --no-sync 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.
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-agentin 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 --projectcommand 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
- Extension design — tool boundaries and test expectations.
- Installing and sharing extensions — other layouts and distribution.
- Extension troubleshooting — imports, readiness and stale results.
