首页
/ Dify Dify Agent 运行时资源模型解析:Home Snapshot、Workspace、Execution Binding 与 RuntimeLease 的状态所有权及生命周期管理

Dify Dify Agent 运行时资源模型解析:Home Snapshot、Workspace、Execution Binding 与 RuntimeLease 的状态所有权及生命周期管理

2026-09-06 12:01:26作者:范靓好Udolf

本文深入讲解 Dify Agent 运行时资源体系(Runtime Resources):Dify 如何把持久化产品资源(Home Snapshot、Workspace、Execution Binding)与请求期执行对象(RuntimeLease)彻底分离,Dify API 如何作为唯一"生命周期台账"管理这三类资源记录,以及资源从创建、执行、退役(Retirement)到物理回收(Collection)的完整闭环。读完本文,你可以准确理解 Dify Agent 多参与者隔离、Backend ref 不透明性、两阶段 Celery 资源回收契约,以及 Local / E2B / Enterprise 三种后端在物理资源映射上的差异与超时配置链路。

原始概念文档位于 dify-agent/docs/dify-agent/concepts/runtime-resources/index.md,配套操作指南见 dify-agent/docs/dify-agent/guide/index.md,Shell 层请求构成见 dify-agent/docs/dify-agent/user-manual/shell-layer/index.md

四类核心资源:持久内容与请求期执行的分离

Dify 在 Agent 运行时中对资源做了一条根本性切分:持久化产品资源归产品侧所有、可长期存在;请求期执行对象只活在单次操作的上下文中。具体是四类概念:

  • Home Snapshot(主页快照):不可变的、属于某个 Agent 的 Home 内容版本;
  • Workspace(工作区):可变的、属于某个产品作用域(会话 Conversation、Build Draft、Workflow 运行)的工作数据;
  • Execution Binding(执行绑定):一个"被实体化的 Agent 参与者",包含其私有的、可恢复会话的 Home,并挂载到某个 Workspace 上;
  • RuntimeLease(运行时租约):一次操作作用域内对物理 Binding 的访问权,是纯调用局部(invocation-local)对象。

身份语义值得特别注意:AgentWorkspaceBinding.id 同时充当参与者身份、实体化 Home 身份、持久化 Agenton 会话身份三重角色,而 agent_id 只是源 Agent 的标识。因此同一个 Agent 可以在同一个 Workspace 中拥有多个活跃的 Binding——每个 Binding 拥有独立的 Home 和会话,但共享该 Workspace 的文件。

Home 与 Workspace 在逻辑上相互独立,但后端仍可以把它们的物理表示耦合起来:当前 E2B 后端把一个 Binding 及其 Workspace 映射到同一个 E2B 资源,而 Local 后端则可以把多个实体化 Home 挂载到同一个共享 Workspace。这一点在 runtime_backend 协议定义ExecutionBindingCreateSpec 中体现为 existing_workspace_ref 字段:

@dataclass(frozen=True, slots=True)
class ExecutionBindingCreateSpec:
    tenant_id: str
    agent_id: str
    binding_id: str
    workspace_id: str
    existing_workspace_ref: str | None
    home_snapshot_ref: str | None = None

create_binding 的协议约束(见 protocols.py)明确规定:带 existing_workspace_ref 时,实现方必须"挂载而不清空"该 Workspace,不支持的共享必须在任何修改发生前就失败;失败时必须清理本次新分配的半成品资源,且不得损坏既有 Workspace。

运行时层图与租约生命周期

Agent 请求并不会暴露独立的 Home、Workspace 或 Sandbox 层。Dify API 根据产品流程选定 Binding,把它不透明的 backend ref 传给 dify.runtime 层:

flowchart LR
    EC["dify.execution_context<br/>request identity"]
    RT["dify.runtime<br/>opaque backend_binding_ref"]
    SH["dify.shell<br/>commands and jobs"]

    EC --> SH
    RT --> SH

