Skip to content

Types API

StateBackend

pydantic_ai_backends.backends.state.StateBackend

A filesystem kept as a JSON document: files, and the directories created.

What :class:~pydantic_ai_backends.workspaces.StateWorkspace serves as a Pydantic AI workspace, and what a host persists between runs — files and directories are both plain JSON, so they fit a PostgreSQL jsonb column.

The operations follow a filesystem's rules and raise its errors, because the workspace contract asks for exactly that: a missing path is FileNotFoundError, reading a directory IsADirectoryError, a file standing where a directory is expected NotADirectoryError.

Paths are absolute POSIX paths; . and .. are collapsed as text, since there are no symlinks here for .. to climb out of. directories records every directory created, by make_dir or as the parent of a written file, so one stays when the last thing in it is removed, as it would on disk. A directory something is stored under exists too, which is how a document written before directories existed keeps its tree.

Text is stored as lines and anything that is not UTF-8 as base64, so the document is always JSON and read_bytes returns exactly what was written.

Example
Python
import json

from pydantic_ai_backends import StateBackend

state = StateBackend()
state.write_bytes("/src/app.py", b"print('hello')")
state.make_dir("/data")
saved = json.dumps({"files": state.files, "directories": sorted(state.directories)})

loaded = json.loads(saved)
restored = StateBackend(files=loaded["files"], directories=loaded["directories"])
Source code in src/pydantic_ai_backends/backends/state.py
Python
class StateBackend:
    """A filesystem kept as a JSON document: files, and the directories created.

    What :class:`~pydantic_ai_backends.workspaces.StateWorkspace` serves as a
    Pydantic AI workspace, and what a host persists between runs — `files` and
    `directories` are both plain JSON, so they fit a PostgreSQL `jsonb` column.

    The operations follow a filesystem's rules and raise its errors, because the
    workspace contract asks for exactly that: a missing path is
    `FileNotFoundError`, reading a directory `IsADirectoryError`, a file standing
    where a directory is expected `NotADirectoryError`.

    Paths are absolute POSIX paths; `.` and `..` are collapsed as text, since
    there are no symlinks here for `..` to climb out of. `directories` records
    every directory created, by `make_dir` or as the parent of a written file, so
    one stays when the last thing in it is removed, as it would on disk. A
    directory something is stored under exists too, which is how a document
    written before `directories` existed keeps its tree.

    Text is stored as lines and anything that is not UTF-8 as base64, so the
    document is always JSON and `read_bytes` returns exactly what was written.

    Example:
        ```python
        import json

        from pydantic_ai_backends import StateBackend

        state = StateBackend()
        state.write_bytes("/src/app.py", b"print('hello')")
        state.make_dir("/data")
        saved = json.dumps({"files": state.files, "directories": sorted(state.directories)})

        loaded = json.loads(saved)
        restored = StateBackend(files=loaded["files"], directories=loaded["directories"])
        ```
    """

    def __init__(
        self,
        files: dict[str, FileData] | None = None,
        directories: Iterable[str] | None = None,
    ) -> None:
        """Load a document, or start an empty one.

        Args:
            files: Files by path. A document a previous instance produced loads
                unchanged, including one written before `encoding` existed.
            directories: Directories created, by path. Any directory holding a
                file exists anyway, so a document from an earlier version, which
                lists none, loads unchanged.
        """
        self._files: dict[str, FileData] = files if files is not None else {}
        self._directories: set[str] = {_normal(path) for path in directories or ()}

    @property
    def files(self) -> dict[str, FileData]:
        """Files by absolute path; JSON-serialisable as it stands."""
        return self._files

    @property
    def directories(self) -> set[str]:
        """Directories created; persist it with `sorted()` beside `files`."""
        return self._directories

    def is_file(self, path: str) -> bool:
        """Whether a file is stored at `path`."""
        return _normal(path) in self._files

    def is_dir(self, path: str) -> bool:
        """Whether `path` is the root, a directory created, or holds anything."""
        path = _normal(path)
        if path == "/" or path in self._directories:
            return True
        prefix = path + "/"
        return any(p.startswith(prefix) for p in self._files) or any(
            d.startswith(prefix) for d in self._directories
        )

    def exists(self, path: str) -> bool:
        """Whether `path` is a file or a directory."""
        return self.is_file(path) or self.is_dir(path)

    def read_bytes(self, path: str) -> bytes:
        """A file's exact bytes.

        Raises:
            IsADirectoryError: `path` is a directory.
            NotADirectoryError: A file stands where one of its parents should be.
            FileNotFoundError: Nothing is at `path`.
            ValueError: The stored content is base64 that does not decode — a
                document this class did not write.
        """
        path = _normal(path)
        stored = self._files.get(path)
        if stored is not None:
            return _content_bytes(stored, path)
        if self.is_dir(path):
            raise IsADirectoryError(path)
        self._check_parents(path)
        raise FileNotFoundError(path)

    def write_bytes(self, path: str, data: bytes) -> None:
        """Store a file, creating and recording its parents.

        Raises:
            IsADirectoryError: `path` is a directory.
            NotADirectoryError: A file stands where one of its parents should be.
        """
        path = _normal(path)
        if self.is_dir(path):
            raise IsADirectoryError(path)
        self._check_parents(path)
        lines, encoding = _to_storage(data)
        now = _timestamp()
        existing = self._files.get(path)
        entry = FileData(
            content=lines,
            created_at=existing["created_at"] if existing else now,
            modified_at=now,
        )
        if encoding is not None:
            entry["encoding"] = encoding
        self._files[path] = entry
        self._record_parents(path)

    def size(self, path: str) -> int:
        """The length of the file at `path` in bytes, as `read_bytes` would return it."""
        return len(self.read_bytes(path))

    def list_dir(self, path: str) -> list[tuple[str, bool]]:
        """The names directly in a directory, each with whether it is one, sorted.

        Raises:
            NotADirectoryError: `path` is a file, or a file stands in its way.
            FileNotFoundError: Nothing is at `path`.
        """
        path = _normal(path)
        if path in self._files:
            raise NotADirectoryError(path)
        if not self.is_dir(path):
            self._check_parents(path)
            raise FileNotFoundError(path)
        prefix = "/" if path == "/" else path + "/"
        entries: dict[str, bool] = {}
        for stored in (*self._files, *self._directories):
            if not stored.startswith(prefix) or stored == path:
                continue
            name, _, rest = stored[len(prefix) :].partition("/")
            entries[name] = entries.get(name, False) or bool(rest) or stored in self._directories
        return sorted(entries.items())

    def make_dir(self, path: str) -> None:
        """Create a directory and any missing parents; one that exists is fine.

        Raises:
            FileExistsError: A file is at `path`.
            NotADirectoryError: A file stands where one of its parents should be.
        """
        path = _normal(path)
        if path in self._files:
            raise FileExistsError(path)
        self._check_parents(path)
        if path != "/":
            self._directories.add(path)
            self._record_parents(path)

    def remove(self, path: str) -> None:
        """Remove a file, or a directory and everything under it.

        Raises:
            FileNotFoundError: Nothing is at `path`.
        """
        path = _normal(path)
        if self._files.pop(path, None) is not None:
            return
        if not self.is_dir(path):
            raise FileNotFoundError(path)
        prefix = "/" if path == "/" else path + "/"
        for stored in [p for p in self._files if p.startswith(prefix)]:
            del self._files[stored]
        self._directories = {d for d in self._directories if d != path and not d.startswith(prefix)}

    def _record_parents(self, path: str) -> None:
        """Record the directories above `path`, which a write or `make_dir` created."""
        parent = posixpath.dirname(path)
        while parent != "/":
            self._directories.add(parent)
            parent = posixpath.dirname(parent)

    def _check_parents(self, path: str) -> None:
        """Refuse a path that runs through a file, the way a filesystem does."""
        parent = posixpath.dirname(path)
        while parent != "/":
            if parent in self._files:
                raise NotADirectoryError(parent)
            parent = posixpath.dirname(parent)

