Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/many-results-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

feat(mode): implement Ralph mode for infinite task loops
7 changes: 7 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ export const globalSettingsSchema = z.object({
lastModeExportPath: z.string().optional(),
lastModeImportPath: z.string().optional(),
appendSystemPrompt: z.string().optional(), // kilocode_change: Custom text to append to system prompt (CLI only)
alwaysAllowRalph: z.boolean().optional(), // kilocode_change
ralphEnabled: z.boolean().optional(), // kilocode_change
ralphLoopLimit: z.number().int().min(0).optional(), // kilocode_change
ralphCompletionDelimiter: z.string().optional(), // kilocode_change
})

export type GlobalSettings = z.infer<typeof globalSettingsSchema>
Expand Down Expand Up @@ -367,6 +371,9 @@ export const EVALS_SETTINGS: RooCodeSettings = {
alwaysAllowModeSwitch: true,
alwaysAllowSubtasks: true,
alwaysAllowExecute: true,
alwaysAllowRalph: false, // kilocode_change
ralphLoopLimit: 5, // kilocode_change
ralphCompletionDelimiter: "<ralph>COMPLETED</ralph>", // kilocode_change
alwaysAllowFollowupQuestions: true,
followupAutoApproveTimeoutMs: 0,
allowedCommands: ["*"],
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export interface CreateTaskOptions {
initialTodos?: TodoItem[]
/** Initial status for the task's history item (e.g., "active" for child tasks) */
initialStatus?: "active" | "delegated" | "completed"
ralphLoopCount?: number // kilocode_change
}

export enum TaskStatus {
Expand All @@ -110,6 +111,7 @@ export enum TaskStatus {
export const taskMetadataSchema = z.object({
task: z.string().optional(),
images: z.array(z.string()).optional(),
ralphLoopCount: z.number().int().min(0).optional(), // kilocode_change
})

export type TaskMetadata = z.infer<typeof taskMetadataSchema>
Expand Down
9 changes: 7 additions & 2 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,11 +825,16 @@ export async function presentAssistantMessage(cline: Task) {
// If execution is not allowed, notify user and break.
if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
// Handle repetition similar to mistake_limit_reached pattern.
const { response, text, images } = await cline.ask(
repetitionCheck.askUser.messageKey as ClineAsk,
const result = await cline.handleMistakeLimitReached(
repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
)

if (!result) {
return
}

const { response, text, images } = result

if (response === "messageResponse") {
// Add user feedback to userContent.
cline.userMessageContent.push(
Expand Down
90 changes: 86 additions & 4 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window error
export interface TaskOptions extends CreateTaskOptions {
context: vscode.ExtensionContext // kilocode_change
provider: ClineProvider
ralphLoopCount?: number // kilocode_change
apiConfiguration: ProviderSettings
enableDiff?: boolean
enableCheckpoints?: boolean
Expand Down Expand Up @@ -438,6 +439,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
initialTodos,
workspacePath,
initialStatus,
ralphLoopCount, // kilocode_change
}: TaskOptions) {
super()
this.context = context // kilocode_change
Expand Down Expand Up @@ -469,6 +471,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.metadata = {
task: historyItem ? historyItem.task : task,
images: historyItem ? [] : images,
ralphLoopCount: ralphLoopCount ?? 0, // kilocode_change
}

// Normal use-case is usually retry similar history task with new workspace.
Expand Down Expand Up @@ -2497,6 +2500,82 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}

// kilocode_change start
/**
* Handles the Ralph mode restart logic.
* If Ralph mode is enabled and within limits, restarts the task.
*
* @param result Optional completion result to check for delimiter
* @returns true if task was restarted, false otherwise
*/
public async handleRalphRestart(result?: string): Promise<boolean> {
const provider = this.providerRef.deref()
const state = await provider?.getState()
if (provider && state?.alwaysAllowRalph && state?.ralphEnabled) {
const delimiter = state.ralphCompletionDelimiter

// Check delimiter if configured and result is provided
if (result !== undefined && delimiter && delimiter.trim() !== "") {
if (result.indexOf(delimiter) !== -1) {
return false
}

const assistantMessages = this.apiConversationHistory.filter((m) => m.role === "assistant").slice(-5)

for (const m of assistantMessages) {
const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content)
if (content.indexOf(delimiter) !== -1) {
return false
}
}
}

const loopLimit = state.ralphLoopLimit ?? 5
const currentLoopCount = this.metadata.ralphLoopCount ?? 0

if (loopLimit <= 0 || currentLoopCount + 1 < loopLimit) {
const firstPrompt = this.metadata.task
const images = this.metadata.images

setTimeout(async () => {
try {
await provider.createTask(firstPrompt, images, undefined, {
ralphLoopCount: currentLoopCount + 1,
})
await provider.postMessageToWebview({ type: "invoke", invoke: "newChat" })
} catch (error) {
console.error("[Task] Failed to restart task in Ralph mode:", error)
}
}, 1000)

await this.abortTask()
return true
}
}
return false
}

/**
* Handles the case where the mistake limit has been reached.
* If Ralph mode is enabled and within limits, restarts the task.
* Otherwise, asks the user for guidance.
*
* @param guidance Optional guidance text to show the user
* @returns Object containing the user's response if Ralph restart didn't happen
*/
public async handleMistakeLimitReached(
guidance: string = t("common:errors.mistake_limit_guidance"),
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] } | undefined> {
const restarted = await this.handleRalphRestart()
if (restarted) {
await this.say("error", "Mistake limit reached. Ralph mode is restarting the task...")
return undefined
}

return await this.ask("mistake_limit_reached", guidance)
}
// kilocode_change end

public async recursivelyMakeClineRequests(
userContent: Anthropic.Messages.ContentBlockParam[],
includeFileDetails: boolean = false,
Expand Down Expand Up @@ -2538,10 +2617,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
),
)

const { response, text, images } = await this.ask(
"mistake_limit_reached",
t("common:errors.mistake_limit_guidance"),
)
const result = await this.handleMistakeLimitReached()

if (!result) {
return true
}

const { response, text, images } = result

if (response === "messageResponse") {
currentUserContent.push(
Expand Down
7 changes: 7 additions & 0 deletions src/core/tools/AttemptCompletionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> {
}
}

// kilocode_change start
const restarted = await task.handleRalphRestart(result)
if (restarted) {
return
}
// kilocode_change end

const { response, text, images } = await task.ask("completion_result", "", false)

if (response === "yesButtonClicked") {
Expand Down
Loading