From 3715d260a5c3dee0fb62ad5721fca545c3eee2fa Mon Sep 17 00:00:00 2001 From: viettranx Date: Sat, 18 Apr 2026 17:45:14 +0700 Subject: [PATCH] feat(agent): inject into system prompt Give the LLM an unambiguous reference for the currently bound chat so it can compare against any target it plans to pass. Block lists channel, chat_id and kind (direct/group) with a one-line policy: omit target to reply here, only set target when forwarding per explicit user request. Threads ChatID through SystemPromptConfig, IdentityData, BridgePromptBuilder, and buildMessages so the pipeline populates the block from pipeline.RunInput.ChatID. --- internal/agent/loop_history.go | 3 +- internal/agent/loop_pipeline_callbacks.go | 3 +- internal/agent/prompt_builder_impl.go | 1 + internal/agent/prompt_config_types.go | 15 +-- internal/agent/systemprompt.go | 146 ++++++++++++---------- 5 files changed, 94 insertions(+), 74 deletions(-) diff --git a/internal/agent/loop_history.go b/internal/agent/loop_history.go index ba2f366f..cae3d62d 100644 --- a/internal/agent/loop_history.go +++ b/internal/agent/loop_history.go @@ -17,7 +17,7 @@ import ( // 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, chatTitle, 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, chatID, peerKind, userID string, historyLimit int, skillFilter []string, lightContext bool) ([]providers.Message, bool) { var messages []providers.Message // Build system prompt — 3-layer mode resolution: runtime > auto-detect > config @@ -204,6 +204,7 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s Workspace: promptWorkspace, Channel: channel, ChannelType: channelType, + ChatID: chatID, ChatTitle: chatTitle, PeerKind: peerKind, OwnerIDs: l.ownerIDs, diff --git a/internal/agent/loop_pipeline_callbacks.go b/internal/agent/loop_pipeline_callbacks.go index 017ec498..cdbfc1c5 100644 --- a/internal/agent/loop_pipeline_callbacks.go +++ b/internal/agent/loop_pipeline_callbacks.go @@ -129,7 +129,7 @@ func (l *Loop) makeBuildMessages() func(ctx context.Context, input *pipeline.Run msgs, _ := l.buildMessages(ctx, history, summary, input.Message, input.ExtraSystemPrompt, input.SessionKey, input.Channel, input.ChannelType, - input.ChatTitle, input.PeerKind, input.UserID, + input.ChatTitle, input.ChatID, input.PeerKind, input.UserID, input.HistoryLimit, input.SkillFilter, input.LightContext) return msgs, nil } @@ -428,4 +428,3 @@ func (l *Loop) makeBootstrapCleanup() func(ctx context.Context, state *pipeline. return l.bootstrapCleanup(ctx, l.agentUUID, state.Input.UserID) } } - diff --git a/internal/agent/prompt_builder_impl.go b/internal/agent/prompt_builder_impl.go index 893222c7..a0c3b301 100644 --- a/internal/agent/prompt_builder_impl.go +++ b/internal/agent/prompt_builder_impl.go @@ -35,6 +35,7 @@ func (b *BridgePromptBuilder) Build(cfg PromptConfig) (string, error) { spc.AgentID = cfg.IdentityData.AgentName spc.Model = cfg.IdentityData.Model spc.Channel = cfg.IdentityData.Channel + spc.ChatID = cfg.IdentityData.ChatID spc.ChatTitle = cfg.IdentityData.ChatTitle spc.PeerKind = cfg.IdentityData.PeerKind } diff --git a/internal/agent/prompt_config_types.go b/internal/agent/prompt_config_types.go index 3c2cde4a..61bac278 100644 --- a/internal/agent/prompt_config_types.go +++ b/internal/agent/prompt_config_types.go @@ -6,13 +6,13 @@ import "github.com/nextlevelbuilder/goclaw/internal/memory" // Each bool field maps to a named template block. type PromptConfig struct { // Section toggles - Identity bool - Persona bool // SOUL.md content - Instructions bool // AGENTS.md content - Tools bool - Skills bool - Team bool - Workspace bool + Identity bool + Persona bool // SOUL.md content + Instructions bool // AGENTS.md content + Tools bool + Skills bool + Team bool + Workspace bool Memory bool // auto-inject L0 section Sandbox bool Orchestration bool // v3 orchestration delegation targets @@ -43,6 +43,7 @@ type IdentityData struct { Emoji string Model string Channel string + ChatID string // current reply target chat id ChatTitle string PeerKind string // "direct" or "group" } diff --git a/internal/agent/systemprompt.go b/internal/agent/systemprompt.go index 7c0761ef..4a299f0a 100644 --- a/internal/agent/systemprompt.go +++ b/internal/agent/systemprompt.go @@ -97,35 +97,36 @@ type SystemPromptConfig struct { DisplayName string // human-readable agent display name Model string 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 - ToolNames []string // registered tool names - SkillsSummary string // XML from skills.Loader.BuildSummary() - HasMemory bool // memory_search/memory_get available? - HasSpawn bool // spawn tool available? - IsTeamContext bool // inject team sections (leader inbound OR team dispatch) - TeamWorkspace string // absolute path to team shared workspace (empty if not in team) - TeamMembers []store.TeamMemberData // team member roster for task assignment - TeamGuidance string // edition-specific guidance from TeamActionPolicy.MemberGuidance() + Channel string // runtime channel instance name (e.g. "my-telegram-bot") + ChannelType string // platform type (e.g. "zalo_personal", "telegram") + ChatID string // current reply target chat id (drives ) + 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 + ToolNames []string // registered tool names + SkillsSummary string // XML from skills.Loader.BuildSummary() + HasMemory bool // memory_search/memory_get available? + HasSpawn bool // spawn tool available? + IsTeamContext bool // inject team sections (leader inbound OR team dispatch) + TeamWorkspace string // absolute path to team shared workspace (empty if not in team) + TeamMembers []store.TeamMemberData // team member roster for task assignment + TeamGuidance string // edition-specific guidance from TeamActionPolicy.MemberGuidance() ContextFiles []bootstrap.ContextFile // bootstrap files for # Project Context - ExtraPrompt string // extra system prompt (subagent context, etc.) - AgentType string // "open" or "predefined" — affects context file framing + ExtraPrompt string // extra system prompt (subagent context, etc.) + AgentType string // "open" or "predefined" — affects context file framing HasSkillSearch bool // skill_search tool registered? (for search-mode prompt) HasSkillManage bool // skill_manage tool registered + skill_evolve enabled for this agent PinnedSkillsSummary string // XML summary of pinned skills only (hybrid mode) - HasMCPToolSearch bool // mcp_tool_search tool registered? (MCP search mode) - HasKnowledgeGraph bool // knowledge_graph_search tool registered? - HasMemoryExpand bool // memory_expand tool registered? (v3 episodic deep retrieval) - MCPToolDescs map[string]string // MCP tool name → description (inline mode only) + HasMCPToolSearch bool // mcp_tool_search tool registered? (MCP search mode) + HasKnowledgeGraph bool // knowledge_graph_search tool registered? + HasMemoryExpand bool // memory_expand tool registered? (v3 episodic deep retrieval) + MCPToolDescs map[string]string // MCP tool name → description (inline mode only) // Sandbox info — matching TS sandboxInfo in system-prompt.ts - SandboxEnabled bool // exec tool runs inside Docker sandbox? - SandboxContainerDir string // container-side workdir (e.g. "/workspace") + SandboxEnabled bool // exec tool runs inside Docker sandbox? + SandboxContainerDir string // container-side workdir (e.g. "/workspace") SandboxWorkspaceAccess string // "none", "ro", "rw" // ProviderType identifies the LLM provider (e.g. "openai", "anthropic", "codex"). @@ -173,46 +174,46 @@ func (cfg SystemPromptConfig) sectionContent(id string, defaultFn func() []strin // coreToolSummaries maps tool names to one-line descriptions. // Shown in the ## Tooling section of the system prompt. var coreToolSummaries = map[string]string{ - "read_file": "Read file contents — only accesses your agent workspace. For docs returned by vault_search (shared/personal/team vault), use vault_read instead", - "write_file": "Create or overwrite files", - "list_files": "List directory contents", - "exec": "Run shell commands", - "memory_search": "Search indexed memory files (MEMORY.md + memory/*.md)", - "memory_get": "Read specific sections of memory files", - "spawn": "Spawn a self-clone subagent to handle a task in the background", - "web_search": "Search the web", - "web_fetch": "Fetch and extract content from a URL", - "datetime": "Get current date/time with timezone — use before creating cron jobs", - "cron": "Manage scheduled jobs and reminders (e.g. 'remind me at 9am', 'check every morning')", - "heartbeat": "Periodic background monitoring with HEARTBEAT.md. Unlike cron, auto-suppresses 'all OK' via HEARTBEAT_OK", - "skill_search": "Search available skills by keyword (weather, translate, github, etc.)", - "skill_manage": "Create, patch, or delete skills from conversation experience", - "publish_skill": "Register a skill directory in the system database, making it discoverable", - "use_skill": "Invoke a skill by name and follow its instructions", - "mcp_tool_search": "Search for available MCP external integration tools by keyword", - "browser": "Browse web pages interactively", - "tts": "Convert text to speech audio", - "edit": "Edit a file by replacing exact text matches", - "message": "Send a PROACTIVE message to another channel/chat — do NOT use this to reply to the user, just respond directly", - "sessions_list": "List sessions for this agent", - "session_status": "Show session status (model, tokens, compaction count)", - "sessions_history": "Fetch message history for a session", - "sessions_send": "Send a message into another session", - "read_image": "Analyze images — call with path from tags", - "read_audio": "Analyze audio — call with media_id from tags", - "read_video": "Analyze video — call with media_id from tags", - "create_video": "Generate videos from text descriptions using AI", - "read_document": "Analyze documents (PDF, DOCX) from tags. If fails, use a skill instead. Path is directly accessible", - "create_image": "Generate images from text descriptions using AI", - "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", - "delegate": "Delegate a task to a linked agent (requires agent_links). See ## Delegation Targets for available agents", - "memory_expand": "Retrieve full session details from episodic memory results — use after memory_search returns episodic hits", - "vault_search": "Search documents in the knowledge vault (hybrid keyword + semantic). Pass the returned doc_id to vault_read for full content", - "vault_read": "Read full content of a vault document by doc_id (from vault_search). Use for shared/personal/team vault docs that read_file cannot reach", + "read_file": "Read file contents — only accesses your agent workspace. For docs returned by vault_search (shared/personal/team vault), use vault_read instead", + "write_file": "Create or overwrite files", + "list_files": "List directory contents", + "exec": "Run shell commands", + "memory_search": "Search indexed memory files (MEMORY.md + memory/*.md)", + "memory_get": "Read specific sections of memory files", + "spawn": "Spawn a self-clone subagent to handle a task in the background", + "web_search": "Search the web", + "web_fetch": "Fetch and extract content from a URL", + "datetime": "Get current date/time with timezone — use before creating cron jobs", + "cron": "Manage scheduled jobs and reminders (e.g. 'remind me at 9am', 'check every morning')", + "heartbeat": "Periodic background monitoring with HEARTBEAT.md. Unlike cron, auto-suppresses 'all OK' via HEARTBEAT_OK", + "skill_search": "Search available skills by keyword (weather, translate, github, etc.)", + "skill_manage": "Create, patch, or delete skills from conversation experience", + "publish_skill": "Register a skill directory in the system database, making it discoverable", + "use_skill": "Invoke a skill by name and follow its instructions", + "mcp_tool_search": "Search for available MCP external integration tools by keyword", + "browser": "Browse web pages interactively", + "tts": "Convert text to speech audio", + "edit": "Edit a file by replacing exact text matches", + "message": "Send a PROACTIVE message to another channel/chat — do NOT use this to reply to the user, just respond directly", + "sessions_list": "List sessions for this agent", + "session_status": "Show session status (model, tokens, compaction count)", + "sessions_history": "Fetch message history for a session", + "sessions_send": "Send a message into another session", + "read_image": "Analyze images — call with path from tags", + "read_audio": "Analyze audio — call with media_id from tags", + "read_video": "Analyze video — call with media_id from tags", + "create_video": "Generate videos from text descriptions using AI", + "read_document": "Analyze documents (PDF, DOCX) from tags. If fails, use a skill instead. Path is directly accessible", + "create_image": "Generate images from text descriptions using AI", + "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", + "delegate": "Delegate a task to a linked agent (requires agent_links). See ## Delegation Targets for available agents", + "memory_expand": "Retrieve full session details from episodic memory results — use after memory_search returns episodic hits", + "vault_search": "Search documents in the knowledge vault (hybrid keyword + semantic). Pass the returned doc_id to vault_read for full content", + "vault_read": "Read full content of a vault document by doc_id (from vault_search). Use for shared/personal/team vault docs that read_file cannot reach", // Tool aliases (edit_file, sessions_spawn, Read, Write, Edit, Bash, etc.) // are registered in the tool registry but excluded from the system prompt @@ -251,6 +252,25 @@ func BuildSystemPrompt(cfg SystemPromptConfig) string { } lines = append(lines, fmt.Sprintf("You are a personal assistant running in %s (%s).", channelLabel, chatType)) lines = append(lines, "") + + // Inject explicit reply-target block so the LLM has a copy-paste-ready + // value to compare against when deciding to forward. Pairs with the + // MessageTool cross-target guard. + if cfg.ChatID != "" { + kind := "direct" + if cfg.PeerKind == "group" { + kind = "group" + } + lines = append(lines, + "", + fmt.Sprintf(" channel: %s", channelLabel), + fmt.Sprintf(" chat_id: %s", cfg.ChatID), + fmt.Sprintf(" kind: %s", kind), + "", + "When using the message tool, omit `target` to reply here. Set `target` only when forwarding to a different chat per explicit user request (also requires `forward=true` + `forward_reason`).", + "", + ) + } } // 1.5. First-run bootstrap override (must be early so model sees it first) @@ -685,5 +705,3 @@ func buildWorkspaceSection(workspace string, sandboxEnabled bool, containerDir s "", } } - -