files property

Files by absolute path; JSON-serialisable as it stands.

directories property

Directories created; persist it with sorted() beside files.

__init__(files=None, directories=None)

Load a document, or start an empty one.

Parameters:

Name Type Description Default
files dict[str, FileData] | None

Files by path. A document a previous instance produced loads unchanged, including one written before encoding existed.

None
directories Iterable[str] | None

Directories created, by path. Any directory holding a file exists anyway, so a document from an earlier version, which lists none, loads unchanged.

None
Source code in src/pydantic_ai_backends/backends/state.py
Python
def __init__(
    self,
    files: dict[str, FileData] | None = None,
    directories: Iterable[str] | None = None,
) -> None:
    """Load a document, or start an empty one.

    Args:
        files: Files by path. A document a previous instance produced loads
            unchanged, including one written before `encoding` existed.
        directories: Directories created, by path. Any directory holding a
            file exists anyway, so a document from an earlier version, which
            lists none, loads unchanged.
    """
    self._files: dict[str, FileData] = files if files is not None else {}
    self._directories: set[str] = {_normal(path) for path in directories or ()}

is_file(path)

Whether a file is stored at path.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def is_file(self, path: str) -> bool:
    """Whether a file is stored at `path`."""
    return _normal(path) in self._files

is_dir(path)

