fix(ralph-loop): scope completion detection to messages since loop start
This commit is contained in:
parent
cf97494073
commit
e17a00a906
105
src/hooks/ralph-loop/completion-promise-detector.test.ts
Normal file
105
src/hooks/ralph-loop/completion-promise-detector.test.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { detectCompletionInSessionMessages } from "./completion-promise-detector"
|
||||||
|
|
||||||
|
type SessionMessage = {
|
||||||
|
info?: { role?: string }
|
||||||
|
parts?: Array<{ type: string; text?: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPluginInput(messages: SessionMessage[]): PluginInput {
|
||||||
|
return {
|
||||||
|
client: {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({ data: messages }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as PluginInput
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("detectCompletionInSessionMessages", () => {
|
||||||
|
describe("#given session with prior DONE and new messages", () => {
|
||||||
|
test("#when sinceMessageIndex excludes prior DONE #then should NOT detect completion", async () => {
|
||||||
|
// #given
|
||||||
|
const messages: SessionMessage[] = [
|
||||||
|
{
|
||||||
|
info: { role: "assistant" },
|
||||||
|
parts: [{ type: "text", text: "Old completion <promise>DONE</promise>" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { role: "assistant" },
|
||||||
|
parts: [{ type: "text", text: "Working on the new task" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const ctx = createPluginInput(messages)
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const detected = await detectCompletionInSessionMessages(ctx, {
|
||||||
|
sessionID: "session-123",
|
||||||
|
promise: "DONE",
|
||||||
|
apiTimeoutMs: 1000,
|
||||||
|
directory: "/tmp",
|
||||||
|
sinceMessageIndex: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(detected).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#when sinceMessageIndex includes current DONE #then should detect completion", async () => {
|
||||||
|
// #given
|
||||||
|
const messages: SessionMessage[] = [
|
||||||
|
{
|
||||||
|
info: { role: "assistant" },
|
||||||
|
parts: [{ type: "text", text: "Old completion <promise>DONE</promise>" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { role: "assistant" },
|
||||||
|
parts: [{ type: "text", text: "Current completion <promise>DONE</promise>" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const ctx = createPluginInput(messages)
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const detected = await detectCompletionInSessionMessages(ctx, {
|
||||||
|
sessionID: "session-123",
|
||||||
|
promise: "DONE",
|
||||||
|
apiTimeoutMs: 1000,
|
||||||
|
directory: "/tmp",
|
||||||
|
sinceMessageIndex: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(detected).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("#given no sinceMessageIndex (backward compat)", () => {
|
||||||
|
test("#then should scan all messages", async () => {
|
||||||
|
// #given
|
||||||
|
const messages: SessionMessage[] = [
|
||||||
|
{
|
||||||
|
info: { role: "assistant" },
|
||||||
|
parts: [{ type: "text", text: "Old completion <promise>DONE</promise>" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { role: "assistant" },
|
||||||
|
parts: [{ type: "text", text: "No completion in latest message" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const ctx = createPluginInput(messages)
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const detected = await detectCompletionInSessionMessages(ctx, {
|
||||||
|
sessionID: "session-123",
|
||||||
|
promise: "DONE",
|
||||||
|
apiTimeoutMs: 1000,
|
||||||
|
directory: "/tmp",
|
||||||
|
})
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(detected).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -52,6 +52,7 @@ export async function detectCompletionInSessionMessages(
|
|||||||
promise: string
|
promise: string
|
||||||
apiTimeoutMs: number
|
apiTimeoutMs: number
|
||||||
directory: string
|
directory: string
|
||||||
|
sinceMessageIndex?: number
|
||||||
},
|
},
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@ -75,7 +76,12 @@ export async function detectCompletionInSessionMessages(
|
|||||||
? responseData
|
? responseData
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const assistantMessages = (messageArray as OpenCodeSessionMessage[]).filter((msg) => msg.info?.role === "assistant")
|
const scopedMessages =
|
||||||
|
typeof options.sinceMessageIndex === "number" && options.sinceMessageIndex >= 0 && options.sinceMessageIndex < messageArray.length
|
||||||
|
? messageArray.slice(options.sinceMessageIndex)
|
||||||
|
: messageArray
|
||||||
|
|
||||||
|
const assistantMessages = (scopedMessages as OpenCodeSessionMessage[]).filter((msg) => msg.info?.role === "assistant")
|
||||||
if (assistantMessages.length === 0) return false
|
if (assistantMessages.length === 0) return false
|
||||||
|
|
||||||
const pattern = buildPromisePattern(options.promise)
|
const pattern = buildPromisePattern(options.promise)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export function createLoopStateController(options: {
|
|||||||
loopOptions?: {
|
loopOptions?: {
|
||||||
maxIterations?: number
|
maxIterations?: number
|
||||||
completionPromise?: string
|
completionPromise?: string
|
||||||
|
messageCountAtStart?: number
|
||||||
ultrawork?: boolean
|
ultrawork?: boolean
|
||||||
strategy?: "reset" | "continue"
|
strategy?: "reset" | "continue"
|
||||||
},
|
},
|
||||||
@ -34,6 +35,7 @@ export function createLoopStateController(options: {
|
|||||||
loopOptions?.maxIterations ??
|
loopOptions?.maxIterations ??
|
||||||
config?.default_max_iterations ??
|
config?.default_max_iterations ??
|
||||||
DEFAULT_MAX_ITERATIONS,
|
DEFAULT_MAX_ITERATIONS,
|
||||||
|
message_count_at_start: loopOptions?.messageCountAtStart,
|
||||||
completion_promise:
|
completion_promise:
|
||||||
loopOptions?.completionPromise ??
|
loopOptions?.completionPromise ??
|
||||||
DEFAULT_COMPLETION_PROMISE,
|
DEFAULT_COMPLETION_PROMISE,
|
||||||
@ -93,5 +95,19 @@ export function createLoopStateController(options: {
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setMessageCountAtStart(sessionID: string, messageCountAtStart: number): RalphLoopState | null {
|
||||||
|
const state = readState(directory, stateDir)
|
||||||
|
if (!state || state.session_id !== sessionID) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
state.message_count_at_start = messageCountAtStart
|
||||||
|
if (!writeState(directory, state, stateDir)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,6 +84,7 @@ export function createRalphLoopEventHandler(
|
|||||||
promise: state.completion_promise,
|
promise: state.completion_promise,
|
||||||
apiTimeoutMs: options.apiTimeoutMs,
|
apiTimeoutMs: options.apiTimeoutMs,
|
||||||
directory: options.directory,
|
directory: options.directory,
|
||||||
|
sinceMessageIndex: state.message_count_at_start,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (completionViaTranscript || completionViaApi) {
|
if (completionViaTranscript || completionViaApi) {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export interface RalphLoopHook {
|
|||||||
options?: {
|
options?: {
|
||||||
maxIterations?: number
|
maxIterations?: number
|
||||||
completionPromise?: string
|
completionPromise?: string
|
||||||
|
messageCountAtStart?: number
|
||||||
ultrawork?: boolean
|
ultrawork?: boolean
|
||||||
strategy?: "reset" | "continue"
|
strategy?: "reset" | "continue"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,12 @@ export function readState(directory: string, customPath?: string): RalphLoopStat
|
|||||||
active: isActive,
|
active: isActive,
|
||||||
iteration: iterationNum,
|
iteration: iterationNum,
|
||||||
max_iterations: Number(data.max_iterations) || DEFAULT_MAX_ITERATIONS,
|
max_iterations: Number(data.max_iterations) || DEFAULT_MAX_ITERATIONS,
|
||||||
|
message_count_at_start:
|
||||||
|
typeof data.message_count_at_start === "number"
|
||||||
|
? data.message_count_at_start
|
||||||
|
: typeof data.message_count_at_start === "string" && data.message_count_at_start.trim() !== ""
|
||||||
|
? Number(data.message_count_at_start)
|
||||||
|
: undefined,
|
||||||
completion_promise: stripQuotes(data.completion_promise) || DEFAULT_COMPLETION_PROMISE,
|
completion_promise: stripQuotes(data.completion_promise) || DEFAULT_COMPLETION_PROMISE,
|
||||||
started_at: stripQuotes(data.started_at) || new Date().toISOString(),
|
started_at: stripQuotes(data.started_at) || new Date().toISOString(),
|
||||||
prompt: body.trim(),
|
prompt: body.trim(),
|
||||||
@ -72,13 +78,17 @@ export function writeState(
|
|||||||
const sessionIdLine = state.session_id ? `session_id: "${state.session_id}"\n` : ""
|
const sessionIdLine = state.session_id ? `session_id: "${state.session_id}"\n` : ""
|
||||||
const ultraworkLine = state.ultrawork !== undefined ? `ultrawork: ${state.ultrawork}\n` : ""
|
const ultraworkLine = state.ultrawork !== undefined ? `ultrawork: ${state.ultrawork}\n` : ""
|
||||||
const strategyLine = state.strategy ? `strategy: "${state.strategy}"\n` : ""
|
const strategyLine = state.strategy ? `strategy: "${state.strategy}"\n` : ""
|
||||||
|
const messageCountAtStartLine =
|
||||||
|
typeof state.message_count_at_start === "number"
|
||||||
|
? `message_count_at_start: ${state.message_count_at_start}\n`
|
||||||
|
: ""
|
||||||
const content = `---
|
const content = `---
|
||||||
active: ${state.active}
|
active: ${state.active}
|
||||||
iteration: ${state.iteration}
|
iteration: ${state.iteration}
|
||||||
max_iterations: ${state.max_iterations}
|
max_iterations: ${state.max_iterations}
|
||||||
completion_promise: "${state.completion_promise}"
|
completion_promise: "${state.completion_promise}"
|
||||||
started_at: "${state.started_at}"
|
started_at: "${state.started_at}"
|
||||||
${sessionIdLine}${ultraworkLine}${strategyLine}---
|
${sessionIdLine}${ultraworkLine}${strategyLine}${messageCountAtStartLine}---
|
||||||
${state.prompt}
|
${state.prompt}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export interface RalphLoopState {
|
|||||||
active: boolean
|
active: boolean
|
||||||
iteration: number
|
iteration: number
|
||||||
max_iterations: number
|
max_iterations: number
|
||||||
|
message_count_at_start?: number
|
||||||
completion_promise: string
|
completion_promise: string
|
||||||
started_at: string
|
started_at: string
|
||||||
prompt: string
|
prompt: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user