OpenProgram Docs

DAG 边字段命名#

一个 DAG 节点带两种不同的父关系。它们分别存在两个不同的名字下——callerpredecessor——这样任何一处代码都不必猜某个字段指的是哪种关系。本文说明这两条边、 为什么需要分开命名,以及这两个名字在后端、前端和 WS 协议里各出现在哪里。

一、两种父关系#

一个节点要表达两种不同的父子关系

关系 含义 字段 举例
caller 谁调用了我(子调用边) 节点顶层 Call.caller 工具被哪个 LLM 调起;ROOT 调起顶层节点
conv 前驱 聊天顺序上我接在谁后面(对话链边) 顶层 Call.predecessor 第二轮 user 接在第一轮 reply 后

为什么需要两个#

两者经常不一样。典型例子是顶层 user 节点:

  • caller = ROOT(它不是被谁调用的,是会话发起)
  • conv 前驱 = 上一轮的 reply(聊天顺序)

只有一个字段没法同时表达"挂在根上"和"接在上一句后面"。分支区分靠的是 conv 前驱 (同一个 conv 前驱有多个孩子 = fork),不是 caller。

两种关系用同一个名字,正是代码里容易混淆的原因。当两者都叫 called_by(一个在节点 顶层、一个在 metadata 里)时,一个按 caller 命名的布局辅助函数实际读的是 conv 前驱; 一处本想在两个字段间取回退的渲染表达式,两边读的是同一个字段。名字分开就消除了这 一整类错误。

二、命名#

关系 名字
子调用边(谁调用我) caller
对话链边(聊天前驱) predecessor

理由:

  • caller 是前端已经在用的名字(msg dict 的 caller key、_node_caller),前后端 统一到同一个词。
  • predecessor 准确表达"对话链上的父",且不和 caller 撞名。

三、两个名字出现在哪里#

后端#

文件 符号 作用
context/nodes.py Call.caller dataclass 的边字段,语义 = caller
store/session/_msg_adapter.py _msg_to_node msg 的 callerCall.caller;msg 的 predecessorCall.predecessor(从 metadata 里 pop 掉,不留镜像)
store/session/_msg_adapter.py _node_to_msg 反向:输出 caller + predecessor 两个明确的 key
store/session/session_store.py _node_conv_predecessor Call.predecessor
store/session/session_store.py _node_caller Call.caller
store/session/memory_index.py append(node, predecessor, caller) 两个索引:children_by_predecessor(conv)/ children_by_caller(caller)
webui/graph_builder.py build_session_graph 构建 graph dict,用 predecessor + caller 两个明确 key
webui/graph_layout/_common.py predecessor_of / caller_of 两个明确的访问函数,各 layout 模块按需调对应的那个
webui/graph_layout/tier.py caller_of(子调用缩进)
webui/graph_layout/{lane,depth,topology}.py predecessor_of(对话链)

前端#

文件 符号 作用
dag/types.ts GNode predecessor(conv)和 caller(子调用)
dag/types.ts layoutParent(n) 返回 n.predecessor(conv 前驱),构建树用
dag/pipeline.ts render n.caller 判 internal;m.predecessor 驱动对话链;_signaturepredecessor
dag/render/{edges,nodes,badges}.ts predecessor 画连线 / 判分支
conversations.ts LegacyMessage / BranchRow msg/branch dict 流转,两个 key 都透传

WS 协议#

后端 graph dict 和前端读取共用 callerpredecessor 这两个键名。两端是同一套 协议:任一键名的改动都要同一批落到两侧,并重新 build。

四、不做磁盘兼容层#

代码只认 caller / predecessor。不为旧键名留别名,也不做回填:

  • Call dataclass 字段就是 caller,背后不留别名
  • _msg_to_node / _node_to_msg 只处理这两个键名
  • from_dict 不从其他键回填
  • 前端 GNode 不带无人写入的父字段

实现状态#

前后端均已实现。Call.callerCall.predecessor 是仅有的两个边字段; graph_builder 和 WS graph dict 输出两个明确的 key;_common.py 提供 predecessor_ofcaller_of 两个独立访问函数,tier 读 caller, lane/depth/topology 读 predecessor。会话数据模型以 ../dag/overview.zh.md 为权威,其中记录了这两条边。

Last updated · 2026-08-13