Whether path is the root, a directory created, or holds anything.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def is_dir(self, path: str) -> bool:
    """Whether `path` is the root, a directory created, or holds anything."""
    path = _normal(path)
    if path == "/" or path in self._directories:
        return True
    prefix = path + "/"
    return any(p.startswith(prefix) for p in self._files) or any(
        d.startswith(prefix) for d in self._directories
    )

exists(path)

Whether path is a file or a directory.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def exists(self, path: str) -> bool:
    """Whether `path` is a file or a directory."""
    return self.is_file(path) or self.is_dir(path)

read_bytes(path)

A file's exact bytes.

Raises:

Type Description
IsADirectoryError

path is a directory.

NotADirectoryError

A file stands where one of its parents should be.

FileNotFoundError

Nothing is at path.

ValueError

The stored content is base64 that does not decode — a document this class did not write.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def read_bytes(self, path: str) -> bytes:
    """A file's exact bytes.

    Raises:
        IsADirectoryError: `path` is a directory.
        NotADirectoryError: A file stands where one of its parents should be.
        FileNotFoundError: Nothing is at `path`.
        ValueError: The stored content is base64 that does not decode — a
            document this class did not write.
    """
    path = _normal(path)
    stored = self._files.get(path)
    if stored is not None:
        return _content_bytes(stored, path)
    if self.is_dir(path):
        raise IsADirectoryError(path)
    self._check_parents(path)
    raise FileNotFoundError(path)

write_bytes(path, data)

Store a file, creating and recording its parents.

Raises:

Type Description
IsADirectoryError

path is a directory.

NotADirectoryError

A file stands where one of its parents should be.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def write_bytes(self, path: str, data: bytes) -> None:
    """Store a file, creating and recording its parents.

    Raises:
        IsADirectoryError: `path` is a directory.
        NotADirectoryError: A file stands where one of its parents should be.
    """
    path = _normal(path)
    if self.is_dir(path):
        raise IsADirectoryError(path)
    self._check_parents(path)
    lines, encoding = _to_storage(data)
    now = _timestamp()
    existing = self._files.get(path)
    entry = FileData(
        content=lines,
        created_at=existing["created_at"] if existing else now,
        modified_at=now,
    )
    if encoding is not None:
        entry["encoding"] = encoding
    self._files[path] = entry
    self._record_parents(path)

size(path)

The length of the file at path in bytes, as read_bytes would return it.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def size(self, path: str) -> int:
    """The length of the file at `path` in bytes, as `read_bytes` would return it."""
    return len(self.read_bytes(path))

list_dir(path)

The names directly in a directory, each with whether it is one, sorted.

Raises:

Type Description
NotADirectoryError

path is a file, or a file stands in its way.

FileNotFoundError

Nothing is at path.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def list_dir(self, path: str) -> list[tuple[str, bool]]:
    """The names directly in a directory, each with whether it is one, sorted.

    Raises:
        NotADirectoryError: `path` is a file, or a file stands in its way.
        FileNotFoundError: Nothing is at `path`.
    """
    path = _normal(path)
    if path in self._files:
        raise NotADirectoryError(path)
    if not self.is_dir(path):
        self._check_parents(path)
        raise FileNotFoundError(path)
    prefix = "/" if path == "/" else path + "/"
    entries: dict[str, bool] = {}
    for stored in (*self._files, *self._directories):
        if not stored.startswith(prefix) or stored == path:
            continue
        name, _, rest = stored[len(prefix) :].partition("/")
        entries[name] = entries.get(name, False) or bool(rest) or stored in self._directories
    return sorted(entries.items())

make_dir(path)

Create a directory and any missing parents; one that exists is fine.

Raises:

Type Description
FileExistsError

A file is at path.

NotADirectoryError

