OpenHuman 工作流设计深潜:tinyflows 节点 I/O 信封契约与可选 Agent Kind(agent_ref)
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
本文以 OpenHuman 仓库中的规划文档 tinyflows — Node I/O Alignment & Selectable Agent Kinds 为核心,完整拆解该计划的两条主线:Part A 为 tinyflows 引擎的能力节点(agent/tool_call/http_request/code)引入稳定的输出信封(envelope),消除节点间"碰运气式"的数据形状对齐;Part B 让工作流中的agent节点可以通过agent_ref选择一种已注册的 Agent Kind(coding agent、researcher、crypto agent……),使其携带各自的工具集、模型提示、沙箱与迭代策略参与执行。读完本文,你可以掌握该设计的问题根因、信封契约的字段语义、per-item 执行与端口感知路由的原理,以及AgentRunner能力在 OpenHuman 宿主侧的实际实现路径(含超时、并发、HITL 审批等关键细节)。
该文档状态为proposed(2026-07-05),是 tinyflows 集成计划 的扩展。需要说明的是:截至本文撰写时的仓库快照,Part B 的大部分设计已在源码中落地(agent_ref路由、AgentRunner宿主实现、builder 工具与 UI 选择器均存在),下文会在对应位置用源码证据对照说明"计划设计"与"当前实现"的关系。
一、问题背景:同一个根因引出的两类缺陷
审计发现了两个问题,它们共享同一个根因——节点没有声明式的 I/O 契约:上游节点吐什么、下游节点读什么,只是"恰好对齐"。
- Shape drift(形状漂移)。每个能力节点都把宿主能力的原始返回值原样包进
Item.json(计划文档引用的审计位置:agent.rs:115、tool_call.rs:31、http_request.rs:24)。agent节点还会随子端口(sub-port)不同而吐出三种不同形状,且其普通输出在运行时会在"解析后的模型 JSON"与{text}之间来回翻转(caps.rs:359,369)。下游的=item.<field>表达式只能靠猜。 - No agent identity(没有 Agent 身份)。
agent节点只是一次裸的provider.chat调用,附带一个自由格式的 inlinetools列表(caps.rs中OpenHumanLlm::complete,注释明确写着"no agent loop is driven here")。没有办法说"把这一步作为codingagent 跑"或"作为researcher跑"——尽管 OpenHuman 已经内置了一个大规模 agent 注册表:每个 agent(researcher、code_executor、crypto_agent……)都在自己的agent.toml中声明了专属工具集、模型提示、沙箱模式与迭代策略。仓库中当前存在 29 个内置 agent 目录(计划文档称"35-agent registry",口径差异可能来自 harness 定义的计数方式),例如 code_executor/agent.toml 与 researcher/agent.toml。
Part A 修复契约,Part B 在既有注册表之上构建可选 Agent Kind。两者的排序经过刻意设计:A1(agent 信封)先落地,B(agent kinds 的输出)随后写入同一个信封。
二、Part A:节点 I/O 对齐
A0. 设计原则:归一化的 item 信封
为能力节点采纳一个小型、稳定的输出信封,使每个下游表达式都能拥有可保证的访问器,与 provider 或配置无关:
// agent / tool_call / http_request / code 发出的 item 形状: { "json": <结构化 payload | null>, // 存在时的解析/结构化结果 "text": <string | null>, // 存在时的人类可读文本 "raw": <provider 原生值>, // 逃生舱:未动过的能力原始返回 "error": <null | { message, ... }> // 仅出现在 continue/route 错误项上 }规则明确:=item.text永远可解析(或显式为null);=item.json.<field>是结构化路径;=item.raw为需要 provider 原始 blob 的人保留现有行为。这是纯增量的——raw正是节点今天输出的内容——因此迁移是机械的。
计划还强调保持 cratehost-agnostic(宿主无关):信封定义在vendor/tinyflows(src/data.rs/ 节点执行器)中;宿主适配器(caps.rs)本就产出{text}/ 解析后 JSON,可直接映射到json/text。
A1. 归一化agent节点输出——杠杆最高,最先做
- Crate 侧(
vendor/tinyflows/src/nodes/integration/agent.rs:115):用信封包裹 completion,而不是Item::new(value)。若output_parser跑过,强制转换后的值放进json;completion 文本(如存在)放进text;未动过的响应放进raw;模型自行选中的工具结果保留在json.tool_result并镜像到稳定的tool_result访问器(见 A2)。 - 宿主侧(
src/openhuman/flows/tinyflows/caps.rs中OpenHumanLlm::complete):返回{ json: <解析后或 null>, text: <response.text>, raw: <完整响应> },而不是"要么裸解析对象、要么{text}兜底"。这消除了运行时形状翻转(审计 M1)。 - 测试:更新
agent.rs单元测试 +caps.rsseam 测试;新增 e2e,断言=item.text在 JSON 输出模型与纯文本输出模型上都能解析(mock 两者)。
对照当前仓库实现,这条线已经落地:宿主侧 agent.rs 的build_agent_result函数(L330–L349)会把 harness turn 的最终文本整形为节点输出值——当节点请求结构化输出且文本可解析为 JSON 时返回解析后的对象,否则返回{ text, agent_ref };其文档注释明确写道:"vendoragent节点随后将其折叠进稳定的{ json, text, raw }信封,output_parser子端口仍然生效"。这正是 A1 所定义的折叠行为。
A2. 统一 inline 工具与tool_call节点的结果形状(审计 M2)
让工具结果无论是在agent节点内联执行、还是作为独立tool_call节点执行,都可在同一路径到达。标准统一为信封:tool_call节点发出{ json: <工具输出>, raw: <composio 信封> };agent 的内联工具结果落在item.json.tool_result,节点信封的raw保留完整 completion。并文档化唯一的规范访问器。
A3. 集成节点的 per-item 执行(审计 M3——静默丢数据陷阱)
现状是agent/tool_call/http_request永远只发一个 item,且配置只针对input.first()解析,因此split_out (N) → tool_call只触发一次,丢掉 N−1 个 item。计划方案:
- 新增节点配置旗标
execution: "once" | "per_item"(tool_call/http_request默认per_item;agent默认once,因为 agent turn 通常是批次级别的——但允许per_item)。 per_item模式下:对ctx.input映射执行器,逐 item 重新解析配置(使=item.x指向当前item),每个输入项发出一个输出项,并携带paired_item血缘(vendor/tinyflows/src/data.rs)。- 涉及
agent.rs、tool_call.rs、http_request.rs以及nodes/mod.rs的 per-item 解析路径。 - 测试:
split_out → tool_call跑 N 次;paired_item血缘保留;once模式行为不变。
当前仓库源码印证了 per-item 模式的实际代价与防护:agent.rs 的模块头注释(L54–L61)指出,per-item 执行下引擎对每个输入项调用一次run_agent,全量 harness turn(含记忆注入)会按 item 扇出——引擎获得有界 per-item 并发后,这些调用还会同时到达,因此宿主侧设置了HARNESS_AGENT_SLOTS信号量作为进程级天花板。
A4. 端口感知的collect_input(审计 M4——未走分支泄漏 + BUG-3/BUG-4)
collect_input(vendor/tinyflows/src/engine.rs:171)会拼接所有前驱槽的 item,完全无视Edge.to_port(存了但从不读)以及前驱实际从哪个端口发出。结果是condition之后的节点会读到未走分支的 item。方案:
- 读取
Edge.to_port/ 前驱记录的port,只收集前驱实际从连接端口发出的 item。 - 支持命名 merge 输入(input A 对 B),并消除泄漏。
- 顺带收编审计中的 merge-barrier 缺口(BUG-4)与混合端口扇出丢失(BUG-3)——同属路由/lowering 面(
engine.rs:422、789-826)。 - 测试:接在 true 端口上的节点看不到 condition-false 槽位;由分支前驱喂入的 merge 正确 barrier;
main→a, main→b, error→h的形状能同时跑a和b。
A5.merge模式(审计 M5)
新增merge.mode:append(今天的 concat)、combine_by_key(按键字段 join item)、combine_by_position(zip)。仅在vendor/tinyflows/src/nodes/control_flow/merge.rs做配置层面的变更,barrier 语义不变。
A6. 创作期对齐 lint(供 Part C 工具化)
一个校验路径:给定生产者节点已知/声明的输出信封,把下游引用了生产者不可能发出的=item.<field>标记出来——以结构化、带节点地址的诊断信息呈现。它依赖于收紧validate.rs(审计 BUG-10),并接入面向 agent 的validate/revise_workflow工具,让 builder agent 直接拿到反馈。
A-bugs:随上述一起打包(审计 §3)
| 编号 | 问题 | 修复位置 |
|---|---|---|
| BUG-1(安全,立即热修) | jqenv内建函数泄漏宿主环境变量 | 关闭jaq-std默认特性 / 在vendor/tinyflows/src/expr.rs:302过滤env/input* |
| BUG-2 | switch/transform拿不到nodes作用域 | 使用expr_scope(switch.rs:26、transform.rs:27) |
| BUG-5/6 | 子工作流 HITL 被丢弃;失败时on_run_finish从不触发 | 对照FlowRunObserver验证 |
| BUG-9 | code/output_parser/sub_workflow跳过=解析 | 统一表达式绑定 |
三、Part B:可选 Agent Kind
B0. 核心想法
让agent节点通过引用 OpenHuman 注册表 agent 来声明运行哪个 agent:
{ "kind": "agent", "config": { "agent_ref": "code_executor", // 或 "researcher"、"crypto_agent"…… "prompt": "=item.text", "connection_ref": "…", // 仍由宿主解析,绝不来自模型输出 // 可选的逐节点覆盖: "model": "…", "max_iterations": 6, "tools_allow": ["grep", "edit"], }, }当agent_ref存在时,宿主运行那个注册 agent——携带它自己的精选工具集、模型提示、沙箱模式与迭代策略——作为一个完整的多轮 agent loop,而不是当前的单次provider.chat调用。编码步骤拿到编码工具;研究步骤拿到web_search/web_fetch;加密步骤拿到行情工具。这正是注册表现有的契约(src/openhuman/agent/registry/agents/*/agent.toml)。
B1. Crate 缝——新增AgentRunner能力(宿主无关)
crate 不能知道 OpenHuman 注册表的存在;且"把一个具名 agent 跑到完成(多轮、用工具)"是不同于LlmProvider.complete(单发)的另一种能力。计划在vendor/tinyflows/src/caps/mod.rs新增 trait:
#[async_trait] pub trait AgentRunner: Send + Sync { /// Run a host-registered agent identified by `agent_ref` to completion. /// `request` carries prompt/input/overrides; `conn` is the opaque credential. async fn run_agent(&self, agent_ref: &str, request: Value, conn: Option<&str>) -> Result<Value>; }- 向
Capabilities添加agent: Option<Arc<dyn AgentRunner>>(可选,使没有注册表的宿主继续工作)。 agent.rs分发:config.agent_ref存在且caps.agent已接线 →run_agent(...);否则回落到今天的LlmProvider.complete路径。两条路径都发 A1 信封,因此下游表达式完全一致。- 在
caps/mock.rs提供 mock 实现(echoagent_ref+ request),使 crate 测试覆盖两条路径。 agent_ref只来自受信配置,绝不取自模型输出(与tool_call.connection_ref同一条规则,agent.rs:71-79)。
这一设计判断被仓库源码验证为正确方向:agent.rs 的OpenHumanAgentRunner正是该 trait 的宿主实现(use tinyflows::caps::*;导入AgentRunner,L351–L352 处impl AgentRunner for OpenHumanAgentRunner),其文档注释开宗明义:"agent_ref从受信节点配置解析(绝不来自模型输出),因此提示注入的 completion 无法挑选任意 agent kind"(L50–L52)。
B2. 宿主适配器——基于注册表 + delegate 运行时实现AgentRunner
计划要求(对应src/openhuman/flows/tinyflows/caps.rs):
agent_registry::ops::get_agent(agent_ref)解析条目(tools、模型提示、sandbox、max_iterations、iteration_policy)。仓库中该操作现位于 registry/ops.rs 的pub async fn get_agent(id: &str) -> Result<Option<AgentRegistryEntry>, String>(L28),同文件还有list_agents(include_disabled)(L20)。- 应用可选逐节点覆盖(
model、max_iterations,以及只做收窄的tools_allow——节点可以子集化agent 的工具,永远不能添加)。 - 用 flow run 的
TurnOrigin驱动既有的 delegate 运行时(src/openhuman/agent/tools/delegate.rs;src/openhuman/agent/turn_origin.rs已有 flow origin 变体)。 - 返回
{ json, text, raw }(A1 信封):最终结构化输出进json,最终消息文本进text。
自治/安全:agent kind 的工具仍受SecurityPolicy/ 自治层级门控;sandboxedagent(如code_executor)沙箱运行;flow 自身的审批门仍拦截外发动作。不引入新特权路径。深度/成本护栏:flow 内跑完整 agent loop 的 agent 节点会扇出成本——需要限界(复用MAX_SUB_WORKFLOW_DEPTH风格的计数器或 per-run agent 调用上限),并把取消 token 穿进 delegate 运行(今天的子工作流路径丢弃 token——BUG-5——在此一并修复)。
当前仓库实现比计划更进一步,agent.rs 揭示了完整的路由与防护细节:
- 三级路由(
route_for_agent_ref,L129–L139):agent_ref若命中 harness 的AgentDefinitionRegistry→Harness 路径(Agent::from_config_for_agent+run_single跑完整工具循环,定义中的ToolScope/sandbox_mode/max_iterations直接生效);否则查自定义注册表(find_custom_in_config),已知且启用的 custom entry 由工厂合成一个真实的AgentDefinition再走 harness(保留其tool_allowlist/model);两者皆不中才落到RegistryFallback——仅人设整形的单次 completion(把条目的system_prompt前置为 system 消息、采纳其model,然后走OpenHumanLlm::complete)。未知或 disabled 的agent_ref会返回明确错误而非静默跳过。 - 模型优先级(
resolve_node_model,L244–L257):节点config.model(管理 tier 如reasoning-v1或hint:*别名)> 注册表entry.model> 无覆盖(harness 定义/角色默认)。harness_model_default_override(L273–L278)把 tier 归一化为hint:<role>以便会话构建器路由;raw/BYOK 模型 id(如claude-opus-4)则原样转发,避免被折叠到chat-v1tier。 - 超时治理(L144–L199):
clamp_run_timeout_secs把节点请求的timeout_secs钳制到10..=600,缺省240秒;scale_timeout_for_iteration_cap对迭代上限超过全局旧默认 10 的 agent(如extended策略下可达 50 次迭代的code_executor/tools_agent)按cap × 12s(上限 600s)抬升默认超时;resolve_run_timeout_secs保证显式设置的timeout_secs是作者有意选定的 fast-fail/SLA 边界,绝不被抬升。 - 并发天花板(L73–L114):进程级信号量
HARNESS_AGENT_SLOTS,默认 8 个并发 harness agent turn,可用环境变量OPENHUMAN_FLOWS_MAX_PARALLEL_AGENTS覆盖(非法或 0 值回落到默认,避免零 permit 死锁)。引擎 per-nodeconcurrency只约束单节点扇出;这个宿主级天花板是"承重"的那一个——一个图可以对 200 项数组开concurrency: "all",多个节点/多次 run 还能同时扇出。等待 permit不算错误:过宽的扇出被节流到该宽度,工作流仍会完成,只是更慢。 - 嵌套 HITL 升级(L582–L620):引擎 future 运行在 flow 的
Workfloworigin 下,但 flow 作者只预声明了agent_ref,没有预声明 harness LLM 会从定义ToolScope中挑哪些具体工具。若让内层 turn 继承Workflow { require_approval: false },审批门会视其为信任根并对external_effect工具自动放行(如 Slack/邮件/桌面控制)。因此run_single周围强制require_approval: true,使外效工具像 flow acting 节点一样停靠等待真实的人工决策;只读工具不受影响,不产生审批噪声。取消则通过 run_registry token 中止引擎 future,内层 turn 随之丢弃。 - 同步边界(L481–L492):flow
agent节点无绑定聊天线程,若其中运行的 agent 是 delegating agent 并直接调用spawn_async_subagent,该工具会因无parent_thread_id而拒绝,而非静默丢弃后台结果——需要并行时应建模为并行的 flow 节点(引擎已支持扇出)。
B3. 创作面——为 builder agent 落地选择
- Builder 工具
list_agent_profiles(src/openhuman/flows/builder_tools.rs),由agent_registry::ops::list_agents(false)支撑,返回{ id, display_name, when_to_use, tools, sandbox_mode }。镜像既有search_tool_catalog模式,使workflow_builder子 agent 挑选真实存在的agent_ref,而不是幻觉一个出来。仓库中该工具已就位:workflow_builder 的 tool_wording_tests.rs 甚至专门守护工具描述文案,断言其必须把agent_ref描述为"跑一个 specialist 的完整工具循环",且不得携带过时的措辞。 - 校验(
vendor/tinyflows/src/validate.rs+ 宿主):带无法解析agent_ref的agent节点应产生结构化校验错误(需要宿主把已知 agent 集合传入校验,或在flows::ops::validate中宿主侧校验)。 - workflow_builder 提示词(
src/openhuman/agent/registry/agents/workflow_builder/prompt.md):文档化agent_ref及何时应优先选专用 agent 而非裸 completion。
B4. UI——Agent Kind 选择器
(即将到来的)节点配置面板(集成计划中的U2)为agent节点提供Agent kind下拉框,数据来自agent_registry的list_agentsRPC(已存在),展示display_name+when_to_use,并提供可选的工具子集多选;只读画布则把所选 agent kind 显示为节点徽标。当前仓库中该 UI 已落地为 AgentNodeInspector.tsx:组件头注释写明agent_ref是"哪个已注册agent 运行此节点",且"agent 节点中命名了 harnessAgentDefinition的agent_ref走 Phase A 路由";选择器提供 "inherit" 默认项 + 全部注册 agent(L54–L62)。
四、Part C:阶段排序与归属
计划将工作切分为 8 个阶段,每个阶段随附测试(crate 单测 + 宿主 seam + JSON-RPC E2E),遵循仓库"先测后层"(tests before the next layer)规则,并把设计决策记录到vendor/tinyflows/local/docs/11-decisions.md:
| 阶段 | 工作 | 依赖 | 面(Surface) |
|---|---|---|---|
| C0 | BUG-1 env 泄漏热修;BUG-2 switch/transform 作用域 | — | vendor/tinyflows/src/expr.rs、control_flow/* |
| C1 | A0 信封 +A1 agent 输出归一化 | C0 | agent.rs、caps.rs |
| C2 | A2 工具形状统一;A3 per-item 执行 | C1 | tool_call.rs、http_request.rs、nodes/mod.rs |
| C3 | A4 端口感知collect_input(+ BUG-3/4);A5 merge 模式 | —(与 C1/C2 并行) | engine.rs、merge.rs |
| C4 | B1AgentRunner能力+ mock | C1(共享信封) | caps/mod.rs、caps/mock.rs、agent.rs |
| C5 | B2 宿主OpenHumanAgentRunner(基于 registry+delegate,+ BUG-5 取消) | C4 | tinyflows/caps.rs、agent/tools/delegate.rs |
| C6 | B3list_agent_profiles+agent_ref校验;A6 对齐 lint | C5、BUG-10 | flows/builder_tools.rs、validate.rs |
| C7 | B4 UI agent-kind 选择器 | C5、U2 节点面板 | app/src/components/flows/* |
这个排序的关键约束是 C1 先于 C4/C5:agent kinds 的输出必须写入 Part A 定义的同一信封,下游表达式才不会因"哪种 agent"而出现分支。对照当前仓库可确认 C4–C7 已落地(AgentRunner实现、list_agent_profiles、UI 选择器均存在)。
五、开放设计问题
计划文档保留了四个待决问题,阅读当前实现时仍值得注意:
- crate 中的
agent_ref:专用AgentRunner能力(B1,推荐——保持LlmProvider.complete为干净单发契约)vs. 让complete见到agent_ref时就循环。推荐:前者(仓库实现采纳了此方案)。 - 逐节点 model/tool 覆盖:只允许收窄(
tools_allow是 agent 工具集的子集)——永不放宽,以保留注册表的安全包络。 - flow 内(及子工作流内)agent-kind 节点的 cost/loop 边界:复用深度计数器,还是引入 per-run agent 调用预算?(当前实现以
HARNESS_AGENT_SLOTS并发信号量 + 超时钳制给出了第一版答案。) - 自定义 agent:
upsert_custom_agent已存在——用户自定义 agent kind 可免费地通过agent_ref寻址。确认它们是否应在 flows 中可选(当前实现已允许:已知且启用的 custom entry 会走完整 harness 路径)。
六、延伸阅读路径
围绕本计划,可继续深入仓库的以下位置(均为仓库根相对路径):
- 计划本体与集成背景:tinyflows-node-io-and-agent-kinds.md、tinyflows-integration/README.md
- 宿主能力实现:src/openhuman/flows/tinyflows/caps/agent.rs(
OpenHumanAgentRunner的三级路由、超时与并发治理)、src/openhuman/flows/tinyflows/caps/ 目录(llm.rs、http.rs、code.rs等其余能力适配器) - Agent 注册表:src/openhuman/agent/registry/ops.rs(
list_agents/get_agent)、src/openhuman/agent/registry/agents/(各agent.toml声明的工具集、模型提示与沙箱策略) - 运行时缝:src/openhuman/agent/tools/delegate.rs、src/openhuman/agent/turn_origin.rs
- 创作面与 UI:src/openhuman/flows/builder_tools.rs、app/src/components/flows/canvas/AgentNodeInspector.tsx
需要提醒的适用前提:计划文档引用的vendor/tinyflows/src/...路径属于 vendored 引擎(仓库vendor/下的子模块),在部分 checkout 中未检出内容,文中相关行号引用以计划文档原文为准;而src/openhuman/与app/src/下的路径则可直接在当前仓库中打开核对。
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考