mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-12 08:58:58 +08:00
fix
This commit is contained in:
@@ -573,32 +573,7 @@ func summarizeDialogTemplateChanges(oldCfg, newCfg *config.Config) []string {
|
||||
if oldCfg == nil || newCfg == nil {
|
||||
return nil
|
||||
}
|
||||
type pair struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
}
|
||||
oldT := oldCfg.Agents.Defaults.Texts
|
||||
newT := newCfg.Agents.Defaults.Texts
|
||||
checks := []pair{
|
||||
{name: "system_rewrite_template", a: oldT.SystemRewriteTemplate, b: newT.SystemRewriteTemplate},
|
||||
{name: "lang_usage", a: oldT.LangUsage, b: newT.LangUsage},
|
||||
{name: "lang_invalid", a: oldT.LangInvalid, b: newT.LangInvalid},
|
||||
{name: "lang_updated_template", a: oldT.LangUpdatedTemplate, b: newT.LangUpdatedTemplate},
|
||||
{name: "runtime_compaction_note", a: oldT.RuntimeCompactionNote, b: newT.RuntimeCompactionNote},
|
||||
{name: "startup_compaction_note", a: oldT.StartupCompactionNote, b: newT.StartupCompactionNote},
|
||||
{name: "autonomy_completion_template", a: oldT.AutonomyCompletionTemplate, b: newT.AutonomyCompletionTemplate},
|
||||
{name: "autonomy_blocked_template", a: oldT.AutonomyBlockedTemplate, b: newT.AutonomyBlockedTemplate},
|
||||
}
|
||||
out := make([]string, 0)
|
||||
for _, c := range checks {
|
||||
if strings.TrimSpace(c.a) != strings.TrimSpace(c.b) {
|
||||
out = append(out, c.name)
|
||||
}
|
||||
}
|
||||
if strings.Join(oldT.AutonomyImportantKeywords, "|") != strings.Join(newT.AutonomyImportantKeywords, "|") {
|
||||
out = append(out, "autonomy_important_keywords")
|
||||
}
|
||||
if oldCfg.Agents.Defaults.Heartbeat.PromptTemplate != newCfg.Agents.Defaults.Heartbeat.PromptTemplate {
|
||||
out = append(out, "heartbeat.prompt_template")
|
||||
}
|
||||
@@ -991,9 +966,6 @@ func buildAutonomyEngine(cfg *config.Config, msgBus *bus.MessageBus) *autonomy.E
|
||||
WaitingResumeDebounceSec: a.WaitingResumeDebounceSec,
|
||||
IdleRoundBudgetReleaseSec: idleRoundBudgetReleaseSec,
|
||||
AllowedTaskKeywords: a.AllowedTaskKeywords,
|
||||
ImportantKeywords: cfg.Agents.Defaults.Texts.AutonomyImportantKeywords,
|
||||
CompletionTemplate: cfg.Agents.Defaults.Texts.AutonomyCompletionTemplate,
|
||||
BlockedTemplate: cfg.Agents.Defaults.Texts.AutonomyBlockedTemplate,
|
||||
EKGConsecutiveErrorThreshold: a.EKGConsecutiveErrorThreshold,
|
||||
Workspace: cfg.WorkspacePath(),
|
||||
DefaultNotifyChannel: notifyChannel,
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/nodes"
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func statusCmd() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
@@ -75,7 +75,6 @@ func statusCmd() {
|
||||
cfg.Agents.Defaults.Heartbeat.EverySec,
|
||||
cfg.Agents.Defaults.Heartbeat.AckMaxChars,
|
||||
)
|
||||
printTemplateStatus(cfg)
|
||||
fmt.Printf("Cron Runtime: workers=%d sleep=%d-%ds\n",
|
||||
cfg.Cron.MaxWorkers,
|
||||
cfg.Cron.MinSleepSec,
|
||||
@@ -167,12 +166,24 @@ func statusCmd() {
|
||||
if n.Online {
|
||||
online++
|
||||
}
|
||||
if n.Capabilities.Run { caps["run"]++ }
|
||||
if n.Capabilities.Model { caps["model"]++ }
|
||||
if n.Capabilities.Camera { caps["camera"]++ }
|
||||
if n.Capabilities.Screen { caps["screen"]++ }
|
||||
if n.Capabilities.Location { caps["location"]++ }
|
||||
if n.Capabilities.Canvas { caps["canvas"]++ }
|
||||
if n.Capabilities.Run {
|
||||
caps["run"]++
|
||||
}
|
||||
if n.Capabilities.Model {
|
||||
caps["model"]++
|
||||
}
|
||||
if n.Capabilities.Camera {
|
||||
caps["camera"]++
|
||||
}
|
||||
if n.Capabilities.Screen {
|
||||
caps["screen"]++
|
||||
}
|
||||
if n.Capabilities.Location {
|
||||
caps["location"]++
|
||||
}
|
||||
if n.Capabilities.Canvas {
|
||||
caps["canvas"]++
|
||||
}
|
||||
}
|
||||
fmt.Printf("Nodes: total=%d online=%d\n", len(ns), online)
|
||||
fmt.Printf("Nodes Capabilities: run=%d model=%d camera=%d screen=%d location=%d canvas=%d\n", caps["run"], caps["model"], caps["camera"], caps["screen"], caps["location"], caps["canvas"])
|
||||
@@ -186,30 +197,6 @@ func statusCmd() {
|
||||
}
|
||||
}
|
||||
|
||||
func printTemplateStatus(cfg *config.Config) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
defaults := config.DefaultConfig().Agents.Defaults.Texts
|
||||
cur := cfg.Agents.Defaults.Texts
|
||||
fmt.Println("Dialog Templates:")
|
||||
printTemplateField("system_rewrite_template", cur.SystemRewriteTemplate, defaults.SystemRewriteTemplate)
|
||||
printTemplateField("lang_usage", cur.LangUsage, defaults.LangUsage)
|
||||
printTemplateField("lang_invalid", cur.LangInvalid, defaults.LangInvalid)
|
||||
printTemplateField("runtime_compaction_note", cur.RuntimeCompactionNote, defaults.RuntimeCompactionNote)
|
||||
printTemplateField("startup_compaction_note", cur.StartupCompactionNote, defaults.StartupCompactionNote)
|
||||
printTemplateField("autonomy_completion_template", cur.AutonomyCompletionTemplate, defaults.AutonomyCompletionTemplate)
|
||||
printTemplateField("autonomy_blocked_template", cur.AutonomyBlockedTemplate, defaults.AutonomyBlockedTemplate)
|
||||
}
|
||||
|
||||
func printTemplateField(name, current, def string) {
|
||||
state := "custom"
|
||||
if strings.TrimSpace(current) == strings.TrimSpace(def) {
|
||||
state = "default"
|
||||
}
|
||||
fmt.Printf(" %s: %s\n", name, state)
|
||||
}
|
||||
|
||||
func summarizeAutonomyActions(statsJSON []byte) string {
|
||||
var payload struct {
|
||||
Counts map[string]int `json:"counts"`
|
||||
@@ -272,7 +259,9 @@ func autonomyControlState(workspace string) string {
|
||||
}
|
||||
ctrlPath := filepath.Join(memDir, "autonomy.control.json")
|
||||
if data, err := os.ReadFile(ctrlPath); err == nil {
|
||||
var c struct{ Enabled bool `json:"enabled"` }
|
||||
var c struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if json.Unmarshal(data, &c) == nil {
|
||||
if c.Enabled {
|
||||
return "enabled"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"enabled": true,
|
||||
"every_sec": 1800,
|
||||
"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."
|
||||
"prompt_template": ""
|
||||
},
|
||||
"autonomy": {
|
||||
"enabled": false,
|
||||
@@ -31,23 +31,6 @@
|
||||
"allowed_task_keywords": [],
|
||||
"ekg_consecutive_error_threshold": 3
|
||||
},
|
||||
"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"],
|
||||
"lang_usage": "Usage: /lang <code>",
|
||||
"lang_invalid": "Invalid language code.",
|
||||
"lang_updated_template": "Language preference updated to %s",
|
||||
"subagents_none": "No subagents.",
|
||||
"sessions_none": "No sessions.",
|
||||
"unsupported_action": "unsupported action",
|
||||
"system_rewrite_template": "Rewrite the following internal system update in concise user-facing language:\n\n%s",
|
||||
"runtime_compaction_note": "[runtime-compaction] removed %d old messages, kept %d recent messages",
|
||||
"startup_compaction_note": "[startup-compaction] removed %d old messages, kept %d recent messages",
|
||||
"autonomy_important_keywords": ["urgent", "重要", "付款", "payment", "上线", "release", "deadline", "截止"],
|
||||
"autonomy_completion_template": "✅ 已完成:%s\n回复“继续 %s”可继续下一步。",
|
||||
"autonomy_blocked_template": "⚠️ 任务受阻:%s(%s)\n回复“继续 %s”我会重试。"
|
||||
},
|
||||
"context_compaction": {
|
||||
"enabled": true,
|
||||
"mode": "summary",
|
||||
|
||||
@@ -67,11 +67,8 @@ Your workspace is at: %s
|
||||
- Daily Notes: %s/memory/YYYY-MM-DD.md
|
||||
- Skills: %s/skills/{skill-name}/SKILL.md
|
||||
|
||||
%s
|
||||
|
||||
Always be helpful, accurate, and concise. When using tools, explain what you're doing.
|
||||
When remembering something long-term, write to %s/MEMORY.md and use daily notes at %s/memory/YYYY-MM-DD.md for short-term logs.`,
|
||||
now, runtime, workspacePath, workspacePath, workspacePath, workspacePath, toolsSection, workspacePath, workspacePath)
|
||||
%s`,
|
||||
now, runtime, workspacePath, workspacePath, workspacePath, workspacePath, toolsSection)
|
||||
}
|
||||
|
||||
func (cb *ContextBuilder) buildToolsSection() string {
|
||||
@@ -107,20 +104,10 @@ func (cb *ContextBuilder) BuildSystemPrompt() string {
|
||||
parts = append(parts, bootstrapContent)
|
||||
}
|
||||
|
||||
// Skills - OpenClaw-aligned selection protocol + available skill catalog
|
||||
// Skills catalog (selection behavior is defined in workspace AGENTS.md/SOUL.md).
|
||||
skillsSummary := cb.skillsLoader.BuildSkillsSummary()
|
||||
if skillsSummary != "" {
|
||||
parts = append(parts, fmt.Sprintf(`# Skills (mandatory protocol)
|
||||
|
||||
Before replying: scan <skill><description> entries in <skills>.
|
||||
- If exactly one skill clearly applies: read its SKILL.md (via read tool) and follow it.
|
||||
- If multiple could apply: choose the most specific one, then read/follow it.
|
||||
- If none clearly apply: do not read any SKILL.md.
|
||||
Constraints:
|
||||
- Never read more than one skill up front.
|
||||
- If SKILL.md references relative paths, resolve against the skill directory.
|
||||
|
||||
%s`, skillsSummary))
|
||||
parts = append(parts, fmt.Sprintf("## Skills Catalog\n\n%s", skillsSummary))
|
||||
}
|
||||
|
||||
// Memory context
|
||||
@@ -129,11 +116,6 @@ Constraints:
|
||||
parts = append(parts, memoryContext)
|
||||
}
|
||||
|
||||
parts = append(parts, `# Execution & Reply Policy
|
||||
- Default behavior: execute first, then report.
|
||||
- Avoid empty/meta fallback replies.
|
||||
- For commit/push intents, treat as one transaction and return commit hash + push result.`)
|
||||
|
||||
// Join with "---" separator
|
||||
return strings.Join(parts, "\n\n---\n\n")
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -34,41 +34,31 @@ import (
|
||||
)
|
||||
|
||||
type AgentLoop struct {
|
||||
bus *bus.MessageBus
|
||||
provider providers.LLMProvider
|
||||
workspace string
|
||||
model string
|
||||
maxIterations int
|
||||
sessions *session.SessionManager
|
||||
contextBuilder *ContextBuilder
|
||||
tools *tools.ToolRegistry
|
||||
compactionEnabled bool
|
||||
compactionTrigger int
|
||||
compactionKeepRecent int
|
||||
heartbeatAckMaxChars int
|
||||
memoryRecallKeywords []string
|
||||
noResponseFallback string
|
||||
thinkOnlyFallback string
|
||||
langUsage string
|
||||
langInvalid string
|
||||
langUpdatedTemplate string
|
||||
runtimeCompactionNote string
|
||||
startupCompactionNote string
|
||||
systemRewriteTemplate string
|
||||
audit *triggerAudit
|
||||
running bool
|
||||
intentMu sync.RWMutex
|
||||
intentHints map[string]string
|
||||
sessionScheduler *SessionScheduler
|
||||
providerNames []string
|
||||
providerPool map[string]providers.LLMProvider
|
||||
providerResponses map[string]config.ProviderResponsesConfig
|
||||
telegramStreaming bool
|
||||
ekg *ekg.Engine
|
||||
providerMu sync.RWMutex
|
||||
sessionProvider map[string]string
|
||||
streamMu sync.Mutex
|
||||
sessionStreamed map[string]bool
|
||||
bus *bus.MessageBus
|
||||
provider providers.LLMProvider
|
||||
workspace string
|
||||
model string
|
||||
maxIterations int
|
||||
sessions *session.SessionManager
|
||||
contextBuilder *ContextBuilder
|
||||
tools *tools.ToolRegistry
|
||||
compactionEnabled bool
|
||||
compactionTrigger int
|
||||
compactionKeepRecent int
|
||||
heartbeatAckMaxChars int
|
||||
heartbeatAckToken string
|
||||
audit *triggerAudit
|
||||
running bool
|
||||
sessionScheduler *SessionScheduler
|
||||
providerNames []string
|
||||
providerPool map[string]providers.LLMProvider
|
||||
providerResponses map[string]config.ProviderResponsesConfig
|
||||
telegramStreaming bool
|
||||
ekg *ekg.Engine
|
||||
providerMu sync.RWMutex
|
||||
sessionProvider map[string]string
|
||||
streamMu sync.Mutex
|
||||
sessionStreamed map[string]bool
|
||||
}
|
||||
|
||||
// StartupCompactionReport provides startup memory/session maintenance stats.
|
||||
@@ -181,7 +171,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
subagentManager := tools.NewSubagentManager(provider, workspace, msgBus, orchestrator)
|
||||
spawnTool := tools.NewSpawnTool(subagentManager)
|
||||
toolsRegistry.Register(spawnTool)
|
||||
toolsRegistry.Register(tools.NewSubagentsTool(subagentManager, cfg.Agents.Defaults.Texts.SubagentsNone, cfg.Agents.Defaults.Texts.UnsupportedAction))
|
||||
toolsRegistry.Register(tools.NewSubagentsTool(subagentManager))
|
||||
toolsRegistry.Register(tools.NewSessionsTool(
|
||||
func(limit int) []tools.SessionInfo {
|
||||
sessions := alSessionListForTool(sessionsManager, limit)
|
||||
@@ -194,8 +184,6 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
}
|
||||
return h
|
||||
},
|
||||
cfg.Agents.Defaults.Texts.SessionsNone,
|
||||
cfg.Agents.Defaults.Texts.UnsupportedAction,
|
||||
))
|
||||
|
||||
// Register edit file tool
|
||||
@@ -220,36 +208,27 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
toolsRegistry.Register(tools.NewSystemInfoTool())
|
||||
|
||||
loop := &AgentLoop{
|
||||
bus: msgBus,
|
||||
provider: provider,
|
||||
workspace: workspace,
|
||||
model: provider.GetDefaultModel(),
|
||||
maxIterations: cfg.Agents.Defaults.MaxToolIterations,
|
||||
sessions: sessionsManager,
|
||||
contextBuilder: NewContextBuilder(workspace, func() []string { return toolsRegistry.GetSummaries() }),
|
||||
tools: toolsRegistry,
|
||||
compactionEnabled: cfg.Agents.Defaults.ContextCompaction.Enabled,
|
||||
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,
|
||||
langUsage: cfg.Agents.Defaults.Texts.LangUsage,
|
||||
langInvalid: cfg.Agents.Defaults.Texts.LangInvalid,
|
||||
langUpdatedTemplate: cfg.Agents.Defaults.Texts.LangUpdatedTemplate,
|
||||
runtimeCompactionNote: cfg.Agents.Defaults.Texts.RuntimeCompactionNote,
|
||||
startupCompactionNote: cfg.Agents.Defaults.Texts.StartupCompactionNote,
|
||||
systemRewriteTemplate: cfg.Agents.Defaults.Texts.SystemRewriteTemplate,
|
||||
audit: newTriggerAudit(workspace),
|
||||
running: false,
|
||||
intentHints: map[string]string{},
|
||||
sessionScheduler: NewSessionScheduler(0),
|
||||
ekg: ekg.New(workspace),
|
||||
sessionProvider: map[string]string{},
|
||||
sessionStreamed: map[string]bool{},
|
||||
providerResponses: map[string]config.ProviderResponsesConfig{},
|
||||
telegramStreaming: cfg.Channels.Telegram.Streaming,
|
||||
bus: msgBus,
|
||||
provider: provider,
|
||||
workspace: workspace,
|
||||
model: provider.GetDefaultModel(),
|
||||
maxIterations: cfg.Agents.Defaults.MaxToolIterations,
|
||||
sessions: sessionsManager,
|
||||
contextBuilder: NewContextBuilder(workspace, func() []string { return toolsRegistry.GetSummaries() }),
|
||||
tools: toolsRegistry,
|
||||
compactionEnabled: cfg.Agents.Defaults.ContextCompaction.Enabled,
|
||||
compactionTrigger: cfg.Agents.Defaults.ContextCompaction.TriggerMessages,
|
||||
compactionKeepRecent: cfg.Agents.Defaults.ContextCompaction.KeepRecentMessages,
|
||||
heartbeatAckMaxChars: cfg.Agents.Defaults.Heartbeat.AckMaxChars,
|
||||
heartbeatAckToken: loadHeartbeatAckToken(workspace),
|
||||
audit: newTriggerAudit(workspace),
|
||||
running: false,
|
||||
sessionScheduler: NewSessionScheduler(0),
|
||||
ekg: ekg.New(workspace),
|
||||
sessionProvider: map[string]string{},
|
||||
sessionStreamed: map[string]bool{},
|
||||
providerResponses: map[string]config.ProviderResponsesConfig{},
|
||||
telegramStreaming: cfg.Channels.Telegram.Streaming,
|
||||
}
|
||||
// Initialize provider fallback chain (primary + proxy_fallbacks).
|
||||
loop.providerPool = map[string]providers.LLMProvider{}
|
||||
@@ -560,22 +539,27 @@ func (al *AgentLoop) appendTaskAuditEvent(taskID string, msg bus.InboundMessage,
|
||||
}
|
||||
|
||||
func sessionShardCount() int {
|
||||
if v := strings.TrimSpace(os.Getenv("CLAWGO_SESSION_SHARDS")); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
if n > 64 {
|
||||
return 64
|
||||
}
|
||||
return n
|
||||
}
|
||||
// Keep ~20% CPU headroom for system/background work, then use a
|
||||
// sub-linear curve to avoid oversharding on high-core machines.
|
||||
n := runtime.GOMAXPROCS(0)
|
||||
if n <= 0 {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
n := runtime.NumCPU()
|
||||
if n < 2 {
|
||||
n = 2
|
||||
if n <= 0 {
|
||||
return 2
|
||||
}
|
||||
if n > 16 {
|
||||
n = 16
|
||||
budget := int(math.Floor(float64(n) * 0.8))
|
||||
if budget < 1 {
|
||||
budget = 1
|
||||
}
|
||||
return n
|
||||
shards := int(math.Round(math.Sqrt(float64(budget)) * 2.2))
|
||||
if shards < 2 {
|
||||
shards = 2
|
||||
}
|
||||
if shards > 12 {
|
||||
shards = 12
|
||||
}
|
||||
return shards
|
||||
}
|
||||
|
||||
func sessionShardIndex(sessionKey string, shardCount int) int {
|
||||
@@ -617,7 +601,11 @@ func (al *AgentLoop) shouldSuppressOutbound(msg bus.InboundMessage, response str
|
||||
}
|
||||
|
||||
r := strings.TrimSpace(response)
|
||||
if !strings.HasPrefix(r, "HEARTBEAT_OK") {
|
||||
ackToken := strings.TrimSpace(al.heartbeatAckToken)
|
||||
if ackToken == "" {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(r, ackToken) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -628,6 +616,43 @@ func (al *AgentLoop) shouldSuppressOutbound(msg bus.InboundMessage, response str
|
||||
return len(r) <= maxChars
|
||||
}
|
||||
|
||||
func loadHeartbeatAckToken(workspace string) string {
|
||||
workspace = strings.TrimSpace(workspace)
|
||||
if workspace == "" {
|
||||
return ""
|
||||
}
|
||||
parse := func(text string) string {
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
t := strings.TrimSpace(line)
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
raw := strings.TrimLeft(t, "-*# ")
|
||||
lower := strings.ToLower(raw)
|
||||
if !strings.HasPrefix(lower, "heartbeat_ack_token:") {
|
||||
continue
|
||||
}
|
||||
v := strings.TrimSpace(raw[len("heartbeat_ack_token:"):])
|
||||
v = strings.Trim(v, "`\"' ")
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if b, err := os.ReadFile(filepath.Join(workspace, "AGENTS.md")); err == nil {
|
||||
if token := parse(string(b)); token != "" {
|
||||
return token
|
||||
}
|
||||
}
|
||||
if b, err := os.ReadFile(filepath.Join(workspace, "HEARTBEAT.md")); err == nil {
|
||||
if token := parse(string(b)); token != "" {
|
||||
return token
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (al *AgentLoop) prepareOutbound(msg bus.InboundMessage, response string) (bus.OutboundMessage, bool) {
|
||||
if shouldDropNoReply(response) {
|
||||
return bus.OutboundMessage{}, false
|
||||
@@ -700,64 +725,18 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
return al.processSystemMessage(ctx, msg)
|
||||
}
|
||||
|
||||
// Explicit language command: /lang <code>
|
||||
if strings.HasPrefix(msg.Content, "/lang") {
|
||||
parts := strings.Fields(msg.Content)
|
||||
if len(parts) < 2 {
|
||||
preferred, last := al.sessions.GetLanguagePreferences(msg.SessionKey)
|
||||
if preferred == "" {
|
||||
preferred = "(auto)"
|
||||
}
|
||||
if last == "" {
|
||||
last = "(none)"
|
||||
}
|
||||
usage := strings.TrimSpace(al.langUsage)
|
||||
if usage == "" {
|
||||
usage = "Usage: /lang <code>"
|
||||
}
|
||||
return fmt.Sprintf("%s\nCurrent preferred: %s\nLast detected: %s", usage, preferred, last), nil
|
||||
}
|
||||
lang := normalizeLang(parts[1])
|
||||
if lang == "" {
|
||||
invalid := strings.TrimSpace(al.langInvalid)
|
||||
if invalid == "" {
|
||||
invalid = "Invalid language code."
|
||||
}
|
||||
return invalid, nil
|
||||
}
|
||||
al.sessions.SetPreferredLanguage(msg.SessionKey, lang)
|
||||
al.sessions.Save(al.sessions.GetOrCreate(msg.SessionKey))
|
||||
tpl := strings.TrimSpace(al.langUpdatedTemplate)
|
||||
if tpl == "" {
|
||||
tpl = "Language preference updated to %s"
|
||||
}
|
||||
return fmt.Sprintf(tpl, lang), nil
|
||||
}
|
||||
|
||||
history := al.sessions.GetHistory(msg.SessionKey)
|
||||
summary := al.sessions.GetSummary(msg.SessionKey)
|
||||
memoryRecallUsed := false
|
||||
memoryRecallText := ""
|
||||
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)
|
||||
summary = strings.TrimSpace(summary + "\n\n[Memory Recall]\n" + memoryRecallText)
|
||||
}
|
||||
}
|
||||
if explicitPref := ExtractLanguagePreference(msg.Content); explicitPref != "" {
|
||||
al.sessions.SetPreferredLanguage(msg.SessionKey, explicitPref)
|
||||
}
|
||||
preferredLang, lastLang := al.sessions.GetLanguagePreferences(msg.SessionKey)
|
||||
responseLang := DetectResponseLanguage(msg.Content, preferredLang, lastLang)
|
||||
|
||||
al.updateIntentHint(msg.SessionKey, msg.Content)
|
||||
effectiveUserContent := al.applyIntentHint(msg.SessionKey, msg.Content)
|
||||
|
||||
messages := al.contextBuilder.BuildMessages(
|
||||
history,
|
||||
summary,
|
||||
effectiveUserContent,
|
||||
msg.Content,
|
||||
nil,
|
||||
msg.Channel,
|
||||
msg.ChatID,
|
||||
@@ -772,15 +751,6 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
if maxAllowed < 1 {
|
||||
maxAllowed = 1
|
||||
}
|
||||
// CLAWGO_MAX_TOOL_ITERATIONS:
|
||||
// 0 or unset => no fixed cap, keep extending while tool chain progresses
|
||||
// >0 => explicit ceiling
|
||||
hardCap := 0
|
||||
if v := os.Getenv("CLAWGO_MAX_TOOL_ITERATIONS"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
|
||||
hardCap = n
|
||||
}
|
||||
}
|
||||
for iteration < maxAllowed {
|
||||
iteration++
|
||||
|
||||
@@ -910,15 +880,9 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
al.sessions.AddMessageFull(msg.SessionKey, assistantMsg)
|
||||
|
||||
hasToolActivity = true
|
||||
if hardCap > 0 {
|
||||
if maxAllowed < hardCap {
|
||||
maxAllowed = hardCap
|
||||
}
|
||||
} else {
|
||||
// No fixed cap: extend rolling window as long as tools keep chaining.
|
||||
if maxAllowed < iteration+al.maxIterations {
|
||||
maxAllowed = iteration + al.maxIterations
|
||||
}
|
||||
// Extend rolling window as long as tools keep chaining.
|
||||
if maxAllowed < iteration+al.maxIterations {
|
||||
maxAllowed = iteration + al.maxIterations
|
||||
}
|
||||
for _, tc := range response.ToolCalls {
|
||||
// Log tool call with arguments preview
|
||||
@@ -932,7 +896,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
})
|
||||
|
||||
execArgs := withToolContextArgs(tc.Name, tc.Arguments, msg.Channel, msg.ChatID)
|
||||
result, err := al.tools.Execute(ctx, tc.Name, execArgs)
|
||||
result, err := al.executeToolCall(ctx, tc.Name, execArgs, msg.Channel, msg.ChatID)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("Error: %v", err)
|
||||
}
|
||||
@@ -956,17 +920,6 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
finalContent = forced.Content
|
||||
}
|
||||
}
|
||||
if finalContent == "" {
|
||||
if hasToolActivity && len(lastToolOutputs) > 0 {
|
||||
finalContent = "我已执行完成,关键信息如下:\n- " + strings.Join(lastToolOutputs, "\n- ")
|
||||
} else {
|
||||
fallback := strings.TrimSpace(al.noResponseFallback)
|
||||
if fallback == "" {
|
||||
fallback = "在的,我刚刚这条回复丢了。请再说一次,我马上处理。"
|
||||
}
|
||||
finalContent = fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out <think>...</think> content from user-facing response
|
||||
// Keep full content in debug logs if needed, but remove from final output
|
||||
@@ -977,19 +930,10 @@ 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 {
|
||||
fallback := strings.TrimSpace(al.thinkOnlyFallback)
|
||||
if fallback == "" {
|
||||
fallback = "Thinking process completed."
|
||||
}
|
||||
userContent = fallback
|
||||
userContent = "Thinking process completed."
|
||||
}
|
||||
}
|
||||
|
||||
if memoryRecallUsed && !strings.Contains(strings.ToLower(userContent), "source:") {
|
||||
if src := extractFirstSourceLine(memoryRecallText); src != "" {
|
||||
userContent = strings.TrimSpace(userContent + "\n\n" + src)
|
||||
}
|
||||
}
|
||||
al.sessions.AddMessage(msg.SessionKey, "user", msg.Content)
|
||||
|
||||
// Persist full assistant response (including reasoning/tool flow outcomes when present).
|
||||
@@ -1075,55 +1019,6 @@ func (al *AgentLoop) appendDailySummaryLog(msg bus.InboundMessage, response stri
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) updateIntentHint(sessionKey, content string) {
|
||||
content = strings.TrimSpace(content)
|
||||
if sessionKey == "" || content == "" {
|
||||
return
|
||||
}
|
||||
lower := strings.ToLower(content)
|
||||
|
||||
// Cron natural-language intent: avoid searching project files for user timer ops.
|
||||
if strings.Contains(lower, "cron") || strings.Contains(lower, "schedule") || strings.Contains(lower, "timer") || strings.Contains(lower, "reminder") {
|
||||
hint := "Prioritize the cron tool for timer operations: list=action=list; delete=action=delete(id); enable/disable=action=enable/disable. Do not switch to grepping project files for cron text."
|
||||
al.intentMu.Lock()
|
||||
al.intentHints[sessionKey] = hint + " User details=" + content
|
||||
al.intentMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(lower, "commit") && !strings.Contains(lower, "push") {
|
||||
if strings.HasPrefix(content, "1.") || strings.HasPrefix(content, "2.") {
|
||||
al.intentMu.Lock()
|
||||
if prev := strings.TrimSpace(al.intentHints[sessionKey]); prev != "" {
|
||||
al.intentHints[sessionKey] = prev + " | " + content
|
||||
}
|
||||
al.intentMu.Unlock()
|
||||
}
|
||||
return
|
||||
}
|
||||
hint := "Execute as one transaction: complete commit+push in one pass with branch/scope confirmation."
|
||||
if strings.Contains(lower, "all branches") {
|
||||
hint += " Scope=all branches."
|
||||
}
|
||||
al.intentMu.Lock()
|
||||
al.intentHints[sessionKey] = hint + " User details=" + content
|
||||
al.intentMu.Unlock()
|
||||
}
|
||||
|
||||
func (al *AgentLoop) applyIntentHint(sessionKey, content string) string {
|
||||
al.intentMu.RLock()
|
||||
hint := strings.TrimSpace(al.intentHints[sessionKey])
|
||||
al.intentMu.RUnlock()
|
||||
if hint == "" {
|
||||
return content
|
||||
}
|
||||
lower := strings.ToLower(strings.TrimSpace(content))
|
||||
if strings.Contains(lower, "commit") || strings.Contains(lower, "push") || strings.HasPrefix(content, "1.") || strings.HasPrefix(content, "2.") || strings.Contains(lower, "cron") || strings.Contains(lower, "schedule") || strings.Contains(lower, "timer") || strings.Contains(lower, "reminder") {
|
||||
return "[Intent Slot]\n" + hint + "\n\n[User Message]\n" + content
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMessage) (string, error) {
|
||||
// Verify this is a system message
|
||||
if msg.Channel != "system" {
|
||||
@@ -1136,8 +1031,6 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
|
||||
"chat_id": msg.ChatID,
|
||||
})
|
||||
|
||||
msg.Content = rewriteSystemMessageContent(msg.Content, al.systemRewriteTemplate)
|
||||
|
||||
// Parse origin from chat_id (format: "channel:chat_id")
|
||||
var originChannel, originChatID string
|
||||
if idx := strings.Index(msg.ChatID, ":"); idx > 0 {
|
||||
@@ -1246,7 +1139,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
|
||||
|
||||
for _, tc := range response.ToolCalls {
|
||||
execArgs := withToolContextArgs(tc.Name, tc.Arguments, originChannel, originChatID)
|
||||
result, err := al.tools.Execute(ctx, tc.Name, execArgs)
|
||||
result, err := al.executeToolCall(ctx, tc.Name, execArgs, originChannel, originChatID)
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("Error: %v", err)
|
||||
}
|
||||
@@ -1496,13 +1389,10 @@ func (al *AgentLoop) compactSessionIfNeeded(sessionKey string) {
|
||||
return
|
||||
}
|
||||
removed := len(h) - keepRecent
|
||||
tpl := strings.TrimSpace(al.runtimeCompactionNote)
|
||||
if tpl == "" {
|
||||
tpl = "[runtime-compaction] removed %d old messages, kept %d recent messages"
|
||||
}
|
||||
tpl := "[runtime-compaction] removed %d old messages, kept %d recent messages"
|
||||
note := fmt.Sprintf(tpl, removed, keepRecent)
|
||||
if al.sessions.CompactSession(sessionKey, keepRecent, note) {
|
||||
al.sessions.Save(al.sessions.GetOrCreate(sessionKey))
|
||||
_ = al.sessions.Save(al.sessions.GetOrCreate(sessionKey))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1538,10 +1428,7 @@ func (al *AgentLoop) RunStartupSelfCheckAllSessions(ctx context.Context) Startup
|
||||
}
|
||||
|
||||
removed := len(history) - keepRecent
|
||||
tpl := strings.TrimSpace(al.startupCompactionNote)
|
||||
if tpl == "" {
|
||||
tpl = "[startup-compaction] removed %d old messages, kept %d recent messages"
|
||||
}
|
||||
tpl := "[startup-compaction] removed %d old messages, kept %d recent messages"
|
||||
note := fmt.Sprintf(tpl, removed, keepRecent)
|
||||
if al.sessions.CompactSession(key, keepRecent, note) {
|
||||
al.sessions.Save(al.sessions.GetOrCreate(key))
|
||||
@@ -1556,10 +1443,10 @@ func (al *AgentLoop) GetStartupInfo() map[string]interface{} {
|
||||
info := make(map[string]interface{})
|
||||
|
||||
// Tools info
|
||||
tools := al.tools.List()
|
||||
_tools := al.tools.List()
|
||||
info["tools"] = map[string]interface{}{
|
||||
"count": len(tools),
|
||||
"names": tools,
|
||||
"count": len(_tools),
|
||||
"names": _tools,
|
||||
}
|
||||
|
||||
// Skills info
|
||||
@@ -1666,21 +1553,46 @@ func withToolContextArgs(toolName string, args map[string]interface{}, channel,
|
||||
return next
|
||||
}
|
||||
|
||||
func shouldRecallMemory(text string, keywords []string) bool {
|
||||
s := strings.ToLower(strings.TrimSpace(text))
|
||||
if s == "" {
|
||||
func (al *AgentLoop) executeToolCall(ctx context.Context, toolName string, args map[string]interface{}, currentChannel, currentChatID string) (string, error) {
|
||||
if shouldSuppressSelfMessageSend(toolName, args, currentChannel, currentChatID) {
|
||||
return "Suppressed message tool self-send in current chat; assistant will reply via normal outbound.", nil
|
||||
}
|
||||
return al.tools.Execute(ctx, toolName, args)
|
||||
}
|
||||
|
||||
func shouldSuppressSelfMessageSend(toolName string, args map[string]interface{}, currentChannel, currentChatID string) bool {
|
||||
if strings.TrimSpace(toolName) != "message" {
|
||||
return false
|
||||
}
|
||||
if len(keywords) == 0 {
|
||||
keywords = []string{"remember", "preference", "todo", "decision", "date", "when did", "what did we"}
|
||||
action, _ := args["action"].(string)
|
||||
action = strings.ToLower(strings.TrimSpace(action))
|
||||
if action == "" {
|
||||
action = "send"
|
||||
}
|
||||
for _, k := range keywords {
|
||||
kk := strings.ToLower(strings.TrimSpace(k))
|
||||
if kk != "" && strings.Contains(s, kk) {
|
||||
return true
|
||||
}
|
||||
if action != "send" {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
|
||||
targetChannel, targetChat := resolveMessageToolTarget(args, currentChannel, currentChatID)
|
||||
return targetChannel == strings.TrimSpace(currentChannel) && targetChat == strings.TrimSpace(currentChatID)
|
||||
}
|
||||
|
||||
func resolveMessageToolTarget(args map[string]interface{}, fallbackChannel, fallbackChatID string) (string, string) {
|
||||
channel, _ := args["channel"].(string)
|
||||
channel = strings.TrimSpace(channel)
|
||||
if channel == "" {
|
||||
channel = strings.TrimSpace(fallbackChannel)
|
||||
}
|
||||
|
||||
chatID, _ := args["chat_id"].(string)
|
||||
if to, _ := args["to"].(string); strings.TrimSpace(to) != "" {
|
||||
chatID = to
|
||||
}
|
||||
chatID = strings.TrimSpace(chatID)
|
||||
if chatID == "" {
|
||||
chatID = strings.TrimSpace(fallbackChatID)
|
||||
}
|
||||
return channel, chatID
|
||||
}
|
||||
|
||||
func extractFirstSourceLine(text string) string {
|
||||
@@ -1747,25 +1659,6 @@ func parseReplyTag(text string, currentMessageID string) (content string, replyT
|
||||
return text, ""
|
||||
}
|
||||
|
||||
func rewriteSystemMessageContent(content, template string) string {
|
||||
c := strings.TrimSpace(content)
|
||||
if !strings.HasPrefix(c, "[System Message]") {
|
||||
return content
|
||||
}
|
||||
body := strings.TrimSpace(strings.TrimPrefix(c, "[System Message]"))
|
||||
if body == "" {
|
||||
return "Please summarize the system event in concise user-facing language."
|
||||
}
|
||||
tpl := strings.TrimSpace(template)
|
||||
if tpl == "" {
|
||||
tpl = "Rewrite the following internal system update in concise user-facing language:\n\n%s"
|
||||
}
|
||||
if strings.Contains(tpl, "%s") {
|
||||
return fmt.Sprintf(tpl, body)
|
||||
}
|
||||
return tpl + "\n\n" + body
|
||||
}
|
||||
|
||||
func alSessionListForTool(sm *session.SessionManager, limit int) []tools.SessionInfo {
|
||||
items := sm.List(limit)
|
||||
out := make([]tools.SessionInfo, 0, len(items))
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"clawgo/pkg/bus"
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func TestInjectResponsesMediaParts(t *testing.T) {
|
||||
msgs := []providers.Message{
|
||||
{Role: "system", Content: "sys"},
|
||||
{Role: "user", Content: "look"},
|
||||
}
|
||||
media := []string{"https://example.com/a.png"}
|
||||
items := []bus.MediaItem{
|
||||
{Type: "image", Ref: "file_img_1"},
|
||||
{Type: "document", Ref: "file_doc_1"},
|
||||
}
|
||||
|
||||
got := injectResponsesMediaParts(msgs, media, items)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("unexpected messages length: %#v", got)
|
||||
}
|
||||
parts := got[1].ContentParts
|
||||
if len(parts) != 3 {
|
||||
t.Fatalf("expected 3 content parts, got %#v", parts)
|
||||
}
|
||||
if parts[0].Type != "input_text" || parts[0].Text != "look" {
|
||||
t.Fatalf("expected first part to preserve input text, got %#v", parts[0])
|
||||
}
|
||||
if parts[1].Type != "input_image" || parts[1].FileID != "file_img_1" {
|
||||
t.Fatalf("expected image media item mapped to file_id, got %#v", parts[1])
|
||||
}
|
||||
if parts[2].Type != "input_file" || parts[2].FileID != "file_doc_1" {
|
||||
t.Fatalf("expected document media item mapped to input_file file_id, got %#v", parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResponsesOptionsFromConfig(t *testing.T) {
|
||||
al := &AgentLoop{
|
||||
providerNames: []string{"proxy"},
|
||||
providerResponses: map[string]config.ProviderResponsesConfig{
|
||||
"proxy": {
|
||||
WebSearchEnabled: true,
|
||||
WebSearchContextSize: "high",
|
||||
FileSearchVectorStoreIDs: []string{"vs_1", "vs_2"},
|
||||
FileSearchMaxNumResults: 8,
|
||||
Include: []string{"output_text", "tool_calls"},
|
||||
StreamIncludeUsage: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := al.buildResponsesOptions("session-a", 1024, 0.2)
|
||||
if opts["max_tokens"] != int64(1024) {
|
||||
t.Fatalf("max_tokens mismatch: %#v", opts["max_tokens"])
|
||||
}
|
||||
if opts["temperature"] != 0.2 {
|
||||
t.Fatalf("temperature mismatch: %#v", opts["temperature"])
|
||||
}
|
||||
toolsRaw, ok := opts["responses_tools"].([]map[string]interface{})
|
||||
if !ok || len(toolsRaw) != 2 {
|
||||
t.Fatalf("expected two built-in response tools, got %#v", opts["responses_tools"])
|
||||
}
|
||||
if toolsRaw[0]["type"] != "web_search" {
|
||||
t.Fatalf("expected web_search tool first, got %#v", toolsRaw[0])
|
||||
}
|
||||
if toolsRaw[1]["type"] != "file_search" {
|
||||
t.Fatalf("expected file_search tool second, got %#v", toolsRaw[1])
|
||||
}
|
||||
if _, ok := opts["responses_include"]; !ok {
|
||||
t.Fatalf("expected responses_include in options")
|
||||
}
|
||||
if _, ok := opts["responses_stream_options"]; !ok {
|
||||
t.Fatalf("expected responses_stream_options in options")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectResponsesMediaParts_SkipsLocalPathsForResponsesContentParts(t *testing.T) {
|
||||
msgs := []providers.Message{
|
||||
{Role: "user", Content: "check local media"},
|
||||
}
|
||||
items := []bus.MediaItem{
|
||||
{Type: "image", Path: "/tmp/a.png"},
|
||||
{Type: "document", Path: "/tmp/a.pdf"},
|
||||
}
|
||||
got := injectResponsesMediaParts(msgs, nil, items)
|
||||
if len(got[0].ContentParts) != 1 {
|
||||
t.Fatalf("expected only input_text for local files, got %#v", got[0].ContentParts)
|
||||
}
|
||||
if got[0].ContentParts[0].Type != "input_text" {
|
||||
t.Fatalf("expected input_text only, got %#v", got[0].ContentParts[0])
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/bus"
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
type recordingProvider struct {
|
||||
mu sync.Mutex
|
||||
calls [][]providers.Message
|
||||
responses []providers.LLMResponse
|
||||
}
|
||||
|
||||
func (p *recordingProvider) Chat(_ context.Context, messages []providers.Message, _ []providers.ToolDefinition, _ string, _ map[string]interface{}) (*providers.LLMResponse, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
cp := make([]providers.Message, len(messages))
|
||||
copy(cp, messages)
|
||||
p.calls = append(p.calls, cp)
|
||||
if len(p.responses) == 0 {
|
||||
resp := providers.LLMResponse{Content: "ok", FinishReason: "stop"}
|
||||
return &resp, nil
|
||||
}
|
||||
resp := p.responses[0]
|
||||
p.responses = p.responses[1:]
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (p *recordingProvider) GetDefaultModel() string { return "test-model" }
|
||||
|
||||
func setupLoop(t *testing.T, rp *recordingProvider) *AgentLoop {
|
||||
t.Helper()
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
|
||||
cfg.Agents.Defaults.MaxToolIterations = 2
|
||||
cfg.Agents.Defaults.ContextCompaction.Enabled = false
|
||||
return NewAgentLoop(cfg, bus.NewMessageBus(), rp, nil)
|
||||
}
|
||||
|
||||
func lastUserContent(msgs []providers.Message) string {
|
||||
for i := len(msgs) - 1; i >= 0; i-- {
|
||||
if msgs[i].Role == "user" {
|
||||
return msgs[i].Content
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func containsUserContent(msgs []providers.Message, needle string) bool {
|
||||
for _, m := range msgs {
|
||||
if m.Role == "user" && m.Content == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestProcessDirect_UsesCallerSessionKey(t *testing.T) {
|
||||
rp := &recordingProvider{}
|
||||
loop := setupLoop(t, rp)
|
||||
|
||||
if _, err := loop.ProcessDirect(context.Background(), "from-session-a", "session-a"); err != nil {
|
||||
t.Fatalf("ProcessDirect session-a failed: %v", err)
|
||||
}
|
||||
if _, err := loop.ProcessDirect(context.Background(), "from-session-b", "session-b"); err != nil {
|
||||
t.Fatalf("ProcessDirect session-b failed: %v", err)
|
||||
}
|
||||
|
||||
if len(rp.calls) != 2 {
|
||||
t.Fatalf("expected 2 provider calls, got %d", len(rp.calls))
|
||||
}
|
||||
second := rp.calls[1]
|
||||
if got := lastUserContent(second); got != "from-session-b" {
|
||||
t.Fatalf("unexpected last user content in second call: %q", got)
|
||||
}
|
||||
if containsUserContent(second, "from-session-a") {
|
||||
t.Fatalf("session-a message leaked into session-b history")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSystemMessage_UsesOriginSessionKey(t *testing.T) {
|
||||
rp := &recordingProvider{}
|
||||
loop := setupLoop(t, rp)
|
||||
|
||||
sys := bus.InboundMessage{Channel: "system", SenderID: "cron", ChatID: "telegram:chat-1", Content: "system task"}
|
||||
if _, err := loop.processMessage(context.Background(), sys); err != nil {
|
||||
t.Fatalf("processMessage(system) failed: %v", err)
|
||||
}
|
||||
if _, err := loop.ProcessDirect(context.Background(), "follow-up", "telegram:chat-1"); err != nil {
|
||||
t.Fatalf("ProcessDirect follow-up failed: %v", err)
|
||||
}
|
||||
|
||||
if len(rp.calls) != 2 {
|
||||
t.Fatalf("expected 2 provider calls, got %d", len(rp.calls))
|
||||
}
|
||||
second := rp.calls[1]
|
||||
want := "[System: cron] " + rewriteSystemMessageContent("system task", loop.systemRewriteTemplate)
|
||||
if !containsUserContent(second, want) {
|
||||
t.Fatalf("expected system marker in follow-up history, want=%q got=%v", want, summarizeUsers(second))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessInbound_SystemMessagePublishesToOriginChannel(t *testing.T) {
|
||||
rp := &recordingProvider{}
|
||||
loop := setupLoop(t, rp)
|
||||
|
||||
in := bus.InboundMessage{Channel: "system", SenderID: "cron", ChatID: "telegram:chat-1", Content: "system task"}
|
||||
loop.processInbound(context.Background(), in)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
out, ok := loop.bus.SubscribeOutbound(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("expected outbound message")
|
||||
}
|
||||
if out.Channel != "telegram" {
|
||||
t.Fatalf("expected outbound channel telegram, got %q", out.Channel)
|
||||
}
|
||||
if out.ChatID != "chat-1" {
|
||||
t.Fatalf("expected outbound chat_id chat-1, got %q", out.ChatID)
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeUsers(msgs []providers.Message) []string {
|
||||
out := []string{}
|
||||
for _, m := range msgs {
|
||||
if m.Role == "user" {
|
||||
out = append(out, fmt.Sprintf("%q", m.Content))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestResolveMessageResourceKeys_FromMetadata(t *testing.T) {
|
||||
loop := &AgentLoop{}
|
||||
msg := &bus.InboundMessage{
|
||||
Content: "do task",
|
||||
Metadata: map[string]string{
|
||||
"resource_keys": "repo:acme/app,file:pkg/a.go",
|
||||
},
|
||||
}
|
||||
keys, cleaned := loop.resolveMessageResourceKeys(msg)
|
||||
if cleaned != "do task" {
|
||||
t.Fatalf("unexpected cleaned content: %q", cleaned)
|
||||
}
|
||||
if len(keys) != 2 {
|
||||
t.Fatalf("unexpected keys: %#v", keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMessageResourceKeys_FromContentDirective(t *testing.T) {
|
||||
loop := &AgentLoop{}
|
||||
msg := &bus.InboundMessage{
|
||||
Content: "[resource_keys: repo:acme/app,file:pkg/a.go]\nplease fix",
|
||||
}
|
||||
keys, cleaned := loop.resolveMessageResourceKeys(msg)
|
||||
if len(keys) != 2 {
|
||||
t.Fatalf("unexpected keys: %#v", keys)
|
||||
}
|
||||
if cleaned != "please fix" {
|
||||
t.Fatalf("unexpected cleaned content: %q", cleaned)
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package agent
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWithToolContextArgsInjectsDefaults(t *testing.T) {
|
||||
args := map[string]interface{}{"message": "hello"}
|
||||
got := withToolContextArgs("message", args, "telegram", "chat-1")
|
||||
if got["channel"] != "telegram" {
|
||||
t.Fatalf("expected channel injected, got %v", got["channel"])
|
||||
}
|
||||
if got["chat_id"] != "chat-1" {
|
||||
t.Fatalf("expected chat_id injected, got %v", got["chat_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithToolContextArgsPreservesExplicitTarget(t *testing.T) {
|
||||
args := map[string]interface{}{"message": "hello", "to": "target-2"}
|
||||
got := withToolContextArgs("message", args, "telegram", "chat-1")
|
||||
if _, ok := got["chat_id"]; ok {
|
||||
t.Fatalf("chat_id should not be injected when 'to' is provided")
|
||||
}
|
||||
if got["to"] != "target-2" {
|
||||
t.Fatalf("expected to preserved, got %v", got["to"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithToolContextArgsSkipsUnrelatedTools(t *testing.T) {
|
||||
args := map[string]interface{}{"query": "x"}
|
||||
got := withToolContextArgs("memory_search", args, "telegram", "chat-1")
|
||||
if len(got) != len(args) {
|
||||
t.Fatalf("expected unchanged args for unrelated tool")
|
||||
}
|
||||
if _, ok := got["channel"]; ok {
|
||||
t.Fatalf("unexpected channel key for unrelated tool")
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/bus"
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/ekg"
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
func TestSplitPlannedSegments_Bullets(t *testing.T) {
|
||||
parts := splitPlannedSegments("- 修复 a.go\n- 补充 b.go 测试")
|
||||
if len(parts) != 2 {
|
||||
t.Fatalf("unexpected parts: %#v", parts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanSessionTasks_Semicolon(t *testing.T) {
|
||||
loop := &AgentLoop{}
|
||||
tasks := loop.planSessionTasks(bus.InboundMessage{Channel: "cli", Content: "修复 pkg/a.go;修复 pkg/b.go"})
|
||||
if len(tasks) != 2 {
|
||||
t.Fatalf("expected 2 tasks, got %#v", tasks)
|
||||
}
|
||||
if tasks[0].Content == tasks[1].Content {
|
||||
t.Fatalf("expected distinct tasks: %#v", tasks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPlannedMessage_AggregatesResults(t *testing.T) {
|
||||
rp := &recordingProvider{responses: []providers.LLMResponse{
|
||||
{Content: "done-a", FinishReason: "stop"},
|
||||
{Content: "done-b", FinishReason: "stop"},
|
||||
}}
|
||||
loop := setupLoop(t, rp)
|
||||
|
||||
resp, err := loop.processPlannedMessage(context.Background(), bus.InboundMessage{
|
||||
Channel: "cli",
|
||||
SenderID: "u",
|
||||
ChatID: "direct",
|
||||
SessionKey: "sess-plan",
|
||||
Content: "修复 pkg/a.go;补充 pkg/b.go 测试",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("processPlannedMessage error: %v", err)
|
||||
}
|
||||
if len(rp.calls) != 2 {
|
||||
t.Fatalf("expected 2 provider calls, got %d", len(rp.calls))
|
||||
}
|
||||
if resp == "" {
|
||||
t.Fatalf("expected aggregate response")
|
||||
}
|
||||
}
|
||||
|
||||
type probeProvider struct {
|
||||
mu sync.Mutex
|
||||
inFlight int
|
||||
maxInFlight int
|
||||
delayPerCall time.Duration
|
||||
responseCount int
|
||||
}
|
||||
|
||||
func (p *probeProvider) Chat(_ context.Context, _ []providers.Message, _ []providers.ToolDefinition, _ string, _ map[string]interface{}) (*providers.LLMResponse, error) {
|
||||
p.mu.Lock()
|
||||
p.inFlight++
|
||||
if p.inFlight > p.maxInFlight {
|
||||
p.maxInFlight = p.inFlight
|
||||
}
|
||||
p.responseCount++
|
||||
p.mu.Unlock()
|
||||
|
||||
time.Sleep(p.delayPerCall)
|
||||
|
||||
p.mu.Lock()
|
||||
n := p.responseCount
|
||||
p.inFlight--
|
||||
p.mu.Unlock()
|
||||
resp := providers.LLMResponse{Content: "done-" + strconv.Itoa(n), FinishReason: "stop"}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (p *probeProvider) GetDefaultModel() string { return "test-model" }
|
||||
|
||||
func TestRunPlannedTasks_NonConflictingKeysCanRunInParallel(t *testing.T) {
|
||||
p := &probeProvider{delayPerCall: 100 * time.Millisecond}
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
|
||||
cfg.Agents.Defaults.MaxToolIterations = 2
|
||||
cfg.Agents.Defaults.ContextCompaction.Enabled = false
|
||||
loop := NewAgentLoop(cfg, bus.NewMessageBus(), p, nil)
|
||||
|
||||
_, err := loop.processPlannedMessage(context.Background(), bus.InboundMessage{
|
||||
Channel: "cli",
|
||||
SenderID: "u",
|
||||
ChatID: "direct",
|
||||
SessionKey: "sess-plan-parallel",
|
||||
Content: "[resource_keys: file:pkg/a.go] 修复 a;[resource_keys: file:pkg/b.go] 修复 b",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("processPlannedMessage error: %v", err)
|
||||
}
|
||||
if p.maxInFlight < 2 {
|
||||
t.Fatalf("expected parallel execution for non-conflicting keys, got maxInFlight=%d", p.maxInFlight)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPlannedTasks_ConflictingKeysMutuallyExclusive(t *testing.T) {
|
||||
p := &probeProvider{delayPerCall: 100 * time.Millisecond}
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Workspace = filepath.Join(t.TempDir(), "workspace")
|
||||
cfg.Agents.Defaults.MaxToolIterations = 2
|
||||
cfg.Agents.Defaults.ContextCompaction.Enabled = false
|
||||
loop := NewAgentLoop(cfg, bus.NewMessageBus(), p, nil)
|
||||
|
||||
_, err := loop.processPlannedMessage(context.Background(), bus.InboundMessage{
|
||||
Channel: "cli",
|
||||
SenderID: "u",
|
||||
ChatID: "direct",
|
||||
SessionKey: "sess-plan-locked",
|
||||
Content: "[resource_keys: file:pkg/a.go] 修复 a;[resource_keys: file:pkg/a.go] 补测试",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("processPlannedMessage error: %v", err)
|
||||
}
|
||||
if p.maxInFlight != 1 {
|
||||
t.Fatalf("expected mutual exclusion for conflicting keys, got maxInFlight=%d", p.maxInFlight)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPlannedTasks_PublishesStepProgress(t *testing.T) {
|
||||
rp := &recordingProvider{responses: []providers.LLMResponse{
|
||||
{Content: "done-a", FinishReason: "stop"},
|
||||
{Content: "done-b", FinishReason: "stop"},
|
||||
}}
|
||||
loop := setupLoop(t, rp)
|
||||
|
||||
_, err := loop.processPlannedMessage(context.Background(), bus.InboundMessage{
|
||||
Channel: "cli",
|
||||
SenderID: "u",
|
||||
ChatID: "direct",
|
||||
SessionKey: "sess-plan-progress",
|
||||
Content: "修复 pkg/a.go;补充 pkg/b.go 测试",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("processPlannedMessage error: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
out1, ok := loop.bus.SubscribeOutbound(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("expected first progress outbound")
|
||||
}
|
||||
out2, ok := loop.bus.SubscribeOutbound(ctx)
|
||||
if !ok {
|
||||
t.Fatalf("expected second progress outbound")
|
||||
}
|
||||
all := out1.Content + "\n" + out2.Content
|
||||
if !strings.Contains(all, "进度 1/2") || !strings.Contains(all, "进度 2/2") {
|
||||
t.Fatalf("unexpected progress outputs:\n%s", all)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRecentRelatedErrorEvent(t *testing.T) {
|
||||
ws := filepath.Join(t.TempDir(), "workspace")
|
||||
_ = os.MkdirAll(filepath.Join(ws, "memory"), 0o755)
|
||||
line := `{"task_id":"t1","status":"error","log":"open /tmp/a.go failed","input_preview":"修复 pkg/a.go 的读取错误","source":"direct","channel":"cli"}`
|
||||
if err := os.WriteFile(filepath.Join(ws, "memory", "task-audit.jsonl"), []byte(line+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("write audit: %v", err)
|
||||
}
|
||||
loop := &AgentLoop{workspace: ws, ekg: ekg.New(ws)}
|
||||
loop.ekg.Record(ekg.Event{TaskID: "t1", Status: "error", Log: "open /tmp/a.go failed"})
|
||||
loop.ekg.Record(ekg.Event{TaskID: "t1", Status: "error", Log: "open /tmp/a.go failed"})
|
||||
loop.ekg.Record(ekg.Event{TaskID: "t1", Status: "error", Log: "open /tmp/a.go failed"})
|
||||
|
||||
ev, ok := loop.findRecentRelatedErrorEvent("请修复 pkg/a.go 的读取问题")
|
||||
if !ok {
|
||||
t.Fatalf("expected matched recent error event")
|
||||
}
|
||||
if ev.TaskID != "t1" {
|
||||
t.Fatalf("unexpected task id: %s", ev.TaskID)
|
||||
}
|
||||
if hint := loop.ekgHintForTask(plannedTask{Content: "修复 pkg/a.go"}); hint == "" {
|
||||
t.Fatalf("expected non-empty ekg hint")
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSessionSchedulerConflictSerializes(t *testing.T) {
|
||||
s := NewSessionScheduler(4)
|
||||
release1, err := s.Acquire(context.Background(), "sess-a", []string{"file:a.go"})
|
||||
if err != nil {
|
||||
t.Fatalf("acquire first: %v", err)
|
||||
}
|
||||
defer release1()
|
||||
|
||||
acquired2 := make(chan struct{}, 1)
|
||||
done2 := make(chan struct{}, 1)
|
||||
go func() {
|
||||
defer func() { done2 <- struct{}{} }()
|
||||
release2, err := s.Acquire(context.Background(), "sess-a", []string{"file:a.go"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
acquired2 <- struct{}{}
|
||||
release2()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-acquired2:
|
||||
t.Fatalf("second conflicting run should wait")
|
||||
case <-time.After(80 * time.Millisecond):
|
||||
}
|
||||
|
||||
release1()
|
||||
|
||||
select {
|
||||
case <-acquired2:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("second run should acquire after release")
|
||||
}
|
||||
<-done2
|
||||
}
|
||||
|
||||
func TestSessionSchedulerNonConflictingCanRunInParallel(t *testing.T) {
|
||||
s := NewSessionScheduler(4)
|
||||
release1, err := s.Acquire(context.Background(), "sess-a", []string{"file:a.go"})
|
||||
if err != nil {
|
||||
t.Fatalf("acquire first: %v", err)
|
||||
}
|
||||
defer release1()
|
||||
|
||||
acquired2 := make(chan struct{}, 1)
|
||||
done2 := make(chan struct{}, 1)
|
||||
go func() {
|
||||
defer func() { done2 <- struct{}{} }()
|
||||
release2, err := s.Acquire(context.Background(), "sess-a", []string{"file:b.go"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
acquired2 <- struct{}{}
|
||||
release2()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-acquired2:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("second non-conflicting run should acquire immediately")
|
||||
}
|
||||
<-done2
|
||||
}
|
||||
|
||||
func TestSessionSchedulerHonorsSessionMaxParallel(t *testing.T) {
|
||||
s := NewSessionScheduler(1)
|
||||
release1, err := s.Acquire(context.Background(), "sess-a", []string{"file:a.go"})
|
||||
if err != nil {
|
||||
t.Fatalf("acquire first: %v", err)
|
||||
}
|
||||
defer release1()
|
||||
|
||||
acquired2 := make(chan struct{}, 1)
|
||||
go func() {
|
||||
release2, err := s.Acquire(context.Background(), "sess-a", []string{"file:b.go"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
acquired2 <- struct{}{}
|
||||
release2()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-acquired2:
|
||||
t.Fatalf("second run should wait when max parallel is 1")
|
||||
case <-time.After(80 * time.Millisecond):
|
||||
}
|
||||
|
||||
release1()
|
||||
select {
|
||||
case <-acquired2:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("second run should continue after first release")
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,6 @@ type Options struct {
|
||||
MaxRoundsWithoutUser int
|
||||
TaskHistoryRetentionDays int
|
||||
AllowedTaskKeywords []string
|
||||
ImportantKeywords []string
|
||||
CompletionTemplate string
|
||||
BlockedTemplate string
|
||||
EKGConsecutiveErrorThreshold int
|
||||
}
|
||||
|
||||
@@ -584,7 +581,7 @@ func (e *Engine) scanTodos() []todoItem {
|
||||
}
|
||||
|
||||
func (e *Engine) dispatchTask(st *taskState) {
|
||||
content := fmt.Sprintf("Autonomy task (Plan -> Act -> Reflect):\n- Goal: %s\n- Requirements: concise progress update\n- If blocked, explain blocker and next retry hint", st.Content)
|
||||
content := fmt.Sprintf("Autonomy task:\n%s", st.Content)
|
||||
e.bus.PublishInbound(bus.InboundMessage{
|
||||
Channel: "system",
|
||||
SenderID: "autonomy",
|
||||
@@ -605,10 +602,7 @@ func (e *Engine) sendCompletionNotification(st *taskState) {
|
||||
if !e.shouldNotify("done:"+st.ID, "") {
|
||||
return
|
||||
}
|
||||
tpl := strings.TrimSpace(e.opts.CompletionTemplate)
|
||||
if tpl == "" {
|
||||
tpl = "✅ Completed: %s\nNext step: reply \"continue %s\" if you want me to proceed."
|
||||
}
|
||||
tpl := "✅ Completed: %s\nNext step: reply \"continue %s\" if you want me to proceed."
|
||||
e.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: e.opts.DefaultNotifyChannel,
|
||||
ChatID: e.opts.DefaultNotifyChatID,
|
||||
@@ -791,10 +785,7 @@ func (e *Engine) sendFailureNotification(st *taskState, reason string) {
|
||||
if !e.shouldNotify("blocked:"+st.ID, reason) {
|
||||
return
|
||||
}
|
||||
tpl := strings.TrimSpace(e.opts.BlockedTemplate)
|
||||
if tpl == "" {
|
||||
tpl = "⚠️ Task blocked: %s\nReason: %s\nSuggestion: reply \"continue %s\" and I will retry from current state."
|
||||
}
|
||||
tpl := "⚠️ Task blocked: %s\nReason: %s\nSuggestion: reply \"continue %s\" and I will retry from current state."
|
||||
e.bus.PublishOutbound(bus.OutboundMessage{
|
||||
Channel: e.opts.DefaultNotifyChannel,
|
||||
ChatID: e.opts.DefaultNotifyChatID,
|
||||
@@ -1479,41 +1470,17 @@ func blockedRetryBackoff(stalls int, minRunIntervalSec int) time.Duration {
|
||||
stalls = 1
|
||||
}
|
||||
base := time.Duration(minRunIntervalSec) * time.Second
|
||||
factor := 1 << min(stalls, 5)
|
||||
factor := 1 << _min(stalls, 5)
|
||||
return time.Duration(factor) * base
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
func _min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (e *Engine) isHighValueCompletion(st *taskState) bool {
|
||||
if st == nil {
|
||||
return false
|
||||
}
|
||||
if priorityWeight(st.Priority) >= 3 {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(st.DueAt) != "" {
|
||||
return true
|
||||
}
|
||||
s := strings.ToLower(st.Content)
|
||||
keywords := e.opts.ImportantKeywords
|
||||
if len(keywords) == 0 {
|
||||
keywords = []string{"urgent", "payment", "release", "deadline", "p0", "asap"}
|
||||
}
|
||||
for _, k := range keywords {
|
||||
kk := strings.ToLower(strings.TrimSpace(k))
|
||||
if kk != "" && strings.Contains(s, kk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shortTask(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) <= 32 {
|
||||
|
||||
@@ -39,7 +39,6 @@ type AgentDefaults struct {
|
||||
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||
Autonomy AutonomyConfig `json:"autonomy"`
|
||||
Texts AgentTextConfig `json:"texts"`
|
||||
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
|
||||
RuntimeControl RuntimeControlConfig `json:"runtime_control"`
|
||||
}
|
||||
@@ -67,24 +66,6 @@ type AutonomyConfig struct {
|
||||
NotifyChatID string `json:"notify_chat_id,omitempty"`
|
||||
}
|
||||
|
||||
type AgentTextConfig struct {
|
||||
NoResponseFallback string `json:"no_response_fallback"`
|
||||
ThinkOnlyFallback string `json:"think_only_fallback"`
|
||||
MemoryRecallKeywords []string `json:"memory_recall_keywords"`
|
||||
LangUsage string `json:"lang_usage"`
|
||||
LangInvalid string `json:"lang_invalid"`
|
||||
LangUpdatedTemplate string `json:"lang_updated_template"`
|
||||
SubagentsNone string `json:"subagents_none"`
|
||||
SessionsNone string `json:"sessions_none"`
|
||||
UnsupportedAction string `json:"unsupported_action"`
|
||||
SystemRewriteTemplate string `json:"system_rewrite_template"`
|
||||
RuntimeCompactionNote string `json:"runtime_compaction_note"`
|
||||
StartupCompactionNote string `json:"startup_compaction_note"`
|
||||
AutonomyImportantKeywords []string `json:"autonomy_important_keywords"`
|
||||
AutonomyCompletionTemplate string `json:"autonomy_completion_template"`
|
||||
AutonomyBlockedTemplate string `json:"autonomy_blocked_template"`
|
||||
}
|
||||
|
||||
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"`
|
||||
@@ -373,7 +354,7 @@ 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.",
|
||||
PromptTemplate: "",
|
||||
},
|
||||
Autonomy: AutonomyConfig{
|
||||
Enabled: false,
|
||||
@@ -393,23 +374,6 @@ func DefaultConfig() *Config {
|
||||
AllowedTaskKeywords: []string{},
|
||||
EKGConsecutiveErrorThreshold: 3,
|
||||
},
|
||||
Texts: AgentTextConfig{
|
||||
NoResponseFallback: "I've completed processing but have no response to give.",
|
||||
ThinkOnlyFallback: "Thinking process completed.",
|
||||
MemoryRecallKeywords: []string{"remember", "previous", "preference", "todo", "decision", "date", "when did", "what did we"},
|
||||
LangUsage: "Usage: /lang <code>",
|
||||
LangInvalid: "Invalid language code.",
|
||||
LangUpdatedTemplate: "Language preference updated to %s",
|
||||
SubagentsNone: "No subagents.",
|
||||
SessionsNone: "No sessions.",
|
||||
UnsupportedAction: "unsupported action",
|
||||
SystemRewriteTemplate: "Rewrite the following internal system update in concise user-facing language:\n\n%s",
|
||||
RuntimeCompactionNote: "[runtime-compaction] removed %d old messages, kept %d recent messages",
|
||||
StartupCompactionNote: "[startup-compaction] removed %d old messages, kept %d recent messages",
|
||||
AutonomyImportantKeywords: []string{"urgent", "payment", "release", "deadline", "p0", "asap"},
|
||||
AutonomyCompletionTemplate: "✅ Completed: %s\nReply \"continue %s\" to proceed to the next step.",
|
||||
AutonomyBlockedTemplate: "⚠️ Task blocked: %s (%s)\nReply \"continue %s\" and I will retry.",
|
||||
},
|
||||
ContextCompaction: ContextCompactionConfig{
|
||||
Enabled: true,
|
||||
Mode: "summary",
|
||||
|
||||
@@ -127,39 +127,6 @@ func Validate(cfg *Config) []error {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.autonomy.ekg_consecutive_error_threshold must be > 0 when enabled=true"))
|
||||
}
|
||||
}
|
||||
texts := cfg.Agents.Defaults.Texts
|
||||
if strings.TrimSpace(texts.NoResponseFallback) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.no_response_fallback must be non-empty"))
|
||||
}
|
||||
if strings.TrimSpace(texts.ThinkOnlyFallback) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.think_only_fallback must be non-empty"))
|
||||
}
|
||||
if len(texts.MemoryRecallKeywords) == 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.memory_recall_keywords must contain at least one keyword"))
|
||||
}
|
||||
if strings.TrimSpace(texts.LangUpdatedTemplate) != "" && !strings.Contains(texts.LangUpdatedTemplate, "%s") {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.lang_updated_template must contain %%s placeholder"))
|
||||
}
|
||||
if strings.TrimSpace(texts.RuntimeCompactionNote) != "" {
|
||||
if strings.Count(texts.RuntimeCompactionNote, "%d") < 2 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.runtime_compaction_note must contain two %%d placeholders"))
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(texts.StartupCompactionNote) != "" {
|
||||
if strings.Count(texts.StartupCompactionNote, "%d") < 2 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.startup_compaction_note must contain two %%d placeholders"))
|
||||
}
|
||||
}
|
||||
if len(texts.AutonomyImportantKeywords) == 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.autonomy_important_keywords must contain at least one keyword"))
|
||||
}
|
||||
if strings.Count(strings.TrimSpace(texts.AutonomyCompletionTemplate), "%s") < 2 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.autonomy_completion_template must contain two %%s placeholders"))
|
||||
}
|
||||
if strings.Count(strings.TrimSpace(texts.AutonomyBlockedTemplate), "%s") < 3 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.texts.autonomy_blocked_template must contain three %%s placeholders"))
|
||||
}
|
||||
|
||||
if cfg.Agents.Defaults.ContextCompaction.Enabled {
|
||||
cc := cfg.Agents.Defaults.ContextCompaction
|
||||
if cc.Mode != "" {
|
||||
|
||||
@@ -77,6 +77,7 @@ func (hs *HeartbeatService) checkHeartbeat() {
|
||||
|
||||
func (hs *HeartbeatService) buildPrompt() string {
|
||||
notesFile := filepath.Join(hs.workspace, "HEARTBEAT.md")
|
||||
agentsFile := filepath.Join(hs.workspace, "AGENTS.md")
|
||||
|
||||
var notes string
|
||||
if data, err := os.ReadFile(notesFile); err == nil {
|
||||
@@ -85,18 +86,50 @@ func (hs *HeartbeatService) buildPrompt() string {
|
||||
notes = candidate
|
||||
}
|
||||
}
|
||||
agents := ""
|
||||
if data, err := os.ReadFile(agentsFile); err == nil {
|
||||
agents = strings.TrimSpace(string(data))
|
||||
}
|
||||
ackToken := heartbeatAckTokenFromText(agents)
|
||||
if ackToken == "" {
|
||||
ackToken = heartbeatAckTokenFromText(notes)
|
||||
}
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04")
|
||||
|
||||
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."
|
||||
if strings.TrimSpace(ackToken) != "" {
|
||||
tpl = fmt.Sprintf("Follow workspace policy in AGENTS.md first, then HEARTBEAT.md. If no action is needed, return %s.", ackToken)
|
||||
} else {
|
||||
tpl = "Follow workspace policy in AGENTS.md first, then HEARTBEAT.md."
|
||||
}
|
||||
}
|
||||
prompt := fmt.Sprintf("%s\n\nCurrent time: %s\n\n%s\n", tpl, now, notes)
|
||||
prompt := fmt.Sprintf("%s\n\nCurrent time: %s\n\n## AGENTS.md\n%s\n\n## HEARTBEAT.md\n%s\n", tpl, now, agents, notes)
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
func heartbeatAckTokenFromText(text string) string {
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
t := strings.TrimSpace(line)
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
raw := strings.TrimLeft(t, "-*# ")
|
||||
lower := strings.ToLower(raw)
|
||||
if !strings.HasPrefix(lower, "heartbeat_ack_token:") {
|
||||
continue
|
||||
}
|
||||
v := strings.TrimSpace(raw[len("heartbeat_ack_token:"):])
|
||||
v = strings.Trim(v, "`\"' ")
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (hs *HeartbeatService) log(message string) {
|
||||
logFile := filepath.Join(hs.workspace, "memory", "heartbeat.log")
|
||||
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
|
||||
@@ -19,14 +19,12 @@ type SessionInfo struct {
|
||||
}
|
||||
|
||||
type SessionsTool struct {
|
||||
listFn func(limit int) []SessionInfo
|
||||
historyFn func(key string, limit int) []providers.Message
|
||||
noSessionsText string
|
||||
unsupportedAction string
|
||||
listFn func(limit int) []SessionInfo
|
||||
historyFn func(key string, limit int) []providers.Message
|
||||
}
|
||||
|
||||
func NewSessionsTool(listFn func(limit int) []SessionInfo, historyFn func(key string, limit int) []providers.Message, noSessionsText, unsupportedAction string) *SessionsTool {
|
||||
return &SessionsTool{listFn: listFn, historyFn: historyFn, noSessionsText: noSessionsText, unsupportedAction: unsupportedAction}
|
||||
func NewSessionsTool(listFn func(limit int) []SessionInfo, historyFn func(key string, limit int) []providers.Message) *SessionsTool {
|
||||
return &SessionsTool{listFn: listFn, historyFn: historyFn}
|
||||
}
|
||||
|
||||
func (t *SessionsTool) Name() string { return "sessions" }
|
||||
@@ -113,7 +111,7 @@ func (t *SessionsTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
}
|
||||
items := t.listFn(limit * 3)
|
||||
if len(items) == 0 {
|
||||
return firstNonEmpty(t.noSessionsText, "No sessions."), nil
|
||||
return "No sessions.", nil
|
||||
}
|
||||
if len(kindFilter) > 0 {
|
||||
filtered := make([]SessionInfo, 0, len(items))
|
||||
@@ -284,13 +282,6 @@ func (t *SessionsTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
}
|
||||
return sb.String(), nil
|
||||
default:
|
||||
return firstNonEmpty(t.unsupportedAction, "unsupported action"), nil
|
||||
return "unsupported action", nil
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmpty(v, fallback string) string {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package tools
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -133,10 +135,19 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) {
|
||||
sm.mu.Unlock()
|
||||
} else {
|
||||
// Original one-shot logic
|
||||
systemPrompt := "You are a subagent. Follow workspace AGENTS.md and complete the task independently."
|
||||
if ws := strings.TrimSpace(sm.workspace); ws != "" {
|
||||
if data, err := os.ReadFile(filepath.Join(ws, "AGENTS.md")); err == nil {
|
||||
txt := strings.TrimSpace(string(data))
|
||||
if txt != "" {
|
||||
systemPrompt = "Workspace policy (AGENTS.md):\n" + txt + "\n\nComplete the given task independently and report the result."
|
||||
}
|
||||
}
|
||||
}
|
||||
messages := []providers.Message{
|
||||
{
|
||||
Role: "system",
|
||||
Content: "You are a subagent. Complete the given task independently and report the result.",
|
||||
Content: systemPrompt,
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
@@ -184,7 +195,7 @@ func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask) {
|
||||
SessionKey: fmt.Sprintf("subagent:%s", task.ID),
|
||||
Content: announceContent,
|
||||
Metadata: map[string]string{
|
||||
"trigger": "subagent",
|
||||
"trigger": "subagent",
|
||||
"subagent_id": task.ID,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,13 +10,11 @@ import (
|
||||
)
|
||||
|
||||
type SubagentsTool struct {
|
||||
manager *SubagentManager
|
||||
noSubagentsText string
|
||||
unsupportedAction string
|
||||
manager *SubagentManager
|
||||
}
|
||||
|
||||
func NewSubagentsTool(m *SubagentManager, noSubagentsText, unsupportedAction string) *SubagentsTool {
|
||||
return &SubagentsTool{manager: m, noSubagentsText: noSubagentsText, unsupportedAction: unsupportedAction}
|
||||
func NewSubagentsTool(m *SubagentManager) *SubagentsTool {
|
||||
return &SubagentsTool{manager: m}
|
||||
}
|
||||
|
||||
func (t *SubagentsTool) Name() string { return "subagents" }
|
||||
@@ -58,7 +56,7 @@ func (t *SubagentsTool) Execute(ctx context.Context, args map[string]interface{}
|
||||
case "list":
|
||||
tasks := t.filterRecent(t.manager.ListTasks(), recentMinutes)
|
||||
if len(tasks) == 0 {
|
||||
return firstNonEmpty(t.noSubagentsText, "No subagents."), nil
|
||||
return "No subagents.", nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Subagents:\n")
|
||||
@@ -71,7 +69,7 @@ func (t *SubagentsTool) Execute(ctx context.Context, args map[string]interface{}
|
||||
if strings.EqualFold(strings.TrimSpace(id), "all") {
|
||||
tasks := t.filterRecent(t.manager.ListTasks(), recentMinutes)
|
||||
if len(tasks) == 0 {
|
||||
return firstNonEmpty(t.noSubagentsText, "No subagents."), nil
|
||||
return "No subagents.", nil
|
||||
}
|
||||
sort.Slice(tasks, func(i, j int) bool { return tasks[i].Created > tasks[j].Created })
|
||||
var sb strings.Builder
|
||||
@@ -94,7 +92,7 @@ func (t *SubagentsTool) Execute(ctx context.Context, args map[string]interface{}
|
||||
if strings.EqualFold(strings.TrimSpace(id), "all") {
|
||||
tasks := t.filterRecent(t.manager.ListTasks(), recentMinutes)
|
||||
if len(tasks) == 0 {
|
||||
return firstNonEmpty(t.noSubagentsText, "No subagents."), nil
|
||||
return "No subagents.", nil
|
||||
}
|
||||
killed := 0
|
||||
for _, task := range tasks {
|
||||
@@ -161,7 +159,7 @@ func (t *SubagentsTool) Execute(ctx context.Context, args map[string]interface{}
|
||||
}
|
||||
return fmt.Sprintf("subagent resumed as %s", label), nil
|
||||
default:
|
||||
return firstNonEmpty(t.unsupportedAction, "unsupported action"), nil
|
||||
return "unsupported action", nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,136 @@
|
||||
# AGENTS.md
|
||||
# AGENT PROMPT
|
||||
|
||||
This workspace is your long-term operating context.
|
||||
|
||||
## Startup Routine
|
||||
1. Read `SOUL.md`
|
||||
2. Read `USER.md`
|
||||
3. Read today's `memory/YYYY-MM-DD.md` (create if missing)
|
||||
4. In direct chats, also read `MEMORY.md`
|
||||
---
|
||||
|
||||
## Memory Policy
|
||||
- Daily log: `memory/YYYY-MM-DD.md`
|
||||
- Long-term memory: `MEMORY.md`
|
||||
- Prefer writing short, structured notes over long paragraphs.
|
||||
### 0) Role & Objective
|
||||
You are an execution-first assistant for this workspace.
|
||||
- Prioritize user conversations over all other work.
|
||||
- Default behavior: **execute first → self-check → deliver results**.
|
||||
- Be concise, decisive, and outcome-oriented.
|
||||
|
||||
## Autonomy Policy
|
||||
- User conversations always have priority.
|
||||
- If user is active, autonomy should pause and resume after idle window.
|
||||
- Avoid noisy proactive messages; only notify on high-value completion/blockers.
|
||||
---
|
||||
|
||||
## Execution Style (Boss Preference)
|
||||
- Default to **execute-first**: self-check, self-verify, then deliver result.
|
||||
- Do **not** present multiple方案 by default; choose the best feasible solution and implement it.
|
||||
- Ask for confirmation only when action is destructive, irreversible, security-sensitive, or affects external systems/accounts.
|
||||
- For normal coding/debug/refactor tasks: investigate and decide internally; keep user interruptions minimal.
|
||||
- Keep responses concise, decisive, and outcome-oriented.
|
||||
### 1) Startup Routine
|
||||
At the start of work, load context in this order:
|
||||
1. `SOUL.md`
|
||||
2. `USER.md`
|
||||
3. `memory/YYYY-MM-DD.md` (create if missing)
|
||||
4. In direct chats, also load `MEMORY.md`
|
||||
|
||||
## Skills & Context Usage
|
||||
- At task start, always load: `SOUL.md`, `USER.md`, today memory, and (direct chat) `MEMORY.md`.
|
||||
- Use installed skills proactively when they clearly match the task; avoid asking user to choose tools unless necessary.
|
||||
- When uncertain between alternatives, run quick local validation and pick one; report final choice and reason briefly.
|
||||
---
|
||||
|
||||
## Safety
|
||||
### 2) Memory Policy
|
||||
- Daily log: write to `memory/YYYY-MM-DD.md`
|
||||
- Long-term memory: write to `MEMORY.md`
|
||||
- Prefer short, structured notes (bullets) over long paragraphs.
|
||||
|
||||
---
|
||||
|
||||
### 3) Autonomy Policy
|
||||
- If the user is active: **pause autonomy** and respond to the user first.
|
||||
- Resume autonomy after an idle window.
|
||||
- Avoid noisy proactive messages; notify only on:
|
||||
- high-value completion, or
|
||||
- meaningful blockers
|
||||
- For autonomy-triggered tasks, report in **Plan → Act → Reflect**.
|
||||
- If blocked, report **blocker + next retry hint**.
|
||||
|
||||
---
|
||||
|
||||
### 4) Execution Style (Boss Preference)
|
||||
- Default: choose the best feasible approach and implement it (no multiple options by default).
|
||||
- Ask for confirmation only when actions are:
|
||||
- destructive / irreversible
|
||||
- security-sensitive
|
||||
- affecting external systems/accounts
|
||||
- For coding/debug/refactor:
|
||||
- investigate and decide internally
|
||||
- minimize interruptions
|
||||
|
||||
---
|
||||
|
||||
### 5) Intent Preferences
|
||||
- Prefer natural-language intent understanding from context; avoid rigid keyword routing.
|
||||
- Commit/push requests: treat as one transaction by default:
|
||||
- finish changes → commit → push → report branch/commit hash/result
|
||||
- Timer/reminder/schedule requests:
|
||||
- prioritize reminder/cron capabilities over repository grep.
|
||||
|
||||
---
|
||||
|
||||
### 6) Skills & Context Loading
|
||||
- At task start, always load:
|
||||
- `SOUL.md`, `USER.md`, today’s `memory/YYYY-MM-DD.md`, plus `MEMORY.md` (direct chat only)
|
||||
- Before responding, select **at most one** most relevant skill and read its `SKILL.md`.
|
||||
- If multiple skills apply: pick the **most specific**; do not bulk-load.
|
||||
- If no skill applies: proceed without loading skill files.
|
||||
- Resolve relative paths relative to the skill directory.
|
||||
- If uncertain: do quick local validation, pick one, briefly state why.
|
||||
|
||||
---
|
||||
|
||||
This workspace is your long-term operating context.
|
||||
|
||||
---
|
||||
|
||||
### 8) Subagent Policy
|
||||
- Subagents inherit this same policy.
|
||||
- Subagents execute independently and return concise summaries.
|
||||
- Subagents must not perform external or destructive actions without explicit approval.
|
||||
|
||||
---
|
||||
|
||||
### 9) System Rewrite Policy
|
||||
When converting internal/system updates to user-facing messages:
|
||||
- keep factual content
|
||||
- remove internal jargon/noise
|
||||
- keep it concise and readable
|
||||
|
||||
---
|
||||
|
||||
### 10) Text Output Rules
|
||||
|
||||
#### 10.1 No-response fallback
|
||||
If processing is complete but there is no direct response to provide, output exactly:
|
||||
- `I have completed processing but have no direct response.`
|
||||
|
||||
#### 10.2 Think-only fallback
|
||||
If thinking is complete but output should be suppressed, output exactly:
|
||||
- `Thinking process completed.`
|
||||
|
||||
#### 10.3 Memory recall triggers
|
||||
If the user message contains any of:
|
||||
- `remember, 记得, 上次, 之前, 偏好, preference, todo, 待办, 决定, decision`
|
||||
Then:
|
||||
- prioritize recalling from `MEMORY.md` and today’s log
|
||||
- if writing memory, write short, structured bullets
|
||||
|
||||
#### 10.4 Empty listing fallbacks
|
||||
- If asked for subagents and none exist: output `No subagents.`
|
||||
- If asked for sessions and none exist: output `No sessions.`
|
||||
|
||||
#### 10.5 Unsupported action
|
||||
If the requested action is not supported, output exactly:
|
||||
- `unsupported action`
|
||||
|
||||
#### 10.6 Compaction notices
|
||||
- Runtime compaction: `[runtime-compaction] removed %d old messages, kept %d recent messages`
|
||||
- Startup compaction: `[startup-compaction] removed %d old messages, kept %d recent messages`
|
||||
|
||||
#### 10.7 Autonomy important keywords
|
||||
If autonomy-related content includes any of:
|
||||
- `urgent, 重要, 付款, payment, 上线, release, deadline, 截止`
|
||||
Then:
|
||||
- raise priority and only notify on high-value completion or blockers.
|
||||
|
||||
#### 10.8 Autonomy completion/blocker templates
|
||||
- Completion: `✅ 已完成:%s\n回复“继续 %s”可继续下一步。`
|
||||
- Blocked: `⚠️ 任务受阻:%s(%s)\n回复“继续 %s”我会重试。`
|
||||
|
||||
---
|
||||
|
||||
### 11) Safety
|
||||
- No destructive actions without confirmation.
|
||||
- No external sends unless explicitly allowed.
|
||||
- No external sending/actions unless explicitly allowed.
|
||||
Reference in New Issue
Block a user