refactor: unify RoleDefinition + WorkflowDefinition with description & schema
- Add RoleDefinition<Meta> = { description, run, schema } to core types
- WorkflowDefinition now carries description and RoleDefinition per role
- Add buildDescriptor() in core to derive WorkflowDescriptor from WorkflowDefinition
- Remove buildDescriptorFromRoles / RoleDescriptorInput from workflow-util-role
- Update solve-issue template, examples, and all tests
小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
[test]
|
||||||
|
pathIgnorePatterns = ["dist/**"]
|
||||||
+12
-3
@@ -1,9 +1,14 @@
|
|||||||
import { createRoleModerator, END, type Role } from "@uncaged/workflow";
|
import { createRoleModerator, END, type RoleDefinition } from "@uncaged/workflow";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
type Roles = {
|
type Roles = {
|
||||||
greeter: { greeting: string };
|
greeter: { greeting: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const greeterMetaSchema = z.object({
|
||||||
|
greeting: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const descriptor = {
|
export const descriptor = {
|
||||||
description: "A simple hello world workflow",
|
description: "A simple hello world workflow",
|
||||||
roles: {
|
roles: {
|
||||||
@@ -18,10 +23,14 @@ export const descriptor = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const greeter: Role<Roles["greeter"]> = async (ctx) => ({
|
const greeter: RoleDefinition<Roles["greeter"]> = {
|
||||||
|
description: "Generates a greeting",
|
||||||
|
schema: greeterMetaSchema,
|
||||||
|
run: async (ctx) => ({
|
||||||
content: `Hello, ${ctx.start.content}`,
|
content: `Hello, ${ctx.start.content}`,
|
||||||
meta: { greeting: "Hello!" },
|
meta: { greeting: "Hello!" },
|
||||||
});
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
export const run = createRoleModerator<Roles>({
|
export const run = createRoleModerator<Roles>({
|
||||||
roles: { greeter },
|
roles: { greeter },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/workflow": "workspace:*"
|
"@uncaged/workflow": "workspace:*",
|
||||||
|
"zod": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export {
|
export {
|
||||||
buildDescriptorFromRoles,
|
|
||||||
type CreateRoleArgs,
|
type CreateRoleArgs,
|
||||||
createRole,
|
createRole,
|
||||||
decorateRole,
|
decorateRole,
|
||||||
@@ -15,7 +14,6 @@ export {
|
|||||||
type OnFailOptions,
|
type OnFailOptions,
|
||||||
onFail,
|
onFail,
|
||||||
type RoleDecorator,
|
type RoleDecorator,
|
||||||
type RoleDescriptorInput,
|
|
||||||
type WithDryRunOptions,
|
type WithDryRunOptions,
|
||||||
withDryRun,
|
withDryRun,
|
||||||
} from "@uncaged/workflow-util-role";
|
} from "@uncaged/workflow-util-role";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { createCommitterRole } from "../src/committer.js";
|
|||||||
|
|
||||||
function makeCtx(): ThreadContext {
|
function makeCtx(): ThreadContext {
|
||||||
return {
|
return {
|
||||||
threadId: "01TEST00000000000000000000",
|
threadId: "01TEST000000000000000000TR",
|
||||||
start: {
|
start: {
|
||||||
role: START,
|
role: START,
|
||||||
content: "do thing",
|
content: "do thing",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function toolCallResponse(argsJson: string): Response {
|
|||||||
|
|
||||||
function makeCtx(): ThreadContext {
|
function makeCtx(): ThreadContext {
|
||||||
return {
|
return {
|
||||||
threadId: "01TEST00000000000000000000",
|
threadId: "01TEST000000000000000000TR",
|
||||||
start: {
|
start: {
|
||||||
role: START,
|
role: START,
|
||||||
content: "task",
|
content: "task",
|
||||||
@@ -106,6 +106,6 @@ describe("createReviewerRole", () => {
|
|||||||
{ cwd: "/proj" },
|
{ cwd: "/proj" },
|
||||||
);
|
);
|
||||||
await role(makeCtx());
|
await role(makeCtx());
|
||||||
expect(seen).toContain("uncaged-workflow thread 01TEST00000000000000000000");
|
expect(seen).toContain("uncaged-workflow thread 01TEST000000000000000000TR");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function makeCtx(
|
|||||||
steps: ThreadContext<SolveIssueMeta>["steps"],
|
steps: ThreadContext<SolveIssueMeta>["steps"],
|
||||||
): ThreadContext<SolveIssueMeta> {
|
): ThreadContext<SolveIssueMeta> {
|
||||||
return {
|
return {
|
||||||
threadId: "01TEST00000000000000000000",
|
threadId: "01TEST000000000000000000TR",
|
||||||
start: makeStart(maxRounds),
|
start: makeStart(maxRounds),
|
||||||
steps,
|
steps,
|
||||||
};
|
};
|
||||||
@@ -112,13 +112,13 @@ describe("createSolveIssueRoles", () => {
|
|||||||
extract: null,
|
extract: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(typeof roles.planner).toBe("function");
|
expect(typeof roles.planner.run).toBe("function");
|
||||||
expect(typeof roles.coder).toBe("function");
|
expect(typeof roles.coder.run).toBe("function");
|
||||||
expect(typeof roles.reviewer).toBe("function");
|
expect(typeof roles.reviewer.run).toBe("function");
|
||||||
expect(typeof roles.committer).toBe("function");
|
expect(typeof roles.committer.run).toBe("function");
|
||||||
|
|
||||||
const ctx = makeCtx(10, []);
|
const ctx = makeCtx(10, []);
|
||||||
const plannerOut = await roles.planner(ctx as unknown as ThreadContext);
|
const plannerOut = await roles.planner.run(ctx as unknown as ThreadContext);
|
||||||
expect(plannerOut.meta.plan).toBe("");
|
expect(plannerOut.meta.plan).toBe("");
|
||||||
expect(Array.isArray(plannerOut.meta.files)).toBe(true);
|
expect(Array.isArray(plannerOut.meta.files)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,34 +1,22 @@
|
|||||||
import { committerMetaSchema } from "@uncaged/workflow-role-committer";
|
import { buildDescriptor } from "@uncaged/workflow";
|
||||||
import { reviewerMetaSchema } from "@uncaged/workflow-role-reviewer";
|
|
||||||
import { buildDescriptorFromRoles } from "@uncaged/workflow-util-role";
|
|
||||||
|
|
||||||
import { coderMetaSchema, plannerMetaSchema } from "./roles.js";
|
import { solveIssueModerator } from "./moderator.js";
|
||||||
|
import {
|
||||||
|
createSolveIssueRoles,
|
||||||
|
SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
|
||||||
|
type SolveIssueRolesConfig,
|
||||||
|
} from "./roles.js";
|
||||||
|
|
||||||
|
const BUILD_DESCRIPTOR_CONFIG: SolveIssueRolesConfig = {
|
||||||
|
agent: async () => "",
|
||||||
|
workdir: "/tmp/uncaged-workflow-descriptor-stub",
|
||||||
|
extract: null,
|
||||||
|
};
|
||||||
|
|
||||||
export function buildSolveIssueDescriptor() {
|
export function buildSolveIssueDescriptor() {
|
||||||
return buildDescriptorFromRoles({
|
return buildDescriptor({
|
||||||
description:
|
description: SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
|
||||||
"Plan, implement, review, and commit changes to resolve an issue end-to-end (planner → coder → reviewer → committer).",
|
roles: createSolveIssueRoles(BUILD_DESCRIPTOR_CONFIG),
|
||||||
roles: {
|
moderator: solveIssueModerator,
|
||||||
planner: {
|
|
||||||
name: "planner",
|
|
||||||
schema: plannerMetaSchema,
|
|
||||||
description: "Analyzes the issue and proposes plan, files, and approach.",
|
|
||||||
},
|
|
||||||
coder: {
|
|
||||||
name: "coder",
|
|
||||||
schema: coderMetaSchema,
|
|
||||||
description: "Implements the planner output and summarizes touched files.",
|
|
||||||
},
|
|
||||||
reviewer: {
|
|
||||||
name: "reviewer",
|
|
||||||
schema: reviewerMetaSchema,
|
|
||||||
description: "Runs git diff checks and sets approved when the change is ready.",
|
|
||||||
},
|
|
||||||
committer: {
|
|
||||||
name: "committer",
|
|
||||||
schema: committerMetaSchema,
|
|
||||||
description: "Creates branch, commits, and pushes when review passes.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createRoleModerator, type WorkflowFn } from "@uncaged/workflow";
|
import { createRoleModerator, type WorkflowDefinition, type WorkflowFn } from "@uncaged/workflow";
|
||||||
|
|
||||||
import { solveIssueModerator } from "./moderator.js";
|
import { solveIssueModerator } from "./moderator.js";
|
||||||
import { createSolveIssueRoles, type SolveIssueMeta, type SolveIssueRolesConfig } from "./roles.js";
|
import {
|
||||||
|
createSolveIssueRoles,
|
||||||
|
SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
|
||||||
|
type SolveIssueMeta,
|
||||||
|
type SolveIssueRolesConfig,
|
||||||
|
} from "./roles.js";
|
||||||
|
|
||||||
export { type CursorAgentConfig, createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
export { type CursorAgentConfig, createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
||||||
export { buildSolveIssueDescriptor } from "./descriptor.js";
|
export { buildSolveIssueDescriptor } from "./descriptor.js";
|
||||||
@@ -12,18 +17,26 @@ export {
|
|||||||
createSolveIssueRoles,
|
createSolveIssueRoles,
|
||||||
type PlannerMeta,
|
type PlannerMeta,
|
||||||
plannerMetaSchema,
|
plannerMetaSchema,
|
||||||
|
SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
|
||||||
type SolveIssueMeta,
|
type SolveIssueMeta,
|
||||||
type SolveIssueRoles,
|
type SolveIssueRoles,
|
||||||
type SolveIssueRolesConfig,
|
type SolveIssueRolesConfig,
|
||||||
} from "./roles.js";
|
} from "./roles.js";
|
||||||
|
|
||||||
|
export function createSolveIssueWorkflowDefinition(
|
||||||
|
config: SolveIssueRolesConfig,
|
||||||
|
): WorkflowDefinition<SolveIssueMeta> {
|
||||||
|
return {
|
||||||
|
description: SOLVE_ISSUE_WORKFLOW_DESCRIPTION,
|
||||||
|
roles: createSolveIssueRoles(config),
|
||||||
|
moderator: solveIssueModerator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for a {@link WorkflowFn}: supply an agent and repo paths at runtime, then pass the result
|
* Factory for a {@link WorkflowFn}: supply an agent and repo paths at runtime, then pass the result
|
||||||
* to the bundle `run` export pattern (`createRoleModerator` is already applied).
|
* to the bundle `run` export pattern (`createRoleModerator` is already applied).
|
||||||
*/
|
*/
|
||||||
export function createSolveIssueRun(config: SolveIssueRolesConfig): WorkflowFn {
|
export function createSolveIssueRun(config: SolveIssueRolesConfig): WorkflowFn {
|
||||||
return createRoleModerator<SolveIssueMeta>({
|
return createRoleModerator(createSolveIssueWorkflowDefinition(config));
|
||||||
roles: createSolveIssueRoles(config),
|
|
||||||
moderator: solveIssueModerator,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import type { AgentFn, Role } from "@uncaged/workflow";
|
import type { AgentFn, RoleDefinition } from "@uncaged/workflow";
|
||||||
import { createRole } from "@uncaged/workflow-agent-llm";
|
import { createRole } from "@uncaged/workflow-agent-llm";
|
||||||
import { type CommitterMeta, createCommitterRole } from "@uncaged/workflow-role-committer";
|
import {
|
||||||
import { createReviewerRole, type ReviewerMeta } from "@uncaged/workflow-role-reviewer";
|
type CommitterMeta,
|
||||||
|
committerMetaSchema,
|
||||||
|
createCommitterRole,
|
||||||
|
} from "@uncaged/workflow-role-committer";
|
||||||
|
import {
|
||||||
|
createReviewerRole,
|
||||||
|
type ReviewerMeta,
|
||||||
|
reviewerMetaSchema,
|
||||||
|
} from "@uncaged/workflow-role-reviewer";
|
||||||
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
import type { LlmProvider } from "@uncaged/workflow-util-role";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
@@ -19,6 +27,9 @@ const CODER_SYSTEM = `You are a **coder**. The previous step produced a plan: re
|
|||||||
|
|
||||||
Make focused changes, follow project conventions, and explain what you changed.`;
|
Make focused changes, follow project conventions, and explain what you changed.`;
|
||||||
|
|
||||||
|
export const SOLVE_ISSUE_WORKFLOW_DESCRIPTION =
|
||||||
|
"Plan, implement, review, and commit changes to resolve an issue end-to-end (planner → coder → reviewer → committer).";
|
||||||
|
|
||||||
export const plannerMetaSchema = z.object({
|
export const plannerMetaSchema = z.object({
|
||||||
plan: z.string(),
|
plan: z.string(),
|
||||||
files: z.array(z.string()),
|
files: z.array(z.string()),
|
||||||
@@ -80,10 +91,10 @@ function resolveExtract(config: SolveIssueRolesConfig): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SolveIssueRoles = {
|
export type SolveIssueRoles = {
|
||||||
planner: Role<PlannerMeta>;
|
planner: RoleDefinition<PlannerMeta>;
|
||||||
coder: Role<CoderMeta>;
|
coder: RoleDefinition<CoderMeta>;
|
||||||
reviewer: Role<ReviewerMeta>;
|
reviewer: RoleDefinition<ReviewerMeta>;
|
||||||
committer: Role<CommitterMeta>;
|
committer: RoleDefinition<CommitterMeta>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssueRoles {
|
export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssueRoles {
|
||||||
@@ -95,7 +106,7 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
|
|||||||
cwd: config.workdir,
|
cwd: config.workdir,
|
||||||
};
|
};
|
||||||
|
|
||||||
const planner: Role<PlannerMeta> = createRole({
|
const plannerRun = createRole({
|
||||||
name: "planner",
|
name: "planner",
|
||||||
schema: plannerMetaSchema,
|
schema: plannerMetaSchema,
|
||||||
systemPrompt: PLANNER_SYSTEM,
|
systemPrompt: PLANNER_SYSTEM,
|
||||||
@@ -107,7 +118,7 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const coder: Role<CoderMeta> = createRole({
|
const coderRun = createRole({
|
||||||
name: "coder",
|
name: "coder",
|
||||||
schema: coderMetaSchema,
|
schema: coderMetaSchema,
|
||||||
systemPrompt: CODER_SYSTEM,
|
systemPrompt: CODER_SYSTEM,
|
||||||
@@ -119,7 +130,7 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const reviewer: Role<ReviewerMeta> = createReviewerRole(
|
const reviewerRun = createReviewerRole(
|
||||||
config.agent,
|
config.agent,
|
||||||
{
|
{
|
||||||
provider: extract.provider,
|
provider: extract.provider,
|
||||||
@@ -129,7 +140,7 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
|
|||||||
reviewerConfig,
|
reviewerConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const committer: Role<CommitterMeta> = createCommitterRole(
|
const committerRun = createCommitterRole(
|
||||||
config.agent,
|
config.agent,
|
||||||
{
|
{
|
||||||
provider: extract.provider,
|
provider: extract.provider,
|
||||||
@@ -139,5 +150,26 @@ export function createSolveIssueRoles(config: SolveIssueRolesConfig): SolveIssue
|
|||||||
committerConfig,
|
committerConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { planner, coder, reviewer, committer };
|
return {
|
||||||
|
planner: {
|
||||||
|
description: "Analyzes the issue and proposes plan, files, and approach.",
|
||||||
|
run: plannerRun,
|
||||||
|
schema: plannerMetaSchema,
|
||||||
|
},
|
||||||
|
coder: {
|
||||||
|
description: "Implements the planner output and summarizes touched files.",
|
||||||
|
run: coderRun,
|
||||||
|
schema: coderMetaSchema,
|
||||||
|
},
|
||||||
|
reviewer: {
|
||||||
|
description: "Runs git diff checks and sets approved when the change is ready.",
|
||||||
|
run: reviewerRun,
|
||||||
|
schema: reviewerMetaSchema,
|
||||||
|
},
|
||||||
|
committer: {
|
||||||
|
description: "Creates branch, commits, and pushes when review passes.",
|
||||||
|
run: committerRun,
|
||||||
|
schema: committerMetaSchema,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import { validateWorkflowDescriptor } from "@uncaged/workflow";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
import { buildDescriptorFromRoles } from "../src/build-descriptor.js";
|
|
||||||
|
|
||||||
describe("buildDescriptorFromRoles", () => {
|
|
||||||
test("produces a descriptor that validates and includes JSON schemas per role", () => {
|
|
||||||
const schema = z.object({
|
|
||||||
title: z.string(),
|
|
||||||
count: z.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const descriptor = buildDescriptorFromRoles({
|
|
||||||
description: "Demo workflow",
|
|
||||||
roles: {
|
|
||||||
analyst: {
|
|
||||||
name: "analyst",
|
|
||||||
schema,
|
|
||||||
description: "Analyzes input",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const validated = validateWorkflowDescriptor(descriptor);
|
|
||||||
expect(validated.ok).toBe(true);
|
|
||||||
if (!validated.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(validated.value.description).toBe("Demo workflow");
|
|
||||||
const analyst = validated.value.roles.analyst;
|
|
||||||
expect(analyst.description).toBe("Analyzes input");
|
|
||||||
expect(analyst.schema.type).toBe("object");
|
|
||||||
const props = analyst.schema.properties as Record<string, unknown>;
|
|
||||||
expect(props.title).toMatchObject({ type: "string" });
|
|
||||||
expect(props.count).toMatchObject({ type: "number" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses empty description when spec.description is null", () => {
|
|
||||||
const descriptor = buildDescriptorFromRoles({
|
|
||||||
description: "W",
|
|
||||||
roles: {
|
|
||||||
x: {
|
|
||||||
name: "x",
|
|
||||||
schema: z.object({ n: z.number() }),
|
|
||||||
description: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const validated = validateWorkflowDescriptor(descriptor);
|
|
||||||
expect(validated.ok).toBe(true);
|
|
||||||
if (!validated.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
expect(validated.value.roles.x.description).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws when role key and spec.name diverge", () => {
|
|
||||||
expect(() =>
|
|
||||||
buildDescriptorFromRoles({
|
|
||||||
description: "W",
|
|
||||||
roles: {
|
|
||||||
a: {
|
|
||||||
name: "b",
|
|
||||||
schema: z.object({ n: z.number() }),
|
|
||||||
description: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toThrow(/must match spec.name/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import type { WorkflowDescriptor, WorkflowRoleSchema } from "@uncaged/workflow";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
export type RoleDescriptorInput<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
||||||
name: string;
|
|
||||||
schema: z.ZodType<M>;
|
|
||||||
/** Human-readable role description; use empty string when unknown. */
|
|
||||||
description: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function stripJsonSchemaMeta(json: Record<string, unknown>): WorkflowRoleSchema {
|
|
||||||
const { $schema: _drop, ...rest } = json;
|
|
||||||
return rest as WorkflowRoleSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a {@link WorkflowDescriptor} from role specs, emitting JSON Schema per role via
|
|
||||||
* `z.toJSONSchema`.
|
|
||||||
*/
|
|
||||||
export function buildDescriptorFromRoles(args: {
|
|
||||||
description: string;
|
|
||||||
roles: Record<string, RoleDescriptorInput>;
|
|
||||||
}): WorkflowDescriptor {
|
|
||||||
const roles: WorkflowDescriptor["roles"] = {};
|
|
||||||
for (const [key, spec] of Object.entries(args.roles)) {
|
|
||||||
if (spec.name !== key) {
|
|
||||||
throw new Error(
|
|
||||||
`buildDescriptorFromRoles: role key "${key}" must match spec.name "${spec.name}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const rawJsonSchema = z.toJSONSchema(spec.schema) as Record<string, unknown>;
|
|
||||||
roles[key] = {
|
|
||||||
description: spec.description === null ? "" : spec.description,
|
|
||||||
schema: stripJsonSchemaMeta(rawJsonSchema),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { description: args.description, roles };
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export { buildDescriptorFromRoles, type RoleDescriptorInput } from "./build-descriptor.js";
|
|
||||||
export { type CreateRoleArgs, createRole } from "./create-role.js";
|
export { type CreateRoleArgs, createRole } from "./create-role.js";
|
||||||
export {
|
export {
|
||||||
decorateRole,
|
decorateRole,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
import { buildDescriptor } from "../src/build-descriptor.js";
|
||||||
|
import { END } from "../src/types.js";
|
||||||
|
import { validateWorkflowDescriptor } from "../src/workflow-descriptor.js";
|
||||||
|
|
||||||
|
describe("buildDescriptor", () => {
|
||||||
|
test("produces a descriptor that validates and includes JSON schemas per role", () => {
|
||||||
|
const schema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
count: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type M = { analyst: z.infer<typeof schema> };
|
||||||
|
|
||||||
|
const descriptor = buildDescriptor<M>({
|
||||||
|
description: "Demo workflow",
|
||||||
|
roles: {
|
||||||
|
analyst: {
|
||||||
|
description: "Analyzes input",
|
||||||
|
schema,
|
||||||
|
run: async () => ({ content: "", meta: { title: "", count: 0 } }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
moderator: () => END,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validated = validateWorkflowDescriptor(descriptor);
|
||||||
|
expect(validated.ok).toBe(true);
|
||||||
|
if (!validated.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(validated.value.description).toBe("Demo workflow");
|
||||||
|
const analyst = validated.value.roles.analyst;
|
||||||
|
expect(analyst.description).toBe("Analyzes input");
|
||||||
|
expect(analyst.schema.type).toBe("object");
|
||||||
|
const props = analyst.schema.properties as Record<string, unknown>;
|
||||||
|
expect(props.title).toMatchObject({ type: "string" });
|
||||||
|
expect(props.count).toMatchObject({ type: "number" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,28 +2,46 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import { createRoleModerator } from "../src/create-role-moderator.js";
|
import { createRoleModerator } from "../src/create-role-moderator.js";
|
||||||
import { executeThread } from "../src/engine.js";
|
import { executeThread } from "../src/engine.js";
|
||||||
import { createLogger } from "../src/logger.js";
|
import { createLogger } from "../src/logger.js";
|
||||||
import { END } from "../src/types.js";
|
import { END } from "../src/types.js";
|
||||||
|
|
||||||
|
const plannerMetaSchema = z.object({
|
||||||
|
plan: z.string(),
|
||||||
|
files: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const coderMetaSchema = z.object({
|
||||||
|
diff: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
type DemoMeta = {
|
type DemoMeta = {
|
||||||
planner: Record<string, unknown>;
|
planner: z.infer<typeof plannerMetaSchema>;
|
||||||
coder: Record<string, unknown>;
|
coder: z.infer<typeof coderMetaSchema>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const demoWorkflow = createRoleModerator<DemoMeta>({
|
const demoWorkflow = createRoleModerator<DemoMeta>({
|
||||||
roles: {
|
roles: {
|
||||||
planner: async () => ({
|
planner: {
|
||||||
|
description: "Demo planner",
|
||||||
|
schema: plannerMetaSchema,
|
||||||
|
run: async () => ({
|
||||||
content: "plan-body",
|
content: "plan-body",
|
||||||
meta: { plan: "do-it", files: ["a.ts"] },
|
meta: { plan: "do-it", files: ["a.ts"] },
|
||||||
}),
|
}),
|
||||||
coder: async () => ({
|
},
|
||||||
|
coder: {
|
||||||
|
description: "Demo coder",
|
||||||
|
schema: coderMetaSchema,
|
||||||
|
run: async () => ({
|
||||||
content: "code-body",
|
content: "code-body",
|
||||||
meta: { diff: "+ok" },
|
meta: { diff: "+ok" },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
moderator: (ctx) => {
|
moderator: (ctx) => {
|
||||||
if (ctx.steps.length === 0) {
|
if (ctx.steps.length === 0) {
|
||||||
return "planner";
|
return "planner";
|
||||||
|
|||||||
@@ -13,7 +13,11 @@
|
|||||||
"xxhashjs": "^0.2.2",
|
"xxhashjs": "^0.2.2",
|
||||||
"yaml": "^2.8.4"
|
"yaml": "^2.8.4"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/acorn": "^6.0.4"
|
"@types/acorn": "^6.0.4",
|
||||||
|
"zod": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
import type { RoleMeta, WorkflowDefinition } from "./types.js";
|
||||||
|
import type { WorkflowDescriptor, WorkflowRoleSchema } from "./workflow-descriptor.js";
|
||||||
|
|
||||||
|
function stripJsonSchemaMeta(json: Record<string, unknown>): WorkflowRoleSchema {
|
||||||
|
const { $schema: _drop, ...rest } = json;
|
||||||
|
return rest as WorkflowRoleSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDescriptor<M extends RoleMeta>(
|
||||||
|
def: WorkflowDefinition<M>,
|
||||||
|
): WorkflowDescriptor {
|
||||||
|
const roles: WorkflowDescriptor["roles"] = {};
|
||||||
|
for (const [key, roleDef] of Object.entries(def.roles) as Array<
|
||||||
|
[string, { description: string; schema: z.ZodType }]
|
||||||
|
>) {
|
||||||
|
const rawJsonSchema = z.toJSONSchema(roleDef.schema) as Record<string, unknown>;
|
||||||
|
roles[key] = {
|
||||||
|
description: roleDef.description,
|
||||||
|
schema: stripJsonSchemaMeta(rawJsonSchema),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { description: def.description, roles };
|
||||||
|
}
|
||||||
@@ -65,12 +65,12 @@ export function createRoleModerator<M extends RoleMeta>(
|
|||||||
return { returnCode: 0, summary: "completed: moderator returned END" };
|
return { returnCode: 0, summary: "completed: moderator returned END" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleFn = def.roles[next];
|
const roleDef = def.roles[next];
|
||||||
if (roleFn === undefined) {
|
if (roleDef === undefined) {
|
||||||
return { returnCode: 1, summary: `unknown role: ${next}` };
|
return { returnCode: 1, summary: `unknown role: ${next}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await roleFn(ctx as unknown as ThreadContext);
|
const result = await roleDef.run(ctx as unknown as ThreadContext);
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
const step = {
|
const step = {
|
||||||
role: next,
|
role: next,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export {
|
|||||||
encodeCrockfordBase32Bits,
|
encodeCrockfordBase32Bits,
|
||||||
encodeUint64AsCrockford,
|
encodeUint64AsCrockford,
|
||||||
} from "./base32.js";
|
} from "./base32.js";
|
||||||
|
export { buildDescriptor } from "./build-descriptor.js";
|
||||||
export { validateWorkflowBundle, type WorkflowBundleValidationInput } from "./bundle-validator.js";
|
export { validateWorkflowBundle, type WorkflowBundleValidationInput } from "./bundle-validator.js";
|
||||||
export { createRoleModerator } from "./create-role-moderator.js";
|
export { createRoleModerator } from "./create-role-moderator.js";
|
||||||
export {
|
export {
|
||||||
@@ -53,6 +54,7 @@ export {
|
|||||||
END,
|
END,
|
||||||
type Moderator,
|
type Moderator,
|
||||||
type Role,
|
type Role,
|
||||||
|
type RoleDefinition,
|
||||||
type RoleMeta,
|
type RoleMeta,
|
||||||
type RoleOutput,
|
type RoleOutput,
|
||||||
type RoleResult,
|
type RoleResult,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type * as z from "zod/v4";
|
||||||
|
|
||||||
/** Sentinel values for automaton control flow. */
|
/** Sentinel values for automaton control flow. */
|
||||||
export const START = "__start__" as const;
|
export const START = "__start__" as const;
|
||||||
export const END = "__end__" as const;
|
export const END = "__end__" as const;
|
||||||
@@ -71,6 +73,13 @@ export type Role<Meta extends Record<string, unknown>> = (
|
|||||||
ctx: ThreadContext,
|
ctx: ThreadContext,
|
||||||
) => Promise<RoleResult<Meta>>;
|
) => Promise<RoleResult<Meta>>;
|
||||||
|
|
||||||
|
/** Role wiring: runtime {@link Role}, JSON Schema for `meta`, and human-readable description. */
|
||||||
|
export type RoleDefinition<Meta extends Record<string, unknown>> = {
|
||||||
|
description: string;
|
||||||
|
run: Role<Meta>;
|
||||||
|
schema: z.ZodType<Meta>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Agent — raw string output interface for LLM/CLI adapters.
|
* An Agent — raw string output interface for LLM/CLI adapters.
|
||||||
* Structured meta is extracted by the role's extract layer.
|
* Structured meta is extracted by the role's extract layer.
|
||||||
@@ -89,6 +98,7 @@ export type Moderator<M extends RoleMeta> = (
|
|||||||
|
|
||||||
/** Complete workflow definition as authored by users. */
|
/** Complete workflow definition as authored by users. */
|
||||||
export type WorkflowDefinition<M extends RoleMeta> = {
|
export type WorkflowDefinition<M extends RoleMeta> = {
|
||||||
roles: { [K in keyof M & string]: Role<M[K]> };
|
description: string;
|
||||||
|
roles: { [K in keyof M & string]: RoleDefinition<M[K]> };
|
||||||
moderator: Moderator<M>;
|
moderator: Moderator<M>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user