refactor(cron): rename cron_expression param to cron and enhance documentation

- Rename cron_expression parameter to cron for brevity across CronCreateTool
- Expand tool description with comprehensive usage guidance for one-shot and recurring tasks
- Add best practices for avoiding :00/:30 minute marks to reduce API load spikes
- Document 3-day auto-expiration for recurring jobs and session-only lifetime
- Add additionalProperties: false to all cron tool schemas for stricter validation
- Update integration tests and loop SKILL to use renamed parameter

This improves the developer experience with clearer parameter names and provides users with detailed guidance on scheduling patterns and runtime behavior.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-29 11:05:37 +08:00
parent 439a1a46e2
commit 57cf2b0bf2
6 changed files with 44 additions and 29 deletions

View file

@ -65,7 +65,7 @@ describe('cron-tools', () => {
});
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.',
'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.',
);
const foundCreate = await rig.waitForToolCall('cron_create');
@ -94,7 +94,7 @@ describe('cron-tools', () => {
});
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".',
'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".',
);
const foundCreate = await rig.waitForToolCall('cron_create');

View file

@ -29,7 +29,7 @@ You are setting up a recurring in-session loop. Parse the user's input to extrac
2. Convert the interval to a cron expression
3. Append to the prompt: `\n\nBe concise. If nothing has changed, reply with a single short sentence.`
4. Call `cron_create` with:
- `cron_expression`: the computed cron expression
- `cron`: the computed cron expression
- `prompt`: the extracted prompt with the conciseness instruction appended
- `recurring`: true
5. Confirm to the user: "Loop created — I'll [description] every [interval]."

View file

