11 KiB
wf — Stateless Workflow CLI
将 workflow 引擎降维为无状态单步 CLI。Workflow 是纯数据(CAS 节点),执行是单步原子操作,agent 是可插拔外部命令。
1. CLI Design
1.1 命令总览
# thread 组
uwf thread start <workflow> -p <prompt> # 创建 thread,不执行
uwf thread step <thread-id> [--agent] # 单步执行
uwf thread show <thread-id> [--full] # 查看状态(--full 展开完整历史)
uwf thread list [--all] # 列出活跃 threads(--all 含已归档)
uwf thread kill <thread-id> # 终结 thread,归档
# workflow 组
uwf workflow put <file.yaml> # 注册 workflow(YAML → CAS)
uwf workflow show <workflow-id> # 查看 workflow 定义
uwf workflow list # 列出已注册 workflows
两组对称,各 3-4 个子命令。CAS 操作交给 json-cas CLI,不在 uwf 中重复。
1.2 uwf thread start
uwf thread start <workflow> -p "Fix the login bug described in issue #42"
<workflow>— workflow 名或 CAS hash-p— 用户 prompt(必填)
输出(JSON to stdout):
{
"workflow": "0R3KFM8N2P1QW", // workflow CAS hash
"thread": "01J7K9M2XN..." // ULID
}
做的事:
- 解析 workflow(名字查 registry → CAS hash)
- 生成 thread ULID
- 解析系统 config,确定 agent binding
- 写 StartNode 到 CAS
- 在 threads 索引中记录链头 → StartNode hash
- 输出 JSON
1.3 uwf thread step
uwf thread step 01J7K9M2XN...
uwf thread step 01J7K9M2XN... --agent "bunx uwf-cursor"
输出(JSON to stdout):
// 正常 step
{
"workflow": "0R3KFM8N2P1QW",
"thread": "01J7K9M2XN...",
"progress": {
"role": "developer",
"meta": { "filesChanged": ["src/auth.ts"], "summary": "Fixed redirect" },
"detail": "0QWERTY12345A", // agent 原始输出的 CAS hash
"agent": "0AGENT_CFG_HASH" // 实际使用的 agent 配置 CAS hash
}
}
// thread 结束
{
"workflow": "0R3KFM8N2P1QW",
"thread": "01J7K9M2XN...",
"progress": null
}
做的事:
- 读链头 → 当前 StepNode(或 StartNode)
- 收集 thread 历史(遍历链)
- 调 moderator:评估 JSONata conditions → 得到下一个 role(或 END)
- 若 END → 归档 thread,输出
progress: null - 确定 agent command(
--agentoverride > thread binding > global default) - 构建 prompt(role.systemPrompt + thread context + user prompt)
- 调用:
<agent-cmd> -t <thread-id> -p <prompt>,捕获 stdout - 解析 agent 输出为 progress JSON
- 写 StepNode 到 CAS(prev → 旧链头)
- 更新链头指针
- 输出 JSON
1.4 uwf thread show
uwf thread show 01J7K9M2XN... # 当前状态(最新 StepNode)
uwf thread show 01J7K9M2XN... --full # 遍历链,打印完整 step 历史
纯读操作,不改状态。CAS 节点查看用 json-cas get <hash>。
1.5 Agent CLI 协议
每个 agent 是一个命令,只接受一个参数 — thread-id:
uwf-hermes <thread-id>
约定:
- agent-kit 从 CAS 读 thread 链 → 确定当前 role → 拿到 systemPrompt / extractPrompt / schema
- agent-kit 组装完整 prompt(role systemPrompt + thread context + user prompt from StartNode)
- agent 执行实际逻辑,agent-kit 负责 extract
- stdout → progress 中
meta+ raw output - 所有配置从环境变量读(LLM model、API key、extractor config)
- exit 0 = 成功,非 0 = 失败
stdout 输出格式:
{
"meta": { ... }, // 结构化输出,符合 role schema
"content": "..." // 原始输出文本(wf 负责存入 CAS)
}
agent 框架 package(@uncaged/workflow-agent-kit)帮 agent 作者完成:
- 读 thread CAS 构建上下文
- 调实际 agent 逻辑
- 调 extractor LLM 从 raw output 提取 meta
- 格式化 stdout JSON
2. CAS 结构定义
2.1 类型层级
沿用 json-cas 的三层:bootstrap meta-schema → JSON Schema nodes → data nodes。
下面所有 CAS 节点都遵循 { type: cas_ref, payload: T, timestamp: number } 的标准格式。
2.2 Schema 节点
以下每个 schema 本身是一个 CAS 节点(type 指向 meta-schema)。
RoleSchema
# 定义一个 role 的 meta 输出格式
# 例:developer role 的输出 schema
type: <meta-schema-hash>
payload:
$id: "role-output-developer"
type: object
properties:
filesChanged:
type: array
items: { type: string }
summary:
type: string
required: [filesChanged, summary]
2.3 数据节点
Role
type: <role-schema-hash>
payload:
name: "developer"
description: "Implements code changes"
systemPrompt: "You are a developer agent..."
extractPrompt: "Extract the following from the agent output..."
outputSchema:
$ref: "0ROLESCH_HASH1" # cas_ref → RoleSchema 节点
Moderator
type: <moderator-schema-hash>
payload:
graph:
- from: "$START"
transitions:
- to: "planner"
condition: null # 无条件(default/fallback)
- from: "planner"
transitions:
- to: "developer"
condition: "$not($exists(meta.needsClarification))" # JSONata
- to: "$END"
condition: null # fallback
- from: "developer"
transitions:
- to: "reviewer"
condition: null
- from: "reviewer"
transitions:
- to: "developer"
condition: "meta.approved = false" # JSONata
- to: "$END"
condition: null
JSONata 表达式的求值上下文:
{
"role": "reviewer", // 刚完成的 role
"meta": { "approved": false }, // 刚完成的 role 的 meta
"depth": 3, // 当前第几步
"history": [ // 所有历史 steps 的摘要
{ "role": "planner", "meta": { ... } },
{ "role": "developer", "meta": { ... } }
]
}
Workflow
type: <workflow-schema-hash>
payload:
name: "solve-issue"
description: "End-to-end issue resolution"
roles:
planner:
$ref: "0ROLE_PLANNER_H" # cas_ref → Role
developer:
$ref: "0ROLE_DEVELP_H" # cas_ref → Role
reviewer:
$ref: "0ROLE_REVIEW_H" # cas_ref → Role
moderator:
$ref: "0MODERATOR_HASH" # cas_ref → Moderator
defaultAgent: "uwf-hermes" # 默认 agent 命令前缀
agentOverrides: # per-role override
developer: "uwf-cursor"
AgentConfig
type: <agent-config-schema-hash>
payload:
command: "uwf-hermes"
# 不存 env(从运行环境继承)
# 不存 thread-id(运行时变量)
# → 相同 command 的 steps 自然共享同一个 CAS hash
为什么只存 command?因为 env 从运行环境继承,不进 CAS。这样同一个 agent 命令在不同 step 间去重为同一个 hash。如果需要记录实际运行时的 env snapshot 用于审计,可以单独存一个 AgentExecContext 节点,但不作为默认行为。
StartNode(Thread 起点)
type: <start-node-schema-hash>
payload:
workflow:
$ref: "0WORKFLOW_HASH" # cas_ref → Workflow
prompt: "Fix the login bug..."
agentBinding: # 启动时确定的 agent 分配
planner:
$ref: "0AGENTCFG_HRM" # cas_ref → AgentConfig
developer:
$ref: "0AGENTCFG_CUR" # cas_ref → AgentConfig
reviewer:
$ref: "0AGENTCFG_HRM" # cas_ref → AgentConfig
没有 thread-id 在 payload 里 — thread-id 是索引层面的事,不进 CAS 内容。
StepNode(Thread 每一步)
type: <step-node-schema-hash>
payload:
role: "developer"
meta:
filesChanged: ["src/auth.ts"]
summary: "Fixed redirect loop"
content:
$ref: "0RAW_OUTPUT_HASH" # cas_ref → 原始 agent 输出(content node)
agent:
$ref: "0AGENTCFG_CUR" # cas_ref → 实际使用的 AgentConfig
prev:
$ref: "0PREV_STEP_HASH" # cas_ref → 前一个 StepNode 或 StartNode
2.4 链式结构
threads.json: { "01J7K9M2XN...": "0STEP3_HASH" }
│
▼
StepNode (step 3)
├── role: "reviewer"
├── meta: { approved: true }
├── content → CAS(raw output)
├── agent → CAS(AgentConfig)
└── prev ──→ StepNode (step 2)
├── role: "developer"
├── ...
└── prev ──→ StepNode (step 1)
├── role: "planner"
├── ...
└── prev ──→ StartNode
├── workflow → CAS(Workflow)
├── prompt: "Fix..."
└── agentBinding: {...}
2.5 可变状态
整个系统唯一的可变文件:
// ~/.uncaged/workflow/threads.json
{
"01J7K9M2XN...": "0STEP3_HASH", // active thread → 链头
"01J8AB3QRM...": "0STEP1_HASH"
}
Thread 结束时从这个文件移除。可选:追加一行到 history.jsonl 做归档。
3. 包结构(精简后)
packages/
├── workflow-protocol/ # 类型定义(复用,大幅精简)
├── workflow-cas/ # CAS 存储(复用)
├── workflow-util/ # Base32, ULID, logger(复用)
├── workflow-moderator/ # JSONata moderator 引擎(新,从 #297 演化)
├── workflow-agent-kit/ # Agent CLI 框架(新,含 extractor)
└── cli-workflow/ # wf CLI(重写)
6 个包。砍掉 workflow-runtime, workflow-execute, workflow-register, workflow-reactor, workflow-dashboard, workflow-gateway, 4 个 agent adapter, 2 个 template, workflow(根包)。
可以删除的包(12 个):
workflow-runtime— createWorkflow / AsyncGenerator 不再需要workflow-execute— 引擎循环移到 CLI 的step命令workflow-register— ESM bundle 注册不再需要,workflow 是 CAS 节点workflow-reactor— LLM tool-call 循环移到 agent-kitworkflow-dashboard— 没有长驻进程,不需要 dashboardworkflow-gateway— 同上workflow-agent-cursor— 变成独立的uwf-cursorCLIworkflow-agent-hermes— 变成独立的uwf-hermesCLIworkflow-agent-llm— 变成独立的uwf-llmCLIworkflow-agent-react— 变成独立的uwf-reactCLIworkflow-template-develop— 变成 YAML 文件workflow-template-solve-issue— 变成 YAML 文件
Agent adapters 从 monorepo 内的包变成独立的 CLI 项目(可以在同一个 monorepo,也可以分出去)。Workflow templates 从 ESM bundle 变成 YAML 文件。