Workspace instances the Agent Service hands to each agent. It is the service-side counterpart of the agentscope.workspace module: for every workspace subclass there is one matching manager class that provisions, caches, and evicts it.
Highlights:
- Configurable isolation grain —
PER_AGENT(default),PER_SESSION, orPER_USER— decides how workspaces are shared or isolated across the(user_id, agent_id, session_id)dimensions, so one deployment can serve many tenants cleanly. - Pluggable sandbox backends — the same lifecycle contract wraps a local directory, a Docker container, an E2B cloud sandbox, or a Kubernetes Pod. Swapping backend is a one-line change in
create_app.
Available managers
| Class | Description |
|---|---|
LocalWorkspaceManager | Bare-metal workspaces under a host directory. Zero infra, no sandboxing — the agent runs against your host filesystem. |
DockerWorkspaceManager | One Docker container per workspace, host directory bind-mounted for persistence. Requires a reachable Docker daemon. |
E2BWorkspaceManager | Managed cloud sandboxes via E2B. Sandboxes auto-suspend when idle and resume on next access. |
K8sWorkspaceManager | One Pod + PVC per workspace on a Kubernetes cluster. Fits production clusters where you already run other workloads. |
Integrate with Agent Service
Pick the manager that matches where you want the agent’s tools to execute and pass it tocreate_app.
Isolation grain
isolation decides how workspaces are shared or isolated across the (user_id, agent_id, session_id) triple. The manager mints a workspace_id at session-creation time under the selected policy; every request carrying the same workspace_id lands in the same underlying workspace.
| Value | Sharing rule | Typical use |
|---|---|---|
PER_AGENT (default) | All sessions of the same (user_id, agent_id) share one workspace | Give each agent a persistent, per-user working directory across chats — files, skills, MCP registrations survive between sessions. |
PER_SESSION | Every session gets its own workspace | Sessions must not leak state to each other — e.g. one-shot sandboxed evaluations or short-lived automation runs. |
PER_USER | All sessions of the same user_id share one workspace, regardless of which agent | Multiple agents of the same user collaborate on one filesystem (rare; use with care). |
Explicit
workspace_id on the session-creation request always overrides the policy. This is what the built-in team tools (AgentCreate / AgentInvite) rely on to make a sub-agent’s session share the team leader’s workspace.How it works in the Agent Service
The workspace manager is a singleton application-scoped resource: one instance per Agent Service process, shared across every request. Its job is to (a) decide which workspace a request belongs to under the configuredisolation policy, and (b) serve that workspace as a cheap dependency to the routers and services that need it.
Wiring
The instance you pass tocreate_app(workspace_manager=...) is attached to app.state and exposed to the rest of the codebase through a FastAPI dependency:
- Routers —
/session,/workspace/*(MCP, skills). They callDepends(get_workspace_manager)and thenmanager.get_workspace(...)to resolve the caller’s workspace. ChatService— receives the manager in its constructor from the lifespan and callsget_workspaceon every chat run to obtain the workspace the agent will act inside.- Built-in team tools (
AgentCreate/AgentInvite) — receive the manager to callassign_workspace_id(...)when spinning up sub-agent sessions, so the sub-agent lands in the same workspace as the leader.
Lifecycle
The manager is an async context manager. The application’slifespan enters it on startup and exits it on shutdown through a single AsyncExitStack, so any background machinery it owns (e.g. the idle-TTL sweeper in DockerWorkspaceManager / E2BWorkspaceManager / K8sWorkspaceManager) starts and stops in lockstep with the rest of the service:
End-to-end request flow
Once the service is running, a typical interaction goes:-
Session creation — the
/sessionrouter computes the workspace id once and persists it on the session record:An explicitbody.workspace_idalways wins (that’s how team tools make a sub-agent share the leader’s workspace); otherwise the manager mints one under the configuredisolationpolicy.assign_workspace_idis a pure function — no I/O, no cache lookup — so it is cheap to call on the hot path. -
Every subsequent request carrying that session — chat runs, MCP registration, skill upload, … — pulls the stored
workspace_idoff the session record and asks the manager to materialise it:get_workspaceis the cache-hit path. It looks upworkspace_idin the manager’s internaldict, refreshes the last-access timestamp, and returns the initialisedWorkspacein O(1). Only on a miss does it acquire anasyncio.Lock, provision the backend (build the Docker image, start the E2B sandbox, create the K8s Pod, …), and cache the result — the lock prevents concurrent requests for the same id from racing to spin up two backends. -
Idle eviction — the sandbox-backed managers run a background sweeper (started in
__aenter__) that periodically walks the cache, closes any workspace whose last-access time exceedsttl, and drops it. The next request for that id re-provisions transparently. -
Shutdown — the lifespan exits the manager, which calls
close_all()and tears down every cached workspace and its background sweeper.
Because
workspace_id is minted once at session creation and stored, changing the isolation policy on a running deployment does not re-partition existing sessions — they keep the id they were assigned. New sessions pick up the new policy.Core APIs
Every manager implements the following contract fromWorkspaceManagerBase. Custom subclasses only need to fill in the abstract methods; the isolation logic in assign_workspace_id is inherited from the base and driven by the isolation constructor argument.
| Method | Purpose |
|---|---|
assign_workspace_id(*, user_id, agent_id, session_id) -> str | Mint a workspace id for a fresh session under the configured isolation policy. Pure function, no I/O. Called by the session-creation flow when the client omits workspace_id. |
get_workspace(user_id, agent_id, session_id, workspace_id=None) -> WorkspaceBase | Return an initialized workspace bound to workspace_id. Cache-hit path on the hot request loop; on miss the manager provisions the underlying backend and caches the result. workspace_id=None falls back to assign_workspace_id. |
create_workspace(user_id, agent_id, session_id) -> WorkspaceBase | Provision a brand-new workspace and track it. Used when the caller has no persisted id yet. |
close(workspace_id) | Evict one workspace from the cache and tear down its backend. |
close_all() | Evict every cached workspace — called on service shutdown. |
async with manager: ... | Enter/exit the manager’s lifetime. Enter starts background machinery (e.g. the TTL sweeper); exit calls close_all. |
Custom manager
Any workspace class you build againstWorkspaceBase can be surfaced to the Agent Service by pairing it with a WorkspaceManagerBase subclass. In most cases you only need to fill in a small provisioning + cache scaffold — the isolation policy is inherited.
Custom manager
MyWorkspaceManager to create_app(workspace_manager=...) and the service will use it exactly like the built-in ones.
Distributed deployment
LocalWorkspaceManager and DockerWorkspaceManager are single-node — the workspace state lives on the host running the service process. In a horizontally scaled deployment (multiple Agent Service worker nodes behind a load balancer) a request for the same workspace_id can land on any node, and neither manager can reach a workspace provisioned on another node.
For distributed deployments use a cloud-managed sandbox backend:
E2BWorkspaceManager— sandboxes are addressable by metadata across nodes; the service simply reattaches to the existing sandbox on cache miss.K8sWorkspaceManager— Pods and PVCs are cluster-scoped resources; any service replica in the cluster reattaches to the same Pod by workspace-id-derived name.