mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-15 08:31:14 -07:00
Initial Version
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user