跳转到主要内容
工作区管理器(Workspace Manager)持有隔离策略,并管理智能体服务中每个智能体的 Workspace 实例的生命周期。它是 agentscope.workspace 模块在服务侧的对应实现 —— 每一种 WorkspaceBase 子类都有一个配套的 WorkspaceManagerBase 的子类来负责创建、缓存与删除。 它的特点:
  • 可配置的隔离粒度 —— PER_AGENT(默认)、PER_SESSIONPER_USER —— 决定 workspace 在 (user_id, agent_id, session_id) 三个维度上如何共享或隔离,让同一套部署可以干净地服务多个租户。
  • 可插拔的沙箱后端 —— 同一份生命周期契约既可以包裹本地目录、Docker 容器、E2B 云沙箱,也可以对应 Kubernetes Pod。切换后端只需在 create_app 中改一行。

可选的实现

说明
LocalWorkspaceManager直接落在宿主目录下的裸机式 workspace。零基础设施、零沙箱化 —— 智能体直接跑在开发者的宿主文件系统上。
DockerWorkspaceManager每个 workspace 对应一个 Docker 容器,宿主目录以 bind mount 的方式挂进去做持久化。需要一个可达的 Docker daemon。
E2BWorkspaceManager通过 E2B 托管的云沙箱。空闲时自动挂起,下一次访问时恢复。
K8sWorkspaceManager在 Kubernetes 集群上为每个 workspace 起一个 Pod + PVC。适合已经在跑其他负载的生产集群。
想深入了解 workspace 究竟是什么(文件系统布局、gateway、MCP 接线、内置工具),请看 Workspace 章节。

接入智能体服务

create_app 入口处设置对应的 WorkspaceManager 实例即可。下面是四种内置实现的示例:
from agentscope.app import create_app
from agentscope.app.workspace_manager import (
    IsolationPolicy,
    LocalWorkspaceManager,
)

workspace_manager = LocalWorkspaceManager(
    # 宿主根目录;每个 agent 的工作目录落在 `<basedir>/<user_id>/<agent_id>` 下。
    basedir="/data/workspaces",
    # `PER_AGENT`(默认):同一 agent 的所有 session 共享一个 workspace。
    # `PER_SESSION` / `PER_USER` 改变粒度,详见下文。
    isolation=IsolationPolicy.PER_AGENT,
)

app = create_app(
    # ...existing code...
    workspace_manager=workspace_manager,
)

隔离粒度

isolation 决定 workspace 在 (user_id, agent_id, session_id) 三元组上如何共享或隔离。Manager 会在 session 创建时按所选策略生成一个 workspace_id;后续所有带着同一个 workspace_id 的请求都会落到同一个底层 workspace 上。
取值共享规则典型场景
PER_AGENT(默认)同一 (user_id, agent_id) 下的所有 session 共享一个 workspace让每个 agent 拥有一份持久化的、按用户区分的工作目录 —— 文件、skill、MCP 注册跨 session 也能保留下来。
PER_SESSION每个 session 一个独立的 workspace需要保证 session 之间互不泄露状态的场景 —— 例如一次性沙箱评测、短生命周期的自动化任务。
PER_USER同一 user_id 下的所有 session 共享一个 workspace,不区分是哪个 agent同一用户的多个 agent 在同一份文件系统上协作(少见,谨慎使用)。
在 session 创建请求中显式指定 workspace_id 永远会覆盖策略。这就是内置团队工具(AgentCreate / AgentInvite)用来让子 agent 与团队 leader 共用同一个 workspace 的机制。

在智能体服务中的工作原理

工作区管理器是一个应用级单例资源:一个智能体服务进程只有一份实例,所有请求共享。它的职责有两个:(a)在配置的 isolation 策略下判断一次请求属于哪个 workspace;(b)把该 workspace 作为一个廉价依赖交给需要它的路由和服务。

接线方式

开发者传给 create_app(workspace_manager=...) 的实例会被挂到 app.state 上,并通过一个 FastAPI 依赖对外暴露:
# agentscope/app/_app.py
app.state.workspace_manager = workspace_manager

# agentscope/app/deps.py
async def get_workspace_manager(request: Request) -> WorkspaceManagerBase:
    return request.app.state.workspace_manager
