agent.libx.js 0.93.45 → 0.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3496,6 +3496,253 @@ init_OverlayFilesystem();
3496
3496
  // src/index.ts
3497
3497
  init_tools();
3498
3498
 
3499
+ // src/scheduler.ts
3500
+ function parseCronField(field, min, max) {
3501
+ const vals = /* @__PURE__ */ new Set();
3502
+ for (const part of field.split(",")) {
3503
+ const [rangeStr, stepStr] = part.split("/");
3504
+ const step = stepStr ? parseInt(stepStr, 10) : 1;
3505
+ if (isNaN(step) || step < 1) throw new Error(`invalid cron step: ${part}`);
3506
+ let lo, hi;
3507
+ if (rangeStr === "*") {
3508
+ lo = min;
3509
+ hi = max;
3510
+ } else if (rangeStr.includes("-")) {
3511
+ const [a, b] = rangeStr.split("-").map(Number);
3512
+ if (isNaN(a) || isNaN(b)) throw new Error(`invalid cron range: ${part}`);
3513
+ lo = a;
3514
+ hi = b;
3515
+ } else {
3516
+ const n = parseInt(rangeStr, 10);
3517
+ if (isNaN(n)) throw new Error(`invalid cron value: ${part}`);
3518
+ lo = n;
3519
+ hi = stepStr ? max : n;
3520
+ }
3521
+ for (let i = lo; i <= hi; i += step) vals.add(i);
3522
+ }
3523
+ return [...vals].sort((a, b) => a - b);
3524
+ }
3525
+ function parseCron(expr) {
3526
+ const parts = expr.trim().split(/\s+/);
3527
+ if (parts.length !== 5) throw new Error(`cron expression must have 5 fields (minute hour dom month dow), got ${parts.length}: "${expr}"`);
3528
+ return {
3529
+ minute: parseCronField(parts[0], 0, 59),
3530
+ hour: parseCronField(parts[1], 0, 23),
3531
+ dom: parseCronField(parts[2], 1, 31),
3532
+ month: parseCronField(parts[3], 1, 12),
3533
+ dow: parseCronField(parts[4], 0, 6)
3534
+ };
3535
+ }
3536
+ function cronMatches(fields, date) {
3537
+ return fields.minute.includes(date.getMinutes()) && fields.hour.includes(date.getHours()) && fields.dom.includes(date.getDate()) && fields.month.includes(date.getMonth() + 1) && fields.dow.includes(date.getDay());
3538
+ }
3539
+ function nextCronAfter(cron, after) {
3540
+ const fields = parseCron(cron);
3541
+ const d = new Date(after);
3542
+ d.setSeconds(0, 0);
3543
+ d.setMinutes(d.getMinutes() + 1);
3544
+ const limit = after + 366 * 864e5;
3545
+ while (d.getTime() <= limit) {
3546
+ if (cronMatches(fields, d)) return d.getTime();
3547
+ d.setMinutes(d.getMinutes() + 1);
3548
+ }
3549
+ return null;
3550
+ }
3551
+ var Scheduler = class {
3552
+ jobs = /* @__PURE__ */ new Map();
3553
+ seq = 0;
3554
+ timer = null;
3555
+ firing = false;
3556
+ fire;
3557
+ now;
3558
+ tickMs;
3559
+ constructor(opts) {
3560
+ this.fire = opts.fire;
3561
+ this.now = opts.now ?? Date.now;
3562
+ this.tickMs = opts.tickMs ?? 15e3;
3563
+ if (!opts.manual) this.start();
3564
+ }
3565
+ /** Start the tick timer. Idempotent. */
3566
+ start() {
3567
+ if (this.timer) return;
3568
+ this.timer = setInterval(() => this.tick(), this.tickMs);
3569
+ if (typeof this.timer === "object" && "unref" in this.timer) this.timer.unref();
3570
+ }
3571
+ /** Stop the tick timer. Does not clear jobs. */
3572
+ stop() {
3573
+ if (this.timer) {
3574
+ clearInterval(this.timer);
3575
+ this.timer = null;
3576
+ }
3577
+ }
3578
+ /** Add a scheduled job. Returns the job id. */
3579
+ add(opts) {
3580
+ if ("cron" in opts.trigger) parseCron(opts.trigger.cron);
3581
+ if ("at" in opts.trigger && opts.trigger.at < this.now()) {
3582
+ }
3583
+ if ("everyMs" in opts.trigger && opts.trigger.everyMs < 1e3) {
3584
+ throw new Error("interval must be >= 1000ms");
3585
+ }
3586
+ const id = `sched-${++this.seq}`;
3587
+ this.jobs.set(id, {
3588
+ id,
3589
+ prompt: opts.prompt,
3590
+ trigger: opts.trigger,
3591
+ status: "active",
3592
+ created: this.now(),
3593
+ runs: 0,
3594
+ label: opts.label
3595
+ });
3596
+ return id;
3597
+ }
3598
+ /** Cancel a job. Returns false if not found. */
3599
+ cancel(id) {
3600
+ const j = this.jobs.get(id);
3601
+ if (!j) return false;
3602
+ j.status = "done";
3603
+ return true;
3604
+ }
3605
+ get(id) {
3606
+ return this.jobs.get(id);
3607
+ }
3608
+ list() {
3609
+ return [...this.jobs.values()];
3610
+ }
3611
+ active() {
3612
+ return this.list().filter((j) => j.status === "active");
3613
+ }
3614
+ /** Compute when a job next fires (epoch ms), or null if done/unknown. */
3615
+ nextFire(job) {
3616
+ if (job.status !== "active") return null;
3617
+ const t = job.trigger;
3618
+ if ("at" in t) return t.at;
3619
+ if ("everyMs" in t) return (job.lastRun ?? job.created) + t.everyMs;
3620
+ if ("cron" in t) return nextCronAfter(t.cron, job.lastRun ?? job.created);
3621
+ return null;
3622
+ }
3623
+ /** Check all active jobs and fire any that are due. Reentrant-safe. */
3624
+ async tick() {
3625
+ if (this.firing) return;
3626
+ this.firing = true;
3627
+ try {
3628
+ const now5 = this.now();
3629
+ for (const job of this.jobs.values()) {
3630
+ if (job.status !== "active") continue;
3631
+ const due = this.nextFire(job);
3632
+ if (due == null || due > now5) continue;
3633
+ job.lastRun = now5;
3634
+ job.runs++;
3635
+ if ("at" in job.trigger) job.status = "done";
3636
+ try {
3637
+ await this.fire(job);
3638
+ } catch {
3639
+ }
3640
+ }
3641
+ } finally {
3642
+ this.firing = false;
3643
+ }
3644
+ }
3645
+ /** Export all jobs for session persistence. */
3646
+ snapshot() {
3647
+ return this.list().filter((j) => j.status === "active").map((j) => ({ ...j }));
3648
+ }
3649
+ /** Restore jobs from a snapshot (e.g. on --resume). Clears existing jobs first. */
3650
+ restore(snapshots) {
3651
+ this.jobs.clear();
3652
+ for (const s of snapshots) {
3653
+ this.seq = Math.max(this.seq, parseInt(s.id.replace("sched-", ""), 10) || 0);
3654
+ this.jobs.set(s.id, { ...s });
3655
+ }
3656
+ }
3657
+ /** Number of active jobs. */
3658
+ get size() {
3659
+ return this.active().length;
3660
+ }
3661
+ destroy() {
3662
+ this.stop();
3663
+ this.jobs.clear();
3664
+ }
3665
+ };
3666
+ function makeScheduleTools(scheduler) {
3667
+ return [
3668
+ {
3669
+ name: "ScheduleTask",
3670
+ description: 'Schedule a prompt to fire automatically while this session is alive.\nModes:\n \u2022 One-off: {at: <epoch_ms>} \u2014 fires once at that time, then done.\n \u2022 Interval: {everyMs: <ms>} \u2014 fires repeatedly (\u22651s).\n \u2022 Cron: {cron: "min hr dom mon dow"} \u2014 standard 5-field cron.\nReturns the job id. Jobs only fire while the CLI session is running \u2014 they do NOT survive quitting.',
3671
+ parameters: {
3672
+ type: "object",
3673
+ required: ["prompt", "trigger"],
3674
+ properties: {
3675
+ prompt: { type: "string", description: "The prompt to inject when the job fires." },
3676
+ trigger: {
3677
+ type: "object",
3678
+ description: 'One of: {at: epoch_ms}, {everyMs: ms}, {cron: "5-field expr"}.',
3679
+ properties: {
3680
+ at: { type: "number" },
3681
+ everyMs: { type: "number" },
3682
+ cron: { type: "string" }
3683
+ }
3684
+ },
3685
+ label: { type: "string", description: "Short label for display (optional)." }
3686
+ }
3687
+ },
3688
+ async run({ prompt, trigger, label }) {
3689
+ try {
3690
+ const id = scheduler.add({ prompt, trigger, label });
3691
+ const job = scheduler.get(id);
3692
+ const next = scheduler.nextFire(job);
3693
+ return `Scheduled ${id}${label ? ` (${label})` : ""}. Next fire: ${next ? new Date(next).toLocaleString() : "now"}.`;
3694
+ } catch (e) {
3695
+ return `Error: ${e?.message ?? e}`;
3696
+ }
3697
+ }
3698
+ },
3699
+ {
3700
+ name: "ScheduleList",
3701
+ description: "List all scheduled jobs and their next fire time.",
3702
+ parameters: { type: "object", properties: {} },
3703
+ async run() {
3704
+ const jobs = scheduler.list();
3705
+ if (!jobs.length) return "(no scheduled jobs)";
3706
+ return jobs.map((j) => {
3707
+ const next = scheduler.nextFire(j);
3708
+ const trig = "at" in j.trigger ? `once @ ${new Date(j.trigger.at).toLocaleString()}` : "everyMs" in j.trigger ? `every ${(j.trigger.everyMs / 1e3).toFixed(0)}s` : `cron: ${j.trigger.cron}`;
3709
+ return `${j.id} ${j.status} ${trig} runs:${j.runs} next:${next ? new Date(next).toLocaleTimeString() : "\u2014"}${j.label ? " " + j.label : ""}`;
3710
+ }).join("\n");
3711
+ }
3712
+ },
3713
+ {
3714
+ name: "ScheduleCancel",
3715
+ description: "Cancel a scheduled job by id.",
3716
+ parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
3717
+ async run({ id }) {
3718
+ return scheduler.cancel(String(id)) ? `Cancelled ${id}.` : `Error: no scheduled job '${id}'. Use ScheduleList to see jobs.`;
3719
+ }
3720
+ },
3721
+ {
3722
+ name: "Wakeup",
3723
+ description: 'Self-pacing: schedule a one-off re-invocation of the agent after a delay.\nUse this to resume work later in the session (e.g. "check back in 5 minutes").\nThe prompt fires once, while the session is alive. Equivalent to ScheduleTask with {at: now + delayMs}.',
3724
+ parameters: {
3725
+ type: "object",
3726
+ required: ["delayMs", "prompt"],
3727
+ properties: {
3728
+ delayMs: { type: "number", description: "Delay in milliseconds (minimum 5000)." },
3729
+ prompt: { type: "string", description: "The prompt to inject when waking up." },
3730
+ label: { type: "string" }
3731
+ }
3732
+ },
3733
+ async run({ delayMs, prompt, label }) {
3734
+ const delay = Math.max(5e3, Number(delayMs) || 5e3);
3735
+ try {
3736
+ const id = scheduler.add({ prompt, trigger: { at: Date.now() + delay }, label: label ?? "wakeup" });
3737
+ return `Wakeup ${id} in ${(delay / 1e3).toFixed(0)}s.`;
3738
+ } catch (e) {
3739
+ return `Error: ${e?.message ?? e}`;
3740
+ }
3741
+ }
3742
+ }
3743
+ ];
3744
+ }
3745
+
3499
3746
  // src/presets.ts
