41659b2412
The skill half of the persona/skill pair, as a clean redesign (not a faithful lift of mort's 60-method skills.Storage kitchen sink): - skill.go/skill_version.go/validate.go/inputs.go/schedule.go moved clean; the only host couplings severed: llms.IsTierName -> model.IsTierName, and the chatbot DefaultChatbotInputName const localized. - store.go: a DELIBERATELY LEAN SkillStore — skill lifecycle (CRUD + visibility) + versioning + scheduling ONLY. The KV/file/quota sub-stores that were fused into mort's interface are the tools/ store seams; email/channel grants stay host concerns. - runnable.go: Skill.ToRunnable() lowers a skill into run.RunnableAgent (flat tool list, no palette — composition is a host concern); DueAt() helper. - memory.go: NewMemory() — zero-dep in-process SkillStore (visibility filters, newest-first versions). Tests: ToRunnable mapping, visibility (public/shared/private) listing, version ordering + lookup. No mort dependency (go.mod tidy clean); core imports ZERO from skill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
package skill
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/robfig/cron/v3"
|
|
)
|
|
|
|
// scheduleParser is the cron parser shared across the skills package. It
|
|
// accepts the standard 5-field syntax (minute hour dom month dow) plus
|
|
// descriptors such as @daily, @hourly, etc. We do not enable the seconds
|
|
// field — schedule cadence is governed in minutes, and a seconds field
|
|
// would invite specs that fire below the min-interval floor without
|
|
// surfacing as such in the spec text.
|
|
//
|
|
// Why standalone vs. cron.ParseStandard: ParseStandard rejects descriptors
|
|
// (@daily, @hourly). Skills callers may want to write @daily as a
|
|
// shorthand alongside the explicit "daily" / "weekly" forms we translate
|
|
// below.
|
|
var scheduleParser = cron.NewParser(
|
|
cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
|
|
)
|
|
|
|
// ParseSchedule turns a user-supplied schedule expression into a
|
|
// cron.Schedule. The empty string returns (nil, nil) — callers should
|
|
// treat that as "on-demand only".
|
|
//
|
|
// Why: Skill.Schedule is a string field stored verbatim; the validator,
|
|
// the scheduler runner, and any future tooling all need to round-trip
|
|
// through the same parser. Centralising it here avoids drift.
|
|
//
|
|
// Accepted shorthands:
|
|
// - "daily" → "0 0 * * *" (midnight UTC every day)
|
|
// - "weekly" → "0 0 * * 0" (midnight UTC every Sunday)
|
|
//
|
|
// Anything else is fed through robfig/cron/v3's standard parser
|
|
// (descriptors enabled).
|
|
//
|
|
// Test: schedule_test.go covers shorthand expansion and invalid-spec
|
|
// rejection.
|
|
func ParseSchedule(expr string) (cron.Schedule, error) {
|
|
expr = strings.TrimSpace(expr)
|
|
if expr == "" {
|
|
return nil, nil
|
|
}
|
|
switch strings.ToLower(expr) {
|
|
case "daily":
|
|
expr = "0 0 * * *"
|
|
case "weekly":
|
|
expr = "0 0 * * 0"
|
|
}
|
|
sched, err := scheduleParser.Parse(expr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid schedule %q: %w", expr, err)
|
|
}
|
|
return sched, nil
|
|
}
|
|
|
|
// ScheduleMinInterval returns an estimate of the smallest gap between
|
|
// consecutive fire times for a parsed schedule. It samples the next two
|
|
// fire times from a couple of starting points and returns the smallest
|
|
// observed gap.
|
|
//
|
|
// Why: cron.Schedule does not expose a "smallest interval" API. The
|
|
// validator needs this to enforce a per-skill min-interval floor (so an
|
|
// admin can't accidentally register "* * * * *" and burn GPU minutes).
|
|
// Two probe points are enough to catch irregular schedules whose tightest
|
|
// gap appears at a particular point in the week (e.g. "0 9 * * 1,5",
|
|
// where Mon→Fri is 4d but Fri→Mon is 3d — both sampled).
|
|
//
|
|
// Returns 0 if sched is nil.
|
|
//
|
|
// Test: schedule_test.go covers a "* * * * *" minute-interval probe and
|
|
// the irregular Mon/Fri case.
|
|
func ScheduleMinInterval(sched cron.Schedule) time.Duration {
|
|
if sched == nil {
|
|
return 0
|
|
}
|
|
// Probe from a fixed reference and from a midweek offset. Six fire
|
|
// times across two starts catches weekly irregularities (the worst
|
|
// case is a schedule that fires once a week — we still get one gap
|
|
// per probe). Using a wall-clock-independent reference keeps the
|
|
// test deterministic.
|
|
starts := []time.Time{
|
|
time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), // Monday 00:00
|
|
time.Date(2024, 1, 4, 12, 30, 0, 0, time.UTC), // Thursday 12:30
|
|
time.Date(2024, 6, 15, 23, 59, 59, 0, time.UTC), // mid-year, late
|
|
}
|
|
var min time.Duration
|
|
for _, t := range starts {
|
|
// Sample three consecutive fires per start to capture two gaps.
|
|
f1 := sched.Next(t)
|
|
f2 := sched.Next(f1)
|
|
f3 := sched.Next(f2)
|
|
for _, gap := range []time.Duration{f2.Sub(f1), f3.Sub(f2)} {
|
|
if gap <= 0 {
|
|
continue
|
|
}
|
|
if min == 0 || gap < min {
|
|
min = gap
|
|
}
|
|
}
|
|
}
|
|
return min
|
|
}
|