fix(ralph-loop): harden oracle verification flow
Capture oracle verification sessions more reliably and accept parent-session VERIFIED evidence so ULW loops do not retry after successful review. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -25,6 +25,8 @@ You already emitted <promise>{{INITIAL_PROMISE}}</promise>. This does NOT finish
|
|||||||
REQUIRED NOW:
|
REQUIRED NOW:
|
||||||
- Call Oracle using task(subagent_type="oracle", load_skills=[], run_in_background=false, ...)
|
- Call Oracle using task(subagent_type="oracle", load_skills=[], run_in_background=false, ...)
|
||||||
- Ask Oracle to verify whether the original task is actually complete
|
- Ask Oracle to verify whether the original task is actually complete
|
||||||
|
- Include the original task in the Oracle request
|
||||||
|
- Explicitly tell Oracle to review skeptically and critically, and to look for reasons the task may still be incomplete or wrong
|
||||||
- The system will inspect the Oracle session directly for the verification result
|
- The system will inspect the Oracle session directly for the verification result
|
||||||
- If Oracle does not verify, continue fixing the task and do not consider it complete
|
- If Oracle does not verify, continue fixing the task and do not consider it complete
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ REQUIRED NOW:
|
|||||||
- Oracle does not lie. Treat the verification result as ground truth
|
- Oracle does not lie. Treat the verification result as ground truth
|
||||||
- Do not claim completion early or argue with the failed verification
|
- Do not claim completion early or argue with the failed verification
|
||||||
- After fixing the remaining issues, request Oracle review again using task(subagent_type="oracle", load_skills=[], run_in_background=false, ...)
|
- After fixing the remaining issues, request Oracle review again using task(subagent_type="oracle", load_skills=[], run_in_background=false, ...)
|
||||||
|
- Include the original task in the Oracle request and tell Oracle to review skeptically and critically
|
||||||
- Only when the work is ready for review again, output: <promise>{{PROMISE}}</promise>
|
- Only when the work is ready for review again, output: <promise>{{PROMISE}}</promise>
|
||||||
|
|
||||||
Original task:
|
Original task:
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function createRalphLoopEventHandler(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const completionSessionID = verificationSessionID ?? (state.verification_pending ? undefined : sessionID)
|
const completionSessionID = verificationSessionID ?? sessionID
|
||||||
const transcriptPath = completionSessionID ? options.getTranscriptPath(completionSessionID) : undefined
|
const transcriptPath = completionSessionID ? options.getTranscriptPath(completionSessionID) : undefined
|
||||||
const completionViaTranscript = completionSessionID
|
const completionViaTranscript = completionSessionID
|
||||||
? detectCompletionInTranscript(
|
? detectCompletionInTranscript(
|
||||||
@@ -107,7 +107,13 @@ export function createRalphLoopEventHandler(
|
|||||||
sinceMessageIndex: undefined,
|
sinceMessageIndex: undefined,
|
||||||
})
|
})
|
||||||
: state.verification_pending
|
: state.verification_pending
|
||||||
? false
|
? await detectCompletionInSessionMessages(ctx, {
|
||||||
|
sessionID,
|
||||||
|
promise: state.completion_promise,
|
||||||
|
apiTimeoutMs: options.apiTimeoutMs,
|
||||||
|
directory: options.directory,
|
||||||
|
sinceMessageIndex: state.message_count_at_start,
|
||||||
|
})
|
||||||
: await detectCompletionInSessionMessages(ctx, {
|
: await detectCompletionInSessionMessages(ctx, {
|
||||||
sessionID,
|
sessionID,
|
||||||
promise: state.completion_promise,
|
promise: state.completion_promise,
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ describe("ulw-loop verification", () => {
|
|||||||
expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(false)
|
expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given parent session emits VERIFIED #when oracle session is not tracked #then ulw loop continues instead of completing", async () => {
|
test("#given parent session emits VERIFIED #when oracle session is not tracked #then ulw loop completes from parent session evidence", async () => {
|
||||||
const hook = createRalphLoopHook(createMockPluginInput(), {
|
const hook = createRalphLoopHook(createMockPluginInput(), {
|
||||||
getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath,
|
getTranscriptPath: (sessionID) => sessionID === "ses-oracle" ? oracleTranscriptPath : parentTranscriptPath,
|
||||||
})
|
})
|
||||||
@@ -379,17 +379,13 @@ describe("ulw-loop verification", () => {
|
|||||||
await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
|
await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
parentTranscriptPath,
|
parentTranscriptPath,
|
||||||
`${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done <promise>DONE</promise>" } })}\n${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: `bad parent leak <promise>${ULTRAWORK_VERIFICATION_PROMISE}</promise>` } })}\n`,
|
`${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: "done <promise>DONE</promise>" } })}\n${JSON.stringify({ type: "tool_result", timestamp: new Date().toISOString(), tool_output: { output: `verified <promise>${ULTRAWORK_VERIFICATION_PROMISE}</promise>` } })}\n`,
|
||||||
)
|
)
|
||||||
|
|
||||||
await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
|
await hook.event({ event: { type: "session.idle", properties: { sessionID: "session-123" } } })
|
||||||
|
|
||||||
expect(hook.getState()).not.toBeNull()
|
expect(hook.getState()).toBeNull()
|
||||||
expect(hook.getState()?.iteration).toBe(2)
|
expect(toastCalls.some((toast) => toast.title === "ULTRAWORK LOOP COMPLETE!")).toBe(true)
|
||||||
expect(hook.getState()?.completion_promise).toBe("DONE")
|
|
||||||
expect(hook.getState()?.verification_pending).toBeUndefined()
|
|
||||||
expect(promptCalls).toHaveLength(2)
|
|
||||||
expect(promptCalls[1]?.text).toContain("Verification failed")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given oracle verification fails #when loop restarts #then old oracle session is aborted", async () => {
|
test("#given oracle verification fails #when loop restarts #then old oracle session is aborted", async () => {
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import { readState, writeState } from "../hooks/ralph-loop/storage"
|
|||||||
|
|
||||||
const VERIFICATION_ATTEMPT_PATTERN = /<ulw_verification_attempt_id>(.*?)<\/ulw_verification_attempt_id>/i
|
const VERIFICATION_ATTEMPT_PATTERN = /<ulw_verification_attempt_id>(.*?)<\/ulw_verification_attempt_id>/i
|
||||||
|
|
||||||
|
function getMetadataString(metadata: Record<string, unknown> | undefined, keys: string[]): string | undefined {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = metadata?.[key]
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
function getPluginDirectory(ctx: PluginContext): string | null {
|
function getPluginDirectory(ctx: PluginContext): string | null {
|
||||||
if (typeof ctx === "object" && ctx !== null && "directory" in ctx && typeof ctx.directory === "string") {
|
if (typeof ctx === "object" && ctx !== null && "directory" in ctx && typeof ctx.directory === "string") {
|
||||||
return ctx.directory
|
return ctx.directory
|
||||||
@@ -43,9 +54,9 @@ export function createToolExecuteAfterHandler(args: {
|
|||||||
|
|
||||||
if (input.tool === "task") {
|
if (input.tool === "task") {
|
||||||
const directory = getPluginDirectory(ctx)
|
const directory = getPluginDirectory(ctx)
|
||||||
const sessionId = typeof output.metadata?.sessionId === "string" ? output.metadata.sessionId : undefined
|
const sessionId = getMetadataString(output.metadata, ["sessionId", "sessionID", "session_id"])
|
||||||
const agent = typeof output.metadata?.agent === "string" ? output.metadata.agent : undefined
|
const agent = getMetadataString(output.metadata, ["agent"])
|
||||||
const prompt = typeof output.metadata?.prompt === "string" ? output.metadata.prompt : undefined
|
const prompt = getMetadataString(output.metadata, ["prompt"])
|
||||||
const verificationAttemptId = prompt?.match(VERIFICATION_ATTEMPT_PATTERN)?.[1]?.trim()
|
const verificationAttemptId = prompt?.match(VERIFICATION_ATTEMPT_PATTERN)?.[1]?.trim()
|
||||||
const loopState = directory ? readState(directory) : null
|
const loopState = directory ? readState(directory) : null
|
||||||
const isVerificationContext =
|
const isVerificationContext =
|
||||||
|
|||||||
@@ -20,6 +20,26 @@ export function createToolExecuteBeforeHandler(args: {
|
|||||||
) => Promise<void> {
|
) => Promise<void> {
|
||||||
const { ctx, hooks } = args
|
const { ctx, hooks } = args
|
||||||
|
|
||||||
|
function buildUltraworkOracleVerificationPrompt(prompt: string, originalTask: string, verificationAttemptId: string): string {
|
||||||
|
const verificationPrompt = [
|
||||||
|
"You are verifying the active ULTRAWORK loop result for this session.",
|
||||||
|
"",
|
||||||
|
"Original task:",
|
||||||
|
originalTask,
|
||||||
|
"",
|
||||||
|
"Review the work skeptically and critically.",
|
||||||
|
"Assume it may be incomplete, misleading, or subtly broken until the evidence proves otherwise.",
|
||||||
|
"Look for missing scope, weak verification, process violations, hidden regressions, and any reason the task should NOT be considered complete.",
|
||||||
|
"",
|
||||||
|
`If the work is fully complete, end your response with <promise>${ULTRAWORK_VERIFICATION_PROMISE}</promise>.`,
|
||||||
|
"If the work is not complete, explain the blocking issues clearly and DO NOT emit that promise.",
|
||||||
|
"",
|
||||||
|
`<ulw_verification_attempt_id>${verificationAttemptId}</ulw_verification_attempt_id>`,
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
return `${prompt ? `${prompt}\n\n` : ""}${verificationPrompt}`
|
||||||
|
}
|
||||||
|
|
||||||
return async (input, output): Promise<void> => {
|
return async (input, output): Promise<void> => {
|
||||||
await hooks.writeExistingFileGuard?.["tool.execute.before"]?.(input, output)
|
await hooks.writeExistingFileGuard?.["tool.execute.before"]?.(input, output)
|
||||||
await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output)
|
await hooks.questionLabelTruncator?.["tool.execute.before"]?.(input, output)
|
||||||
@@ -91,7 +111,11 @@ export function createToolExecuteBeforeHandler(args: {
|
|||||||
verification_session_id: undefined,
|
verification_session_id: undefined,
|
||||||
})
|
})
|
||||||
argsObject.run_in_background = false
|
argsObject.run_in_background = false
|
||||||
argsObject.prompt = `${prompt ? `${prompt}\n\n` : ""}You are verifying the active ULTRAWORK loop result for this session. Review whether the original task is truly complete: ${loopState.prompt}\n\nIf the work is fully complete, end your response with <promise>${ULTRAWORK_VERIFICATION_PROMISE}</promise>. If the work is not complete, explain the blocking issues clearly and DO NOT emit that promise.\n\n<ulw_verification_attempt_id>${verificationAttemptId}</ulw_verification_attempt_id>`
|
argsObject.prompt = buildUltraworkOracleVerificationPrompt(
|
||||||
|
prompt,
|
||||||
|
loopState.prompt,
|
||||||
|
verificationAttemptId,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ describe("tool.execute.before ultrawork oracle verification", () => {
|
|||||||
|
|
||||||
expect(readState(directory)?.verification_attempt_id).toBeTruthy()
|
expect(readState(directory)?.verification_attempt_id).toBeTruthy()
|
||||||
expect(output.args.run_in_background).toBe(false)
|
expect(output.args.run_in_background).toBe(false)
|
||||||
|
expect(output.args.prompt).toContain("Original task:")
|
||||||
expect(output.args.prompt).toContain("Ship feature")
|
expect(output.args.prompt).toContain("Ship feature")
|
||||||
|
expect(output.args.prompt).toContain("Review the work skeptically and critically")
|
||||||
expect(output.args.prompt).toContain(`<promise>${ULTRAWORK_VERIFICATION_PROMISE}</promise>`)
|
expect(output.args.prompt).toContain(`<promise>${ULTRAWORK_VERIFICATION_PROMISE}</promise>`)
|
||||||
|
|
||||||
clearState(directory)
|
clearState(directory)
|
||||||
@@ -171,6 +173,45 @@ describe("tool.execute.before ultrawork oracle verification", () => {
|
|||||||
rmSync(directory, { recursive: true, force: true })
|
rmSync(directory, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("#given ulw loop is awaiting verification #when oracle metadata uses sessionID #then oracle session id is stored", async () => {
|
||||||
|
const directory = join(tmpdir(), `tool-after-ulw-sessionid-${Date.now()}`)
|
||||||
|
mkdirSync(directory, { recursive: true })
|
||||||
|
writeState(directory, {
|
||||||
|
active: true,
|
||||||
|
iteration: 3,
|
||||||
|
completion_promise: ULTRAWORK_VERIFICATION_PROMISE,
|
||||||
|
initial_completion_promise: "DONE",
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
prompt: "Ship feature",
|
||||||
|
session_id: "ses-main",
|
||||||
|
ultrawork: true,
|
||||||
|
verification_pending: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handler = createToolExecuteAfterHandler({
|
||||||
|
ctx: createCtx(directory) as unknown as Parameters<typeof createToolExecuteAfterHandler>[0]["ctx"],
|
||||||
|
hooks: {} as Parameters<typeof createToolExecuteAfterHandler>[0]["hooks"],
|
||||||
|
})
|
||||||
|
|
||||||
|
await handler(
|
||||||
|
{ tool: "task", sessionID: "ses-main", callID: "call-1" },
|
||||||
|
{
|
||||||
|
title: "oracle task",
|
||||||
|
output: "done",
|
||||||
|
metadata: {
|
||||||
|
agent: "oracle",
|
||||||
|
sessionID: "ses-oracle-alt",
|
||||||
|
sync: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(readState(directory)?.verification_session_id).toBe("ses-oracle-alt")
|
||||||
|
|
||||||
|
clearState(directory)
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
test("#given newer oracle attempt exists #when older oracle task finishes #then old session does not overwrite active verification", async () => {
|
test("#given newer oracle attempt exists #when older oracle task finishes #then old session does not overwrite active verification", async () => {
|
||||||
const directory = join(tmpdir(), `tool-race-ulw-${Date.now()}`)
|
const directory = join(tmpdir(), `tool-race-ulw-${Date.now()}`)
|
||||||
mkdirSync(directory, { recursive: true })
|
mkdirSync(directory, { recursive: true })
|
||||||
|
|||||||
Reference in New Issue
Block a user