mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-08 00:17:30 +08:00
align heartbeat/cron flow with openclaw-style triggers
This commit is contained in:
@@ -60,9 +60,47 @@ func gatewayCmd() {
|
|||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
cronStorePath := filepath.Join(filepath.Dir(getConfigPath()), "cron", "jobs.json")
|
cronStorePath := filepath.Join(filepath.Dir(getConfigPath()), "cron", "jobs.json")
|
||||||
cronService := cron.NewCronService(cronStorePath, nil)
|
cronService := cron.NewCronService(cronStorePath, func(job *cron.CronJob) (string, error) {
|
||||||
|
if job == nil || strings.TrimSpace(job.Payload.Message) == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
targetChannel := strings.TrimSpace(job.Payload.Channel)
|
||||||
|
targetChatID := strings.TrimSpace(job.Payload.To)
|
||||||
|
if targetChannel == "" || targetChatID == "" {
|
||||||
|
targetChannel = "internal"
|
||||||
|
targetChatID = "cron"
|
||||||
|
}
|
||||||
|
msgBus.PublishInbound(bus.InboundMessage{
|
||||||
|
Channel: "system",
|
||||||
|
SenderID: "cron",
|
||||||
|
ChatID: fmt.Sprintf("%s:%s", targetChannel, targetChatID),
|
||||||
|
Content: job.Payload.Message,
|
||||||
|
SessionKey: fmt.Sprintf("cron:%s", job.ID),
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"trigger": "cron",
|
||||||
|
"job_id": job.ID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return "scheduled", nil
|
||||||
|
})
|
||||||
configureCronServiceRuntime(cronService, cfg)
|
configureCronServiceRuntime(cronService, cfg)
|
||||||
heartbeatService := heartbeat.NewHeartbeatService(cfg.WorkspacePath(), nil, 30*60, true)
|
hbInterval := cfg.Agents.Defaults.Heartbeat.EverySec
|
||||||
|
if hbInterval <= 0 {
|
||||||
|
hbInterval = 30 * 60
|
||||||
|
}
|
||||||
|
heartbeatService := heartbeat.NewHeartbeatService(cfg.WorkspacePath(), func(prompt string) (string, error) {
|
||||||
|
msgBus.PublishInbound(bus.InboundMessage{
|
||||||
|
Channel: "system",
|
||||||
|
SenderID: "heartbeat",
|
||||||
|
ChatID: "internal:heartbeat",
|
||||||
|
Content: prompt,
|
||||||
|
SessionKey: "heartbeat:default",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"trigger": "heartbeat",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return "queued", nil
|
||||||
|
}, hbInterval, cfg.Agents.Defaults.Heartbeat.Enabled)
|
||||||
sentinelService := sentinel.NewService(
|
sentinelService := sentinel.NewService(
|
||||||
getConfigPath(),
|
getConfigPath(),
|
||||||
cfg.WorkspacePath(),
|
cfg.WorkspacePath(),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type AgentLoop struct {
|
|||||||
compactionEnabled bool
|
compactionEnabled bool
|
||||||
compactionTrigger int
|
compactionTrigger int
|
||||||
compactionKeepRecent int
|
compactionKeepRecent int
|
||||||
|
heartbeatAckMaxChars int
|
||||||
running bool
|
running bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
|||||||
compactionEnabled: cfg.Agents.Defaults.ContextCompaction.Enabled,
|
compactionEnabled: cfg.Agents.Defaults.ContextCompaction.Enabled,
|
||||||
compactionTrigger: cfg.Agents.Defaults.ContextCompaction.TriggerMessages,
|
compactionTrigger: cfg.Agents.Defaults.ContextCompaction.TriggerMessages,
|
||||||
compactionKeepRecent: cfg.Agents.Defaults.ContextCompaction.KeepRecentMessages,
|
compactionKeepRecent: cfg.Agents.Defaults.ContextCompaction.KeepRecentMessages,
|
||||||
|
heartbeatAckMaxChars: cfg.Agents.Defaults.Heartbeat.AckMaxChars,
|
||||||
running: false,
|
running: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +164,9 @@ func (al *AgentLoop) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if response != "" {
|
if response != "" {
|
||||||
|
if al.shouldSuppressOutbound(msg, response) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
ChatID: msg.ChatID,
|
ChatID: msg.ChatID,
|
||||||
@@ -178,6 +183,27 @@ func (al *AgentLoop) Stop() {
|
|||||||
al.running = false
|
al.running = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (al *AgentLoop) shouldSuppressOutbound(msg bus.InboundMessage, response string) bool {
|
||||||
|
if msg.Metadata == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
trigger := strings.ToLower(strings.TrimSpace(msg.Metadata["trigger"]))
|
||||||
|
if trigger != "heartbeat" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
r := strings.TrimSpace(response)
|
||||||
|
if !strings.HasPrefix(r, "HEARTBEAT_OK") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
maxChars := al.heartbeatAckMaxChars
|
||||||
|
if maxChars <= 0 {
|
||||||
|
maxChars = 64
|
||||||
|
}
|
||||||
|
return len(r) <= maxChars
|
||||||
|
}
|
||||||
|
|
||||||
func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey string) (string, error) {
|
func (al *AgentLoop) ProcessDirect(ctx context.Context, content, sessionKey string) (string, error) {
|
||||||
msg := bus.InboundMessage{
|
msg := bus.InboundMessage{
|
||||||
Channel: "cli",
|
Channel: "cli",
|
||||||
|
|||||||
@@ -37,10 +37,17 @@ type AgentDefaults struct {
|
|||||||
MaxTokens int `json:"max_tokens" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOKENS"`
|
MaxTokens int `json:"max_tokens" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||||
Temperature float64 `json:"temperature" env:"CLAWGO_AGENTS_DEFAULTS_TEMPERATURE"`
|
Temperature float64 `json:"temperature" env:"CLAWGO_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||||
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||||
|
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||||
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
|
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
|
||||||
RuntimeControl RuntimeControlConfig `json:"runtime_control"`
|
RuntimeControl RuntimeControlConfig `json:"runtime_control"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HeartbeatConfig struct {
|
||||||
|
Enabled bool `json:"enabled" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_ENABLED"`
|
||||||
|
EverySec int `json:"every_sec" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_EVERY_SEC"`
|
||||||
|
AckMaxChars int `json:"ack_max_chars" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_ACK_MAX_CHARS"`
|
||||||
|
}
|
||||||
|
|
||||||
type RuntimeControlConfig struct {
|
type RuntimeControlConfig struct {
|
||||||
IntentMaxInputChars int `json:"intent_max_input_chars" env:"CLAWGO_INTENT_MAX_INPUT_CHARS"`
|
IntentMaxInputChars int `json:"intent_max_input_chars" env:"CLAWGO_INTENT_MAX_INPUT_CHARS"`
|
||||||
AutonomyTickIntervalSec int `json:"autonomy_tick_interval_sec" env:"CLAWGO_AUTONOMY_TICK_INTERVAL_SEC"`
|
AutonomyTickIntervalSec int `json:"autonomy_tick_interval_sec" env:"CLAWGO_AUTONOMY_TICK_INTERVAL_SEC"`
|
||||||
@@ -254,6 +261,11 @@ func DefaultConfig() *Config {
|
|||||||
MaxTokens: 8192,
|
MaxTokens: 8192,
|
||||||
Temperature: 0.7,
|
Temperature: 0.7,
|
||||||
MaxToolIterations: 20,
|
MaxToolIterations: 20,
|
||||||
|
Heartbeat: HeartbeatConfig{
|
||||||
|
Enabled: true,
|
||||||
|
EverySec: 30 * 60,
|
||||||
|
AckMaxChars: 64,
|
||||||
|
},
|
||||||
ContextCompaction: ContextCompactionConfig{
|
ContextCompaction: ContextCompactionConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Mode: "summary",
|
Mode: "summary",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"clawgo/pkg/lifecycle"
|
"clawgo/pkg/lifecycle"
|
||||||
@@ -29,7 +30,7 @@ func NewHeartbeatService(workspace string, onHeartbeat func(string) (string, err
|
|||||||
|
|
||||||
func (hs *HeartbeatService) Start() error {
|
func (hs *HeartbeatService) Start() error {
|
||||||
if !hs.enabled {
|
if !hs.enabled {
|
||||||
return fmt.Errorf("heartbeat service is disabled")
|
return nil
|
||||||
}
|
}
|
||||||
hs.runner.Start(hs.runLoop)
|
hs.runner.Start(hs.runLoop)
|
||||||
return nil
|
return nil
|
||||||
@@ -73,24 +74,22 @@ func (hs *HeartbeatService) checkHeartbeat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HeartbeatService) buildPrompt() string {
|
func (hs *HeartbeatService) buildPrompt() string {
|
||||||
notesDir := filepath.Join(hs.workspace, "memory")
|
notesFile := filepath.Join(hs.workspace, "HEARTBEAT.md")
|
||||||
notesFile := filepath.Join(notesDir, "HEARTBEAT.md")
|
|
||||||
|
|
||||||
var notes string
|
var notes string
|
||||||
if data, err := os.ReadFile(notesFile); err == nil {
|
if data, err := os.ReadFile(notesFile); err == nil {
|
||||||
notes = string(data)
|
candidate := string(data)
|
||||||
|
if !isEffectivelyEmptyMarkdown(candidate) {
|
||||||
|
notes = candidate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().Format("2006-01-02 15:04")
|
now := time.Now().Format("2006-01-02 15:04")
|
||||||
|
|
||||||
prompt := fmt.Sprintf(`# Heartbeat Check
|
prompt := fmt.Sprintf(`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.
|
||||||
|
|
||||||
Current time: %s
|
Current time: %s
|
||||||
|
|
||||||
Check if there are any tasks I should be aware of or actions I should take.
|
|
||||||
Review the memory file for any important updates or changes.
|
|
||||||
Be proactive in identifying potential issues or improvements.
|
|
||||||
|
|
||||||
%s
|
%s
|
||||||
`, now, notes)
|
`, now, notes)
|
||||||
|
|
||||||
@@ -108,3 +107,17 @@ func (hs *HeartbeatService) log(message string) {
|
|||||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||||
f.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, message))
|
f.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isEffectivelyEmptyMarkdown(content string) bool {
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
t := strings.TrimSpace(line)
|
||||||
|
if t == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(t, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user