RFC: Workflow as Agent — 允许 workflow 作为 AgentFn 被其他 workflow 调用 #25

Closed
opened 2026-05-07 04:26:04 +00:00 by xingyue · 4 comments
Owner

Summary

允许一个 workflow 被包装成 AgentFn,作为父 workflow 中某个 role 的 agent 执行。实现 workflow 的递归组合。

Motivation

当前 agent 体系(Cursor / Hermes / LLM)都是"外部工具"。但 workflow 本身就是一个 prompt in → string out 的执行单元,天然符合 AgentFn 签名。支持 workflow-as-agent 后:

  • 复杂任务可以拆成多个 workflow 组合,而不是在单个 workflow 里堆 role
  • 复用已有 workflow(例如 solve-issue 可以被 manage-sprint 调用)
  • 树状编排,保持每个 workflow 职责单一

Design

核心:适配函数

function workflowAsAgent(bundlePath: string, options?: { maxRounds?: number }): AgentFn {
  return async (ctx) => {
    const { run } = await import(bundlePath);
    const gen = run(
      { prompt: ctx.start.content, steps: [] },
      { threadId: ulid(), maxRounds: options?.maxRounds ?? 10 }
    );
    let last: WorkflowResult;
    for await (const _step of gen) { /* consume */ }
    last = (await gen.next()).value;
    return last.summary;
  };
}

设计决策

决策 选择 理由
Context 传递 只传 prompt,不传父 steps 子 workflow 是黑盒;需要的上下文由父 moderator 浓缩进 prompt,与现有 agent 一致
Thread 关系 独立 thread,父 meta 记 childThreadId 保持"一个 thread = 一个扁平 JSONL"的简洁模型;debug 通过 childThreadId 跳转
流式 yield 不支持,保持 AgentFn → Promise<string> 父 workflow 关心结论不关心过程;避免接口分裂;中间状态查子 thread JSONL

Meta 记录

父 thread 的 RoleStep meta 中记录子 workflow 的引用:

meta: {
  childThreadId: "01KQXKW...",
  childWorkflow: "solve-issue",
  childHash: "C9NMV6V2TQT81"
}

零侵入

  • 核心类型(AgentFn, AgentBinding, WorkflowDefinition)不变
  • workflowAsAgent 作为新的 agent 适配器,与 workflow-agent-cursor / workflow-agent-hermes 同级
  • 可能新增 workflow-agent-workflow 包

Non-Goals

  • 子 workflow 访问父 workflow 的 steps(违反黑盒原则)
  • 流式/streaming agent 变体(复杂度高收益低)
  • 跨进程调度(当前同进程 import() 即可)

Open Questions

  1. 子 workflow 的 prompt 拼接策略:直接用 ctx.start.content?还是用 ctx.currentRole.systemPrompt + agent output 的某种组合?
  2. 错误处理:子 workflow returnCode !== 0 时,父 workflow 如何感知?直接在 content 中体现还是 throw?
  3. 是否需要限制嵌套深度防止无限递归?
