Workspaces¶
When your agent writes a file, where does it actually go? Into its
workspace: the environment a run works in. Every file tool — read_file,
write_file, edit_file, ls, grep — and every command execute runs goes
through it, and so do memory, context files, plans, evicted tool output and
uploads. Your agent code doesn't know which one it is: swap the workspace and the
same agent runs in memory, in a directory on disk, or in an isolated container.
Workspaces are part of Pydantic AI (2.52 and later). Inside a tool, the run's
workspace is ctx.workspace. pydantic-deep picks one for you and re-exports the
workspace capabilities from
pydantic-ai-backend.
Full reference
This page covers the essentials. For every option, how refs work and how each sandbox is isolated, see the pydantic-ai-backend workspaces docs.
Which workspace?¶
| Workspace | Files | Commands | Reach for it when… |
|---|---|---|---|
StateWorkspace() (default) |
In memory | No | testing, or anything that must leave no trace |
LocalWorkspace(path) |
A directory on this machine | Yes | a CLI tool, or working on real local files |
DockerWorkspace(...) |
A container on this host | Yes | running code you don't fully trust |
SandboxdWorkspace(...) |
A sandboxd session |
Yes | containers, without a Docker socket in the agent's process |
KubernetesWorkspace(...) |
A pod | Yes | running agents' code on a cluster |
DaytonaWorkspace(...) |
A Daytona sandbox | Yes | a hosted sandbox |
Pass one to create_deep_agent:
from pydantic_deep import create_deep_agent, LocalWorkspace
agent = create_deep_agent(workspace=LocalWorkspace("./project"))
Any Pydantic AI workspace capability works here, including the harness's E2B, Modal and Sprites workspaces.
The default has no commands
StateWorkspace() is files only, so the agent gets no execute tool unless
you ask for it with include_execute=True — and then every command answers
that this workspace does not support command execution. Pick a workspace
that runs commands when the agent needs a shell.
The workspaces, by example¶
StateWorkspace — in memory, zero side effects¶
from pydantic_deep import create_deep_agent, DeepAgentDeps
agent = create_deep_agent() # StateWorkspace() by default
result = await agent.run("Write /notes.md with three ideas", deps=DeepAgentDeps())
# The run's workspace outlives the run
print(await result.workspace.read_text("/notes.md"))
Each run without history gets a new document. A run that continues a
conversation (message_history=result.all_messages()) works in the same one:
the history carries the workspace's ref.
To start from files of your own, fill a StateBackend document and hand the run
a backend for it:
from pydantic_ai.workspaces import WorkspaceRef
from pydantic_deep import StateBackend, StateWorkspace
document = StateBackend()
document.write_bytes("/src/app.py", b"print('hello')")
documents = StateWorkspace(store={"demo": document})
result = await agent.run(
"Review /src/app.py",
deps=DeepAgentDeps(),
workspace=documents.backend(WorkspaceRef(provider="state", id="demo")),
)
LocalWorkspace — real files on disk¶
from pydantic_deep import create_deep_agent, DeepAgentDeps, LocalWorkspace
agent = create_deep_agent(workspace=LocalWorkspace("./workspace"))
result = await agent.run("Create a Python script and run it", deps=DeepAgentDeps())
Commands run on your machine, in that directory. LocalWorkspace(path,
read_only=True) refuses every write.
LocalWorkspace points the run at the directory but confines nothing: an
absolute path, a .. or a symlink reaches any file the process can. To keep the
file tools - which usually run without approval - inside it, wrap the workspace
in ConfinedWorkspace and pass it to the run:
from pydantic_ai.workspaces import LocalWorkspaceBackend, Workspace
from pydantic_deep import ConfinedWorkspace
project = ConfinedWorkspace(Workspace(LocalWorkspaceBackend("./project")))
result = await agent.run(prompt, deps=DeepAgentDeps(), workspace=project)
Commands are not confined by it; isolate those in a container. The CLI and the ACP server confine their project this way.
DockerWorkspace — safe code execution¶
from pydantic_ai.workspaces import WorkspaceRef
from pydantic_deep import create_deep_agent, DeepAgentDeps, DockerWorkspace
docker = DockerWorkspace(runtime="python-datascience", container_name="analysis")
agent = create_deep_agent(workspace=docker)
try:
result = await agent.run("Analyze data with pandas", deps=DeepAgentDeps())
finally:
await docker.destroy(WorkspaceRef(provider="docker", id="analysis"))
Containers are kept after a run
A container is created on first use and not removed when the run ends —
a later run carrying its ref attaches to it again. Remove it with
await docker.destroy(ref). A container_name makes the ref predictable;
without one, take it from result.workspace.ref.
volumes={"/host/dir": "/workspace"} mounts a host directory into the
container, so the agent can work on a project in place.
Per-run workspaces¶
An application serving many users usually wants one workspace per session rather
than one per agent. Build the agent with workspace=False and pass each run its
session's workspace:
from pydantic_ai.workspaces import Workspace
agent = create_deep_agent(workspace=False)
session_workspace = Workspace(
DockerWorkspace(container_name=f"session-{session_id}").backend()
)
result = await agent.run(prompt, deps=deps, workspace=session_workspace)
agent.run(workspace=...) also overrides the capability for one run when the
agent has one.
In your own tools¶
A tool reaches the workspace through its run context:
from pydantic_ai import RunContext
from pydantic_deep import DeepAgentDeps
async def count_lines(ctx: RunContext[DeepAgentDeps], path: str) -> int:
"""Count the lines in a file."""
return len((await ctx.workspace.read_text(path)).splitlines())
read_bytes/write_bytes, read_text/write_text, list_dir, stat,
exists, make_dir and remove work in every workspace; run works in those
that run commands. A missing file raises FileNotFoundError, a read-only
workspace raises PermissionError.
Uploads¶
deps.upload_file(name, data) queues a file for the next run. It is written
into the workspace when that run starts — under uploads/ by default — and the
system prompt tells the agent it is there. See File uploads.
Subagents and forks¶
Subagents work in their parent's workspace. Forked branches each work in an overlay of it, and the winning branch's changes are written back on merge.
Skills can live in a workspace too¶
skill_directories reads skills from the machine the agent is built on.
WorkspaceSkillsDirectory discovers them inside the run's workspace instead — a
skills/ folder in its container, its sandbox session, its document:
from pydantic_deep import WorkspaceSkillsDirectory
agent = create_deep_agent(
workspace=docker,
skill_directories=[WorkspaceSkillsDirectory(path="skills")],
)
Coming from backends¶
pydantic-deep 0.3.44 and earlier took a backend in DeepAgentDeps. That
abstraction is gone:
| Before | Now |
|---|---|
DeepAgentDeps(backend=StateBackend()) |
create_deep_agent() — StateWorkspace() is the default |
DeepAgentDeps(backend=LocalBackend(root_dir=p)) |
create_deep_agent(workspace=LocalWorkspace(p)) |
DeepAgentDeps(backend=DockerSandbox(...)) |
create_deep_agent(workspace=DockerWorkspace(...)) |
CompositeBackend(routes=...) |
no equivalent: one workspace per run |
ctx.deps.backend.read(path) |
await ctx.workspace.read_text(path) |
ctx.deps.backend.write(path, text) |
await ctx.workspace.write_text(path, text) |
ctx.deps.backend.execute(cmd) |
await ctx.workspace.run(cmd, shell=True) |
deps.files / get_files_summary() |
await result.workspace.list_dir("/") |
BackendSkillsDirectory(backend=...) |
WorkspaceSkillsDirectory(path=...) |
sandbox.stop() |
await capability.destroy(ref) |
Recap¶
- The workspace is where the run works — and it's decoupled from your agent code.
StateWorkspacefor tests,LocalWorkspacefor real work,DockerWorkspaceand the other sandboxes for untrusted code.- Choose it with
create_deep_agent(workspace=...), or per run withagent.run(workspace=...); read it in tools asctx.workspace.
Learn more¶
- Workspaces documentation — the full reference
- Docker — containers, runtimes and isolation
- Console toolset — the file and shell tools