DifyRuntimeLayer 在其资源上下文打开时调用所选 ExecutionBindingBackend.acquire(),操作结束时调用 release(),并且只在上下文活跃期间暴露由此得到的 RuntimeLease。该层不创建、不退役、不销毁任何持久资源,也绝不把后端 SDK 对象写进 Agenton 会话快照。

在源码 layer.py 中,租约的获取与释放被封装在 resource_context() 里:

@override
@asynccontextmanager
async def resource_context(self) -> AsyncGenerator[None]:
    if self._lease is not None:
        raise RuntimeError("DifyRuntimeLayer resource_context() is already active")
    async with open_runtime_lease(self.backend, self.config.backend_binding_ref) as lease:
        self._lease = lease
        try:
            yield
        finally:
            self._lease = None

配套的 open_runtime_lease 是一个确定性释放的异步上下文管理器:主操作抛异常时照常 release,若释放本身也失败且主操作已成功,则把释放异常抛出,否则降级为告警日志。RuntimeLease 接口本身极其精简——只有 layout(规范化的 home_dir / workspace_dir)与 commands(Shell 命令协议)两个属性(见 protocols.py)。

Shell 层只消费 RuntimeLease.commandsRuntimeLease.layout,仅跟踪请求局部的 shell 作业 id 与偏移量;关闭一次 run 只清空该作业的本地状态,不会退役 Binding

状态所有权:Dify API 是唯一生命周期台账

Dify API 是生命周期台账,持久化三类资源记录(见 models/agent.py,台账表由 2026-07-21 迁移 等引入):

记录 含义 后端字段
agent_home_snapshots Agent 拥有的一个不可变 Home 版本 snapshot_ref
agent_workspaces 产品作用域拥有的一个可变 Workspace backend_workspace_ref
agent_workspace_bindings 挂载到 Workspace 上的一个实体化参与者、私有 Home 与可恢复会话 backend_binding_ref

关键边界规则:

  • Backend refs 是不透明字符串,只由所选后端适配器解释;
  • Dify API 把最新的 Agenton 会话快照存放在 Binding 记录上,但绝不序列化 RuntimeLease、SDK 客户端、凭据或临时访问令牌
  • Dify Agent 不连接 Dify 产品数据库,也没有任何持久资源注册表。它的私有控制面端点只是执行 Dify API 发起的"创建/销毁后端资源"请求。Redis 中的 run 记录与事件流是可观测性状态,不是 Home/Workspace/Binding 台账;
  • 当配置了 DIFY_AGENT_API_TOKEN 时,每个私有控制面请求都必须携带与 Dify API 侧 AGENT_BACKEND_API_TOKEN 匹配的 Bearer token。

这种"产品侧记账、执行侧无状态"的设计,使得 Dify Agent 进程可以被任意重启与横向扩展,所有真相(source of truth)都在 Dify API 的数据库里。

创建与执行流程:无快照创建、fail-fast 解析

Agent 创建不产生 Home Snapshot

Agent 的创建创建 Home Snapshot。一个不带逻辑 Home Snapshot 的配置,会在 Binding 创建时请求所选后端实体化其"部署默认 Home"。这个默认 Home 是可变的、私有于该 Binding 的,既不会产生 agent_home_snapshots 行,也不会产生任何隐式 snapshot ref。

Build Draft Apply:从 Binding 捕获快照

Build Draft Apply 使用 POST /home-snapshots/from-binding(客户端实现在 client/_client.py):Dify Agent 获取精确的源 Binding 租约,通过后端的原生快照操作捕获其实体化 Home,释放租约,返回一个新的不透明 snapshot ref。随后 Dify API 落一条新的不可变 agent_home_snapshots 行,并在产生的配置版本上记录其逻辑 id。源 Binding 不可用时没有重放、也没有降级回退——协议层 HomeSnapshotBackend.create_from_runtime 的契约(protocols.py)同样规定捕获操作不得修改或释放源租约,删除操作必须幂等且不得波及由快照派生的 Binding 与 Workspace。

请求前的 Binding 解析:快速失败,绝不隐式替换

