refactor: extract planner and coder into standalone role packages

- New @uncaged/workflow-role-planner (phaseSchema, createPlannerRole)
- New @uncaged/workflow-role-coder (coderMetaSchema, createCoderRole)
- solve-issue template imports from new packages, keeps dry-run defaults

小橘 <xiaoju@shazhou.work>
This commit is contained in:
2026-05-06 11:40:19 +00:00
parent 45bb5af99a
commit c9cdfe37db
14 changed files with 225 additions and 65 deletions
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@uncaged/workflow-role-coder",
"version": "0.1.0",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "echo 'TODO'",
"test": "bun test"
},
"dependencies": {
"@uncaged/workflow": "workspace:*",
"@uncaged/workflow-agent-llm": "workspace:*",
"@uncaged/workflow-util-role": "workspace:*",
"zod": "^4.0.0"
}
}
+51
View File
@@ -0,0 +1,51 @@
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
import { createRole } from "@uncaged/workflow-agent-llm";
import type { LlmProvider } from "@uncaged/workflow-util-role";
import * as z from "zod/v4";
export const coderMetaSchema = z.object({
completedPhase: z.string(),
filesChanged: z.array(z.string()),
summary: z.string(),
});
export type CoderMeta = z.infer<typeof coderMetaSchema>;
export type CoderConfig = {
cwd: string;
};
export const DEFAULT_CODER_CONFIG: CoderConfig = {
cwd: ".",
};
function coderSystemPrompt(config: CoderConfig): string {
return `You are a **coder**. The project is at \`${config.cwd}\`.
Read the thread: the planner produced ordered **phases**. Identify the **next** phase that is not yet completed according to prior coder steps (each coder step reports a completedPhase).
Implement **only that phase** — do not tackle multiple phases in one turn unless the planner defined a single phase. Follow project conventions; summarize what changed and list touched files.
When done with the phase you worked on, set **completedPhase** to that phase's **name** exactly as given by the planner.`;
}
/**
* Coder role: implements the next incomplete planner phase and reports structured completion metadata.
*/
export function createCoderRole(
adapter: AgentFn,
extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: CoderMeta },
config: CoderConfig = DEFAULT_CODER_CONFIG,
): Role<CoderMeta> {
return createRole({
name: "coder",
schema: coderMetaSchema,
systemPrompt: async (_ctx: ThreadContext) => coderSystemPrompt(config),
agent: adapter,
extract: {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: extract.dryRunMeta,
},
});
}
@@ -0,0 +1,7 @@
export {
type CoderConfig,
type CoderMeta,
coderMetaSchema,
createCoderRole,
DEFAULT_CODER_CONFIG,
} from "./coder.js";
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../workflow" },
{ "path": "../workflow-agent-llm" },
{ "path": "../workflow-util-role" }
]
}
@@ -0,0 +1,17 @@
{
"name": "@uncaged/workflow-role-planner",
"version": "0.1.0",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "echo 'TODO'",
"test": "bun test"
},
"dependencies": {
"@uncaged/workflow": "workspace:*",
"@uncaged/workflow-agent-llm": "workspace:*",
"@uncaged/workflow-util-role": "workspace:*",
"zod": "^4.0.0"
}
}
@@ -0,0 +1,8 @@
export {
createPlannerRole,
DEFAULT_PLANNER_CONFIG,
type PlannerConfig,
type PlannerMeta,
phaseSchema,
plannerMetaSchema,
} from "./planner.js";
@@ -0,0 +1,52 @@
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
import { createRole } from "@uncaged/workflow-agent-llm";
import type { LlmProvider } from "@uncaged/workflow-util-role";
import * as z from "zod/v4";
export const phaseSchema = z.object({
name: z.string(),
description: z.string(),
acceptance: z.string(),
});
export const plannerMetaSchema = z.object({
phases: z.array(phaseSchema),
});
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
/** Reserved for future planner options; empty for now. */
export type PlannerConfig = Record<string, never>;
export const DEFAULT_PLANNER_CONFIG: PlannerConfig = {};
const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time.
Each phase must have: a short **name** (stable identifier), a **description** of what to do in that phase, and **acceptance** criteria for when that phase is done.
Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. Do not emit separate file lists or a free-form "approach" field — put that detail inside phase descriptions.`;
function plannerSystemPrompt(_config: PlannerConfig): string {
return PLANNER_SYSTEM;
}
/**
* Planner role: produces ordered implementation phases for the coder to execute sequentially.
*/
export function createPlannerRole(
adapter: AgentFn,
extract: { provider: LlmProvider; dryRun: boolean | null; dryRunMeta: PlannerMeta },
config: PlannerConfig = DEFAULT_PLANNER_CONFIG,
): Role<PlannerMeta> {
return createRole({
name: "planner",
schema: plannerMetaSchema,
systemPrompt: async (_ctx: ThreadContext) => plannerSystemPrompt(config),
agent: adapter,
extract: {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: extract.dryRunMeta,
},
});
}
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../workflow" },
{ "path": "../workflow-agent-llm" },
{ "path": "../workflow-util-role" }
]
}
@@ -8,9 +8,11 @@ import {
validateWorkflowDescriptor,
} from "@uncaged/workflow";
import type { PlannerMeta } from "@uncaged/workflow-role-planner";
import { buildSolveIssueDescriptor } from "../src/descriptor.js";
import { solveIssueModerator } from "../src/moderator.js";
import { createSolveIssueRoles, type PlannerMeta, type SolveIssueMeta } from "../src/roles.js";
import { createSolveIssueRoles, type SolveIssueMeta } from "../src/roles.js";
const DEFAULT_PHASES: PlannerMeta["phases"] = [
{ name: "phase-a", description: "Do the work", acceptance: "Done" },
@@ -12,9 +12,9 @@
"@uncaged/workflow": "workspace:*",
"@uncaged/workflow-agent-cursor": "workspace:*",
"@uncaged/workflow-role-committer": "workspace:*",
"@uncaged/workflow-agent-llm": "workspace:*",
"@uncaged/workflow-role-coder": "workspace:*",
"@uncaged/workflow-role-planner": "workspace:*",
"@uncaged/workflow-role-reviewer": "workspace:*",
"@uncaged/workflow-util-role": "workspace:*",
"zod": "^4.0.0"
"@uncaged/workflow-util-role": "workspace:*"
}
}
@@ -9,15 +9,21 @@ import {
} from "./roles.js";
export { type CursorAgentConfig, createCursorAgent } from "@uncaged/workflow-agent-cursor";
export { buildSolveIssueDescriptor } from "./descriptor.js";
export { solveIssueModerator } from "./moderator.js";
export {
type CoderMeta,
coderMetaSchema,
createSolveIssueRoles,
createCoderRole,
} from "@uncaged/workflow-role-coder";
export {
createPlannerRole,
type PlannerMeta,
phaseSchema,
plannerMetaSchema,
} from "@uncaged/workflow-role-planner";
export { buildSolveIssueDescriptor } from "./descriptor.js";
export { solveIssueModerator } from "./moderator.js";
export {
createSolveIssueRoles,
SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
type SolveIssueMeta,
type SolveIssueRoles,
@@ -1,17 +1,21 @@
import type { AgentFn, RoleDefinition } from "@uncaged/workflow";
import { createRole } from "@uncaged/workflow-agent-llm";
import { type CoderMeta, coderMetaSchema, createCoderRole } from "@uncaged/workflow-role-coder";
import {
type CommitterMeta,
committerMetaSchema,
createCommitterRole,
} from "@uncaged/workflow-role-committer";
import {
createPlannerRole,
type PlannerMeta,
plannerMetaSchema,
} from "@uncaged/workflow-role-planner";
import {
createReviewerRole,
type ReviewerMeta,
reviewerMetaSchema,
} from "@uncaged/workflow-role-reviewer";
import type { LlmProvider } from "@uncaged/workflow-util-role";
import * as z from "zod/v4";
const DRY_RUN_PROVIDER: LlmProvider = {
baseUrl: "http://127.0.0.1:9",
@@ -19,40 +23,15 @@ const DRY_RUN_PROVIDER: LlmProvider = {
model: "template-dry-run",
};
const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time.
Each phase must have: a short **name** (stable identifier), a **description** of what to do in that phase, and **acceptance** criteria for when that phase is done.
Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. Do not emit separate file lists or a free-form "approach" field — put that detail inside phase descriptions.`;
const CODER_SYSTEM = `You are a **coder**. Read the thread: the planner produced ordered **phases**. Identify the **next** phase that is not yet completed according to prior coder steps (each coder step reports a completedPhase).
Implement **only that phase** — do not tackle multiple phases in one turn unless the planner defined a single phase. Follow project conventions; summarize what changed and list touched files.
When done with the phase you worked on, set **completedPhase** to that phase's **name** exactly as given by the planner.`;
export const SOLVE_ISSUE_WORKFLOW_DESCRIPTION =
"Phased plan, incremental implementation per phase, review, and commit to resolve an issue end-to-end (planner → coder [repeat per phase] → reviewer → committer).";
export const phaseSchema = z.object({
name: z.string(),
description: z.string(),
acceptance: z.string(),
});
export const plannerMetaSchema = z.object({
phases: z.array(phaseSchema),
});
export const coderMetaSchema = z.object({
completedPhase: z.string(),
filesChanged: z.array(z.string()),
summary: z.string(),
});
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
export type CoderMeta = z.infer<typeof coderMetaSchema>;
export type SolveIssueMeta = {
planner: PlannerMeta;
coder: CoderMeta;
reviewer: ReviewerMeta;
committer: CommitterMeta;
};
const PLANNER_DRY_RUN_META: PlannerMeta = {
phases: [
@@ -80,13 +59,6 @@ const COMMITTER_DRY_RUN_META: CommitterMeta = {
commitSha: "0000000",
};
export type SolveIssueMeta = {
planner: PlannerMeta;
coder: CoderMeta;
reviewer: ReviewerMeta;
committer: CommitterMeta;
};
/** Wiring for workflow-role LLM structured extraction. Use `null` for stub extract (dry-run meta from built-in placeholders). */
export type SolveIssueRolesConfig = {
agent: AgentFn;
@@ -132,35 +104,30 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
const committerConfig = {
cwd: config.workdir,
};
const coderConfig = {
cwd: config.workdir,
};
const plannerAgent = resolveRoleAgent(config, "planner");
const coderAgent = resolveRoleAgent(config, "coder");
const reviewerAgent = resolveRoleAgent(config, "reviewer");
const committerAgent = resolveRoleAgent(config, "committer");
const plannerRun = createRole({
name: "planner",
schema: plannerMetaSchema,
systemPrompt: PLANNER_SYSTEM,
agent: plannerAgent,
extract: {
const plannerRun = createPlannerRole(plannerAgent, {
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: PLANNER_DRY_RUN_META,
},
});
const coderRun = createRole({
name: "coder",
schema: coderMetaSchema,
systemPrompt: CODER_SYSTEM,
agent: coderAgent,
extract: {
const coderRun = createCoderRole(
coderAgent,
{
provider: extract.provider,
dryRun: extract.dryRun,
dryRunMeta: CODER_DRY_RUN_META,
},
});
coderConfig,
);
const reviewerRun = createReviewerRole(
reviewerAgent,
@@ -8,7 +8,10 @@
"include": ["src/**/*.ts"],
"references": [
{ "path": "../workflow" },
{ "path": "../workflow-agent-llm" },
{ "path": "../workflow-role-coder" },
{ "path": "../workflow-role-committer" },
{ "path": "../workflow-role-planner" },
{ "path": "../workflow-role-reviewer" },
{ "path": "../workflow-util-role" }
]
}
+2
View File
@@ -21,6 +21,8 @@
{ "path": "packages/workflow-util-role" },
{ "path": "packages/workflow-agent-llm" },
{ "path": "packages/workflow-role-committer" },
{ "path": "packages/workflow-role-coder" },
{ "path": "packages/workflow-role-planner" },
{ "path": "packages/workflow-role-reviewer" },
{ "path": "packages/workflow-agent-cursor" },
{ "path": "packages/workflow-agent-hermes" },