3500
3747
  init_tools();
3501
3748
  import { MemFilesystem } from "@livx.cc/wcli/core";
@@ -8985,6 +9232,21 @@ async function repl(args, ai, cfg, cwd) {
8985
9232
  if (args.voice && !args.duplex) agent.options.tools = [...agent.options.tools ?? [], exitSessionTool(() => {
8986
9233
  exitRequested = true;
8987
9234
  })];
9235
+ const scheduleQueue = [];
9236
+ let scheduleTurn;
9237
+ const scheduler = new Scheduler({
9238
+ fire: (job) => {
9239
+ if (!activeTurn && scheduleTurn) {
9240
+ err(dim(` \u23F0 scheduled \u203A ${job.prompt.slice(0, 60)}${job.prompt.length > 60 ? "\u2026" : ""}
9241
+ `));
9242
+ void scheduleTurn(job.prompt).then(() => editorRef?.redrawNow());
9243
+ } else {
9244
+ scheduleQueue.push(job.prompt);
9245
+ }
9246
+ },
9247
+ tickMs: 15e3
9248
+ });
9249
+ agent.options.tools = [...agent.options.tools ?? [], ...makeScheduleTools(scheduler)];
8988
9250
  const duplex = args.duplex;
8989
9251
  let dx;
8990
9252
  let voiceIO;
@@ -9303,6 +9565,11 @@ async function repl(args, ai, cfg, cwd) {
9303
9565
  installCancelGuards(mounted);
9304
9566
  const store = new SessionStore(cwd);
9305
9567
  let session = startSession(args, store, face, cwd);
9568
+ if (session.meta.scheduledJobs?.length) {
9569
+ scheduler.restore(session.meta.scheduledJobs);
9570
+ err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
9571
+ `));
9572
+ }
9306
9573
  const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join9(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
9307
9574
  const cpHooks = checkpoints.hooks?.();
9308
9575
  if (cpHooks) work.hooks = composeHooks(work.hooks, cpHooks);
@@ -9356,6 +9623,11 @@ async function repl(args, ai, cfg, cwd) {
9356
9623
  goalTurns = m.goalTurns ?? 0;
9357
9624
  goalTokens = m.goalTokens ?? 0;
9358
9625
  goalLastReason = m.goalLastReason;
9626
+ if (m.scheduledJobs?.length) {
9627
+ scheduler.restore(m.scheduledJobs);
9628
+ err(dim(` \u23F0 ${scheduler.size} scheduled job(s) re-armed
9629
+ `));
9630
+ }
9359
9631
  err(dim(` resumed ${data.meta.id} (${data.meta.turns} turns)${data.meta.title ? " \u2014 " + data.meta.title : ""}
9360
9632
  `));
9361
9633
  if (goalCondition) err(dim(` \u25CE goal active: ${goalCondition} (${goalTurns} turns)
@@ -9441,6 +9713,7 @@ async function repl(args, ai, cfg, cwd) {
9441
9713
  }
9442
9714
  return r;
9443
9715
  };
9716
+ scheduleTurn = turn;
9444
9717
  const runSkill = async (sk, extra = "") => {
9445
9718
  try {
9446
9719
  const body = await fs.readFile(sk.path);
@@ -10417,6 +10690,7 @@ ${extra}` : body);
10417
10690
  if (r && r !== "off") parts.push(`reasoning:${r}`);
10418
10691
  if (verboseOutput) parts.push("verbose");
10419
10692
  if (goalCondition) parts.push(`\u25CE goal (${goalTurns} turns)`);
10693
+ if (scheduler.size) parts.push(`\u23F0 ${scheduler.size} scheduled`);
10420
10694
  if (inputStash.length) parts.push(`${inputStash.length} stashed (\u2303S to pop)`);
10421
10695
  const taskLines = [];
10422
10696
  if (dx) {
@@ -10485,8 +10759,20 @@ ${extra}` : body);
10485
10759
  `));
10486
10760
  quit = await dispatchLine(next) === "quit";
10487
10761
  }
10762
+ while (!quit && scheduleQueue.length) {
10763
+ const prompt = scheduleQueue.shift();
10764
+ err(dim(` \u23F0 scheduled \u203A ${prompt.slice(0, 60)}${prompt.length > 60 ? "\u2026" : ""}
10765
+ `));
10766
+ await turn(prompt);
10767
+ }
10768
+ session.meta.scheduledJobs = scheduler.snapshot();
10769
+ try {
10770
+ store.save(session);
10771
+ } catch {
10772
+ }
10488
10773
  if (quit) break;
10489
10774
  }
10775
+ scheduler.destroy();
10490
10776
  voiceIO?.stop();
10491
10777
  if (dx) {
10492
10778
  const running = [...dx.tasks.values()].filter((t) => t.status === "running");