在 Agent 请求之前,Dify API 加载特定的产品上下文:

  • 若该上下文没有关联 Binding,则实体化一个,并在同一个数据库事务中保存 Binding id;
  • 否则只解析那一个 Binding,并校验其 owner 与 config/Home 世代。

缺失、已退役或不匹配的 Binding 一律快速失败:Dify API 不按 Agent、Workspace、候选数量或时间新近度去搜索,也不隐式创建替代物。

POST /execution-bindings 的请求构成

创建请求接受一个精确的 home_snapshot_refnull:精确 ref 必须无回退地实体化;null 则选择后端的部署默认 Home。它返回不透明的 Binding 与 Workspace refs。每一个 create 请求都代表一个新的参与者——即使 Agent、Snapshot、config 世代与 Workspace 与另一个 Binding 完全相同。发给 Agenton 的请求构成为:

{
  "name": "runtime",
  "type": "dify.runtime",
  "config": {"backend_binding_ref": "opaque-backend-binding-ref"}
}

执行期获取与释放语义

每个 Agent 请求在执行期间获取该 ref,结束后释放:

  • Local 的 release 关闭该操作的 shellctl 连接;
  • E2B 的 release 还会暂停(pause)底层 E2B 资源,内存保留
  • 后续请求或 Binding 文件操作会为同一个 Binding ref 获取新的租约;
  • 若后端确认资源已消失,acquire 直接失败,绝不创建一个空的替代 Workspace

这一失败语义在源码中落实为 BindingLostError:E2B 后端在 e2b.py 中检查资源是否仍存在(如 E2B Binding {binding_ref!r} no longer exists),企业后端在 enterprise.py 中做同样的存在性校验,而执行器 runner.pyBindingLostError 有专门的处理分支——证实"确认丢失即失败、不静默替换"是贯穿协议到实现的硬约束。

退役与回收:两阶段 Celery 契约

退役只是数据库状态迁移

Retirement 是从 ACTIVERETIRED数据库状态迁移,它阻止新的产品侧使用,且不在调用方事务内做任何网络 I/O。产品生命周期路径同步提交这一迁移;事务提交后,才有一个 Celery 任务请求 Dify Agent 销毁物理资源。回收器(collector)成功后删除对应的台账行。若某个 collector 抛异常,任务记录 tenant、资源类型与资源 ID,然后继续批内其他独立资源;所有资源都尝试过之后,任何失败都会使 Celery 任务失败并阻止 Agent 聚合删除。失败的 RETIRED 行保留下来供后续重试。发布 Celery 任务的失败也会向产品调用方传播。整个过程中没有自动重试、也没有对账(reconciliation)。

统一的任务 collect_agent_resources 注册在普通 Celery worker 上,显式使用既有的 retention 队列——标准 worker 已经在消费该队列,因此不需要专门的 Agent 资源 worker 或新队列。在 collect_agent_resources_task.py 中可以清楚看到两阶段契约:

@shared_task(queue="retention")
def collect_agent_resources(
    *,
    tenant_id: str,
    binding_ids: list[str],
    workspace_ids: list[str],
    home_snapshot_ids: list[str],
    purge_agent_ids: list[str] | None = None,
) -> None:
    collectors = (
        (workspace_ids, "workspace_id", AgentWorkspaceService.collect_retired_workspace),
        (binding_ids, "binding_id", AgentWorkspaceService.collect_retired_binding),
        (home_snapshot_ids, "home_snapshot_id", AgentHomeSnapshotService.collect_retired_home_snapshot),
    )
    # 第一阶段:尽力遍历整批 RETIRED 资源,单个失败不隐藏后续失败
    # 第二阶段:仅当全部成功后才 purge_archived_agents

任务 docstring 明确:"第一阶段尝试每一个显式指定的 RETIRED 工作资源;第二阶段仅在整批第一阶段成功后清除所请求的归档 Agent 聚合。任何回收失败都会跳过聚合清除,并在所有显式资源都尝试过后抛出一个汇总错误。"

