Merge pull request 'feat: Role 四段式描述 (identity/prepare/execute/report)' (#361) from feat/359-role-four-phase into main

This commit is contained in:
2026-05-21 03:11:00 +00:00
21 changed files with 186 additions and 24 deletions
+40
View File
@@ -0,0 +1,40 @@
name: "analyze-topic"
description: "Single-role topic analysis using four-phase role description"
roles:
analyst:
description: "Analyzes a given topic and produces a structured summary"
identity: |
You are a research analyst with expertise in breaking down complex topics
into clear, structured summaries. You think critically and cite key points.
prepare: |
Review the topic carefully. Consider multiple perspectives and identify
the core question being asked.
execute: |
Analyze the topic by:
1. Identifying the main thesis or question
2. Listing 3-5 key points with brief explanations
3. Noting any counterarguments or caveats
Keep your analysis concise (under 500 words).
report: |
Provide your analysis as markdown under the frontmatter.
The frontmatter must include your structured findings.
outputSchema:
type: object
properties:
thesis:
type: string
keyPoints:
type: array
items:
type: string
caveats:
type: string
required: [thesis, keyPoints]
conditions: {}
graph:
$START:
- role: "analyst"
condition: null
analyst:
- role: "$END"
condition: null
+12 -3
View File
@@ -3,7 +3,10 @@ description: "End-to-end issue resolution"
roles:
planner:
description: "Creates implementation plan"
systemPrompt: "You are a planning agent. Analyze the issue and create a step-by-step plan."
identity: "You are a planning agent. You analyze issues and create step-by-step plans."
prepare: "Read the issue description and any linked context carefully."
execute: "Analyze the issue and create a detailed, actionable implementation plan."
report: "Output the plan summary and list of concrete steps."
outputSchema:
type: object
properties:
@@ -16,7 +19,10 @@ roles:
required: [plan, steps]
developer:
description: "Implements code changes"
systemPrompt: "You are a developer agent. Implement the plan."
identity: "You are a developer agent. You implement code changes according to plans."
prepare: "Load coding tools and review the project structure and conventions."
execute: "Implement the plan. Write code, tests, and ensure existing tests pass."
report: "List all files changed and provide a summary of the implementation."
outputSchema:
type: object
properties:
@@ -29,7 +35,10 @@ roles:
required: [filesChanged, summary]
reviewer:
description: "Reviews code changes"
systemPrompt: "You are a code reviewer. Review the implementation."
identity: "You are a code reviewer. You review implementations for correctness and quality."
prepare: "Review the project's coding standards and conventions."
execute: "Review the implementation against the plan. Check for bugs, edge cases, and style."
report: "Approve or reject with detailed comments explaining your decision."
outputSchema:
type: object
properties:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/cli-workflow",
"version": "0.1.0",
"version": "0.5.0",
"files": [
"src",
"dist",
@@ -211,7 +211,10 @@ describe("cmdThreadRead ### Content section", () => {
roles: {
writer: {
description: "Write",
systemPrompt: "You are a writer.",
identity: "You are a writer.",
prepare: "",
execute: "Write content as requested.",
report: "Summarize what was written.",
outputSchema: "placeholder00" as CasRef,
},
},
+3 -1
View File
@@ -45,7 +45,9 @@ function runAction(action: () => Promise<void>): void {
const program = new Command();
program.name("uwf").description("Stateless workflow CLI");
// eslint-disable-next-line -- dynamic import for version
const pkg = await import("../package.json", { with: { type: "json" } });
program.name("uwf").description("Stateless workflow CLI").version(pkg.default.version, "-V, --version");
program.option("--format <fmt>", "Output format: json or yaml", "json");
const workflow = program.command("workflow").description("Workflow registry and CAS");
+2 -1
View File
@@ -500,7 +500,8 @@ function formatThreadReadMarkdown(options: {
];
const roleDef = workflow.roles[item.payload.role];
if (roleDef) {
stepLines.push("", "### Prompt", "", roleDef.systemPrompt);
const prompt = roleDef.identity;
stepLines.push("", "### Prompt", "", prompt);
}
if (item.payload.detail) {
const content = extractLastAssistantContent(uwf, item.payload.detail);
@@ -69,7 +69,10 @@ async function materializeWorkflowPayload(
);
roles[roleName] = {
description: role.description,
systemPrompt: role.systemPrompt,
identity: role.identity,
prepare: role.prepare,
execute: role.execute,
report: role.report,
outputSchema,
};
}
+6 -1
View File
@@ -17,7 +17,12 @@ function isRoleDefinition(value: unknown): boolean {
const outputSchema = value.outputSchema;
const schemaOk = isRecord(outputSchema) && typeof outputSchema.type === "string";
return (
typeof value.description === "string" && typeof value.systemPrompt === "string" && schemaOk
typeof value.description === "string" &&
typeof value.identity === "string" &&
typeof value.prepare === "string" &&
typeof value.execute === "string" &&
typeof value.report === "string" &&
schemaOk
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-hermes",
"version": "0.1.0",
"version": "0.5.0",
"files": [
"src",
"dist",
+3 -3
View File
@@ -1,6 +1,6 @@
import { spawn } from "node:child_process";
import { type AgentContext, type AgentRunResult, createAgent } from "@uncaged/workflow-agent-kit";
import { type AgentContext, type AgentRunResult, buildRolePrompt, createAgent } from "@uncaged/workflow-agent-kit";
import {
loadHermesSession,
@@ -34,12 +34,12 @@ function buildHistorySummary(steps: AgentContext["steps"]): string {
/** Assemble system prompt, task, and prior step outputs for Hermes. */
export function buildHermesPrompt(ctx: AgentContext): string {
const roleDef = ctx.workflow.roles[ctx.role];
const systemPrompt = roleDef?.systemPrompt ?? "";
const rolePrompt = roleDef !== undefined ? buildRolePrompt(roleDef) : "";
const parts: string[] = [];
if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") {
parts.push(ctx.outputFormatInstruction, "");
}
parts.push(systemPrompt, "", "## Task", ctx.start.prompt);
parts.push(rolePrompt, "", "## Task", ctx.start.prompt);
const historyBlock = buildHistorySummary(ctx.steps);
if (historyBlock !== "") {
parts.push("", historyBlock);
@@ -0,0 +1,54 @@
import { describe, expect, test } from "vitest";
import type { RoleDefinition } from "@uncaged/workflow-protocol";
import { buildRolePrompt } from "../src/build-role-prompt.js";
describe("buildRolePrompt", () => {
test("all fields present", () => {
const role: RoleDefinition = {
description: "A coder",
identity: "You are a senior developer.",
prepare: "Load cursor-agent skill.",
execute: "Implement the feature.",
report: "Summarize changes.",
outputSchema: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toContain("## Identity");
expect(result).toContain("You are a senior developer.");
expect(result).toContain("## Prepare");
expect(result).toContain("Load cursor-agent skill.");
expect(result).toContain("## Execute");
expect(result).toContain("Implement the feature.");
expect(result).toContain("## Report");
expect(result).toContain("Summarize changes.");
});
test("empty fields are omitted", () => {
const role: RoleDefinition = {
description: "A reviewer",
identity: "You are a code reviewer.",
prepare: "",
execute: "Review the PR diff carefully.",
report: "",
outputSchema: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toContain("## Identity");
expect(result).toContain("## Execute");
expect(result).not.toContain("## Prepare");
expect(result).not.toContain("## Report");
});
test("all empty returns empty string", () => {
const role: RoleDefinition = {
description: "Minimal",
identity: "",
prepare: "",
execute: "",
report: "",
outputSchema: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toBe("");
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-agent-kit",
"version": "0.1.0",
"version": "0.5.0",
"files": [
"src",
"dist",
@@ -0,0 +1,29 @@
import type { RoleDefinition } from "@uncaged/workflow-protocol";
/**
* Build the role prompt from a RoleDefinition.
*
* Assembles structured sections: Identity, Prepare, Execute, Report.
* Empty strings are omitted from the output.
*/
export function buildRolePrompt(role: RoleDefinition): string {
const sections: string[] = [];
if (role.identity !== "") {
sections.push(`## Identity\n\n${role.identity}`);
}
if (role.prepare !== "") {
sections.push(`## Prepare\n\n${role.prepare}`);
}
if (role.execute !== "") {
sections.push(`## Execute\n\n${role.execute}`);
}
if (role.report !== "") {
sections.push(`## Report\n\n${role.report}`);
}
return sections.join("\n\n");
}
+1
View File
@@ -7,6 +7,7 @@ export {
resolveModel,
} from "./extract.js";
export { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
export { buildRolePrompt } from "./build-role-prompt.js";
export type { FrontmatterFastPathResult } from "./frontmatter.js";
export { tryFrontmatterFastPath } from "./frontmatter.js";
export { createAgent } from "./run.js";
+1 -1
View File
@@ -7,7 +7,7 @@ export type AgentContext = ModeratorContext & {
store: Store;
workflow: WorkflowPayload;
/**
* Prepend to the role's systemPrompt when building the agent prompt.
* Prepend to the role's prompt when building the agent prompt.
* Contains the frontmatter deliverable format instruction derived from the
* role's output schema. Populated by `createAgent` at run time.
*/
@@ -9,17 +9,26 @@ const solveIssueWorkflow: WorkflowPayload = {
roles: {
planner: {
description: "Creates implementation plan",
systemPrompt: "You are a planning agent...",
identity: "You are a planning agent.",
prepare: "Review the issue context.",
execute: "Create a step-by-step plan.",
report: "Output the plan and steps.",
outputSchema: "5GWKR8TN1V3JA",
},
developer: {
description: "Implements code changes",
systemPrompt: "You are a developer agent...",
identity: "You are a developer agent.",
prepare: "Load coding tools.",
execute: "Implement the plan.",
report: "List files changed and summary.",
outputSchema: "8CNWT4KR6D1HV",
},
reviewer: {
description: "Reviews code changes",
systemPrompt: "You are a code reviewer...",
identity: "You are a code reviewer.",
prepare: "Review project conventions.",
execute: "Review the implementation.",
report: "Approve or reject with comments.",
outputSchema: "1VPBG9SM5E7WK",
},
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-moderator",
"version": "0.1.0",
"version": "0.5.0",
"files": [
"src",
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-protocol",
"version": "0.1.0",
"version": "0.5.0",
"files": [
"src",
"dist",
+5 -2
View File
@@ -2,10 +2,13 @@ import type { JSONSchema } from "@uncaged/json-cas";
const ROLE_DEFINITION: JSONSchema = {
type: "object",
required: ["description", "systemPrompt", "outputSchema"],
required: ["description", "identity", "prepare", "execute", "report", "outputSchema"],
properties: {
description: { type: "string" },
systemPrompt: { type: "string" },
identity: { type: "string" },
prepare: { type: "string" },
execute: { type: "string" },
report: { type: "string" },
outputSchema: { type: "string", format: "cas_ref" },
},
additionalProperties: false,
+4 -1
View File
@@ -18,7 +18,10 @@ export type StepRecord = {
export type RoleDefinition = {
description: string;
systemPrompt: string;
identity: string;
prepare: string;
execute: string;
report: string;
outputSchema: CasRef;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@uncaged/workflow-util",
"version": "0.5.0-alpha.4",
"version": "0.5.0",
"files": [
"src",
"dist",