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;
}
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 env = { ...process.env };
if (edgePrompt !== null) {
env.UWF_EDGE_PROMPT = edgePrompt;
}
let stdout: string;
try {
stdout = execFileSync(agent.command, argv, {
encoding: "utf8",
env: process.env,
env,
stdio: ["ignore", "pipe", "pipe"],
});
} catch (e) {
@@ -712,7 +716,7 @@ async function cmdThreadStepOnce(
fail(nextResult.error.message);
}
if (nextResult.value === END_ROLE) {
if (nextResult.value.role === END_ROLE) {
await archiveThread(storageRoot, threadId, workflowHash, headHash);
return {
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 agent = resolveAgentConfig(config, workflow, role, agentOverride);
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
const uwfAfter = await createUwfStore(storageRoot);
@@ -58,6 +58,7 @@ function normalizeGraph(graph: Record<string, Transition[]>): Record<string, Tra
result[node] = transitions.map((t) => ({
role: t.role,
condition: t.condition ?? null,
prompt: t.prompt ?? null,
}));
}
return result;
@@ -133,6 +133,7 @@ export async function buildContext(threadId: ThreadId, role: string): Promise<Ag
}
const steps = await buildHistory(store, chain.stepsNewestFirst);
const edgePrompt = process.env.UWF_EDGE_PROMPT ?? null;
return {
threadId,
@@ -142,6 +143,7 @@ export async function buildContext(threadId: ThreadId, role: string): Promise<Ag
workflow,
store,
outputFormatInstruction: "",
edgePrompt,
};
}
@@ -178,6 +180,7 @@ export async function buildContextWithMeta(
}
const steps = await buildHistory(store, chain.stepsNewestFirst);
const edgePrompt = process.env.UWF_EDGE_PROMPT ?? null;
return {
threadId,
@@ -187,6 +190,7 @@ export async function buildContextWithMeta(
workflow,
store,
outputFormatInstruction: "",
edgePrompt,
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.
*/
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 = {
+4 -4
View File
@@ -1,7 +1,7 @@
import type { ModeratorContext, WorkflowPayload } from "@uncaged/workflow-protocol";
import jsonata from "jsonata";
import type { Result } from "./types.js";
import type { EvaluateResult, Result } from "./types.js";
const START_ROLE = "$START";
@@ -78,7 +78,7 @@ function currentRole(context: ModeratorContext): string {
export async function evaluate(
workflow: WorkflowPayload,
context: ModeratorContext,
): Promise<Result<string, Error>> {
): Promise<Result<EvaluateResult, Error>> {
const role = currentRole(context);
const transitions = workflow.graph[role];
if (transitions === undefined) {
@@ -90,7 +90,7 @@ export async function evaluate(
for (const transition of transitions) {
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];
@@ -106,7 +106,7 @@ export async function evaluate(
return evalResult;
}
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 type { EvaluateResult } from "./types.js";
+6
View File
@@ -1 +1,7 @@
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 = {
role: string;
condition: string | null;
prompt: string | null;
};
export type ConditionDefinition = {