From 5a7f417899bb6e766d175d868b4409a23a3dfcb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Mon, 25 May 2026 04:52:53 +0000 Subject: [PATCH] feat: migrate examples to status-based routing + fix mustache HTML escape - Migrate solve-issue.yaml, analyze-topic.yaml, debate.yaml to new format - Add status enum field to all role frontmatter schemas - Use {{{ }}} (triple mustache) for prompt templates with user content - Disable mustache HTML escaping globally (prompts are plain text, not HTML) - Add 2 new tests for HTML escape behavior - 9 moderator tests pass Phase 2 of #490 (closes #492) --- examples/analyze-topic.yaml | 13 +++--- examples/debate.yaml | 45 +++++++------------ examples/solve-issue.yaml | 38 ++++++---------- .../__tests__/evaluate.test.ts | 27 +++++++++++ packages/workflow-moderator/src/evaluate.ts | 3 ++ 5 files changed, 64 insertions(+), 62 deletions(-) diff --git a/examples/analyze-topic.yaml b/examples/analyze-topic.yaml index 118881a..abedb78 100644 --- a/examples/analyze-topic.yaml +++ b/examples/analyze-topic.yaml @@ -22,6 +22,8 @@ roles: frontmatter: type: object properties: + status: + enum: ["_"] thesis: type: string keyPoints: @@ -30,14 +32,9 @@ roles: type: string caveats: type: string - required: [thesis, keyPoints] -conditions: {} + required: [status, thesis, keyPoints] graph: $START: - - role: "analyst" - condition: null - prompt: "Analyze the topic in the task and produce a structured summary with key points." + _: { role: "analyst", prompt: "Analyze the topic in the task and produce a structured summary with key points." } analyst: - - role: "$END" - condition: null - prompt: "Analysis complete. Finish the workflow." + _: { role: "$END", prompt: "Analysis complete. Finish the workflow." } diff --git a/examples/debate.yaml b/examples/debate.yaml index f81f321..ce5b529 100644 --- a/examples/debate.yaml +++ b/examples/debate.yaml @@ -16,15 +16,16 @@ roles: 3. If you find yourself genuinely convinced by the other side, you may concede. output: | Provide your argument in the frontmatter. - Set conceded to true ONLY if you are genuinely convinced and wish to stop debating. + Set status to "conceded" ONLY if you are genuinely convinced and wish to stop debating. + Otherwise set status to "continue". frontmatter: type: object properties: + status: + enum: ["continue", "conceded"] argument: type: string - conceded: - type: boolean - required: [argument, conceded] + required: [status, argument] for: description: "Argues for the proposition" goal: | @@ -40,38 +41,22 @@ roles: 3. If you find yourself genuinely convinced by the other side, you may concede. output: | Provide your argument in the frontmatter. - Set conceded to true ONLY if you are genuinely convinced and wish to stop debating. + Set status to "conceded" ONLY if you are genuinely convinced and wish to stop debating. + Otherwise set status to "continue". frontmatter: type: object properties: + status: + enum: ["continue", "conceded"] argument: type: string - conceded: - type: boolean - required: [argument, conceded] -conditions: - againstConceded: - description: "The against side conceded" - expression: "$last('against').conceded = true" - forConceded: - description: "The for side conceded" - expression: "$last('for').conceded = true" + required: [status, argument] graph: $START: - - role: "against" - condition: null - prompt: "Present your opening argument against the proposition." + _: { role: "against", prompt: "Present your opening argument against the proposition." } against: - - role: "$END" - condition: "againstConceded" - prompt: "The against side conceded. Debate over." - - role: "for" - condition: null - prompt: "Counter the opposing argument. Address their points directly." + conceded: { role: "$END", prompt: "The against side conceded. Debate over." } + continue: { role: "for", prompt: "Counter the opposing argument: {{{argument}}}" } for: - - role: "$END" - condition: "forConceded" - prompt: "The for side conceded. Debate over." - - role: "against" - condition: null - prompt: "Counter the opposing argument. Address their points directly." + conceded: { role: "$END", prompt: "The for side conceded. Debate over." } + continue: { role: "against", prompt: "Counter the opposing argument: {{{argument}}}" } diff --git a/examples/solve-issue.yaml b/examples/solve-issue.yaml index b5b710e..8e6c8ef 100644 --- a/examples/solve-issue.yaml +++ b/examples/solve-issue.yaml @@ -27,11 +27,13 @@ roles: frontmatter: type: object properties: + status: + enum: ["_"] repoPath: type: string plan: type: string - required: [repoPath, plan] + required: [status, repoPath, plan] developer: description: "Implements code changes" goal: "You are a developer agent. You implement code changes according to plans." @@ -50,13 +52,15 @@ roles: frontmatter: type: object properties: + status: + enum: ["_"] filesChanged: type: array items: type: string summary: type: string - required: [filesChanged, summary] + required: [status, filesChanged, summary] reviewer: description: "Reviews code changes" goal: "You are a code reviewer. You review implementations for correctness and quality." @@ -71,32 +75,18 @@ roles: frontmatter: type: object properties: - approved: - type: boolean + status: + enum: ["approved", "rejected"] comments: type: string - required: [approved, comments] -conditions: - notApproved: - description: "Reviewer rejected the implementation" - expression: "$last('reviewer').approved = false" + required: [status, comments] graph: $START: - - role: "planner" - condition: null - prompt: "Analyze the issue described in the task and produce a detailed implementation plan." + _: { role: "planner", prompt: "Analyze the issue described in the task and produce a detailed implementation plan." } planner: - - role: "developer" - condition: null - prompt: "Implement the plan from the planner. Write code, tests, and ensure existing tests pass." + _: { role: "developer", prompt: "Implement the plan from the planner. Write code, tests, and ensure existing tests pass." } developer: - - role: "reviewer" - condition: null - prompt: "Review the developer's implementation against the plan for correctness and quality." + _: { role: "reviewer", prompt: "Review the developer's implementation against the plan for correctness and quality." } reviewer: - - role: "developer" - condition: "notApproved" - prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues." - - role: "$END" - condition: null - prompt: "The review passed. Complete the workflow." + approved: { role: "$END", prompt: "The review passed. Complete the workflow." } + rejected: { role: "developer", prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues: {{{comments}}}" } diff --git a/packages/workflow-moderator/__tests__/evaluate.test.ts b/packages/workflow-moderator/__tests__/evaluate.test.ts index 63ee58d..bf0800c 100644 --- a/packages/workflow-moderator/__tests__/evaluate.test.ts +++ b/packages/workflow-moderator/__tests__/evaluate.test.ts @@ -74,6 +74,33 @@ describe("evaluate", () => { }); }); + test("mustache does not HTML-escape prompt content", () => { + const result = evaluate(solveIssueGraph, "reviewer", { + status: "rejected", + comments: 'use & "Result" types', + }); + expect(result).toEqual({ + ok: true, + value: { role: "developer", prompt: 'Fix: use & "Result" types' }, + }); + }); + + test("triple mustache also works for unescaped output", () => { + const graph: Record> = { + reviewer: { + _: { role: "developer", prompt: "Fix: {{{comments}}}" }, + }, + }; + const result = evaluate(graph, "reviewer", { + status: "_", + comments: "", + }); + expect(result).toEqual({ + ok: true, + value: { role: "developer", prompt: "Fix: " }, + }); + }); + test("mustache template with nested object paths", () => { const graph: Record> = { reviewer: { diff --git a/packages/workflow-moderator/src/evaluate.ts b/packages/workflow-moderator/src/evaluate.ts index 108a1dc..69aa469 100644 --- a/packages/workflow-moderator/src/evaluate.ts +++ b/packages/workflow-moderator/src/evaluate.ts @@ -3,6 +3,9 @@ import mustache from "mustache"; import type { EvaluateResult, Result } from "./types.js"; +// Disable HTML escaping — prompts are plain text, not HTML. +mustache.escape = (text: string) => text; + const START_ROLE = "$START"; const UNIT_STATUS = "_";