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)
This commit is contained in:
viettranx committed 2026-03-27 21:03:34 +07:00
1 parent 8b08dec04c
commit 7f796b0120
6 files changed
+46 -3

No files matched your search

+1
View File
@@ -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"],
+1 -1
View File
@@ -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
+29 -2
View File
@@ -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,
+1
View File
@@ -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
+12
View File
@@ -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, "")
+2
View File
@@ -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."