diff --git a/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts b/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts index cd38a4c..fafb86a 100644 --- a/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts +++ b/packages/cli-workflow/src/__tests__/solve-issue-tea-worktree.test.ts @@ -70,7 +70,6 @@ describe("solve-issue workflow: tea pr create worktree fix", () => { // Basic structure validation expect(workflow.name).toBe("solve-issue"); expect(workflow.roles).toBeDefined(); - expect(workflow.conditions).toBeDefined(); expect(workflow.graph).toBeDefined(); // Verify committer role exists with required fields diff --git a/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts b/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts index d2decf1..bf5a4fb 100644 --- a/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts +++ b/packages/cli-workflow/src/__tests__/workflow-resolution.test.ts @@ -25,7 +25,6 @@ async function storeWorkflow(uwf: UwfStore, name: string): Promise { name, description: "Test workflow", roles: {}, - conditions: {}, graph: {}, }; return await uwf.store.put(uwf.schemas.workflow, payload); @@ -36,7 +35,6 @@ async function createWorkflowYaml(name: string, version: string | null = null): name, description: version !== null ? `Test workflow (${version})` : "Test workflow", roles: {}, - conditions: {}, graph: {}, }; const yaml = stringify(payload); @@ -145,7 +143,7 @@ describe("Strategy 2: File Path Resolution", () => { test("should fail on valid YAML with invalid WorkflowPayload shape", async () => { await makeUwfStore(storageRoot); const yamlPath = join(tmpDir, "invalid-workflow.yaml"); - await writeFile(yamlPath, "name: test\n# missing roles, conditions, and graph"); + await writeFile(yamlPath, "name: test\n# missing roles and graph"); await expect(cmdThreadStart(storageRoot, yamlPath, "prompt", projectRoot)).rejects.toThrow(); }); diff --git a/packages/cli-workflow/src/commands/thread.ts b/packages/cli-workflow/src/commands/thread.ts index bd34435..5850bb0 100644 --- a/packages/cli-workflow/src/commands/thread.ts +++ b/packages/cli-workflow/src/commands/thread.ts @@ -8,10 +8,8 @@ import type { AgentAlias, AgentConfig, CasRef, - ModeratorContext, StartNodePayload, StartOutput, - StepContext, StepNodePayload, StepOutput, ThreadId, @@ -53,6 +51,7 @@ import { import { materializeWorkflowPayload } from "./workflow.js"; const END_ROLE = "$END"; +const START_ROLE = "$START"; export const THREAD_READ_DEFAULT_QUOTA = 4000; const PL_THREAD_START = "7HNQ4B2X"; @@ -670,17 +669,32 @@ function formatThreadReadMarkdown(options: { return parts.join("\n\n---\n\n"); } -function buildModeratorContext(uwf: UwfStore, chain: ChainState): ModeratorContext { - const chronological = [...chain.stepsNewestFirst].reverse(); - const steps: StepContext[] = chronological.map((step) => ({ - role: step.role, - output: expandOutput(uwf, step.output), - detail: step.detail, - agent: step.agent, - edgePrompt: step.edgePrompt ?? "", - content: null, // Moderator doesn't need content - })); - return { start: chain.start, steps }; +type EvaluateLastOutput = Record & { status: string }; + +function resolveEvaluateArgs( + uwf: UwfStore, + chain: ChainState, +): { lastRole: string; lastOutput: EvaluateLastOutput } { + if (chain.headIsStart) { + return { lastRole: START_ROLE, lastOutput: { status: "_" } }; + } + + const lastStep = chain.stepsNewestFirst[0]; + if (lastStep === undefined) { + fail("empty step chain"); + } + + const raw = expandOutput(uwf, lastStep.output); + const base = + typeof raw === "object" && raw !== null && !Array.isArray(raw) + ? (raw as Record) + : {}; + const status = typeof base.status === "string" ? base.status : "_"; + + return { + lastRole: lastStep.role, + lastOutput: { ...base, status }, + }; } function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayload { @@ -924,9 +938,9 @@ async function cmdThreadStepOnce( const chain = walkChain(uwf, headHash); const workflowHash = chain.start.workflow; const workflow = loadWorkflowPayload(uwf, workflowHash); - const context = buildModeratorContext(uwf, chain); + const { lastRole, lastOutput } = resolveEvaluateArgs(uwf, chain); - const nextResult = await evaluate(workflow, context); + const nextResult = evaluate(workflow.graph, lastRole, lastOutput); if (!nextResult.ok) { failStep(plog, `moderator evaluate failed: ${nextResult.error.message}`); } @@ -976,8 +990,11 @@ async function cmdThreadStepOnce( await saveThreadsIndex(storageRoot, freshIndex); const chainAfter = walkChain(uwfAfter, newHead); - const contextAfter = buildModeratorContext(uwfAfter, chainAfter); - const afterResult = await evaluate(workflow, contextAfter); + const { lastRole: lastRoleAfter, lastOutput: lastOutputAfter } = resolveEvaluateArgs( + uwfAfter, + chainAfter, + ); + const afterResult = evaluate(workflow.graph, lastRoleAfter, lastOutputAfter); if (!afterResult.ok) { failStep(plog, `post-step moderator evaluate failed: ${afterResult.error.message}`); } diff --git a/packages/cli-workflow/src/commands/workflow.ts b/packages/cli-workflow/src/commands/workflow.ts index ea72e94..e67ee5b 100644 --- a/packages/cli-workflow/src/commands/workflow.ts +++ b/packages/cli-workflow/src/commands/workflow.ts @@ -2,12 +2,7 @@ import { readFile } from "node:fs/promises"; import type { JSONSchema } from "@uncaged/json-cas"; import { putSchema, validate } from "@uncaged/json-cas"; -import type { - CasRef, - RoleDefinition, - Transition, - WorkflowPayload, -} from "@uncaged/workflow-protocol"; +import type { CasRef, RoleDefinition, Target, WorkflowPayload } from "@uncaged/workflow-protocol"; import { parse } from "yaml"; import { @@ -51,20 +46,23 @@ function isJsonSchema(value: unknown): value is JSONSchema { return typeof value === "object" && value !== null && !Array.isArray(value); } -/** Normalize graph transitions: ensure condition is null (not undefined) for fallback entries. */ -function normalizeGraph(graph: Record): Record { - const result: Record = {}; - for (const [node, transitions] of Object.entries(graph)) { - result[node] = transitions.map((t) => { - if (typeof t.prompt !== "string" || t.prompt.trim() === "") { - fail(`graph[${node}] transition to "${t.role}": prompt is required (non-empty string)`); +/** Normalize graph: validate each status → target mapping. */ +function normalizeGraph( + graph: Record>, +): Record> { + const result: Record> = {}; + for (const [node, statusMap] of Object.entries(graph)) { + const normalized: Record = {}; + for (const [status, target] of Object.entries(statusMap)) { + if (typeof target.prompt !== "string" || target.prompt.trim() === "") { + fail(`graph[${node}][${status}] → "${target.role}": prompt is required (non-empty string)`); } - return { - role: t.role, - condition: t.condition ?? null, - prompt: t.prompt, + normalized[status] = { + role: target.role, + prompt: target.prompt, }; - }); + } + result[node] = normalized; } return result; } @@ -106,7 +104,6 @@ export async function materializeWorkflowPayload( name: raw.name, description: raw.description, roles, - conditions: raw.conditions, graph: normalizeGraph(raw.graph), }; } diff --git a/packages/cli-workflow/src/validate.ts b/packages/cli-workflow/src/validate.ts index 61dffd2..e80c715 100644 --- a/packages/cli-workflow/src/validate.ts +++ b/packages/cli-workflow/src/validate.ts @@ -30,23 +30,12 @@ function isRoleDefinition(value: unknown): boolean { ); } -function isConditionDefinition(value: unknown): boolean { +function isTarget(value: unknown): boolean { if (!isRecord(value)) { return false; } - return typeof value.description === "string" && typeof value.expression === "string"; -} - -function isTransition(value: unknown): boolean { - if (!isRecord(value)) { - return false; - } - const condition = value.condition; return ( - typeof value.role === "string" && - typeof value.prompt === "string" && - value.prompt.trim() !== "" && - (condition === null || condition === undefined || typeof condition === "string") + typeof value.role === "string" && typeof value.prompt === "string" && value.prompt.trim() !== "" ); } @@ -62,7 +51,7 @@ function isGraph(value: unknown): boolean { return false; } return Object.values(value).every( - (transitions) => Array.isArray(transitions) && transitions.every((t) => isTransition(t)), + (statusMap) => isRecord(statusMap) && Object.values(statusMap).every((t) => isTarget(t)), ); } @@ -101,11 +90,7 @@ export function parseWorkflowPayload(raw: unknown): WorkflowPayload | null { if (typeof raw.name !== "string" || typeof raw.description !== "string") { return null; } - if ( - !isStringRecord(raw.roles, isRoleDefinition) || - !isStringRecord(raw.conditions, isConditionDefinition) || - !isGraph(raw.graph) - ) { + if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) { return null; } return raw as WorkflowPayload; diff --git a/packages/workflow-moderator/__tests__/evaluate.test.ts b/packages/workflow-moderator/__tests__/evaluate.test.ts index dcb7656..63ee58d 100644 --- a/packages/workflow-moderator/__tests__/evaluate.test.ts +++ b/packages/workflow-moderator/__tests__/evaluate.test.ts @@ -1,312 +1,95 @@ import { describe, expect, test } from "bun:test"; -import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol"; +import type { Target, WorkflowPayload } from "@uncaged/workflow-protocol"; import { evaluate } from "../src/evaluate.js"; -const solveIssueWorkflow: WorkflowPayload = { - name: "solve-issue", - description: "End-to-end issue resolution", - roles: { - planner: { - description: "Creates implementation plan", - goal: "You are a planning agent.", - capabilities: ["planning"], - procedure: "Create a step-by-step plan.", - output: "Output the plan and steps.", - frontmatter: "5GWKR8TN1V3JA", - }, - developer: { - description: "Implements code changes", - goal: "You are a developer agent.", - capabilities: ["coding"], - procedure: "Implement the plan.", - output: "List files changed and summary.", - frontmatter: "8CNWT4KR6D1HV", - }, - reviewer: { - description: "Reviews code changes", - goal: "You are a code reviewer.", - capabilities: ["code-review"], - procedure: "Review the implementation.", - output: "Approve or reject with comments.", - frontmatter: "1VPBG9SM5E7WK", - }, +const solveIssueGraph: WorkflowPayload["graph"] = { + $START: { + _: { role: "planner", prompt: "Start planning from the issue in the task." }, }, - conditions: { - needsClarification: { - description: "Planner requests clarification from user", - expression: "$exists($last('planner').needsClarification)", - }, - rejected: { - description: "Reviewer rejected the implementation", - expression: "$last('reviewer').approved = false", - }, + planner: { + _: { role: "developer", prompt: "Implement the plan: {{plan}}" }, }, - graph: { - $START: [ - { - role: "planner", - condition: null, - prompt: "Start planning from the issue in the task.", - }, - ], - planner: [ - { - role: "developer", - condition: "needsClarification", - prompt: "Clarification is needed; hand off to developer.", - }, - { role: "$END", condition: null, prompt: "Planning complete; end workflow." }, - ], - developer: [ - { - role: "reviewer", - condition: null, - prompt: "Implementation done; send to reviewer.", - }, - ], - reviewer: [ - { - role: "developer", - condition: "rejected", - prompt: "Reviewer rejected; return to developer.", - }, - { role: "$END", condition: null, prompt: "Review passed; end workflow." }, - ], + developer: { + _: { role: "reviewer", prompt: "Review the changes: {{summary}}" }, + }, + reviewer: { + approved: { role: "$END", prompt: "Done." }, + rejected: { role: "developer", prompt: "Fix: {{comments}}" }, }, }; -function makeContext(steps: ModeratorContext["steps"]): ModeratorContext { - return { - start: { - workflow: "4KNM2PXR3B1QW", - prompt: "Fix the login bug", - }, - steps, - }; -} - describe("evaluate", () => { - test("$START → first role (fallback)", async () => { - const result = await evaluate(solveIssueWorkflow, makeContext([])); + test("$START → first role (unit status _)", () => { + const result = evaluate(solveIssueGraph, "$START", { status: "_" }); expect(result).toEqual({ ok: true, value: { role: "planner", prompt: "Start planning from the issue in the task." }, }); }); - test("condition match (rejected → developer)", async () => { - const context = makeContext([ - { - role: "reviewer", - output: { approved: false }, - detail: "2MXBG6PN4A8JR", - agent: "uwf-hermes", - }, - ]); - const result = await evaluate(solveIssueWorkflow, context); + test("status-based routing (reviewer rejected → developer)", () => { + const result = evaluate(solveIssueGraph, "reviewer", { + status: "rejected", + comments: "missing tests", + }); expect(result).toEqual({ ok: true, - value: { role: "developer", prompt: "Reviewer rejected; return to developer." }, + value: { role: "developer", prompt: "Fix: missing tests" }, }); }); - test("fallback when condition does not match → $END", async () => { - const context = makeContext([ - { - role: "reviewer", - output: { approved: true }, - detail: "2MXBG6PN4A8JR", - agent: "uwf-hermes", - }, - ]); - const result = await evaluate(solveIssueWorkflow, context); + test("status-based routing (reviewer approved → $END)", () => { + const result = evaluate(solveIssueGraph, "reviewer", { status: "approved" }); expect(result).toEqual({ ok: true, - value: { role: "$END", prompt: "Review passed; end workflow." }, + value: { role: "$END", prompt: "Done." }, }); }); - test("missing role in graph → error", async () => { - const context = makeContext([ - { - role: "unknown-role", - output: {}, - detail: "2MXBG6PN4A8JR", - agent: "uwf-hermes", - }, - ]); - const result = await evaluate(solveIssueWorkflow, context); + test("missing role in graph → error", () => { + const result = evaluate(solveIssueGraph, "unknown-role", { status: "_" }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.message).toBe('no transitions defined for role "unknown-role"'); } }); - test("output expansion in context works with JSONata", async () => { - const context = makeContext([ - { - role: "planner", - output: { needsClarification: true }, - detail: "7BQST3VW9F2MA", - agent: "uwf-hermes", - }, - ]); - const result = await evaluate(solveIssueWorkflow, context); + test("missing status in graph → error", () => { + const result = evaluate(solveIssueGraph, "reviewer", { status: "pending" }); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.message).toBe('no transition for role "reviewer" with status "pending"'); + } + }); + + test("mustache template rendering with simple fields", () => { + const result = evaluate(solveIssueGraph, "planner", { + status: "_", + plan: "Add auth middleware", + }); expect(result).toEqual({ ok: true, - value: { role: "developer", prompt: "Clarification is needed; hand off to developer." }, + value: { role: "developer", prompt: "Implement the plan: Add auth middleware" }, }); }); - test("$last returns most recent matching role's frontmatter", async () => { - const workflow: WorkflowPayload = { - ...solveIssueWorkflow, - conditions: { - devFailed: { - description: "Developer failed", - expression: "$last('developer').status = 'failed'", + test("mustache template with nested object paths", () => { + const graph: Record> = { + reviewer: { + _: { + role: "developer", + prompt: "Address: {{review.comments}}", }, }, - graph: { - $START: [ - { - role: "developer", - condition: null, - prompt: "Begin development.", - }, - ], - developer: [ - { role: "$END", condition: "devFailed", prompt: "Development failed; end." }, - { - role: "reviewer", - condition: null, - prompt: "Development succeeded; review.", - }, - ], - }, }; - const context = makeContext([ - { - role: "developer", - output: { status: "done" }, - detail: "1VPBG9SM5E7WK", - agent: "uwf-hermes", - }, - { - role: "reviewer", - output: { approved: false }, - detail: "2MXBG6PN4A8JR", - agent: "uwf-hermes", - }, - { - role: "developer", - output: { status: "failed" }, - detail: "3QNTH7WK8D2PA", - agent: "uwf-hermes", - }, - ]); - const result = await evaluate(workflow, context); - expect(result).toEqual({ - ok: true, - value: { role: "$END", prompt: "Development failed; end." }, + const result = evaluate(graph, "reviewer", { + status: "_", + review: { comments: "refactor the handler" }, }); - }); - - test("$first returns earliest matching role's frontmatter", async () => { - const workflow: WorkflowPayload = { - ...solveIssueWorkflow, - conditions: { - firstPlanReady: { - description: "First planner run was ready", - expression: "$first('planner').status = 'ready'", - }, - }, - graph: { - $START: [ - { - role: "planner", - condition: null, - prompt: "Begin planning.", - }, - ], - planner: [ - { role: "$END", condition: "firstPlanReady", prompt: "First plan was ready; end." }, - { - role: "developer", - condition: null, - prompt: "Plan not ready on first pass; implement.", - }, - ], - }, - }; - const context = makeContext([ - { - role: "planner", - output: { status: "ready", plan: "ABC123" }, - detail: "7BQST3VW9F2MA", - agent: "uwf-hermes", - }, - { - role: "developer", - output: { status: "done" }, - detail: "1VPBG9SM5E7WK", - agent: "uwf-hermes", - }, - { - role: "planner", - output: { status: "revised", plan: "DEF456" }, - detail: "4RNMK6PX8B3WQ", - agent: "uwf-hermes", - }, - ]); - const result = await evaluate(workflow, context); expect(result).toEqual({ ok: true, - value: { role: "$END", prompt: "First plan was ready; end." }, - }); - }); - - test("$last returns undefined for unmatched role", async () => { - const workflow: WorkflowPayload = { - ...solveIssueWorkflow, - conditions: { - hasReviewer: { - description: "Reviewer has run", - expression: "$exists($last('reviewer'))", - }, - }, - graph: { - $START: [ - { - role: "planner", - condition: null, - prompt: "Begin planning.", - }, - ], - planner: [ - { role: "$END", condition: "hasReviewer", prompt: "Reviewer already ran; end." }, - { - role: "developer", - condition: null, - prompt: "No reviewer yet; implement.", - }, - ], - }, - }; - const context = makeContext([ - { - role: "planner", - output: { status: "ready" }, - detail: "7BQST3VW9F2MA", - agent: "uwf-hermes", - }, - ]); - const result = await evaluate(workflow, context); - // no reviewer step → $exists returns false → fallback to developer - expect(result).toEqual({ - ok: true, - value: { role: "developer", prompt: "No reviewer yet; implement." }, + value: { role: "developer", prompt: "Address: refactor the handler" }, }); }); }); diff --git a/packages/workflow-moderator/package.json b/packages/workflow-moderator/package.json index db8b99f..270f5d0 100644 --- a/packages/workflow-moderator/package.json +++ b/packages/workflow-moderator/package.json @@ -19,9 +19,10 @@ }, "dependencies": { "@uncaged/workflow-protocol": "workspace:^", - "jsonata": "^1.8.7" + "mustache": "^4.2.0" }, "devDependencies": { + "@types/mustache": "^4.2.6", "typescript": "^5.8.3" }, "publishConfig": { diff --git a/packages/workflow-moderator/src/evaluate.ts b/packages/workflow-moderator/src/evaluate.ts index a515cd0..108a1dc 100644 --- a/packages/workflow-moderator/src/evaluate.ts +++ b/packages/workflow-moderator/src/evaluate.ts @@ -1,65 +1,39 @@ -import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol"; -import jsonata from "jsonata"; +import type { Target } from "@uncaged/workflow-protocol"; +import mustache from "mustache"; import type { EvaluateResult, Result } from "./types.js"; const START_ROLE = "$START"; +const UNIT_STATUS = "_"; -function isTruthy(value: unknown): boolean { - if (value === null || value === undefined) { - return false; - } - if (typeof value === "boolean") { - return value; - } - if (typeof value === "number") { - return value !== 0 && !Number.isNaN(value); - } - if (typeof value === "string") { - return value.length > 0; - } - return true; -} +type LastOutput = Record & { status: string }; -function findByRole( - steps: ModeratorContext["steps"], - role: string, - direction: "first" | "last", -): unknown { - if (direction === "last") { - for (let i = steps.length - 1; i >= 0; i--) { - if (steps[i].role === role) { - return steps[i].output; - } - } - } else { - for (const step of steps) { - if (step.role === role) { - return step.output; - } - } - } - return undefined; -} +export function evaluate( + graph: Record>, + lastRole: string, + lastOutput: LastOutput, +): Result { + const status = lastRole === START_ROLE ? UNIT_STATUS : lastOutput.status; + + const roleTargets = graph[lastRole]; + if (roleTargets === undefined) { + return { + ok: false, + error: new Error(`no transitions defined for role "${lastRole}"`), + }; + } + + const target = roleTargets[status]; + if (target === undefined) { + return { + ok: false, + error: new Error(`no transition for role "${lastRole}" with status "${status}"`), + }; + } -async function evaluateJsonata( - expression: string, - context: ModeratorContext, -): Promise> { try { - const expr = jsonata(expression); - expr.registerFunction( - "first", - (role: string) => findByRole(context.steps, role, "first"), - "", - ); - expr.registerFunction( - "last", - (role: string) => findByRole(context.steps, role, "last"), - "", - ); - const result = await expr.evaluate(context); - return { ok: true, value: result }; + const prompt = mustache.render(target.prompt, lastOutput); + return { ok: true, value: { role: target.role, prompt } }; } catch (error) { return { ok: false, @@ -67,51 +41,3 @@ async function evaluateJsonata( }; } } - -function currentRole(context: ModeratorContext): string { - if (context.steps.length === 0) { - return START_ROLE; - } - return context.steps[context.steps.length - 1].role; -} - -export async function evaluate( - workflow: WorkflowPayload, - context: ModeratorContext, -): Promise> { - const role = currentRole(context); - const transitions = workflow.graph[role]; - if (transitions === undefined) { - return { - ok: false, - error: new Error(`no transitions defined for role "${role}"`), - }; - } - - for (const transition of transitions) { - if (transition.condition === null) { - return { ok: true, value: { role: transition.role, prompt: transition.prompt } }; - } - - const conditionDef = workflow.conditions[transition.condition]; - if (conditionDef === undefined) { - return { - ok: false, - error: new Error(`unknown condition "${transition.condition}"`), - }; - } - - const evalResult = await evaluateJsonata(conditionDef.expression, context); - if (!evalResult.ok) { - return evalResult; - } - if (isTruthy(evalResult.value)) { - return { ok: true, value: { role: transition.role, prompt: transition.prompt } }; - } - } - - return { - ok: false, - error: new Error(`no transition matched for role "${role}"`), - }; -} diff --git a/packages/workflow-protocol/src/index.ts b/packages/workflow-protocol/src/index.ts index 99a11f3..3fe7f2d 100644 --- a/packages/workflow-protocol/src/index.ts +++ b/packages/workflow-protocol/src/index.ts @@ -7,7 +7,6 @@ export type { AgentAlias, AgentConfig, CasRef, - ConditionDefinition, ModelAlias, ModelConfig, ModeratorContext, @@ -26,12 +25,12 @@ export type { StepNodePayload, StepOutput, StepRecord, + Target, ThreadForkOutput, ThreadId, ThreadListItem, ThreadStepsOutput, ThreadsIndex, - Transition, WorkflowConfig, WorkflowName, WorkflowPayload, diff --git a/packages/workflow-protocol/src/schemas.ts b/packages/workflow-protocol/src/schemas.ts index d0b36b3..77aed81 100644 --- a/packages/workflow-protocol/src/schemas.ts +++ b/packages/workflow-protocol/src/schemas.ts @@ -14,22 +14,11 @@ const ROLE_DEFINITION: JSONSchema = { additionalProperties: false, }; -const CONDITION_DEFINITION: JSONSchema = { +const TARGET: JSONSchema = { type: "object", - required: ["description", "expression"], - properties: { - description: { type: "string" }, - expression: { type: "string" }, - }, - additionalProperties: false, -}; - -const TRANSITION: JSONSchema = { - type: "object", - required: ["role", "condition", "prompt"], + required: ["role", "prompt"], properties: { role: { type: "string" }, - condition: { anyOf: [{ type: "string" }, { type: "null" }] }, prompt: { type: "string" }, }, additionalProperties: false, @@ -38,7 +27,7 @@ const TRANSITION: JSONSchema = { export const WORKFLOW_SCHEMA: JSONSchema = { title: "Workflow", type: "object", - required: ["name", "description", "roles", "conditions", "graph"], + required: ["name", "description", "roles", "graph"], properties: { name: { type: "string" }, description: { type: "string" }, @@ -46,15 +35,11 @@ export const WORKFLOW_SCHEMA: JSONSchema = { type: "object", additionalProperties: ROLE_DEFINITION, }, - conditions: { - type: "object", - additionalProperties: CONDITION_DEFINITION, - }, graph: { type: "object", additionalProperties: { - type: "array", - items: TRANSITION, + type: "object", + additionalProperties: TARGET, }, }, }, diff --git a/packages/workflow-protocol/src/types.ts b/packages/workflow-protocol/src/types.ts index 60ee3b6..28252db 100644 --- a/packages/workflow-protocol/src/types.ts +++ b/packages/workflow-protocol/src/types.ts @@ -27,23 +27,16 @@ export type RoleDefinition = { frontmatter: CasRef; }; -export type Transition = { +export type Target = { role: string; - condition: string | null; prompt: string; }; -export type ConditionDefinition = { - description: string; - expression: string; -}; - export type WorkflowPayload = { name: string; description: string; roles: Record; - conditions: Record; - graph: Record; + graph: Record>; }; // ── 4.3 Thread 节点 ─────────────────────────────────────────────────