Workflow 终态与 Workflow-only Agent 的隐式退役

  • Workflow 终态事件时,graph 层先同步地退役并提交该 run 的 Workspace,再入队回收任务;
  • 当某次 Workflow 变更可能使某些 Workflow-only Agent 成为孤儿时,主产品事务先提交;随后一个新会话重新检查有效所有权(effective ownership),只对仍然无人所有的 Agent 执行退役。

关于"有效引用"的定义,源码比概念文档更精确:从 retirement_service.pyretained_agent_ids 实现看,所有权键是 tenant、App、Workflow 与 Workflow 版本,且 docstring 写明"草稿和每一个已发布版本(无论当前还是历史)都同等计入所有权,App 的 current-Workflow 指针不是所有权的一部分"。该所有权检查只适用于 Workflow-only Agent 的隐式退役;对 roster Agent 或 Agent App 的显式删除即使在 Workflow 仍引用它时也照常执行。

级联规则与硬性不变量

  • 退役最后一个 Binding 时连带退役其 Workspace。Workspace 回收通过一个 Binding 销毁物理 Workspace,然后收集剩余的实体化 Home;
  • Home Snapshot 在其拥有者 Agent 退役时退役。RETIRED 是 Home Snapshot 唯一的物理删除条件——Draft 与 Config 上的 Snapshot 引用只是历史指针,不会"续命";
  • 删除批中所有外部资源都成功后,Dify API 在一个数据库事务中硬删除归档 Agent 及其 Drafts、Config Snapshots、Config Revisions、调试会话映射与资源台账。Workflow Agent 绑定属于其 Workflow 且保持不变,因此在显式删除后可能持有悬空的 Agent ID。Dify Agent 自身始终保持无状态;
  • 不变量:一个 RETIRED 的 Workspace 若没有 RETIRED 的 Binding,就无法确定通过哪个后端参与者去销毁该 Workspace——这种状态是生命周期不变量违规,回收直接失败,而不是记为成功。

没有 TTL、没有 GC、没有全局对账器

当前实现中不存在基于年龄的 TTL、周期性 GC 或全局孤儿对账器。后端的销毁操作在支持的地方是幂等的。Dify API 在后端 create 返回成功之后不做跨系统补偿:此后任何 API 层失败(Python 异常、flush、commit 失败)都可能留下物理孤儿,留给未来的全局对账器处理。

后端仍会在 create 操作返回成功之前失败时清理半成品资源——例如 E2B 在初始化失败时杀掉 Sandbox,Local 删除不完整操作创建的路径。这种后端本地清理不跨越数据库 commit 边界

Binding 文件边界:产品定位符与下载超时链

用产品定位符寻址,而非 Binding id

Dify API 的公开文件 API 接受的是产品定位符(Conversation、调试用 Build Draft,或 Workflow 节点执行),而不是 Binding id 或 backend ref。Dify API 对该对象做授权,解析其关联的活跃 Binding——它不选择"最新的 Binding",也不回退到其他产品上下文。

解析后的请求经由 Dify Agent 的私有端点到达执行侧(路由定义见 binding_files.py):

  • POST /execution-bindings/files/list
  • POST /execution-bindings/files/read
  • POST /execution-bindings/files/download

每个操作接收一个 backend_binding_ref获取一个全新的 RuntimeLease,执行文件动作,然后释放租约——文件操作与执行请求共享同一套租约纪律。

路径解析与安全边界

BindingFileService 的路径规则:

输入形式 解析基准
相对路径 workspace_dir 解析
~~/... home_dir 解析
绝对路径 保持在 Binding 文件系统命名空间内

强制 Workspace 包含性、拒绝 ..——所选后端的隔离策略才是最终权威。List 与 preview 通过 RuntimeLease.commands 运行有界检查脚本;Download 则在 Binding 内部运行 dify-agent file upload --no-download-link,让字节直接从运行时流式传输到 Dify 既有的 ToolFile 端点。Dify Agent 只返回规范的 ToolFile 引用,并在 Dify API 为浏览器签名 URL 之前释放租约。

