Skip to main content
The workspace manager owns the isolation policy and lifecycle of the 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 grainPER_AGENT (default), PER_SESSION, or PER_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.
For an in-depth look at what a workspace is (filesystem layout, gateway, MCP wiring, builtin tools), see the Workspace building-block chapter.

Integrate with Agent Service

Pick the manager that matches where you want the agent’s tools to execute and pass it to create_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 configured isolation policy, and (b) serve that workspace as a cheap dependency to the routers and services that need it.

Wiring

The instance you pass to create_app(workspace_manager=...) is attached to app.state and exposed to the rest of the codebase through a FastAPI dependency:
Every router, service, and built-in tool that touches a workspace consumes it via this dependency (or via constructor injection for the services):
  • Routers/session, /workspace/* (MCP, skills). They call Depends(get_workspace_manager) and then manager.get_workspace(...) to resolve the caller’s workspace.
  • ChatService — receives the manager in its constructor from the lifespan and calls get_workspace on every chat run to obtain the workspace the agent will act inside.
  • Built-in team tools (AgentCreate / AgentInvite) — receive the manager to call assign_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’s lifespan 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:
  1. Session creation — the /session router computes the workspace id once and persists it on the session record:
    An explicit body.workspace_id always wins (that’s how team tools make a sub-agent share the leader’s workspace); otherwise the manager mints one under the configured isolation policy. assign_workspace_id is a pure function — no I/O, no cache lookup — so it is cheap to call on the hot path.
  2. Every subsequent request carrying that session — chat runs, MCP registration, skill upload, … — pulls the stored workspace_id off the session record and asks the manager to materialise it:
    get_workspace is the cache-hit path. It looks up workspace_id in the manager’s internal dict, refreshes the last-access timestamp, and returns the initialised Workspace in O(1). Only on a miss does it acquire an asyncio.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.
  3. 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 exceeds ttl, and drops it. The next request for that id re-provisions transparently.
  4. 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 from WorkspaceManagerBase. 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 against WorkspaceBase 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
Pass an instance of MyWorkspaceManager to create_app(workspace_manager=...) and the service will use it exactly like the built-in ones.
If your backend needs a background sweeper for idle eviction or long-running provisioning, override __aenter__ / __aexit__ to start/stop that machinery — see the source of DockerWorkspaceManager for a full example.

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 the agentscope.workspace.id label; any service replica reattaches by that label on cache miss.
  • OpenSandboxWorkspaceManager — sandboxes carry the agentscope.workspace.id metadata tag; any service replica reattaches by filtering list_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.
Keep LocalWorkspaceManager for local development, BubblewrapWorkspaceManager or DockerWorkspaceManager for a single-host production box, and pick E2B, Daytona, OpenSandbox, or K8s once you scale out.