From 6431f5792db61b194feef16e667cc8da09db192a Mon Sep 17 00:00:00 2001 From: DBT Date: Mon, 23 Feb 2026 15:57:33 +0000 Subject: [PATCH] reduce hardcoded agent text via config-driven prompts and recall keywords --- README.md | 8 +++++++- README_EN.md | 8 +++++++- cmd/clawgo/cmd_gateway.go | 2 +- config.example.json | 8 +++++++- pkg/agent/loop.go | 29 +++++++++++++++++++++++------ pkg/config/config.go | 20 +++++++++++++++++--- pkg/heartbeat/service.go | 35 ++++++++++++++++++----------------- 7 files changed, 80 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index f13a642..8428ba2 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,13 @@ clawgo channel test --channel telegram --to -m "ping" "heartbeat": { "enabled": true, "every_sec": 1800, - "ack_max_chars": 64 + "ack_max_chars": 64, + "prompt_template": "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." + }, + "texts": { + "no_response_fallback": "I've completed processing but have no response to give.", + "think_only_fallback": "Thinking process completed.", + "memory_recall_keywords": ["remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision"] }, "context_compaction": { "enabled": true, diff --git a/README_EN.md b/README_EN.md index 81d2eeb..d413f9e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -150,7 +150,13 @@ Heartbeat + context compaction config example: "heartbeat": { "enabled": true, "every_sec": 1800, - "ack_max_chars": 64 + "ack_max_chars": 64, + "prompt_template": "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." + }, + "texts": { + "no_response_fallback": "I've completed processing but have no response to give.", + "think_only_fallback": "Thinking process completed.", + "memory_recall_keywords": ["remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision"] }, "context_compaction": { "enabled": true, diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index 72ea5c4..442804d 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -587,5 +587,5 @@ func buildHeartbeatService(cfg *config.Config, msgBus *bus.MessageBus) *heartbea }, }) return "queued", nil - }, hbInterval, cfg.Agents.Defaults.Heartbeat.Enabled) + }, hbInterval, cfg.Agents.Defaults.Heartbeat.Enabled, cfg.Agents.Defaults.Heartbeat.PromptTemplate) } diff --git a/config.example.json b/config.example.json index 731a338..88094e0 100644 --- a/config.example.json +++ b/config.example.json @@ -10,7 +10,13 @@ "heartbeat": { "enabled": true, "every_sec": 1800, - "ack_max_chars": 64 + "ack_max_chars": 64, + "prompt_template": "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." + }, + "texts": { + "no_response_fallback": "I've completed processing but have no response to give.", + "think_only_fallback": "Thinking process completed.", + "memory_recall_keywords": ["remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision"] }, "context_compaction": { "enabled": true, diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index e4e6dc9..16dcebe 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -37,6 +37,9 @@ type AgentLoop struct { compactionTrigger int compactionKeepRecent int heartbeatAckMaxChars int + memoryRecallKeywords []string + noResponseFallback string + thinkOnlyFallback string audit *triggerAudit running bool } @@ -148,6 +151,9 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers compactionTrigger: cfg.Agents.Defaults.ContextCompaction.TriggerMessages, compactionKeepRecent: cfg.Agents.Defaults.ContextCompaction.KeepRecentMessages, heartbeatAckMaxChars: cfg.Agents.Defaults.Heartbeat.AckMaxChars, + memoryRecallKeywords: cfg.Agents.Defaults.Texts.MemoryRecallKeywords, + noResponseFallback: cfg.Agents.Defaults.Texts.NoResponseFallback, + thinkOnlyFallback: cfg.Agents.Defaults.Texts.ThinkOnlyFallback, audit: newTriggerAudit(workspace), running: false, } @@ -309,7 +315,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) summary := al.sessions.GetSummary(msg.SessionKey) memoryRecallUsed := false memoryRecallText := "" - if shouldRecallMemory(msg.Content) { + if shouldRecallMemory(msg.Content, al.memoryRecallKeywords) { if recall, err := al.tools.Execute(ctx, "memory_search", map[string]interface{}{"query": msg.Content, "maxResults": 3}); err == nil && strings.TrimSpace(recall) != "" { memoryRecallUsed = true memoryRecallText = strings.TrimSpace(recall) @@ -459,7 +465,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } if finalContent == "" { - finalContent = "I've completed processing but have no response to give." + fallback := strings.TrimSpace(al.noResponseFallback) + if fallback == "" { + fallback = "I've completed processing but have no response to give." + } + finalContent = fallback } // Filter out ... content from user-facing response @@ -472,7 +482,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) // For now, let's assume thoughts are auxiliary and empty response is okay if tools did work. // If no tools ran and only thoughts, user might be confused. if iteration == 1 { - userContent = "Thinking process completed." + fallback := strings.TrimSpace(al.thinkOnlyFallback) + if fallback == "" { + fallback = "Thinking process completed." + } + userContent = fallback } } @@ -843,14 +857,17 @@ func truncateString(s string, maxLen int) string { return s[:maxLen-3] + "..." } -func shouldRecallMemory(text string) bool { +func shouldRecallMemory(text string, keywords []string) bool { s := strings.ToLower(strings.TrimSpace(text)) if s == "" { return false } - keywords := []string{"remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision", "日期", "when did", "what did we"} + if len(keywords) == 0 { + keywords = []string{"remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision", "日期", "when did", "what did we"} + } for _, k := range keywords { - if strings.Contains(s, k) { + kk := strings.ToLower(strings.TrimSpace(k)) + if kk != "" && strings.Contains(s, kk) { return true } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 8e269ce..7fde7dd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -38,14 +38,22 @@ type AgentDefaults struct { 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"` + Texts AgentTextConfig `json:"texts"` ContextCompaction ContextCompactionConfig `json:"context_compaction"` RuntimeControl RuntimeControlConfig `json:"runtime_control"` } +type AgentTextConfig struct { + NoResponseFallback string `json:"no_response_fallback"` + ThinkOnlyFallback string `json:"think_only_fallback"` + MemoryRecallKeywords []string `json:"memory_recall_keywords"` +} + 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"` + 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"` + PromptTemplate string `json:"prompt_template" env:"CLAWGO_AGENTS_DEFAULTS_HEARTBEAT_PROMPT_TEMPLATE"` } type RuntimeControlConfig struct { @@ -265,6 +273,12 @@ func DefaultConfig() *Config { Enabled: true, EverySec: 30 * 60, AckMaxChars: 64, + PromptTemplate: "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.", + }, + Texts: AgentTextConfig{ + NoResponseFallback: "I've completed processing but have no response to give.", + ThinkOnlyFallback: "Thinking process completed.", + MemoryRecallKeywords: []string{"remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision", "日期", "when did", "what did we"}, }, ContextCompaction: ContextCompactionConfig{ Enabled: true, diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go index c4d6191..367de28 100644 --- a/pkg/heartbeat/service.go +++ b/pkg/heartbeat/service.go @@ -11,20 +11,22 @@ import ( ) type HeartbeatService struct { - workspace string - onHeartbeat func(string) (string, error) - interval time.Duration - enabled bool - runner *lifecycle.LoopRunner + workspace string + onHeartbeat func(string) (string, error) + interval time.Duration + enabled bool + promptTemplate string + runner *lifecycle.LoopRunner } -func NewHeartbeatService(workspace string, onHeartbeat func(string) (string, error), intervalS int, enabled bool) *HeartbeatService { +func NewHeartbeatService(workspace string, onHeartbeat func(string) (string, error), intervalS int, enabled bool, promptTemplate string) *HeartbeatService { return &HeartbeatService{ - workspace: workspace, - onHeartbeat: onHeartbeat, - interval: time.Duration(intervalS) * time.Second, - enabled: enabled, - runner: lifecycle.NewLoopRunner(), + workspace: workspace, + onHeartbeat: onHeartbeat, + interval: time.Duration(intervalS) * time.Second, + enabled: enabled, + promptTemplate: strings.TrimSpace(promptTemplate), + runner: lifecycle.NewLoopRunner(), } } @@ -86,12 +88,11 @@ func (hs *HeartbeatService) buildPrompt() string { now := time.Now().Format("2006-01-02 15:04") - 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 - -%s -`, now, notes) + tpl := hs.promptTemplate + if strings.TrimSpace(tpl) == "" { + tpl = "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." + } + prompt := fmt.Sprintf("%s\n\nCurrent time: %s\n\n%s\n", tpl, now, notes) return prompt }