feat(cron): make cron tools opt-in via experimental settings

Change cron/loop tools from opt-out to opt-in. Cron tools are now
disabled by default and can be enabled via:
- settings.json: { "experimental": { "cron": true } }
- Environment variable: QWEN_CODE_ENABLE_CRON=1

This ensures experimental features are explicitly enabled by users
who want to try them.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-29 02:25:28 +00:00
parent 99e5a9fbfd
commit 439a1a46e2
9 changed files with 75 additions and 32 deletions

View file

@ -14,13 +14,15 @@ describe('cron-tools', () => {
if (rig) {
await rig.cleanup();
}
// Clean up env var if set by disable test
delete process.env['QWEN_CODE_DISABLE_CRON'];
// Clean up env vars
delete process.env['QWEN_CODE_ENABLE_CRON'];
});
it('should have cron tools registered', async () => {
it('should have cron tools registered when enabled via settings', async () => {
rig = new TestRig();
await rig.setup('cron-tools-registered');
await rig.setup('cron-tools-registered', {
settings: { experimental: { cron: true } },
});
const result = await rig.run(
'Do you have access to tools called cron_create, cron_list, and cron_delete? Reply with just "yes" or "no".',
@ -30,9 +32,37 @@ describe('cron-tools', () => {
expect(result.toLowerCase()).toContain('yes');
});
it('should have cron tools registered when enabled via env var', async () => {
rig = new TestRig();
await rig.setup('cron-tools-env-var');
process.env['QWEN_CODE_ENABLE_CRON'] = '1';
const result = await rig.run(
'Do you have access to tools called cron_create, cron_list, and cron_delete? Reply with just "yes" or "no".',
);
validateModelOutput(result, null, 'cron tools via env var');
expect(result.toLowerCase()).toContain('yes');
});
it('should NOT have cron tools by default', async () => {
rig = new TestRig();
await rig.setup('cron-tools-disabled-by-default');
const result = await rig.run(
'Do you have access to a tool called cron_create? Reply with just "yes" or "no".',
);
validateModelOutput(result, null, 'cron disabled by default');
expect(result.toLowerCase()).toContain('no');
});
it('should create, list, and delete a cron job in a single turn', async () => {
rig = new TestRig();
await rig.setup('cron-create-list-delete');
await rig.setup('cron-create-list-delete', {
settings: { experimental: { cron: true } },
});
const result = await rig.run(
'Call cron_create with cron_expression "*/5 * * * *", prompt "test ping", recurring true. Then call cron_list. Then delete that job using cron_delete. Then call cron_list again. How many jobs remain? Reply with just the number.',
@ -59,7 +89,9 @@ describe('cron-tools', () => {
it('should create a one-shot (non-recurring) job', async () => {
rig = new TestRig();
await rig.setup('cron-one-shot');
await rig.setup('cron-one-shot', {
settings: { experimental: { cron: true } },
});
const result = await rig.run(
'Do these steps: (1) Call cron_create with cron_expression "*/5 * * * *", prompt "one-shot test", recurring false. (2) Call cron_list. Is the job marked as recurring or one-shot? Remember the answer. (3) Delete all cron jobs. Reply with just "recurring" or "one-shot".',
@ -81,23 +113,11 @@ describe('cron-tools', () => {
validateModelOutput(result, 'one-shot', 'cron one-shot');
});
it('should not have cron tools when QWEN_CODE_DISABLE_CRON=1', async () => {
rig = new TestRig();
await rig.setup('cron-disable-flag');
process.env['QWEN_CODE_DISABLE_CRON'] = '1';
const result = await rig.run(
'Do you have access to a tool called cron_create? Reply with just "yes" or "no".',
);
validateModelOutput(result, null, 'cron disable flag');
expect(result.toLowerCase()).toContain('no');
});
it('should exit normally in -p mode when no cron jobs are created', async () => {
rig = new TestRig();
await rig.setup('cron-no-jobs-exit');
await rig.setup('cron-no-jobs-exit', {
settings: { experimental: { cron: true } },
});
// A normal -p call without cron should still exit quickly
const result = await rig.run('What is 2+2? Reply with just the number.');

View file

@ -1089,6 +1089,7 @@ export async function loadCliConfig(
maxSessionTurns:
argv.maxSessionTurns ?? settings.model?.maxSessionTurns ?? -1,
experimentalZedIntegration: argv.acp || argv.experimentalAcp || false,
cronEnabled: settings.experimental?.cron ?? false,
listExtensions: argv.listExtensions || false,
overrideExtensions: overrideExtensions || argv.extensions,
noBrowser: !!process.env['NO_BROWSER'],

View file

@ -1594,9 +1594,20 @@ const SETTINGS_SCHEMA = {
category: 'Experimental',
requiresRestart: true,
default: {},
description: 'Setting to enable experimental features',
description: 'Settings to enable experimental features.',
showInDialog: false,
properties: {},
properties: {
cron: {
type: 'boolean',
label: 'Enable Cron/Loop Tools',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable in-session cron/loop tools (experimental). When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can also be enabled via QWEN_CODE_ENABLE_CRON=1 environment variable.',
showInDialog: true,
},
},
},
} as const satisfies SettingsSchema;

View file

@ -144,7 +144,7 @@ describe('runNonInteractive', () => {
}),
getExperimentalZedIntegration: vi.fn().mockReturnValue(false),
isInteractive: vi.fn().mockReturnValue(false),
isCronDisabled: vi.fn().mockReturnValue(true),
isCronEnabled: vi.fn().mockReturnValue(false),
getCronScheduler: vi.fn().mockReturnValue(null),
} as unknown as Config;

View file

@ -372,7 +372,7 @@ export async function runNonInteractive(
currentMessages = [{ role: 'user', parts: toolResponseParts }];
} else {
// No more tool calls — check if cron jobs are keeping us alive
const scheduler = config.isCronDisabled()
const scheduler = !config.isCronEnabled()
? null
: config.getCronScheduler();
if (scheduler && scheduler.size > 0) {

View file

@ -204,7 +204,7 @@ describe('useGeminiStream', () => {
.mockReturnValue(contentGeneratorConfig),
getMaxSessionTurns: vi.fn(() => 50),
getArenaAgentClient: vi.fn(() => null),
isCronDisabled: vi.fn(() => true),
isCronEnabled: vi.fn(() => false),
getCronScheduler: vi.fn(() => null),
} as unknown as Config;
mockOnDebugMessage = vi.fn();

View file

@ -1643,7 +1643,7 @@ export const useGeminiStream = (
// Start the scheduler on mount, stop on unmount
useEffect(() => {
if (config.isCronDisabled()) return;
if (!config.isCronEnabled()) return;
const scheduler = config.getCronScheduler();
scheduler.start((job: { prompt: string }) => {
cronQueueRef.current.push(job.prompt);

View file

@ -370,6 +370,7 @@ export interface ConfigParameters {
maxSessionTurns?: number;
sessionTokenLimit?: number;
experimentalZedIntegration?: boolean;
cronEnabled?: boolean;
listExtensions?: boolean;
overrideExtensions?: string[];
allowedMcpServers?: string[];
@ -557,6 +558,7 @@ export class Config {
private readonly cliVersion?: string;
private readonly experimentalZedIntegration: boolean = false;
private readonly cronEnabled: boolean = false;
private readonly chatRecordingEnabled: boolean;
private readonly loadMemoryFromIncludeDirectories: boolean = false;
private readonly importFormat: 'tree' | 'flat';
@ -680,6 +682,7 @@ export class Config {
this.sessionTokenLimit = params.sessionTokenLimit ?? -1;
this.experimentalZedIntegration =
params.experimentalZedIntegration ?? false;
this.cronEnabled = params.cronEnabled ?? false;
this.listExtensions = params.listExtensions ?? false;
this.overrideExtensions = params.overrideExtensions;
this.noBrowser = params.noBrowser ?? false;
@ -1687,8 +1690,10 @@ export class Config {
return this.cronScheduler;
}
isCronDisabled(): boolean {
return process.env['QWEN_CODE_DISABLE_CRON'] === '1';
isCronEnabled(): boolean {
// Cron is experimental and opt-in: enabled via settings or env var
if (process.env['QWEN_CODE_ENABLE_CRON'] === '1') return true;
return this.cronEnabled;
}
getEnableRecursiveFileSearch(): boolean {
@ -2211,7 +2216,7 @@ export class Config {
}
// Register cron tools unless disabled
if (!this.isCronDisabled()) {
if (this.isCronEnabled()) {
await registerCoreTool(CronCreateTool, this);
await registerCoreTool(CronListTool, this);
await registerCoreTool(CronDeleteTool, this);

View file

@ -1445,9 +1445,15 @@
}
},
"experimental": {
"description": "Setting to enable experimental features",
"description": "Settings to enable experimental features.",
"type": "object",
"properties": {}
"properties": {
"cron": {
"description": "Enable in-session cron/loop tools (experimental). When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can also be enabled via QWEN_CODE_ENABLE_CRON=1 environment variable.",
"type": "boolean",
"default": false
}
}
},
"$version": {
"type": "number",