From 638329a5623253555b4c1590e886c9a56bfa0ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Sat, 23 May 2026 03:57:04 +0000 Subject: [PATCH] feat: edge prompt + session resume implementation (#402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- examples/solve-issue.yaml | 5 + packages/cli-workflow/src/commands/thread.ts | 2 +- packages/workflow-agent-hermes/package.json | 4 +- .../workflow-agent-hermes/src/acp-client.ts | 126 +++++++++++------- packages/workflow-agent-hermes/src/hermes.ts | 110 +++++++++++++-- .../src/session-cache.ts | 70 ++++++++++ .../build-continuation-prompt.test.ts | 70 ++++++++++ .../src/build-continuation-prompt.ts | 53 ++++++++ packages/workflow-agent-kit/src/index.ts | 3 +- .../__tests__/evaluate.test.ts | 14 +- packages/workflow-moderator/src/evaluate.ts | 4 +- 11 files changed, 391 insertions(+), 70 deletions(-) create mode 100644 packages/workflow-agent-hermes/src/session-cache.ts create mode 100644 packages/workflow-agent-kit/__tests__/build-continuation-prompt.test.ts create mode 100644 packages/workflow-agent-kit/src/build-continuation-prompt.ts diff --git a/examples/solve-issue.yaml b/examples/solve-issue.yaml index 4e5d65b..fd793e8 100644 --- a/examples/solve-issue.yaml +++ b/examples/solve-issue.yaml @@ -62,14 +62,19 @@ graph: $START: - role: "planner" condition: null + prompt: null planner: - role: "developer" condition: null + prompt: null developer: - role: "reviewer" condition: null + prompt: null reviewer: - role: "developer" condition: "notApproved" + prompt: "The reviewer rejected your implementation. Read their feedback and fix the issues." - role: "$END" condition: null + prompt: null diff --git a/packages/cli-workflow/src/commands/thread.ts b/packages/cli-workflow/src/commands/thread.ts index 01432c7..ed54c1e 100644 --- a/packages/cli-workflow/src/commands/thread.ts +++ b/packages/cli-workflow/src/commands/thread.ts @@ -753,7 +753,7 @@ async function cmdThreadStepOnce( fail(afterResult.error.message); } - const done = afterResult.value === END_ROLE; + const done = afterResult.value.role === END_ROLE; if (done) { await archiveThread(storageRoot, threadId, workflowHash, newHead); } diff --git a/packages/workflow-agent-hermes/package.json b/packages/workflow-agent-hermes/package.json index 62f3bd6..d8a1fb3 100644 --- a/packages/workflow-agent-hermes/package.json +++ b/packages/workflow-agent-hermes/package.json @@ -22,7 +22,9 @@ }, "dependencies": { "@uncaged/json-cas": "^0.4.0", - "@uncaged/workflow-agent-kit": "workspace:^" + "@uncaged/workflow-agent-kit": "workspace:^", + "@uncaged/workflow-protocol": "workspace:^", + "@uncaged/workflow-util": "workspace:^" }, "devDependencies": { "typescript": "^5.8.3" diff --git a/packages/workflow-agent-hermes/src/acp-client.ts b/packages/workflow-agent-hermes/src/acp-client.ts index 1144d8c..400c7c6 100644 --- a/packages/workflow-agent-hermes/src/acp-client.ts +++ b/packages/workflow-agent-hermes/src/acp-client.ts @@ -46,53 +46,8 @@ export class HermesAcpClient { /** Spawn hermes acp, initialize, create session */ async connect(cwd: string): Promise { - const child = spawn(HERMES_COMMAND, ["acp"], { - env: process.env, - shell: false, - stdio: ["pipe", "pipe", "pipe"], - }); - - this.process = child; - - child.stderr?.on("data", (chunk: Buffer) => { - this.stderrBuffer += chunk.toString(); - }); - - child.on("error", (cause) => { - const message = cause instanceof Error ? cause.message : String(cause); - this.rejectAll(new Error(`hermes acp spawn failed: ${message}`)); - }); - - child.on("close", (code) => { - if (code !== 0 && this.pending.size > 0) { - const detail = this.stderrBuffer.trim() !== "" ? ` stderr=${this.stderrBuffer.trim()}` : ""; - this.rejectAll( - new Error(`hermes acp exited unexpectedly with code ${code ?? "null"}${detail}`), - ); - } - }); - - if (child.stdout === null) { - throw new Error("hermes acp process stdout is not available"); - } - const rl = createInterface({ input: child.stdout }); - rl.on("line", (line) => { - this.handleLine(line.trim()); - }); - - const initResponse = await this.sendRequest("initialize", { - protocolVersion: PROTOCOL_VERSION, - clientInfo: { name: "uwf", version: "0.1.0" }, - capabilities: {}, - }); - - if ((initResponse as { error?: unknown }).error !== undefined) { - throw new Error( - `initialize failed: ${JSON.stringify((initResponse as { error: unknown }).error)}`, - ); - } - - this.sendNotification("initialized"); + await this.ensureProcess(); + await this.initialize(); const sessionResponse = (await this.sendRequest("session/new", { cwd, @@ -108,6 +63,27 @@ export class HermesAcpClient { return sessionId; } + /** Spawn hermes acp, initialize, resume an existing session */ + async resume(sessionId: string, cwd: string): Promise { + await this.ensureProcess(); + await this.initialize(); + + const response = await this.sendRequest("session/resume", { + cwd, + sessionId, + mcpServers: [], + }); + + if ((response as { error?: unknown }).error !== undefined) { + throw new Error( + `session/resume failed: ${JSON.stringify((response as { error: unknown }).error)}`, + ); + } + + this.sessionId = sessionId; + return sessionId; + } + /** Send prompt and collect full response text + structured messages. */ async prompt(text: string): Promise { if (this.sessionId === null) { @@ -358,4 +334,60 @@ export class HermesAcpClient { } this.pending.clear(); } + + private async ensureProcess(): Promise { + if (this.process !== null) { + return; + } + + const child = spawn(HERMES_COMMAND, ["acp"], { + env: process.env, + shell: false, + stdio: ["pipe", "pipe", "pipe"], + }); + + this.process = child; + + child.stderr?.on("data", (chunk: Buffer) => { + this.stderrBuffer += chunk.toString(); + }); + + child.on("error", (cause) => { + const message = cause instanceof Error ? cause.message : String(cause); + this.rejectAll(new Error(`hermes acp spawn failed: ${message}`)); + }); + + child.on("close", (code) => { + if (code !== 0 && this.pending.size > 0) { + const detail = this.stderrBuffer.trim() !== "" ? ` stderr=${this.stderrBuffer.trim()}` : ""; + this.rejectAll( + new Error(`hermes acp exited unexpectedly with code ${code ?? "null"}${detail}`), + ); + } + }); + + if (child.stdout === null) { + throw new Error("hermes acp process stdout is not available"); + } + const rl = createInterface({ input: child.stdout }); + rl.on("line", (line) => { + this.handleLine(line.trim()); + }); + } + + private async initialize(): Promise { + const initResponse = await this.sendRequest("initialize", { + protocolVersion: PROTOCOL_VERSION, + clientInfo: { name: "uwf", version: "0.1.0" }, + capabilities: {}, + }); + + if ((initResponse as { error?: unknown }).error !== undefined) { + throw new Error( + `initialize failed: ${JSON.stringify((initResponse as { error: unknown }).error)}`, + ); + } + + this.sendNotification("initialized"); + } } diff --git a/packages/workflow-agent-hermes/src/hermes.ts b/packages/workflow-agent-hermes/src/hermes.ts index 547b312..a1ba4c5 100644 --- a/packages/workflow-agent-hermes/src/hermes.ts +++ b/packages/workflow-agent-hermes/src/hermes.ts @@ -1,15 +1,19 @@ import type { Store } from "@uncaged/json-cas"; - import { type AgentContext, type AgentRunResult, + buildContinuationPrompt, buildRolePrompt, createAgent, } from "@uncaged/workflow-agent-kit"; +import { createLogger } from "@uncaged/workflow-util"; import { HermesAcpClient } from "./acp-client.js"; +import { getCachedSessionId, isResumeDisabled, setCachedSessionId } from "./session-cache.js"; import { storeHermesSessionDetail } from "./session-detail.js"; +const log = createLogger({ sink: { kind: "stderr" } }); + function buildHistorySummary(steps: AgentContext["steps"]): string { if (steps.length === 0) { return ""; @@ -29,12 +33,11 @@ function buildHistorySummary(steps: AgentContext["steps"]): string { return lines.join("\n"); } -/** Assemble system prompt, task, and prior step outputs for Hermes. */ -export function buildHermesPrompt(ctx: AgentContext): string { +function buildInitialPrompt(ctx: AgentContext): string { const roleDef = ctx.workflow.roles[ctx.role]; const rolePrompt = roleDef !== undefined ? buildRolePrompt(roleDef) : ""; const parts: string[] = []; - if (ctx.outputFormatInstruction !== undefined && ctx.outputFormatInstruction !== "") { + if (ctx.outputFormatInstruction !== "") { parts.push(ctx.outputFormatInstruction, ""); } parts.push(rolePrompt, "", "## Task", ctx.start.prompt); @@ -45,6 +48,69 @@ export function buildHermesPrompt(ctx: AgentContext): string { return parts.join("\n"); } +/** Assemble system prompt, task, and prior step outputs for Hermes. */ +export function buildHermesPrompt(ctx: AgentContext): string { + if (ctx.edgePrompt !== null) { + const parts: string[] = []; + if (ctx.outputFormatInstruction !== "") { + parts.push(ctx.outputFormatInstruction, ""); + } + parts.push(buildContinuationPrompt(ctx.steps, ctx.role, ctx.edgePrompt)); + return parts.join("\n"); + } + + return buildInitialPrompt(ctx); +} + +async function storePromptResult( + store: Store, + sessionId: string, + messages: Awaited>["messages"], +): Promise<{ detailHash: string }> { + const session = { + session_id: sessionId, + model: "", + session_start: new Date().toISOString(), + messages, + }; + return storeHermesSessionDetail(store, session); +} + +type PromptAttempt = { + useContinuation: boolean; + resumed: boolean; +}; + +async function prepareSession( + client: HermesAcpClient, + ctx: AgentContext, + cwd: string, +): Promise { + if (ctx.edgePrompt === null || isResumeDisabled()) { + await client.connect(cwd); + return { useContinuation: false, resumed: false }; + } + + const cachedSessionId = await getCachedSessionId(ctx.threadId, ctx.role); + if (cachedSessionId === null) { + log("6RWK3N8Q", `no cached session for ${ctx.threadId}:${ctx.role}, starting new session`); + await client.connect(cwd); + return { useContinuation: false, resumed: false }; + } + + try { + await client.resume(cachedSessionId, cwd); + log("9MHT4V2P", `resumed hermes session ${cachedSessionId} for ${ctx.threadId}:${ctx.role}`); + return { useContinuation: true, resumed: true }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log("3XPN7K4W", `session resume failed, falling back to new session: ${message}`); + await client.close(); + await client.connect(cwd); + return { useContinuation: false, resumed: false }; + } +} + /** * Agent CLI factory: parses argv, runs Hermes, extracts output, writes StepNode. * @@ -60,15 +126,38 @@ export function createHermesAgent(): () => Promise { void client.close(); }); - async function runHermes(ctx: AgentContext): Promise { - const fullPrompt = buildHermesPrompt(ctx); - await client.connect(process.cwd()); + async function runPrompt(ctx: AgentContext, useContinuation: boolean): Promise { + const effectiveCtx = useContinuation ? ctx : { ...ctx, edgePrompt: null as string | null }; + const fullPrompt = buildHermesPrompt(effectiveCtx); const { text, sessionId, messages } = await client.prompt(fullPrompt); - const session = { session_id: sessionId, model: "", session_start: new Date().toISOString(), messages }; - const { detailHash } = await storeHermesSessionDetail(ctx.store, session); + const { detailHash } = await storePromptResult(ctx.store, sessionId, messages); + + if (!isResumeDisabled()) { + await setCachedSessionId(ctx.threadId, ctx.role, sessionId); + } + return { output: text, detailHash, sessionId }; } + async function runHermes(ctx: AgentContext): Promise { + const cwd = process.cwd(); + const attempt = await prepareSession(client, ctx, cwd); + + try { + return await runPrompt(ctx, attempt.useContinuation); + } catch (error) { + if (!attempt.resumed) { + throw error; + } + + const message = error instanceof Error ? error.message : String(error); + log("8FQW2R6N", `continuation prompt failed, retrying with initial prompt: ${message}`); + await client.close(); + await client.connect(cwd); + return runPrompt(ctx, false); + } + } + async function continueHermes( _sessionId: string, message: string, @@ -77,8 +166,7 @@ export function createHermesAgent(): () => Promise { // Client is already connected from runHermes — same ACP session, // so the agent sees the full conversation history (crucial for retries). const { text, sessionId, messages } = await client.prompt(message); - const session = { session_id: sessionId, model: "", session_start: new Date().toISOString(), messages }; - const { detailHash } = await storeHermesSessionDetail(store, session); + const { detailHash } = await storePromptResult(store, sessionId, messages); return { output: text, detailHash, sessionId }; } diff --git a/packages/workflow-agent-hermes/src/session-cache.ts b/packages/workflow-agent-hermes/src/session-cache.ts new file mode 100644 index 0000000..20af7d2 --- /dev/null +++ b/packages/workflow-agent-hermes/src/session-cache.ts @@ -0,0 +1,70 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +import { resolveStorageRoot } from "@uncaged/workflow-agent-kit"; +import type { ThreadId } from "@uncaged/workflow-protocol"; + +type HermesSessionCache = Record; + +function getCachePath(): string { + return join(resolveStorageRoot(), "cache", "hermes-sessions.json"); +} + +function cacheKey(threadId: ThreadId, role: string): string { + return `${threadId}:${role}`; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +async function readCache(): Promise { + const path = getCachePath(); + try { + const text = await readFile(path, "utf8"); + const raw = JSON.parse(text) as unknown; + if (!isRecord(raw)) { + return {}; + } + const cache: HermesSessionCache = {}; + for (const [key, value] of Object.entries(raw)) { + if (typeof value === "string" && value !== "") { + cache[key] = value; + } + } + return cache; + } catch (e) { + const err = e as NodeJS.ErrnoException; + if (err.code === "ENOENT") { + return {}; + } + throw e; + } +} + +async function writeCache(cache: HermesSessionCache): Promise { + const path = getCachePath(); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(cache, null, 2)}\n`, "utf8"); +} + +export function isResumeDisabled(): boolean { + const flag = process.env.UWF_NO_RESUME; + return flag !== undefined && flag !== ""; +} + +export async function getCachedSessionId(threadId: ThreadId, role: string): Promise { + const cache = await readCache(); + const sessionId = cache[cacheKey(threadId, role)]; + return sessionId ?? null; +} + +export async function setCachedSessionId( + threadId: ThreadId, + role: string, + sessionId: string, +): Promise { + const cache = await readCache(); + cache[cacheKey(threadId, role)] = sessionId; + await writeCache(cache); +} diff --git a/packages/workflow-agent-kit/__tests__/build-continuation-prompt.test.ts b/packages/workflow-agent-kit/__tests__/build-continuation-prompt.test.ts new file mode 100644 index 0000000..8f58483 --- /dev/null +++ b/packages/workflow-agent-kit/__tests__/build-continuation-prompt.test.ts @@ -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."); + }); +}); diff --git a/packages/workflow-agent-kit/src/build-continuation-prompt.ts b/packages/workflow-agent-kit/src/build-continuation-prompt.ts new file mode 100644 index 0000000..472aa24 --- /dev/null +++ b/packages/workflow-agent-kit/src/build-continuation-prompt.ts @@ -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"); +} diff --git a/packages/workflow-agent-kit/src/index.ts b/packages/workflow-agent-kit/src/index.ts index 97fa132..2ef24a2 100644 --- a/packages/workflow-agent-kit/src/index.ts +++ b/packages/workflow-agent-kit/src/index.ts @@ -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, diff --git a/packages/workflow-moderator/__tests__/evaluate.test.ts b/packages/workflow-moderator/__tests__/evaluate.test.ts index 01fdd5d..6cf4872 100644 --- a/packages/workflow-moderator/__tests__/evaluate.test.ts +++ b/packages/workflow-moderator/__tests__/evaluate.test.ts @@ -69,7 +69,7 @@ function makeContext(steps: ModeratorContext["steps"]): ModeratorContext { describe("evaluate", () => { test("$START → first role (fallback)", async () => { const result = await evaluate(solveIssueWorkflow, makeContext([])); - expect(result).toEqual({ ok: true, value: "planner" }); + expect(result).toEqual({ ok: true, value: { role: "planner", prompt: null } }); }); test("condition match (rejected → developer)", async () => { @@ -82,7 +82,7 @@ describe("evaluate", () => { }, ]); const result = await evaluate(solveIssueWorkflow, context); - expect(result).toEqual({ ok: true, value: "developer" }); + expect(result).toEqual({ ok: true, value: { role: "developer", prompt: null } }); }); test("fallback when condition does not match → $END", async () => { @@ -95,7 +95,7 @@ describe("evaluate", () => { }, ]); const result = await evaluate(solveIssueWorkflow, context); - expect(result).toEqual({ ok: true, value: "$END" }); + expect(result).toEqual({ ok: true, value: { role: "$END", prompt: null } }); }); test("missing role in graph → error", async () => { @@ -124,7 +124,7 @@ describe("evaluate", () => { }, ]); const result = await evaluate(solveIssueWorkflow, context); - expect(result).toEqual({ ok: true, value: "developer" }); + expect(result).toEqual({ ok: true, value: { role: "developer", prompt: null } }); }); test("$last returns most recent matching role's frontmatter", async () => { @@ -165,7 +165,7 @@ describe("evaluate", () => { }, ]); const result = await evaluate(workflow, context); - expect(result).toEqual({ ok: true, value: "$END" }); + expect(result).toEqual({ ok: true, value: { role: "$END", prompt: null } }); }); test("$first returns earliest matching role's frontmatter", async () => { @@ -206,7 +206,7 @@ describe("evaluate", () => { }, ]); const result = await evaluate(workflow, context); - expect(result).toEqual({ ok: true, value: "$END" }); + expect(result).toEqual({ ok: true, value: { role: "$END", prompt: null } }); }); test("$last returns undefined for unmatched role", async () => { @@ -236,6 +236,6 @@ describe("evaluate", () => { ]); const result = await evaluate(workflow, context); // no reviewer step → $exists returns false → fallback to developer - expect(result).toEqual({ ok: true, value: "developer" }); + expect(result).toEqual({ ok: true, value: { role: "developer", prompt: null } }); }); }); diff --git a/packages/workflow-moderator/src/evaluate.ts b/packages/workflow-moderator/src/evaluate.ts index a515cd0..c2ffa9a 100644 --- a/packages/workflow-moderator/src/evaluate.ts +++ b/packages/workflow-moderator/src/evaluate.ts @@ -90,7 +90,7 @@ export async function evaluate( for (const transition of transitions) { if (transition.condition === null) { - return { ok: true, value: { role: transition.role, prompt: transition.prompt } }; + return { ok: true, value: { role: transition.role, prompt: transition.prompt ?? null } }; } const conditionDef = workflow.conditions[transition.condition]; @@ -106,7 +106,7 @@ export async function evaluate( return evalResult; } if (isTruthy(evalResult.value)) { - return { ok: true, value: { role: transition.role, prompt: transition.prompt } }; + return { ok: true, value: { role: transition.role, prompt: transition.prompt ?? null } }; } }