mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 23:17:30 +08:00
add autonomy quiet-hours policy and reflect logs with task persistence
This commit is contained in:
@@ -649,6 +649,7 @@ func buildAutonomyEngine(cfg *config.Config, msgBus *bus.MessageBus) *autonomy.E
|
|||||||
MaxConsecutiveStalls: a.MaxConsecutiveStalls,
|
MaxConsecutiveStalls: a.MaxConsecutiveStalls,
|
||||||
MaxDispatchPerTick: a.MaxDispatchPerTick,
|
MaxDispatchPerTick: a.MaxDispatchPerTick,
|
||||||
NotifyCooldownSec: a.NotifyCooldownSec,
|
NotifyCooldownSec: a.NotifyCooldownSec,
|
||||||
|
QuietHours: a.QuietHours,
|
||||||
Workspace: cfg.WorkspacePath(),
|
Workspace: cfg.WorkspacePath(),
|
||||||
DefaultNotifyChannel: a.NotifyChannel,
|
DefaultNotifyChannel: a.NotifyChannel,
|
||||||
DefaultNotifyChatID: a.NotifyChatID,
|
DefaultNotifyChatID: a.NotifyChatID,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"max_consecutive_stalls": 3,
|
"max_consecutive_stalls": 3,
|
||||||
"max_dispatch_per_tick": 2,
|
"max_dispatch_per_tick": 2,
|
||||||
"notify_cooldown_sec": 300,
|
"notify_cooldown_sec": 300,
|
||||||
|
"quiet_hours": "23:00-08:00",
|
||||||
"notify_channel": "",
|
"notify_channel": "",
|
||||||
"notify_chat_id": ""
|
"notify_chat_id": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -26,6 +27,7 @@ type Options struct {
|
|||||||
DefaultNotifyChannel string
|
DefaultNotifyChannel string
|
||||||
DefaultNotifyChatID string
|
DefaultNotifyChatID string
|
||||||
NotifyCooldownSec int
|
NotifyCooldownSec int
|
||||||
|
QuietHours string
|
||||||
}
|
}
|
||||||
|
|
||||||
type taskState struct {
|
type taskState struct {
|
||||||
@@ -168,6 +170,7 @@ func (e *Engine) tick() {
|
|||||||
st.Status = "running"
|
st.Status = "running"
|
||||||
st.LastRunAt = now
|
st.LastRunAt = now
|
||||||
st.LastAutonomyAt = now
|
st.LastAutonomyAt = now
|
||||||
|
e.writeReflectLog("dispatch", st, "task dispatched to agent loop")
|
||||||
dispatched++
|
dispatched++
|
||||||
}
|
}
|
||||||
e.persistStateLocked()
|
e.persistStateLocked()
|
||||||
@@ -231,6 +234,7 @@ func (e *Engine) dispatchTask(st *taskState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) sendCompletionNotification(st *taskState) {
|
func (e *Engine) sendCompletionNotification(st *taskState) {
|
||||||
|
e.writeReflectLog("complete", st, "task marked completed")
|
||||||
if !e.shouldNotify("done:" + st.ID) {
|
if !e.shouldNotify("done:" + st.ID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -242,6 +246,7 @@ func (e *Engine) sendCompletionNotification(st *taskState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) sendFailureNotification(st *taskState, reason string) {
|
func (e *Engine) sendFailureNotification(st *taskState, reason string) {
|
||||||
|
e.writeReflectLog("blocked", st, reason)
|
||||||
if !e.shouldNotify("blocked:" + st.ID) {
|
if !e.shouldNotify("blocked:" + st.ID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -257,6 +262,9 @@ func (e *Engine) shouldNotify(key string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
if inQuietHours(now, e.opts.QuietHours) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if last, ok := e.lastNotify[key]; ok {
|
if last, ok := e.lastNotify[key]; ok {
|
||||||
if now.Sub(last) < time.Duration(e.opts.NotifyCooldownSec)*time.Second {
|
if now.Sub(last) < time.Duration(e.opts.NotifyCooldownSec)*time.Second {
|
||||||
return false
|
return false
|
||||||
@@ -266,6 +274,55 @@ func (e *Engine) shouldNotify(key string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Engine) writeReflectLog(stage string, st *taskState, outcome string) {
|
||||||
|
if strings.TrimSpace(e.opts.Workspace) == "" || st == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memDir := filepath.Join(e.opts.Workspace, "memory")
|
||||||
|
_ = os.MkdirAll(memDir, 0755)
|
||||||
|
path := filepath.Join(memDir, time.Now().Format("2006-01-02")+".md")
|
||||||
|
line := fmt.Sprintf("- [%s] [autonomy][%s] task=%s status=%s outcome=%s\n", time.Now().Format("15:04"), stage, st.Content, st.Status, outcome)
|
||||||
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, _ = f.WriteString(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inQuietHours(now time.Time, spec string) bool {
|
||||||
|
spec = strings.TrimSpace(spec)
|
||||||
|
if spec == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := strings.Split(spec, "-")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parseHM := func(v string) (int, bool) {
|
||||||
|
hm := strings.Split(strings.TrimSpace(v), ":")
|
||||||
|
if len(hm) != 2 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
h, err1 := strconv.Atoi(hm[0])
|
||||||
|
m, err2 := strconv.Atoi(hm[1])
|
||||||
|
if err1 != nil || err2 != nil || h < 0 || h > 23 || m < 0 || m > 59 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return h*60 + m, true
|
||||||
|
}
|
||||||
|
start, ok1 := parseHM(parts[0])
|
||||||
|
end, ok2 := parseHM(parts[1])
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nowMin := now.Hour()*60 + now.Minute()
|
||||||
|
if start <= end {
|
||||||
|
return nowMin >= start && nowMin <= end
|
||||||
|
}
|
||||||
|
return nowMin >= start || nowMin <= end
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) persistStateLocked() {
|
func (e *Engine) persistStateLocked() {
|
||||||
items := make([]TaskItem, 0, len(e.state))
|
items := make([]TaskItem, 0, len(e.state))
|
||||||
for _, st := range e.state {
|
for _, st := range e.state {
|
||||||
|
|||||||
@@ -45,15 +45,16 @@ type AgentDefaults struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AutonomyConfig struct {
|
type AutonomyConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ENABLED"`
|
Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_ENABLED"`
|
||||||
TickIntervalSec int `json:"tick_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TICK_INTERVAL_SEC"`
|
TickIntervalSec int `json:"tick_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_TICK_INTERVAL_SEC"`
|
||||||
MinRunIntervalSec int `json:"min_run_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MIN_RUN_INTERVAL_SEC"`
|
MinRunIntervalSec int `json:"min_run_interval_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MIN_RUN_INTERVAL_SEC"`
|
||||||
MaxPendingDurationSec int `json:"max_pending_duration_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_PENDING_DURATION_SEC"`
|
MaxPendingDurationSec int `json:"max_pending_duration_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_PENDING_DURATION_SEC"`
|
||||||
MaxConsecutiveStalls int `json:"max_consecutive_stalls" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_CONSECUTIVE_STALLS"`
|
MaxConsecutiveStalls int `json:"max_consecutive_stalls" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_CONSECUTIVE_STALLS"`
|
||||||
MaxDispatchPerTick int `json:"max_dispatch_per_tick" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_DISPATCH_PER_TICK"`
|
MaxDispatchPerTick int `json:"max_dispatch_per_tick" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_MAX_DISPATCH_PER_TICK"`
|
||||||
NotifyCooldownSec int `json:"notify_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_COOLDOWN_SEC"`
|
NotifyCooldownSec int `json:"notify_cooldown_sec" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_COOLDOWN_SEC"`
|
||||||
NotifyChannel string `json:"notify_channel" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHANNEL"`
|
QuietHours string `json:"quiet_hours" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_QUIET_HOURS"`
|
||||||
NotifyChatID string `json:"notify_chat_id" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHAT_ID"`
|
NotifyChannel string `json:"notify_channel" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHANNEL"`
|
||||||
|
NotifyChatID string `json:"notify_chat_id" env:"CLAWGO_AGENTS_DEFAULTS_AUTONOMY_NOTIFY_CHAT_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentTextConfig struct {
|
type AgentTextConfig struct {
|
||||||
@@ -305,6 +306,7 @@ func DefaultConfig() *Config {
|
|||||||
MaxConsecutiveStalls: 3,
|
MaxConsecutiveStalls: 3,
|
||||||
MaxDispatchPerTick: 2,
|
MaxDispatchPerTick: 2,
|
||||||
NotifyCooldownSec: 300,
|
NotifyCooldownSec: 300,
|
||||||
|
QuietHours: "23:00-08:00",
|
||||||
NotifyChannel: "",
|
NotifyChannel: "",
|
||||||
NotifyChatID: "",
|
NotifyChatID: "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ func Validate(cfg *Config) []error {
|
|||||||
if aut.NotifyCooldownSec <= 0 {
|
if aut.NotifyCooldownSec <= 0 {
|
||||||
errs = append(errs, fmt.Errorf("agents.defaults.autonomy.notify_cooldown_sec must be > 0 when enabled=true"))
|
errs = append(errs, fmt.Errorf("agents.defaults.autonomy.notify_cooldown_sec must be > 0 when enabled=true"))
|
||||||
}
|
}
|
||||||
|
if qh := strings.TrimSpace(aut.QuietHours); qh != "" {
|
||||||
|
parts := strings.Split(qh, "-")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
errs = append(errs, fmt.Errorf("agents.defaults.autonomy.quiet_hours must be HH:MM-HH:MM"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
texts := cfg.Agents.Defaults.Texts
|
texts := cfg.Agents.Defaults.Texts
|
||||||
if strings.TrimSpace(texts.NoResponseFallback) == "" {
|
if strings.TrimSpace(texts.NoResponseFallback) == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user