mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
feat(core): enable loop/cron tools by default (#4950)
Graduate cron/loop from experimental opt-in to enabled-by-default. Flip env var polarity from QWEN_CODE_ENABLE_CRON to QWEN_CODE_DISABLE_CRON for users who want to opt out. Update integration tests, docs, and VS Code schema accordingly.
This commit is contained in:
parent
f479f7a660
commit
9fc7b07602
10 changed files with 39 additions and 51 deletions
|
|
@ -6,7 +6,7 @@ Scheduled tasks let Qwen Code re-run a prompt automatically on an interval. Use
|
|||
|
||||
Tasks are session-scoped: they live in the current Qwen Code process and are gone when you exit. Nothing is written to disk.
|
||||
|
||||
> **Note:** Scheduled tasks are an experimental feature. Enable them with `experimental.cron: true` in your [settings](../configuration/settings.md), or set `QWEN_CODE_ENABLE_CRON=1` in your environment.
|
||||
> **Tip:** Scheduled tasks are enabled by default. To disable them, set `experimental.cron: false` in your [settings](../configuration/settings.md), or set `QWEN_CODE_DISABLE_CRON=1` in your environment.
|
||||
|
||||
## Schedule a recurring prompt with /loop
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ function setupAcpCronTest(rig: TestRig) {
|
|||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
QWEN_CODE_ENABLE_CRON: '1',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -302,9 +301,7 @@ async function initSession(
|
|||
'cron job fires and streams results via sessionUpdate after prompt returns',
|
||||
async () => {
|
||||
const rig = new TestRig();
|
||||
rig.setup('acp-cron-e2e', {
|
||||
settings: { experimental: { cron: true } },
|
||||
});
|
||||
rig.setup('acp-cron-e2e');
|
||||
|
||||
const {
|
||||
sendRequest,
|
||||
|
|
|
|||
|
|
@ -22,15 +22,12 @@ describe('cron-tools', () => {
|
|||
if (rig) {
|
||||
await rig.cleanup();
|
||||
}
|
||||
// Clean up env vars
|
||||
delete process.env['QWEN_CODE_ENABLE_CRON'];
|
||||
delete process.env['QWEN_CODE_DISABLE_CRON'];
|
||||
});
|
||||
|
||||
it('should have cron tools registered when enabled via settings', async () => {
|
||||
it('should have cron tools registered by default', async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('cron-tools-registered', {
|
||||
settings: { experimental: { cron: true } },
|
||||
});
|
||||
await rig.setup('cron-tools-registered');
|
||||
|
||||
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".',
|
||||
|
|
@ -43,31 +40,37 @@ describe('cron-tools', () => {
|
|||
// Env vars set in the test process are not forwarded into Docker containers,
|
||||
// so this test cannot pass in sandbox mode.
|
||||
(IS_SANDBOX ? it.skip : it)(
|
||||
'should have cron tools registered when enabled via env var',
|
||||
'should NOT have cron tools when disabled via env var',
|
||||
async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('cron-tools-env-var');
|
||||
await rig.setup('cron-tools-disabled-env-var');
|
||||
|
||||
process.env['QWEN_CODE_ENABLE_CRON'] = '1';
|
||||
process.env['QWEN_CODE_DISABLE_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".',
|
||||
'Try to create a cron job with cron_create using cron "*/5 * * * *", prompt "disabled test", and recurring true. If you cannot call that tool, say so briefly.',
|
||||
);
|
||||
|
||||
validateModelOutput(result, null, 'cron tools via env var');
|
||||
expect(result.toLowerCase()).toContain('yes');
|
||||
validateModelOutput(result, null, 'cron disabled via env var');
|
||||
const toolLogs = rig.readToolLogs();
|
||||
expect(
|
||||
toolLogs.some((log) => log.toolRequest.name === 'cron_create'),
|
||||
'cron_create should not be callable when cron is disabled',
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it('should NOT have cron tools by default', async () => {
|
||||
it('should NOT have cron tools when disabled via settings', async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('cron-tools-disabled-by-default');
|
||||
await rig.setup('cron-tools-disabled-via-settings', {
|
||||
settings: { experimental: { cron: false } },
|
||||
});
|
||||
|
||||
const result = await rig.run(
|
||||
'Try to create a cron job with cron_create using cron "*/5 * * * *", prompt "disabled test", and recurring true. If you cannot call that tool, say so briefly.',
|
||||
);
|
||||
|
||||
validateModelOutput(result, null, 'cron disabled by default');
|
||||
validateModelOutput(result, null, 'cron disabled via settings');
|
||||
const toolLogs = rig.readToolLogs();
|
||||
expect(
|
||||
toolLogs.some((log) => log.toolRequest.name === 'cron_create'),
|
||||
|
|
@ -77,9 +80,7 @@ describe('cron-tools', () => {
|
|||
|
||||
it('should create, list, and delete a cron job in a single turn', async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('cron-create-list-delete', {
|
||||
settings: { experimental: { cron: true } },
|
||||
});
|
||||
await rig.setup('cron-create-list-delete');
|
||||
|
||||
const result = await rig.run(
|
||||
'Call cron_create with cron "*/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.',
|
||||
|
|
@ -106,9 +107,7 @@ describe('cron-tools', () => {
|
|||
|
||||
it('should create a one-shot (non-recurring) job', async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('cron-one-shot', {
|
||||
settings: { experimental: { cron: true } },
|
||||
});
|
||||
await rig.setup('cron-one-shot');
|
||||
|
||||
const result = await rig.run(
|
||||
'Do these steps: (1) Call cron_create with cron "*/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".',
|
||||
|
|
@ -132,9 +131,7 @@ describe('cron-tools', () => {
|
|||
|
||||
it('should exit normally in -p mode when no cron jobs are created', async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('cron-no-jobs-exit', {
|
||||
settings: { experimental: { cron: true } },
|
||||
});
|
||||
await rig.setup('cron-no-jobs-exit');
|
||||
|
||||
// A normal -p call without cron should still exit quickly
|
||||
const result = await rig.run('What is 2+2? Reply with just the number.');
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
* invoke them in the same session.
|
||||
*
|
||||
* Cron tools (cron_create, cron_list, cron_delete) are convenient deferred
|
||||
* targets: deterministic, side-effect-free in -p mode, and gated behind the
|
||||
* `experimental.cron` setting so we control when they're registered.
|
||||
* targets: deterministic, side-effect-free in -p mode, and enabled by default
|
||||
* (can be disabled via `experimental.cron: false`).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
|
|
@ -33,9 +33,7 @@ describe('tool-search / deferred tools', () => {
|
|||
|
||||
it('reveals a deferred tool via select: and lets the model invoke it', async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('tool-search-select-then-invoke', {
|
||||
settings: { experimental: { cron: true } },
|
||||
});
|
||||
await rig.setup('tool-search-select-then-invoke');
|
||||
|
||||
// Force the model down the select: path so the assertion isn't dependent
|
||||
// on whether the model spontaneously chose keyword search vs. select.
|
||||
|
|
@ -74,9 +72,7 @@ describe('tool-search / deferred tools', () => {
|
|||
|
||||
it('finds deferred tools via keyword search', async () => {
|
||||
rig = new TestRig();
|
||||
await rig.setup('tool-search-keyword', {
|
||||
settings: { experimental: { cron: true } },
|
||||
});
|
||||
await rig.setup('tool-search-keyword');
|
||||
|
||||
// The tool_search response is a synthetic <functions>...</functions>
|
||||
// block; we check the ARGS the model sent (a keyword query, not select:)
|
||||
|
|
@ -116,9 +112,9 @@ describe('tool-search / deferred tools', () => {
|
|||
|
||||
it('does not register deferred tools when their feature flag is off', async () => {
|
||||
rig = new TestRig();
|
||||
// No experimental.cron setting → cron_* tools must not be registered at
|
||||
// all (deferred or otherwise). tool_search has nothing to surface.
|
||||
await rig.setup('tool-search-no-cron');
|
||||
await rig.setup('tool-search-no-cron', {
|
||||
settings: { experimental: { cron: false } },
|
||||
});
|
||||
|
||||
const result = await rig.run(
|
||||
'Call tool_search with query "select:cron_list". ' +
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ function makeEnv(): NodeJS.ProcessEnv {
|
|||
delete env['NO_COLOR'];
|
||||
return {
|
||||
...env,
|
||||
QWEN_CODE_ENABLE_CRON: '1',
|
||||
FORCE_COLOR: '1',
|
||||
TERM: 'xterm-256color',
|
||||
NODE_NO_WARNINGS: '1',
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export class InteractiveSession {
|
|||
* @example
|
||||
* ```ts
|
||||
* const session = await InteractiveSession.start({
|
||||
* env: { QWEN_CODE_ENABLE_CRON: '1' },
|
||||
* env: { QWEN_CODE_DISABLE_CRON: '1' },
|
||||
* args: ['--approval-mode', 'yolo'],
|
||||
* });
|
||||
* ```
|
||||
|
|
|
|||
|
|
@ -1876,7 +1876,7 @@ export async function loadCliConfig(
|
|||
maxWallTimeSeconds: resolveMaxWallTimeSeconds(argv, settings),
|
||||
maxToolCalls: resolveMaxToolCalls(argv, settings),
|
||||
experimentalZedIntegration: argv.acp || argv.experimentalAcp || false,
|
||||
cronEnabled: settings.experimental?.cron ?? false,
|
||||
cronEnabled: settings.experimental?.cron ?? true,
|
||||
computerUseEnabled: settings.tools?.computerUse?.enabled ?? true,
|
||||
emitToolUseSummaries: settings.experimental?.emitToolUseSummaries ?? true,
|
||||
listExtensions: argv.listExtensions || false,
|
||||
|
|
|
|||
|
|
@ -2501,9 +2501,9 @@ const SETTINGS_SCHEMA = {
|
|||
label: 'Enable Cron/Loop Tools',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
default: true,
|
||||
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.',
|
||||
'Enable in-session cron/loop tools. When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can be disabled via QWEN_CODE_DISABLE_CRON=1 environment variable.',
|
||||
showInDialog: true,
|
||||
},
|
||||
emitToolUseSummaries: {
|
||||
|
|
|
|||
|
|
@ -1143,7 +1143,7 @@ export class Config {
|
|||
private readonly cliVersion?: string;
|
||||
private runtimeStatusEnabled = false;
|
||||
private readonly experimentalZedIntegration: boolean = false;
|
||||
private readonly cronEnabled: boolean = false;
|
||||
private readonly cronEnabled: boolean = true;
|
||||
private readonly forkSubagentEnabled: boolean = false;
|
||||
private workflowsEnabled = false;
|
||||
private readonly computerUseEnabled: boolean = true;
|
||||
|
|
@ -1328,7 +1328,7 @@ export class Config {
|
|||
this.sessionTokenLimit = params.sessionTokenLimit ?? -1;
|
||||
this.experimentalZedIntegration =
|
||||
params.experimentalZedIntegration ?? false;
|
||||
this.cronEnabled = params.cronEnabled ?? false;
|
||||
this.cronEnabled = params.cronEnabled ?? true;
|
||||
this.forkSubagentEnabled = params.forkSubagentEnabled ?? false;
|
||||
this.workflowsEnabled = params.workflowsEnabled ?? false;
|
||||
this.computerUseEnabled = params.computerUseEnabled ?? true;
|
||||
|
|
@ -3312,8 +3312,7 @@ export class Config {
|
|||
}
|
||||
|
||||
isCronEnabled(): boolean {
|
||||
// Cron is experimental and opt-in: enabled via settings or env var
|
||||
if (process.env['QWEN_CODE_ENABLE_CRON'] === '1') return true;
|
||||
if (process.env['QWEN_CODE_DISABLE_CRON'] === '1') return false;
|
||||
return this.cronEnabled;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2534,9 +2534,9 @@
|
|||
"type": "object",
|
||||
"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.",
|
||||
"description": "Enable in-session cron/loop tools. When enabled, the model can create recurring prompts using cron_create, cron_list, and cron_delete tools. Can be disabled via QWEN_CODE_DISABLE_CRON=1 environment variable.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
"default": true
|
||||
},
|
||||
"emitToolUseSummaries": {
|
||||
"description": "Generate a short LLM-based label after each tool batch completes. In compact mode the label replaces the generic `Tool × N` header; in full mode it appears as a dim `● <label>` line below the tool group. Requires a fast model to be configured; runs in parallel with the next API call so latency is hidden. Currently affects interactive CLI rendering only — SDK / non-interactive emission of the `tool_use_summary` message is not yet wired (the message factory is exported for a follow-up PR). Can be overridden with QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0 or =1.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue