diff --git a/config.example.json b/config.example.json index 88094e0..8e3d9da 100644 --- a/config.example.json +++ b/config.example.json @@ -16,7 +16,15 @@ "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"] + "memory_recall_keywords": ["remember", "记得", "上次", "之前", "偏好", "preference", "todo", "待办", "决定", "decision"], + "lang_usage": "Usage: /lang ", + "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", + "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" }, "context_compaction": { "enabled": true, diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 16dcebe..9b82703 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -36,12 +36,20 @@ type AgentLoop struct { compactionEnabled bool compactionTrigger int compactionKeepRecent int - heartbeatAckMaxChars int - memoryRecallKeywords []string - noResponseFallback string - thinkOnlyFallback string - audit *triggerAudit - running bool + heartbeatAckMaxChars int + memoryRecallKeywords []string + noResponseFallback string + thinkOnlyFallback string + langUsage string + langInvalid string + langUpdatedTemplate string + runtimeCompactionNote string + startupCompactionNote string + toolNoSubagents string + toolNoSessions string + toolUnsupportedAction string + audit *triggerAudit + running bool } // StartupCompactionReport provides startup memory/session maintenance stats. @@ -102,7 +110,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)) + toolsRegistry.Register(tools.NewSubagentsTool(subagentManager, cfg.Agents.Defaults.Texts.SubagentsNone, cfg.Agents.Defaults.Texts.UnsupportedAction)) toolsRegistry.Register(tools.NewSessionsTool( func(limit int) []tools.SessionInfo { sessions := alSessionListForTool(sessionsManager, limit) @@ -115,6 +123,8 @@ 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 @@ -150,12 +160,17 @@ 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, - memoryRecallKeywords: cfg.Agents.Defaults.Texts.MemoryRecallKeywords, - noResponseFallback: cfg.Agents.Defaults.Texts.NoResponseFallback, - thinkOnlyFallback: cfg.Agents.Defaults.Texts.ThinkOnlyFallback, - audit: newTriggerAudit(workspace), - running: false, + 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, + audit: newTriggerAudit(workspace), + running: false, } // 注入递归运行逻辑,使 subagent 具备 full tool-calling 能力 @@ -288,15 +303,27 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) if last == "" { last = "(none)" } - return fmt.Sprintf("Usage: /lang \nCurrent preferred: %s\nLast detected: %s", preferred, last), nil + usage := strings.TrimSpace(al.langUsage) + if usage == "" { + usage = "Usage: /lang " + } + return fmt.Sprintf("%s\nCurrent preferred: %s\nLast detected: %s", usage, preferred, last), nil } lang := normalizeLang(parts[1]) if lang == "" { - return "Invalid language code.", nil + 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)) - return fmt.Sprintf("Language preference updated to %s", lang), nil + tpl := strings.TrimSpace(al.langUpdatedTemplate) + if tpl == "" { + tpl = "Language preference updated to %s" + } + return fmt.Sprintf(tpl, lang), nil } // Update tool contexts @@ -731,7 +758,11 @@ func (al *AgentLoop) compactSessionIfNeeded(sessionKey string) { return } removed := len(h) - keepRecent - note := fmt.Sprintf("[runtime-compaction] removed %d old messages, kept %d recent messages", removed, keepRecent) + tpl := strings.TrimSpace(al.runtimeCompactionNote) + if tpl == "" { + 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)) } @@ -769,7 +800,11 @@ func (al *AgentLoop) RunStartupSelfCheckAllSessions(ctx context.Context) Startup } removed := len(history) - keepRecent - note := fmt.Sprintf("[startup-compaction] removed %d old messages, kept %d recent messages", removed, keepRecent) + tpl := strings.TrimSpace(al.startupCompactionNote) + if tpl == "" { + 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)) report.CompactedSessions++ diff --git a/pkg/config/config.go b/pkg/config/config.go index 7fde7dd..ee6ad96 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -44,9 +44,17 @@ type AgentDefaults struct { } type AgentTextConfig struct { - NoResponseFallback string `json:"no_response_fallback"` - ThinkOnlyFallback string `json:"think_only_fallback"` - MemoryRecallKeywords []string `json:"memory_recall_keywords"` + 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"` + RuntimeCompactionNote string `json:"runtime_compaction_note"` + StartupCompactionNote string `json:"startup_compaction_note"` } type HeartbeatConfig struct { @@ -276,9 +284,17 @@ func DefaultConfig() *Config { 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"}, + 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"}, + LangUsage: "Usage: /lang ", + LangInvalid: "Invalid language code.", + LangUpdatedTemplate: "Language preference updated to %s", + SubagentsNone: "No subagents.", + SessionsNone: "No sessions.", + UnsupportedAction: "unsupported action", + RuntimeCompactionNote: "[runtime-compaction] removed %d old messages, kept %d recent messages", + StartupCompactionNote: "[startup-compaction] removed %d old messages, kept %d recent messages", }, ContextCompaction: ContextCompactionConfig{ Enabled: true, diff --git a/pkg/tools/sessions_tool.go b/pkg/tools/sessions_tool.go index 2325a97..7b3382b 100644 --- a/pkg/tools/sessions_tool.go +++ b/pkg/tools/sessions_tool.go @@ -19,12 +19,14 @@ type SessionInfo struct { } type SessionsTool struct { - listFn func(limit int) []SessionInfo - historyFn func(key string, limit int) []providers.Message + listFn func(limit int) []SessionInfo + historyFn func(key string, limit int) []providers.Message + noSessionsText string + unsupportedAction string } -func NewSessionsTool(listFn func(limit int) []SessionInfo, historyFn func(key string, limit int) []providers.Message) *SessionsTool { - return &SessionsTool{listFn: listFn, historyFn: historyFn} +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 (t *SessionsTool) Name() string { return "sessions" } @@ -111,7 +113,7 @@ func (t *SessionsTool) Execute(ctx context.Context, args map[string]interface{}) } items := t.listFn(limit * 3) if len(items) == 0 { - return "No sessions.", nil + return firstNonEmpty(t.noSessionsText, "No sessions."), nil } if len(kindFilter) > 0 { filtered := make([]SessionInfo, 0, len(items)) @@ -283,6 +285,13 @@ func (t *SessionsTool) Execute(ctx context.Context, args map[string]interface{}) } return strings.TrimSpace(sb.String()), nil default: - return "unsupported action", nil + return firstNonEmpty(t.unsupportedAction, "unsupported action"), nil } } + +func firstNonEmpty(v, fallback string) string { + if strings.TrimSpace(v) != "" { + return v + } + return fallback +} diff --git a/pkg/tools/sessions_tool_test.go b/pkg/tools/sessions_tool_test.go index 5f03bf2..2d2b56d 100644 --- a/pkg/tools/sessions_tool_test.go +++ b/pkg/tools/sessions_tool_test.go @@ -15,7 +15,7 @@ func TestSessionsToolListWithKindsAndQuery(t *testing.T) { {Key: "telegram:1", Kind: "main", Summary: "project alpha", UpdatedAt: time.Now()}, {Key: "cron:1", Kind: "cron", Summary: "nightly sync", UpdatedAt: time.Now()}, } - }, nil) + }, nil, "", "") out, err := tool.Execute(context.Background(), map[string]interface{}{ "action": "list", @@ -37,7 +37,7 @@ func TestSessionsToolHistoryWithoutTools(t *testing.T) { {Role: "tool", Content: "tool output"}, {Role: "assistant", Content: "ok"}, } - }) + }, "", "") out, err := tool.Execute(context.Background(), map[string]interface{}{ "action": "history", @@ -58,7 +58,7 @@ func TestSessionsToolHistoryFromMe(t *testing.T) { {Role: "assistant", Content: "a1"}, {Role: "assistant", Content: "a2"}, } - }) + }, "", "") out, err := tool.Execute(context.Background(), map[string]interface{}{ "action": "history", diff --git a/pkg/tools/subagents_tool.go b/pkg/tools/subagents_tool.go index 8d5fb2c..da04968 100644 --- a/pkg/tools/subagents_tool.go +++ b/pkg/tools/subagents_tool.go @@ -10,11 +10,13 @@ import ( ) type SubagentsTool struct { - manager *SubagentManager + manager *SubagentManager + noSubagentsText string + unsupportedAction string } -func NewSubagentsTool(m *SubagentManager) *SubagentsTool { - return &SubagentsTool{manager: m} +func NewSubagentsTool(m *SubagentManager, noSubagentsText, unsupportedAction string) *SubagentsTool { + return &SubagentsTool{manager: m, noSubagentsText: noSubagentsText, unsupportedAction: unsupportedAction} } func (t *SubagentsTool) Name() string { return "subagents" } @@ -56,7 +58,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 "No subagents.", nil + return firstNonEmpty(t.noSubagentsText, "No subagents."), nil } var sb strings.Builder sb.WriteString("Subagents:\n") @@ -69,7 +71,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 "No subagents.", nil + return firstNonEmpty(t.noSubagentsText, "No subagents."), nil } sort.Slice(tasks, func(i, j int) bool { return tasks[i].Created > tasks[j].Created }) var sb strings.Builder @@ -92,7 +94,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 "No subagents.", nil + return firstNonEmpty(t.noSubagentsText, "No subagents."), nil } killed := 0 for _, task := range tasks { @@ -159,7 +161,7 @@ func (t *SubagentsTool) Execute(ctx context.Context, args map[string]interface{} } return fmt.Sprintf("subagent resumed as %s", label), nil default: - return "unsupported action", nil + return firstNonEmpty(t.unsupportedAction, "unsupported action"), nil } } diff --git a/pkg/tools/subagents_tool_test.go b/pkg/tools/subagents_tool_test.go index c1f4970..b72bae0 100644 --- a/pkg/tools/subagents_tool_test.go +++ b/pkg/tools/subagents_tool_test.go @@ -11,7 +11,7 @@ func TestSubagentsInfoAll(t *testing.T) { m.tasks["subagent-1"] = &SubagentTask{ID: "subagent-1", Status: "completed", Label: "a", Created: 2} m.tasks["subagent-2"] = &SubagentTask{ID: "subagent-2", Status: "running", Label: "b", Created: 3} - tool := NewSubagentsTool(m) + tool := NewSubagentsTool(m, "", "") out, err := tool.Execute(context.Background(), map[string]interface{}{"action": "info", "id": "all"}) if err != nil { t.Fatal(err) @@ -26,7 +26,7 @@ func TestSubagentsKillAll(t *testing.T) { m.tasks["subagent-1"] = &SubagentTask{ID: "subagent-1", Status: "running", Label: "a", Created: 2} m.tasks["subagent-2"] = &SubagentTask{ID: "subagent-2", Status: "running", Label: "b", Created: 3} - tool := NewSubagentsTool(m) + tool := NewSubagentsTool(m, "", "") out, err := tool.Execute(context.Background(), map[string]interface{}{"action": "kill", "id": "all"}) if err != nil { t.Fatal(err)