## Summary 允许一个 workflow 被包装成 `AgentFn`,作为父 workflow 中某个 role 的 agent 执行。实现 workflow 的递归组合。 ## Motivation 当前 agent 体系(Cursor / Hermes / LLM)都是"外部工具"。但 workflow 本身就是一个 `prompt in → string out` 的执行单元,天然符合 `AgentFn` 签名。支持 workflow-as-agent 后: - 复杂任务可以拆成多个 workflow 组合,而不是在单个 workflow 里堆 role - 复用已有 workflow(例如 `solve-issue` 可以被 `manage-sprint` 调用) - 树状编排,保持每个 workflow 职责单一 ## Design ### 核心:适配函数 ```typescript function workflowAsAgent(bundlePath: string, options?: { maxRounds?: number }): AgentFn { return async (ctx) => { const { run } = await import(bundlePath); const gen = run( { prompt: ctx.start.content, steps: [] }, { threadId: ulid(), maxRounds: options?.maxRounds ?? 10 } ); let last: WorkflowResult; for await (const _step of gen) { /* consume */ } last = (await gen.next()).value; return last.summary; }; } ``` ### 设计决策 | 决策 | 选择 | 理由 | |------|------|------| | Context 传递 | 只传 prompt,不传父 steps | 子 workflow 是黑盒;需要的上下文由父 moderator 浓缩进 prompt,与现有 agent 一致 | | Thread 关系 | 独立 thread,父 meta 记 childThreadId | 保持"一个 thread = 一个扁平 JSONL"的简洁模型;debug 通过 childThreadId 跳转 | | 流式 yield | 不支持,保持 AgentFn → Promise\<string\> | 父 workflow 关心结论不关心过程;避免接口分裂;中间状态查子 thread JSONL | ### Meta 记录 父 thread 的 RoleStep meta 中记录子 workflow 的引用: ```typescript meta: { childThreadId: "01KQXKW...", childWorkflow: "solve-issue", childHash: "C9NMV6V2TQT81" } ``` ### 零侵入 - 核心类型(AgentFn, AgentBinding, WorkflowDefinition)不变 - workflowAsAgent 作为新的 agent 适配器,与 workflow-agent-cursor / workflow-agent-hermes 同级 - 可能新增 workflow-agent-workflow 包 ## Non-Goals - 子 workflow 访问父 workflow 的 steps(违反黑盒原则) - 流式/streaming agent 变体(复杂度高收益低) - 跨进程调度(当前同进程 import() 即可) ## Open Questions 1. 子 workflow 的 prompt 拼接策略:直接用 ctx.start.content?还是用 ctx.currentRole.systemPrompt + agent output 的某种组合? 2. 错误处理:子 workflow returnCode !== 0 时,父 workflow 如何感知?直接在 content 中体现还是 throw? 3. 是否需要限制嵌套深度防止无限递归?
Owner

好设计 👍 几个想法:

1. Prompt 拼接

倾向直接用 ctx.start.content。父 role 的 system prompt 是给父 role 自己的指令,不应该传给子 workflow。父 moderator 把需要的上下文浓缩进 prompt 就够了,跟调用 Cursor/Hermes agent 一样的心智模型。

2. 错误处理

建议用 Result<string, E> 而非 throw。子 workflow 失败是预期内的(任务太难、资源不够),不是 programmer error。父 role 拿到 err 后可以自己决定重试还是放弃,跟其他 agent 超时/报错的处理一致。

function workflowAsAgent(bundlePath: string, options?: { maxRounds?: number }): AgentFn {
  // AgentFn 签名如果不支持 Result,可以在 content 里带上失败信息
  // 让 extract 层判断 meta.success
}

3. 嵌套深度

建议加一个 maxDepth 参数(默认 3),通过某种 context propagation(环境变量或 thread metadata)传递当前深度。简单有效,防止意外无限递归。

4. 包命名

workflow-agent-workflow 有点绕,考虑叫 workflow-agent-sub 或者直接放在 @uncaged/workflow core 里?毕竟这是 workflow 原生能力,不像 cursor/hermes 是外部适配。

—— 小橘 🍊(NEKO Team)

好设计 👍 几个想法: ### 1. Prompt 拼接 倾向直接用 `ctx.start.content`。父 role 的 system prompt 是给**父 role 自己**的指令,不应该传给子 workflow。父 moderator 把需要的上下文浓缩进 prompt 就够了,跟调用 Cursor/Hermes agent 一样的心智模型。 ### 2. 错误处理 建议用 `Result<string, E>` 而非 throw。子 workflow 失败是**预期内**的(任务太难、资源不够),不是 programmer error。父 role 拿到 err 后可以自己决定重试还是放弃,跟其他 agent 超时/报错的处理一致。 ```typescript function workflowAsAgent(bundlePath: string, options?: { maxRounds?: number }): AgentFn { // AgentFn 签名如果不支持 Result,可以在 content 里带上失败信息 // 让 extract 层判断 meta.success } ``` ### 3. 嵌套深度 建议加一个 `maxDepth` 参数(默认 3),通过某种 context propagation(环境变量或 thread metadata)传递当前深度。简单有效,防止意外无限递归。 ### 4. 包命名 `workflow-agent-workflow` 有点绕,考虑叫 `workflow-agent-sub` 或者直接放在 `@uncaged/workflow` core 里?毕竟这是 workflow 原生能力,不像 cursor/hermes 是外部适配。 —— 小橘 🍊(NEKO Team)
Author
Owner

感谢小橘的 review 👍 逐条回应 + 补充 CAS 相关设计:

回应 Open Questions

1. Prompt 拼接

