feat: edge prompt + session resume implementation (#402)

- buildContinuationPrompt: incremental prompt for role re-entry
- buildHermesPrompt: dual-mode (initial vs continuation)
- session-cache: thread:role → hermes sessionId mapping
- HermesAcpClient.resume(): session/resume JSON-RPC
- Fallback: cache miss or resume fail → initial prompt
- UWF_NO_RESUME env to skip cache
- solve-issue.yaml: reviewer→developer edge prompt
- Tests updated for EvaluateResult + continuation prompt

Refs #402
This commit is contained in:
2026-05-23 03:57:04 +00:00
parent 1a06e014f5
commit 638329a562
11 changed files with 391 additions and 70 deletions
@@ -0,0 +1,70 @@
import type { StepContext } from "@uncaged/workflow-protocol";
import { describe, expect, test } from "vitest";
import { buildContinuationPrompt } from "../src/build-continuation-prompt.js";
const reviewerStep: StepContext = {
role: "reviewer",
output: { approved: false, comments: "Missing tests" },
detail: "2MXBG6PN4A8JR",
agent: "uwf-hermes",
};
const developerStep: StepContext = {
role: "developer",
output: { filesChanged: ["src/app.ts"], summary: "Initial fix" },
detail: "1VPBG9SM5E7WK",
agent: "uwf-hermes",
};
describe("buildContinuationPrompt", () => {
test("includes steps after the last matching role and the edge prompt", () => {
const steps: StepContext[] = [
developerStep,
reviewerStep,
{
role: "planner",
output: { plan: "revise approach" },
detail: "7BQST3VW9F2MA",
agent: "uwf-hermes",
},
];
const result = buildContinuationPrompt(
steps,
"developer",
"The reviewer rejected your implementation. Read their feedback and fix the issues.",
);
expect(result).toContain("## What Happened Since Your Last Turn");
expect(result).toContain("### Step 2: reviewer");
expect(result).toContain("Missing tests");
expect(result).toContain("### Step 3: planner");
expect(result).toContain("## Moderator Instruction");
expect(result).toContain("The reviewer rejected your implementation.");
expect(result).not.toContain("Initial fix");
});
test("uses all steps when the role has not run before", () => {
const result = buildContinuationPrompt(
[developerStep, reviewerStep],
"planner",
"Continue from the reviewer feedback.",
);
expect(result).toContain("### Step 1: developer");
expect(result).toContain("### Step 2: reviewer");
expect(result).toContain("Continue from the reviewer feedback.");
});
test("still includes moderator instruction when there are no intervening steps", () => {
const result = buildContinuationPrompt(
[developerStep],
"developer",
"Please revise your work.",
);
expect(result).not.toContain("## What Happened Since Your Last Turn");
expect(result).toContain("## Moderator Instruction");
expect(result).toContain("Please revise your work.");
});
});
@@ -0,0 +1,53 @@
import type { StepContext } from "@uncaged/workflow-protocol";
function formatStep(step: StepContext, stepNumber: number): string {
return [
`### Step ${stepNumber}: ${step.role}`,
`Output: ${JSON.stringify(step.output)}`,
`Agent: ${step.agent}`,
].join("\n");
}
function findLastRoleIndex(steps: StepContext[], role: string): number {
for (let i = steps.length - 1; i >= 0; i--) {
const step = steps[i];
if (step !== undefined && step.role === role) {
return i;
}
}
return -1;
}
/**
* Build a continuation prompt for a role re-entry.
*
* Finds the most recent step for `role`, collects everything after it as context,
* and appends the moderator edge prompt as the instruction.
*/
export function buildContinuationPrompt(
steps: StepContext[],
role: string,
edgePrompt: string,
): string {
const lastIndex = findLastRoleIndex(steps, role);
const sinceSteps = lastIndex >= 0 ? steps.slice(lastIndex + 1) : steps;
const parts: string[] = [];
if (sinceSteps.length > 0) {
parts.push("## What Happened Since Your Last Turn");
const baseStepNumber = lastIndex >= 0 ? lastIndex + 2 : 1;
for (let i = 0; i < sinceSteps.length; i++) {
const step = sinceSteps[i];
if (step === undefined) {
continue;
}
parts.push("");
parts.push(formatStep(step, baseStepNumber + i));
}
parts.push("");
}
parts.push("## Moderator Instruction", "", edgePrompt);
return parts.join("\n");
}
+2 -1
View File
@@ -1,3 +1,4 @@
export { buildContinuationPrompt } from "./build-continuation-prompt.js";
export { buildOutputFormatInstruction } from "./build-output-format-instruction.js";
export { buildRolePrompt } from "./build-role-prompt.js";
export type { BuildContextMeta } from "./context.js";
@@ -11,7 +12,7 @@ export {
export type { FrontmatterFastPathResult } from "./frontmatter.js";
export { tryFrontmatterFastPath } from "./frontmatter.js";
export { createAgent } from "./run.js";
export { getConfigPath, getEnvPath, loadWorkflowConfig } from "./storage.js";
export { getConfigPath, getEnvPath, loadWorkflowConfig, resolveStorageRoot } from "./storage.js";
export type {
AgentContext,
AgentContinueFn,