CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
cron-create.ts233 linesDownload Raw Back to tools
1/**2 * cron_create tool — creates a new cron job (in-session or durable).3 */4 5import type { ToolInvocation, ToolResult } from './tools.js';6import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';7import { ToolNames, ToolDisplayNames } from './tool-names.js';8import type { Config } from '../config/config.js';9import type { PermissionDecision } from '../permissions/types.js';10import { parseCron, nextFireTime } from '../utils/cronParser.js';11import { humanReadableCron } from '../utils/cronDisplay.js';12import { CRON_TASKS_DISPLAY_PATH } from '../services/cronTasksFile.js';13 14/** "1 day" / "7 days" / "0.5 days". Callers handle the Infinity case. */15function formatDays(days: number): string {16  return days === 1 ? '1 day' : `${days} days`;17}18 19/**20 * Expiry paragraph for the tool description. The max age comes from config21 * (settings / QWEN_CODE_CRON_MAX_AGE_DAYS) at construction time —22 * changing it requires a restart, so baking it into the static23 * description is safe. Infinity (setting 0) disables expiry.24 */25function recurringExpiryBlurb(maxAgeDays: number): string {26  if (!Number.isFinite(maxAgeDays)) {27    return (28      'Recurring tasks never auto-expire in this configuration — they keep ' +29      'firing until deleted with CronDelete. Tell the user the job runs ' +30      'until cancelled when scheduling recurring jobs.'31    );32  }33  const span = formatDays(maxAgeDays);34  return (35    `Recurring tasks auto-expire after ${span} — they fire one final time, ` +36    'then are deleted. This bounds how long a forgotten schedule keeps ' +37    `firing. Tell the user about the ${span} limit when scheduling ` +38    'recurring jobs.'39  );40}41 42export interface CronCreateParams {43  cron: string;44  prompt: string;45  recurring?: boolean;46  durable?: boolean;47}48 49class CronCreateInvocation extends BaseToolInvocation<50  CronCreateParams,51  ToolResult52> {53  constructor(54    private config: Config,55    params: CronCreateParams,56  ) {57    super(params);58  }59 60  getDescription(): string {61    return `${this.params.cron}: ${this.params.prompt}`;62  }63 64  /**65   * The scheduled prompt fires against the agent at cron-trigger time66   * and executes with full tool access. The CronCreateTool's L3 default67   * must NOT be 'allow', because AUTO mode short-circuits at L4 when68   * `finalPermission === 'allow'` — the classifier never runs and an69   * arbitrary scheduled prompt is silently approved. `'ask'` routes70   * the call through the classifier (or manual approval in DEFAULT).71   */72  override async getDefaultPermission(): Promise<PermissionDecision> {73    return 'ask';74  }75 76  async execute(): Promise<ToolResult> {77    const scheduler = this.config.getCronScheduler();78    const recurring = this.params.recurring !== false;79    const durable = this.params.durable === true;80    const prompt = this.params.prompt.trim();81 82    try {83      // Validate cron expression before creating the job84      parseCron(this.params.cron);85      // Reject expressions that parse but never match a real date86      // (e.g. "0 0 30 2 *") — otherwise the job would be accepted and87      // silently never fire. Throws with a clear message.88      nextFireTime(this.params.cron, new Date());89 90      const job = durable91        ? await scheduler.createDurable(this.params.cron, prompt, recurring)92        : scheduler.create(this.params.cron, prompt, recurring);93 94      const display = humanReadableCron(job.cronExpr);95      const returnDisplay = `Scheduled ${job.id} (${display})${durable ? ' [durable]' : ''}`;96 97      const where = durable98        ? `Persisted to ${CRON_TASKS_DISPLAY_PATH}`99        : 'Session-only (not written to disk, dies when Qwen Code exits)';100      const maxAgeDays = this.config.getCronRecurringMaxAgeDays();101      const expiry = Number.isFinite(maxAgeDays)102        ? `Auto-expires after ${formatDays(maxAgeDays)}. Use CronDelete to cancel sooner.`103        : 'Never auto-expires. Use CronDelete to cancel.';104      const llmContent = recurring105        ? `Scheduled recurring job ${job.id} (${job.cronExpr}). ${where}. ` +106          expiry107        : `Scheduled one-shot task ${job.id} (${job.cronExpr}). ${where}. ` +108          'It will fire once then auto-delete.';109 110      return { llmContent, returnDisplay };111    } catch (error) {112      const message = error instanceof Error ? error.message : String(error);113      return {114        llmContent: `Error creating cron job: ${message}`,115        returnDisplay: message,116        error: { message },117      };118    }119  }120}121 122export class CronCreateTool extends BaseDeclarativeTool<123  CronCreateParams,124  ToolResult125> {126  static readonly Name = ToolNames.CRON_CREATE;127 128  constructor(private config: Config) {129    // Resolved once: each call re-reads/re-parses the env var and an130    // invalid value would warn on every call site below.131    const maxAgeDays = config.getCronRecurringMaxAgeDays();132    super(133      CronCreateTool.Name,134      ToolDisplayNames.CRON_CREATE,135      'Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.\n\n' +136        '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' +137        '## One-shot tasks (recurring: false)\n\n' +138        'For "remind me at X" or "at <time>, do Y" requests — fire once then auto-delete.\n' +139        'Pin minute/hour/day-of-month/month to specific values:\n' +140        '  "remind me at 2:30pm today to check the deploy" → cron: "30 14 <today_dom> <today_month> *", recurring: false\n' +141        '  "tomorrow morning, run the smoke test" → cron: "57 8 <tomorrow_dom> <tomorrow_month> *", recurring: false\n\n' +142        '## Recurring jobs (recurring: true, the default)\n\n' +143        'For "every N minutes" / "every hour" / "weekdays at 9am" requests:\n' +144        '  "*/5 * * * *" (every 5 min), "0 * * * *" (hourly), "0 9 * * 1-5" (weekdays at 9am local)\n\n' +145        '## Avoid the :00 and :30 minute marks when the task allows it\n\n' +146        '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' +147        '  "every morning around 9" → "57 8 * * *" or "3 9 * * *" (not "0 9 * * *")\n' +148        '  "hourly" → "7 * * * *" (not "0 * * * *")\n' +149        '  "in an hour or so, remind me to..." → pick whatever minute you land on, don\'t round\n\n' +150        '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' +151        '## Durability\n\n' +152        'By default (durable: false) the job lives only in this Qwen Code session — nothing is written to disk, and the job is gone when Qwen Code exits. ' +153        `Pass durable: true to write to ${CRON_TASKS_DISPLAY_PATH} so the job survives restarts. ` +154        'Only use durable: true when the user explicitly asks for persistence ("keep doing this every day", "set this up permanently"). ' +155        'Most "remind me in 5 minutes" requests should stay session-only.\n\n' +156        '## Runtime behavior\n\n' +157        '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' +158        `${recurringExpiryBlurb(maxAgeDays)}\n\n` +159        'Returns a job ID you can pass to CronDelete.',160      Kind.Other,161      {162        type: 'object',163        properties: {164          cron: {165            type: 'string',166            description:167              '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).',168          },169          prompt: {170            type: 'string',171            description: 'The prompt to enqueue at each fire time.',172          },173          recurring: {174            type: 'boolean',175            description:176              `true (default) = fire on every cron match until deleted${177                Number.isFinite(maxAgeDays)178                  ? ` or auto-expired after ${formatDays(maxAgeDays)}`179                  : ''180              }. ` +181              '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.',182          },183          durable: {184            type: 'boolean',185            description: `true = persist to ${CRON_TASKS_DISPLAY_PATH} and survive restarts. false (default) = in-memory only, dies when Qwen Code exits. Use true only when the user asks the task to survive across sessions.`,186          },187        },188        required: ['cron', 'prompt'],189        additionalProperties: false,190      },191      true, // isOutputMarkdown192      false, // canUpdateOutput193      true, // shouldDefer — scheduling is infrequent194      false, // alwaysLoad195      'cron schedule reminder recurring timer',196    );197  }198 199  protected createInvocation(200    params: CronCreateParams,201  ): ToolInvocation<CronCreateParams, ToolResult> {202    return new CronCreateInvocation(this.config, params);203  }204 205  protected override validateToolParamValues(206    params: CronCreateParams,207  ): string | null {208    if (!params.prompt || params.prompt.trim() === '') {209      return 'Parameter "prompt" must be a non-empty string.';210    }211    return null;212  }213 214  /**215   * Forward the prompt and cadence to the classifier. The scheduled216   * prompt will be enqueued and executed against the agent at fire-time,217   * so it must go through the same scrutiny as a direct command. Without218   * this override the default projection returns `''` and the classifier219   * sees `cron_create({})` — blind to what the agent will be asked to220   * do in 8 hours.221   */222  override toAutoClassifierInput(223    params: CronCreateParams,224  ): Record<string, unknown> {225    return {226      cron: params.cron,227      prompt: params.prompt,228      recurring: params.recurring ?? true,229      durable: params.durable ?? false,230    };231  }232}233 
basant307/AI_Governance_Project · CoolFace