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

可选的实现

所有 manager 都会缓存自己创建的 workspace,并在最后一次访问的 ttl 秒(默认 3600)后驱逐它。LocalWorkspaceManager 在下一次 get_workspace() 调用时顺带回收过期项;沙箱类 manager 还会每隔 sweep_interval 秒跑一次后台 sweeper,因此空闲的容器、沙箱或 Pod 无需等到下一个请求就会被释放。
想深入了解 workspace 究竟是什么(文件系统布局、gateway、MCP 接线、内置工具),请看 Workspace 章节。

接入智能体服务

create_app 入口处设置对应的 WorkspaceManager 实例即可。下面是七种内置实现的示例:

隔离粒度

isolation 决定 workspace 在 (user_id, agent_id, session_id) 三元组上如何共享或隔离。Manager 会在 session 创建时按所选策略生成一个 workspace_id;后续所有带着同一个 workspace_id 的请求都会落到同一个底层 workspace 上。
在 session 创建请求中显式指定 workspace_id 永远会覆盖策略。这就是内置团队工具(AgentCreate / AgentInvite)用来让子 agent 与团队 leader 共用同一个 workspace 的机制。

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

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

接线方式

开发者传给 create_app(workspace_manager=...) 的实例会被挂到 app.state 上,并通过一个 FastAPI 依赖对外暴露:
所有会碰到 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 退出它,所以它所拥有的任何后台机制(例如 Bubblewrap / Docker / E2B / Daytona / OpenSandbox / K8s manager 的空闲 TTL sweeper)都会与服务的其他部分同步启停:

端到端请求流程

服务跑起来之后,一次典型的交互长这样:
  1. 创建 session —— /session router 只计算一次 workspace id 并把它写到 session 记录上:
    显式传入的 body.workspace_id 永远优先(团队工具就是靠它让子 agent 共享 leader 的 workspace);否则由 manager 按配置的 isolation 策略生成一个。assign_workspace_id 是一个纯函数 —— 不做 I/O,不查缓存 —— 所以在热路径上调用非常便宜。
  2. 后续每个请求带着这个 session —— 无论是 chat 运行、MCP 注册还是 skill 上传 —— 都会从 session 记录里读出保存好的 workspace_id,然后请求 manager 把它落到具体的 workspace 上:
    get_workspace 是缓存命中的路径。它在 manager 内部的 dict 里查 workspace_id,刷新一下最后访问时间,就在 O(1) 内返回已初始化好的 Workspace。只有 miss 的时候才会抢一把 asyncio.Lock,把后端准备好(构建 Docker 镜像、启动 E2B 或 OpenSandbox 沙箱、创建 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 构造参数驱动。

自定义 Manager

任何基于 WorkspaceBase 构建的 workspace 类,都可以通过配一个 WorkspaceManagerBase 子类的方式接到智能体服务里。大多数情况下只需要写一小段准备 + 缓存的样板代码 —— 隔离策略由基类自动继承。
自定义 Manager
把一个 MyWorkspaceManager 实例传给 create_app(workspace_manager=...),服务就会把它当作内置 manager 一样使用。
如果后端需要一个后台 sweeper 来做空闲驱逐,或者需要跑长时间的准备逻辑,重写 __aenter__ / __aexit__ 来启停这些机制 —— DockerWorkspaceManager 的源码就是一个完整示例。

分布式部署

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