From eda00d1c8e86484f0a3fa9c67315d01fc070f17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Wed, 6 May 2026 05:18:56 +0000 Subject: [PATCH] =?UTF-8?q?docs(rfc-001):=20bundle=20contract=20=E2=86=92?= =?UTF-8?q?=20AsyncGenerator,=20Role/Moderator=20=E2=86=92=20helper=20patt?= =?UTF-8?q?ern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking design change: - Section 2: WorkflowFn is now an AsyncGenerator that yields RoleOutput - Section 8: Role/Moderator demoted from contract to optional helper - Engine controls the loop via generator protocol (DIP) - Zero injection — bundles don't import engine types 小橘 --- docs/rfc-001-workflow-engine.md | 82 ++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/docs/rfc-001-workflow-engine.md b/docs/rfc-001-workflow-engine.md index 1ba54a0..3f95bcc 100644 --- a/docs/rfc-001-workflow-engine.md +++ b/docs/rfc-001-workflow-engine.md @@ -19,15 +19,64 @@ Monorepo uses **bun workspace**. ## 2. Workflow Physical Implementation -A **Workflow** is a single-file ESM module that default-exports a function: +A **Workflow** is a single-file ESM module that default-exports an **AsyncGenerator** function: ```typescript +/** What each yield produces — one role's output. */ +type RoleOutput = { + role: string; + content: string; + meta: Record; +}; + +/** What the generator returns when done. */ +type WorkflowResult = { + returnCode: number; + summary: string; +}; + +/** The bundle contract — an AsyncGenerator, not a Promise. */ type WorkflowFn = ( prompt: string, options: { isDryRun: boolean; maxRounds: number } -) => Promise<{ returnCode: number; summary: string }>; +) => AsyncGenerator; ``` +### Why AsyncGenerator? + +The workflow **yields** each role output instead of writing to an injected writer or +exporting a framework-specific shape: + +```typescript +// Example bundle — zero framework dependency +export default async function* (prompt, options) { + const plan = await callLLM("plan: " + prompt); + yield { role: "planner", content: plan, meta: { files: ["src/auth.ts"] } }; + + const code = await callLLM("implement: " + plan); + yield { role: "coder", content: code, meta: { diff: "..." } }; + + return { returnCode: 0, summary: "Fixed auth bug" }; +} +``` + +**Engine controls the loop**, not the bundle: +- Each `yield` → engine writes to `.data.jsonl`, checks `AbortSignal`, handles pause/resume +- `return` → engine writes the final result, marks thread complete +- **Fork** = replay the first N yields from persisted `.data.jsonl`, then resume iteration +- **Zero injection** — the bundle doesn't import or receive anything from the engine + +This follows the **Dependency Inversion Principle**: the engine depends on the +generator protocol (a language primitive), not on a framework-specific `WorkflowDefinition`. +Bundles remain pure functions with no coupling to `@uncaged/workflow`. + +### Relationship to Role/Moderator Pattern + +The Role + Moderator pattern from Section 8 is one **implementation strategy** inside a +bundle, not the bundle contract itself. A helper like `createRoleModerator(roles, moderator)` +can produce the AsyncGenerator internally, but simple workflows can yield directly without +any framework types. + ### Constraints - Single `.esm.js` file @@ -202,11 +251,32 @@ No concurrency control or timeout settings in the registry — those belong to e |---------|-------------| | `uncaged-workflow fork [--from-role ]` | Fork from a historical thread state | -## 8. Workflow Execution Model: Role, Moderator, Agent +## 8. Role/Moderator Pattern (Helper, Not Contract) -A workflow is a finite-state automaton driven by three concepts: +The bundle contract is the AsyncGenerator from Section 2. The Role + Moderator pattern +below is a **convenience helper** for the common case of multi-role workflows with a +routing function. It lives in `@uncaged/workflow` as an optional utility. -### Core Types +### Helper Function + +```typescript +function createRoleModerator( + def: { roles: { [K in keyof M & string]: Role }; moderator: Moderator } +): (prompt: string, options: { isDryRun: boolean; maxRounds: number }) => AsyncGenerator; +``` + +Usage in a bundle: + +```typescript +import { createRoleModerator } from "@uncaged/workflow"; + +export default createRoleModerator({ + roles: { planner, coder }, + moderator(ctx) { return ctx.steps.length === 0 ? "planner" : END; }, +}); +``` + +### Supporting Types ```typescript /** Sentinel values for automaton control flow. */ @@ -267,7 +337,7 @@ type WorkflowDefinition = { }; ``` -### Execution Flow +### Execution Flow (when using createRoleModerator) ``` START (prompt) → Moderator → Role A → Moderator → Role B → ... → Moderator → END