Major refactoring of deerflow/runtime/: - runs/callbacks/ - new callback system (builder, events, title, tokens) - runs/internal/ - execution internals (executor, supervisor, stream_logic, registry) - runs/internal/execution/ - execution artifacts and events handling - runs/facade.py - high-level run facade - runs/observer.py - run observation protocol - runs/types.py - type definitions - runs/store/ - simplified store interfaces (create, delete, query, event) Refactor stream_bridge/: - Replace old providers with contract.py and exceptions.py - Remove async_provider.py, base.py, memory.py Add documentation: - README.md and README_zh.md for runtime module Remove deprecated: - manager.py moved to internal/ - worker.py, schemas.py - user_context.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
21 KiB
deerflow.runtime 设计说明
本文基于当前代码实现,说明 backend/packages/harness/deerflow/runtime 的总体设计、约束边界、stream_bridge 与 runs 的协作方式、与外部基础设施和 app 层的交互方式,以及 actor_context 如何通过动态注入实现用户隔离。
1. 总体定位
deerflow.runtime 是 DeerFlow 的运行时内核层。它位于 agent / tool / middleware 之下、app / gateway / infra 之上,主要负责定义“运行时语义”和“基础边界契约”,而不直接拥有 Web 接口、数据库模型或具体基础设施实现。
当前 runtime 的公开表面由 __init__.py 统一导出,主要包括四类能力:
runs- run 领域类型、执行 façade、生命周期观察者、store 协议
stream_bridge- 流式事件桥接契约与公共类型
actor_context- 请求/任务级的 actor 上下文与用户隔离桥
serialization- 运行时对外事件与 LangChain / LangGraph 数据的序列化能力
从结构上看,可以把当前 runtime 理解成:
runtime
├─ runs
│ ├─ facade / types / observer / store
│ ├─ internal/*
│ └─ callbacks/*
├─ stream_bridge
│ ├─ contract
│ └─ exceptions
├─ actor_context
└─ serialization / converters
2. 总体设计与约束范式
2.1 设计目标
runtime 当前最核心的设计目标是把“运行时控制面”和“基础设施实现”解耦。
它自己只关心:
- run 是什么、状态如何变化
- 执行时会产出哪些生命周期事件和流式事件
- 哪些能力必须由外部注入,例如 checkpointer、event store、stream bridge、durable store
- 当前 actor 是谁,以及下游如何据此做隔离
它刻意不关心:
- 事件是落到内存、Redis 还是别的消息介质
- run / thread / feedback 是怎么持久化的
- HTTP / SSE / FastAPI 细节
- 认证插件如何识别 request user
2.2 约束边界
当前 runtime 的边界约束比较明确:
runs负责运行编排,不直接写 ORM 或 SQL。stream_bridge只定义流语义,不提供 app 级基础设施装配。actor_context只定义运行时上下文,不依赖 auth plugin。- durable 数据只能通过协议边界接入:
RunCreateStoreRunQueryStoreRunDeleteStoreRunEventStore
- 生命周期副作用只能通过
RunObserver接入。 - 用户隔离不是散落在每个模块里做,而是通过 actor context 自上而下传递。
这套范式可以概括成一句话:
runtime 定义语义和边界,app.infra 提供实现和装配。
3. runs 子系统的设计
3.1 作用
runtime/runs 是运行编排域。它负责:
- 定义 run 的领域对象与状态机
- 组织 create / stream / wait / join / cancel / delete 等操作
- 维护进程内运行控制面
- 在执行期间发出流式事件与生命周期事件
- 通过 callbacks 收集 trace、token、title、message 等运行数据
3.2 核心对象
关键对象有:
RunSpec- 由 app 输入层构建,是执行器输入
RunRecord- 运行中的记录对象,由
RunRegistry管理
- 运行中的记录对象,由
RunStatuspending/starting/running/success/error/interrupted/timeout
RunScope- 区分 stateful / stateless 与临时 thread
3.3 当前约束
当前 runs 明确限制了一些能力范围:
multitask_strategy当前主路径只支持reject和interruptenqueue、after_seconds、批量执行等尚未进入当前主路径RunRegistry是进程内状态,不是 durable source of truth- 外部查询可以走 durable store,但控制面仍然以内存 registry 为中心
3.4 façade 与内部组件
RunsFacade 在 runs/facade.py 中暴露统一入口:
create_backgroundcreate_and_streamcreate_and_waitjoin_streamjoin_waitcancelget_runlist_runsdelete_run
它底层组合了:
RunRegistryExecutionPlannerRunSupervisorRunStreamServiceRunWaitServiceRunCreateStore/RunQueryStore/RunDeleteStoreRunObserver
也就是说,RunsFacade 是 public entry point,而真正的执行和状态推进拆散在内部组件中。
4. stream_bridge 的设计和实现思路
4.1 为什么单独抽象
StreamBridge 在 stream_bridge/contract.py 中定义。
把它单独抽象出来的原因是:run 执行期间需要一个“可订阅、可回放、可终止、可恢复”的事件通道,而这件事不能直接绑定到 HTTP SSE、in-memory queue 或 Redis 细节。
所以:
- harness 负责定义流语义
- app 层负责选择和实现流后端
4.2 契约内容
StreamBridge 当前提供这些关键方法:
publish(run_id, event, data)publish_end(run_id)publish_terminal(run_id, kind, data)subscribe(run_id, last_event_id, heartbeat_interval)cleanup(run_id, delay=0)cancel(run_id)mark_awaiting_input(run_id)start()close()
公共类型包括:
StreamEventStreamStatusResumeResultHEARTBEAT_SENTINELEND_SENTINELCANCELLED_SENTINEL
4.3 语义边界
当前契约显式区分了两类终止语义:
end/cancel/error- 是 run 级别的真实业务终止事件
close()- 是 bridge 自身关闭
- 不应被当作 run 被取消
4.4 当前实现方式
当前实际使用的实现是 app 层的 MemoryStreamBridge。
它的设计是“每个 run 一条内存事件日志”:
_RunStream保存事件列表、offset 映射、状态、subscriber 计数和 awaiting-input 标记publish()生成递增事件 ID 并追加到 per-run logsubscribe()支持 replay、heartbeat、resume、terminal 退出cleanup_loop()处理:- 过老 stream
- 长时间无 publish 的 active stream
- orphan terminal stream
- TTL 过期 stream
mark_awaiting_input()为 HITL 场景延长超时
Redis 版本当前仍在 RedisStreamBridge 中作为占位。
4.5 调用链路
stream bridge 在运行链路中的作用可以概括为:
RunsFacade
-> RunStreamService
-> StreamBridge
-> app route converts events to SSE
更具体地说:
_RunExecution._start()会发布metadata_RunExecution._stream()会把 agent 的astream()输出统一转成 bridge 事件_RunExecution._finish_success()/_finish_failed()/_finish_aborted()会发布 terminal 事件RunWaitService通过subscribe()等待values/error/ terminal- app 路由层再把这些事件转换为对外 SSE
4.6 后续扩展
后续可以沿几个方向扩展:
- Redis 真正落地,支持跨进程 / 多实例流桥接
- 更完整的 Last-Event-ID gap recovery
- 更细粒度的 HITL 状态管理
- 跨节点运行协调和 dead-letter 策略
5. 如何与外部通信,store 如何读写数据
5.1 两条主要外部边界
runtime 自身不直接发 HTTP 请求,也不直接写 ORM,但通过两条主边界与外界交互:
StreamBridge- 对外输出流式运行事件
store/observer- 对外输出 durable 数据与生命周期副作用
5.2 store 边界协议
在 runs/store 中定义了四个协议:
RunCreateStoreRunQueryStoreRunDeleteStoreRunEventStore
这些协议不是 harness 内部的数据层,而是 harness 对 app 层的依赖声明。
5.3 app 层如何提供 store 实现
当前 app 层提供了这些实现:
这里的统一模式是:
- harness 只看协议
- app 层自己决定 session、commit、访问控制和后端选型
- durable 数据最终通过
store.repositories.*落数据库,或者通过 JSONL 落盘
5.4 runs 生命周期数据是怎么写出去的
单次执行器 _RunExecution 不直接写数据库。
它把数据写出去的方式有三条:
- bridge 事件
- 流式发布给订阅者
- callback ->
RunEventStore- 执行 trace / message / tool / custom event 以批次方式落地
- lifecycle event ->
RunObserver- 把 run 开始、完成、失败、取消、thread status 更新发给 app 层观察者
5.5 RunEventStore 的后端
RunEventStore 当前由 app 层工厂 app/infra/run_events/factory.py 统一构造:
run_events.backend == "db"- 走
AppRunEventStore
- 走
run_events.backend == "jsonl"- 走
JsonlRunEventStore
- 走
因此,runtime 不关心事件最终是数据库还是文件,它只要求支持 put_batch() 和相关读取方法。
6. runs 生命周期数据、callback 和查询回写
6.1 单次 run 的主流程
_RunExecution.run() 的主流程是:
_start()_prepare()_stream()_finish_after_stream()finally_emit_final_thread_status()callbacks.flush()bridge.cleanup(run_id)
6.2 start 阶段记录什么
_start() 会:
- 把 run 状态置为
running - 发出
RUN_STARTED - 抽取首条 human message,并发出
HUMAN_MESSAGE - 捕获 pre-run checkpoint id
- 发布
metadata流事件
6.3 callbacks 收集什么
当前 callbacks 位于 runs/callbacks。
主要有三类:
RunEventCallback- 记录 run_start / run_end / llm_request / llm_response / tool_start / tool_end / tool_result / custom_event 等
- 按批 flush 到
RunEventStore
RunTokenCallback- 聚合 token 使用、LLM 调用次数、lead/subagent/middleware token、message_count、首条 human message、最后一条 AI message
RunTitleCallback- 从 title middleware 响应或 custom event 中提取 thread title
6.4 completion_data 如何形成
RunTokenCallback.completion_data() 会得到 RunCompletionData,包括:
total_input_tokenstotal_output_tokenstotal_tokensllm_call_countlead_agent_tokenssubagent_tokensmiddleware_tokensmessage_countlast_ai_messagefirst_human_message
执行器在完成 / 失败 / 取消时都会把这份数据带入 lifecycle payload。
6.5 app 层如何回写
执行器通过 RunEventEmitter 发出 RunLifecycleEvent。
app 层 StorageRunObserver 再根据事件类型回写 durable 状态:
RUN_STARTED- 更新 run 状态为
running
- 更新 run 状态为
RUN_COMPLETED- 写 completion_data
- 同步 title 到 thread metadata
RUN_FAILED- 写 error 和 completion_data
RUN_CANCELLED- 写
interrupted状态与 completion_data
- 写
THREAD_STATUS_UPDATED- 同步 thread status
6.6 查询路径
RunsFacade.get_run() / list_runs() 有两条路径:
- 注入了
RunQueryStore时,优先查 durable store - 否则回退到
RunRegistry
这意味着:
- 内存 registry 负责控制面
- durable store 负责对外查询面
7. actor_context 如何动态注入并实现用户隔离
7.1 设计目标
actor_context 在 actor_context.py 中定义。
它的目标是让 runtime 和下游基础模块可以依赖“当前 actor 是谁”这个运行时事实,但不直接依赖 auth plugin、FastAPI request 或具体用户模型。
7.2 当前实现方式
当前实现是一个基于 ContextVar 的请求/任务级上下文:
ActorContext- 当前只有
user_id
- 当前只有
_current_actorContextVar[ActorContext | None]
bind_actor_context(actor)- 绑定当前 actor
reset_actor_context(token)- 恢复之前上下文
get_actor_context()- 获取当前 actor
get_effective_user_id()- 取当前 user_id,如果没有则返回
DEFAULT_USER_ID
- 取当前 user_id,如果没有则返回
resolve_user_id(value=AUTO | explicit | None)- 在 repository / storage 边界统一解析 user_id
7.3 app 如何动态注入
动态注入链路当前在 auth plugin 侧完成。
HTTP 请求路径:
app.plugins.auth.security.middleware- 从认证后的 request user 构造
ActorContext(user_id=...) - 在请求处理期间绑定 / 重置 runtime actor context
- 从认证后的 request user 构造
app.plugins.auth.security.actor_context- 提供
bind_request_actor_context(request)和bind_user_actor_context(user_id) - 在路由或非 HTTP 入口中显式绑定 runtime actor
- 提供
非 HTTP / 外部通道路径:
这些入口在把外部消息转入 runtime 前,也会用 bind_user_actor_context(user_id) 包住执行过程。这样做的意义是:
- runtime 不区分请求来自 HTTP、飞书还是别的 channel
- 只要入口能解析出 user_id,就能把同一套隔离语义注入进去
- 同一份 runtime/store/path/memory 代码不需要知道上层协议来源
因此 runtime 自己不知道 request 是什么,也不知道 auth plugin 的 user model 长什么样;它只知道当前 ContextVar 中是否绑定了 ActorContext。
7.4 注入后的传播语义
这里的“动态注入”本质上不是把 user_id 一层层作为函数参数硬传下去,而是在 app 边界把 actor 绑定进 ContextVar,让当前请求/任务上下文中的 runtime 代码按需读取。
当前语义可以理解为:
- 入口边界先
bind_actor_context(...) - 在该上下文内创建的异步调用链共享同一个 actor 视图
- 请求结束或任务退出后用
reset_actor_context(token)恢复
这有两个直接效果:
- 运行链路中的大部分接口不需要把
user_id塞进每一层函数签名 - 真正需要 durable 隔离或路径隔离的边界,仍然可以通过
resolve_user_id()/get_effective_user_id()显式取值
7.5 用户隔离如何生效
用户隔离当前是通过“动态注入 + 下游统一读取”实现的。
几条关键链路如下:
- path / uploads / sandbox / memory
- 通过
get_effective_user_id()把 user_id 带入路径解析和目录隔离
- 通过
- app storage adapter
- 通过
resolve_user_id(AUTO)在RunStoreAdapter、ThreadMetaStorage等处做查询和写入隔离
- 通过
- run event store
AppRunEventStore会读取get_actor_context(),判断当前 actor 是否可见指定 thread
也就是说,用户隔离并不是靠单一中间件“一次性做完”,而是:
- app 边界把 actor 动态绑定进 runtime context
- runtime 及其下游模块在需要时读取该 context
- 每个边界按自己的职责决定如何使用 user_id
7.6 这种方式的优点
当前设计有几个明显优点:
- runtime 不依赖具体 auth 实现
- HTTP 和非 HTTP 入口都能复用同一套隔离机制
- user_id 可以自然传递到路径、memory、store、事件可见性等不同边界
- 需要强约束时可通过
AUTO+resolve_user_id()强制要求 actor context 存在
7.7 后续如何扩展
ActorContext 文件里已经预留了扩展点注释,后续完全可以在不破坏当前模式的前提下继续扩展:
tenant_id- 用于多租户隔离
subject_id- 用于更稳定的主体标识
scopes- 用于更细粒度授权
auth_source- 用于记录来源渠道
扩展方式建议保持现有模式不变:
- 继续由 app/auth 边界负责绑定 richer
ActorContext - runtime 只依赖抽象上下文字段,不依赖 request/user 对象
- 下游基础模块按需读取必要字段
- 在 store / path / sandbox / stream / memory 等边界逐步引入 tenant-aware 或 scope-aware 行为
更具体地说,后续如果要做多租户和更强隔离,推荐按边界渐进式扩展:
- store 边界
- 在
RunStoreAdapter、ThreadMetaStorage、feedback/event store 中引入tenant_id过滤
- 在
- 路径与沙箱边界
- 把目录分片从
user_id扩展成tenant_id/user_id
- 把目录分片从
- 事件可见性边界
- 在 run event 查询和 thread 查询时叠加
scopes或subject_id
- 在 run event 查询和 thread 查询时叠加
- 外部通道边界
- 为不同来源填充
auth_source,区分 API / channel / internal job
- 为不同来源填充
这样 runtime 仍然只依赖“当前 actor 上下文”这个抽象,不会重新耦合回 FastAPI request 或某个认证实现。
8. 与 app 层的交互
8.1 app 如何装配 runtime
当前 app 层会在 app/gateway/services/runs/facade_factory.py 装配 RunsFacade。
它会组装:
RunRegistryExecutionPlannerRunSupervisorRunStreamServiceRunWaitServiceRunsRuntimebridgecheckpointerstoreevent_storeagent_factory_resolver
StorageRunObserverAppRunCreateStoreAppRunQueryStoreAppRunDeleteStore
8.2 app.state 如何提供基础设施
init_persistence()创建:persistencecheckpointerrun_storethread_meta_storagerun_event_store
init_runtime()创建:stream_bridge
然后这些对象挂在 app.state,供依赖注入和 façade 构造使用。
8.3 stream_bridge 的 app 边界
当前具体 stream bridge 的装配已经完全属于 app 层:
- harness 只导出
StreamBridge契约 - 具体实现由
app.infra.stream_bridge.build_stream_bridge构造
这条边界非常清晰:
- harness 定义运行语义和接口
- app 选择和构造基础设施实现
9. 设计总结
可以把当前 deerflow.runtime 总结为一句话:
它是一个“以 run orchestration 为核心、以 stream bridge 为流式边界、以 actor context 为动态隔离桥、以 store / observer 为 durable 与副作用边界”的运行时内核层。
更具体地说:
runs负责编排和生命周期推进stream_bridge负责流语义actor_context负责运行时用户上下文和隔离桥serialization/converters负责对外事件与消息格式转换- app 层通过 infra 负责真正的持久化、流式基础设施和 auth 注入
这套结构的优势是:
- 运行语义与基础设施实现解耦
- 请求身份与 runtime 逻辑解耦
- HTTP、CLI、channel worker 等多种入口都可以复用同一套 runtime 边界
- 后续可平滑扩展到多租户、跨进程 stream bridge、更多 durable backend
当前的主要限制也同样清楚:
RunRegistry仍然是进程内控制面- Redis bridge 仍未落地
- 一些多任务策略和批量能力仍未进入主路径
actor_context目前只携带user_id,还没有 tenant / scopes / auth_source 等 richer context
因此,当前最准确的理解方式不是“最终态平台”,而是“已经具备清晰语义和扩展边界的 runtime kernel”。