默认下载超时链

各层留出时间接收与规范化下一层的结果,默认值构成一条单调递增的链(取值见 guide/index.md 的配置表):

配置项 默认值 所在层
sandbox CLI upload 180 秒 Binding 内 CLI
DIFY_AGENT_BINDING_FILE_DOWNLOAD_COMMAND_TIMEOUT_SECONDS 210 秒 Dify Agent shell 命令
AGENT_BACKEND_BINDING_FILE_DOWNLOAD_TIMEOUT_SECONDS 240 秒 Dify API 客户端
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS 3600 秒 E2B 资源活跃上限

RuntimeLayout 的语义

RuntimeLayout.home_dirRuntimeLayout.workspace_dir后端执行命名空间内的规范路径——它们不是宿主机路径、不是产品 id、也不是请求配置。Shell 命令从 workspace_dir 启动,HOME 被强制指向 home_dir;标准临时变量 TMPDIRTMPTEMP 也直接指向 workspace_dir,因此 Workspace 既是命令的 cwd 也是临时空间。在 Local 后端,同一个 shellctl 命名空间内可能存在兄弟实体化 Home,但路径隔离把活跃租约限制在其自己的 Home 加上共享 Workspace 之内。

后端支持矩阵

后端 Home Snapshot 操作 Binding 操作 物理关系
Local 支持 支持,包括默认空 Home 与把多个 Binding 挂载到同一个 Workspace 快照目录、每 Binding 的实体化 Home 与 Workspace 目录相互独立
E2B 支持 支持,模板支撑的默认 Home;不支持共享 Workspace 挂载 Binding 与 Workspace ref 映射到同一个 E2B 资源;checkpoint 使用 E2B snapshot
Enterprise 未实现 支持默认 Home 的 Binding 创建、acquire 与耦合式 destroy Binding 与 Workspace ref 映射到同一个 Gateway sandbox;显式 Home Snapshot 实体化快速失败

行为细节:

  • Local 为每个 Binding id 创建新 Home;在不退役 Workspace 的情况下销毁一个 Binding,兄弟 Home 与共享 Workspace 保持完好;
  • 当前 E2B 拒绝 existing_workspace_ref,抛出 shared_workspace_unsupported,因为其 Binding 与 Workspace 就是同一个 Sandbox;它也拒绝"仅 Binding"的销毁。两条路径都不创建回退 Workspace、不切换后端
  • Enterprise 中显式 Home Snapshot 实体化会 fail fast,但默认 Home 的 Binding 创建、acquire 与耦合销毁均已支持(enterprise.pyexisting_workspace_ref 的处理与 E2B 一致)。

关于 DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS:它把 E2B 资源的连续活跃时间限制在一小时,覆盖的是被一个 RuntimeLease 持有的完整 Agent run,而不是某一次工具调用。其 3600 秒默认值有意DIFY_AGENT_RUN_TIMEOUT_SECONDS 相同,但两者独立可配。资源在超时时暂停,但该配置拥有 Agent run 的终态判定,也不是保留期 TTL——它既不会删除暂停的资源,也不会删除不可变快照。

小结

Dify Agent 的运行时资源模型可以用三句话概括:逻辑上四类资源各司其职(不可变 Home Snapshot、可变 Workspace、参与者级 Binding、操作级 RuntimeLease),台账上只信 Dify API 三张表,执行侧永远无状态。租约保证每次操作获得确定性的获取/释放边界;fail-fast 的 Binding 解析与"确认丢失即失败"的 acquire 语义杜绝了静默替换;两阶段的 retention 队列回收任务把"物理销毁"与"逻辑归档"解耦,让失败的 RETIRED 行天然可重试。理解这套模型后,读者既能看懂 runtime_backend 各后端的实现约束,也能正确评估 E2B 共享 Workspace 限制、Enterprise 快照缺失等部署级取舍。

登录后查看全文
热门项目推荐
相关项目推荐