A file stands where one of its parents should be.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def make_dir(self, path: str) -> None:
    """Create a directory and any missing parents; one that exists is fine.

    Raises:
        FileExistsError: A file is at `path`.
        NotADirectoryError: A file stands where one of its parents should be.
    """
    path = _normal(path)
    if path in self._files:
        raise FileExistsError(path)
    self._check_parents(path)
    if path != "/":
        self._directories.add(path)
        self._record_parents(path)

remove(path)

Remove a file, or a directory and everything under it.

Raises:

Type Description
FileNotFoundError

Nothing is at path.

Source code in src/pydantic_ai_backends/backends/state.py
Python
def remove(self, path: str) -> None:
    """Remove a file, or a directory and everything under it.

    Raises:
        FileNotFoundError: Nothing is at `path`.
    """
    path = _normal(path)
    if self._files.pop(path, None) is not None:
        return
    if not self.is_dir(path):
        raise FileNotFoundError(path)
    prefix = "/" if path == "/" else path + "/"
    for stored in [p for p in self._files if p.startswith(prefix)]:
        del self._files[stored]
    self._directories = {d for d in self._directories if d != path and not d.startswith(prefix)}

FileInfo

pydantic_ai_backends.types.FileInfo

Bases: _FileInfoFields

Information about a file or directory.

modified_at is an ISO 8601 timestamp, present when the backend can report one: StateBackend records it on every write, filesystem-backed listings take st_mtime, and the remote wire carries it end to end. A backend that derives its listing from shell ls output has no reliable timestamp to give, so the key is absent there — read it with .get("modified_at") and treat a missing key as unknown, never as "just now".

Source code in src/pydantic_ai_backends/types.py
Python
class FileInfo(_FileInfoFields, total=False):
    """Information about a file or directory.

    `modified_at` is an ISO 8601 timestamp, present when the backend can report
    one: `StateBackend` records it on every write, filesystem-backed listings
    take `st_mtime`, and the remote wire carries it end to end. A backend that
    derives its listing from shell `ls` output has no reliable timestamp to
    give, so the key is absent there — read it with `.get("modified_at")` and
    treat a missing key as unknown, never as "just now".
    """

    modified_at: str | None
Python
from pydantic_ai_backends import FileInfo

# Example
file_info: FileInfo = {
    "name": "app.py",
    "path": "/workspace/app.py",
    "is_dir": False,
    "size": 1234,
    "modified_at": "2026-08-16T12:00:00+00:00",  # absent when the workspace cannot report one
}

FileData

pydantic_ai_backends.types.FileData

Bases: _FileDataFields

One file as StateBackend stores it.

A dictionary of these is a JSON document, and callers depend on that: the backend's whole use beyond a single process is that a host can persist StateBackend.files and hand it back to StateBackend(files=...) later. That guarantee is what encoding exists for.

Text is stored as content, split on newlines, with no encoding key. Content that is not valid UTF-8 — a PNG, a zip — is stored base64 in a single content entry with encoding="base64", and only the backend's own readers ever see the difference.

It used to be lines of text unconditionally, with bytes decoded using errors="surrogateescape". That round-tripped correctly in Python, which is exactly why it survived: the lone surrogates it produces are re-encoded by read_bytes into the original bytes, and json.dumps emits them without complaint while json.loads reads them back. Nothing stricter accepts them. PostgreSQL jsonb rejects an unpaired escape outright, a text column cannot hold one because it is not valid UTF-8, and any non-Python reader of the same document refuses it. So a workspace was serialisable right up until an agent wrote an image into it, and the failure landed at the storage layer rather than at the write that caused it.

A document written before encoding existed still loads: no key means text, and the surrogates such a document may contain are encoded back to their original bytes as they always were.

Source code in src/pydantic_ai_backends/types.py
Python
class FileData(_FileDataFields, total=False):
    """One file as `StateBackend` stores it.

    **A dictionary of these is a JSON document**, and callers depend on that:
    the backend's whole use beyond a single process is that a host can persist
    `StateBackend.files` and hand it back to `StateBackend(files=...)` later.
    That guarantee is what `encoding` exists for.

    Text is stored as `content`, split on newlines, with no `encoding` key.
    Content that is not valid UTF-8 — a PNG, a zip — is stored base64 in a
    single `content` entry with `encoding="base64"`, and only the backend's own
    readers ever see the difference.

    It used to be lines of text unconditionally, with bytes decoded using
    `errors="surrogateescape"`. That round-tripped correctly *in Python*, which
    is exactly why it survived: the lone surrogates it produces are re-encoded
    by `read_bytes` into the original bytes, and `json.dumps` emits them without
    complaint while `json.loads` reads them back. Nothing stricter accepts them.
    PostgreSQL `jsonb` rejects an unpaired escape outright, a `text` column
    cannot hold one because it is not valid UTF-8, and any non-Python reader of
    the same document refuses it. So a workspace was serialisable right up until
    an agent wrote an image into it, and the failure landed at the storage layer
    rather than at the write that caused it.

    A document written before `encoding` existed still loads: no key means text,
    and the surrogates such a document may contain are encoded back to their
    original bytes as they always were.
    """

    encoding: Literal["base64"]
    """Present only when `content` holds base64 rather than lines of text."""

