Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions cli/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"csgclaw/internal/participant"
runtimecodex "csgclaw/internal/runtime/codex"
"csgclaw/internal/sandboxproviders"
"csgclaw/internal/scheduledtask"
"csgclaw/internal/server"
"csgclaw/internal/taskcore"
"csgclaw/internal/team"
Expand All @@ -60,6 +61,7 @@ var (
NewLLMService = newLLMService
NewTeamService = newTeamService
NewAgentTaskService = newAgentTaskService
NewScheduledTaskService = newScheduledTaskService
CheckModelProvider = checkModelProvider
CheckCatalogModelProvider = agent.CheckModelProvider
EnsureCLIProxy = func(ctx context.Context) error {
Expand Down Expand Up @@ -599,6 +601,10 @@ func startServerWithConfigPath(ctx context.Context, run *command.Context, cfg co
if err != nil {
return err
}
scheduledTaskSvc, err := NewScheduledTaskService(agentTaskSvc)
if err != nil {
return err
}
ensureBootstrapManager := EnsureBootstrapManager
startConfiguredAgents := StartConfiguredAgents
return RunServer(server.Options{
Expand All @@ -613,6 +619,7 @@ func startServerWithConfigPath(ctx context.Context, run *command.Context, cfg co
LLM: llmSvc,
Team: teamSvc,
AgentTask: agentTaskSvc,
ScheduledTask: scheduledTaskSvc,
TeamAdapters: teamAdapters,
Upgrade: upgradeManager,
ActivityDecider: channelActivityDecider(codexBridgeMgr),
Expand Down Expand Up @@ -1113,6 +1120,18 @@ func newAgentTaskService(agentSvc *agent.Service, imSvc *im.Service, participant
return agenttask.NewService(core, imSvc, agentSvc, participantSvc), nil
}

func newScheduledTaskService(agentTaskSvc *agenttask.Service) (*scheduledtask.Service, error) {
dir, err := config.DefaultScheduledTasksDir()
if err != nil {
return nil, err
}
store, err := scheduledtask.NewStore(dir)
if err != nil {
return nil, err
}
return scheduledtask.NewService(store, agentTaskSvc)
}

func buildFeishuComponents() (feishu.AgentCredentialProvider, *feishu.Service, error) {
participantsPath, err := defaultParticipantsPath()
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/agenttask/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Service struct {
}

type CreateInput struct {
ID string
AgentID string
Title string
Body string
Expand Down Expand Up @@ -89,6 +90,7 @@ func (s *Service) CreateAgentTask(ctx context.Context, input CreateInput) (taskc

assignedTo := s.participantIDForAgentID(agentID)
task, err := s.core.CreateRoot(taskcore.CreateRootInput{
ID: strings.TrimSpace(input.ID),
AssignmentType: taskcore.AssignmentTypeAgent,
AssignmentID: agentID,
Title: title,
Expand Down
8 changes: 8 additions & 0 deletions internal/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
agentruntime "csgclaw/internal/runtime"
"csgclaw/internal/sandbox"
"csgclaw/internal/sandboxproviders"
"csgclaw/internal/scheduledtask"
"csgclaw/internal/team"
hub "csgclaw/internal/template"
"csgclaw/internal/upgrade"
Expand All @@ -49,6 +50,7 @@ type Handler struct {
hub *hub.Service
teamSvc *team.Service
agentTaskSvc *agenttask.Service
scheduledTaskSvc *scheduledtask.Service
connectors *connectors.Service
teamAdapters *team.AdapterRegistry
teamPlanJobsMu sync.Mutex
Expand Down Expand Up @@ -704,6 +706,12 @@ func (h *Handler) SetAgentTaskService(svc *agenttask.Service) {
}
}

func (h *Handler) SetScheduledTaskService(svc *scheduledtask.Service) {
if h != nil {
h.scheduledTaskSvc = svc
}
}

func (h *Handler) SetConnectorService(svc *connectors.Service) {
if h != nil {
h.connectors = svc
Expand Down
24 changes: 24 additions & 0 deletions internal/api/rest_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,30 @@ func (h *Handler) updateAgentTask(w http.ResponseWriter, r *http.Request) {
func (h *Handler) listGlobalTasks(w http.ResponseWriter, r *http.Request) {
h.handleListGlobalTasks(w, r)
}

func (h *Handler) listScheduledTasks(w http.ResponseWriter, r *http.Request) {
h.handleListScheduledTasks(w, r)
}

func (h *Handler) createScheduledTask(w http.ResponseWriter, r *http.Request) {
h.handleCreateScheduledTask(w, r)
}

func (h *Handler) updateScheduledTask(w http.ResponseWriter, r *http.Request) {
h.handleUpdateScheduledTask(w, r)
}

func (h *Handler) deleteScheduledTask(w http.ResponseWriter, r *http.Request) {
h.handleDeleteScheduledTask(w, r)
}

func (h *Handler) listScheduledTaskRuns(w http.ResponseWriter, r *http.Request) {
h.handleListScheduledTaskRuns(w, r)
}

func (h *Handler) runScheduledTaskNow(w http.ResponseWriter, r *http.Request) {
h.handleRunScheduledTaskNow(w, r)
}
func (h *Handler) listIMMessages(w http.ResponseWriter, r *http.Request) { h.handleIMMessages(w, r) }
func (h *Handler) createIMMessage(w http.ResponseWriter, r *http.Request) { h.handleIMMessages(w, r) }
func (h *Handler) listIMConversations(w http.ResponseWriter, r *http.Request) { h.handleIMRooms(w, r) }
Expand Down
10 changes: 10 additions & 0 deletions internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ func (h *Handler) registerCoreRoutes(router chi.Router) {
r.Patch("/", h.updateAgentTask)
})
})
r.Route("/scheduled-tasks", func(r chi.Router) {
r.Get("/", h.listScheduledTasks)
r.Post("/", h.createScheduledTask)
r.Route("/{scheduled_task_id}", func(r chi.Router) {
r.Patch("/", h.updateScheduledTask)
r.Delete("/", h.deleteScheduledTask)
r.Get("/runs", h.listScheduledTaskRuns)
r.Post("/run-now", h.runScheduledTaskNow)
})
})
r.Get("/tasks", h.listGlobalTasks)
})
}
Expand Down
229 changes: 229 additions & 0 deletions internal/api/scheduled_task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package api

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"

"csgclaw/internal/apitypes"
"csgclaw/internal/scheduledtask"
)

func (h *Handler) requireScheduledTaskService(w http.ResponseWriter) (*scheduledtask.Service, bool) {
if h == nil || h.scheduledTaskSvc == nil {
http.Error(w, "scheduled task service is not configured", http.StatusServiceUnavailable)
return nil, false
}
return h.scheduledTaskSvc, true
}

func (h *Handler) handleListScheduledTasks(w http.ResponseWriter, r *http.Request) {
svc, ok := h.requireScheduledTaskService(w)
if !ok {
return
}
items := svc.List()
resp := make([]apitypes.ScheduledTask, 0, len(items))
for _, item := range items {
resp = append(resp, apiScheduledTask(item))
}
writeJSON(w, http.StatusOK, resp)
}

func (h *Handler) handleCreateScheduledTask(w http.ResponseWriter, r *http.Request) {
svc, ok := h.requireScheduledTaskService(w)
if !ok {
return
}
var req apitypes.CreateScheduledTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("decode request: %v", err), http.StatusBadRequest)
return
}
enabled := true
if req.Enabled != nil {
enabled = *req.Enabled
}
item, err := svc.Create(scheduledtask.CreateInput{
Title: strings.TrimSpace(req.Title),
AgentID: strings.TrimSpace(req.AgentID),
Prompt: strings.TrimSpace(req.Prompt),
Recurrence: strings.TrimSpace(req.Recurrence),
FirstRunAt: req.FirstRunAt.In(time.Local),
ExpiresAt: localTimePtr(req.ExpiresAt),
Enabled: enabled,
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
writeJSON(w, http.StatusCreated, apiScheduledTask(item))
}

func (h *Handler) handleUpdateScheduledTask(w http.ResponseWriter, r *http.Request) {
svc, ok := h.requireScheduledTaskService(w)
if !ok {
return
}
var req patchScheduledTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("decode request: %v", err), http.StatusBadRequest)
return
}
item, err := svc.Update(scheduledtask.UpdateInput{
ID: pathValue(r, "scheduled_task_id"),
Title: req.Title,
AgentID: req.AgentID,
Prompt: req.Prompt,
Recurrence: req.Recurrence,
NextRunAt: localTimeValuePtr(req.NextRunAt),
ExpiresAt: localOptionalTimeDoublePtr(req.ExpiresAt),
Enabled: req.Enabled,
})
if err != nil {
writeScheduledTaskError(w, err)
return
}
writeJSON(w, http.StatusOK, apiScheduledTask(item))
}

func (h *Handler) handleDeleteScheduledTask(w http.ResponseWriter, r *http.Request) {
svc, ok := h.requireScheduledTaskService(w)
if !ok {
return
}
if err := svc.Delete(pathValue(r, "scheduled_task_id")); err != nil {
writeScheduledTaskError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) handleListScheduledTaskRuns(w http.ResponseWriter, r *http.Request) {
svc, ok := h.requireScheduledTaskService(w)
if !ok {
return
}
runs := svc.Runs(pathValue(r, "scheduled_task_id"))
resp := make([]apitypes.ScheduledTaskRun, 0, len(runs))
for _, run := range runs {
resp = append(resp, apiScheduledTaskRun(run))
}
writeJSON(w, http.StatusOK, resp)
}

func (h *Handler) handleRunScheduledTaskNow(w http.ResponseWriter, r *http.Request) {
svc, ok := h.requireScheduledTaskService(w)
if !ok {
return
}
run, err := svc.RunNow(r.Context(), pathValue(r, "scheduled_task_id"))
if err != nil {
writeScheduledTaskError(w, err)
return
}
writeJSON(w, http.StatusCreated, apiScheduledTaskRun(run))
}

func writeScheduledTaskError(w http.ResponseWriter, err error) {
if errors.Is(err, scheduledtask.ErrActiveTask) {
http.Error(w, err.Error(), http.StatusConflict)
return
}
if strings.Contains(strings.ToLower(err.Error()), "not found") {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
http.Error(w, err.Error(), http.StatusBadRequest)
}

func apiScheduledTask(item scheduledtask.Task) apitypes.ScheduledTask {
return apitypes.ScheduledTask{
ID: item.ID,
Title: item.Title,
AgentID: item.AgentID,
Prompt: item.Prompt,
Recurrence: item.Recurrence,
Enabled: item.Enabled,
NextRunAt: item.NextRunAt,
LastRunAt: item.LastRunAt,
ExpiresAt: item.ExpiresAt,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}

func apiScheduledTaskRun(run scheduledtask.Run) apitypes.ScheduledTaskRun {
return apitypes.ScheduledTaskRun{
ID: run.ID,
ScheduledTaskID: run.ScheduledTaskID,
TriggeredAt: run.TriggeredAt,
Status: run.Status,
TaskID: run.TaskID,
Error: run.Error,
}
}

func localTimePtr(value *time.Time) *time.Time {
if value == nil {
return nil
}
next := value.In(time.Local)
return &next
}

func localTimeValuePtr(value *time.Time) *time.Time {
return localTimePtr(value)
}

func localTimeDoublePtr(value *time.Time) **time.Time {
if value == nil {
return nil
}
next := localTimePtr(value)
return &next
}

type patchScheduledTaskRequest struct {
Title *string `json:"title,omitempty"`
AgentID *string `json:"agent_id,omitempty"`
Prompt *string `json:"prompt,omitempty"`
Recurrence *string `json:"recurrence,omitempty"`
NextRunAt *time.Time `json:"next_run_at,omitempty"`
ExpiresAt optionalTime `json:"expires_at,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}

type optionalTime struct {
set bool
value *time.Time
}

func (t *optionalTime) UnmarshalJSON(data []byte) error {
t.set = true
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
t.value = nil
return nil
}
var value time.Time
if err := json.Unmarshal(data, &value); err != nil {
return err
}
t.value = &value
return nil
}

func localOptionalTimeDoublePtr(value optionalTime) **time.Time {
if !value.set {
return nil
}
if value.value == nil {
var cleared *time.Time
return &cleared
}
return localTimeDoublePtr(value.value)
}
Loading
Loading