# `uwf` — Stateless Workflow CLI > 将 workflow 引擎降维为无状态单步 CLI。Workflow 是纯数据(CAS 节点),执行是单步原子操作,agent 是可插拔外部命令。 --- ## 1. CLI Design ### 1.1 命令总览 ``` # thread 组 uwf thread start -p # 创建 thread,不执行 uwf thread step [--agent] # 单步执行 uwf thread show # thread-id → head 查询 uwf thread list [--all] # 列出活跃 threads(--all 含已归档) uwf thread kill # 终结 thread,归档 # workflow 组 uwf workflow put # 注册 workflow(YAML → CAS) uwf workflow show # 查看 workflow 定义 uwf workflow list # 列出已注册 workflows ``` 两组对称,各 3-4 个子命令。CAS 操作交给 `json-cas` CLI,不在 `uwf` 中重复。 ### 1.2 `uwf thread start` ```bash uwf thread start -p "Fix the login bug described in issue #42" ``` - `` — workflow 名或 CAS hash - `-p` — 用户 prompt(必填) **输出(JSON to stdout):** ```jsonc { "workflow": "4KNM2PXR3B1QW", // workflow CAS hash (XXH64, 13-char Crockford Base32) "thread": "01J7K9M2XNPQR5VWBCDF8G3H4T" // ULID } ``` **做的事:** 1. 解析 workflow(名字查 registry → CAS hash) 2. 生成 thread ULID 3. 写 StartNode 到 CAS 4. 在 threads.yaml 中记录链头 → StartNode hash 5. 输出 JSON ### 1.3 `uwf thread step` ```bash uwf thread step 01J7K9M2XNPQR5VWBCDF8G3H4T uwf thread step 01J7K9M2XNPQR5VWBCDF8G3H4T --agent "bunx uwf-cursor" ``` **输出(JSON to stdout):** ```jsonc { "workflow": "4KNM2PXR3B1QW", "thread": "01J7K9M2XNPQR5VWBCDF8G3H4T", "head": "8FWKR3TN5V1QA", // 新链头 StepNode 的 CAS hash "done": false // true = moderator 返回 END,thread 已归档 } ``` `done: true` 时 head 仍然有值(最后一个 StepNode),但 thread 已从 threads.yaml 移除。 对已结束或不存在的 thread 调用 step 会报错(非 active thread)。 详细信息通过 `uwf thread show ` 或 `json-cas get ` 查看。 **做的事:** 1. 读链头 → 当前 StepNode(或 StartNode) 2. 收集 thread 历史(遍历链) 3. 调 moderator:评估 JSONata conditions → 得到下一个 role(或 END) 4. 若 END → 归档 thread,输出最后链头,退出 5. 确定 agent command(`--agent` override > config.yaml per-workflow/role > config.yaml defaultAgent) 6. 调用:` `,捕获 stdout 得到新 StepNode hash 7. 更新链头指针 8. 再次调 moderator(基于新 StepNode)判断 done 9. 输出 JSON ### 1.4 `uwf thread show` ```bash uwf thread show 01J7K9M2XNPQR5VWBCDF8G3H4T ``` **输出(JSON to stdout):** ```jsonc { "workflow": "4KNM2PXR3B1QW", "thread": "01J7K9M2XNPQR5VWBCDF8G3H4T", "head": "8FWKR3TN5V1QA", "done": false } ``` 纯 thread-id → head 查询。详细内容用 `json-cas get ` 或 `json-cas walk ` 查看。 ### 1.5 Agent CLI 协议 每个 agent 是一个命令,接受 thread-id 和 role 两个参数: ```bash uwf-hermes ``` **约定:** - `uwf step` 负责 moderator 决策,将 role 传给 agent CLI - agent-kit 根据 thread + role 从 CAS 读 systemPrompt / outputSchema - agent-kit 组装完整 prompt(role systemPrompt + thread context + user prompt from StartNode) - agent 执行实际逻辑,agent-kit 负责 extract - agent 将 StepNode 写入 CAS(含 output、detail、agent、prev),但**不挪链头指针** - stdout 输出新 StepNode 的 CAS hash(纯文本,一行) - 所有配置从环境变量读(LLM model、API key、extractor config) - exit 0 = 成功,非 0 = 失败 **stdout 输出:** ``` 8FWKR3TN5V1QA ``` `uwf step` 拿到这个 hash 后更新链头指针、判断 done。 --- ## 2. CAS 结构定义 ### 2.1 类型层级 沿用 json-cas 的三层:bootstrap meta-schema → JSON Schema nodes → data nodes。 下面所有 CAS 节点都遵循 `{ type: cas_ref, payload: T, timestamp: number }` 的标准格式。 `cas_ref` 类型的字符串字段在 json-cas 中已内置支持,不需要额外的 `$ref` 包装。 ### 2.2 数据节点 #### `Workflow` Roles 和 moderator 内联在 Workflow 中,只有 outputSchema 独立为 CAS 节点(方便 json-cas 校验)。 ```yaml type: payload: name: "solve-issue" description: "End-to-end issue resolution" roles: planner: description: "Creates implementation plan" systemPrompt: "You are a planning agent..." outputSchema: "5GWKR8TN1V3JA" # cas_ref → JSON Schema 节点(json-cas 内置) developer: description: "Implements code changes" systemPrompt: "You are a developer agent..." outputSchema: "8CNWT4KR6D1HV" # cas_ref → JSON Schema 节点 reviewer: description: "Reviews code changes" systemPrompt: "You are a code reviewer..." outputSchema: "1VPBG9SM5E7WK" # cas_ref → JSON Schema 节点 conditions: needsClarification: description: "Planner requests clarification from user" expression: "$exists(steps[-1].output.needsClarification)" notApproved: description: "Reviewer rejected the implementation" expression: "steps[-1].output.approved = false" graph: $START: - role: "planner" condition: null # 无条件(fallback) planner: - role: "developer" condition: "needsClarification" - role: "$END" condition: null developer: - role: "reviewer" condition: null reviewer: - role: "developer" condition: "notApproved" - role: "$END" condition: null ``` - `roles` — 内联定义,每个 role 的 `outputSchema` 是独立的 cas_ref(指向 json-cas 内置 JSON Schema 节点) - `conditions` — `Record`,命名条件,方便画图描述 - `graph` — `Record`,每个 Transition = `{ role, condition }` - `condition` 引用 conditions 中的 key,`null` = fallback - 按数组顺序求值,第一个匹配的 transition 胜出 - 不含 agent binding — agent 配置在 `~/.uncaged/workflow/config.yaml` 中管理 JSONata 表达式的求值上下文: ```jsonc { "start": { // StartNode 信息 "workflow": "4KNM2PXR3B1QW", "prompt": "Fix the login bug..." }, "steps": [ // 所有已完成 steps,从旧到新 { "role": "planner", "output": { "phases": [...] }, "detail": "7BQST3VW9F2MA", "agent": "uwf-hermes" }, { "role": "developer", "output": { "filesChanged": ["src/auth.ts"], "summary": "Fixed redirect" }, "detail": "9KRVW3TN5F1QA", "agent": "uwf-cursor" }, { "role": "reviewer", "output": { "approved": false }, "detail": "2MXBG6PN4A8JR", "agent": "uwf-hermes" } ] } ``` 注:`output` 在上下文中会被自动展开为实际的 CAS 节点内容(而非 hash),方便 JSONata 表达式直接访问字段。 #### `StartNode`(Thread 起点) ```yaml type: payload: workflow: "4KNM2PXR3B1QW" # cas_ref → Workflow prompt: "Fix the login bug..." ``` - 没有 thread-id — thread-id 是索引层面的事,不进 CAS 内容 - 没有 agent binding — 运行时从 config.yaml 解析 #### `StepNode`(Thread 每一步) ```yaml type: payload: start: "4TNVW8KR2B3MA" # cas_ref → StartNode(每个 step 都引用) prev: "2MXBG6PN4A8JR" # cas_ref → 前一个 StepNode,第一步为 null role: "developer" output: "9KRVW3TN5F1QA" # cas_ref → 结构化输出节点(符合 role 的 outputSchema) detail: "7BQST3VW9F2MA" # cas_ref → 执行详情(content node / 子 workflow terminal StepNode / ...) agent: "uwf-cursor" # 实际使用的 agent 命令(纯字符串) ``` - `start` — 每个 StepNode 都直接引用 StartNode,方便随机访问 - `prev` — 前一个 StepNode 的 cas_ref,第一步为 `null`(不指向 StartNode) - `output` — cas_ref,指向符合 role outputSchema 的 CAS 节点,可用 json-cas 校验 - `detail` — cas_ref,指向执行详情。可以是原始 agent 输出(content node),也可以是子 workflow thread 的 terminal StepNode(workflowAsAgent 场景) - `agent` — 纯字符串,不是 CAS 节点 ### 2.3 链式结构 ``` threads.yaml: { "01J7K9M2XNPQR5VWBCDF8G3H4T": "8FWKR3TN5V1QA" } │ ▼ StepNode (step 3) ├── start ──→ StartNode │ ├── workflow → CAS(Workflow) │ └── prompt: "Fix..." ├── prev ──→ StepNode (step 2) │ ├── start ──→ (same StartNode) │ ├── prev ──→ StepNode (step 1) │ │ ├── start ──→ (same StartNode) │ │ ├── prev: null │ │ ├── role: "planner" │ │ └── ... │ ├── role: "developer" │ └── ... ├── role: "reviewer" ├── output → CAS({ approved: true }) ├── detail → CAS(raw output | sub-workflow terminal node) └── agent: "uwf-hermes" ``` ### 2.4 可变状态 系统两个顶层 YAML 文件和一个 env 文件: ```yaml # ~/.uncaged/workflow/config.yaml — 全局配置 providers: openai: baseUrl: "https://api.openai.com/v1" apiKeyEnv: "OPENAI_API_KEY" anthropic: baseUrl: "https://api.anthropic.com/v1" apiKeyEnv: "ANTHROPIC_API_KEY" openrouter: baseUrl: "https://openrouter.ai/api/v1" apiKeyEnv: "OPENROUTER_API_KEY" models: sonnet: provider: "openrouter" name: "anthropic/claude-sonnet-4" gpt4o-mini: provider: "openai" name: "gpt-4o-mini" agents: hermes: command: "uwf-hermes" args: [] cursor: command: "uwf-cursor" args: [] defaultAgent: "hermes" agentOverrides: solve-issue: developer: "cursor" defaultModel: "sonnet" modelOverrides: extract: "gpt4o-mini" ``` ```yaml # ~/.uncaged/workflow/threads.yaml — active thread 链头指针 01J7K9M2XNPQR5VWBCDF8G3H4T: "8FWKR3TN5V1QA" 01J8AB3QRMSTV6WKXZ2C4DF7GN: "3CNWT9KR6D2HV" ``` Thread 结束时从 threads.yaml 移除。可选:追加到 `history.jsonl` 做归档。 ```bash # ~/.uncaged/workflow/.env — 敏感信息(API keys) OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... OPENROUTER_API_KEY=sk-or-... ``` - `config.yaml` — 非敏感配置(agent 命令、model 名、provider 名) - `.env` — 敏感信息(API keys),agent-kit 启动时自动加载 - `threads.yaml` — 运行时状态 --- ## 3. 包结构 全新包,不复用现有 packages,避免命名冲突。CAS 直接依赖 `@uncaged/json-cas`。 ``` packages/ ├── cli-workflow/ # @uncaged/cli-workflow — uwf CLI(thread/workflow 命令) ├── workflow-moderator/ # @uncaged/workflow-moderator — JSONata moderator 引擎 ├── workflow-agent-kit/ # @uncaged/workflow-agent-kit — Agent CLI 框架(含 extractor) ├── workflow-agent-hermes/ # @uncaged/workflow-agent-hermes — uwf-hermes CLI ├── workflow-agent-cursor/ # @uncaged/workflow-agent-cursor — uwf-cursor CLI └── workflow-protocol/ # @uncaged/workflow-protocol — 共享类型定义 ``` **外部依赖:** - `@uncaged/json-cas` — CAS 存储、hash、schema 校验 - `@uncaged/json-cas-fs` — 文件系统 CAS 后端 **现有包全部保留不动**,新旧并存,逐步迁移。 --- ## 4. 关键数据类型 JSONata 求值上下文本质上是 thread 链表的线性化表达。StepNode payload 和上下文中的 step 共享大量字段,提取为公共类型。 ### 4.1 公共类型 ```typescript /** CAS hash — XXH64, 13-char Crockford Base32 */ type CasRef = string; /** Thread ID — ULID, 26-char Crockford Base32 */ type ThreadId = string; /** 一个 step 的核心数据,被 StepNode payload 和 JSONata 上下文共享 */ type StepRecord = { role: string; output: CasRef; // cas_ref → 结构化输出节点(符合 role outputSchema) detail: CasRef; // cas_ref → 执行详情(content node / 子 workflow terminal StepNode) agent: string; // 实际使用的 agent 命令(纯字符串) }; ``` ### 4.2 Workflow 定义 ```typescript type RoleDefinition = { description: string; systemPrompt: string; outputSchema: CasRef; // cas_ref → json-cas 内置 JSON Schema 节点 }; type Transition = { role: string; // 目标 role 名 或 "$END" condition: string | null; // 引用 conditions 中的 key,null = fallback }; type ConditionDefinition = { description: string; expression: string; // JSONata expression }; type WorkflowPayload = { name: string; description: string; roles: Record; conditions: Record; graph: Record; // Record }; ``` ### 4.3 Thread 节点 ```typescript type StartNodePayload = { workflow: CasRef; // cas_ref → Workflow prompt: string; }; type StepNodePayload = StepRecord & { start: CasRef; // cas_ref → StartNode(每个 step 都引用) prev: CasRef | null; // cas_ref → 前一个 StepNode,第一步为 null }; ``` ### 4.4 JSONata 求值上下文 Thread 链表的线性化。`steps[n]` 的字段和 `StepRecord` 一致,但 `output` 被展开为实际内容。 ```typescript /** JSONata 上下文中的 step — output 被展开 */ type StepContext = Omit & { output: unknown; // 展开后的 CAS 节点内容,非 hash }; type ModeratorContext = { start: StartNodePayload; steps: StepContext[]; // 从旧到新 }; ``` ### 4.5 CLI 输出 ```typescript /** uwf thread start */ type StartOutput = { workflow: CasRef; thread: ThreadId; }; /** uwf thread step / uwf thread show */ type StepOutput = { workflow: CasRef; thread: ThreadId; head: CasRef; done: boolean; }; /** uwf thread list */ type ThreadListItem = { thread: ThreadId; workflow: CasRef; head: CasRef; }; ``` ### 4.6 配置 ```typescript /** Alias types for config references */ type AgentAlias = string; type ModelAlias = string; type ProviderAlias = string; type WorkflowName = string; type RoleName = string; type Scenario = string; // e.g. "extract" type ProviderConfig = { baseUrl: string; apiKeyEnv: string; // env var name to read API key from }; type ModelConfig = { provider: ProviderAlias; name: string; // e.g. "anthropic/claude-sonnet-4", "gpt-4o-mini" }; type AgentConfig = { command: string; args: string[]; }; /** ~/.uncaged/workflow/config.yaml */ type WorkflowConfig = { providers: Record; models: Record; agents: Record; defaultAgent: AgentAlias; agentOverrides: Record> | null; defaultModel: ModelAlias; modelOverrides: Record | null; }; /** ~/.uncaged/workflow/threads.yaml */ type ThreadsIndex = Record; // ^ thread-id ^ head StepNode/StartNode hash ``` ### 4.7 类型关系图 ``` WorkflowConfig (config.yaml) ThreadsIndex (threads.yaml) ← 唯二可变状态 │ │ thread-id → head hash ▼ StepNodePayload ──extends──→ StepRecord ←──maps to──→ StepContext │ │ │ ├── start → StartNodePayload│ │ (output 展开) ├── prev → StepNodePayload │ │ │ ├── role ├── role │ ├── output (CasRef) ├── output (展开) │ ├── detail (CasRef) ├── detail (CasRef) │ └── agent (string) └── agent (string) │ └── start.workflow → WorkflowPayload ├── roles: Record ├── conditions: Record └── graph: Record ```