# 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](#declare-the-editable-project) and register those functions
  from a small entry file.
- **You change that library often.** Source edits need only
  [`/reload`](#reload-after-changes), not a reinstall.
- **You want to test the library and the Vis tool separately.** See [Test the
  implementation and the tool](#test-the-implementation-and-the-tool).
- **The agent's own Python code should import your library.** [Install the project
  into shared packages](#install-a-project-into-shared-packages).
- **Your project already installs everything into shared packages, and a second `.venv` only
  causes failures.** [Use shared packages for project extensions](#use-shared-packages-for-project-extensions).

For a new extension, the [one-file tutorial](extending.md) or an [automatically
prepared package](extension-packages.md#package-manifest) 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](#project-and-shared-environments) 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:

```text
project/
  einmal/
    pyproject.toml
    uv.lock                       # generated by uv lock
    src/einmal/__init__.py
    tests/test_status.py
  .vis/extensions/einmal_tools.py
```

```toml
# 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"]
```

```python
# einmal/src/einmal/__init__.py
def status() -> str:
    """Return the integration status."""
    return "ready"
```

```python
# .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](extension-api.md#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:

```toml
[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:

   ```bash
   vis-agent python uv lock --project ./einmal
   ```

2. Install the locked project for Vis:

   ```bash
   vis-agent python uv sync --project ./einmal --locked
   ```

3. Verify both the import and its source location:

   ```bash
   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`](configuration.md#python-package-index) 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:

```python
# 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:

```bash
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](#install-a-project-into-shared-packages).

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](#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](jail.md#filesystem-access). 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.

```bash
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:

```bash
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.

```bash
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`:

```yaml
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](#install-a-project-into-shared-packages)
also apply here: every shared consumer uses the same versions and the same editable checkout.

## See also

- [Extension design](extension-design.md) — tool boundaries and test expectations.
- [Installing and sharing extensions](extension-packages.md) — other layouts and distribution.
- [Extension troubleshooting](extension-troubleshooting.md) — imports, readiness and stale results.
