mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
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:
@@ -93,6 +93,7 @@ export interface CliArgs {
|
|||||||
rawOutput: boolean | undefined;
|
rawOutput: boolean | undefined;
|
||||||
acceptRawOutputRisk: boolean | undefined;
|
acceptRawOutputRisk: boolean | undefined;
|
||||||
isCommand: boolean | undefined;
|
isCommand: boolean | undefined;
|
||||||
|
forever: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseArguments(
|
export async function parseArguments(
|
||||||
@@ -289,6 +290,12 @@ export async function parseArguments(
|
|||||||
.option('accept-raw-output-risk', {
|
.option('accept-raw-output-risk', {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Suppress the security warning when using --raw-output.',
|
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
|
// Register MCP subcommands
|
||||||
@@ -827,6 +834,14 @@ export async function loadCliConfig(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
enableConseca: settings.security?.enableConseca,
|
enableConseca: settings.security?.enableConseca,
|
||||||
|
isForeverMode: !!argv.forever,
|
||||||
|
sisyphusMode: argv.forever
|
||||||
|
? {
|
||||||
|
enabled: true,
|
||||||
|
idleTimeout: settings.hooksConfig?.idleTimeout,
|
||||||
|
prompt: 'continue workflow',
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
|||||||
rawOutput: undefined,
|
rawOutput: undefined,
|
||||||
acceptRawOutputRisk: undefined,
|
acceptRawOutputRisk: undefined,
|
||||||
isCommand: undefined,
|
isCommand: undefined,
|
||||||
|
forever: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
buildUserSteeringHintPrompt,
|
buildUserSteeringHintPrompt,
|
||||||
GeminiCliOperation,
|
GeminiCliOperation,
|
||||||
getPlanModeExitMessage,
|
getPlanModeExitMessage,
|
||||||
|
SCHEDULE_WORK_TOOL_NAME,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type {
|
import type {
|
||||||
Config,
|
Config,
|
||||||
@@ -229,6 +230,16 @@ export const useGeminiStream = (
|
|||||||
const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] =
|
const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] =
|
||||||
useStateAndRef<boolean>(true);
|
useStateAndRef<boolean>(true);
|
||||||
const processedMemoryToolsRef = useRef<Set<string>>(new Set());
|
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 { startNewPrompt, getPromptCount } = useSessionStats();
|
||||||
const storage = config.storage;
|
const storage = config.storage;
|
||||||
const logger = useLogger(storage);
|
const logger = useLogger(storage);
|
||||||
@@ -1248,6 +1259,15 @@ export const useGeminiStream = (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.ToolCallRequest:
|
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);
|
toolCallRequests.push(event.value);
|
||||||
break;
|
break;
|
||||||
case ServerGeminiEventType.UserCancelled:
|
case ServerGeminiEventType.UserCancelled:
|
||||||
@@ -1893,7 +1913,7 @@ export const useGeminiStream = (
|
|||||||
|
|
||||||
// Compute effective timeout: use configured value, or default if
|
// Compute effective timeout: use configured value, or default if
|
||||||
// Idle hooks are registered (e.g. by an extension).
|
// Idle hooks are registered (e.g. by an extension).
|
||||||
let idleTimeoutSeconds = configuredIdleTimeout;
|
let idleTimeoutSeconds: number = configuredIdleTimeout;
|
||||||
if (idleTimeoutSeconds <= 0) {
|
if (idleTimeoutSeconds <= 0) {
|
||||||
const hookSystem = config.getHookSystem();
|
const hookSystem = config.getHookSystem();
|
||||||
const hasIdleHooks = hookSystem
|
const hasIdleHooks = hookSystem
|
||||||
@@ -1932,6 +1952,46 @@ export const useGeminiStream = (
|
|||||||
};
|
};
|
||||||
}, [streamingState, configuredIdleTimeout, config, submitQuery]);
|
}, [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(
|
const lastOutputTime = Math.max(
|
||||||
lastToolOutputTime,
|
lastToolOutputTime,
|
||||||
lastShellOutputTime,
|
lastShellOutputTime,
|
||||||
@@ -1957,5 +2017,6 @@ export const useGeminiStream = (
|
|||||||
backgroundShells,
|
backgroundShells,
|
||||||
dismissBackgroundShell,
|
dismissBackgroundShell,
|
||||||
retryStatus,
|
retryStatus,
|
||||||
|
sisyphusSecondsRemaining,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { WebFetchTool } from '../tools/web-fetch.js';
|
|||||||
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
||||||
import { WebSearchTool } from '../tools/web-search.js';
|
import { WebSearchTool } from '../tools/web-search.js';
|
||||||
import { AskUserTool } from '../tools/ask-user.js';
|
import { AskUserTool } from '../tools/ask-user.js';
|
||||||
|
import { ScheduleWorkTool } from '../tools/schedule-work.js';
|
||||||
import { ExitPlanModeTool } from '../tools/exit-plan-mode.js';
|
import { ExitPlanModeTool } from '../tools/exit-plan-mode.js';
|
||||||
import { EnterPlanModeTool } from '../tools/enter-plan-mode.js';
|
import { EnterPlanModeTool } from '../tools/enter-plan-mode.js';
|
||||||
import { GeminiClient } from '../core/client.js';
|
import { GeminiClient } from '../core/client.js';
|
||||||
@@ -242,6 +243,13 @@ export interface AgentSettings {
|
|||||||
browser?: BrowserAgentCustomConfig;
|
browser?: BrowserAgentCustomConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SisyphusModeSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
idleTimeout?: number;
|
||||||
|
prompt?: string;
|
||||||
|
a2aPort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomTheme {
|
export interface CustomTheme {
|
||||||
type: 'custom';
|
type: 'custom';
|
||||||
name: string;
|
name: string;
|
||||||
@@ -589,6 +597,8 @@ export interface ConfigParameters {
|
|||||||
mcpEnabled?: boolean;
|
mcpEnabled?: boolean;
|
||||||
extensionsEnabled?: boolean;
|
extensionsEnabled?: boolean;
|
||||||
agents?: AgentSettings;
|
agents?: AgentSettings;
|
||||||
|
sisyphusMode?: SisyphusModeSettings;
|
||||||
|
isForeverMode?: boolean;
|
||||||
onReload?: () => Promise<{
|
onReload?: () => Promise<{
|
||||||
disabledSkills?: string[];
|
disabledSkills?: string[];
|
||||||
adminSkillsEnabled?: boolean;
|
adminSkillsEnabled?: boolean;
|
||||||
@@ -788,6 +798,8 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
|
|
||||||
private readonly enableAgents: boolean;
|
private readonly enableAgents: boolean;
|
||||||
private agents: AgentSettings;
|
private agents: AgentSettings;
|
||||||
|
private readonly isForeverMode: boolean;
|
||||||
|
private readonly sisyphusMode: SisyphusModeSettings;
|
||||||
private readonly enableEventDrivenScheduler: boolean;
|
private readonly enableEventDrivenScheduler: boolean;
|
||||||
private readonly skillsSupport: boolean;
|
private readonly skillsSupport: boolean;
|
||||||
private disabledSkills: string[];
|
private disabledSkills: string[];
|
||||||
@@ -885,6 +897,13 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
this._activeModel = params.model;
|
this._activeModel = params.model;
|
||||||
this.enableAgents = params.enableAgents ?? false;
|
this.enableAgents = params.enableAgents ?? false;
|
||||||
this.agents = params.agents ?? {};
|
this.agents = params.agents ?? {};
|
||||||
|
this.isForeverMode = params.isForeverMode ?? false;
|
||||||
|
this.sisyphusMode = {
|
||||||
|
enabled: params.sisyphusMode?.enabled ?? false,
|
||||||
|
idleTimeout: params.sisyphusMode?.idleTimeout,
|
||||||
|
prompt: params.sisyphusMode?.prompt,
|
||||||
|
a2aPort: params.sisyphusMode?.a2aPort,
|
||||||
|
};
|
||||||
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
|
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
|
||||||
this.planEnabled = params.plan ?? true;
|
this.planEnabled = params.plan ?? true;
|
||||||
this.trackerEnabled = params.tracker ?? false;
|
this.trackerEnabled = params.tracker ?? false;
|
||||||
@@ -2473,6 +2492,14 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
return remoteThreshold;
|
return remoteThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIsForeverMode(): boolean {
|
||||||
|
return this.isForeverMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSisyphusMode(): SisyphusModeSettings {
|
||||||
|
return this.sisyphusMode;
|
||||||
|
}
|
||||||
|
|
||||||
async getUserCaching(): Promise<boolean | undefined> {
|
async getUserCaching(): Promise<boolean | undefined> {
|
||||||
await this.ensureExperimentsLoaded();
|
await this.ensureExperimentsLoaded();
|
||||||
|
|
||||||
@@ -2558,6 +2585,7 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isInteractiveShellEnabled(): boolean {
|
isInteractiveShellEnabled(): boolean {
|
||||||
|
if (this.isForeverMode) return false;
|
||||||
return (
|
return (
|
||||||
this.interactive &&
|
this.interactive &&
|
||||||
this.ptyInfo !== 'child_process' &&
|
this.ptyInfo !== 'child_process' &&
|
||||||
@@ -2859,15 +2887,22 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
maybeRegister(ShellTool, () =>
|
maybeRegister(ShellTool, () =>
|
||||||
registry.registerTool(new ShellTool(this, this._messageBus)),
|
registry.registerTool(new ShellTool(this, this._messageBus)),
|
||||||
);
|
);
|
||||||
|
if (!this.isForeverMode) {
|
||||||
maybeRegister(MemoryTool, () =>
|
maybeRegister(MemoryTool, () =>
|
||||||
registry.registerTool(new MemoryTool(this._messageBus)),
|
registry.registerTool(new MemoryTool(this._messageBus)),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
maybeRegister(WebSearchTool, () =>
|
maybeRegister(WebSearchTool, () =>
|
||||||
registry.registerTool(new WebSearchTool(this, this._messageBus)),
|
registry.registerTool(new WebSearchTool(this, this._messageBus)),
|
||||||
);
|
);
|
||||||
maybeRegister(AskUserTool, () =>
|
maybeRegister(AskUserTool, () =>
|
||||||
registry.registerTool(new AskUserTool(this._messageBus)),
|
registry.registerTool(new AskUserTool(this._messageBus)),
|
||||||
);
|
);
|
||||||
|
if (this.isForeverMode) {
|
||||||
|
maybeRegister(ScheduleWorkTool, () =>
|
||||||
|
registry.registerTool(new ScheduleWorkTool(this._messageBus)),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (this.getUseWriteTodos()) {
|
if (this.getUseWriteTodos()) {
|
||||||
maybeRegister(WriteTodosTool, () =>
|
maybeRegister(WriteTodosTool, () =>
|
||||||
registry.registerTool(new WriteTodosTool(this._messageBus)),
|
registry.registerTool(new WriteTodosTool(this._messageBus)),
|
||||||
@@ -2877,10 +2912,12 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
maybeRegister(ExitPlanModeTool, () =>
|
maybeRegister(ExitPlanModeTool, () =>
|
||||||
registry.registerTool(new ExitPlanModeTool(this, this._messageBus)),
|
registry.registerTool(new ExitPlanModeTool(this, this._messageBus)),
|
||||||
);
|
);
|
||||||
|
if (!this.isForeverMode) {
|
||||||
maybeRegister(EnterPlanModeTool, () =>
|
maybeRegister(EnterPlanModeTool, () =>
|
||||||
registry.registerTool(new EnterPlanModeTool(this, this._messageBus)),
|
registry.registerTool(new EnterPlanModeTool(this, this._messageBus)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isTrackerEnabled()) {
|
if (this.isTrackerEnabled()) {
|
||||||
maybeRegister(TrackerCreateTaskTool, () =>
|
maybeRegister(TrackerCreateTaskTool, () =>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
BaseDeclarativeTool,
|
||||||
|
BaseToolInvocation,
|
||||||
|
type ToolResult,
|
||||||
|
Kind,
|
||||||
|
} from './tools.js';
|
||||||
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
|
import { SCHEDULE_WORK_TOOL_NAME } from './tool-names.js';
|
||||||
|
|
||||||
|
export interface ScheduleWorkParams {
|
||||||
|
inMinutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScheduleWorkTool extends BaseDeclarativeTool<
|
||||||
|
ScheduleWorkParams,
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
constructor(messageBus: MessageBus) {
|
||||||
|
super(
|
||||||
|
SCHEDULE_WORK_TOOL_NAME,
|
||||||
|
'Schedule Work',
|
||||||
|
'Schedule work to resume automatically after a break. Use this to wait for long-running processes or to pause your execution. The system will automatically wake you up.',
|
||||||
|
Kind.Communicate,
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['inMinutes'],
|
||||||
|
properties: {
|
||||||
|
inMinutes: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Minutes to wait before automatically resuming work.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageBus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override validateToolParamValues(
|
||||||
|
params: ScheduleWorkParams,
|
||||||
|
): string | null {
|
||||||
|
if (params.inMinutes <= 0) {
|
||||||
|
return 'inMinutes must be greater than 0.';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createInvocation(
|
||||||
|
params: ScheduleWorkParams,
|
||||||
|
messageBus: MessageBus,
|
||||||
|
toolName: string,
|
||||||
|
toolDisplayName: string,
|
||||||
|
): ScheduleWorkInvocation {
|
||||||
|
return new ScheduleWorkInvocation(
|
||||||
|
params,
|
||||||
|
messageBus,
|
||||||
|
toolName,
|
||||||
|
toolDisplayName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScheduleWorkInvocation extends BaseToolInvocation<
|
||||||
|
ScheduleWorkParams,
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
getDescription(): string {
|
||||||
|
return `Scheduling work to resume in ${this.params.inMinutes} minutes.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||||
|
return {
|
||||||
|
llmContent: `Work scheduled. The system will wake you up in ${this.params.inMinutes} minutes. DO NOT make any further tool calls. Instead, provide a brief text summary of the work completed so far to end your turn.`,
|
||||||
|
returnDisplay: `Scheduled work to resume in ${this.params.inMinutes} minutes.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,6 +151,7 @@ export {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LS_TOOL_NAME_LEGACY = 'list_directory'; // Just to be safe if anything used the old exported name directly
|
export const LS_TOOL_NAME_LEGACY = 'list_directory'; // Just to be safe if anything used the old exported name directly
|
||||||
|
export const SCHEDULE_WORK_TOOL_NAME = 'schedule_work';
|
||||||
|
|
||||||
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);
|
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);
|
||||||
|
|
||||||
@@ -228,6 +229,7 @@ export const ALL_BUILTIN_TOOL_NAMES = [
|
|||||||
GET_INTERNAL_DOCS_TOOL_NAME,
|
GET_INTERNAL_DOCS_TOOL_NAME,
|
||||||
ENTER_PLAN_MODE_TOOL_NAME,
|
ENTER_PLAN_MODE_TOOL_NAME,
|
||||||
EXIT_PLAN_MODE_TOOL_NAME,
|
EXIT_PLAN_MODE_TOOL_NAME,
|
||||||
|
SCHEDULE_WORK_TOOL_NAME,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user