refactor: status-based graph routing + mustache prompt templates

- Delete ConditionDefinition, Transition types from workflow-protocol
- Add Target type, change graph to Record<string, Record<string, Target>>
- Remove conditions from WorkflowPayload and WORKFLOW_SCHEMA
- Replace jsonata with mustache in workflow-moderator
- Rewrite evaluate() to simple map lookup + mustache render
- Update cli-workflow to use new 3-arg evaluate(graph, role, output)
- 296 tests pass, 0 fail

Phase 1 of #490 (closes #491)
This commit is contained in:
2026-05-25 04:39:11 +00:00
committed by xiaomo
parent ff959be3ef
commit d00f9df2dd
11 changed files with 142 additions and 459 deletions
@@ -70,7 +70,6 @@ describe("solve-issue workflow: tea pr create worktree fix", () => {
// Basic structure validation // Basic structure validation
expect(workflow.name).toBe("solve-issue"); expect(workflow.name).toBe("solve-issue");
expect(workflow.roles).toBeDefined(); expect(workflow.roles).toBeDefined();
expect(workflow.conditions).toBeDefined();
expect(workflow.graph).toBeDefined(); expect(workflow.graph).toBeDefined();
// Verify committer role exists with required fields // Verify committer role exists with required fields
@@ -25,7 +25,6 @@ async function storeWorkflow(uwf: UwfStore, name: string): Promise<CasRef> {
name, name,
description: "Test workflow", description: "Test workflow",
roles: {}, roles: {},
conditions: {},
graph: {}, graph: {},
}; };
return await uwf.store.put(uwf.schemas.workflow, payload); return await uwf.store.put(uwf.schemas.workflow, payload);
@@ -36,7 +35,6 @@ async function createWorkflowYaml(name: string, version: string | null = null):
name, name,
description: version !== null ? `Test workflow (${version})` : "Test workflow", description: version !== null ? `Test workflow (${version})` : "Test workflow",
roles: {}, roles: {},
conditions: {},
graph: {}, graph: {},
}; };
const yaml = stringify(payload); 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 () => { test("should fail on valid YAML with invalid WorkflowPayload shape", async () => {
await makeUwfStore(storageRoot); await makeUwfStore(storageRoot);
const yamlPath = join(tmpDir, "invalid-workflow.yaml"); 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(); await expect(cmdThreadStart(storageRoot, yamlPath, "prompt", projectRoot)).rejects.toThrow();
}); });
+34 -17
View File
@@ -8,10 +8,8 @@ import type {
AgentAlias, AgentAlias,
AgentConfig, AgentConfig,
CasRef, CasRef,
ModeratorContext,
StartNodePayload, StartNodePayload,
StartOutput, StartOutput,
StepContext,
StepNodePayload, StepNodePayload,
StepOutput, StepOutput,
ThreadId, ThreadId,
@@ -53,6 +51,7 @@ import {
import { materializeWorkflowPayload } from "./workflow.js"; import { materializeWorkflowPayload } from "./workflow.js";
const END_ROLE = "$END"; const END_ROLE = "$END";
const START_ROLE = "$START";
export const THREAD_READ_DEFAULT_QUOTA = 4000; export const THREAD_READ_DEFAULT_QUOTA = 4000;
const PL_THREAD_START = "7HNQ4B2X"; const PL_THREAD_START = "7HNQ4B2X";
@@ -670,17 +669,32 @@ function formatThreadReadMarkdown(options: {
return parts.join("\n\n---\n\n"); return parts.join("\n\n---\n\n");
} }
function buildModeratorContext(uwf: UwfStore, chain: ChainState): ModeratorContext { type EvaluateLastOutput = Record<string, unknown> & { status: string };
const chronological = [...chain.stepsNewestFirst].reverse();
const steps: StepContext[] = chronological.map((step) => ({ function resolveEvaluateArgs(
role: step.role, uwf: UwfStore,
output: expandOutput(uwf, step.output), chain: ChainState,
detail: step.detail, ): { lastRole: string; lastOutput: EvaluateLastOutput } {
agent: step.agent, if (chain.headIsStart) {
edgePrompt: step.edgePrompt ?? "", return { lastRole: START_ROLE, lastOutput: { status: "_" } };
content: null, // Moderator doesn't need content }
}));
return { start: chain.start, steps }; 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<string, unknown>)
: {};
const status = typeof base.status === "string" ? base.status : "_";
return {
lastRole: lastStep.role,
lastOutput: { ...base, status },
};
} }
function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayload { function loadWorkflowPayload(uwf: UwfStore, workflowRef: CasRef): WorkflowPayload {
@@ -924,9 +938,9 @@ async function cmdThreadStepOnce(
const chain = walkChain(uwf, headHash); const chain = walkChain(uwf, headHash);
const workflowHash = chain.start.workflow; const workflowHash = chain.start.workflow;
const workflow = loadWorkflowPayload(uwf, workflowHash); 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) { if (!nextResult.ok) {
failStep(plog, `moderator evaluate failed: ${nextResult.error.message}`); failStep(plog, `moderator evaluate failed: ${nextResult.error.message}`);
} }
@@ -976,8 +990,11 @@ async function cmdThreadStepOnce(
await saveThreadsIndex(storageRoot, freshIndex); await saveThreadsIndex(storageRoot, freshIndex);
const chainAfter = walkChain(uwfAfter, newHead); const chainAfter = walkChain(uwfAfter, newHead);
const contextAfter = buildModeratorContext(uwfAfter, chainAfter); const { lastRole: lastRoleAfter, lastOutput: lastOutputAfter } = resolveEvaluateArgs(
const afterResult = await evaluate(workflow, contextAfter); uwfAfter,
chainAfter,
);
const afterResult = evaluate(workflow.graph, lastRoleAfter, lastOutputAfter);
if (!afterResult.ok) { if (!afterResult.ok) {
failStep(plog, `post-step moderator evaluate failed: ${afterResult.error.message}`); failStep(plog, `post-step moderator evaluate failed: ${afterResult.error.message}`);
} }
+16 -19
View File
@@ -2,12 +2,7 @@ import { readFile } from "node:fs/promises";
import type { JSONSchema } from "@uncaged/json-cas"; import type { JSONSchema } from "@uncaged/json-cas";
import { putSchema, validate } from "@uncaged/json-cas"; import { putSchema, validate } from "@uncaged/json-cas";
import type { import type { CasRef, RoleDefinition, Target, WorkflowPayload } from "@uncaged/workflow-protocol";
CasRef,
RoleDefinition,
Transition,
WorkflowPayload,
} from "@uncaged/workflow-protocol";
import { parse } from "yaml"; import { parse } from "yaml";
import { import {
@@ -51,20 +46,23 @@ function isJsonSchema(value: unknown): value is JSONSchema {
return typeof value === "object" && value !== null && !Array.isArray(value); return typeof value === "object" && value !== null && !Array.isArray(value);
} }
/** Normalize graph transitions: ensure condition is null (not undefined) for fallback entries. */ /** Normalize graph: validate each status → target mapping. */
function normalizeGraph(graph: Record<string, Transition[]>): Record<string, Transition[]> { function normalizeGraph(
const result: Record<string, Transition[]> = {}; graph: Record<string, Record<string, Target>>,
for (const [node, transitions] of Object.entries(graph)) { ): Record<string, Record<string, Target>> {
result[node] = transitions.map((t) => { const result: Record<string, Record<string, Target>> = {};
if (typeof t.prompt !== "string" || t.prompt.trim() === "") { for (const [node, statusMap] of Object.entries(graph)) {
fail(`graph[${node}] transition to "${t.role}": prompt is required (non-empty string)`); const normalized: Record<string, Target> = {};
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 { normalized[status] = {
role: t.role, role: target.role,
condition: t.condition ?? null, prompt: target.prompt,
prompt: t.prompt,
}; };
}); }
result[node] = normalized;
} }
return result; return result;
} }
@@ -106,7 +104,6 @@ export async function materializeWorkflowPayload(
name: raw.name, name: raw.name,
description: raw.description, description: raw.description,
roles, roles,
conditions: raw.conditions,
graph: normalizeGraph(raw.graph), graph: normalizeGraph(raw.graph),
}; };
} }
+4 -19
View File
@@ -30,23 +30,12 @@ function isRoleDefinition(value: unknown): boolean {
); );
} }
function isConditionDefinition(value: unknown): boolean { function isTarget(value: unknown): boolean {
if (!isRecord(value)) { if (!isRecord(value)) {
return false; 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 ( return (
typeof value.role === "string" && typeof value.role === "string" && typeof value.prompt === "string" && value.prompt.trim() !== ""
typeof value.prompt === "string" &&
value.prompt.trim() !== "" &&
(condition === null || condition === undefined || typeof condition === "string")
); );
} }
@@ -62,7 +51,7 @@ function isGraph(value: unknown): boolean {
return false; return false;
} }
return Object.values(value).every( 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") { if (typeof raw.name !== "string" || typeof raw.description !== "string") {
return null; return null;
} }
if ( if (!isStringRecord(raw.roles, isRoleDefinition) || !isGraph(raw.graph)) {
!isStringRecord(raw.roles, isRoleDefinition) ||
!isStringRecord(raw.conditions, isConditionDefinition) ||
!isGraph(raw.graph)
) {
return null; return null;
} }
return raw as WorkflowPayload; return raw as WorkflowPayload;
@@ -1,312 +1,95 @@
import { describe, expect, test } from "bun:test"; 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"; import { evaluate } from "../src/evaluate.js";
const solveIssueWorkflow: WorkflowPayload = { const solveIssueGraph: WorkflowPayload["graph"] = {
name: "solve-issue", $START: {
description: "End-to-end issue resolution", _: { role: "planner", prompt: "Start planning from the issue in the task." },
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",
},
}, },
conditions: { planner: {
needsClarification: { _: { role: "developer", prompt: "Implement the plan: {{plan}}" },
description: "Planner requests clarification from user",
expression: "$exists($last('planner').needsClarification)",
},
rejected: {
description: "Reviewer rejected the implementation",
expression: "$last('reviewer').approved = false",
},
}, },
graph: { developer: {
$START: [ _: { role: "reviewer", prompt: "Review the changes: {{summary}}" },
{ },
role: "planner", reviewer: {
condition: null, approved: { role: "$END", prompt: "Done." },
prompt: "Start planning from the issue in the task.", rejected: { role: "developer", prompt: "Fix: {{comments}}" },
},
],
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." },
],
}, },
}; };
function makeContext(steps: ModeratorContext["steps"]): ModeratorContext {
return {
start: {
workflow: "4KNM2PXR3B1QW",
prompt: "Fix the login bug",
},
steps,
};
}
describe("evaluate", () => { describe("evaluate", () => {
test("$START → first role (fallback)", async () => { test("$START → first role (unit status _)", () => {
const result = await evaluate(solveIssueWorkflow, makeContext([])); const result = evaluate(solveIssueGraph, "$START", { status: "_" });
expect(result).toEqual({ expect(result).toEqual({
ok: true, ok: true,
value: { role: "planner", prompt: "Start planning from the issue in the task." }, value: { role: "planner", prompt: "Start planning from the issue in the task." },
}); });
}); });
test("condition match (rejected → developer)", async () => { test("status-based routing (reviewer rejected → developer)", () => {
const context = makeContext([ const result = evaluate(solveIssueGraph, "reviewer", {
{ status: "rejected",
role: "reviewer", comments: "missing tests",
output: { approved: false }, });
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ expect(result).toEqual({
ok: true, 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 () => { test("status-based routing (reviewer approved → $END)", () => {
const context = makeContext([ const result = evaluate(solveIssueGraph, "reviewer", { status: "approved" });
{
role: "reviewer",
output: { approved: true },
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result).toEqual({ expect(result).toEqual({
ok: true, ok: true,
value: { role: "$END", prompt: "Review passed; end workflow." }, value: { role: "$END", prompt: "Done." },
}); });
}); });
test("missing role in graph → error", async () => { test("missing role in graph → error", () => {
const context = makeContext([ const result = evaluate(solveIssueGraph, "unknown-role", { status: "_" });
{
role: "unknown-role",
output: {},
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
},
]);
const result = await evaluate(solveIssueWorkflow, context);
expect(result.ok).toBe(false); expect(result.ok).toBe(false);
if (!result.ok) { if (!result.ok) {
expect(result.error.message).toBe('no transitions defined for role "unknown-role"'); expect(result.error.message).toBe('no transitions defined for role "unknown-role"');
} }
}); });
test("output expansion in context works with JSONata", async () => { test("missing status in graph → error", () => {
const context = makeContext([ const result = evaluate(solveIssueGraph, "reviewer", { status: "pending" });
{ expect(result.ok).toBe(false);
role: "planner", if (!result.ok) {
output: { needsClarification: true }, expect(result.error.message).toBe('no transition for role "reviewer" with status "pending"');
detail: "7BQST3VW9F2MA", }
agent: "uwf-hermes", });
},
]); test("mustache template rendering with simple fields", () => {
const result = await evaluate(solveIssueWorkflow, context); const result = evaluate(solveIssueGraph, "planner", {
status: "_",
plan: "Add auth middleware",
});
expect(result).toEqual({ expect(result).toEqual({
ok: true, 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 () => { test("mustache template with nested object paths", () => {
const workflow: WorkflowPayload = { const graph: Record<string, Record<string, Target>> = {
...solveIssueWorkflow, reviewer: {
conditions: { _: {
devFailed: { role: "developer",
description: "Developer failed", prompt: "Address: {{review.comments}}",
expression: "$last('developer').status = 'failed'",
}, },
}, },
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([ const result = evaluate(graph, "reviewer", {
{ status: "_",
role: "developer", review: { comments: "refactor the handler" },
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." },
}); });
});
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({ expect(result).toEqual({
ok: true, ok: true,
value: { role: "$END", prompt: "First plan was ready; end." }, value: { role: "developer", prompt: "Address: refactor the handler" },
});
});
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." },
}); });
}); });
}); });
+2 -1
View File
@@ -19,9 +19,10 @@
}, },
"dependencies": { "dependencies": {
"@uncaged/workflow-protocol": "workspace:^", "@uncaged/workflow-protocol": "workspace:^",
"jsonata": "^1.8.7" "mustache": "^4.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/mustache": "^4.2.6",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"publishConfig": { "publishConfig": {
+28 -102
View File
@@ -1,65 +1,39 @@
import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol"; import type { Target } from "@uncaged/workflow-protocol";
import jsonata from "jsonata"; import mustache from "mustache";
import type { EvaluateResult, Result } from "./types.js"; import type { EvaluateResult, Result } from "./types.js";
const START_ROLE = "$START"; const START_ROLE = "$START";
const UNIT_STATUS = "_";
function isTruthy(value: unknown): boolean { type LastOutput = Record<string, unknown> & { status: string };
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;
}
function findByRole( export function evaluate(
steps: ModeratorContext["steps"], graph: Record<string, Record<string, Target>>,
role: string, lastRole: string,
direction: "first" | "last", lastOutput: LastOutput,
): unknown { ): Result<EvaluateResult, Error> {
if (direction === "last") { const status = lastRole === START_ROLE ? UNIT_STATUS : lastOutput.status;
for (let i = steps.length - 1; i >= 0; i--) {
if (steps[i].role === role) { const roleTargets = graph[lastRole];
return steps[i].output; if (roleTargets === undefined) {
} return {
} ok: false,
} else { error: new Error(`no transitions defined for role "${lastRole}"`),
for (const step of steps) { };
if (step.role === role) { }
return step.output;
} const target = roleTargets[status];
} if (target === undefined) {
} return {
return undefined; ok: false,
} error: new Error(`no transition for role "${lastRole}" with status "${status}"`),
};
}
async function evaluateJsonata(
expression: string,
context: ModeratorContext,
): Promise<Result<unknown, Error>> {
try { try {
const expr = jsonata(expression); const prompt = mustache.render(target.prompt, lastOutput);
expr.registerFunction( return { ok: true, value: { role: target.role, prompt } };
"first",
(role: string) => findByRole(context.steps, role, "first"),
"<s:x>",
);
expr.registerFunction(
"last",
(role: string) => findByRole(context.steps, role, "last"),
"<s:x>",
);
const result = await expr.evaluate(context);
return { ok: true, value: result };
} catch (error) { } catch (error) {
return { return {
ok: false, 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<Result<EvaluateResult, Error>> {
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}"`),
};
}
+1 -2
View File
@@ -7,7 +7,6 @@ export type {
AgentAlias, AgentAlias,
AgentConfig, AgentConfig,
CasRef, CasRef,
ConditionDefinition,
ModelAlias, ModelAlias,
ModelConfig, ModelConfig,
ModeratorContext, ModeratorContext,
@@ -26,12 +25,12 @@ export type {
StepNodePayload, StepNodePayload,
StepOutput, StepOutput,
StepRecord, StepRecord,
Target,
ThreadForkOutput, ThreadForkOutput,
ThreadId, ThreadId,
ThreadListItem, ThreadListItem,
ThreadStepsOutput, ThreadStepsOutput,
ThreadsIndex, ThreadsIndex,
Transition,
WorkflowConfig, WorkflowConfig,
WorkflowName, WorkflowName,
WorkflowPayload, WorkflowPayload,
+5 -20
View File
@@ -14,22 +14,11 @@ const ROLE_DEFINITION: JSONSchema = {
additionalProperties: false, additionalProperties: false,
}; };
const CONDITION_DEFINITION: JSONSchema = { const TARGET: JSONSchema = {
type: "object", type: "object",
required: ["description", "expression"], required: ["role", "prompt"],
properties: {
description: { type: "string" },
expression: { type: "string" },
},
additionalProperties: false,
};
const TRANSITION: JSONSchema = {
type: "object",
required: ["role", "condition", "prompt"],
properties: { properties: {
role: { type: "string" }, role: { type: "string" },
condition: { anyOf: [{ type: "string" }, { type: "null" }] },
prompt: { type: "string" }, prompt: { type: "string" },
}, },
additionalProperties: false, additionalProperties: false,
@@ -38,7 +27,7 @@ const TRANSITION: JSONSchema = {
export const WORKFLOW_SCHEMA: JSONSchema = { export const WORKFLOW_SCHEMA: JSONSchema = {
title: "Workflow", title: "Workflow",
type: "object", type: "object",
required: ["name", "description", "roles", "conditions", "graph"], required: ["name", "description", "roles", "graph"],
properties: { properties: {
name: { type: "string" }, name: { type: "string" },
description: { type: "string" }, description: { type: "string" },
@@ -46,15 +35,11 @@ export const WORKFLOW_SCHEMA: JSONSchema = {
type: "object", type: "object",
additionalProperties: ROLE_DEFINITION, additionalProperties: ROLE_DEFINITION,
}, },
conditions: {
type: "object",
additionalProperties: CONDITION_DEFINITION,
},
graph: { graph: {
type: "object", type: "object",
additionalProperties: { additionalProperties: {
type: "array", type: "object",
items: TRANSITION, additionalProperties: TARGET,
}, },
}, },
}, },
+2 -9
View File
@@ -27,23 +27,16 @@ export type RoleDefinition = {
frontmatter: CasRef; frontmatter: CasRef;
}; };
export type Transition = { export type Target = {
role: string; role: string;
condition: string | null;
prompt: string; prompt: string;
}; };
export type ConditionDefinition = {
description: string;
expression: string;
};
export type WorkflowPayload = { export type WorkflowPayload = {
name: string; name: string;
description: string; description: string;
roles: Record<string, RoleDefinition>; roles: Record<string, RoleDefinition>;
conditions: Record<string, ConditionDefinition>; graph: Record<string, Record<string, Target>>;
graph: Record<string, Transition[]>;
}; };
// ── 4.3 Thread 节点 ───────────────────────────────────────────────── // ── 4.3 Thread 节点 ─────────────────────────────────────────────────