From f445e1cac3b74db5a52846fa1afcd066fe85c747 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 21 Oct 2026 15:15:20 -0700 Subject: [PATCH] Add scheduled tasks with a Project tab section and lead tools --- TODO.md | 3 - api/openapi.yaml | 100 ++ go.mod | 2 +- internal/agents/coordinator.go | 19 +- internal/agents/lead.go | 1 + internal/agents/leadtools.go | 2 +- internal/agents/schedule.go | 263 +++++ internal/api/api.gen.go | 912 +++++++++++------- internal/ids/ids.go | 1 + internal/schedule/schedule.go | 124 +++ internal/server/schedules.go | 46 + internal/server/server.go | 3 +- internal/store/coordinate.go | 9 + .../store/migrations/096-add-schedules.sql | 23 + internal/store/schedule.go | 183 ++++ internal/store/workflow.go | 14 + mac/Sources/Byre/Dialogs.swift | 3 +- mac/Sources/Byre/Project.swift | 13 + mac/Sources/Byre/Schedules.swift | 259 +++++ mac/Sources/Byre/Store.swift | 28 + mac/Sources/Byre/WorkflowEditor.swift | 28 +- 21 files changed, 1689 insertions(+), 347 deletions(-) create mode 100644 internal/agents/schedule.go create mode 100644 internal/schedule/schedule.go create mode 100644 internal/server/schedules.go create mode 100644 internal/store/migrations/096-add-schedules.sql create mode 100644 internal/store/schedule.go create mode 100644 mac/Sources/Byre/Schedules.swift diff --git a/TODO.md b/TODO.md index b582388..ae63ce7 100644 --- a/TODO.md +++ b/TODO.md @@ -10,9 +10,6 @@ # M5 — Workflows -- Scheduled tasks - A `schedules` table (project, cron, prompt or workflow) and a timer that creates the task the way the lead's `create_task` does, managed with lead tools and no UI, like workflows. Example: every Monday, run `go vet ./...` and `gofmt -l` and check `docs/operations.md` against the code. Claude Code Routines, Codex Automations, Copilot cloud automations, Devin, Factory and Conductor all schedule agents. - # M6 — Fan-out # M7 — Inbox triage diff --git a/api/openapi.yaml b/api/openapi.yaml index 8f27b96..714882d 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -366,6 +366,66 @@ paths: "400": { $ref: "#/components/responses/Error" } "404": { $ref: "#/components/responses/Error" } + /api/projects/{projectId}/schedules: + parameters: + - { name: projectId, in: path, required: true, schema: { type: string } } + get: + operationId: listSchedules + description: > + The project's schedules, soonest next run first. schedule.saved and schedule.deleted events tell clients of changes. + responses: + "200": + description: The schedules. + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/Schedule" } + "404": { $ref: "#/components/responses/Error" } + post: + operationId: createSchedule + description: Creates a schedule run as the calling user, who needs a git identity. Records schedule.saved. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ScheduleSave" } + responses: + "201": + description: The new schedule. + content: + application/json: + schema: { $ref: "#/components/schemas/Schedule" } + "400": { $ref: "#/components/responses/Error" } + "404": { $ref: "#/components/responses/Error" } + + /api/schedules/{scheduleId}: + parameters: + - { name: scheduleId, in: path, required: true, schema: { type: string } } + put: + operationId: updateSchedule + description: Replaces a schedule's settings and recomputes its next run. Records schedule.saved. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ScheduleSave" } + responses: + "200": + description: The schedule. + content: + application/json: + schema: { $ref: "#/components/schemas/Schedule" } + "400": { $ref: "#/components/responses/Error" } + "404": { $ref: "#/components/responses/Error" } + delete: + operationId: deleteSchedule + description: Deletes a schedule; tasks it created stay. Records schedule.deleted. + responses: + "204": + description: Deleted. + "404": { $ref: "#/components/responses/Error" } + /api/projects/{projectId}/todo: parameters: - { name: projectId, in: path, required: true, schema: { type: string } } @@ -2124,6 +2184,46 @@ components: name: { type: string, minLength: 1 } command: { type: string, minLength: 1 } + ScheduleEvery: + type: string + enum: [hour, day, week, month] + + Weekday: + type: string + enum: [mon, tue, wed, thu, fri, sat, sun] + + Schedule: + type: object + required: [id, project_id, owner, title, every, weekdays, hour, minute, day_of_month, prompt, model, workflow, next_run] + properties: + id: { type: string } + project_id: { type: string } + owner: { type: string, description: "The login each run's task is created as." } + title: { type: string, description: "The title of each task it creates." } + every: { $ref: "#/components/schemas/ScheduleEvery" } + weekdays: { type: array, items: { $ref: "#/components/schemas/Weekday" }, description: "The days a weekly schedule runs on, Monday first; empty otherwise." } + hour: { type: integer, description: "0 to 23, in the server's local time; 0 for an hourly schedule." } + minute: { type: integer, description: "0 to 59; for an hourly schedule, the minute past each hour." } + day_of_month: { type: integer, description: "1 to 31 for a monthly schedule, which runs on the month's last day in shorter months; 0 otherwise." } + prompt: { type: string, description: "The task's first message, or with workflow, the run's brief." } + model: { type: string, description: "The task's agent's model; empty with workflow." } + workflow: { type: string, description: "The workflow each run starts, by name; empty for a single agent." } + next_run: { type: string, format: date-time } + + ScheduleSave: + type: object + required: [title, every, hour, minute] + properties: + title: { type: string, minLength: 1 } + every: { $ref: "#/components/schemas/ScheduleEvery" } + weekdays: { type: array, items: { $ref: "#/components/schemas/Weekday" }, description: "Required, and only allowed, for a weekly schedule." } + hour: { type: integer, minimum: 0, maximum: 23 } + minute: { type: integer, minimum: 0, maximum: 59 } + day_of_month: { type: integer, minimum: 1, maximum: 31, description: "Required, and only allowed, for a monthly schedule." } + prompt: { type: string, description: "Required without workflow." } + model: { type: string, description: "Required without workflow, refused with it." } + workflow: { type: string } + Permission: type: object required: [name, status, detail, settings] diff --git a/go.mod b/go.mod index f6472ed..eda809b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/getkin/kin-openapi v0.142.0 github.com/go-git/go-git/v5 v5.19.3 github.com/oapi-codegen/runtime v1.7.0 + howett.net/plist v1.0.0 modernc.org/sqlite v1.60.1 tailscale.com v1.104.2 ) @@ -78,7 +79,6 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gvisor.dev/gvisor v0.0.0-20260915211658-a6f909f08a72 // indirect - howett.net/plist v1.0.0 // indirect modernc.org/libc v1.77.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect diff --git a/internal/agents/coordinator.go b/internal/agents/coordinator.go index de5fc8c..641c347 100644 --- a/internal/agents/coordinator.go +++ b/internal/agents/coordinator.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "time" ) const abandonedMergeText = "The Merge turn ended unfinished (interrupted, failed to start, or with no final message), so byre didn't squash or push the task. Merge again to retry." @@ -17,21 +18,29 @@ func (m *Manager) poke() { func (m *Manager) coordinate() { started := map[string]bool{} + timer := time.NewTimer(time.Hour) + timer.Stop() for { select { case <-m.watchStop: return case <-m.pokes: - m.coordinatePass(context.Background(), started) + case <-timer.C: + } + next := m.coordinatePass(context.Background(), started) + timer.Stop() + if next != nil { + timer.Reset(time.Until(*next)) } } } -func (m *Manager) coordinatePass(ctx context.Context, started map[string]bool) { +func (m *Manager) coordinatePass(ctx context.Context, started map[string]bool) *time.Time { work, err := m.store.Coordinate(ctx) if err != nil { slog.Error("coordinate", "err", err) - return + retry := time.Now().Add(time.Minute) + return &retry } for _, id := range work.AbandonedMerges { slog.Info("merge turn ended unfinished", "task", id) @@ -84,9 +93,13 @@ func (m *Manager) coordinatePass(ctx context.Context, started map[string]bool) { for _, p := range work.ReadyPrompts { once("ready:"+p.TaskID, func() { m.promptReady(p) }) } + for _, sc := range work.DueSchedules { + once(fmt.Sprintf("schedule:%s:%d", sc.Id, sc.NextRun.UnixNano()), func() { m.runSchedule(sc) }) + } for key := range started { if !current[key] { delete(started, key) } } + return work.NextSchedule } diff --git a/internal/agents/lead.go b/internal/agents/lead.go index 3841044..2da0837 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -34,6 +34,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left - You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it. - Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine. - Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix". +- Schedules: when the human asks for work to run on a timer (every Monday, nightly), you manage it with list_schedules, save_schedule and delete_schedule; the human sees and edits the same schedules in the Project tab. On each run, byre creates a task from a schedule's prompt or workflow as create_task would, created by byre, and you get its created signal; a run that fails to start reaches you as a signal. - Whenever you name a task in a message, link it as a Markdown link to #/task/, for example [Fix login redirect](#/task/tskDa7e4K7305e). byre draws it in that task's color, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task//. When you point the human at a question to answer or a finished turn, link to #/task//transcript. - Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line
Title, the Markdown content, then a line
. - You are long-running. Your context window is large. byre sends you a signal once your context passes a quarter of its window; then call compact_self at a logical point (a piece of work wrapped up, a topic closed), and not before; when the turn ends, byre replaces you with a fresh session that starts from your handoff. diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index 2cda9f3..d65c35d 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -591,7 +591,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { m.mu.Unlock() return "byre will ask for your handoff once this turn ends.", nil }), - }, append(m.workflowTools(tool), m.noteTools(tool)...)...) + }, append(append(m.workflowTools(tool), m.scheduleTools(l, tool)...), m.noteTools(tool)...)...) } func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor string) error { diff --git a/internal/agents/schedule.go b/internal/agents/schedule.go new file mode 100644 index 0000000..302fe23 --- /dev/null +++ b/internal/agents/schedule.go @@ -0,0 +1,263 @@ +package agents + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + "time" + + "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/ids" + "g.fc.run/ai/byre/internal/mcp" + "g.fc.run/ai/byre/internal/schedule" + "g.fc.run/ai/byre/internal/store" +) + +func (m *Manager) SaveSchedule(ctx context.Context, projectID, scheduleID string, save api.ScheduleSave, login, actor string) (api.Schedule, error) { + sc, err := schedule.Timing(save) + if err != nil { + return api.Schedule{}, err + } + sc.Prompt = strings.TrimSpace(deref(save.Prompt)) + sc.Model = strings.TrimSpace(deref(save.Model)) + sc.Workflow = strings.TrimSpace(deref(save.Workflow)) + switch { + case sc.Workflow != "" && sc.Model != "": + return api.Schedule{}, errModelWithWorkflow + case sc.Workflow == "" && sc.Prompt == "": + return api.Schedule{}, fmt.Errorf("%w: prompt is required without a workflow", ErrInvalid) + case sc.Workflow == "" && sc.Model == "": + return api.Schedule{}, fmt.Errorf("%w: model is required without a workflow", ErrInvalid) + case sc.Workflow != "": + _, err = m.store.GetWorkflow(ctx, sc.Workflow) + if err != nil { + return api.Schedule{}, err + } + } + + if scheduleID != "" { + existing, err := m.store.GetSchedule(ctx, scheduleID) + if err != nil { + return api.Schedule{}, err + } + sc.Id = existing.Id + sc.ProjectId = existing.ProjectId + sc.Owner = existing.Owner + sc, err = m.store.UpdateSchedule(ctx, sc, actor) + if err != nil { + return api.Schedule{}, err + } + m.poke() + return sc, nil + } + + user, err := m.store.GetUser(ctx, login) + if err != nil { + return api.Schedule{}, err + } + if user.GitName == "" || user.GitEmail == "" { + return api.Schedule{}, ErrNoGitIdentity + } + project, err := m.store.GetProject(ctx, projectID) + if err != nil { + return api.Schedule{}, err + } + sc.Id = ids.New(ids.Schedule) + sc.ProjectId = project.Id + sc.Owner = user.Login + sc, err = m.store.CreateSchedule(ctx, sc, actor) + if err != nil { + return api.Schedule{}, err + } + m.poke() + return sc, nil +} + +func (m *Manager) DeleteSchedule(ctx context.Context, scheduleID, actor string) error { + err := m.store.DeleteSchedule(ctx, scheduleID, actor) + if err != nil { + return err + } + m.poke() + return nil +} + +func (m *Manager) runSchedule(sc api.Schedule) { + ctx := context.Background() + err := m.startScheduled(ctx, sc) + if err == nil { + slog.Info("scheduled task started", "schedule", sc.Id, "project", sc.ProjectId) + } else { + slog.Error("start scheduled task", "schedule", sc.Id, "project", sc.ProjectId, "err", err) + m.signalSchedule(ctx, sc, fmt.Sprintf("Schedule %s %q didn't start its run: %v.", sc.Id, sc.Title, err)) + } + for i := 0; ; i++ { + err = m.store.AdvanceSchedule(ctx, sc) + if err == nil { + break + } + slog.Error("advance schedule", "schedule", sc.Id, "err", err) + if i == 0 { + m.signalSchedule(ctx, sc, fmt.Sprintf("Schedule %s %q couldn't move on to its next run: %v. byre retries every minute; the schedule doesn't run again until it does.", sc.Id, sc.Title, err)) + } + time.Sleep(time.Minute) + } + m.poke() +} + +func (m *Manager) signalSchedule(ctx context.Context, sc api.Schedule, text string) { + err := m.addLeadInput(ctx, store.LeadInput{Owner: sc.Owner, Kind: store.LeadSignal, Actor: "byre", Text: text}) + if err != nil { + slog.Error("signal lead", "schedule", sc.Id, "err", err) + } +} + +func (m *Manager) startScheduled(ctx context.Context, sc api.Schedule) error { + project, err := m.store.GetProject(ctx, sc.ProjectId) + if err != nil { + return err + } + user, err := m.store.GetUser(ctx, sc.Owner) + if err != nil { + return err + } + if user.GitName == "" || user.GitEmail == "" { + return ErrNoGitIdentity + } + if sc.Workflow != "" { + _, err = m.StartWorkflow(ctx, project, user, sc.Title, project.DefaultBranch, "byre", sc.Workflow, sc.Prompt, nil, "") + return err + } + task, err := m.CreateTask(ctx, project, sc.Title, user.Login, project.DefaultBranch, "byre") + if err != nil { + return err + } + _, err = m.create(ctx, task, user, "", sc.Prompt, sc.Model, "byre", "", "", nil) + if err != nil { + return fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err) + } + return nil +} + +func scheduleText(sc api.Schedule) string { + b := &strings.Builder{} + fmt.Fprintf(b, "- %s %q: project %s, owner %s, every %s", sc.Id, sc.Title, sc.ProjectId, sc.Owner, sc.Every) + switch sc.Every { + case api.Hour: + fmt.Fprintf(b, " at minute %d", sc.Minute) + case api.Week: + days := []string{} + for _, d := range sc.Weekdays { + days = append(days, string(d)) + } + fmt.Fprintf(b, " on %s at %02d:%02d", strings.Join(days, ", "), sc.Hour, sc.Minute) + case api.Month: + fmt.Fprintf(b, " on day %d at %02d:%02d", sc.DayOfMonth, sc.Hour, sc.Minute) + default: + fmt.Fprintf(b, " at %02d:%02d", sc.Hour, sc.Minute) + } + fmt.Fprintf(b, ", next run %s", sc.NextRun.Local().Format(time.RFC3339)) + if sc.Workflow != "" { + fmt.Fprintf(b, ", workflow %q", sc.Workflow) + } else { + fmt.Fprintf(b, ", model %s", sc.Model) + } + if sc.Prompt != "" { + fmt.Fprintf(b, "\n prompt: %s", clip(sc.Prompt, 500)) + } + return b.String() +} + +func (m *Manager) scheduleTools(l *live, tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool { + return []mcp.Tool{ + tool("list_schedules", + "List the schedules: each one's id, project, owner, title, timing, model or workflow, prompt, and next run in the server's local time. Give project_id for one project's.", + `{"type": "object", "properties": {"project_id": {"type": "string"}}}`, + func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { + args := struct { + ProjectID string `json:"project_id"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", err + } + list, err := m.store.ListSchedules(ctx, args.ProjectID) + if err != nil { + return "", err + } + if len(list) == 0 { + return "No schedules.", nil + } + lines := []string{} + for _, sc := range list { + lines = append(lines, scheduleText(sc)) + } + return strings.Join(lines, "\n"), nil + }), + + tool("save_schedule", + "Create a schedule, or with schedule_id replace one's settings (project_id is then ignored). On each run byre creates a task in the project from its default branch, as create_task does, "+ + "as the schedule's owner (you, for one you create), and you get its created signal. Times are the server's local time. "+ + "every hour runs at minute past each hour (hour 0); day at hour:minute; week at hour:minute on each of weekdays; month at hour:minute on day_of_month, or the month's last day when shorter. "+ + "weekdays only for week, day_of_month only for month. Each run's agent sees nothing else, so the prompt must be a complete brief. "+ + "With workflow, each run starts that workflow, prompt is optional and is its brief, and model can't be given; the workflow is looked up by name at each run. "+ + "A run missed while byre was down happens once when it starts. The human sees and edits schedules in the Project tab. Only when the human asks.", + `{"type": "object", "properties": { + "project_id": {"type": "string"}, + "schedule_id": {"type": "string"}, + "title": {"type": "string", "description": "The title of each task it creates."}, + "every": {"type": "string", "enum": ["hour", "day", "week", "month"]}, + "weekdays": {"type": "array", "items": {"type": "string", "enum": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]}}, + "hour": {"type": "integer", "minimum": 0, "maximum": 23}, + "minute": {"type": "integer", "minimum": 0, "maximum": 59}, + "day_of_month": {"type": "integer", "minimum": 1, "maximum": 31}, + "prompt": {"type": "string"}, + "model": {"type": "string", "description": "Without workflow, the agent's model; defaults to yours."}, + "workflow": {"type": "string", "description": "The name of a workflow for each run to freeze a copy of and run."} + }, "required": ["title", "every", "hour", "minute"]}`, + func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { + args := struct { + ProjectID string `json:"project_id"` + ScheduleID string `json:"schedule_id"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", err + } + save := api.ScheduleSave{} + err = json.Unmarshal(raw, &save) + if err != nil { + return "", err + } + if deref(save.Workflow) == "" { + model := m.modelOr(deref(save.Model), l) + save.Model = &model + } + sc, err := m.SaveSchedule(ctx, args.ProjectID, args.ScheduleID, save, user.Login, leadActor(user.Login)) + if err != nil { + return "", err + } + return "Saved:\n" + scheduleText(sc), nil + }), + + tool("delete_schedule", + "Delete a schedule. Tasks it already created stay. Only when the human asks.", + `{"type": "object", "properties": {"schedule_id": {"type": "string"}}, "required": ["schedule_id"]}`, + func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { + args := struct { + ScheduleID string `json:"schedule_id"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", err + } + err = m.DeleteSchedule(ctx, args.ScheduleID, leadActor(user.Login)) + if err != nil { + return "", err + } + return fmt.Sprintf("Deleted schedule %s.", args.ScheduleID), nil + }), + } +} diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 916f834..fde7513 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -345,6 +345,30 @@ func (e ReviewState) Valid() bool { } } +// Defines values for ScheduleEvery. +const ( + Day ScheduleEvery = "day" + Hour ScheduleEvery = "hour" + Month ScheduleEvery = "month" + Week ScheduleEvery = "week" +) + +// Valid indicates whether the value is a known member of the ScheduleEvery enum. +func (e ScheduleEvery) Valid() bool { + switch e { + case Day: + return true + case Hour: + return true + case Month: + return true + case Week: + return true + default: + return false + } +} + // Defines values for SnapshotTrigger. const ( SnapshotTriggerAnswer SnapshotTrigger = "answer" @@ -540,6 +564,39 @@ func (e TaskTab) Valid() bool { } } +// Defines values for Weekday. +const ( + Fri Weekday = "fri" + Mon Weekday = "mon" + Sat Weekday = "sat" + Sun Weekday = "sun" + Thu Weekday = "thu" + Tue Weekday = "tue" + Wed Weekday = "wed" +) + +// Valid indicates whether the value is a known member of the Weekday enum. +func (e Weekday) Valid() bool { + switch e { + case Fri: + return true + case Mon: + return true + case Sat: + return true + case Sun: + return true + case Thu: + return true + case Tue: + return true + case Wed: + return true + default: + return false + } +} + // Defines values for WorkflowRunStageState. const ( StageStateCleanedUp WorkflowRunStageState = "cleaned_up" @@ -1263,6 +1320,63 @@ type SavedCommandSave struct { Name string `json:"name"` } +// Schedule defines model for Schedule. +type Schedule struct { + // DayOfMonth 1 to 31 for a monthly schedule, which runs on the month's last day in shorter months; 0 otherwise. + DayOfMonth int `json:"day_of_month"` + Every ScheduleEvery `json:"every"` + + // Hour 0 to 23, in the server's local time; 0 for an hourly schedule. + Hour int `json:"hour"` + Id string `json:"id"` + + // Minute 0 to 59; for an hourly schedule, the minute past each hour. + Minute int `json:"minute"` + + // Model The task's agent's model; empty with workflow. + Model string `json:"model"` + NextRun time.Time `json:"next_run"` + + // Owner The login each run's task is created as. + Owner string `json:"owner"` + ProjectId string `json:"project_id"` + + // Prompt The task's first message, or with workflow, the run's brief. + Prompt string `json:"prompt"` + + // Title The title of each task it creates. + Title string `json:"title"` + + // Weekdays The days a weekly schedule runs on, Monday first; empty otherwise. + Weekdays []Weekday `json:"weekdays"` + + // Workflow The workflow each run starts, by name; empty for a single agent. + Workflow string `json:"workflow"` +} + +// ScheduleEvery defines model for ScheduleEvery. +type ScheduleEvery string + +// ScheduleSave defines model for ScheduleSave. +type ScheduleSave struct { + // DayOfMonth Required, and only allowed, for a monthly schedule. + DayOfMonth *int `json:"day_of_month,omitempty"` + Every ScheduleEvery `json:"every"` + Hour int `json:"hour"` + Minute int `json:"minute"` + + // Model Required without workflow, refused with it. + Model *string `json:"model,omitempty"` + + // Prompt Required without workflow. + Prompt *string `json:"prompt,omitempty"` + Title string `json:"title"` + + // Weekdays Required, and only allowed, for a weekly schedule. + Weekdays *[]Weekday `json:"weekdays,omitempty"` + Workflow *string `json:"workflow,omitempty"` +} + // SessionCreate defines model for SessionCreate. type SessionCreate struct { // Images Blob ids from uploadBlob, sent to the agent as images alongside the prompt. @@ -1650,6 +1764,9 @@ type UserUpdate struct { GitName string `json:"git_name"` } +// Weekday defines model for Weekday. +type Weekday string + // WorkflowDefinition A workflow's definition, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews. type WorkflowDefinition struct { Description string `json:"description,omitempty"` @@ -1874,6 +1991,9 @@ type SetAuditWorkflowJSONRequestBody = ProjectWorkflow // SetReviewWorkflowJSONRequestBody defines body for SetReviewWorkflow for application/json ContentType. type SetReviewWorkflowJSONRequestBody = ProjectWorkflow +// CreateScheduleJSONRequestBody defines body for CreateSchedule for application/json ContentType. +type CreateScheduleJSONRequestBody = ScheduleSave + // CreateTaskJSONRequestBody defines body for CreateTask for application/json ContentType. type CreateTaskJSONRequestBody = TaskCreate @@ -1883,6 +2003,9 @@ type StartTodoItemJSONRequestBody = TodoStart // SetFindingStateJSONRequestBody defines body for SetFindingState for application/json ContentType. type SetFindingStateJSONRequestBody = FindingStateUpdate +// UpdateScheduleJSONRequestBody defines body for UpdateSchedule for application/json ContentType. +type UpdateScheduleJSONRequestBody = ScheduleSave + // CompactSessionJSONRequestBody defines body for CompactSession for application/json ContentType. type CompactSessionJSONRequestBody = CompactCreate @@ -2030,6 +2153,12 @@ type ServerInterface interface { // (PUT /api/projects/{projectId}/review-workflow) SetReviewWorkflow(w http.ResponseWriter, r *http.Request, projectId string) + // (GET /api/projects/{projectId}/schedules) + ListSchedules(w http.ResponseWriter, r *http.Request, projectId string) + + // (POST /api/projects/{projectId}/schedules) + CreateSchedule(w http.ResponseWriter, r *http.Request, projectId string) + // (GET /api/projects/{projectId}/tasks) ListTasks(w http.ResponseWriter, r *http.Request, projectId string) @@ -2045,6 +2174,12 @@ type ServerInterface interface { // (PUT /api/reviews/{reviewId}/findings/{index}) SetFindingState(w http.ResponseWriter, r *http.Request, reviewId string, index int) + // (DELETE /api/schedules/{scheduleId}) + DeleteSchedule(w http.ResponseWriter, r *http.Request, scheduleId string) + + // (PUT /api/schedules/{scheduleId}) + UpdateSchedule(w http.ResponseWriter, r *http.Request, scheduleId string) + // (GET /api/sessions/{sessionId}) GetSession(w http.ResponseWriter, r *http.Request, sessionId string) @@ -2998,6 +3133,58 @@ func (siw *ServerInterfaceWrapper) SetReviewWorkflow(w http.ResponseWriter, r *h handler.ServeHTTP(w, r) } +// ListSchedules operation middleware +func (siw *ServerInterfaceWrapper) ListSchedules(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "projectId" ------------- + var projectId string + + err = runtime.BindStyledParameterWithOptions("simple", "projectId", r.PathValue("projectId"), &projectId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListSchedules(w, r, projectId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateSchedule operation middleware +func (siw *ServerInterfaceWrapper) CreateSchedule(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "projectId" ------------- + var projectId string + + err = runtime.BindStyledParameterWithOptions("simple", "projectId", r.PathValue("projectId"), &projectId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateSchedule(w, r, projectId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListTasks operation middleware func (siw *ServerInterfaceWrapper) ListTasks(w http.ResponseWriter, r *http.Request) { @@ -3137,6 +3324,58 @@ func (siw *ServerInterfaceWrapper) SetFindingState(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } +// DeleteSchedule operation middleware +func (siw *ServerInterfaceWrapper) DeleteSchedule(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "scheduleId" ------------- + var scheduleId string + + err = runtime.BindStyledParameterWithOptions("simple", "scheduleId", r.PathValue("scheduleId"), &scheduleId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "scheduleId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteSchedule(w, r, scheduleId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UpdateSchedule operation middleware +func (siw *ServerInterfaceWrapper) UpdateSchedule(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "scheduleId" ------------- + var scheduleId string + + err = runtime.BindStyledParameterWithOptions("simple", "scheduleId", r.PathValue("scheduleId"), &scheduleId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "scheduleId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateSchedule(w, r, scheduleId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetSession operation middleware func (siw *ServerInterfaceWrapper) GetSession(w http.ResponseWriter, r *http.Request) { @@ -4437,6 +4676,10 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/projects/{projectId}/audit-workflow", wrapper.SetAuditWorkflow) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.ListTasks) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.CreateTask) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/schedules", wrapper.ListSchedules) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/schedules", wrapper.CreateSchedule) + m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/schedules/{scheduleId}", wrapper.DeleteSchedule) + m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/schedules/{scheduleId}", wrapper.UpdateSchedule) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/todo", wrapper.GetTodo) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/todo/tasks", wrapper.StartTodoItem) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}", wrapper.GetTask) @@ -4502,335 +4745,346 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "uAkTUcDQwhEz+dUoqT98+F8DAA==", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/ids/ids.go b/internal/ids/ids.go index 6266737..1a7628c 100644 --- a/internal/ids/ids.go +++ b/internal/ids/ids.go @@ -32,6 +32,7 @@ const ( Transfer Prefix = "xfr" Terminal Prefix = "trm" SavedCommand Prefix = "cmd" + Schedule Prefix = "sch" ) func New(prefix Prefix) string { diff --git a/internal/schedule/schedule.go b/internal/schedule/schedule.go new file mode 100644 index 0000000..e439ef5 --- /dev/null +++ b/internal/schedule/schedule.go @@ -0,0 +1,124 @@ +package schedule + +import ( + "errors" + "fmt" + "slices" + "strings" + "time" + + "g.fc.run/ai/byre/internal/api" +) + +var ErrInvalid = errors.New("invalid schedule") + +var Weekdays = []api.Weekday{api.Mon, api.Tue, api.Wed, api.Thu, api.Fri, api.Sat, api.Sun} + +var goWeekdays = map[api.Weekday]time.Weekday{ + api.Mon: time.Monday, + api.Tue: time.Tuesday, + api.Wed: time.Wednesday, + api.Thu: time.Thursday, + api.Fri: time.Friday, + api.Sat: time.Saturday, + api.Sun: time.Sunday, +} + +func invalid(format string, args ...any) error { + return fmt.Errorf("%w: %s", ErrInvalid, fmt.Sprintf(format, args...)) +} + +func Timing(save api.ScheduleSave) (api.Schedule, error) { + sc := api.Schedule{ + Title: strings.TrimSpace(save.Title), + Every: save.Every, + Weekdays: []api.Weekday{}, + Hour: save.Hour, + Minute: save.Minute, + } + if sc.Title == "" { + return sc, invalid("title is required") + } + if save.Minute < 0 || save.Minute > 59 { + return sc, invalid("minute must be 0 to 59") + } + if save.Hour < 0 || save.Hour > 23 { + return sc, invalid("hour must be 0 to 23") + } + switch save.Every { + case api.Hour, api.Day, api.Week, api.Month: + default: + return sc, invalid("every must be hour, day, week or month") + } + if save.Every == api.Hour && save.Hour != 0 { + return sc, invalid("an hourly schedule runs at a minute past every hour, so hour must be 0") + } + if (save.Weekdays != nil && len(*save.Weekdays) > 0) != (save.Every == api.Week) { + return sc, invalid("weekdays are required for a weekly schedule, and only allowed for one") + } + if save.Weekdays != nil { + for _, d := range Weekdays { + if slices.Contains(*save.Weekdays, d) { + sc.Weekdays = append(sc.Weekdays, d) + } + } + for _, d := range *save.Weekdays { + if _, found := goWeekdays[d]; !found { + return sc, invalid("unknown weekday %q", d) + } + } + } + if (save.DayOfMonth != nil) != (save.Every == api.Month) { + return sc, invalid("day_of_month is required for a monthly schedule, and only allowed for one") + } + if save.DayOfMonth != nil { + if *save.DayOfMonth < 1 || *save.DayOfMonth > 31 { + return sc, invalid("day_of_month must be 1 to 31") + } + sc.DayOfMonth = *save.DayOfMonth + } + return sc, nil +} + +func Next(sc api.Schedule, after time.Time) time.Time { + t := after.Local() + y, m, d := t.Date() + at := func(y int, m time.Month, d, h int) time.Time { + return time.Date(y, m, d, h, sc.Minute, 0, 0, time.Local) + } + switch sc.Every { + case api.Hour: + c := at(y, m, d, t.Hour()) + for !c.After(t) { + c = c.Add(time.Hour) + } + return c + case api.Day: + c := at(y, m, d, sc.Hour) + if !c.After(t) { + c = at(y, m, d+1, sc.Hour) + } + return c + case api.Week: + days := map[time.Weekday]bool{} + for _, w := range sc.Weekdays { + days[goWeekdays[w]] = true + } + for i := 0; i <= 7; i++ { + c := at(y, m, d+i, sc.Hour) + if days[c.Weekday()] && c.After(t) { + return c + } + } + case api.Month: + for i := 0; i <= 1; i++ { + first := time.Date(y, m+time.Month(i), 1, 0, 0, 0, 0, time.Local) + last := first.AddDate(0, 1, -1).Day() + c := at(first.Year(), first.Month(), min(sc.DayOfMonth, last), sc.Hour) + if c.After(t) { + return c + } + } + } + panic(fmt.Sprintf("schedule %s has no next run after %s", sc.Id, after)) +} diff --git a/internal/server/schedules.go b/internal/server/schedules.go new file mode 100644 index 0000000..90de1c5 --- /dev/null +++ b/internal/server/schedules.go @@ -0,0 +1,46 @@ +package server + +import ( + "net/http" + + "g.fc.run/ai/byre/internal/api" +) + +func (s *Server) ListSchedules(w http.ResponseWriter, r *http.Request, projectID string) { + list, err := s.store.ListSchedules(r.Context(), projectID) + if err != nil { + writeStoreError(w, err) + return + } + writeJSON(w, http.StatusOK, list) +} + +func (s *Server) CreateSchedule(w http.ResponseWriter, r *http.Request, projectID string) { + s.saveSchedule(w, r, projectID, "", http.StatusCreated) +} + +func (s *Server) UpdateSchedule(w http.ResponseWriter, r *http.Request, scheduleID string) { + s.saveSchedule(w, r, "", scheduleID, http.StatusOK) +} + +func (s *Server) saveSchedule(w http.ResponseWriter, r *http.Request, projectID, scheduleID string, status int) { + req := api.ScheduleSave{} + if !readJSON(w, r, &req) { + return + } + sc, err := s.agents.SaveSchedule(r.Context(), projectID, scheduleID, req, actor(r), actor(r)) + if err != nil { + writeAgentError(w, err) + return + } + writeJSON(w, status, sc) +} + +func (s *Server) DeleteSchedule(w http.ResponseWriter, r *http.Request, scheduleID string) { + err := s.agents.DeleteSchedule(r.Context(), scheduleID, actor(r)) + if err != nil { + writeAgentError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/server/server.go b/internal/server/server.go index b12e5af..5aa78fe 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -19,6 +19,7 @@ import ( "g.fc.run/ai/byre/internal/clients" "g.fc.run/ai/byre/internal/git" "g.fc.run/ai/byre/internal/ids" + "g.fc.run/ai/byre/internal/schedule" "g.fc.run/ai/byre/internal/store" "g.fc.run/ai/byre/internal/terminal" "g.fc.run/ai/byre/internal/todo" @@ -980,7 +981,7 @@ func writeAgentError(w http.ResponseWriter, err error) { writeError(w, http.StatusNotFound, err) case errors.Is(err, agents.ErrConflict): writeError(w, http.StatusConflict, err) - case errors.Is(err, agents.ErrInvalid), errors.Is(err, agents.ErrNoGitIdentity), errors.Is(err, workflow.ErrInvalid): + case errors.Is(err, agents.ErrInvalid), errors.Is(err, agents.ErrNoGitIdentity), errors.Is(err, workflow.ErrInvalid), errors.Is(err, schedule.ErrInvalid): writeError(w, http.StatusBadRequest, err) default: writeError(w, http.StatusInternalServerError, err) diff --git a/internal/store/coordinate.go b/internal/store/coordinate.go index 3a83718..37c4616 100644 --- a/internal/store/coordinate.go +++ b/internal/store/coordinate.go @@ -3,6 +3,9 @@ package store import ( "context" "database/sql" + "time" + + "g.fc.run/ai/byre/internal/api" ) type Work struct { @@ -19,6 +22,8 @@ type Work struct { AdvanceStages []string LeadDeliveries []LeadDelivery QueuedDelivery []QueuedDelivery + DueSchedules []api.Schedule + NextSchedule *time.Time } func (s *Store) Coordinate(ctx context.Context) (Work, error) { @@ -88,6 +93,10 @@ func (s *Store) Coordinate(ctx context.Context) (Work, error) { return err } work.QueuedDelivery, err = queuedDeliveries(ctx, tx) + if err != nil { + return err + } + work.DueSchedules, work.NextSchedule, err = dueSchedules(ctx, tx) return err }) return work, err diff --git a/internal/store/migrations/096-add-schedules.sql b/internal/store/migrations/096-add-schedules.sql new file mode 100644 index 0000000..ebda4ec --- /dev/null +++ b/internal/store/migrations/096-add-schedules.sql @@ -0,0 +1,23 @@ +CREATE TABLE schedules ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + owner TEXT NOT NULL REFERENCES users(login), + title TEXT NOT NULL, + every TEXT NOT NULL CHECK (every IN ('hour', 'day', 'week', 'month')), + hour INTEGER NOT NULL CHECK (hour BETWEEN 0 AND 23), + minute INTEGER NOT NULL CHECK (minute BETWEEN 0 AND 59), + day_of_month INTEGER NOT NULL CHECK (day_of_month BETWEEN 0 AND 31), + prompt TEXT NOT NULL, + model TEXT NOT NULL, + workflow TEXT NOT NULL, + next_run_at TEXT NOT NULL, + created_at TEXT NOT NULL +); + +CREATE INDEX schedules_by_next_run ON schedules (next_run_at); + +CREATE TABLE schedule_weekdays ( + schedule_id TEXT NOT NULL REFERENCES schedules(id), + weekday TEXT NOT NULL CHECK (weekday IN ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')), + PRIMARY KEY (schedule_id, weekday) +); diff --git a/internal/store/schedule.go b/internal/store/schedule.go new file mode 100644 index 0000000..c3b53a3 --- /dev/null +++ b/internal/store/schedule.go @@ -0,0 +1,183 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "fmt" + "slices" + "time" + + "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/schedule" +) + +const scheduleColumns = `id, project_id, owner, title, every, hour, minute, day_of_month, prompt, model, workflow, next_run_at` + +func querySchedules(ctx context.Context, q querier, where string, args ...any) ([]api.Schedule, error) { + rows, err := q.QueryContext(ctx, `SELECT `+scheduleColumns+` FROM schedules `+where+` ORDER BY next_run_at, id`, args...) + if err != nil { + return nil, err + } + out := []api.Schedule{} + for rows.Next() { + sc := api.Schedule{Weekdays: []api.Weekday{}} + next := "" + err = rows.Scan(&sc.Id, &sc.ProjectId, &sc.Owner, &sc.Title, &sc.Every, &sc.Hour, &sc.Minute, &sc.DayOfMonth, &sc.Prompt, &sc.Model, &sc.Workflow, &next) + if err != nil { + rows.Close() + return nil, err + } + sc.NextRun, err = parseTime(next) + if err != nil { + rows.Close() + return nil, err + } + out = append(out, sc) + } + err = errors.Join(rows.Err(), rows.Close()) + if err != nil { + return nil, err + } + for i := range out { + days, err := queryIDs(ctx, q, `SELECT weekday FROM schedule_weekdays WHERE schedule_id = ?`, out[i].Id) + if err != nil { + return nil, err + } + for _, d := range schedule.Weekdays { + if slices.Contains(days, string(d)) { + out[i].Weekdays = append(out[i].Weekdays, d) + } + } + } + return out, nil +} + +func (s *Store) ListSchedules(ctx context.Context, projectID string) ([]api.Schedule, error) { + if projectID == "" { + return querySchedules(ctx, s.db, ``) + } + _, err := s.GetProject(ctx, projectID) + if err != nil { + return nil, err + } + return querySchedules(ctx, s.db, `WHERE project_id = ?`, projectID) +} + +func (s *Store) GetSchedule(ctx context.Context, id string) (api.Schedule, error) { + list, err := querySchedules(ctx, s.db, `WHERE id = ?`, id) + if err != nil { + return api.Schedule{}, err + } + if len(list) == 0 { + return api.Schedule{}, fmt.Errorf("schedule %s: %w", id, ErrNotFound) + } + return list[0], nil +} + +func writeWeekdays(ctx context.Context, tx *sql.Tx, sc api.Schedule) error { + _, err := tx.ExecContext(ctx, `DELETE FROM schedule_weekdays WHERE schedule_id = ?`, sc.Id) + if err != nil { + return err + } + for _, d := range sc.Weekdays { + _, err = tx.ExecContext(ctx, `INSERT INTO schedule_weekdays (schedule_id, weekday) VALUES (?, ?)`, sc.Id, string(d)) + if err != nil { + return err + } + } + return nil +} + +func (s *Store) CreateSchedule(ctx context.Context, sc api.Schedule, actor string) (api.Schedule, error) { + sc.NextRun = schedule.Next(sc, time.Now()) + err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + _, err := tx.ExecContext(ctx, `INSERT INTO schedules (`+scheduleColumns+`, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + sc.Id, sc.ProjectId, sc.Owner, sc.Title, string(sc.Every), sc.Hour, sc.Minute, sc.DayOfMonth, sc.Prompt, sc.Model, sc.Workflow, formatTime(sc.NextRun), now()) + if err != nil { + return err + } + err = writeWeekdays(ctx, tx, sc) + if err != nil { + return err + } + return emit(api.Event{Kind: "schedule.saved", ProjectId: &sc.ProjectId, Actor: actor, Payload: ToPayload(sc)}) + }) + return sc, err +} + +func (s *Store) UpdateSchedule(ctx context.Context, sc api.Schedule, actor string) (api.Schedule, error) { + sc.NextRun = schedule.Next(sc, time.Now()) + err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + _, err := tx.ExecContext(ctx, `UPDATE schedules SET title = ?, every = ?, hour = ?, minute = ?, day_of_month = ?, prompt = ?, model = ?, workflow = ?, next_run_at = ? WHERE id = ?`, + sc.Title, string(sc.Every), sc.Hour, sc.Minute, sc.DayOfMonth, sc.Prompt, sc.Model, sc.Workflow, formatTime(sc.NextRun), sc.Id) + if err != nil { + return err + } + err = writeWeekdays(ctx, tx, sc) + if err != nil { + return err + } + return emit(api.Event{Kind: "schedule.saved", ProjectId: &sc.ProjectId, Actor: actor, Payload: ToPayload(sc)}) + }) + return sc, err +} + +func (s *Store) DeleteSchedule(ctx context.Context, id, actor string) error { + return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + projectID := "" + err := tx.QueryRowContext(ctx, `SELECT project_id FROM schedules WHERE id = ?`, id).Scan(&projectID) + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("schedule %s: %w", id, ErrNotFound) + } + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, `DELETE FROM schedule_weekdays WHERE schedule_id = ?`, id) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, `DELETE FROM schedules WHERE id = ?`, id) + if err != nil { + return err + } + return emit(api.Event{Kind: "schedule.deleted", ProjectId: &projectID, Actor: actor, Payload: map[string]any{"id": id}}) + }) +} + +func (s *Store) AdvanceSchedule(ctx context.Context, sc api.Schedule) error { + return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + list, err := querySchedules(ctx, tx, `WHERE id = ? AND next_run_at = ?`, sc.Id, formatTime(sc.NextRun)) + if err != nil || len(list) == 0 { + return err + } + next := list[0] + next.NextRun = schedule.Next(next, time.Now()) + _, err = tx.ExecContext(ctx, `UPDATE schedules SET next_run_at = ? WHERE id = ?`, formatTime(next.NextRun), sc.Id) + if err != nil { + return err + } + return emit(api.Event{Kind: "schedule.saved", ProjectId: &next.ProjectId, Actor: "byre", Payload: ToPayload(next)}) + }) +} + +func dueSchedules(ctx context.Context, tx *sql.Tx) ([]api.Schedule, *time.Time, error) { + at := formatTime(time.Now()) + due, err := querySchedules(ctx, tx, `WHERE next_run_at <= ?`, at) + if err != nil { + return nil, nil, err + } + soonest := "" + err = tx.QueryRowContext(ctx, `SELECT next_run_at FROM schedules WHERE next_run_at > ? ORDER BY next_run_at LIMIT 1`, at).Scan(&soonest) + if errors.Is(err, sql.ErrNoRows) { + return due, nil, nil + } + if err != nil { + return nil, nil, err + } + t, err := parseTime(soonest) + if err != nil { + return nil, nil, err + } + return due, &t, nil +} diff --git a/internal/store/workflow.go b/internal/store/workflow.go index 465eaf7..8bf0dca 100644 --- a/internal/store/workflow.go +++ b/internal/store/workflow.go @@ -73,6 +73,20 @@ func (s *Store) PutWorkflow(ctx context.Context, name, from string, def api.Work if err != nil { return err } + _, err = tx.ExecContext(ctx, `UPDATE schedules SET workflow = ? WHERE workflow = ?`, name, from) + if err != nil { + return err + } + repointed, err := querySchedules(ctx, tx, `WHERE workflow = ?`, name) + if err != nil { + return err + } + for _, sc := range repointed { + err = emit(api.Event{Kind: "schedule.saved", ProjectId: &sc.ProjectId, Actor: actor, Payload: ToPayload(sc)}) + if err != nil { + return err + } + } err = emit(api.Event{Kind: "workflow.deleted", Actor: actor, Payload: map[string]any{"name": from}}) if err != nil { return err diff --git a/mac/Sources/Byre/Dialogs.swift b/mac/Sources/Byre/Dialogs.swift index c2c9a21..63408b8 100644 --- a/mac/Sources/Byre/Dialogs.swift +++ b/mac/Sources/Byre/Dialogs.swift @@ -185,7 +185,8 @@ final class Overlays { return false } if let modal { - if enter && !marked { + let multiline = (NSApp.keyWindow?.firstResponder as? NSTextView).map { !$0.isFieldEditor } ?? false + if enter && !marked && !multiline { if modal.canSubmit(), let last = modal.buttons().last { answerModal(last.value) } diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index c487681..a2b56a8 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -171,6 +171,7 @@ private struct ProjectBlock: View { let project: Project let todo: Result? @Environment(Store.self) private var store + @Environment(Overlays.self) private var overlays var body: some View { let active = (store.tasks[project.id] ?? []).filter { $0.status == .active } @@ -203,6 +204,18 @@ private struct ProjectBlock: View { TaskRows(active: active) heading("TODO") TodoBlock(project: project, todo: todo, active: active) + HStack(alignment: .firstTextBaseline) { + heading("Schedules") + Spacer() + Button("New") { + Task { + await editSchedule(project, nil, store, overlays) + } + } + .buttonStyle(WebButtonStyle()) + .help("Create a task in this project on a timer") + } + ScheduleRows(project: project) } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/mac/Sources/Byre/Schedules.swift b/mac/Sources/Byre/Schedules.swift new file mode 100644 index 0000000..9c66298 --- /dev/null +++ b/mac/Sources/Byre/Schedules.swift @@ -0,0 +1,259 @@ +import ByreAPI +import SwiftUI + +typealias Schedule = Components.Schemas.Schedule +typealias ScheduleSave = Components.Schemas.ScheduleSave +typealias ScheduleEvery = Components.Schemas.ScheduleEvery +typealias Weekday = Components.Schemas.Weekday + +private let weekdayNames: [(Weekday, String, String)] = [ + (.mon, "M", "Mon"), (.tue, "T", "Tue"), (.wed, "W", "Wed"), (.thu, "T", "Thu"), (.fri, "F", "Fri"), (.sat, "S", "Sat"), (.sun, "S", "Sun"), +] + +private func twoDigits(_ n: Int) -> String { + n < 10 ? "0\(n)" : "\(n)" +} + +private func clock(_ hour: Int, _ minute: Int) -> String { + "\(hour):\(twoDigits(minute))" +} + +private func ordinal(_ n: Int) -> String { + let suffix = (11...13).contains(n % 100) ? "th" : [1: "st", 2: "nd", 3: "rd"][n % 10] ?? "th" + return "\(n)\(suffix)" +} + +func scheduleTiming(_ sc: Schedule) -> String { + let at = clock(sc.hour, sc.minute) + switch sc.every { + case .hour: + return "Every hour at :\(twoDigits(sc.minute))" + case .day: + return "Daily at \(at)" + case .week: + let days = Set(sc.weekdays) + if days.count == 7 { + return "Daily at \(at)" + } + if days == [.mon, .tue, .wed, .thu, .fri] { + return "Weekdays at \(at)" + } + if days == [.sat, .sun] { + return "Weekends at \(at)" + } + let names = weekdayNames.filter { days.contains($0.0) }.map(\.2) + return names.count == 1 ? "\(names[0])days at \(at)" : "\(names.joined(separator: ", ")) at \(at)" + case .month: + return "Monthly on the \(ordinal(sc.dayOfMonth)) at \(at)" + } +} + +private let nextRunFormat: DateFormatter = { + let f = DateFormatter() + f.setLocalizedDateFormatFromTemplate("EEE MMM d jmm") + return f +}() + +struct ScheduleRows: View { + let project: Project + @Environment(Store.self) private var store + @Environment(Overlays.self) private var overlays + + var body: some View { + let list = store.schedules[project.id] ?? [] + if list.isEmpty { + Text("No schedules.") + .foregroundStyle(Palette.muted) + .padding(.horizontal, 10) + .padding(.vertical, 6) + } + VStack(alignment: .leading, spacing: 0) { + ForEach(list, id: \.id) { sc in + HoverRow { + Task { + await editSchedule(project, sc, store, overlays) + } + } content: { + Text(sc.title) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + Text(scheduleTiming(sc)) + .font(Fonts.font(11)) + .foregroundStyle(Palette.muted) + Text("next \(nextRunFormat.string(from: sc.nextRun))") + .font(Fonts.font(11)) + .foregroundStyle(Palette.muted) + } + .help(sc.workflow.isEmpty ? "Runs one agent on \(sc.model)" : "Runs workflow \(sc.workflow)") + } + } + .padding(.bottom, 8) + } +} + +@MainActor +@Observable +private final class ScheduleDraft { + var title: String + var every: ScheduleEvery + var weekdays: Set + var hour: Int + var minute: Int + var dayOfMonth: Int + var workflow: String + var model: String + var prompt: String + var workflows: [String] = [] + var failure = "" + + init(_ sc: Schedule?, defaultModel: String) { + title = sc?.title ?? "" + every = sc?.every ?? .week + weekdays = Set(sc?.weekdays ?? [.mon]) + hour = sc?.hour ?? 9 + minute = sc?.minute ?? 0 + dayOfMonth = sc.map { $0.dayOfMonth == 0 ? 1 : $0.dayOfMonth } ?? 1 + workflow = sc?.workflow ?? "" + model = sc.map { $0.model.isEmpty ? defaultModel : $0.model } ?? defaultModel + prompt = sc?.prompt ?? "" + } + + var valid: Bool { + !trimmed(title).isEmpty && (every != .week || !weekdays.isEmpty) && (!workflow.isEmpty || !trimmed(prompt).isEmpty) + } + + var save: ScheduleSave { + ScheduleSave( + title: title, + every: every, + weekdays: every == .week ? weekdayNames.map(\.0).filter { weekdays.contains($0) } : nil, + hour: every == .hour ? 0 : hour, + minute: minute, + dayOfMonth: every == .month ? dayOfMonth : nil, + prompt: prompt, + model: workflow.isEmpty ? model : nil, + workflow: workflow.isEmpty ? nil : workflow + ) + } +} + +@MainActor +func editSchedule(_ project: Project, _ existing: Schedule?, _ store: Store, _ overlays: Overlays) async { + let draft = ScheduleDraft(existing, defaultModel: store.defaultModel) + Task { + draft.workflows = (try? await store.api.client.listWorkflows().ok.body.json.map(\.name)) ?? [] + } + while true { + let buttons = existing == nil + ? [ModalButton(label: "Cancel", value: "cancel"), ModalButton(label: "Create", value: "save", primary: true)] + : [ModalButton(label: "Delete", value: "delete", danger: true), ModalButton(label: "Cancel", value: "cancel"), ModalButton(label: "Save", value: "save", primary: true)] + let choice = await overlays.showModal(ScheduleEditor(draft: draft, project: project), buttons, inPane: true, canSubmit: { draft.valid }) + switch choice { + case "save": + do { + if let existing { + _ = try await store.api.client.updateSchedule(path: .init(scheduleId: existing.id), body: .json(draft.save)).ok.body.json + } else { + _ = try await store.api.client.createSchedule(path: .init(projectId: project.id), body: .json(draft.save)).created.body.json + } + return + } catch { + draft.failure = errorText(error) + } + case "delete": + guard let existing, await overlays.showConfirm(["Delete the schedule \"\(existing.title)\"? Tasks it already created stay."], action: "Delete", danger: true, inPane: true) else { + continue + } + do { + _ = try await store.api.client.deleteSchedule(path: .init(scheduleId: existing.id)).noContent + } catch { + overlays.showAlert(errorText(error)) + } + return + default: + return + } + } +} + +private struct ScheduleEditor: View { + @Bindable var draft: ScheduleDraft + let project: Project + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Schedule a task in \(project.name), started from \(project.defaultBranch). Times are the server's.") + .foregroundStyle(Palette.muted) + .fixedSize(horizontal: false, vertical: true) + TextField("Task title", text: $draft.title) + .modifier(FieldStyle()) + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text("Every").foregroundStyle(Palette.muted) + ChoiceMenu(value: $draft.every, options: [(.hour, "hour"), (.day, "day"), (.week, "week"), (.month, "month")]) + if draft.every == .week { + Text("on").foregroundStyle(Palette.muted) + HStack(spacing: 4) { + ForEach(weekdayNames, id: \.0) { day in + dayToggle(day) + } + } + } + if draft.every == .month { + Text("on the").foregroundStyle(Palette.muted) + ChoiceMenu(value: $draft.dayOfMonth, options: (1...31).map { ($0, ordinal($0)) }) + } + Text("at").foregroundStyle(Palette.muted) + if draft.every == .hour { + ChoiceMenu(value: $draft.minute, options: (0...59).map { ($0, ":\(twoDigits($0))") }) + } else { + ChoiceMenu(value: $draft.hour, options: (0...23).map { ($0, String($0)) }) + Text(":").foregroundStyle(Palette.muted) + ChoiceMenu(value: $draft.minute, options: (0...59).map { ($0, twoDigits($0)) }) + } + } + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text("Run").foregroundStyle(Palette.muted) + let names = draft.workflow.isEmpty || draft.workflows.contains(draft.workflow) ? draft.workflows : [draft.workflow] + draft.workflows + ChoiceMenu(value: $draft.workflow, options: [("", "one agent")] + names.map { ($0, "workflow \($0)") }) + if draft.workflow.isEmpty { + Text("on").foregroundStyle(Palette.muted) + ModelPicker(model: $draft.model) + } + } + Text(draft.workflow.isEmpty ? "The agent's first message: a complete brief, since it sees nothing else." : "The workflow's brief, given to every stage (optional).") + .foregroundStyle(Palette.muted) + .fixedSize(horizontal: false, vertical: true) + PromptEditor(text: $draft.prompt) + if !draft.failure.isEmpty { + Text(draft.failure) + .foregroundStyle(Palette.danger) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func dayToggle(_ day: (Weekday, String, String)) -> some View { + let on = draft.weekdays.contains(day.0) + return Button { + if on { + draft.weekdays.remove(day.0) + } else { + draft.weekdays.insert(day.0) + } + } label: { + Text(day.1) + .font(Fonts.font(11, weight: on ? 700 : 400)) + .foregroundStyle(on ? Palette.text : Palette.muted) + .frame(width: 20, height: 20) + .background(on ? Palette.accentTint : .clear, in: RoundedRectangle(cornerRadius: 4)) + .overlay(RoundedRectangle(cornerRadius: 4).stroke(on ? Palette.accent : Palette.border)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help(day.2) + .accessibilityLabel(day.2) + } +} diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index 0f392b4..7fe0507 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -58,6 +58,7 @@ final class Store { var me: User? var projects: [Project] = [] var tasks: [String: [ByreTask]] = [:] + var schedules: [String: [Schedule]] = [:] var sessions: [String: [AgentSession]] = [:] var inbox: [InboxItem] = [] private(set) var terminals: [String: [TaskTerminal]] = [:] @@ -313,9 +314,34 @@ final class Store { projects = try await api.client.listProjects().ok.body.json try await each(projects.map(\.id)) { id in try await self.loadTasks(id) + try await self.loadSchedules(id) } } + func loadSchedules(_ projectId: String) async throws { + let latest = claimRequest("schedules:\(projectId)") + let list = try await api.client.listSchedules(path: .init(projectId: projectId)).ok.body.json + if !latest() { + return + } + schedules[projectId] = list + } + + private func applySchedule(_ ev: ByreEvent) { + guard let projectId = ev.projectId else { + return + } + var list = schedules[projectId] ?? [] + if ev.kind == "schedule.saved" { + let sc = try! decodePayload(ev.payload, as: Schedule.self) + list.removeAll { $0.id == sc.id } + list.append(sc) + } else if let id = str(ev.payload.value, "id") { + list.removeAll { $0.id == id } + } + schedules[projectId] = list.sorted { ($0.nextRun, $0.id) < ($1.nextRun, $1.id) } + } + private func claimRequest(_ key: String) -> () -> Bool { let seq = (requestSeqs[key] ?? 0) + 1 requestSeqs[key] = seq @@ -671,6 +697,8 @@ final class Store { spawn("load projects") { try await self.loadProjects() } + case "schedule.saved", "schedule.deleted": + applySchedule(ev) case "task.created", "task.title", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "task.ready_cleared", "snapshot.created": if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && !["answer", "fork", "ask_human"].contains(str(ev.payload.value, "trigger"))) { clearTask(self, taskId) diff --git a/mac/Sources/Byre/WorkflowEditor.swift b/mac/Sources/Byre/WorkflowEditor.swift index d47094c..26fcdc4 100644 --- a/mac/Sources/Byre/WorkflowEditor.swift +++ b/mac/Sources/Byre/WorkflowEditor.swift @@ -295,7 +295,7 @@ private struct StepRow: View { if let agents = node.agents { Text("team of \(agents.count)").foregroundStyle(Palette.muted).fixedSize() } else { - StepModelMenu(model: $node.model) + ModelPicker(model: $node.model) } case .order: Text("in order").foregroundStyle(Palette.muted) @@ -326,7 +326,7 @@ private struct StepRow: View { TextField("Agent name", text: Binding(get: { node.agents?[i].name ?? "" }, set: { node.agents?[i].name = $0 })) .textFieldStyle(.plain) .font(Fonts.font(13, weight: 700)) - StepModelMenu(model: Binding(get: { node.agents?[i].model ?? "" }, set: { node.agents?[i].model = $0 })) + ModelPicker(model: Binding(get: { node.agents?[i].model ?? "" }, set: { node.agents?[i].model = $0 })) } .padding(.top, 4) PromptEditor(text: Binding(get: { node.agents?[i].prompt ?? "" }, set: { node.agents?[i].prompt = $0 })) @@ -437,22 +437,34 @@ private struct StepRow: View { } } -private struct StepModelMenu: View { +struct ModelPicker: View { @Binding var model: String var body: some View { + let list = model.isEmpty || models.contains(model) ? models : [model] + models + ChoiceMenu(value: $model, options: list.map { ($0, $0) }, placeholder: "model") + } +} + +struct ChoiceMenu: View { + @Binding var value: Value + let options: [(Value, String)] + var placeholder = "" + + var body: some View { + let label = options.first { $0.0 == value }?.1 Menu { - ForEach(models, id: \.self) { m in - Button(m) { model = m } + ForEach(options, id: \.0) { option in + Button(option.1) { value = option.0 } } } label: { HStack(spacing: 6) { - Text(model.isEmpty ? "model" : model) + Text(label ?? placeholder) Image(systemName: "chevron.up.chevron.down") .font(Fonts.font(8, weight: 600)) } .font(Fonts.font(12)) - .foregroundStyle(model.isEmpty ? Palette.muted : Palette.text) + .foregroundStyle(label == nil ? Palette.muted : Palette.text) .contentShape(Rectangle()) } .menuStyle(.button) @@ -462,7 +474,7 @@ private struct StepModelMenu: View { } } -private struct PromptEditor: View { +struct PromptEditor: View { @Binding var text: String @FocusState private var focused: Bool