Create Workspace
Each backend has its own persistence model and directory layout. Pick the tab matching your target environment:- Local
- Docker
- E2B
- Daytona
- Kubernetes
- OpenSandbox
LocalWorkspace persists state directly under workdir on the host filesystem; restarts simply re-open the same directory. The directory has the following layout:{workdir}/
├── .mcp # registered MCP client configs (JSON array)
├── data/ # offloaded multimodal payloads (deduped by SHA-256)
├── skills/ # skill subdirectories, each with SKILL.md
│ └── .skills # name/hash index for de-duplication
└── sessions/ # per-session context.jsonl and tool-result files
from agentscope.workspace import LocalWorkspace
workspace = LocalWorkspace(
workdir="/data/my-workspace",
default_mcps=[],
skill_paths=["./skills/web-search"],
)
await workspace.initialize()
DockerWorkspace bind-mounts the host workdir to /workspace inside the container, so the layout below lives on the host and survives container restarts. Omit workdir for a purely ephemeral container whose writable layer disappears with it.{workdir}/ # host directory, bind-mounted to /workspace in container
├── .mcp # registered MCP client configs (JSON array)
├── data/ # offloaded multimodal payloads
├── skills/ # skill subdirectories, each with SKILL.md
└── sessions/ # per-session context.jsonl and tool-result files
from agentscope.workspace import DockerWorkspace
workspace = DockerWorkspace(
base_image="python:3.11-slim",
workdir="/data/docker-workspaces/agent-1", # bind-mounted to /workspace
node_version="20",
extra_pip=["numpy", "pandas"],
default_mcps=[],
skill_paths=["./skills/web-search"],
)
await workspace.initialize()
E2BWorkspace uses the sandbox filesystem itself as the persistence layer; there is no host workdir. Each sandbox is tagged with workspace_id in its E2B metadata; on restart the workspace looks it up via AsyncSandbox.list(...) and reconnects with connect(sandbox_id=...). Pausing keeps disk state, resuming restores it intact.$workdir/ # inside the sandbox
├── .mcp # registered MCP client configs (JSON array)
├── data/ # offloaded multimodal payloads
├── skills/ # skill subdirectories, each with SKILL.md
└── sessions/ # per-session context.jsonl and tool-result files
from agentscope.workspace import E2BWorkspace
workspace = E2BWorkspace(
template="base",
api_key="your-e2b-api-key", # or set E2B_API_KEY
timeout_seconds=300,
default_mcps=[],
skill_paths=["./skills/web-search"],
)
await workspace.initialize()
DaytonaWorkspace targets a Daytona deployment; the sandbox filesystem is the persistence layer, so there is no host workdir. Each sandbox carries workspace_id in its Daytona labels; on restart the workspace reattaches by that label and reuses the sandbox’s disk state.$workdir/ # inside the sandbox
├── .mcp # registered MCP client configs (JSON array)
├── data/ # offloaded multimodal payloads
├── skills/ # skill subdirectories, each with SKILL.md
└── sessions/ # per-session context.jsonl and tool-result files
from agentscope.workspace import DaytonaWorkspace
workspace = DaytonaWorkspace(
api_key="your-daytona-api-key", # "" reads the SDK's env config
api_url="", # optional, for self-hosted deployments
timeout_seconds=300,
extra_pip=["numpy", "pandas"],
default_mcps=[],
skill_paths=["./skills/web-search"],
)
await workspace.initialize()
K8sWorkspace runs one Pod per workspace on a Kubernetes cluster, with a PVC mounted as the persistence layer, so the filesystem below survives Pod restarts. On restart the workspace reattaches to the Pod and PVC by their workspace-id-derived names. Set delete_pvc_on_close=True to remove the PVC when the workspace closes./workspace/ # inside the Pod, backed by the PVC
├── .mcp # registered MCP client configs (JSON array)
├── data/ # offloaded multimodal payloads
├── skills/ # skill subdirectories, each with SKILL.md
└── sessions/ # per-session context.jsonl and tool-result files
from agentscope.workspace import K8sWorkspace
workspace = K8sWorkspace(
namespace="agentscope", # namespace for the Pod and PVC
kubeconfig=None, # None uses the in-cluster config
image="python:3.11-slim", # container image
storage_size="1Gi", # PVC size backing the filesystem
default_mcps=[],
skill_paths=["./skills/web-search"],
)
await workspace.initialize()
OpenSandboxWorkspace targets an OpenSandbox deployment; the sandbox filesystem is the persistence layer, so there is no host workdir. Each sandbox is tagged with workspace_id under the agentscope.workspace.id metadata key; on restart the workspace filters list_sandbox_infos(...) by that key and resumes the matching sandbox, restoring its disk state intact./workspace/ # inside the sandbox (SANDBOX_WORKDIR)
├── .mcp # registered MCP client configs (JSON array)
├── data/ # offloaded multimodal payloads
├── skills/ # skill subdirectories, each with SKILL.md
└── sessions/ # per-session context.jsonl and tool-result files
from agentscope.workspace import OpenSandboxWorkspace
workspace = OpenSandboxWorkspace(
image="python:3.11-slim",
api_key="your-opensandbox-api-key", # or use the SDK's env fallback
domain="your-opensandbox-domain", # optional, for self-hosted deployments
protocol="http",
timeout_seconds=300,
extra_pip=["numpy", "pandas"],
default_mcps=[],
skill_paths=["./skills/web-search"],
)
await workspace.initialize()
initialize(); extra_pip packages are installed into it alongside the base gateway requirements. Because the slim base image streams apt-get + uv + pip for several minutes on a cold start, bootstrap commands run under a longer per-command timeout; leave request_timeout_seconds at its default (which matches the bootstrap budget) unless you know your image is pre-provisioned.default_mcps and skill_paths are seed-time inputs: they populate a brand-new workspace on first initialize(). On restart the workspace restores its MCP list from the persisted .mcp file, so re-passing the seeds is a no-op.Integrate with Agent
A workspace plugs intoAgent along two axes: as a source of tools, MCPs, and skills, and as the offloader for context compression:
from agentscope.agent import Agent
from agentscope.tool import Toolkit
from agentscope.workspace import LocalWorkspace
workspace = LocalWorkspace(workdir="./my-workspace")
await workspace.initialize()
agent = Agent(
name="coder",
system_prompt="You are a coding assistant.",
model=model,
toolkit=Toolkit(
tools=await workspace.list_tools(),
mcps=await workspace.list_mcps(),
skills_or_loaders=await workspace.list_skills(),
),
offloader=workspace,
)
| Axis | Wiring | What the Agent Gets |
|---|---|---|
| Resources | Toolkit(tools=..., mcps=..., skills_or_loaders=...) | Built-in tools, MCP-provided tools, and skills available in the workspace |
| Offloading | Agent(offloader=workspace) | When context compression triggers or a tool result exceeds the size limit, the agent calls workspace.offload_context() / offload_tool_result() and stores the returned reference path in place of the original payload |
list_tools() are already bound to the workspace’s execution backend, so on a sandboxed workspace they run inside the container or sandbox. To bind your own tool instances to the same environment, obtain the backend with workspace.get_backend() and pass it to the tool constructor; see Switch Tool Backend.
Because the workspace exposes its resources as flat lists, you can partition them into
ToolGroups when the agent has too many tools to keep all active at once. Pass the groups to Toolkit(tool_groups=[...]) and the agent activates them on demand through the built-in meta tool; only the reserved basic group stays always-on.from agentscope.tool import Toolkit, ToolGroup
mcps = await workspace.list_mcps()
skills = await workspace.list_skills()
toolkit = Toolkit(
tools=await workspace.list_tools(), # always active (basic group)
tool_groups=[
ToolGroup(name="search", description="Web search and retrieval.",
mcps=[m for m in mcps if m.name.startswith("search")]),
ToolGroup(name="coding", description="Code editing skills.",
skills_or_loaders=skills),
],
)