encoding instance-attribute

Present only when content holds base64 rather than lines of text.

Python
from pydantic_ai_backends import FileData

# Text: lines, and no encoding key.
file_data: FileData = {
    "content": ["line 1", "line 2", "line 3"],
    "created_at": "2024-01-15T10:30:00Z",
    "modified_at": "2024-01-15T11:00:00Z",
}

# Content that is not valid UTF-8: one base64 entry, marked.
image_data: FileData = {
    "content": ["iVBORw0KGgo..."],
    "created_at": "2024-01-15T10:30:00Z",
    "modified_at": "2024-01-15T11:00:00Z",
    "encoding": "base64",
}

A dictionary of these is always a JSON document, which is what lets a host persist a StateBackend and restore it.

WriteResult

pydantic_ai_backends.types.WriteResult dataclass

Result of a write operation.

Source code in src/pydantic_ai_backends/types.py
Python
@dataclass
class WriteResult:
    """Result of a write operation."""

    path: str | None = None
    error: str | None = None
Python
from pydantic_ai_backends import WriteResult

# Success
result = WriteResult(path="/workspace/app.py")

# Error
result = WriteResult(error="Permission denied")

EditResult

pydantic_ai_backends.types.EditResult dataclass

Result of an edit operation.

Source code in src/pydantic_ai_backends/types.py
Python
@dataclass
class EditResult:
    """Result of an edit operation."""

    path: str | None = None
    error: str | None = None
    occurrences: int | None = None
Python
from pydantic_ai_backends import EditResult

# Success
result = EditResult(path="/workspace/app.py", occurrences=3)

# Error
result = EditResult(error="String not found")

ExecuteResponse

pydantic_ai_backends.types.ExecuteResponse dataclass

Response from command execution in a sandbox.

Source code in src/pydantic_ai_backends/types.py
Python
@dataclass
class ExecuteResponse:
    """Response from command execution in a sandbox."""

    output: str
    exit_code: int | None = None
    truncated: bool = False
Python
from pydantic_ai_backends import ExecuteResponse

# Example
response = ExecuteResponse(
    output="Hello, World!\n",
    exit_code=0,
    truncated=False,
)

GrepMatch

pydantic_ai_backends.types.GrepMatch

Bases: TypedDict

A single grep match result.

Source code in src/pydantic_ai_backends/types.py
Python
class GrepMatch(TypedDict):
    """A single grep match result."""

    path: str
    line_number: int
    line: str
Python
from pydantic_ai_backends import GrepMatch

# Example
match: GrepMatch = {
    "path": "/workspace/app.py",
    "line_number": 42,
    "line": "def hello_world():",
}

RuntimeConfig

pydantic_ai_backends.types.RuntimeConfig

Bases: BaseModel

Configuration for a Docker runtime environment.

Describes a pre-configured execution environment so a DockerSandbox needs no manual package installation. Give it either a ready-made image, or a base_image plus packages to build from.

Example
Python
from pydantic_ai_backends import RuntimeConfig, DockerSandbox

# Custom runtime with ML packages
ml_runtime = RuntimeConfig(
    name="ml-env",
    description="Machine learning environment",
    base_image="python:3.12-slim",
    packages=["torch", "transformers", "datasets"],
)

