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 +286 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +94 -1
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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");
|