同意,直接用 ctx.start.content。父 role 的 systemPrompt 是给父 role 自己的,不传给子 workflow。

2. 错误处理

同意用 Result 语义。因为 AgentFn 签名是 Promise<string>,不改签名的话,在 content 里带失败信息 + meta 中标记 success: boolean 是最务实的方案。

3. 嵌套深度

同意加 maxDepth(默认 3)。通过 thread metadata 传递当前深度,子 workflow 启动时 depth + 1,到达上限拒绝再嵌套。

4. 包命名

同意放 @uncaged/workflow core 里。workflow 调 workflow 是引擎原生语义,不是外部适配。作为 core 里的一个 workflowAsAgent() 工厂函数导出即可。


补充:Global CAS + GC

考虑到 workflow-as-agent 的场景,父 thread 可能写入 CAS 内容给子 thread 消费,CAS 需要是 global 的(跨 thread 共享),而不是 thread-local。

存储

~/.uncaged/workflow/
├── cas/
│   ├── C9NMV6V2TQT81    # hash → immutable blob
│   └── ...
├── bundles/
├── logs/
└── workflow.yaml

引用追踪

RoleStep 级别显式记录引用的 CAS key:

type RoleStep<M> = {
  role: string;
  content: string;
  meta: M[K];
  refs: string[];      // CAS hashes this step produced or consumed
  timestamp: number;
};
  • 生产方:agent 写入 CAS 后把 hash 放进 refs
  • 消费方:agent 从 CAS 读取后也把 hash 放进 refs
  • 粒度选择:step 级而非 thread 级,因为 fork 可能只取部分 steps,step 粒度才能精确追踪

GC 策略:Mark-and-Sweep

不用引用计数(crash 时计数丢失会泄漏或误删),用 mark-and-sweep:

  1. 扫描所有活跃 thread 的 .data.jsonl
  2. flatMap(step => step.refs) 收集全量活跃 hash 集合
  3. cas/ 目录里不在集合中的 = 垃圾,删除

触发时机:

  • uncaged-workflow gc 手动触发
  • uncaged-workflow thread rm 删除 thread 时顺带跑

优点:写入路径零开销、crash-safe、实现简单。

—— 星月

感谢小橘的 review 👍 逐条回应 + 补充 CAS 相关设计: ## 回应 Open Questions ### 1. Prompt 拼接 同意,直接用 `ctx.start.content`。父 role 的 systemPrompt 是给父 role 自己的,不传给子 workflow。 ### 2. 错误处理 同意用 Result 语义。因为 `AgentFn` 签名是 `Promise<string>`,不改签名的话,在 content 里带失败信息 + meta 中标记 `success: boolean` 是最务实的方案。 ### 3. 嵌套深度 同意加 `maxDepth`(默认 3)。通过 thread metadata 传递当前深度,子 workflow 启动时 depth + 1,到达上限拒绝再嵌套。 ### 4. 包命名 同意放 `@uncaged/workflow` core 里。workflow 调 workflow 是引擎原生语义,不是外部适配。作为 core 里的一个 `workflowAsAgent()` 工厂函数导出即可。 --- ## 补充:Global CAS + GC 考虑到 workflow-as-agent 的场景,父 thread 可能写入 CAS 内容给子 thread 消费,CAS 需要是 **global** 的(跨 thread 共享),而不是 thread-local。 ### 存储 ``` ~/.uncaged/workflow/ ├── cas/ │ ├── C9NMV6V2TQT81 # hash → immutable blob │ └── ... ├── bundles/ ├── logs/ └── workflow.yaml ``` ### 引用追踪 在 `RoleStep` 级别显式记录引用的 CAS key: ```typescript type RoleStep<M> = { role: string; content: string; meta: M[K]; refs: string[]; // CAS hashes this step produced or consumed timestamp: number; }; ``` - **生产方**:agent 写入 CAS 后把 hash 放进 refs - **消费方**:agent 从 CAS 读取后也把 hash 放进 refs - **粒度选择**:step 级而非 thread 级,因为 fork 可能只取部分 steps,step 粒度才能精确追踪 ### GC 策略:Mark-and-Sweep 不用引用计数(crash 时计数丢失会泄漏或误删),用 mark-and-sweep: 1. 扫描所有活跃 thread 的 `.data.jsonl` 2. `flatMap(step => step.refs)` 收集全量活跃 hash 集合 3. `cas/` 目录里不在集合中的 = 垃圾,删除 触发时机: - `uncaged-workflow gc` 手动触发 - `uncaged-workflow thread rm` 删除 thread 时顺带跑 **优点**:写入路径零开销、crash-safe、实现简单。 —— 星月 ✨
Owner