sandbox = DockerSandbox(runtime=ml_runtime)
Source code in src/pydantic_ai_backends/types.py
Python
class RuntimeConfig(BaseModel):
    """Configuration for a Docker runtime environment.

    Describes a pre-configured execution environment so a `DockerSandbox` needs
    no manual package installation. Give it either a ready-made `image`, or a
    `base_image` plus `packages` to build from.

    Example:
        ```python
        from pydantic_ai_backends import RuntimeConfig, DockerSandbox

        # Custom runtime with ML packages
        ml_runtime = RuntimeConfig(
            name="ml-env",
            description="Machine learning environment",
            base_image="python:3.12-slim",
            packages=["torch", "transformers", "datasets"],
        )

        sandbox = DockerSandbox(runtime=ml_runtime)
        ```
    """

    name: str
    """Unique name for the runtime (e.g., "python-datascience")."""

    description: str = ""
    """Human-readable description of the runtime."""

    image: str | None = None
    """Ready-to-use Docker image (e.g., "myregistry/python-ds:v1")."""

    base_image: str | None = None
    """Base image to build upon (e.g., "python:3.12-slim")."""

    packages: list[str] = []
    """Packages to install (e.g., ["pandas", "numpy", "matplotlib"])."""

    package_manager: Literal["pip", "npm", "apt", "cargo"] = "pip"
    """Package manager to use for installation."""

    setup_commands: list[str] = []
    """Additional setup commands to run (e.g., ["apt-get update"])."""

    env_vars: dict[str, str] = {}
    """Environment variables to set in the container."""

    work_dir: str = "/workspace"
    """Working directory inside the container."""

    cache_image: bool = True
    """Whether to cache the built image locally."""

    run_as_uid: int | None = None
    """Build the image around an unprivileged user, and run containers as them.

    `None` — the default — runs as root, which is what a container does unless
    told otherwise. Naming a uid instead adds three things to the generated
    image: a real user with that id (so `whoami` and anything else calling
    `getpwuid` works — the base image's own, if it has one at that id), a home
    directory it owns, and, if the image has `python3`, a virtualenv at
    :data:`VENV_PATH` it owns, put first on `PATH`.

    The virtualenv is the part that makes this workable rather than merely
    safer. A non-root user cannot write to the interpreter's own
    `site-packages`, so without one an agent's first `pip install` fails — and
    `uv`, unlike pip, has no `--user` mode to fall back on, so it fails with no
    way forward at all. Owning a virtualenv means both simply work, and console
    scripts land somewhere already on `PATH`.

    Only meaningful with `base_image`: a ready-made `image` was not built with
    this user, so running it unprivileged would leave an agent unable to install
    anything system-wide.

    The uid has to match whoever owns the workspace mounted into the container,
    which is why it is a number rather than a name.
    """

name instance-attribute

Unique name for the runtime (e.g., "python-datascience").

description = '' class-attribute instance-attribute

Human-readable description of the runtime.

image = None class-attribute instance-attribute

Ready-to-use Docker image (e.g., "myregistry/python-ds:v1").

base_image = None class-attribute instance-attribute

Base image to build upon (e.g., "python:3.12-slim").

packages = [] class-attribute instance-attribute

Packages to install (e.g., ["pandas", "numpy", "matplotlib"]).

package_manager = 'pip' class-attribute instance-attribute

Package manager to use for installation.

setup_commands = [] class-attribute instance-attribute

Additional setup commands to run (e.g., ["apt-get update"]).

env_vars = {} class-attribute instance-attribute

Environment variables to set in the container.

work_dir = '/workspace' class-attribute instance-attribute

Working directory inside the container.

cache_image = True class-attribute instance-attribute

Whether to cache the built image locally.

run_as_uid = None class-attribute instance-attribute

Build the image around an unprivileged user, and run containers as them.

None — the default — runs as root, which is what a container does unless told otherwise. Naming a uid instead adds three things to the generated image: a real user with that id (so whoami and anything else calling getpwuid works — the base image's own, if it has one at that id), a home directory it owns, and, if the image has python3, a virtualenv at :data:VENV_PATH it owns, put first on PATH.

The virtualenv is the part that makes this workable rather than merely safer. A non-root user cannot write to the interpreter's own site-packages, so without one an agent's first pip install fails — and uv, unlike pip, has no --user mode to fall back on, so it fails with no way forward at all. Owning a virtualenv means both simply work, and console scripts land somewhere already on PATH.

Only meaningful with base_image: a ready-made image was not built with this user, so running it unprivileged would leave an agent unable to install anything system-wide.

The uid has to match whoever owns the workspace mounted into the container, which is why it is a number rather than a name.

Python
from pydantic_ai_backends import RuntimeConfig

# Custom runtime
runtime = RuntimeConfig(
    name="ml-env",
    base_image="python:3.12-slim",
    packages=["torch", "transformers"],
    env_vars={"PYTHONUNBUFFERED": "1"},
    work_dir="/workspace",
)