refactor: replace Moderator function with ModeratorTable in WorkflowDefinition (#200)

- WorkflowDefinition.moderator → WorkflowDefinition.table (ModeratorTable)
- Moderator type + tableToModerator no longer exported from protocol/runtime
- tableToModerator internalized in workflow-execute engine layer
- WorkflowDescriptor gains graph: WorkflowGraph (auto-extracted from table)
- buildDescriptor extracts serializable graph edges from ModeratorTable
- validateWorkflowDescriptor validates graph structure
- All templates (develop, solve-issue) export table directly
- CLI init scaffold updated to use ModeratorTable
- 99 tests pass, 0 failures
This commit is contained in:
2026-05-12 10:01:30 +08:00
parent 0fe17b0fb2
commit db45089922
30 changed files with 202 additions and 70 deletions
@@ -17,7 +17,7 @@ import {
} from "../src/commands/workflow/index.js"; } from "../src/commands/workflow/index.js";
import { addCliArgs } from "./bundle-fixture.js"; import { addCliArgs } from "./bundle-fixture.js";
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {} }; const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {}, graph: { edges: [] } };
`; `;
const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas"; const wfPutImport = `import { putContentMerkleNode } from "@uncaged/workflow-cas";
@@ -153,6 +153,7 @@ export const run = async function* (input) { return { returnCode: 0, summary: in
schema: { type: "object", properties: { greeting: { type: "string" } } }, schema: { type: "object", properties: { greeting: { type: "string" } } },
}, },
}, },
graph: { edges: [] },
}; };
${wfPutImport} ${wfPutImport}
export const run = async function* (input, options) { export const run = async function* (input, options) {
@@ -24,6 +24,7 @@ export const descriptor = {
coder: { description: "coder", schema: {} }, coder: { description: "coder", schema: {} },
reviewer: { description: "reviewer", schema: {} }, reviewer: { description: "reviewer", schema: {} },
}, },
graph: { edges: [] },
}; };
export const run = async function* (input, options) { export const run = async function* (input, options) {
const cas = options.cas; const cas = options.cas;
@@ -64,6 +64,7 @@ describe("init template", () => {
const moder = await readFile(join(tdir, "src", "moderator.ts"), "utf8"); const moder = await readFile(join(tdir, "src", "moderator.ts"), "utf8");
expect(moder).not.toContain("export default"); expect(moder).not.toContain("export default");
expect(moder).toContain("ModeratorTable");
}); });
test("finds workspace walking up from nested cwd", async () => { test("finds workspace walking up from nested cwd", async () => {
@@ -82,7 +82,7 @@ describe("init workspace", () => {
for (const term of [ for (const term of [
"RoleDefinition", "RoleDefinition",
"WorkflowDefinition", "WorkflowDefinition",
"Moderator", "ModeratorTable",
"AgentFn", "AgentFn",
"ExtractFn", "ExtractFn",
"RoleMeta", "RoleMeta",
@@ -36,6 +36,7 @@ const threadFixtureDescriptor = `export const descriptor = {
only: { description: "only", schema: {} }, only: { description: "only", schema: {} },
noop: { description: "noop", schema: {} }, noop: { description: "noop", schema: {} },
}, },
graph: { edges: [] },
}; };
`; `;
@@ -57,17 +57,13 @@ export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
} }
export function templateModeratorTs(): string { export function templateModeratorTs(): string {
return `import { END, type Moderator, type ModeratorContext } from "@uncaged/workflow-runtime"; return `import { END, START, type ModeratorTable } from "@uncaged/workflow-runtime";
import type { HelloTemplateMeta } from "./roles.js"; import type { HelloTemplateMeta } from "./roles.js";
export const helloTemplateModerator: Moderator<HelloTemplateMeta> = ( export const helloTemplateTable: ModeratorTable<HelloTemplateMeta> = {
ctx: ModeratorContext<HelloTemplateMeta>, [START]: [{ condition: "FALLBACK", role: "greeter" }],
) => { greeter: [{ condition: "FALLBACK", role: END }],
if (ctx.steps.length === 0) {
return "greeter";
}
return END;
}; };
`; `;
} }
@@ -75,7 +71,7 @@ export const helloTemplateModerator: Moderator<HelloTemplateMeta> = (
export function templateIndexTs(): string { export function templateIndexTs(): string {
return `import type { WorkflowDefinition } from "@uncaged/workflow-runtime"; return `import type { WorkflowDefinition } from "@uncaged/workflow-runtime";
import { helloTemplateModerator } from "./moderator.js"; import { helloTemplateTable } from "./moderator.js";
import { import {
HELLO_TEMPLATE_DESCRIPTION, HELLO_TEMPLATE_DESCRIPTION,
type HelloTemplateMeta, type HelloTemplateMeta,
@@ -87,14 +83,14 @@ export {
type HelloTemplateMeta, type HelloTemplateMeta,
greeterRole, greeterRole,
} from "./roles.js"; } from "./roles.js";
export { helloTemplateModerator } from "./moderator.js"; export { helloTemplateTable } from "./moderator.js";
export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = { export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = {
description: HELLO_TEMPLATE_DESCRIPTION, description: HELLO_TEMPLATE_DESCRIPTION,
roles: { roles: {
greeter: greeterRole, greeter: greeterRole,
}, },
moderator: helloTemplateModerator, table: helloTemplateTable,
}; };
`; `;
} }
@@ -85,7 +85,7 @@ function agentsMd(): string {
| 层级 | 目录 / 产物 | 职责 | | 层级 | 目录 / 产物 | 职责 |
|------|----------------|------| |------|----------------|------|
| **Workspace** | 仓库根(\`package.json\`\`workspaces: ["templates/*", "workflows"]\`) | Bun monorepo:统一管理本地模板包与 workflow 实例 | | **Workspace** | 仓库根(\`package.json\`\`workspaces: ["templates/*", "workflows"]\`) | Bun monorepo:统一管理本地模板包与 workflow 实例 |
| **Template** | \`templates/<name>/\`(如 \`src/roles.ts\`\`src/moderator.ts\`\`src/index.ts\`) | 纯数据:**WorkflowDefinition**(各 **RoleDefinition** + **Moderator**),**不绑定**具体 Agent | | **Template** | \`templates/<name>/\`(如 \`src/roles.ts\`\`src/moderator.ts\`\`src/index.ts\`) | 纯数据:**WorkflowDefinition**(各 **RoleDefinition** + **ModeratorTable**),**不绑定**具体 Agent |
| **Workflow instance** | \`workflows/\`(或单独包) | 把模板与运行时 **AgentFn** / **ExtractFn** 组合,产出可注册的 **单文件 ESM bundle**(\`run\` + \`descriptor\` 命名导出) | | **Workflow instance** | \`workflows/\`(或单独包) | 把模板与运行时 **AgentFn** / **ExtractFn** 组合,产出可注册的 **单文件 ESM bundle**(\`run\` + \`descriptor\` 命名导出) |
Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下放绑定与打包入口。 Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下放绑定与打包入口。
@@ -94,19 +94,19 @@ Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下
- **RoleMeta**:\`Record<string, Record<string, unknown>>\`,角色名 → 该角色结构化 meta 的形状约定。 - **RoleMeta**:\`Record<string, Record<string, unknown>>\`,角色名 → 该角色结构化 meta 的形状约定。
- **RoleDefinition<Meta>**:纯数据——\`description\`\`systemPrompt\`\`schema\`(Zod v4)。不含执行逻辑。 - **RoleDefinition<Meta>**:纯数据——\`description\`\`systemPrompt\`\`schema\`(Zod v4)。不含执行逻辑。
- **WorkflowDefinition<M extends RoleMeta>**:\`description\` + \`roles\`(各角色定义)+ **Moderator** - **WorkflowDefinition<M extends RoleMeta>**:\`description\` + \`roles\`(各角色定义)+ **ModeratorTable**(声明式路由表)
- **Moderator**:\`(ctx: ModeratorContext<M>) => (角色名) | END\`。同步、纯函数,只做路由 - **ModeratorTable**:\`START\` 与各角色名映射到有序 transition 列表(条件 + 下一角色或 \`END\`);可序列化,供描述符提取 **graph**
- **AgentFn**:\`(ctx: AgentContext) => Promise<string>\`,原始文本输出;从上下文读取当前角色的 \`systemPrompt\` - **AgentFn**:\`(ctx: AgentContext) => Promise<string>\`,原始文本输出;从上下文读取当前角色的 \`systemPrompt\`
- **ExtractFn**:从 CAS content hash 解析结构化数据(引擎与 Agent 都可使用)。 - **ExtractFn**:从 CAS content hash 解析结构化数据(引擎与 Agent 都可使用)。
引擎循环简述:**Moderator** → 选角色 → **Agent** 产出文本 → **Extract** 写入 **meta** → 追加 step,重复直至 **END**。详见 \`docs/architecture.md\` 中的三阶段说明。 引擎循环简述:**ModeratorTable** 选下一角色 → **Agent** 产出文本 → **Extract** 写入 **meta** → 追加 step,重复直至 **END**。详见 \`docs/architecture.md\` 中的三阶段说明。
## 3. 开发流程 ## 3. 开发流程
1. **定义 RoleMeta**:为每个角色约定 meta 的 TypeScript 类型(与 Zod schema 对齐)。 1. **定义 RoleMeta**:为每个角色约定 meta 的 TypeScript 类型(与 Zod schema 对齐)。
2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`description\` 2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`description\`
3. **编写 Moderator**:根据 \`ctx.steps\`业务状态返回下一个角色名或 \`END\` 3. **编写 ModeratorTable**: \`START\`各角色声明 transition(\`FALLBACK\` 或命名条件 + \`check\`
4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / moderator 导出)。 4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / table 导出)。
5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\` 5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AgentFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\`
6. **构建**:打包为单个 **.esm.js** bundle,使用 **uncaged-workflow add** 注册。 6. **构建**:打包为单个 **.esm.js** bundle,使用 **uncaged-workflow add** 注册。
@@ -153,7 +153,7 @@ uncaged-workflow add <name> <path/to/bundle.esm.js>
--- ---
编写新 workflow 时,先对齐 **RoleMeta → RoleDefinition(Zod)→ Moderator → 绑定 → 单文件 bundle**,再对照本节规范自检。 编写新 workflow 时,先对齐 **RoleMeta → RoleDefinition(Zod)→ ModeratorTable → 绑定 → 单文件 bundle**,再对照本节规范自检。
`; `;
} }
@@ -164,7 +164,7 @@ Local workflow development workspace (Bun monorepo).
## Layout ## Layout
- \`templates/\` — reusable workflow definition packages (roles + moderator), no agent binding - \`templates/\` — reusable workflow definition packages (roles + ModeratorTable), no agent binding
- \`workflows/\` — workflow instances that bind templates to agents and export \`run\` + \`descriptor\` - \`workflows/\` — workflow instances that bind templates to agents and export \`run\` + \`descriptor\`
## Commands ## Commands
+14 -11
View File
@@ -189,25 +189,28 @@ export const run: WorkflowRun;
## WorkflowDescriptor ## WorkflowDescriptor
Defines the workflow's metadata and role sequence: Serialized metadata for the registry (per-role JSON Schema plus a static routing graph):
\`\`\`typescript \`\`\`typescript
type WorkflowDescriptor = { type WorkflowDescriptor = {
name: string; // verb-first kebab-case, e.g. "solve-issue" description: string;
description: string; // one-line summary roles: Record<string, { description: string; schema: unknown /* JSON Schema */ }>;
roles: string[]; // ordered role names, e.g. ["planner", "coder", "reviewer"] graph: {
edges: Array<{
from: string;
to: string;
condition: string;
conditionDescription: string | null;
}>;
};
}; };
\`\`\` \`\`\`
## WorkflowRun ## WorkflowRun
The main function that creates and returns a moderator: Async generator from \`createWorkflow(definition, binding)\` (**@uncaged/workflow-runtime**) — yields each role output until the workflow completes.
\`\`\`typescript The **ModeratorTable** on **WorkflowDefinition** is declarative routing (from each role and \`START\` to the next role or \`END\`); the engine evaluates conditions at runtime.
type WorkflowRun = (ctx: WorkflowContext) => Moderator;
\`\`\`
The **Moderator** controls the flow — it decides which role runs next, handles retries, and determines when the workflow is complete.
## Role Definition ## Role Definition
@@ -226,7 +229,7 @@ Each role has:
# 1. Initialize a workspace # 1. Initialize a workspace
uncaged-workflow init workspace my-workflow uncaged-workflow init workspace my-workflow
# 2. Write your template (roles + moderator + descriptor) # 2. Write your template (roles + ModeratorTable + descriptor)
# 3. Build the ESM bundle # 3. Build the ESM bundle
bun run build bun run build
+4
View File
@@ -6,6 +6,10 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./src/index.ts" "import": "./src/index.ts"
},
"./moderator-table.js": {
"types": "./dist/moderator-table.d.ts",
"import": "./src/moderator-table.ts"
} }
}, },
"peerDependencies": { "peerDependencies": {
+2 -5
View File
@@ -18,7 +18,6 @@ export type {
ExtractResult, ExtractResult,
FALLBACK, FALLBACK,
LlmProvider, LlmProvider,
Moderator,
ModeratorCondition, ModeratorCondition,
ModeratorContext, ModeratorContext,
ModeratorTable, ModeratorTable,
@@ -37,6 +36,8 @@ export type {
WorkflowDefinition, WorkflowDefinition,
WorkflowDescriptor, WorkflowDescriptor,
WorkflowFn, WorkflowFn,
WorkflowGraph,
WorkflowGraphEdge,
WorkflowResult, WorkflowResult,
WorkflowRoleDescriptor, WorkflowRoleDescriptor,
WorkflowRoleSchema, WorkflowRoleSchema,
@@ -50,7 +51,3 @@ export { END, START } from "./types.js";
// ── Constructor functions ────────────────────────────────────────── // ── Constructor functions ──────────────────────────────────────────
export { err, ok } from "./result.js"; export { err, ok } from "./result.js";
// ── Moderator Table ────────────────────────────────────────────────
export { tableToModerator } from "./moderator-table.js";
+14 -1
View File
@@ -27,9 +27,22 @@ export type WorkflowRoleDescriptor = {
schema: WorkflowRoleSchema; schema: WorkflowRoleSchema;
}; };
/** Serializable routing edges derived from a moderator transition table. */
export type WorkflowGraphEdge = {
from: string;
to: string;
condition: string;
conditionDescription: string | null;
};
export type WorkflowGraph = {
edges: readonly WorkflowGraphEdge[];
};
export type WorkflowDescriptor = { export type WorkflowDescriptor = {
description: string; description: string;
roles: Record<string, WorkflowRoleDescriptor>; roles: Record<string, WorkflowRoleDescriptor>;
graph: WorkflowGraph;
}; };
// ── Role & Thread ────────────────────────────────────────────────── // ── Role & Thread ──────────────────────────────────────────────────
@@ -160,7 +173,7 @@ export type Moderator<M extends RoleMeta> = (
export type WorkflowDefinition<M extends RoleMeta> = { export type WorkflowDefinition<M extends RoleMeta> = {
description: string; description: string;
roles: { [K in keyof M & string]: RoleDefinition<M[K]> }; roles: { [K in keyof M & string]: RoleDefinition<M[K]> };
moderator: Moderator<M>; table: ModeratorTable<M>;
}; };
// ── Declarative Moderator Table ──────────────────────────────────── // ── Declarative Moderator Table ────────────────────────────────────
@@ -1,12 +1,35 @@
import type { RoleMeta, WorkflowDefinition } from "@uncaged/workflow-protocol"; import type {
ModeratorTable,
ModeratorTransition,
RoleMeta,
WorkflowDefinition,
WorkflowDescriptor,
WorkflowGraph,
WorkflowGraphEdge,
} from "@uncaged/workflow-protocol";
import { END } from "@uncaged/workflow-protocol";
import * as z from "zod/v4"; import * as z from "zod/v4";
import type { WorkflowDescriptor, WorkflowRoleSchema } from "./types.js"; import type { WorkflowRoleSchema } from "./types.js";
function stripJsonSchemaMeta(json: Record<string, unknown>): WorkflowRoleSchema { function stripJsonSchemaMeta(json: Record<string, unknown>): WorkflowRoleSchema {
const { $schema: _drop, ...rest } = json; const { $schema: _drop, ...rest } = json;
return rest as WorkflowRoleSchema; return rest as WorkflowRoleSchema;
} }
function graphFromTable<M extends RoleMeta>(table: ModeratorTable<M>): WorkflowGraph {
const edges: WorkflowGraphEdge[] = [];
const entries = Object.entries(table) as Array<[string, ModeratorTransition<M>[]]>;
for (const [from, transitions] of entries) {
for (const t of transitions) {
const conditionName = t.condition === "FALLBACK" ? "FALLBACK" : t.condition.name;
const conditionDescription = t.condition === "FALLBACK" ? null : t.condition.description;
const to = t.role === END ? END : t.role;
edges.push({ from, to, condition: conditionName, conditionDescription });
}
}
return { edges };
}
export function buildDescriptor<M extends RoleMeta>( export function buildDescriptor<M extends RoleMeta>(
def: WorkflowDefinition<M>, def: WorkflowDefinition<M>,
): WorkflowDescriptor { ): WorkflowDescriptor {
@@ -20,5 +43,9 @@ export function buildDescriptor<M extends RoleMeta>(
schema: stripJsonSchemaMeta(rawJsonSchema), schema: stripJsonSchemaMeta(rawJsonSchema),
}; };
} }
return { description: def.description, roles }; return {
description: def.description,
roles,
graph: graphFromTable(def.table),
};
} }
@@ -404,7 +404,7 @@ export function validateWorkflowBundle(input: WorkflowBundleValidationInput): Re
if (!descriptorExportExists(program)) { if (!descriptorExportExists(program)) {
return err( return err(
'workflow bundle must export descriptor (e.g. "export const descriptor = { description, roles }")', 'workflow bundle must export descriptor (e.g. "export const descriptor = { description, roles, graph }")',
); );
} }
@@ -9,6 +9,8 @@ export type {
ExtractedBundleExports, ExtractedBundleExports,
WorkflowBundleValidationInput, WorkflowBundleValidationInput,
WorkflowDescriptor, WorkflowDescriptor,
WorkflowGraph,
WorkflowGraphEdge,
WorkflowRoleDescriptor, WorkflowRoleDescriptor,
WorkflowRoleSchema, WorkflowRoleSchema,
} from "./types.js"; } from "./types.js";
@@ -3,6 +3,8 @@ import type { WorkflowDescriptor, WorkflowFn } from "@uncaged/workflow-protocol"
export type { export type {
WorkflowDescriptor, WorkflowDescriptor,
WorkflowFn, WorkflowFn,
WorkflowGraph,
WorkflowGraphEdge,
WorkflowRoleDescriptor, WorkflowRoleDescriptor,
WorkflowRoleSchema, WorkflowRoleSchema,
} from "@uncaged/workflow-protocol"; } from "@uncaged/workflow-protocol";
@@ -1,6 +1,64 @@
import { err, ok, type Result } from "@uncaged/workflow-util"; import { err, ok, type Result } from "@uncaged/workflow-util";
import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js"; import type {
WorkflowDescriptor,
WorkflowGraph,
WorkflowGraphEdge,
WorkflowRoleDescriptor,
WorkflowRoleSchema,
} from "./types.js";
function validateDescriptorGraphEdge(
item: unknown,
index: number,
): Result<WorkflowGraphEdge, string> {
if (item === null || typeof item !== "object" || Array.isArray(item)) {
return err(`descriptor.graph.edges[${index}] must be a non-array object`);
}
const e = item as Record<string, unknown>;
if (typeof e.from !== "string") {
return err(`descriptor.graph.edges[${index}].from must be a string`);
}
if (typeof e.to !== "string") {
return err(`descriptor.graph.edges[${index}].to must be a string`);
}
if (typeof e.condition !== "string") {
return err(`descriptor.graph.edges[${index}].condition must be a string`);
}
const cdRaw = e.conditionDescription;
if (cdRaw !== null && cdRaw !== undefined && typeof cdRaw !== "string") {
return err(`descriptor.graph.edges[${index}].conditionDescription must be a string or null`);
}
const conditionDescription: string | null = cdRaw === undefined || cdRaw === null ? null : cdRaw;
return ok({
from: e.from,
to: e.to,
condition: e.condition,
conditionDescription,
});
}
function validateDescriptorGraph(graphRaw: unknown): Result<WorkflowGraph, string> {
if (graphRaw === null || typeof graphRaw !== "object" || Array.isArray(graphRaw)) {
return err("descriptor.graph must be a non-array object");
}
const graphRecord = graphRaw as Record<string, unknown>;
const edgesRaw = graphRecord.edges;
if (!Array.isArray(edgesRaw)) {
return err("descriptor.graph.edges must be an array");
}
const edges: WorkflowGraphEdge[] = [];
for (let i = 0; i < edgesRaw.length; i++) {
const edgeResult = validateDescriptorGraphEdge(edgesRaw[i], i);
if (!edgeResult.ok) {
return edgeResult;
}
edges.push(edgeResult.value);
}
return ok({ edges });
}
export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> { export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> {
if (value === null || typeof value !== "object" || Array.isArray(value)) { if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -36,5 +94,10 @@ export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescr
}; };
} }
return ok({ description, roles }); const graphResult = validateDescriptorGraph(root.graph);
if (!graphResult.ok) {
return graphResult;
}
return ok({ description, roles, graph: graphResult.value });
} }
+2
View File
@@ -3,6 +3,8 @@ export type {
ExtractedBundleExports, ExtractedBundleExports,
WorkflowBundleValidationInput, WorkflowBundleValidationInput,
WorkflowDescriptor, WorkflowDescriptor,
WorkflowGraph,
WorkflowGraphEdge,
WorkflowRoleDescriptor, WorkflowRoleDescriptor,
WorkflowRoleSchema, WorkflowRoleSchema,
} from "./bundle/index.js"; } from "./bundle/index.js";
@@ -1,4 +1,5 @@
import { putContentNodeWithRefs } from "@uncaged/workflow-cas"; import { putContentNodeWithRefs } from "@uncaged/workflow-cas";
import { tableToModerator } from "@uncaged/workflow-protocol/moderator-table.js";
import type * as z from "zod/v4"; import type * as z from "zod/v4";
import { import {
@@ -57,7 +58,9 @@ function agentForRole(binding: AgentBinding, roleName: string): AgentFn {
} }
async function advanceOneRound<M extends RoleMeta>( async function advanceOneRound<M extends RoleMeta>(
def: Pick<WorkflowDefinition<M>, "roles" | "moderator">, def: Pick<WorkflowDefinition<M>, "roles"> & {
pickNext: (ctx: ModeratorContext<M>) => (keyof M & string) | typeof END;
},
binding: AgentBinding, binding: AgentBinding,
params: { params: {
thread: ModeratorContext<M>; thread: ModeratorContext<M>;
@@ -67,7 +70,7 @@ async function advanceOneRound<M extends RoleMeta>(
const { thread, runtime } = params; const { thread, runtime } = params;
const modCtx: ModeratorContext<M> = thread; const modCtx: ModeratorContext<M> = thread;
const next = def.moderator(modCtx); const next = def.pickNext(modCtx);
if (!isRoleNext(next)) { if (!isRoleNext(next)) {
return { return {
kind: "complete", kind: "complete",
@@ -128,16 +131,19 @@ async function advanceOneRound<M extends RoleMeta>(
} }
/** /**
* Binds pure role definitions + moderator to runtime agents. * Binds pure role definitions + moderator table to runtime agents.
* Assign with `export const run = createWorkflow(def, binding)`. * Assign with `export const run = createWorkflow(def, binding)`.
* *
* Structured meta extraction is delegated to {@link WorkflowRuntime.extract}, which the * Structured meta extraction is delegated to {@link WorkflowRuntime.extract}, which the
* engine resolves from the workflow registry's `extract` scene. * engine resolves from the workflow registry's `extract` scene.
*/ */
export function createWorkflow<M extends RoleMeta>( export function createWorkflow<M extends RoleMeta>(
def: Pick<WorkflowDefinition<M>, "roles" | "moderator">, def: Pick<WorkflowDefinition<M>, "roles" | "table">,
binding: AgentBinding, binding: AgentBinding,
): WorkflowFn { ): WorkflowFn {
const pickNext = tableToModerator(def.table);
const loopDef = { roles: def.roles, pickNext };
return async function* workflowLoop( return async function* workflowLoop(
thread: ThreadContext, thread: ThreadContext,
runtime: WorkflowRuntime, runtime: WorkflowRuntime,
@@ -148,7 +154,7 @@ export function createWorkflow<M extends RoleMeta>(
let currentThread = thread as ModeratorContext<M>; let currentThread = thread as ModeratorContext<M>;
while (true) { while (true) {
const outcome = await advanceOneRound(def, binding, { const outcome = await advanceOneRound(loopDef, binding, {
thread: currentThread, thread: currentThread,
runtime, runtime,
}); });
+3 -2
View File
@@ -10,7 +10,6 @@ export type {
ExtractResult, ExtractResult,
FALLBACK, FALLBACK,
LlmProvider, LlmProvider,
Moderator,
ModeratorCondition, ModeratorCondition,
ModeratorContext, ModeratorContext,
ModeratorTable, ModeratorTable,
@@ -26,9 +25,11 @@ export type {
WorkflowDefinition, WorkflowDefinition,
WorkflowDescriptor, WorkflowDescriptor,
WorkflowFn, WorkflowFn,
WorkflowGraph,
WorkflowGraphEdge,
WorkflowResult, WorkflowResult,
WorkflowRoleDescriptor, WorkflowRoleDescriptor,
WorkflowRoleSchema, WorkflowRoleSchema,
WorkflowRuntime, WorkflowRuntime,
} from "./types.js"; } from "./types.js";
export { END, START, tableToModerator } from "./types.js"; export { END, START } from "./types.js";
+3 -2
View File
@@ -12,7 +12,6 @@ export type {
ExtractResult, ExtractResult,
FALLBACK, FALLBACK,
LlmProvider, LlmProvider,
Moderator,
ModeratorCondition, ModeratorCondition,
ModeratorContext, ModeratorContext,
ModeratorTable, ModeratorTable,
@@ -30,10 +29,12 @@ export type {
WorkflowDefinition, WorkflowDefinition,
WorkflowDescriptor, WorkflowDescriptor,
WorkflowFn, WorkflowFn,
WorkflowGraph,
WorkflowGraphEdge,
WorkflowResult, WorkflowResult,
WorkflowRoleDescriptor, WorkflowRoleDescriptor,
WorkflowRoleSchema, WorkflowRoleSchema,
WorkflowRuntime, WorkflowRuntime,
} from "@uncaged/workflow-protocol"; } from "@uncaged/workflow-protocol";
export { END, START, tableToModerator } from "@uncaged/workflow-protocol"; export { END, START } from "@uncaged/workflow-protocol";
@@ -1,11 +1,14 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { tableToModerator } from "@uncaged/workflow-protocol/moderator-table.js";
import { validateWorkflowDescriptor } from "@uncaged/workflow-register"; import { validateWorkflowDescriptor } from "@uncaged/workflow-register";
import { END, type ModeratorContext, type RoleStep, START } from "@uncaged/workflow-runtime"; import { END, type ModeratorContext, type RoleStep, START } from "@uncaged/workflow-runtime";
import { buildDevelopDescriptor } from "../src/descriptor.js"; import { buildDevelopDescriptor } from "../src/descriptor.js";
import { developModerator } from "../src/index.js"; import { developTable } from "../src/moderator.js";
import type { CommitterMeta, PlannerMeta } from "../src/roles/index.js"; import type { CommitterMeta, PlannerMeta } from "../src/roles/index.js";
import type { DevelopMeta } from "../src/roles.js"; import type { DevelopMeta } from "../src/roles.js";
const developModerator = tableToModerator(developTable);
const DEFAULT_PHASES: PlannerMeta["phases"] = [ const DEFAULT_PHASES: PlannerMeta["phases"] = [
{ {
hash: "4KNMR2PX", hash: "4KNMR2PX",
@@ -232,6 +235,7 @@ describe("buildDevelopDescriptor", () => {
"reviewer", "reviewer",
"tester", "tester",
]); ]);
expect(validated.value.graph.edges.length).toBeGreaterThan(0);
for (const key of ["planner", "coder", "reviewer", "tester", "committer"] as const) { for (const key of ["planner", "coder", "reviewer", "tester", "committer"] as const) {
const role = validated.value.roles[key]; const role = validated.value.roles[key];
expect(role).toBeDefined(); expect(role).toBeDefined();
@@ -15,5 +15,8 @@
"@uncaged/workflow-register": "workspace:*", "@uncaged/workflow-register": "workspace:*",
"@uncaged/workflow-runtime": "workspace:*", "@uncaged/workflow-runtime": "workspace:*",
"zod": "^4.0.0" "zod": "^4.0.0"
},
"devDependencies": {
"@uncaged/workflow-protocol": "workspace:*"
} }
} }
@@ -1,12 +1,12 @@
import { buildDescriptor } from "@uncaged/workflow-register"; import { buildDescriptor } from "@uncaged/workflow-register";
import { developModerator } from "./moderator.js"; import { developTable } from "./moderator.js";
import { DEVELOP_WORKFLOW_DESCRIPTION, developRoles } from "./roles.js"; import { DEVELOP_WORKFLOW_DESCRIPTION, developRoles } from "./roles.js";
export function buildDevelopDescriptor() { export function buildDevelopDescriptor() {
return buildDescriptor({ return buildDescriptor({
description: DEVELOP_WORKFLOW_DESCRIPTION, description: DEVELOP_WORKFLOW_DESCRIPTION,
roles: developRoles, roles: developRoles,
moderator: developModerator, table: developTable,
}); });
} }
@@ -1,10 +1,10 @@
import type { WorkflowDefinition } from "@uncaged/workflow-runtime"; import type { WorkflowDefinition } from "@uncaged/workflow-runtime";
import { developModerator } from "./moderator.js"; import { developTable } from "./moderator.js";
import { DEVELOP_WORKFLOW_DESCRIPTION, type DevelopMeta, developRoles } from "./roles.js"; import { DEVELOP_WORKFLOW_DESCRIPTION, type DevelopMeta, developRoles } from "./roles.js";
export { buildDevelopDescriptor } from "./descriptor.js"; export { buildDevelopDescriptor } from "./descriptor.js";
export { developModerator } from "./moderator.js"; export { developTable } from "./moderator.js";
export { export {
type CoderMeta, type CoderMeta,
type CommitterMeta, type CommitterMeta,
@@ -33,5 +33,5 @@ export {
export const developWorkflowDefinition: WorkflowDefinition<DevelopMeta> = { export const developWorkflowDefinition: WorkflowDefinition<DevelopMeta> = {
description: DEVELOP_WORKFLOW_DESCRIPTION, description: DEVELOP_WORKFLOW_DESCRIPTION,
roles: developRoles, roles: developRoles,
moderator: developModerator, table: developTable,
}; };
@@ -3,7 +3,6 @@ import {
type ModeratorCondition, type ModeratorCondition,
type ModeratorTable, type ModeratorTable,
START, START,
tableToModerator,
} from "@uncaged/workflow-runtime"; } from "@uncaged/workflow-runtime";
import type { DevelopMeta } from "./roles.js"; import type { DevelopMeta } from "./roles.js";
@@ -88,4 +87,4 @@ const table: ModeratorTable<DevelopMeta> = {
committer: [{ condition: "FALLBACK", role: END }], committer: [{ condition: "FALLBACK", role: END }],
}; };
export const developModerator = tableToModerator(table); export { table as developTable };
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { createCasStore } from "@uncaged/workflow-cas"; import { createCasStore } from "@uncaged/workflow-cas";
import { createExtract } from "@uncaged/workflow-execute"; import { createExtract } from "@uncaged/workflow-execute";
import { tableToModerator } from "@uncaged/workflow-protocol/moderator-table.js";
import { validateWorkflowDescriptor } from "@uncaged/workflow-register"; import { validateWorkflowDescriptor } from "@uncaged/workflow-register";
import { import {
createWorkflow, createWorkflow,
@@ -14,10 +15,12 @@ import {
} from "@uncaged/workflow-runtime"; } from "@uncaged/workflow-runtime";
import { buildSolveIssueDescriptor } from "../src/descriptor.js"; import { buildSolveIssueDescriptor } from "../src/descriptor.js";
import type { DeveloperMeta } from "../src/developer.js"; import type { DeveloperMeta } from "../src/developer.js";
import { solveIssueModerator, solveIssueWorkflowDefinition } from "../src/index.js"; import { solveIssueTable, solveIssueWorkflowDefinition } from "../src/index.js";
import type { PreparerMeta, SubmitterMeta } from "../src/roles/index.js"; import type { PreparerMeta, SubmitterMeta } from "../src/roles/index.js";
import type { SolveIssueMeta } from "../src/roles.js"; import type { SolveIssueMeta } from "../src/roles.js";
const solveIssueModerator = tableToModerator(solveIssueTable);
function jsonResponse(payload: Record<string, unknown>): Response { function jsonResponse(payload: Record<string, unknown>): Response {
return new Response(JSON.stringify(payload), { return new Response(JSON.stringify(payload), {
status: 200, status: 200,
@@ -388,6 +391,7 @@ describe("buildSolveIssueDescriptor", () => {
"preparer", "preparer",
"submitter", "submitter",
]); ]);
expect(validated.value.graph.edges.length).toBe(4);
for (const key of ["preparer", "developer", "submitter"] as const) { for (const key of ["preparer", "developer", "submitter"] as const) {
const role = validated.value.roles[key]; const role = validated.value.roles[key];
expect(role).toBeDefined(); expect(role).toBeDefined();
@@ -18,6 +18,7 @@
}, },
"devDependencies": { "devDependencies": {
"@uncaged/workflow-cas": "workspace:*", "@uncaged/workflow-cas": "workspace:*",
"@uncaged/workflow-execute": "workspace:*" "@uncaged/workflow-execute": "workspace:*",
"@uncaged/workflow-protocol": "workspace:*"
} }
} }
@@ -1,12 +1,12 @@
import { buildDescriptor } from "@uncaged/workflow-register"; import { buildDescriptor } from "@uncaged/workflow-register";
import { solveIssueModerator } from "./moderator.js"; import { solveIssueTable } from "./moderator.js";
import { SOLVE_ISSUE_WORKFLOW_DESCRIPTION, solveIssueRoles } from "./roles.js"; import { SOLVE_ISSUE_WORKFLOW_DESCRIPTION, solveIssueRoles } from "./roles.js";
export function buildSolveIssueDescriptor() { export function buildSolveIssueDescriptor() {
return buildDescriptor({ return buildDescriptor({
description: SOLVE_ISSUE_WORKFLOW_DESCRIPTION, description: SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
roles: solveIssueRoles, roles: solveIssueRoles,
moderator: solveIssueModerator, table: solveIssueTable,
}); });
} }
@@ -1,6 +1,6 @@
import type { WorkflowDefinition } from "@uncaged/workflow-runtime"; import type { WorkflowDefinition } from "@uncaged/workflow-runtime";
import { solveIssueModerator } from "./moderator.js"; import { solveIssueTable } from "./moderator.js";
import { SOLVE_ISSUE_WORKFLOW_DESCRIPTION, type SolveIssueMeta, solveIssueRoles } from "./roles.js"; import { SOLVE_ISSUE_WORKFLOW_DESCRIPTION, type SolveIssueMeta, solveIssueRoles } from "./roles.js";
export { buildSolveIssueDescriptor } from "./descriptor.js"; export { buildSolveIssueDescriptor } from "./descriptor.js";
@@ -9,7 +9,7 @@ export {
developerMetaSchema, developerMetaSchema,
developerRole, developerRole,
} from "./developer.js"; } from "./developer.js";
export { solveIssueModerator } from "./moderator.js"; export { solveIssueTable } from "./moderator.js";
export { export {
type PreparerMeta, type PreparerMeta,
preparerMetaSchema, preparerMetaSchema,
@@ -28,5 +28,5 @@ export {
export const solveIssueWorkflowDefinition: WorkflowDefinition<SolveIssueMeta> = { export const solveIssueWorkflowDefinition: WorkflowDefinition<SolveIssueMeta> = {
description: SOLVE_ISSUE_WORKFLOW_DESCRIPTION, description: SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
roles: solveIssueRoles, roles: solveIssueRoles,
moderator: solveIssueModerator, table: solveIssueTable,
}; };
@@ -1,4 +1,4 @@
import { END, type ModeratorTable, START, tableToModerator } from "@uncaged/workflow-runtime"; import { END, type ModeratorTable, START } from "@uncaged/workflow-runtime";
import type { SolveIssueMeta } from "./roles.js"; import type { SolveIssueMeta } from "./roles.js";
@@ -9,4 +9,4 @@ const table: ModeratorTable<SolveIssueMeta> = {
submitter: [{ condition: "FALLBACK", role: END }], submitter: [{ condition: "FALLBACK", role: END }],
}; };
export const solveIssueModerator = tableToModerator(table); export { table as solveIssueTable };