Enhance CronTool to support executing shell commands and update job handling

This commit is contained in:
Satyam Tiwari
2026-02-12 20:21:49 +05:30
parent 8968d58fed
commit 9c98c11351
4 changed files with 71 additions and 6 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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),
} }
} }
@@ -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{

View File

@@ -8,10 +8,12 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strings" "strings"
"time" "time"
) )
type ExecTool struct { type ExecTool struct {
workingDir string workingDir string
timeout time.Duration timeout time.Duration
@@ -91,7 +93,13 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (st
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout) cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
defer cancel() defer cancel()
cmd := exec.CommandContext(cmdCtx, "sh", "-c", command) var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.CommandContext(cmdCtx, "powershell", "-NoProfile", "-NonInteractive", "-Command", command)
} else {
cmd = exec.CommandContext(cmdCtx, "sh", "-c", command)
}
if cwd != "" { if cwd != "" {
cmd.Dir = cwd cmd.Dir = cwd
} }