feat: make edge prompt required (Phase 1)
- Transition.prompt: string | null → string - EvaluateResult.prompt: string | null → string - AgentContext.edgePrompt: string | null → string - CLI YAML validation rejects missing prompt - All tests updated Phase 2 will replace edgePrompt === null checks with findLastRoleIndex. Refs #405, #406, #404
This commit is contained in:
@@ -154,25 +154,43 @@ conditions:
|
|||||||
graph:
|
graph:
|
||||||
$START:
|
$START:
|
||||||
- role: "planner"
|
- role: "planner"
|
||||||
|
condition: null
|
||||||
|
prompt: "Analyze the issue and produce an implementation plan."
|
||||||
planner:
|
planner:
|
||||||
- role: "$END"
|
- role: "$END"
|
||||||
condition: "insufficientInfo"
|
condition: "insufficientInfo"
|
||||||
|
prompt: "Insufficient information to proceed; end the workflow."
|
||||||
- role: "developer"
|
- role: "developer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Implement the plan from the planner."
|
||||||
developer:
|
developer:
|
||||||
- role: "$END"
|
- role: "$END"
|
||||||
condition: "devFailed"
|
condition: "devFailed"
|
||||||
|
prompt: "Development failed; end the workflow."
|
||||||
- role: "reviewer"
|
- role: "reviewer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Send the implementation to the reviewer."
|
||||||
reviewer:
|
reviewer:
|
||||||
- role: "developer"
|
- role: "developer"
|
||||||
condition: "rejected"
|
condition: "rejected"
|
||||||
|
prompt: "Reviewer rejected the implementation; fix the issues."
|
||||||
- role: "tester"
|
- role: "tester"
|
||||||
|
condition: null
|
||||||
|
prompt: "Review passed; run tests on the implementation."
|
||||||
tester:
|
tester:
|
||||||
- role: "developer"
|
- role: "developer"
|
||||||
condition: "fixCode"
|
condition: "fixCode"
|
||||||
|
prompt: "Tests found code issues; return to developer."
|
||||||
- role: "planner"
|
- role: "planner"
|
||||||
condition: "fixSpec"
|
condition: "fixSpec"
|
||||||
|
prompt: "Tests found spec issues; return to planner."
|
||||||
- role: "committer"
|
- role: "committer"
|
||||||
|
condition: null
|
||||||
|
prompt: "Tests passed; commit and push the changes."
|
||||||
committer:
|
committer:
|
||||||
- role: "developer"
|
- role: "developer"
|
||||||
condition: "hookFailed"
|
condition: "hookFailed"
|
||||||
|
prompt: "Push hook failed; return to developer to fix."
|
||||||
- role: "$END"
|
- role: "$END"
|
||||||
|
condition: null
|
||||||
|
prompt: "Commit succeeded; complete the workflow."
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ graph:
|
|||||||
$START:
|
$START:
|
||||||
- role: "analyst"
|
- role: "analyst"
|
||||||
condition: null
|
condition: null
|
||||||
|
prompt: "Analyze the topic in the task and produce a structured summary with key points."
|
||||||
analyst:
|
analyst:
|
||||||
- role: "$END"
|
- role: "$END"
|
||||||
condition: null
|
condition: null
|
||||||
|
prompt: "Analysis complete. Finish the workflow."
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ graph:
|
|||||||
$START:
|
$START:
|
||||||
- role: "planner"
|
- role: "planner"
|
||||||
condition: null
|
condition: null
|
||||||
prompt: null
|
prompt: "Analyze the issue described in the task and produce a detailed implementation plan."
|
||||||
planner:
|
planner:
|
||||||
- role: "developer"
|
- role: "developer"
|
||||||
condition: null
|
condition: null
|
||||||
prompt: null
|
prompt: "Implement the plan from the planner. Write code, tests, and ensure existing tests pass."
|
||||||
developer:
|
developer:
|
||||||
- role: "reviewer"
|
- role: "reviewer"
|
||||||
condition: null
|
condition: null
|
||||||
prompt: null
|
prompt: "Review the developer's implementation against the plan for correctness and quality."
|
||||||
reviewer:
|
reviewer:
|
||||||
- role: "developer"
|
- role: "developer"
|
||||||
condition: "notApproved"
|
condition: "notApproved"
|
||||||
prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues."
|
prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues."
|
||||||
- role: "$END"
|
- role: "$END"
|
||||||
condition: null
|
condition: null
|
||||||
prompt: null
|
prompt: "The review passed. Complete the workflow."
|
||||||
|
|||||||
@@ -624,12 +624,14 @@ function resolveAgentConfig(
|
|||||||
return agentConfig;
|
return agentConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnAgent(agent: AgentConfig, threadId: ThreadId, role: string, edgePrompt: string | null): CasRef {
|
function spawnAgent(
|
||||||
|
agent: AgentConfig,
|
||||||
|
threadId: ThreadId,
|
||||||
|
role: string,
|
||||||
|
edgePrompt: string,
|
||||||
|
): CasRef {
|
||||||
const argv = [...agent.args, threadId, role];
|
const argv = [...agent.args, threadId, role];
|
||||||
const env = { ...process.env };
|
const env = { ...process.env, UWF_EDGE_PROMPT: edgePrompt };
|
||||||
if (edgePrompt !== null) {
|
|
||||||
env.UWF_EDGE_PROMPT = edgePrompt;
|
|
||||||
}
|
|
||||||
let stdout: string;
|
let stdout: string;
|
||||||
try {
|
try {
|
||||||
stdout = execFileSync(agent.command, argv, {
|
stdout = execFileSync(agent.command, argv, {
|
||||||
|
|||||||
@@ -55,11 +55,16 @@ function isJsonSchema(value: unknown): value is JSONSchema {
|
|||||||
function normalizeGraph(graph: Record<string, Transition[]>): Record<string, Transition[]> {
|
function normalizeGraph(graph: Record<string, Transition[]>): Record<string, Transition[]> {
|
||||||
const result: Record<string, Transition[]> = {};
|
const result: Record<string, Transition[]> = {};
|
||||||
for (const [node, transitions] of Object.entries(graph)) {
|
for (const [node, transitions] of Object.entries(graph)) {
|
||||||
result[node] = transitions.map((t) => ({
|
result[node] = transitions.map((t) => {
|
||||||
role: t.role,
|
if (typeof t.prompt !== "string" || t.prompt.trim() === "") {
|
||||||
condition: t.condition ?? null,
|
fail(`graph[${node}] transition to "${t.role}": prompt is required (non-empty string)`);
|
||||||
prompt: t.prompt ?? null,
|
}
|
||||||
}));
|
return {
|
||||||
|
role: t.role,
|
||||||
|
condition: t.condition ?? null,
|
||||||
|
prompt: t.prompt,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ function isTransition(value: unknown): boolean {
|
|||||||
const condition = value.condition;
|
const condition = value.condition;
|
||||||
return (
|
return (
|
||||||
typeof value.role === "string" &&
|
typeof value.role === "string" &&
|
||||||
|
typeof value.prompt === "string" &&
|
||||||
|
value.prompt.trim() !== "" &&
|
||||||
(condition === null || condition === undefined || typeof condition === "string")
|
(condition === null || condition === undefined || typeof condition === "string")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import type { AgentContext } from "@uncaged/workflow-agent-kit";
|
import type { AgentContext } from "@uncaged/workflow-agent-kit";
|
||||||
|
import type { ThreadId } from "@uncaged/workflow-protocol";
|
||||||
import { buildClaudeCodePrompt } from "../src/claude-code.js";
|
import { buildClaudeCodePrompt } from "../src/claude-code.js";
|
||||||
|
|
||||||
function makeCtx(overrides: Partial<AgentContext> = {}): AgentContext {
|
function makeCtx(overrides: Partial<AgentContext> = {}): AgentContext {
|
||||||
return {
|
return {
|
||||||
|
threadId: "01JTEST0000000000000000000" as ThreadId,
|
||||||
|
edgePrompt: "Proceed with the assigned role.",
|
||||||
workflow: {
|
workflow: {
|
||||||
roles: {
|
roles: {
|
||||||
developer: {
|
developer: {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function buildInitialPrompt(ctx: AgentContext): string {
|
|||||||
|
|
||||||
/** Assemble system prompt, task, and prior step outputs for Hermes. */
|
/** Assemble system prompt, task, and prior step outputs for Hermes. */
|
||||||
export function buildHermesPrompt(ctx: AgentContext): string {
|
export function buildHermesPrompt(ctx: AgentContext): string {
|
||||||
if (ctx.edgePrompt !== null) {
|
if (ctx.edgePrompt !== "") {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (ctx.outputFormatInstruction !== "") {
|
if (ctx.outputFormatInstruction !== "") {
|
||||||
parts.push(ctx.outputFormatInstruction, "");
|
parts.push(ctx.outputFormatInstruction, "");
|
||||||
@@ -86,7 +86,7 @@ async function prepareSession(
|
|||||||
ctx: AgentContext,
|
ctx: AgentContext,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
): Promise<PromptAttempt> {
|
): Promise<PromptAttempt> {
|
||||||
if (ctx.edgePrompt === null || isResumeDisabled()) {
|
if (ctx.edgePrompt === "" || isResumeDisabled()) {
|
||||||
await client.connect(cwd);
|
await client.connect(cwd);
|
||||||
return { useContinuation: false, resumed: false };
|
return { useContinuation: false, resumed: false };
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ export function createHermesAgent(): () => Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function runPrompt(ctx: AgentContext, useContinuation: boolean): Promise<AgentRunResult> {
|
async function runPrompt(ctx: AgentContext, useContinuation: boolean): Promise<AgentRunResult> {
|
||||||
const effectiveCtx = useContinuation ? ctx : { ...ctx, edgePrompt: null as string | null };
|
const effectiveCtx = useContinuation ? ctx : { ...ctx, edgePrompt: "" };
|
||||||
const fullPrompt = buildHermesPrompt(effectiveCtx);
|
const fullPrompt = buildHermesPrompt(effectiveCtx);
|
||||||
const { text, sessionId, messages } = await client.prompt(fullPrompt);
|
const { text, sessionId, messages } = await client.prompt(fullPrompt);
|
||||||
const { detailHash } = await storePromptResult(ctx.store, sessionId, messages);
|
const { detailHash } = await storePromptResult(ctx.store, sessionId, messages);
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ function fail(message: string): never {
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readEdgePrompt(): string {
|
||||||
|
const value = process.env.UWF_EDGE_PROMPT;
|
||||||
|
if (value === undefined || value === "") {
|
||||||
|
fail("UWF_EDGE_PROMPT environment variable is required");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRef): ChainState {
|
function walkChain(store: Store, schemas: AgentStore["schemas"], headHash: CasRef): ChainState {
|
||||||
const headNode = store.get(headHash);
|
const headNode = store.get(headHash);
|
||||||
if (headNode === null) {
|
if (headNode === null) {
|
||||||
@@ -133,7 +141,7 @@ export async function buildContext(threadId: ThreadId, role: string): Promise<Ag
|
|||||||
}
|
}
|
||||||
|
|
||||||
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
||||||
const edgePrompt = process.env.UWF_EDGE_PROMPT ?? null;
|
const edgePrompt = readEdgePrompt();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
threadId,
|
threadId,
|
||||||
@@ -180,7 +188,7 @@ export async function buildContextWithMeta(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
||||||
const edgePrompt = process.env.UWF_EDGE_PROMPT ?? null;
|
const edgePrompt = readEdgePrompt();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
threadId,
|
threadId,
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ export type AgentContext = ModeratorContext & {
|
|||||||
*/
|
*/
|
||||||
outputFormatInstruction: string;
|
outputFormatInstruction: string;
|
||||||
/**
|
/**
|
||||||
* Edge prompt from the graph transition that led to this role.
|
* Edge prompt from the graph transition that led to this role (UWF_EDGE_PROMPT).
|
||||||
* null on first entry (use full role definition), non-null on re-entry
|
* Phase 2 will use visit history to choose full role definition vs continuation.
|
||||||
* (use as continuation instruction from moderator).
|
|
||||||
*/
|
*/
|
||||||
edgePrompt: string | null;
|
edgePrompt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentRunResult = {
|
export type AgentRunResult = {
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ function stepsToPayload(name: string, description: string, steps: WorkFlowSteps)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const targetRole = t.target === "END" ? "$END" : t.target;
|
||||||
return {
|
return {
|
||||||
role: t.target === "END" ? "$END" : t.target,
|
role: targetRole,
|
||||||
condition: condName,
|
condition: condName,
|
||||||
|
prompt: `Transition to ${targetRole}.`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,7 +89,14 @@ function stepsToPayload(name: string, description: string, steps: WorkFlowSteps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (steps.length > 0) {
|
if (steps.length > 0) {
|
||||||
graph["$START"] = [{ role: steps[0].role.name, condition: null }];
|
const firstRole = steps[0].role.name;
|
||||||
|
graph["$START"] = [
|
||||||
|
{
|
||||||
|
role: firstRole,
|
||||||
|
condition: null,
|
||||||
|
prompt: `Begin workflow at role ${firstRole}.`,
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name, description, roles, conditions, graph };
|
return { name, description, roles, conditions, graph };
|
||||||
|
|||||||
@@ -9,27 +9,27 @@ const solveIssueWorkflow: WorkflowPayload = {
|
|||||||
roles: {
|
roles: {
|
||||||
planner: {
|
planner: {
|
||||||
description: "Creates implementation plan",
|
description: "Creates implementation plan",
|
||||||
identity: "You are a planning agent.",
|
goal: "You are a planning agent.",
|
||||||
prepare: "Review the issue context.",
|
capabilities: ["planning"],
|
||||||
execute: "Create a step-by-step plan.",
|
procedure: "Create a step-by-step plan.",
|
||||||
report: "Output the plan and steps.",
|
output: "Output the plan and steps.",
|
||||||
outputSchema: "5GWKR8TN1V3JA",
|
frontmatter: "5GWKR8TN1V3JA",
|
||||||
},
|
},
|
||||||
developer: {
|
developer: {
|
||||||
description: "Implements code changes",
|
description: "Implements code changes",
|
||||||
identity: "You are a developer agent.",
|
goal: "You are a developer agent.",
|
||||||
prepare: "Load coding tools.",
|
capabilities: ["coding"],
|
||||||
execute: "Implement the plan.",
|
procedure: "Implement the plan.",
|
||||||
report: "List files changed and summary.",
|
output: "List files changed and summary.",
|
||||||
outputSchema: "8CNWT4KR6D1HV",
|
frontmatter: "8CNWT4KR6D1HV",
|
||||||
},
|
},
|
||||||
reviewer: {
|
reviewer: {
|
||||||
description: "Reviews code changes",
|
description: "Reviews code changes",
|
||||||
identity: "You are a code reviewer.",
|
goal: "You are a code reviewer.",
|
||||||
prepare: "Review project conventions.",
|
capabilities: ["code-review"],
|
||||||
execute: "Review the implementation.",
|
procedure: "Review the implementation.",
|
||||||
report: "Approve or reject with comments.",
|
output: "Approve or reject with comments.",
|
||||||
outputSchema: "1VPBG9SM5E7WK",
|
frontmatter: "1VPBG9SM5E7WK",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
@@ -43,15 +43,35 @@ const solveIssueWorkflow: WorkflowPayload = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
graph: {
|
graph: {
|
||||||
$START: [{ role: "planner", condition: null }],
|
$START: [
|
||||||
planner: [
|
{
|
||||||
{ role: "developer", condition: "needsClarification" },
|
role: "planner",
|
||||||
{ role: "$END", condition: null },
|
condition: null,
|
||||||
|
prompt: "Start planning from the issue in the task.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
developer: [{ role: "reviewer", condition: null }],
|
|
||||||
reviewer: [
|
reviewer: [
|
||||||
{ role: "developer", condition: "rejected" },
|
{
|
||||||
{ role: "$END", condition: null },
|
role: "developer",
|
||||||
|
condition: "rejected",
|
||||||
|
prompt: "Reviewer rejected; return to developer.",
|
||||||
|
},
|
||||||
|
{ role: "$END", condition: null, prompt: "Review passed; end workflow." },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -69,7 +89,10 @@ function makeContext(steps: ModeratorContext["steps"]): ModeratorContext {
|
|||||||
describe("evaluate", () => {
|
describe("evaluate", () => {
|
||||||
test("$START → first role (fallback)", async () => {
|
test("$START → first role (fallback)", async () => {
|
||||||
const result = await evaluate(solveIssueWorkflow, makeContext([]));
|
const result = await evaluate(solveIssueWorkflow, makeContext([]));
|
||||||
expect(result).toEqual({ ok: true, value: { role: "planner", prompt: null } });
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { role: "planner", prompt: "Start planning from the issue in the task." },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("condition match (rejected → developer)", async () => {
|
test("condition match (rejected → developer)", async () => {
|
||||||
@@ -82,7 +105,10 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const result = await evaluate(solveIssueWorkflow, context);
|
const result = await evaluate(solveIssueWorkflow, context);
|
||||||
expect(result).toEqual({ ok: true, value: { role: "developer", prompt: null } });
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { role: "developer", prompt: "Reviewer rejected; return to developer." },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fallback when condition does not match → $END", async () => {
|
test("fallback when condition does not match → $END", async () => {
|
||||||
@@ -95,7 +121,10 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const result = await evaluate(solveIssueWorkflow, context);
|
const result = await evaluate(solveIssueWorkflow, context);
|
||||||
expect(result).toEqual({ ok: true, value: { role: "$END", prompt: null } });
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { role: "$END", prompt: "Review passed; end workflow." },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("missing role in graph → error", async () => {
|
test("missing role in graph → error", async () => {
|
||||||
@@ -124,7 +153,10 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const result = await evaluate(solveIssueWorkflow, context);
|
const result = await evaluate(solveIssueWorkflow, context);
|
||||||
expect(result).toEqual({ ok: true, value: { role: "developer", prompt: null } });
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { role: "developer", prompt: "Clarification is needed; hand off to developer." },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("$last returns most recent matching role's frontmatter", async () => {
|
test("$last returns most recent matching role's frontmatter", async () => {
|
||||||
@@ -137,10 +169,20 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
graph: {
|
graph: {
|
||||||
$START: [{ role: "developer", condition: null }],
|
$START: [
|
||||||
|
{
|
||||||
|
role: "developer",
|
||||||
|
condition: null,
|
||||||
|
prompt: "Begin development.",
|
||||||
|
},
|
||||||
|
],
|
||||||
developer: [
|
developer: [
|
||||||
{ role: "$END", condition: "devFailed" },
|
{ role: "$END", condition: "devFailed", prompt: "Development failed; end." },
|
||||||
{ role: "reviewer", condition: null },
|
{
|
||||||
|
role: "reviewer",
|
||||||
|
condition: null,
|
||||||
|
prompt: "Development succeeded; review.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -165,7 +207,10 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const result = await evaluate(workflow, context);
|
const result = await evaluate(workflow, context);
|
||||||
expect(result).toEqual({ ok: true, value: { role: "$END", prompt: null } });
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { role: "$END", prompt: "Development failed; end." },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("$first returns earliest matching role's frontmatter", async () => {
|
test("$first returns earliest matching role's frontmatter", async () => {
|
||||||
@@ -178,10 +223,20 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
graph: {
|
graph: {
|
||||||
$START: [{ role: "planner", condition: null }],
|
$START: [
|
||||||
|
{
|
||||||
|
role: "planner",
|
||||||
|
condition: null,
|
||||||
|
prompt: "Begin planning.",
|
||||||
|
},
|
||||||
|
],
|
||||||
planner: [
|
planner: [
|
||||||
{ role: "$END", condition: "firstPlanReady" },
|
{ role: "$END", condition: "firstPlanReady", prompt: "First plan was ready; end." },
|
||||||
{ role: "developer", condition: null },
|
{
|
||||||
|
role: "developer",
|
||||||
|
condition: null,
|
||||||
|
prompt: "Plan not ready on first pass; implement.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -206,7 +261,10 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const result = await evaluate(workflow, context);
|
const result = await evaluate(workflow, context);
|
||||||
expect(result).toEqual({ ok: true, value: { role: "$END", prompt: null } });
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { role: "$END", prompt: "First plan was ready; end." },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("$last returns undefined for unmatched role", async () => {
|
test("$last returns undefined for unmatched role", async () => {
|
||||||
@@ -219,10 +277,20 @@ describe("evaluate", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
graph: {
|
graph: {
|
||||||
$START: [{ role: "planner", condition: null }],
|
$START: [
|
||||||
|
{
|
||||||
|
role: "planner",
|
||||||
|
condition: null,
|
||||||
|
prompt: "Begin planning.",
|
||||||
|
},
|
||||||
|
],
|
||||||
planner: [
|
planner: [
|
||||||
{ role: "$END", condition: "hasReviewer" },
|
{ role: "$END", condition: "hasReviewer", prompt: "Reviewer already ran; end." },
|
||||||
{ role: "developer", condition: null },
|
{
|
||||||
|
role: "developer",
|
||||||
|
condition: null,
|
||||||
|
prompt: "No reviewer yet; implement.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -236,6 +304,9 @@ describe("evaluate", () => {
|
|||||||
]);
|
]);
|
||||||
const result = await evaluate(workflow, context);
|
const result = await evaluate(workflow, context);
|
||||||
// no reviewer step → $exists returns false → fallback to developer
|
// no reviewer step → $exists returns false → fallback to developer
|
||||||
expect(result).toEqual({ ok: true, value: { role: "developer", prompt: null } });
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { role: "developer", prompt: "No reviewer yet; implement." },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export async function evaluate(
|
|||||||
|
|
||||||
for (const transition of transitions) {
|
for (const transition of transitions) {
|
||||||
if (transition.condition === null) {
|
if (transition.condition === null) {
|
||||||
return { ok: true, value: { role: transition.role, prompt: transition.prompt ?? null } };
|
return { ok: true, value: { role: transition.role, prompt: transition.prompt } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const conditionDef = workflow.conditions[transition.condition];
|
const conditionDef = workflow.conditions[transition.condition];
|
||||||
@@ -106,7 +106,7 @@ export async function evaluate(
|
|||||||
return evalResult;
|
return evalResult;
|
||||||
}
|
}
|
||||||
if (isTruthy(evalResult.value)) {
|
if (isTruthy(evalResult.value)) {
|
||||||
return { ok: true, value: { role: transition.role, prompt: transition.prompt ?? null } };
|
return { ok: true, value: { role: transition.role, prompt: transition.prompt } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
||||||
|
|
||||||
/** The result of moderator evaluation — which role to go to, and the edge prompt (if any). */
|
/** The result of moderator evaluation — which role to go to, and the edge prompt. */
|
||||||
export type EvaluateResult = {
|
export type EvaluateResult = {
|
||||||
role: string;
|
role: string;
|
||||||
prompt: string | null;
|
prompt: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ const CONDITION_DEFINITION: JSONSchema = {
|
|||||||
|
|
||||||
const TRANSITION: JSONSchema = {
|
const TRANSITION: JSONSchema = {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["role", "condition"],
|
required: ["role", "condition", "prompt"],
|
||||||
properties: {
|
properties: {
|
||||||
role: { type: "string" },
|
role: { type: "string" },
|
||||||
condition: { anyOf: [{ type: "string" }, { type: "null" }] },
|
condition: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||||
|
prompt: { type: "string" },
|
||||||
},
|
},
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export type RoleDefinition = {
|
|||||||
export type Transition = {
|
export type Transition = {
|
||||||
role: string;
|
role: string;
|
||||||
condition: string | null;
|
condition: string | null;
|
||||||
prompt: string | null;
|
prompt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConditionDefinition = {
|
export type ConditionDefinition = {
|
||||||
|
|||||||
Reference in New Issue
Block a user