diff --git a/README.ja.md b/README.ja.md index e33b312f9..a7f43b9a7 100644 --- a/README.ja.md +++ b/README.ja.md @@ -195,6 +195,9 @@ picoclaw onboard "api_key": "YOUR_BRAVE_API_KEY", "max_results": 5 } + }, + "cron": { + "exec_timeout_minutes": 5 } }, "heartbeat": { @@ -697,6 +700,9 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る "search": { "apiKey": "BSA..." } + }, + "cron": { + "exec_timeout_minutes": 5 } }, "heartbeat": { diff --git a/README.md b/README.md index a6f421e9d..e80e2213c 100644 --- a/README.md +++ b/README.md @@ -774,6 +774,9 @@ picoclaw agent -m "Hello" "enabled": true, "max_results": 5 } + }, + "cron": { + "exec_timeout_minutes": 5 } }, "heartbeat": { diff --git a/README.zh.md b/README.zh.md index 48d7677f0..e7dc8d769 100644 --- a/README.zh.md +++ b/README.zh.md @@ -236,6 +236,9 @@ picoclaw onboard "api_key": "YOUR_BRAVE_API_KEY", "max_results": 5 } + }, + "cron": { + "exec_timeout_minutes": 5 } } } @@ -644,6 +647,9 @@ picoclaw agent -m "你好" "search": { "api_key": "BSA..." } + }, + "cron": { + "exec_timeout_minutes": 5 } }, "heartbeat": { diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 10b53948b..fd7ec484a 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -562,7 +562,8 @@ func gatewayCmd() { }) // Setup cron tool and service - cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace) + execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute + cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout) heartbeatService := heartbeat.NewHeartbeatService( cfg.WorkspacePath(), @@ -987,14 +988,14 @@ func getConfigPath() string { return filepath.Join(home, ".picoclaw", "config.json") } -func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool) *cron.CronService { +func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *cron.CronService { cronStorePath := filepath.Join(workspace, "cron", "jobs.json") // Create cron service cronService := cron.NewCronService(cronStorePath, nil) // Create and register CronTool - cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict) + cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout) agentLoop.RegisterTool(cronTool) // Set the onJob handler diff --git a/config/config.example.json b/config/config.example.json index 62ad2c5fe..7cd0ab8c6 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -127,6 +127,9 @@ "api_key": "pplx-xxx", "max_results": 5 } + }, + "cron": { + "exec_timeout_minutes": 5 } }, "heartbeat": { diff --git a/pkg/config/config.go b/pkg/config/config.go index 558bf6a93..1d34f56f3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -218,8 +218,13 @@ type WebToolsConfig struct { Perplexity PerplexityConfig `json:"perplexity"` } +type CronToolsConfig struct { + ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout +} + type ToolsConfig struct { - Web WebToolsConfig `json:"web"` + Web WebToolsConfig `json:"web"` + Cron CronToolsConfig `json:"cron"` } func DefaultConfig() *Config { @@ -334,6 +339,9 @@ func DefaultConfig() *Config { MaxResults: 5, }, }, + Cron: CronToolsConfig{ + ExecTimeoutMinutes: 5, // default 5 minutes for LLM operations + }, }, Heartbeat: HeartbeatConfig{ Enabled: true, diff --git a/pkg/tools/cron.go b/pkg/tools/cron.go index 4b6f973d8..21bee42ef 100644 --- a/pkg/tools/cron.go +++ b/pkg/tools/cron.go @@ -28,12 +28,15 @@ type CronTool struct { } // NewCronTool creates a new CronTool -func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool) *CronTool { +// execTimeout: 0 means no timeout, >0 sets the timeout duration +func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *CronTool { + execTool := NewExecTool(workspace, restrict) + execTool.SetTimeout(execTimeout) // 0 means no timeout return &CronTool{ cronService: cronService, executor: executor, msgBus: msgBus, - execTool: NewExecTool(workspace, restrict), + execTool: execTool, } } diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 1ca3fc35a..713850f97 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -89,7 +89,14 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *To return ErrorResult(guardError) } - cmdCtx, cancel := context.WithTimeout(ctx, t.timeout) + // timeout == 0 means no timeout + var cmdCtx context.Context + var cancel context.CancelFunc + if t.timeout > 0 { + cmdCtx, cancel = context.WithTimeout(ctx, t.timeout) + } else { + cmdCtx, cancel = context.WithCancel(ctx) + } defer cancel() var cmd *exec.Cmd