feat(protocol): add edge prompt to Transition + EvaluateResult (#402)

- Transition type gains prompt: string | null
- evaluate() returns EvaluateResult { role, prompt } instead of string
- normalizeGraph coerces prompt: undefined → null
- spawnAgent passes edge prompt via UWF_EDGE_PROMPT env
- AgentContext gains edgePrompt field

Refs #402
This commit is contained in:
2026-05-23 03:49:15 +00:00
parent d5d05334f5
commit 1a06e014f5
8 changed files with 33 additions and 9 deletions
+10 -5
View File
@@ -624,13 +624,17 @@ function resolveAgentConfig(
return agentConfig; return agentConfig;
} }
function spawnAgent(agent: AgentConfig, threadId: ThreadId, role: string): CasRef { function spawnAgent(agent: AgentConfig, threadId: ThreadId, role: string, edgePrompt: string | null): CasRef {
const argv = [...agent.args, threadId, role]; const argv = [...agent.args, threadId, role];
const env = { ...process.env };
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, {
encoding: "utf8", encoding: "utf8",
env: process.env, env,
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
}); });
} catch (e) { } catch (e) {
@@ -712,7 +716,7 @@ async function cmdThreadStepOnce(
fail(nextResult.error.message); fail(nextResult.error.message);
} }
if (nextResult.value === END_ROLE) { if (nextResult.value.role === END_ROLE) {
await archiveThread(storageRoot, threadId, workflowHash, headHash); await archiveThread(storageRoot, threadId, workflowHash, headHash);
return { return {
workflow: workflowHash, workflow: workflowHash,
@@ -722,12 +726,13 @@ async function cmdThreadStepOnce(
}; };
} }
const role = nextResult.value; const role = nextResult.value.role;
const edgePrompt = nextResult.value.prompt;
const config = await loadWorkflowConfig(storageRoot); const config = await loadWorkflowConfig(storageRoot);
const agent = resolveAgentConfig(config, workflow, role, agentOverride); const agent = resolveAgentConfig(config, workflow, role, agentOverride);
loadDotenv({ path: getEnvPath(storageRoot) }); loadDotenv({ path: getEnvPath(storageRoot) });
const newHead = spawnAgent(agent, threadId, role); const newHead = spawnAgent(agent, threadId, role, edgePrompt);
// Re-create store to pick up nodes written by the agent subprocess // Re-create store to pick up nodes written by the agent subprocess
const uwfAfter = await createUwfStore(storageRoot); const uwfAfter = await createUwfStore(storageRoot);
@@ -58,6 +58,7 @@ function normalizeGraph(graph: Record<string, Transition[]>): Record<string, Tra
result[node] = transitions.map((t) => ({ result[node] = transitions.map((t) => ({
role: t.role, role: t.role,
condition: t.condition ?? null, condition: t.condition ?? null,
prompt: t.prompt ?? null,
})); }));
} }
return result; return result;
@@ -133,6 +133,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;
return { return {
threadId, threadId,
@@ -142,6 +143,7 @@ export async function buildContext(threadId: ThreadId, role: string): Promise<Ag
workflow, workflow,
store, store,
outputFormatInstruction: "", outputFormatInstruction: "",
edgePrompt,
}; };
} }
@@ -178,6 +180,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;
return { return {
threadId, threadId,
@@ -187,6 +190,7 @@ export async function buildContextWithMeta(
workflow, workflow,
store, store,
outputFormatInstruction: "", outputFormatInstruction: "",
edgePrompt,
meta: { storageRoot, store, schemas, headHash, chain }, meta: { storageRoot, store, schemas, headHash, chain },
}; };
} }
+6
View File
@@ -12,6 +12,12 @@ export type AgentContext = ModeratorContext & {
* role's output schema. Populated by `createAgent` at run time. * role's output schema. Populated by `createAgent` at run time.
*/ */
outputFormatInstruction: string; outputFormatInstruction: string;
/**
* Edge prompt from the graph transition that led to this role.
* null on first entry (use full role definition), non-null on re-entry
* (use as continuation instruction from moderator).
*/
edgePrompt: string | null;
}; };
export type AgentRunResult = { export type AgentRunResult = {
+4 -4
View File
@@ -1,7 +1,7 @@
import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol"; import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol";
import jsonata from "jsonata"; import jsonata from "jsonata";
import type { Result } from "./types.js"; import type { EvaluateResult, Result } from "./types.js";
const START_ROLE = "$START"; const START_ROLE = "$START";
@@ -78,7 +78,7 @@ function currentRole(context: ModeratorContext): string {
export async function evaluate( export async function evaluate(
workflow: WorkflowPayload, workflow: WorkflowPayload,
context: ModeratorContext, context: ModeratorContext,
): Promise<Result<string, Error>> { ): Promise<Result<EvaluateResult, Error>> {
const role = currentRole(context); const role = currentRole(context);
const transitions = workflow.graph[role]; const transitions = workflow.graph[role];
if (transitions === undefined) { if (transitions === undefined) {
@@ -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: transition.role }; 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: transition.role }; return { ok: true, value: { role: transition.role, prompt: transition.prompt } };
} }
} }
+1
View File
@@ -1 +1,2 @@
export { evaluate } from "./evaluate.js"; export { evaluate } from "./evaluate.js";
export type { EvaluateResult } from "./types.js";
+6
View File
@@ -1 +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). */
export type EvaluateResult = {
role: string;
prompt: string | null;
};
+1
View File
@@ -28,6 +28,7 @@ export type RoleDefinition = {
export type Transition = { export type Transition = {
role: string; role: string;
condition: string | null; condition: string | null;
prompt: string | null;
}; };
export type ConditionDefinition = { export type ConditionDefinition = {