agent-accelerator 0.5.9 → 0.6.0

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.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +366 -161
  7. package/src/agent/architecture.md +13 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +444 -365
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +56 -27
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +39 -30
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +147 -59
  25. package/src/providers/openai-compat.ts +57 -49
  26. package/src/providers/openai.ts +54 -37
  27. package/src/providers/openrouter.ts +85 -30
  28. package/src/providers/registry.ts +6 -21
  29. package/src/providers/shared.ts +87 -33
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +7 -6
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +6 -14
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +15 -3
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. package/src/utils/session.ts +1 -2
@@ -42,7 +42,6 @@ import {
42
42
  readTask,
43
43
  readTasks,
44
44
  removeTaskFolder,
45
- resolveHome,
46
45
  runPath,
47
46
  touchHeartbeat,
48
47
  updateDeliveries,
@@ -54,6 +53,7 @@ import {
54
53
  type PendingDelivery,
55
54
  type RunHeader,
56
55
  } from "./store.ts";
56
+ import { resolveHome } from "../utils/home.ts";
57
57
  import {
58
58
  assertCookShorterThanRepeat,
59
59
  cookTime,
@@ -150,7 +150,7 @@ function requireText(field: string, value: unknown): void {
150
150
  // Throws unless every requested worker tool name exists in the developer's pool; in: create/update validation, out: collect.
151
151
  function checkTools(value: unknown, pool: PoolTools): void {
152
152
  if (!Array.isArray(value) || value.some((n) => typeof n !== "string")) {
153
- throw new ValidationError("tools: must be an array of worker tool names, e.g. [\"recent_news\"].");
153
+ throw new ValidationError('tools: must be an array of worker tool names, e.g. ["recent_news"].');
154
154
  }
155
155
  const unknown = (value as string[]).filter((n) => Object.keys(grantWorkerTools(pool, [n])).length === 0);
156
156
  if (unknown.length > 0) {
@@ -179,7 +179,9 @@ function checkSchedule(
179
179
  collect(problems, () => {
180
180
  const repeatMs = durationMs(sa.repeatEvery, "scheduled_action.repeatEvery");
181
181
  if (repeatMs === 0) {
182
- throw new ValidationError('scheduled_action.repeatEvery: must be greater than 00:00:00 (e.g. "01:00:00" = every day).');
182
+ throw new ValidationError(
183
+ 'scheduled_action.repeatEvery: must be greater than 00:00:00 (e.g. "01:00:00" = every day).'
184
+ );
183
185
  }
184
186
  if (cookMs !== undefined) assertCookShorterThanRepeat(cookMs, repeatMs);
185
187
  });
@@ -188,15 +190,24 @@ function checkSchedule(
188
190
  problems.push("scheduled_action.retries: must be a whole number >= 0.");
189
191
  }
190
192
  if (sa.onMissed !== undefined && sa.onMissed !== "skip" && sa.onMissed !== "run") {
191
- problems.push(`scheduled_action.onMissed: must be "skip" or "run" (omit it to decide later with run_now or skip); got ${JSON.stringify(sa.onMissed)}.`);
193
+ problems.push(
194
+ `scheduled_action.onMissed: must be "skip" or "run" (omit it to decide later with run_now or skip); got ${JSON.stringify(sa.onMissed)}.`
195
+ );
192
196
  }
193
197
  }
194
198
 
195
199
  // Validates role, tools and timeoutMs of a create or update; in: validateInput/updateScheduledAction, out: the problems list.
196
- function checkOptionalFields(input: Pick<ScheduledActionPatch, "role" | "tools" | "timeoutMs">, pool: PoolTools | undefined, problems: string[]): void {
200
+ function checkOptionalFields(
201
+ input: Pick<ScheduledActionPatch, "role" | "tools" | "timeoutMs">,
202
+ pool: PoolTools | undefined,
203
+ problems: string[]
204
+ ): void {
197
205
  if (input.role !== undefined && typeof input.role !== "string") problems.push("role: must be a string.");
198
206
  if (input.tools !== undefined && pool) collect(problems, () => checkTools(input.tools, pool));
199
- if (input.timeoutMs !== undefined && !(typeof input.timeoutMs === "number" && Number.isFinite(input.timeoutMs) && input.timeoutMs > 0)) {
207
+ if (
208
+ input.timeoutMs !== undefined &&
209
+ !(typeof input.timeoutMs === "number" && Number.isFinite(input.timeoutMs) && input.timeoutMs > 0)
210
+ ) {
200
211
  problems.push("timeoutMs: must be a number of milliseconds > 0.");
201
212
  }
202
213
  }
@@ -290,7 +301,11 @@ export class Scheduler {
290
301
  this.startDue(now);
291
302
  if (!this.hasPending()) this.stop();
292
303
  } catch (err) {
293
- telemetryBus.emitWarning("scheduled_action.error", `[Agent Accelerator] Scheduler tick failed: ${errText(err)}`, {});
304
+ telemetryBus.emitWarning(
305
+ "scheduled_action.error",
306
+ `[Agent Accelerator] Scheduler tick failed: ${errText(err)}`,
307
+ {}
308
+ );
294
309
  }
295
310
  }
296
311
 
@@ -324,7 +339,9 @@ export class Scheduler {
324
339
  // True while anything keeps the clock useful (a run in flight, a saved delivery, a waiting or cooking action); in: ensureRunning and tick, out: start/stop of the interval.
325
340
  private hasPending(): boolean {
326
341
  const { live, pending } = this.snapshot();
327
- return this.running.size > 0 || pending.length > 0 || live.some((t) => t.state === "waiting" || t.state === "cooking");
342
+ return (
343
+ this.running.size > 0 || pending.length > 0 || live.some((t) => t.state === "waiting" || t.state === "cooking")
344
+ );
328
345
  }
329
346
 
330
347
  // ----- startup -----------------------------------------------------------
@@ -412,8 +429,18 @@ export class Scheduler {
412
429
  return true;
413
430
  });
414
431
  if (!changed) return;
415
- const next = policy === "skip" ? "Skipping it (onMissed: skip)." : policy === "run" ? "Running it now (onMissed: run)." : "Use run_now or skip.";
416
- emit("missed", `"${name}" (${id}) missed its run (${reason}). ${next}`, { id, name, reason, ...(policy ? { onMissed: policy } : {}) }, "warn");
432
+ const next =
433
+ policy === "skip"
434
+ ? "Skipping it (onMissed: skip)."
435
+ : policy === "run"
436
+ ? "Running it now (onMissed: run)."
437
+ : "Use run_now or skip.";
438
+ emit(
439
+ "missed",
440
+ `"${name}" (${id}) missed its run (${reason}). ${next}`,
441
+ { id, name, reason, ...(policy ? { onMissed: policy } : {}) },
442
+ "warn"
443
+ );
417
444
  if (policy === "skip") this.skip(id);
418
445
  else if (policy === "run") this.startCook(id, true);
419
446
  }
@@ -439,12 +466,21 @@ export class Scheduler {
439
466
  });
440
467
  if (!removed) return; // another process took this delivery: exactly one process delivers it
441
468
  const deliverAt = parseLocalIso(entry.deliverAt).getTime();
442
- const late = entry.late === true || now.getTime() - deliverAt > 2 * this.tickMs || parseLocalIso(entry.readyAt).getTime() > deliverAt;
469
+ const late =
470
+ entry.late === true ||
471
+ now.getTime() - deliverAt > 2 * this.tickMs ||
472
+ parseLocalIso(entry.readyAt).getTime() > deliverAt;
443
473
  let name = entry.id;
444
474
  updateTask(this.home, entry.id, (t) => {
445
475
  if (!t) return;
446
476
  name = t.name;
447
- t.lastRun = { status: entry.status, file: entry.file, deliverAt: entry.deliverAt, deliveredAt: toLocalIso(now), late };
477
+ t.lastRun = {
478
+ status: entry.status,
479
+ file: entry.file,
480
+ deliverAt: entry.deliverAt,
481
+ deliveredAt: toLocalIso(now),
482
+ late,
483
+ };
448
484
  });
449
485
  const delivery: ScheduledDelivery = {
450
486
  id: entry.id,
@@ -502,7 +538,8 @@ export class Scheduler {
502
538
  }
503
539
  if (t.state === "cooking") return undefined;
504
540
  // Another process may have started (and even finished) this run since the caller looked: a scheduled start needs the action to be waiting and due right now.
505
- if (!manual && (t.state !== "waiting" || !t.nextCookAt || parseLocalIso(t.nextCookAt).getTime() > now.getTime())) return undefined;
541
+ if (!manual && (t.state !== "waiting" || !t.nextCookAt || parseLocalIso(t.nextCookAt).getTime() > now.getTime()))
542
+ return undefined;
506
543
  ctx.replacesMissed = manual && t.state === "missed";
507
544
  ctx.agent = this.agents.get(t.agent)?.deref();
508
545
  if (!ctx.agent) {
@@ -515,7 +552,8 @@ export class Scheduler {
515
552
  const deliverAt = manual || !t.nextDeliverAt ? now : parseLocalIso(t.nextDeliverAt);
516
553
  let next = { cook: t.nextCookAt, deliver: t.nextDeliverAt };
517
554
  if (!manual) {
518
- const following = t.schedule.repeatEvery && t.nextDeliverAt ? nextOccurrence(deliverAt, t.schedule.repeatEvery) : undefined;
555
+ const following =
556
+ t.schedule.repeatEvery && t.nextDeliverAt ? nextOccurrence(deliverAt, t.schedule.repeatEvery) : undefined;
519
557
  next = following
520
558
  ? { cook: toLocalIso(cookTime(following, cookMs)), deliver: toLocalIso(following) }
521
559
  : { cook: null, deliver: null };
@@ -531,7 +569,12 @@ export class Scheduler {
531
569
  });
532
570
  if (ctx.noAgent) {
533
571
  const name = this.nameOf(id);
534
- emit("missed", `"${name}" (${id}) cannot run: no live Agent named in its task.json is registered.`, { id, name, reason: "no_agent" }, "warn");
572
+ emit(
573
+ "missed",
574
+ `"${name}" (${id}) cannot run: no live Agent named in its task.json is registered.`,
575
+ { id, name, reason: "no_agent" },
576
+ "warn"
577
+ );
535
578
  }
536
579
  if (!started || !ctx.agent) return undefined;
537
580
  emit("started", `"${started.name}" (${id}) started cooking.`, {
@@ -561,23 +604,39 @@ export class Scheduler {
561
604
  }
562
605
  const cookMs = durationMs(t.schedule.startCookingInAdvance, "schedule.startCookingInAdvance");
563
606
  let deliverAt = parseLocalIso(t.nextDeliverAt);
564
- while (cookTime(deliverAt, cookMs).getTime() <= now.getTime()) deliverAt = nextOccurrence(deliverAt, t.schedule.repeatEvery);
607
+ while (cookTime(deliverAt, cookMs).getTime() <= now.getTime())
608
+ deliverAt = nextOccurrence(deliverAt, t.schedule.repeatEvery);
565
609
  t.nextDeliverAt = toLocalIso(deliverAt);
566
610
  t.nextCookAt = toLocalIso(cookTime(deliverAt, cookMs));
567
611
  });
568
612
  if (ctx.skipped) {
569
613
  const { name, deliverAt } = ctx.skipped;
570
- emit("skipped", `"${name}" (${id}) skipped a run: the previous one is still cooking.`, { id, name, reason: "overlap", deliverAt }, "warn");
614
+ emit(
615
+ "skipped",
616
+ `"${name}" (${id}) skipped a run: the previous one is still cooking.`,
617
+ { id, name, reason: "overlap", deliverAt },
618
+ "warn"
619
+ );
571
620
  }
572
621
  }
573
622
 
574
623
  // Starts one run without awaiting it and tracks it until it ends; in: startCook, out: this.running + the run's files.
575
624
  private launch(task: ScheduledActionRecord, agent: Agent, manual: boolean, missed: boolean): void {
576
- const entry: RunningEntry = { controller: new AbortController(), done: Promise.resolve(), deleted: false, manual, missed };
625
+ const entry: RunningEntry = {
626
+ controller: new AbortController(),
627
+ done: Promise.resolve(),
628
+ deleted: false,
629
+ manual,
630
+ missed,
631
+ };
577
632
  this.running.set(task.id, entry);
578
633
  entry.done = this.runWorker(task, agent, entry)
579
634
  .catch((err) => {
580
- telemetryBus.emitWarning("scheduled_action.error", `[Agent Accelerator] Run of "${task.id}" failed unexpectedly: ${errText(err)}`, { id: task.id });
635
+ telemetryBus.emitWarning(
636
+ "scheduled_action.error",
637
+ `[Agent Accelerator] Run of "${task.id}" failed unexpectedly: ${errText(err)}`,
638
+ { id: task.id }
639
+ );
581
640
  })
582
641
  .finally(() => {
583
642
  this.running.delete(task.id);
@@ -597,14 +656,21 @@ export class Scheduler {
597
656
  }
598
657
 
599
658
  /** A fresh stateless worker built from the saved fields; retries only while there is time before delivery. */
600
- private async execute(task: ScheduledActionRecord, agent: Agent, entry: RunningEntry, deliverAt: Date): Promise<Outcome> {
659
+ private async execute(
660
+ task: ScheduledActionRecord,
661
+ agent: Agent,
662
+ entry: RunningEntry,
663
+ deliverAt: Date
664
+ ): Promise<Outcome> {
601
665
  let worker: Agent | undefined;
602
666
  try {
603
667
  const { Agent: AgentClass } = await import("../agent/agent.ts");
604
668
  const pool = agent.dynamicSubagents?.tools ?? {};
605
669
  const missing = task.tools.filter((n) => Object.keys(grantWorkerTools(pool, [n])).length === 0);
606
670
  if (missing.length > 0) {
607
- throw new Error(`worker tool(s) no longer in the pool: ${missing.join(", ")} (available: ${Object.keys(pool).join(", ") || "none"})`);
671
+ throw new Error(
672
+ `worker tool(s) no longer in the pool: ${missing.join(", ")} (available: ${Object.keys(pool).join(", ") || "none"})`
673
+ );
608
674
  }
609
675
  worker = new AgentClass(
610
676
  buildDynamicWorkerConfig(agent, {
@@ -635,7 +701,12 @@ export class Scheduler {
635
701
  }
636
702
 
637
703
  // One worker run with a per-attempt timeout and the delete signal, raced so a hung provider cannot hold it; in: execute, out: the worker's AgentResponse.
638
- private async attempt(worker: Agent, task: ScheduledActionRecord, agent: Agent, entry: RunningEntry): Promise<AgentResponse> {
704
+ private async attempt(
705
+ worker: Agent,
706
+ task: ScheduledActionRecord,
707
+ agent: Agent,
708
+ entry: RunningEntry
709
+ ): Promise<AgentResponse> {
639
710
  const ms = scheduledRunTimeoutMs(agent.dynamicSubagents?.workerTimeoutMs ?? Infinity, task.timeoutMs);
640
711
  // One controller per attempt (the timeout is per attempt), fed by the entry's delete controller.
641
712
  const controller = new AbortController();
@@ -645,7 +716,9 @@ export class Scheduler {
645
716
  const timer = ms > 0 ? setTimeout(() => controller.abort(new Error(`timed out after ${ms}ms`)), ms) : undefined;
646
717
  // Raced so a timeout or delete ends the attempt even if the provider ignores the signal.
647
718
  const aborted = new Promise<never>((_, reject) => {
648
- controller.signal.addEventListener("abort", () => reject(controller.signal.reason ?? new Error("aborted")), { once: true });
719
+ controller.signal.addEventListener("abort", () => reject(controller.signal.reason ?? new Error("aborted")), {
720
+ once: true,
721
+ });
649
722
  });
650
723
  try {
651
724
  return await Promise.race([worker.run(task.task, { signal: controller.signal }), aborted]);
@@ -656,7 +729,13 @@ export class Scheduler {
656
729
  }
657
730
 
658
731
  // Writes a finished run: run file, then the `delivery.json` entry, then `task.json` (a crash in between is repaired on start); a manual run is due when it finishes and is late only if it replaced a missed occurrence, a scheduled one is late only past its second; in: runWorker, out: the files and the ready/failed event.
659
- private finish(task: ScheduledActionRecord, outcome: Outcome, startedAt: string, deliverAt: Date, run: Pick<RunningEntry, "manual" | "missed">): void {
732
+ private finish(
733
+ task: ScheduledActionRecord,
734
+ outcome: Outcome,
735
+ startedAt: string,
736
+ deliverAt: Date,
737
+ run: Pick<RunningEntry, "manual" | "missed">
738
+ ): void {
660
739
  const now = this.now();
661
740
  const ok = "res" in outcome;
662
741
  const finishedAt = toLocalIso(now);
@@ -677,16 +756,35 @@ export class Scheduler {
677
756
  const body = ok ? outcome.res.text.trim() || "[The worker returned no text.]" : `The run failed: ${outcome.error}`;
678
757
  const file = writeRunFile(this.home, task.id, deliverAt, header, body);
679
758
  updateDeliveries(this.home, task.id, (pending) => {
680
- pending.push({ id: task.id, file, status: header.status, readyAt: finishedAt, deliverAt: header.deliverAt, ...(run.missed ? { late: true } : {}) });
759
+ pending.push({
760
+ id: task.id,
761
+ file,
762
+ status: header.status,
763
+ readyAt: finishedAt,
764
+ deliverAt: header.deliverAt,
765
+ ...(run.missed ? { late: true } : {}),
766
+ });
681
767
  });
682
768
  updateTask(this.home, task.id, (t) => {
683
769
  if (!t || t.state !== "cooking") return;
684
770
  delete t.current;
685
771
  t.state = t.nextDeliverAt ? "waiting" : "done";
686
772
  });
687
- const data = { id: task.id, name: task.name, file: runPath(this.home, file), readyAt: finishedAt, deliverAt: header.deliverAt };
773
+ const data = {
774
+ id: task.id,
775
+ name: task.name,
776
+ file: runPath(this.home, file),
777
+ readyAt: finishedAt,
778
+ deliverAt: header.deliverAt,
779
+ };
688
780
  if (ok) emit("ready", `"${task.name}" (${task.id}) finished; its result is saved.`, data);
689
- else emit("failed", `"${task.name}" (${task.id}) failed: ${(outcome as { error: string }).error}`, { ...data, error: (outcome as { error: string }).error }, "warn");
781
+ else
782
+ emit(
783
+ "failed",
784
+ `"${task.name}" (${task.id}) failed: ${(outcome as { error: string }).error}`,
785
+ { ...data, error: (outcome as { error: string }).error },
786
+ "warn"
787
+ );
690
788
  }
691
789
 
692
790
  // ----- commands ----------------------------------------------------------
@@ -704,7 +802,10 @@ export class Scheduler {
704
802
  );
705
803
  }
706
804
  const started = this.startCook(id, true);
707
- if (!started) throw new ValidationError(`Scheduled action "${id}" could not be started (see the scheduled_action.invalid warning).`);
805
+ if (!started)
806
+ throw new ValidationError(
807
+ `Scheduled action "${id}" could not be started (see the scheduled_action.invalid warning).`
808
+ );
708
809
  return started;
709
810
  }
710
811
 
@@ -713,7 +814,8 @@ export class Scheduler {
713
814
  const now = this.now();
714
815
  const rec = updateTask(this.home, id, (t) => {
715
816
  if (!t) throw notFound(id);
716
- if (t.state !== "missed") throw new ValidationError(`Scheduled action "${id}" is ${t.state}, not missed: nothing to skip.`);
817
+ if (t.state !== "missed")
818
+ throw new ValidationError(`Scheduled action "${id}" is ${t.state}, not missed: nothing to skip.`);
717
819
  const next = this.afterMissed(t, now);
718
820
  t.nextCookAt = next.cook;
719
821
  t.nextDeliverAt = next.deliver;
@@ -721,7 +823,12 @@ export class Scheduler {
721
823
  delete t.current;
722
824
  return t;
723
825
  });
724
- emit("skipped", `"${rec.name}" (${id}) skipped its missed run.`, { id, name: rec.name, reason: "skip", nextDeliverAt: rec.nextDeliverAt });
826
+ emit("skipped", `"${rec.name}" (${id}) skipped its missed run.`, {
827
+ id,
828
+ name: rec.name,
829
+ reason: "skip",
830
+ nextDeliverAt: rec.nextDeliverAt,
831
+ });
725
832
  this.ensureRunning();
726
833
  return rec;
727
834
  }
@@ -809,31 +916,35 @@ export function createScheduledAction(agent: Agent, input: ScheduledActionInput)
809
916
  const sa = input.scheduled_action;
810
917
  const lead = sa.startCookingInAdvance ?? DEFAULT_COOK_LEAD;
811
918
  const deliverAt = resolveDelivery(now, sa.timeToDeliver, sa.meridiem);
812
- const record = createTask(s.home, () => `${sa.id}-${randomSuffix()}`, (id) => {
813
- const rec: ScheduledActionRecord = {
814
- id,
815
- name: sa.name.trim(),
816
- agent: agent.name,
817
- ...(input.role ? { role: input.role } : {}),
818
- instructions: input.instructions,
819
- task: input.task,
820
- tools: input.tools ?? [],
821
- ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
822
- schedule: {
823
- timeToDeliver: sa.timeToDeliver,
824
- meridiem: sa.meridiem,
825
- startCookingInAdvance: lead,
826
- ...(sa.repeatEvery !== undefined ? { repeatEvery: sa.repeatEvery } : {}),
827
- ...(sa.retries !== undefined ? { retries: sa.retries } : {}),
828
- ...(sa.onMissed !== undefined ? { onMissed: sa.onMissed } : {}),
829
- },
830
- state: "waiting",
831
- nextCookAt: toLocalIso(cookTime(deliverAt, durationMs(lead, "scheduled_action.startCookingInAdvance"))),
832
- nextDeliverAt: toLocalIso(deliverAt),
833
- createdAt: toLocalIso(now),
834
- };
835
- return rec;
836
- });
919
+ const record = createTask(
920
+ s.home,
921
+ () => `${sa.id}-${randomSuffix()}`,
922
+ (id) => {
923
+ const rec: ScheduledActionRecord = {
924
+ id,
925
+ name: sa.name.trim(),
926
+ agent: agent.name,
927
+ ...(input.role ? { role: input.role } : {}),
928
+ instructions: input.instructions,
929
+ task: input.task,
930
+ tools: input.tools ?? [],
931
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
932
+ schedule: {
933
+ timeToDeliver: sa.timeToDeliver,
934
+ meridiem: sa.meridiem,
935
+ startCookingInAdvance: lead,
936
+ ...(sa.repeatEvery !== undefined ? { repeatEvery: sa.repeatEvery } : {}),
937
+ ...(sa.retries !== undefined ? { retries: sa.retries } : {}),
938
+ ...(sa.onMissed !== undefined ? { onMissed: sa.onMissed } : {}),
939
+ },
940
+ state: "waiting",
941
+ nextCookAt: toLocalIso(cookTime(deliverAt, durationMs(lead, "scheduled_action.startCookingInAdvance"))),
942
+ nextDeliverAt: toLocalIso(deliverAt),
943
+ createdAt: toLocalIso(now),
944
+ };
945
+ return rec;
946
+ }
947
+ );
837
948
  s.agents.set(agent.name, new WeakRef(agent));
838
949
  emit("created", `"${record.name}" (${record.id}) scheduled for ${record.nextDeliverAt}.`, {
839
950
  id: record.id,
@@ -857,7 +968,11 @@ export function listScheduledActions(opts?: HomeOptions): Array<ScheduledActionR
857
968
  * recomputes the schedule (`DD` relative to now); schedule changes are
858
969
  * refused while the action is cooking.
859
970
  */
860
- export function updateScheduledAction(id: string, patch: ScheduledActionPatch, opts?: HomeOptions): ScheduledActionRecord {
971
+ export function updateScheduledAction(
972
+ id: string,
973
+ patch: ScheduledActionPatch,
974
+ opts?: HomeOptions
975
+ ): ScheduledActionRecord {
861
976
  const s = schedulerFor(opts);
862
977
  const now = s.now();
863
978
  const sa = patch.scheduled_action ?? {};
@@ -868,11 +983,14 @@ export function updateScheduledAction(id: string, patch: ScheduledActionPatch, o
868
983
  if (stored) throw new ValidationError(`Scheduled action "${id}" is invalid in its task.json: ${stored}`);
869
984
  const scheduleChanged = Object.values(scheduleChange).some((v) => v !== undefined);
870
985
  if (scheduleChanged && t.state === "cooking") {
871
- throw new ValidationError(`Scheduled action "${id}" is cooking right now; schedule changes are refused while it runs. Try again after it finishes.`);
986
+ throw new ValidationError(
987
+ `Scheduled action "${id}" is cooking right now; schedule changes are refused while it runs. Try again after it finishes.`
988
+ );
872
989
  }
873
990
  const deliveryChanged = scheduleChange.timeToDeliver !== undefined || scheduleChange.meridiem !== undefined;
874
991
  const merged = { ...t.schedule };
875
- for (const [key, value] of Object.entries(scheduleChange)) if (value !== undefined) (merged as Record<string, unknown>)[key] = value;
992
+ for (const [key, value] of Object.entries(scheduleChange))
993
+ if (value !== undefined) (merged as Record<string, unknown>)[key] = value;
876
994
 
877
995
  const problems: string[] = [];
878
996
  const pool = s.agents.get(t.agent)?.deref()?.dynamicSubagents?.tools;
@@ -903,7 +1021,12 @@ export function updateScheduledAction(id: string, patch: ScheduledActionPatch, o
903
1021
  }
904
1022
  return t;
905
1023
  });
906
- emit("updated", `"${rec.name}" (${id}) updated.`, { id, name: rec.name, nextCookAt: rec.nextCookAt, nextDeliverAt: rec.nextDeliverAt });
1024
+ emit("updated", `"${rec.name}" (${id}) updated.`, {
1025
+ id,
1026
+ name: rec.name,
1027
+ nextCookAt: rec.nextCookAt,
1028
+ nextDeliverAt: rec.nextDeliverAt,
1029
+ });
907
1030
  s.ensureRunning();
908
1031
  return rec;
909
1032
  }
@@ -923,6 +1046,9 @@ export function skipScheduledAction(id: string, opts?: HomeOptions): ScheduledAc
923
1046
  * `force: true` (the run is aborted, then deleted). Past run files are kept
924
1047
  * unless `purge: true`.
925
1048
  */
926
- export function deleteScheduledAction(id: string, opts?: HomeOptions & { force?: boolean; purge?: boolean }): Promise<DeleteResult> {
1049
+ export function deleteScheduledAction(
1050
+ id: string,
1051
+ opts?: HomeOptions & { force?: boolean; purge?: boolean }
1052
+ ): Promise<DeleteResult> {
927
1053
  return schedulerFor(opts).delete(id, { force: opts?.force, purge: opts?.purge });
928
1054
  }
@@ -17,8 +17,6 @@ import * as path from "node:path";
17
17
  import { z } from "zod";
18
18
  import { readWithBackup, withFileMutex, writeFileDurable } from "../utils/durable.ts";
19
19
  import { telemetryBus } from "../telemetry.ts";
20
- import { resolveHome } from "../utils/home.ts";
21
- export { resolveHome };
22
20
  import { ValidationError } from "../types/errors.ts";
23
21
  import type { CostSource } from "../session/usage.ts";
24
22
  import type { ScheduledActionRecord, ScheduledRunStatus } from "../types/scheduler.ts";
@@ -106,7 +104,9 @@ export interface Heartbeat {
106
104
 
107
105
  /** Marks an action as running in this process (pid, host, token); written before the record flips to `cooking`; in: startCook, out: `<id>/.heartbeat`. */
108
106
  export function writeHeartbeat(home: string, id: string, token: string): void {
109
- fs.writeFileSync(heartbeatFile(taskDir(home, id)), JSON.stringify({ pid: process.pid, host: os.hostname(), token }), { mode: 0o600 });
107
+ fs.writeFileSync(heartbeatFile(taskDir(home, id)), JSON.stringify({ pid: process.pid, host: os.hostname(), token }), {
108
+ mode: 0o600,
109
+ });
110
110
  }
111
111
 
112
112
  /** Refreshes the heartbeat's modification time; false when the file is gone; in: the scheduler tick for each run in flight. */
@@ -147,7 +147,8 @@ const isMissingFolder = (err: unknown): boolean => (err as { code?: string } | n
147
147
  // Deletes the leftover temp files of a task.json / delivery.json write that never finished (a killed process); only called while holding the action's mutex, which is what makes it safe (run files are written outside the mutex, so they are never touched); in: the three mutating helpers, out: nothing.
148
148
  function sweepShardTemps(dir: string): void {
149
149
  try {
150
- for (const name of fs.readdirSync(dir)) if (/^(task|delivery)\.json\.tmp-\d+-[0-9a-z]+$/.test(name)) fs.rmSync(path.join(dir, name), { force: true });
150
+ for (const name of fs.readdirSync(dir))
151
+ if (/^(task|delivery)\.json\.tmp-\d+-[0-9a-z]+$/.test(name)) fs.rmSync(path.join(dir, name), { force: true });
151
152
  } catch {
152
153
  // why: an unreadable folder or an undeletable leftover is harmless.
153
154
  }
@@ -288,7 +289,8 @@ export function updateTask<T>(home: string, id: string, fn: (rec: ScheduledActio
288
289
  const rec = readTask(home, id);
289
290
  const before = rec ? JSON.stringify(rec) : "";
290
291
  const result = fn(rec);
291
- if (rec && JSON.stringify(rec) !== before) writeFileDurable(taskFile(dir), JSON.stringify({ version: 2, task: rec }, null, 2) + "\n");
292
+ if (rec && JSON.stringify(rec) !== before)
293
+ writeFileDurable(taskFile(dir), JSON.stringify({ version: 2, task: rec }, null, 2) + "\n");
292
294
  return result;
293
295
  });
294
296
  } catch (err) {
@@ -301,7 +303,11 @@ export function updateTask<T>(home: string, id: string, fn: (rec: ScheduledActio
301
303
  * Creates an action in a new folder. The folder is made with a create-only `mkdir`, so two processes can never take the
302
304
  * same id; `nextId` is called again until a free one is found, then `build` makes the record for it.
303
305
  */
304
- export function createTask(home: string, nextId: () => string, build: (id: string) => ScheduledActionRecord): ScheduledActionRecord {
306
+ export function createTask(
307
+ home: string,
308
+ nextId: () => string,
309
+ build: (id: string) => ScheduledActionRecord
310
+ ): ScheduledActionRecord {
305
311
  fs.mkdirSync(baseDir(home), { recursive: true, mode: 0o700 });
306
312
  for (let attempt = 0; attempt < 100; attempt++) {
307
313
  const id = nextId();
@@ -330,7 +336,8 @@ export function deleteTask(home: string, id: string): boolean {
330
336
  try {
331
337
  removed = withFileMutex(mutexFile(dir), () => {
332
338
  const had = fs.existsSync(taskFile(dir)) || fs.existsSync(deliveryFile(dir));
333
- for (const f of [taskFile(dir), deliveryFile(dir)]) fs.rmSync(f, { force: true }), fs.rmSync(`${f}.bak`, { force: true });
339
+ for (const f of [taskFile(dir), deliveryFile(dir)])
340
+ fs.rmSync(f, { force: true }), fs.rmSync(`${f}.bak`, { force: true });
334
341
  fs.rmSync(heartbeatFile(dir), { force: true });
335
342
  return had;
336
343
  });
@@ -352,7 +359,8 @@ export function readDeliveries(home: string): PendingDelivery[] {
352
359
  for (const folder of actionFolders(home)) {
353
360
  if (!TASK_ID_PATTERN.test(folder) || folder.length > MAX_TASK_ID_LENGTH) continue;
354
361
  const file = deliveryFile(taskDir(home, folder));
355
- for (const entry of readShard(file, parsePending) ?? []) if (inOwnFolder(file, folder, entry["id"])) out.push(entry as unknown as PendingDelivery);
362
+ for (const entry of readShard(file, parsePending) ?? [])
363
+ if (inOwnFolder(file, folder, entry["id"])) out.push(entry as unknown as PendingDelivery);
356
364
  }
357
365
  return out.sort((a, b) => String(a.deliverAt).localeCompare(String(b.deliverAt)));
358
366
  }
@@ -441,7 +449,9 @@ const deliverySchema = z.object({
441
449
  /** `undefined` when a `delivery.json` entry is usable, else a one-line reason. Never throws. */
442
450
  export function validateDelivery(raw: unknown): string | undefined {
443
451
  const parsed = deliverySchema.safeParse(raw);
444
- return parsed.success ? undefined : parsed.error.issues.map((i) => `${i.path.join(".") || "entry"}: ${i.message}`).join("; ");
452
+ return parsed.success
453
+ ? undefined
454
+ : parsed.error.issues.map((i) => `${i.path.join(".") || "entry"}: ${i.message}`).join("; ");
445
455
  }
446
456
 
447
457
  // ---------------------------------------------------------------------------
@@ -472,7 +482,10 @@ export function writeRunFile(home: string, id: string, deliverAt: Date, header:
472
482
  }
473
483
 
474
484
  /** Reads a run file (header fields as strings + result text). `undefined` when missing or outside the folder. */
475
- export function readRun(home: string, relativeFile: string): { header: Record<string, string>; body: string } | undefined {
485
+ export function readRun(
486
+ home: string,
487
+ relativeFile: string
488
+ ): { header: Record<string, string>; body: string } | undefined {
476
489
  const file = runPath(home, relativeFile);
477
490
  if (!file.startsWith(baseDir(home) + path.sep)) return undefined;
478
491
  let text: string;
@@ -7,8 +7,18 @@
7
7
  import { ValidationError } from "../types/errors.ts";
8
8
 
9
9
  const MONTHS = [
10
- "january", "february", "march", "april", "may", "june",
11
- "july", "august", "september", "october", "november", "december",
10
+ "january",
11
+ "february",
12
+ "march",
13
+ "april",
14
+ "may",
15
+ "june",
16
+ "july",
17
+ "august",
18
+ "september",
19
+ "october",
20
+ "november",
21
+ "december",
12
22
  ];
13
23
  const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
14
24
 
@@ -21,7 +31,11 @@ const pad2 = (n: number): string => String(n).padStart(2, "0");
21
31
  const show = (v: unknown): string => JSON.stringify(v) ?? String(v);
22
32
 
23
33
  /** Parses `DD:HH:MM` (DD >= 2 digits, MM < 60). HH is range-checked by the caller. */
24
- export function parseDDHHMM(value: unknown, field: string, example = "00:07:00"): { dd: number; hh: number; mm: number } {
34
+ export function parseDDHHMM(
35
+ value: unknown,
36
+ field: string,
37
+ example = "00:07:00"
38
+ ): { dd: number; hh: number; mm: number } {
25
39
  const m = typeof value === "string" ? DDHHMM.exec(value) : null;
26
40
  if (!m || Number(m[3]) > 59) {
27
41
  throw new ValidationError(