mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-24 04:17:28 +08:00
align heartbeat/cron flow with openclaw-style triggers
This commit is contained in:
@@ -60,9 +60,47 @@ func gatewayCmd() {
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
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)
|
||||
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(
|
||||
getConfigPath(),
|
||||
cfg.WorkspacePath(),
|
||||
|
||||
@@ -36,6 +36,7 @@ type AgentLoop struct {
|
||||
compactionEnabled bool
|
||||
compactionTrigger int
|
||||
compactionKeepRecent int
|
||||
heartbeatAckMaxChars int
|
||||
running bool
|
||||
}
|
||||
|
||||
@@ -131,6 +132,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
compactionEnabled: cfg.Agents.Defaults.ContextCompaction.Enabled,
|
||||
compactionTrigger: cfg.Agents.Defaults.ContextCompaction.TriggerMessages,
|
||||
compactionKeepRecent: cfg.Agents.Defaults.ContextCompaction.KeepRecentMessages,
|
||||
heartbeatAckMaxChars: cfg.Agents.Defaults.Heartbeat.AckMaxChars,
|
||||
running: false,
|
||||
}
|
||||
|
||||
@@ -162,6 +164,9 @@ func (al *AgentLoop) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if response != "" {
|
||||
if al.shouldSuppressOutbound(msg, response) {
|
||||
continue
|
||||
}
|
||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: msg.Channel,
|
||||
ChatID: msg.ChatID,
|
||||
@@ -178,6 +183,27 @@ func (al *AgentLoop) Stop() {
|
||||
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) {
|
||||
msg := bus.InboundMessage{
|
||||
Channel: "cli",
|
||||
|
||||
@@ -37,10 +37,17 @@ type AgentDefaults struct {
|
||||
MaxTokens int `json:"max_tokens" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOKENS"`
|
||||
Temperature float64 `json:"temperature" env:"CLAWGO_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
|
||||
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 {
|
||||
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"`
|
||||
@@ -254,6 +261,11 @@ func DefaultConfig() *Config {
|
||||
MaxTokens: 8192,
|
||||
Temperature: 0.7,
|
||||
MaxToolIterations: 20,
|
||||
Heartbeat: HeartbeatConfig{
|
||||
Enabled: true,
|
||||
EverySec: 30 * 60,
|
||||
AckMaxChars: 64,
|
||||
},
|
||||
ContextCompaction: ContextCompactionConfig{
|
||||
Enabled: true,
|
||||
Mode: "summary",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/lifecycle"
|
||||
@@ -29,7 +30,7 @@ func NewHeartbeatService(workspace string, onHeartbeat func(string) (string, err
|
||||
|
||||
func (hs *HeartbeatService) Start() error {
|
||||
if !hs.enabled {
|
||||
return fmt.Errorf("heartbeat service is disabled")
|
||||
return nil
|
||||
}
|
||||
hs.runner.Start(hs.runLoop)
|
||||
return nil
|
||||
@@ -73,24 +74,22 @@ func (hs *HeartbeatService) checkHeartbeat() {
|
||||
}
|
||||
|
||||
func (hs *HeartbeatService) buildPrompt() string {
|
||||
notesDir := filepath.Join(hs.workspace, "memory")
|
||||
notesFile := filepath.Join(notesDir, "HEARTBEAT.md")
|
||||
notesFile := filepath.Join(hs.workspace, "HEARTBEAT.md")
|
||||
|
||||
var notes string
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
`, now, notes)
|
||||
|
||||
@@ -108,3 +107,17 @@ func (hs *HeartbeatService) log(message string) {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
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