Dify Dify Agent 运行时资源模型解析:Home Snapshot、Workspace、Execution Binding 与 RuntimeLease 的状态所有权及生命周期管理
本文深入讲解 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.commands 与 RuntimeLease.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_ref 或 null:精确 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.py 对 BindingLostError 有专门的处理分支——证实"确认丢失即失败、不静默替换"是贯穿协议到实现的硬约束。
退役与回收:两阶段 Celery 契约
退役只是数据库状态迁移
Retirement 是从 ACTIVE 到 RETIRED 的数据库状态迁移,它阻止新的产品侧使用,且不在调用方事务内做任何网络 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.py 的 retained_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/listPOST /execution-bindings/files/readPOST /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_dir 与 RuntimeLayout.workspace_dir 是后端执行命名空间内的规范路径——它们不是宿主机路径、不是产品 id、也不是请求配置。Shell 命令从 workspace_dir 启动,HOME 被强制指向 home_dir;标准临时变量 TMPDIR、TMP、TEMP 也直接指向 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.py 对
existing_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 快照缺失等部署级取舍。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00