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 bubblewrap sandbox, a Docker container, an E2B or Daytona or OpenSandbox cloud sandbox, or a Kubernetes Pod. Swapping backend is a one-line change in
create_app.
Available managers
Every manager caches its workspaces and evicts them
ttl seconds after the last access (3600 by default). LocalWorkspaceManager collects expired entries on the next get_workspace() call; the sandboxed managers additionally run a background sweeper every sweep_interval seconds, so an idle container, sandbox, or Pod is released without waiting for the next request.
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.
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 the Bubblewrap / Docker / E2B / Daytona / OpenSandbox / K8s managers) 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 or OpenSandbox 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.
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, BubblewrapWorkspaceManager, 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 none of these managers 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.DaytonaWorkspaceManager— sandboxes carry theagentscope.workspace.idlabel; any service replica reattaches by that label on cache miss.OpenSandboxWorkspaceManager— sandboxes carry theagentscope.workspace.idmetadata tag; any service replica reattaches by filteringlist_sandbox_infos(...)on cache miss, no host-local state involved.K8sWorkspaceManager— Pods and PVCs are cluster-scoped resources; any service replica in the cluster reattaches to the same Pod by workspace-id-derived name.