Merge pull request #74 from SatyamDevv/main
feat(cron): Add support for direct shell command execution in scheduled jobs
This commit is contained in:
@@ -1034,7 +1034,7 @@ func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace
|
|||||||
cronService := cron.NewCronService(cronStorePath, nil)
|
cronService := cron.NewCronService(cronStorePath, nil)
|
||||||
|
|
||||||
// Create and register CronTool
|
// Create and register CronTool
|
||||||
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus)
|
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace)
|
||||||
agentLoop.RegisterTool(cronTool)
|
agentLoop.RegisterTool(cronTool)
|
||||||
|
|
||||||
// Set the onJob handler
|
// Set the onJob handler
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type CronSchedule struct {
|
|||||||
type CronPayload struct {
|
type CronPayload struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
Command string `json:"command,omitempty"`
|
||||||
Deliver bool `json:"deliver"`
|
Deliver bool `json:"deliver"`
|
||||||
Channel string `json:"channel,omitempty"`
|
Channel string `json:"channel,omitempty"`
|
||||||
To string `json:"to,omitempty"`
|
To string `json:"to,omitempty"`
|
||||||
@@ -358,6 +359,20 @@ func (cs *CronService) AddJob(name string, schedule CronSchedule, message string
|
|||||||
return &job, nil
|
return &job, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cs *CronService) UpdateJob(job *CronJob) error {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
|
|
||||||
|
for i := range cs.store.Jobs {
|
||||||
|
if cs.store.Jobs[i].ID == job.ID {
|
||||||
|
cs.store.Jobs[i] = *job
|
||||||
|
cs.store.Jobs[i].UpdatedAtMS = time.Now().UnixMilli()
|
||||||
|
return cs.saveStoreUnsafe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("job not found")
|
||||||
|
}
|
||||||
|
|
||||||
func (cs *CronService) RemoveJob(jobID string) bool {
|
func (cs *CronService) RemoveJob(jobID string) bool {
|
||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
defer cs.mu.Unlock()
|
defer cs.mu.Unlock()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package tools
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -21,17 +21,19 @@ type CronTool struct {
|
|||||||
cronService *cron.CronService
|
cronService *cron.CronService
|
||||||
executor JobExecutor
|
executor JobExecutor
|
||||||
msgBus *bus.MessageBus
|
msgBus *bus.MessageBus
|
||||||
|
execTool *ExecTool
|
||||||
channel string
|
channel string
|
||||||
chatID string
|
chatID string
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCronTool creates a new CronTool
|
// NewCronTool creates a new CronTool
|
||||||
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus) *CronTool {
|
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string) *CronTool {
|
||||||
return &CronTool{
|
return &CronTool{
|
||||||
cronService: cronService,
|
cronService: cronService,
|
||||||
executor: executor,
|
executor: executor,
|
||||||
msgBus: msgBus,
|
msgBus: msgBus,
|
||||||
|
execTool: NewExecTool(workspace, false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ func (t *CronTool) Name() string {
|
|||||||
|
|
||||||
// Description returns the tool description
|
// Description returns the tool description
|
||||||
func (t *CronTool) Description() string {
|
func (t *CronTool) Description() string {
|
||||||
return "Schedule reminders and tasks. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules (e.g., '0 9 * * *' for daily at 9am)."
|
return "Schedule reminders, tasks, or system commands. IMPORTANT: When user asks to be reminded or scheduled, you MUST call this tool. Use 'at_seconds' for one-time reminders (e.g., 'remind me in 10 minutes' → at_seconds=600). Use 'every_seconds' ONLY for recurring tasks (e.g., 'every 2 hours' → every_seconds=7200). Use 'cron_expr' for complex recurring schedules. Use 'command' to execute shell commands directly."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters returns the tool parameters schema
|
// Parameters returns the tool parameters schema
|
||||||
@@ -57,7 +59,11 @@ func (t *CronTool) Parameters() map[string]interface{} {
|
|||||||
},
|
},
|
||||||
"message": map[string]interface{}{
|
"message": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The reminder/task message to display when triggered (required for add)",
|
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.",
|
||||||
|
},
|
||||||
|
"command": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
|
||||||
},
|
},
|
||||||
"at_seconds": map[string]interface{}{
|
"at_seconds": map[string]interface{}{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -165,6 +171,15 @@ func (t *CronTool) addJob(args map[string]interface{}) (string, error) {
|
|||||||
deliver = d
|
deliver = d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command, _ := args["command"].(string)
|
||||||
|
if command != "" {
|
||||||
|
// Commands must be processed by agent/exec tool, so deliver must be false (or handled specifically)
|
||||||
|
// Actually, let's keep deliver=false to let the system know it's not a simple chat message
|
||||||
|
// But for our new logic in ExecuteJob, we can handle it regardless of deliver flag if Payload.Command is set.
|
||||||
|
// However, logically, it's not "delivered" to chat directly as is.
|
||||||
|
deliver = false
|
||||||
|
}
|
||||||
|
|
||||||
// Truncate message for job name (max 30 chars)
|
// Truncate message for job name (max 30 chars)
|
||||||
messagePreview := utils.Truncate(message, 30)
|
messagePreview := utils.Truncate(message, 30)
|
||||||
|
|
||||||
@@ -179,6 +194,12 @@ func (t *CronTool) addJob(args map[string]interface{}) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("Error adding job: %v", err), nil
|
return fmt.Sprintf("Error adding job: %v", err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if command != "" {
|
||||||
|
job.Payload.Command = command
|
||||||
|
// Need to save the updated payload
|
||||||
|
t.cronService.UpdateJob(job)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("Created job '%s' (id: %s)", job.Name, job.ID), nil
|
return fmt.Sprintf("Created job '%s' (id: %s)", job.Name, job.ID), nil
|
||||||
}
|
}
|
||||||
@@ -252,6 +273,27 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
|||||||
chatID = "direct"
|
chatID = "direct"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute command if present
|
||||||
|
if job.Payload.Command != "" {
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"command": job.Payload.Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := t.execTool.Execute(ctx, args)
|
||||||
|
if err != nil {
|
||||||
|
output = fmt.Sprintf("Error executing scheduled command: %v", err)
|
||||||
|
} else {
|
||||||
|
output = fmt.Sprintf("Scheduled command '%s' executed:\n%s", job.Payload.Command, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.msgBus.PublishOutbound(bus.OutboundMessage{
|
||||||
|
Channel: channel,
|
||||||
|
ChatID: chatID,
|
||||||
|
Content: output,
|
||||||
|
})
|
||||||
|
return "ok"
|
||||||
|
}
|
||||||
|
|
||||||
// If deliver=true, send message directly without agent processing
|
// If deliver=true, send message directly without agent processing
|
||||||
if job.Payload.Deliver {
|
if job.Payload.Deliver {
|
||||||
t.msgBus.PublishOutbound(bus.OutboundMessage{
|
t.msgBus.PublishOutbound(bus.OutboundMessage{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type ExecTool struct {
|
type ExecTool struct {
|
||||||
workingDir string
|
workingDir string
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
@@ -94,7 +95,7 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (st
|
|||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
cmd = exec.CommandContext(cmdCtx, "cmd", "/c", command)
|
cmd = exec.CommandContext(cmdCtx, "powershell", "-NoProfile", "-NonInteractive", "-Command", command)
|
||||||
} else {
|
} else {
|
||||||
cmd = exec.CommandContext(cmdCtx, "sh", "-c", command)
|
cmd = exec.CommandContext(cmdCtx, "sh", "-c", command)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user