OpenProgram · 设计 · 运行时 / UI
嵌套 LLM 节点内容:聊天式有序块
展开 LLM 节点后,按执行顺序显示完整可展示内容:Markdown 正文、思考摘要、函数引用、并行组、重试边界。复用聊天窗口组件。取消固定的「LLM Reply / Reasoning」分区;底层仍区分内容类型。本页定义当前协议、持久化和 UI 语义。
0. 现状复核(对照最新代码)
| 能力 | 实现边界 | 必须保持的约束 |
|---|---|---|
| execution_stream.v1 | 每个嵌套 LLM 节点维护 attempt 与有序 blocks;块包含 text、reasoning_summary、tool_ref、tool_arguments、refusal 等类型。 | 所有增量和 snapshot 都使用相同的 block_id、block_index;前端不根据 WS 到达时间重排。 |
| DAG / tool identity | 工具节点的 metadata 保存原始 tool_call_id 与 qualified tool_call_occurrence_id;tool_ref 只保存引用的 ref_node_id、名称和 occurrence。 | 同一个 occurrence 只能对应一个工具行;provider 重新使用原始 call id 时,按 round/message-qualified occurrence 建立新行。参数、进度、结果和错误从该 DAG 节点读取,不能在 stream 中复制第二份终态数据。 |
| attempt 语义 | 正常工具结果后的模型请求仍属于当前 attempt;传输失败、结构化输出失败等真正恢复才创建新 attempt。 | turn_end 只表示一次 provider round 结束,不自动创建 attempt 边界;新 attempt 必须有明确失败原因。 |
| 持久化 | checkpoint、metadata.stream、live snapshot 和 terminal snapshot 都包含完整可展示 attempts/blocks(受 retention 策略约束);legacy preview 字段仍可截断。 | owner 消失或 worker 重启后,历史读取接口只凭持久化 snapshot 恢复顺序、引用和终态,不重新调用模型。live snapshot 也不能静默丢失 Markdown 前缀。 |
工具引用身份:不重复传输工具参数/结果全文。工具执行入口在创建或发现 DAG 工具节点时写入原始
tool_call_id 和 qualified occurrence,stream 再通过 occurrence_id → ref_node_id 发布引用。映射未及时可见时先发布带稳定 occurrence 的占位行,随后用同一 block_id 补齐 ref_node_id;不能改成第二行或伪造完成状态。现有
tool_arguments 块适合「参数仍在生成」的瞬时预览;终态展示以树节点为准,避免双源真相。1. 展开后如何排列
- 正文直接 Markdown,无「LLM Reply」标题。
- 思考摘要出现在实际发生的位置,默认可折叠;仅当模型返回可展示摘要时显示,不推测不补写。
- 函数行出现在调用发起位置;展开看参数/进度/结果/错误(数据来自被引用节点)。
- 函数之后的新正文接在函数行之后,不得并入前一段正文。
- 内部签名等不作为聊天内容。
- 「完整」= 全部可展示内容可访问;大工具结果可折叠/按需加载,但不能只留截断摘要却无法打开全文。
2. 执行树与节点内容的职责
| 视图 | 用途 |
|---|---|
| 执行树 | 调用关系、并行分支、运行状态、耗时、失败位置 |
| 节点内容 | 该 LLM 节点内部按序发生的正文 / 思考 / 函数引用 |
- 卡片折叠:名称、状态、耗时、短预览。
- 卡片展开与右侧详情:同一
LlmNodeContent组件,禁止两套逻辑。 - 函数行 = 对真实执行节点的引用;点击定位树节点;禁止复制工具状态/结果。
3. 函数内部再调模型
- 允许递归:函数展开后可见内层 LLM 的同一内容组件。
- 默认:当前选中 LLM 展开;内部函数只显示摘要行;用户展开后再进子内容。
- 深层达到递归上限时显示明确边界,并提供「在详情中查看」入口;详情面板从该 DAG 节点重新以根节点渲染内容。
- 展开状态按
node_id、attempt_id、block_id持久在会话 UI 状态;流式更新和重新挂载不重置。 - 递归内容最多渲染 8 层;达到上限只隐藏后续内嵌内容,不改变执行树、工具状态或历史数据。
- 普通代码函数:参数/状态/日志/返回值;仅当内部有模型调用才嵌套模型内容。
4. 并行函数
- 只有执行端实际启动并发批次并明确知道其成员时,多个 tool_call → 一个
parallel_group块。当前 Python dispatcher 按顺序执行多个 tool_call,因此不发布 group_id。 - 组内顺序 = 发起顺序,稳定;完成先后只改状态,不重排。
- 每个组成员从第一次发布开始就带同一个
group_id;如果组 id 在第二个成员到达时才生成,必须为此前所有成员发布带新组 id 的更新事件。前端不能通过“相邻工具行”推断并行。 - 各函数行独立更新;禁止因某函数先完成而挪到正文前。
5. 有序块协议(扩展 execution_stream.v1)
每个 LLM node_id 在每次 attempt 内维护有序 blocks[]。顺序由执行端确定;前端不得按 WS 到达时间猜测。
| 块 kind | 必要字段 | 说明 |
|---|---|---|
text | block_id, content, status | Markdown 正文;流式 delta |
reasoning_summary | block_id, content, status, retention | 可展示思考;策略可省略持久化 |
tool_ref | block_id, tool_call_id, occurrence_id, node_id, optional group_id | 引用树节点;不内嵌结果全文。occurrence 缺失时仅兼容旧记录。 |
tool_arguments | (可保留) | 参数流式预览;终态以树为准 |
parallel_group | group_id, ordered tool_ref ids | 并行外壳;组 id 由执行端产生;子引用保持发起序。没有明确并发批次时不得创建。 |
attempt_boundary | attempt_id, reason, status | 失败重试边界;旧 attempt 折叠保留 |
refusal / unsupported | 已有 | 保持 |
继续生成 vs 失败重试
- 正常工具后继续:同一 attempt 内接着追加 text / tool_ref。
- 失败重试:新 attempt;UI 显示边界;新输出不得拼到上一段半截 Markdown。
- 重复事件:同一 qualified
occurrence_id不生成第二行;原始tool_call_id可在不同 provider round 中重复。 - 工具节点晚到:先占位
tool_ref(稳定 id),再补齐树数据。 - 一次 provider round 结束后,如果产生了工具结果,下一次模型请求继续写入同一个 attempt;只有恢复策略显式调用
start_attempt(reason=...)才产生新的 attempt。
{
"type": "execution_stream",
"schema_version": 1,
"op": "block_started",
"node_id": "call_9f3e",
"attempt_id": "att_2",
"block_id": "blk_tool_7",
"block_index": 4,
"kind": "tool_ref",
"tool_call_id": "call_abc",
"occurrence_id": "occ_7e4c",
"ref_node_id": "exec_read_file_01",
"group_id": "par_1"
}
6. Markdown 流式与滚动
- 复用顶层 chat 的
renderMarkdown/ ThinkingStep / FunctionStep 视觉与安全策略。 - 进行中的 text 块增量更新;已结束块不整段重渲。
- 未闭合代码块/表格/链接不导致整区消失。
- 大内容按块虚拟化或按需加载。
- 贴底跟随;上滚停止跟随;展开函数/高亮完成尽量保阅读位置。
- 「复制正文」≠「复制完整执行记录」(后者可含思考与工具参数)。
7. 断线 / 刷新 / 失败
- 持久化有序块 + 函数引用(及 attempt 列表);禁止仅存最终 reply 再靠树临时拼接。
- 刷新后交织顺序与实时一致;已完成函数不得显示为执行中。
- 未完成正文保留,并标明中断/取消/失败。
- 未持久化的思考显示「此内容未保存」,不伪造空白成功。
- 恢复用 snapshot,禁止为了显示再调模型。
- live snapshot 与 durable snapshot 都保留完整可展示 block 内容;只有兼容用的短 preview 字段允许受限。
- 历史读取优先取
metadata.stream.snapshot;兼容旧记录时从metadata.blocks构造单一 legacy attempt,并保留已持久化的工具块。无法解析引用时显示未解析状态,并保留tool_call_id。
8. 实现顺序
- 有序块协议 + 持久化:扩展 kind;tool_ref / parallel_group;metadata 与 checkpoint 写入完整 attempt.blocks。
- 统一内容组件
LlmNodeContent:复用 chat Markdown / 思考折叠 / 函数行(只读树)。 - 接入节点详情:去掉 Reply/Reasoning 分区;验证序列、刷新、恢复。
- 卡片展开复用同一组件。
- 并行、递归、重试边界、滚动验收。
任一阶段不得缩成「先拼两个字符串再慢慢加」——那会破坏顺序契约。
9. 验收场景(必须)
模型先输出 Markdown → 并行调用两个函数 → 其中一个函数内部再调模型(含 read_file)→ 外层继续输出;期间断线并刷新。恢复后:块顺序、函数状态、层级与断线前一致。
- 无 Reply/Reasoning 标题栏。
- 并行组内顺序不因完成先后重排。
- 重试出现 attempt 边界,半截 Markdown 不拼接。
- tool_ref 与树节点一一对应,无重复结果副本。
- owner 消失后从历史接口恢复同一 blocks 顺序;恢复的工具状态来自持久化 DAG 节点。
实现状态
| 范围 | 状态 | 证据边界 |
|---|---|---|
| 协议与状态合并 | 已实现 | CallStreamState、execution-stream reducer 与协议测试覆盖有序 blocks、tool_ref、attempt 和版本恢复。 |
| 统一节点内容组件 | 已实现 | 卡片展开和右侧详情使用 LlmNodeContent;Markdown、思考折叠、工具引用和 earlier attempt 使用同一渲染路径。 |
| 权限边界、展开状态与递归详情 | 已实现 | 隐藏节点在投影前丢弃;io/llm 仅显示允许的后代;节点/attempt/block 展开状态持久化;递归超过 8 层显示详情入口。 |
| 工具身份、正常续写、并行事件一致性 | 本任务修复 | 以真实 DAG 节点映射、同一 attempt 续写和显式并行批次为验收边界。 |
| owner 消失 / worker 重启后的完整恢复 | 本任务修复 | 以持久化 snapshot 和历史读取接口的可观测结果为准;真实模型长任务仍需现场验收。 |
| 真实模型与默认 App 验收 | 待现场执行 | 需要在默认 profile、端口 18100、现有 App 中执行确定性 nested tool 场景;本页不把单元测试当作现场验收。 |