feat(core): add --forever flag and schedule_work tool for autonomous agent mode

- Add schedule_work tool: agent declares pause duration, system auto-resumes
- Add --forever CLI flag with SisyphusModeSettings config
- Forever mode disables MemoryTool, EnterPlanModeTool, interactive shell
- useGeminiStream detects schedule_work calls and runs countdown timer
- Auto-submits resume prompt when timer reaches zero
This commit is contained in:
Sandy Tao
2026-03-09 10:14:53 -07:00
parent 5570b1c046
commit e4cc67b63d
6 changed files with 205 additions and 7 deletions

View File

@@ -93,6 +93,7 @@ export interface CliArgs {
rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined;
isCommand: boolean | undefined;
forever: boolean | undefined;
}
export async function parseArguments(
@@ -289,6 +290,12 @@ export async function parseArguments(
.option('accept-raw-output-risk', {
type: 'boolean',
description: 'Suppress the security warning when using --raw-output.',
})
.option('forever', {
type: 'boolean',
description:
'Run as a long-running autonomous agent with auto-resume and schedule_work support.',
default: false,
}),
)
// Register MCP subcommands
@@ -827,6 +834,14 @@ export async function loadCliConfig(
};
},
enableConseca: settings.security?.enableConseca,
isForeverMode: !!argv.forever,
sisyphusMode: argv.forever
? {
enabled: true,
idleTimeout: settings.hooksConfig?.idleTimeout,
prompt: 'continue workflow',
}
: undefined,
});
}

View File

@@ -498,6 +498,7 @@ describe('gemini.tsx main function kitty protocol', () => {
rawOutput: undefined,
acceptRawOutputRisk: undefined,
isCommand: undefined,
forever: undefined,
});
await act(async () => {

View File

@@ -37,6 +37,7 @@ import {
buildUserSteeringHintPrompt,
GeminiCliOperation,
getPlanModeExitMessage,
SCHEDULE_WORK_TOOL_NAME,
} from '@google/gemini-cli-core';
import type {
Config,
@@ -229,6 +230,16 @@ export const useGeminiStream = (
const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] =
useStateAndRef<boolean>(true);
const processedMemoryToolsRef = useRef<Set<string>>(new Set());
// Sisyphus Mode: schedule_work auto-resume timer
const sisyphusTargetTimestampRef = useRef<number | null>(null);
const [sisyphusSecondsRemaining, setSisyphusSecondsRemaining] = useState<
number | null
>(null);
const submitQueryRef = useRef<(query: PartListUnion) => Promise<void>>(() =>
Promise.resolve(),
);
const { startNewPrompt, getPromptCount } = useSessionStats();
const storage = config.storage;
const logger = useLogger(storage);
@@ -1248,6 +1259,15 @@ export const useGeminiStream = (
);
break;
case ServerGeminiEventType.ToolCallRequest:
if (event.value.name === SCHEDULE_WORK_TOOL_NAME) {
const args = event.value.args;
const inMinutes = Number(args?.['inMinutes'] ?? 0);
if (inMinutes > 0) {
const delayMs = inMinutes * 60 * 1000;
sisyphusTargetTimestampRef.current = Date.now() + delayMs;
setSisyphusSecondsRemaining(Math.ceil(delayMs / 1000));
}
}
toolCallRequests.push(event.value);
break;
case ServerGeminiEventType.UserCancelled:
@@ -1893,7 +1913,7 @@ export const useGeminiStream = (
// Compute effective timeout: use configured value, or default if
// Idle hooks are registered (e.g. by an extension).
let idleTimeoutSeconds = configuredIdleTimeout;
let idleTimeoutSeconds: number = configuredIdleTimeout;
if (idleTimeoutSeconds <= 0) {
const hookSystem = config.getHookSystem();
const hasIdleHooks = hookSystem
@@ -1932,6 +1952,46 @@ export const useGeminiStream = (
};
}, [streamingState, configuredIdleTimeout, config, submitQuery]);
// Keep submitQueryRef in sync for Sisyphus timer
useEffect(() => {
submitQueryRef.current = submitQuery;
}, [submitQuery]);
// Sisyphus: auto-resume when countdown reaches zero
useEffect(() => {
if (
streamingState === StreamingState.Idle &&
sisyphusSecondsRemaining !== null &&
sisyphusSecondsRemaining <= 0
) {
sisyphusTargetTimestampRef.current = null;
setSisyphusSecondsRemaining(null);
void submitQueryRef.current(
'System: The scheduled break has ended. Please resume your work.',
);
}
}, [streamingState, sisyphusSecondsRemaining]);
// Sisyphus: countdown timer interval
useEffect(() => {
if (
sisyphusTargetTimestampRef.current === null ||
streamingState !== StreamingState.Idle
) {
return;
}
const timer = setInterval(() => {
if (sisyphusTargetTimestampRef.current !== null) {
const remainingMs = sisyphusTargetTimestampRef.current - Date.now();
const remainingSecs = Math.max(0, Math.ceil(remainingMs / 1000));
setSisyphusSecondsRemaining(remainingSecs);
}
}, 1000);
return () => clearInterval(timer);
}, [streamingState]);
const lastOutputTime = Math.max(
lastToolOutputTime,
lastShellOutputTime,
@@ -1957,5 +2017,6 @@ export const useGeminiStream = (
backgroundShells,
dismissBackgroundShell,
retryStatus,
sisyphusSecondsRemaining,
};
};