@ -27,7 +27,7 @@ describe('CronCreateTool', () => {
it('creates a recurring job by default', async () => {
const invocation = tool.build({
cron_expression: '*/5 * * * *',
cron: '*/5 * * * *',
prompt: 'check status',
});
const result = await invocation.execute(new AbortController().signal);
@ -39,7 +39,7 @@ describe('CronCreateTool', () => {
it('creates a one-shot job when recurring=false', async () => {
const invocation = tool.build({
cron_expression: '*/1 * * * *',
cron: '*/1 * * * *',
prompt: 'once',
recurring: false,
});
@ -53,7 +53,7 @@ describe('CronCreateTool', () => {
it('returns error for invalid cron expression', async () => {
const invocation = tool.build({
cron_expression: 'bad cron',
cron: 'bad cron',
prompt: 'fail',
});
const result = await invocation.execute(new AbortController().signal);
@ -61,9 +61,7 @@ describe('CronCreateTool', () => {
});
it('validates required params', () => {
expect(() =>
tool.build({ cron_expression: '*/1 * * * *' } as never),
).toThrow();
expect(() => tool.build({ cron: '*/1 * * * *' } as never)).toThrow();
expect(() => tool.build({ prompt: 'test' } as never)).toThrow();
});
});

View file

@ -9,7 +9,7 @@ import type { Config } from '../config/config.js';
import { nextFireTime } from '../utils/cronParser.js';
export interface CronCreateParams {
cron_expression: string;
cron: string;
prompt: string;
recurring?: boolean;
}
@ -28,7 +28,7 @@ class CronCreateInvocation extends BaseToolInvocation<
getDescription(): string {
const recurrence =
this.params.recurring !== false ? 'recurring' : 'one-shot';
return `Create ${recurrence} cron job: ${this.params.cron_expression}`;
return `Create ${recurrence} cron job: ${this.params.cron}`;
}
async execute(): Promise<ToolResult> {
@ -37,12 +37,12 @@ class CronCreateInvocation extends BaseToolInvocation<
try {
const job = scheduler.create(
this.params.cron_expression,
this.params.cron,
this.params.prompt,
recurring,
);
const next = nextFireTime(this.params.cron_expression, new Date());
const next = nextFireTime(this.params.cron, new Date());
const result = [
`Created ${recurring ? 'recurring' : 'one-shot'} cron job.`,
` ID: ${job.id}`,
@ -76,34 +76,49 @@ export class CronCreateTool extends BaseDeclarativeTool<
super(
CronCreateTool.Name,
ToolDisplayNames.CRON_CREATE,
'Create a new in-session cron job that fires a prompt on a schedule. ' +
'The job runs within the current session and is gone when the session ends. ' +
'Use standard 5-field cron expressions (minute hour day-of-month month day-of-week). ' +
'Examples: "*/5 * * * *" (every 5 min), "0 */2 * * *" (every 2 hours), "*/1 * * * *" (every minute).',
'Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.\n\n' +
'Uses standard 5-field cron in the user\'s local timezone: minute hour day-of-month month day-of-week. "0 9 * * *" means 9am local — no timezone conversion needed.\n\n' +
'## One-shot tasks (recurring: false)\n\n' +
'For "remind me at X" or "at <time>, do Y" requests — fire once then auto-delete.\n' +
'Pin minute/hour/day-of-month/month to specific values:\n' +
' "remind me at 2:30pm today to check the deploy" → cron: "30 14 <today_dom> <today_month> *", recurring: false\n' +
' "tomorrow morning, run the smoke test" → cron: "57 8 <tomorrow_dom> <tomorrow_month> *", recurring: false\n\n' +
'## Recurring jobs (recurring: true, the default)\n\n' +
'For "every N minutes" / "every hour" / "weekdays at 9am" requests:\n' +
' "*/5 * * * *" (every 5 min), "0 * * * *" (hourly), "0 9 * * 1-5" (weekdays at 9am local)\n\n' +
'## Avoid the :00 and :30 minute marks when the task allows it\n\n' +
'Every user who asks for "9am" gets `0 9`, and every user who asks for "hourly" gets `0 *` — which means requests from across the planet land on the API at the same instant. When the user\'s request is approximate, pick a minute that is NOT 0 or 30:\n' +
' "every morning around 9" → "57 8 * * *" or "3 9 * * *" (not "0 9 * * *")\n' +
' "hourly" → "7 * * * *" (not "0 * * * *")\n' +
' "in an hour or so, remind me to..." → pick whatever minute you land on, don\'t round\n\n' +
'Only use minute 0 or 30 when the user names that exact time and clearly means it ("at 9:00 sharp", "at half past", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.\n\n' +
'## Session-only\n\n' +
'Jobs live only in this Claude session — nothing is written to disk, and the job is gone when Claude exits.\n\n' +
'## Runtime behavior\n\n' +
'Jobs only fire while the REPL is idle (not mid-query). The scheduler adds a small deterministic jitter on top of whatever you pick: recurring tasks fire up to 10% of their period late (max 15 min); one-shot tasks landing on :00 or :30 fire up to 90 s early. Picking an off-minute is still the bigger lever.\n\n' +
'Recurring tasks auto-expire after 3 days — they fire one final time, then are deleted. This bounds session lifetime. Tell the user about the 3-day limit when scheduling recurring jobs.\n\n' +
'Returns a job ID you can pass to CronDelete.',
Kind.Other,
{
type: 'object',
properties: {
cron_expression: {
cron: {
type: 'string',
description:
'Standard 5-field cron expression. Fields: minute (0-59), hour (0-23), ' +
'day-of-month (1-31), month (1-12), day-of-week (0-6, 0=Sunday). ' +
'Supports: *, values, ranges (1-5), steps (*/15), lists (1,15,30).',
'Standard 5-field cron expression in local time: "M H DoM Mon DoW" (e.g. "*/5 * * * *" = every 5 minutes, "30 14 28 2 *" = Feb 28 at 2:30pm local once).',
},
prompt: {
type: 'string',
description:
'The prompt to send when the job fires. This is injected into the ' +
'session as if the user typed it.',
description: 'The prompt to enqueue at each fire time.',
},
recurring: {
type: 'boolean',
description:
'If true (default), the job fires repeatedly. If false, it fires once and is deleted.',
'true (default) = fire on every cron match until deleted or auto-expired after 3 days. false = fire once at the next match, then auto-delete. Use false for "remind me at X" one-shot requests with pinned minute/hour/dom/month.',
},
},
required: ['cron_expression', 'prompt'],
required: ['cron', 'prompt'],
additionalProperties: false,
},
);
}

View file

@ -54,17 +54,18 @@ export class CronDeleteTool extends BaseDeclarativeTool<
super(
CronDeleteTool.Name,
ToolDisplayNames.CRON_DELETE,
'Delete an active in-session cron job by its ID. Use cron_list to find job IDs.',
'Cancel a cron job previously scheduled with CronCreate. Removes it from the in-memory session store.',
Kind.Other,
{
type: 'object',
properties: {
id: {
type: 'string',
description: 'The 8-character ID of the cron job to delete.',
description: 'Job ID returned by CronCreate.',
},
},
required: ['id'],
additionalProperties: false,
},
);
}

View file

@ -68,11 +68,12 @@ export class CronListTool extends BaseDeclarativeTool<
super(
CronListTool.Name,
ToolDisplayNames.CRON_LIST,
'List all active in-session cron jobs, including their IDs, schedules, and next fire times.',
'List all cron jobs scheduled via CronCreate in this session.',
Kind.Other,
{
type: 'object',
properties: {},
additionalProperties: false,
},
);
}