所有会碰到 workspace 的 router、service 与内置工具都通过这个依赖来消费它(服务类则通过构造函数注入):
  • Router —— /session/workspace/*(MCP、skill)。它们调用 Depends(get_workspace_manager),然后用 manager.get_workspace(...) 解析出调用方对应的 workspace。
  • ChatService —— 在 lifespan 里通过构造函数拿到 manager,每次 chat 运行时调用 get_workspace 拿到 agent 将要作用其中的 workspace。
  • 内置团队工具AgentCreate / AgentInvite)—— 拿到 manager 后在派生子 agent session 时调用 assign_workspace_id(...),让子 agent 落到与 leader 相同的 workspace 里。

生命周期

Manager 是一个异步上下文管理器。应用的 lifespan 会在启动时进入它,在关闭时通过一个 AsyncExitStack 退出它,所以它所拥有的任何后台机制(例如 DockerWorkspaceManager / E2BWorkspaceManager / K8sWorkspaceManager 的空闲 TTL sweeper)都会与服务的其他部分同步启停:
# agentscope/app/_lifespan.py(节选)
workspace_manager = app.state.workspace_manager
async with AsyncExitStack() as stack:
    await stack.enter_async_context(storage)
    await stack.enter_async_context(message_bus)
    await stack.enter_async_context(workspace_manager)  # ← 就是这里
    ...
    yield
# ← 退出时,`__aexit__` 会调用 `close_all()` 拆掉每一个缓存中的 workspace。

端到端请求流程

服务跑起来之后,一次典型的交互长这样:
  1. 创建 session —— /session router 只计算一次 workspace id 并把它写到 session 记录上:
    # agentscope/app/_router/_session.py(节选)
    resolved_workspace_id = body.workspace_id or (
        workspace_manager.assign_workspace_id(
            user_id=user_id,
            agent_id=agent_id,
            session_id=session_id,
        )
    )
    
    显式传入的 body.workspace_id 永远优先(团队工具就是靠它让子 agent 共享 leader 的 workspace);否则由 manager 按配置的 isolation 策略生成一个。assign_workspace_id 是一个纯函数 —— 不做 I/O,不查缓存 —— 所以在热路径上调用非常便宜。
  2. 后续每个请求带着这个 session —— 无论是 chat 运行、MCP 注册还是 skill 上传 —— 都会从 session 记录里读出保存好的 workspace_id,然后请求 manager 把它落到具体的 workspace 上:
    workspace = await workspace_manager.get_workspace(
        user_id, agent_id, session_id, session_record.config.workspace_id,
    )
    
    get_workspace 是缓存命中的路径。它在 manager 内部的 dict 里查 workspace_id,刷新一下最后访问时间,就在 O(1) 内返回已初始化好的 Workspace。只有 miss 的时候才会抢一把 asyncio.Lock,把后端准备好(构建 Docker 镜像、启动 E2B 沙箱、创建 K8s Pod……),再把结果缓存起来 —— 这把锁能防止多个请求同时冲进来给同一个 id 起两份后端。
  3. 空闲驱逐 —— 沙箱类的 manager 会跑一个后台 sweeper(在 __aenter__ 里启动),周期性遍历缓存,把最后访问时间超过 ttl 的 workspace 关闭并踢出。下一次访问该 id 时会透明地重新准备一份。
  4. 关闭 —— lifespan 退出 manager,manager 调用 close_all() 拆掉所有缓存中的 workspace 及其后台 sweeper。
由于 workspace_id 只在 session 创建时生成一次并持久化,运行中的部署改动 isolation 策略并不会对现存 session 重新分组 —— 它们会继续保留当初分配的 id。只有新 session 才会走新策略。

核心 API

每个 manager 都要实现来自 WorkspaceManagerBase 的以下契约。自定义子类只需要补齐这些抽象方法即可;assign_workspace_id 里的隔离逻辑由基类提供,并由 isolation 构造参数驱动。
方法用途
assign_workspace_id(*, user_id, agent_id, session_id) -> str在配置的 isolation 策略下为一个新 session 生成 workspace id。纯函数,无 I/O。当客户端在 session 创建请求中省略了 workspace_id 时会被调用。
get_workspace(user_id, agent_id, session_id, workspace_id=None) -> WorkspaceBase返回一个绑定到 workspace_id 的已初始化 workspace。热路径上的缓存命中路径;miss 时 manager 会准备底层后端并缓存结果。workspace_id=None 会回退到 assign_workspace_id
create_workspace(user_id, agent_id, session_id) -> WorkspaceBase全新准备一个 workspace 并纳入管理。调用方还没有可复用的 id 时使用。
close(workspace_id)把某一个 workspace 从缓存中踢掉,并拆掉它的后端。
close_all()踢掉所有缓存的 workspace —— 服务关闭时调用。
async with manager: ...进入 / 退出 manager 的生命周期。进入时启动后台机制(例如 TTL sweeper);退出时调用 close_all

自定义 Manager

任何基于 WorkspaceBase 构建的 workspace 类,都可以通过配一个 WorkspaceManagerBase 子类的方式接到智能体服务里。大多数情况下只需要写一小段准备 + 缓存的样板代码 —— 隔离策略由基类自动继承。
自定义 Manager
import asyncio
import time
from typing import Self

from agentscope.app.workspace_manager import (
    IsolationPolicy,
    WorkspaceManagerBase,
)
from agentscope.workspace import WorkspaceBase


class MyWorkspaceManager(WorkspaceManagerBase):
    """封装自定义 `MyWorkspace` 后端的轻量 manager。"""

    def __init__(
        self,
        *,
        isolation: IsolationPolicy = IsolationPolicy.PER_AGENT,
        ttl: float = 3600.0,
    ) -> None:
        super().__init__(isolation=isolation)
        self._ttl = ttl
        # workspace_id -> (workspace, last_access_monotonic)
        self._cache: dict[str, tuple[WorkspaceBase, float]] = {}
        self._lock = asyncio.Lock()

    async def get_workspace(
        self,
        user_id: str,
        agent_id: str,
        session_id: str,
        workspace_id: str | None = None,
    ) -> WorkspaceBase:
        # 当此 session 没有持久化的显式绑定时,
        # 回退到 manager 的 isolation 策略。
        if workspace_id is None:
            workspace_id = self.assign_workspace_id(
                user_id=user_id,
                agent_id=agent_id,
                session_id=session_id,
            )
        async with self._lock:
            hit = self._cache.get(workspace_id)
            if hit is not None:
                ws, _ = hit
                self._cache[workspace_id] = (ws, time.monotonic())
                return ws
            ws = MyWorkspace(workspace_id=workspace_id)  # 你的后端
            await ws.initialize()
            self._cache[workspace_id] = (ws, time.monotonic())
            return ws

    async def create_workspace(
        self,
        user_id: str,
        agent_id: str,
        session_id: str,
    ) -> WorkspaceBase:
        ws = MyWorkspace()  # 让 workspace 自己生成 id
        await ws.initialize()
        async with self._lock:
            self._cache[ws.workspace_id] = (ws, time.monotonic())
        return ws

    async def close(self, workspace_id: str) -> None:
        async with self._lock:
            entry = self._cache.pop(workspace_id, None)
        if entry is not None:
            await entry[0].close()

    async def close_all(self) -> None:
        async with self._lock:
            entries = list(self._cache.values())
            self._cache.clear()
        await asyncio.gather(
            *(ws.close() for ws, _ in entries),
            return_exceptions=True,
        )
把一个 MyWorkspaceManager 实例传给 create_app(workspace_manager=...),服务就会把它当作内置 manager 一样使用。
如果后端需要一个后台 sweeper 来做空闲驱逐,或者需要跑长时间的准备逻辑,重写 __aenter__ / __aexit__ 来启停这些机制 —— DockerWorkspaceManager 的源码就是一个完整示例。

分布式部署

LocalWorkspaceManagerDockerWorkspaceManager 都是单节点的 —— workspace 状态活在跑服务进程的那台机器上。如果做水平扩容(负载均衡后挂多个智能体服务 worker 节点),同一个 workspace_id 的请求可能落到任意一个节点,而这两个 manager 都够不到其他节点上准备好的 workspace。 分布式部署要用云托管的沙箱后端
  • E2BWorkspaceManager —— 沙箱可以通过 metadata 跨节点寻址;缓存 miss 时服务直接重连已存在的沙箱即可。
  • K8sWorkspaceManager —— Pod 与 PVC 都是集群级资源;集群内任意服务副本都可以按 workspace-id 派生出的名字重连到同一个 Pod。
本地开发用 LocalWorkspaceManager,单机生产用 DockerWorkspaceManager,一旦要横向扩容就切到 E2B 或 K8s。