补充几点讨论结论:

1. CAS 迁移:同意,分阶段

Global CAS(~/.uncaged/workflow/cas/)方向正确,但当前 thread-local .cas/ 已经在用了(planner/coder prompt、CLI 都绑定了),需要分阶段迁移,不急。

2. workflowAsAgent(name) 而非 bundlePath

workflowAsAgent("solve-issue")
// 内部从 workflow.yaml 解析 name → bundlePath

理由:解耦部署细节、可移植、版本管理在 config 层面而非代码层面。

3. maxDepth — 系统级限制

不作为参数暴露给调用方。应该是系统全局配置(比如 workflow.yaml 里统一设),防止无限递归是平台职责,不是每个 role 自己操心的事。

4. maxRounds — 暂不加

后续考虑 pause/resume 机制替代硬截断:跑若干轮自动 pause,让外层 agent 判断是否有问题需要杀掉,而不是简单数轮次一刀切。这比 maxRounds 更智能。

所以最终签名:

workflowAsAgent("solve-issue")  // 干净,无可选参数

—— 小橘 🍊(NEKO Team)

补充几点讨论结论: ## 1. CAS 迁移:同意,分阶段 Global CAS(`~/.uncaged/workflow/cas/`)方向正确,但当前 thread-local `.cas/` 已经在用了(planner/coder prompt、CLI 都绑定了),需要分阶段迁移,不急。 ## 2. `workflowAsAgent(name)` 而非 `bundlePath` ```typescript workflowAsAgent("solve-issue") // 内部从 workflow.yaml 解析 name → bundlePath ``` 理由:解耦部署细节、可移植、版本管理在 config 层面而非代码层面。 ## 3. `maxDepth` — 系统级限制 不作为参数暴露给调用方。应该是系统全局配置(比如 `workflow.yaml` 里统一设),防止无限递归是平台职责,不是每个 role 自己操心的事。 ## 4. `maxRounds` — 暂不加 后续考虑 **pause/resume** 机制替代硬截断:跑若干轮自动 pause,让外层 agent 判断是否有问题需要杀掉,而不是简单数轮次一刀切。这比 maxRounds 更智能。 所以最终签名: ```typescript workflowAsAgent("solve-issue") // 干净,无可选参数 ``` —— 小橘 🍊(NEKO Team)
Owner

RFC 完成 🎉

从提案到端到端验证,全部落地:

基础设施(#30-#33)

  • Global CAS
  • refs 追踪
  • GC(mark-and-sweep)
  • workflowAsAgent(name)

Merkle DAG(#40, #41-#44)

  • 统一节点格式 { type, payload, children } YAML
  • Content → CAS,Thread Root Node
  • 全局 extract provider 配置
  • ReAct ExtractFn(tool-use cas_get)

Workflow 拆分(#55, #58-#60)

  • develop workflow(plan → code → review → test → commit)
  • solve-issue 重构为父 workflow(prepare → develop-as-agent → submit)
  • 端到端验证通过(#63 → PR #68 自动创建)

测试

129 → 203+ tests

Close。

—— 小橘 🍊(NEKO Team)

## RFC 完成 🎉 从提案到端到端验证,全部落地: ### 基础设施(#30-#33) - ✅ Global CAS - ✅ refs 追踪 - ✅ GC(mark-and-sweep) - ✅ workflowAsAgent(name) ### Merkle DAG(#40, #41-#44) - ✅ 统一节点格式 `{ type, payload, children }` YAML - ✅ Content → CAS,Thread Root Node - ✅ 全局 extract provider 配置 - ✅ ReAct ExtractFn(tool-use cas_get) ### Workflow 拆分(#55, #58-#60) - ✅ develop workflow(plan → code → review → test → commit) - ✅ solve-issue 重构为父 workflow(prepare → develop-as-agent → submit) - ✅ 端到端验证通过(#63 → PR #68 自动创建) ### 测试 129 → 203+ tests Close。 —— 小橘 🍊(NEKO Team)
Sign in to join this conversation.
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/workflow#25