Files
EugeneTes 6e3241fed9 feat(scheduler): scheduled jobs inside skills (.volcanic/skills/<skill>/jobs/<job>.md)
Skill directories may now declare prompt or watcher jobs as files (cron or one-off at).
The file is the source of truth: the scheduler re-reads every chat's jobs/ every minute,
around agent runs and before /jobs and /skills, and schedules, re-plans or removes them.
Ids are <skill>/<job>; run state lives in .volcanic/scheduler/skill-jobs-state.json, never
in jobs.json. Watchers run from the skill directory with VOLCANIC_WORKSPACE and
VOLCANIC_SKILL_DIR. Broken files are shown with the reason in /jobs, /skills, schedule list
and the new <skill_jobs> prompt section; cancel answers with the file to edit. Every part of
a skill directory is optional; file reading and frontmatter parsing move to Abstractions.
2026-10-14 12:54:40 +00:00

48 lines
2.6 KiB
C#

using System.Text.Json.Serialization;
namespace Volcanic.AgenticCore.Extensions.Scheduler;
/// <summary>Prompt: fixed text through the agent. Watcher: a script whose output goes to the agent.</summary>
public enum JobKind { Prompt, Watcher }
/// <summary>Exactly one is set. <see cref="At"/> is an absolute instant; <see cref="Cron"/> is 5-field wall-clock time in the target's zone.</summary>
public sealed record JobSchedule(DateTimeOffset? At, string? Cron);
/// <summary>The outcome of a job's last fire. <see cref="StderrTail"/> is stored redacted.</summary>
public sealed record JobResult(bool Ok, int? ExitCode, bool TimedOut, string? StderrTail, DateTimeOffset At);
/// <summary>A persisted job (<c>jobs.json</c>).</summary>
public sealed record ScheduledJob
{
public required string Id { get; init; }
/// <summary>Who the job belongs to: the chat id (invariant culture) on Telegram.</summary>
public required string Target { get; init; }
public required string Name { get; init; }
public required JobKind Kind { get; init; }
public required JobSchedule Schedule { get; init; }
/// <summary>Prompt only.</summary>
public string? Text { get; init; }
/// <summary>Watcher only.</summary>
public string? Command { get; init; }
/// <summary>Watcher only; default <see cref="SchedulerOptions.DefaultScriptTimeout"/>, at most 3600.</summary>
public int? TimeoutSeconds { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset? LastRunAt { get; init; }
public JobResult? LastResult { get; init; }
/// <summary>Set for a job declared by a skill's <c>jobs/&lt;job&gt;.md</c>; such jobs are never written to <c>jobs.json</c>.</summary>
[JsonIgnore]
public SkillJobSource? Skill { get; init; }
}
/// <summary>What the <c>schedule</c> tool asks for. <see cref="At"/> is ISO 8601 text; without an offset it's read in the target's zone.</summary>
public sealed record NewJob(string Name, JobKind Kind, string? At = null, string? Cron = null, string? Text = null, string? Command = null, int? TimeoutSeconds = null);
/// <summary>A job and its next fire time (null: never, e.g. a one-off already firing).</summary>
public sealed record JobView(ScheduledJob Job, DateTimeOffset? NextRunAt);
/// <summary>What a zone change did to one target's jobs.</summary>
public sealed record ZoneChange(int CronJobs, int OneOffs);
/// <summary>A job the model (or a stored file) got wrong. The message is a sentence the model can act on.</summary>
public sealed class JobValidationException(string message) : Exception(message);