From 7f796b01208d934a12aec4e969c68abf4c0e5dd2 Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 27 Mar 2026 21:03:34 +0700 Subject: [PATCH] fix(agent): channel-aware tool filtering, group chat title in system prompt - Add filteredToolNamesForChannel() so system prompt Tooling section excludes channel-specific tools for wrong channels (e.g. list_group_members hidden for Telegram, create_forum_topic hidden for Feishu) - Add ChannelAware interface to CreateForumTopicTool (Telegram-only) - Add coreToolSummaries for list_group_members and create_forum_topic (were showing as "(custom tool)") - Inject group chat title into system prompt identity line (e.g. "telegram (group chat \"My Group\")" instead of "a group chat") - Sanitize ChatTitle to prevent prompt injection (strip quotes/newlines, truncate to 100 chars) --- cmd/gateway_consumer_normal.go | 1 + internal/agent/loop.go | 2 +- internal/agent/loop_history.go | 31 +++++++++++++++++++++++++++++-- internal/agent/loop_types.go | 1 + internal/agent/systemprompt.go | 12 ++++++++++++ internal/tools/telegram_forum.go | 2 ++ 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/cmd/gateway_consumer_normal.go b/cmd/gateway_consumer_normal.go index 4022f59f..2a4a5ebf 100644 --- a/cmd/gateway_consumer_normal.go +++ b/cmd/gateway_consumer_normal.go @@ -336,6 +336,7 @@ func processNormalMessage( ForwardMedia: fwdMedia, Channel: msg.Channel, ChannelType: resolveChannelType(channelMgr, msg.Channel), + ChatTitle: msg.Metadata["chat_title"], ChatID: msg.ChatID, PeerKind: peerKind, LocalKey: msg.Metadata["local_key"], diff --git a/internal/agent/loop.go b/internal/agent/loop.go index f91aca97..4de6735e 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -65,7 +65,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) // buildMessages resolves context files once and also detects BOOTSTRAP.md presence // (hadBootstrap) — no extra DB roundtrip needed for bootstrap detection. - messages, hadBootstrap := l.buildMessages(ctx, history, summary, req.Message, req.ExtraSystemPrompt, req.SessionKey, req.Channel, req.ChannelType, req.PeerKind, req.UserID, req.HistoryLimit, req.SkillFilter, req.LightContext) + messages, hadBootstrap := l.buildMessages(ctx, history, summary, req.Message, req.ExtraSystemPrompt, req.SessionKey, req.Channel, req.ChannelType, req.ChatTitle, req.PeerKind, req.UserID, req.HistoryLimit, req.SkillFilter, req.LightContext) // 1b. Determine image routing strategy. // If read_image tool has a dedicated vision provider, images are NOT attached inline diff --git a/internal/agent/loop_history.go b/internal/agent/loop_history.go index 429b44e1..57718066 100644 --- a/internal/agent/loop_history.go +++ b/internal/agent/loop_history.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log/slog" + "slices" "strings" "sync" "time" @@ -41,6 +42,29 @@ func (l *Loop) filteredToolNames() []string { return names } +// filteredToolNamesForChannel returns tool names after applying both policy +// and ChannelAware filters. Tools that implement ChannelAware and don't list +// the current channelType are excluded — keeps the system prompt Tooling +// section consistent with the actual tool definitions sent to the LLM. +func (l *Loop) filteredToolNamesForChannel(channelType string) []string { + names := l.filteredToolNames() + if channelType == "" { + return names + } + filtered := names[:0:0] + for _, name := range names { + if tool, ok := l.tools.Get(name); ok { + if ca, ok := tool.(tools.ChannelAware); ok { + if !slices.Contains(ca.RequiredChannelTypes(), channelType) { + continue + } + } + } + filtered = append(filtered, name) + } + return filtered +} + // buildCredentialCLIContext generates the TOOLS.md supplement for credentialed CLIs. // Returns empty string if no secure CLI store is configured or no enabled CLIs. func (l *Loop) buildCredentialCLIContext(ctx context.Context) string { @@ -75,7 +99,7 @@ func (l *Loop) buildMCPToolDescs(toolNames []string) map[string]string { // buildMessages constructs the full message list for an LLM request. // Returns the messages and whether BOOTSTRAP.md was present in context files // (used by the caller for auto-cleanup without an extra DB roundtrip). -func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, summary, userMessage, extraSystemPrompt, sessionKey, channel, channelType, peerKind, userID string, historyLimit int, skillFilter []string, lightContext bool) ([]providers.Message, bool) { +func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, summary, userMessage, extraSystemPrompt, sessionKey, channel, channelType, chatTitle, peerKind, userID string, historyLimit int, skillFilter []string, lightContext bool) ([]providers.Message, bool) { var messages []providers.Message // Build full system prompt using the new builder (matching TS buildAgentSystemPrompt) @@ -155,7 +179,9 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s } // Build tool list, filtering out skill_manage when skill_evolve is off. - toolNames := l.filteredToolNames() + // Also applies ChannelAware filtering so channel-specific tools don't + // appear in ## Tooling when the current channel doesn't support them. + toolNames := l.filteredToolNamesForChannel(channelType) if !l.skillEvolve { filtered := toolNames[:0:0] for _, n := range toolNames { @@ -191,6 +217,7 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s Workspace: promptWorkspace, Channel: channel, ChannelType: channelType, + ChatTitle: chatTitle, PeerKind: peerKind, OwnerIDs: l.ownerIDs, Mode: mode, diff --git a/internal/agent/loop_types.go b/internal/agent/loop_types.go index 842ea90c..faabe556 100644 --- a/internal/agent/loop_types.go +++ b/internal/agent/loop_types.go @@ -400,6 +400,7 @@ type RunRequest struct { ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results) Channel string // source channel instance name (e.g. "my-telegram-bot") ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context + ChatTitle string // group chat display name (e.g. Telegram group title) ChatID string // source chat ID PeerKind string // "direct" or "group" (for session key building and tool context) RunID string // unique run identifier diff --git a/internal/agent/systemprompt.go b/internal/agent/systemprompt.go index e2fbed79..3665138e 100644 --- a/internal/agent/systemprompt.go +++ b/internal/agent/systemprompt.go @@ -27,6 +27,7 @@ type SystemPromptConfig struct { Workspace string Channel string // runtime channel instance name (e.g. "my-telegram-bot") ChannelType string // platform type (e.g. "zalo_personal", "telegram") + ChatTitle string // group chat display name (shown in identity line) PeerKind string // "direct" or "group" OwnerIDs []string // owner sender IDs Mode PromptMode // full or minimal @@ -106,6 +107,8 @@ var coreToolSummaries = map[string]string{ "create_audio": "Generate music or sound effects from text descriptions using AI", "knowledge_graph_search": "Find people, projects, and their connections — use for relationship questions (who works with whom, project dependencies) that memory_search may miss", "team_tasks": "Team task board — track progress, manage dependencies (spawn auto-creates delegation tasks)", + "list_group_members": "List all members of the current group chat (Feishu/Lark only)", + "create_forum_topic": "Create a forum topic in a Telegram supergroup", // Legacy tool aliases — kept for backward compatibility with older clients "edit_file": "Alias for edit — Edit a file by replacing exact text matches", @@ -138,6 +141,15 @@ func BuildSystemPrompt(cfg SystemPromptConfig) string { chatType := "a direct chat" if cfg.PeerKind == "group" { chatType = "a group chat" + if cfg.ChatTitle != "" { + // Sanitize: strip quotes/newlines, truncate to prevent prompt injection + // (group admins control the title). + title := strings.NewReplacer("\"", "", "\n", " ", "\r", "").Replace(cfg.ChatTitle) + if len([]rune(title)) > 100 { + title = string([]rune(title)[:100]) + } + chatType = fmt.Sprintf("group chat \"%s\"", title) + } } lines = append(lines, fmt.Sprintf("You are a personal assistant running in %s (%s).", channelLabel, chatType)) lines = append(lines, "") diff --git a/internal/tools/telegram_forum.go b/internal/tools/telegram_forum.go index a781d15b..83ede7e1 100644 --- a/internal/tools/telegram_forum.go +++ b/internal/tools/telegram_forum.go @@ -28,6 +28,8 @@ func NewCreateForumTopicTool(provider ForumTopicCreatorProvider) *CreateForumTop func (t *CreateForumTopicTool) Name() string { return "create_forum_topic" } +func (t *CreateForumTopicTool) RequiredChannelTypes() []string { return []string{"telegram"} } + func (t *CreateForumTopicTool) Description() string { return "Create a new forum topic in a Telegram supergroup. " + "Returns the topic's message_thread_id which can be used for routing messages to the topic."