Initial Version

This commit is contained in:
Raza Khan
2026-02-06 14:34:32 -08:00
parent a3af4a8cae
commit f18e45d34d
8 changed files with 418 additions and 13 deletions

View File

@@ -70,6 +70,11 @@ export interface CliArgs {
prompt: string | undefined;
promptInteractive: string | undefined;
ralphWiggum: boolean | undefined;
completionPromise: string | undefined;
maxIterations: number | undefined;
memoryFile: string | undefined;
yolo: boolean | undefined;
approvalMode: string | undefined;
allowedMcpServerNames: string[] | undefined;
@@ -141,6 +146,31 @@ export async function parseArguments(
description: 'Run in sandbox?',
})
.option('ralph-wiggum', {
alias: 'ralphWiggum',
type: 'boolean',
description:
'Enable Ralph Wiggum mode (iterative loop with YOLO mode).',
})
.option('completion-promise', {
alias: 'completionPromise',
type: 'string',
description:
'The string to look for in the output to signal completion in Ralph Wiggum mode.',
})
.option('max-iterations', {
alias: 'maxIterations',
type: 'number',
description: 'Maximum number of iterations for Ralph Wiggum mode.',
})
.option('memory-file', {
alias: 'memoryFile',
type: 'string',
description:
'Task-specific memory file for Ralph Wiggum mode (defaults to memories.md).',
default: 'memories.md',
})
.option('yolo', {
alias: 'y',
type: 'boolean',

View File

@@ -476,6 +476,10 @@ describe('gemini.tsx main function kitty protocol', () => {
prompt: undefined,
promptInteractive: undefined,
query: undefined,
ralphWiggum: undefined,
completionPromise: undefined,
maxIterations: undefined,
memoryFile: undefined,
yolo: undefined,
approvalMode: undefined,
allowedMcpServerNames: undefined,

View File

@@ -24,7 +24,7 @@ import { loadSettings, SettingScope } from './config/settings.js';
import { getStartupWarnings } from './utils/startupWarnings.js';
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { runNonInteractive } from './nonInteractiveCli.js';
import { runNonInteractive, runRalphWiggum } from './nonInteractiveCli.js';
import {
cleanupCheckpoints,
registerCleanup,
@@ -740,13 +740,25 @@ export async function main() {
initializeOutputListenersAndFlush();
await runNonInteractive({
config,
settings,
input,
prompt_id,
resumedSessionData,
});
if (argv.ralphWiggum) {
await runRalphWiggum({
config,
settings,
input,
prompt_id,
resumedSessionData,
completionPromise: argv.completionPromise,
maxIterations: argv.maxIterations,
});
} else {
await runNonInteractive({
config,
settings,
input,
prompt_id,
resumedSessionData,
});
}
// Call cleanup before process.exit, which causes cleanup to not run
await runExitCleanup();
process.exit(ExitCodes.SUCCESS);

View File

@@ -55,13 +55,187 @@ interface RunNonInteractiveParams {
resumedSessionData?: ResumedSessionData;
}
interface IterationResult {
iteration: number;
status: 'Success' | 'Failed';
testsPassed?: number;
testsFailed?: number;
testsTotal?: number;
}
function extractTestStats(output: string): {
passed?: number;
failed?: number;
total?: number;
} {
// Common patterns for test runners (Vitest, Jest, Mocha, etc.)
const patterns = [
// Vitest/Jest: "Tests: 3 passed, 1 failed, 4 total"
/Tests:\s*(?:(\d+)\s+passed)?(?:,\s*)?(?:(\d+)\s+failed)?(?:,\s*)?(?:(\d+)\s+total)?/i,
// Mocha: "3 passing (10ms)"
/(\d+)\s+passing/i,
// Mocha: "1 failing"
/(\d+)\s+failing/i,
// Generic: "Passed: 3, Failed: 1"
/Passed:\s*(\d+)/i,
/Failed:\s*(\d+)/i,
];
let passed: number | undefined;
let failed: number | undefined;
let total: number | undefined;
// Try Vitest/Jest pattern first as it is most comprehensive
const vitestMatch = output.match(patterns[0]);
if (vitestMatch && (vitestMatch[1] || vitestMatch[2] || vitestMatch[3])) {
passed = vitestMatch[1] ? parseInt(vitestMatch[1], 10) : 0;
failed = vitestMatch[2] ? parseInt(vitestMatch[2], 10) : 0;
total = vitestMatch[3] ? parseInt(vitestMatch[3], 10) : 0;
return { passed, failed, total };
}
// Fallback to individual patterns
const passingMatch = output.match(patterns[1]);
if (passingMatch) {
passed = parseInt(passingMatch[1], 10);
} else {
const passedMatch = output.match(patterns[3]);
if (passedMatch) passed = parseInt(passedMatch[1], 10);
}
const failingMatch = output.match(patterns[2]);
if (failingMatch) {
failed = parseInt(failingMatch[1], 10);
} else {
const failedMatch = output.match(patterns[4]);
if (failedMatch) failed = parseInt(failedMatch[1], 10);
}
return { passed, failed, total };
}
function printSummary(results: IterationResult[]) {
process.stderr.write('\n--- Ralph Wiggum Mode Summary ---\n');
process.stderr.write(
'| Iteration | Status | Tests Passed | Tests Failed |\n',
);
process.stderr.write(
'|-----------|---------|--------------|--------------|\n',
);
for (const result of results) {
const passed = result.testsPassed !== undefined ? result.testsPassed : '-';
const failed = result.testsFailed !== undefined ? result.testsFailed : '-';
process.stderr.write(
`| ${result.iteration.toString().padEnd(9)} | ${result.status.padEnd(7)} | ${passed.toString().padEnd(12)} | ${failed.toString().padEnd(12)} |\n`,
);
}
process.stderr.write('---------------------------------\n\n');
}
import fs from 'node:fs';
import path from 'node:path';
// ... (existing imports)
export async function runRalphWiggum({
config,
settings,
input,
prompt_id,
resumedSessionData,
completionPromise,
maxIterations,
memoryFile,
}: RunNonInteractiveParams & {
completionPromise?: string;
maxIterations?: number;
memoryFile?: string;
}): Promise<void> {
const effectiveMaxIterations = maxIterations ?? 10;
let iterations = 0;
let currentResumedSessionData = resumedSessionData;
const results: IterationResult[] = [];
const effectiveMemoryFile = memoryFile || 'memories.md';
const memoriesPath = path.join(process.cwd(), effectiveMemoryFile);
if (!fs.existsSync(memoriesPath)) {
fs.writeFileSync(
memoriesPath,
`# Ralph Wiggum Memories\n\nTask: ${input}\n\nUse this file (${effectiveMemoryFile}) to store notes on what worked and what didn't work across iterations. The agent will read this at the start of each run.\n\n`,
);
}
process.stderr.write(
`[Ralph Wiggum] Starting loop. Max iterations: ${effectiveMaxIterations}\n`,
);
while (iterations < effectiveMaxIterations) {
iterations++;
process.stderr.write(
`[Ralph Wiggum] Iteration ${iterations}/${effectiveMaxIterations}\n`,
);
let currentInput = input;
try {
if (fs.existsSync(memoriesPath)) {
const memories = fs.readFileSync(memoriesPath, 'utf-8');
if (memories.trim()) {
currentInput = `Context from previous iterations (${effectiveMemoryFile}):\n${memories}\n\nTask:\n${input}`;
process.stderr.write(
`[Ralph Wiggum] Loaded context from ${effectiveMemoryFile}\n`,
);
}
}
} catch (error) {
process.stderr.write(
`[Ralph Wiggum] Failed to read ${effectiveMemoryFile}: ${error}\n`,
);
}
const output = await runNonInteractive({
config,
settings,
input: currentInput,
prompt_id,
resumedSessionData: currentResumedSessionData,
});
const stats = extractTestStats(output);
const success =
completionPromise && output.includes(completionPromise) ? true : false;
results.push({
iteration: iterations,
status: success ? 'Success' : 'Failed',
testsPassed: stats.passed,
testsFailed: stats.failed,
testsTotal: stats.total,
});
if (success) {
process.stderr.write(
`[Ralph Wiggum] Completion promise "${completionPromise}" met. Exiting.\n`,
);
printSummary(results);
return;
}
// Clear resumedSessionData so we don't try to resume partially through
currentResumedSessionData = undefined;
}
process.stderr.write(
`[Ralph Wiggum] Max iterations reached without meeting completion promise.\n`,
);
printSummary(results);
}
export async function runNonInteractive({
config,
settings,
input,
prompt_id,
resumedSessionData,
}: RunNonInteractiveParams): Promise<void> {
}: RunNonInteractiveParams): Promise<string> {
return promptIdContext.run(prompt_id, async () => {
const consolePatcher = new ConsolePatcher({
stderr: true,
@@ -181,6 +355,9 @@ export async function runNonInteractive({
}
};
// Store accumulated response text to return
let fullResponseText = '';
let errorToHandle: unknown | undefined;
try {
consolePatcher.patch();
@@ -316,6 +493,13 @@ export async function runNonInteractive({
const isRaw =
config.getRawOutput() || config.getAcceptRawOutputRisk();
const output = isRaw ? event.value : stripAnsi(event.value);
// Accumulate full response
if (event.value) {
fullResponseText += event.value;
responseText += output;
}
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.MESSAGE,
@@ -325,7 +509,7 @@ export async function runNonInteractive({
delta: true,
});
} else if (config.getOutputFormat() === OutputFormat.JSON) {
responseText += output;
// responseText is already updated
} else {
if (event.value) {
textOutput.write(output);
@@ -381,7 +565,7 @@ export async function runNonInteractive({
),
});
}
return;
return fullResponseText;
} else if (event.type === GeminiEventType.AgentExecutionBlocked) {
const blockMessage = `Agent execution blocked: ${event.value.systemMessage?.trim() || event.value.reason}`;
if (config.getOutputFormat() === OutputFormat.TEXT) {
@@ -488,7 +672,7 @@ export async function runNonInteractive({
} else {
textOutput.ensureTrailingNewline(); // Ensure a final newline
}
return;
return fullResponseText;
}
currentMessages = [{ role: 'user', parts: toolResponseParts }];
@@ -512,7 +696,7 @@ export async function runNonInteractive({
} else {
textOutput.ensureTrailingNewline(); // Ensure a final newline
}
return;
return fullResponseText;
}
}
} catch (error) {
@@ -528,5 +712,6 @@ export async function runNonInteractive({
if (errorToHandle) {
handleError(errorToHandle, config);
}
return fullResponseText;
});
}