diff --git a/internal/agent/loop_history.go b/internal/agent/loop_history.go index 95106992..c38ce9cc 100644 --- a/internal/agent/loop_history.go +++ b/internal/agent/loop_history.go @@ -202,6 +202,21 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s } toolNames = filtered } + // Exclude tool aliases from the system prompt tool list. + // Aliases are sent as separate provider definitions (LLM can still call them), + // but listing them in the prompt adds ~300 tokens of noise that dilutes persona. + if l.tools != nil { + aliasSet := l.tools.Aliases() + if len(aliasSet) > 0 { + noAlias := toolNames[:0:0] + for _, n := range toolNames { + if _, isAlias := aliasSet[n]; !isAlias { + noAlias = append(noAlias, n) + } + } + toolNames = noAlias + } + } // Always build MCP tool descriptions for inline tools — in hybrid search // mode the kept inline tools still need descriptions in the system prompt. mcpToolDescs := l.buildMCPToolDescs(toolNames) diff --git a/internal/agent/systemprompt.go b/internal/agent/systemprompt.go index 8434d855..957e1a52 100644 --- a/internal/agent/systemprompt.go +++ b/internal/agent/systemprompt.go @@ -130,20 +130,9 @@ var coreToolSummaries = map[string]string{ "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", - "sessions_spawn": "Alias for spawn — Spawn a self-clone subagent to handle a task in the background", - - // Claude Code tool aliases — enable Claude Code skills without modification - "Read": "Alias for read_file — Read file contents", - "Write": "Alias for write_file — Create or overwrite files", - "Edit": "Alias for edit — Edit a file by replacing exact text matches", - "Bash": "Alias for exec — Run shell commands", - "WebFetch": "Alias for web_fetch — Fetch and extract content from a URL", - "WebSearch": "Alias for web_search — Search the web", - "Agent": "Alias for spawn — Spawn a subagent or delegate to another agent", - "Skill": "Alias for use_skill — Invoke a skill by name", - "ToolSearch": "Alias for mcp_tool_search — Search for available MCP tools", + // Tool aliases (edit_file, sessions_spawn, Read, Write, Edit, Bash, etc.) + // are registered in the tool registry but excluded from the system prompt + // to reduce prompt size (~300 tokens). They work without being listed here. } // BuildSystemPrompt constructs the full system prompt with all sections. @@ -223,6 +212,11 @@ func BuildSystemPrompt(cfg SystemPromptConfig) string { // 2. ## Tooling lines = append(lines, buildToolingSection(cfg.ToolNames, cfg.SandboxEnabled, cfg.ShellDenyGroups)...) + // 2.3. ## Tool Call Style — narration minimalism + non-disclosure of tool internals + if !cfg.IsBootstrap { + lines = append(lines, buildToolCallStyleSection()...) + } + // 2.5. Credentialed CLI context (appended after tooling, before safety) — skip during bootstrap if !cfg.IsBootstrap && cfg.CredentialCLIContext != "" { lines = append(lines, cfg.CredentialCLIContext, "") @@ -328,19 +322,14 @@ func BuildSystemPrompt(cfg SystemPromptConfig) string { lines = append(lines, buildRuntimeSection(cfg)...) // 16. Recency reinforcements — skip during bootstrap (short prompt, no drift risk) + // Consolidated: persona reminder + slim AGENTS.md reminder (no memory duplication). + // Memory recall is covered by the dedicated ## Memory Recall section above. if !cfg.IsBootstrap { if len(personaFiles) > 0 { lines = append(lines, buildPersonaReminder(personaFiles, cfg.AgentType, cfg.ProviderType)...) } if !isMinimal { - lines = append(lines, "Reminder: Follow AGENTS.md rules — memory recall before answering, NO_REPLY when silent, match the user's language.", "") - } - if !isMinimal && cfg.HasMemory { - memReminder := "Reminder: Before answering questions about prior work, decisions, or preferences, always run memory_search first. If nothing relevant is found, say you checked but found nothing." - if cfg.HasKnowledgeGraph { - memReminder += " Also run knowledge_graph_search when the question involves people, teams, projects, or connections." - } - lines = append(lines, memReminder, "") + lines = append(lines, "Reminder: Follow AGENTS.md rules — NO_REPLY when silent, match the user's language.", "") } } diff --git a/internal/agent/systemprompt_sections.go b/internal/agent/systemprompt_sections.go index 96545e4f..2cace9ec 100644 --- a/internal/agent/systemprompt_sections.go +++ b/internal/agent/systemprompt_sections.go @@ -89,8 +89,28 @@ func buildSandboxSection(cfg SystemPromptConfig) []string { return lines } +// buildToolCallStyleSection generates the ## Tool Call Style section. +// Matches TS system-prompt.ts "Tool Call Style" — narration minimalism + non-disclosure. +// Prevents the agent from exposing internal tool names to users. +func buildToolCallStyleSection() []string { + return []string{ + "## Tool Call Style", + "", + "Default: do not narrate routine, low-risk tool calls (just call the tool).", + "Narrate only when it helps: multi-step work, complex problems, sensitive actions, or when the user explicitly asks.", + "Keep narration brief and value-dense; avoid repeating obvious steps.", + "Use plain human language — never mention tool names, function names, or internal mechanics to users.", + "", + "WRONG: \"I searched memory_search and knowledge_graph_search but results were sparse...\"", + "RIGHT: \"Hmm, I remember you mentioned that before...\" or \"I don't recall that specifically.\"", + "", + "When runtime-generated events (subagent completions, cron results) provide metadata, rewrite in your natural voice before relaying to the user.", + "When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI commands.", + "", + } +} + // buildMemoryRecallSection generates the ## Memory Recall section for the system prompt. -// Supplements (does NOT replace) the recency reminder at the end of the prompt. func buildMemoryRecallSection(hasMemoryGet, hasKG bool) []string { lines := []string{"## Memory Recall", ""} @@ -98,12 +118,12 @@ func buildMemoryRecallSection(hasMemoryGet, hasKG bool) []string { lines = append(lines, "Before answering questions about prior work, decisions, people, preferences, or todos: "+ "call memory_search with a relevant query; then use memory_get to pull only the needed lines. "+ - "If no relevant results found, tell the user you checked but found nothing.") + "If no relevant results found, say so naturally without mentioning tool names.") } else { lines = append(lines, "Before answering questions about prior work, decisions, people, preferences, or todos: "+ "call memory_search with a relevant query and answer from the matching results. "+ - "If no relevant results found, tell the user you checked but found nothing.") + "If no relevant results found, say so naturally without mentioning tool names.") } if hasKG { diff --git a/internal/bootstrap/templates/AGENTS.md b/internal/bootstrap/templates/AGENTS.md index 0aa367c9..6343ca75 100644 --- a/internal/bootstrap/templates/AGENTS.md +++ b/internal/bootstrap/templates/AGENTS.md @@ -21,26 +21,23 @@ Talk like a person, not a customer service bot. ## Memory -You start fresh each session. Use tools to maintain continuity: +You start fresh each session. Your tools handle recall automatically. -- **Recall:** Use `memory_search` before answering about prior work, decisions, or preferences -- **Save:** Use `write_file` to persist important information: - - Daily notes → `memory/YYYY-MM-DD.md` (raw logs, what happened today) - - Long-term → `MEMORY.md` (curated: key decisions, lessons, significant events) -- **No "mental notes"** — if you want to remember something, write it to a file NOW with a tool call +- Before answering about past events, check your memory first — then answer naturally +- Save important info to files NOW — "mental notes" don't survive sessions +- Daily notes → `memory/YYYY-MM-DD.md` | Long-term → `MEMORY.md` - When asked to "remember this" → write immediately, don't just acknowledge -- **Recall details:** Use `memory_search` first, then `memory_get` to pull only the needed lines. - If `knowledge_graph_search` is available, also run it for questions about people, teams, projects, or connections — it finds multi-hop relationships that `memory_search` misses. -- When asked to save or remember something, you MUST call a write tool (`write_file` or `edit`) in THIS turn. Never claim "already saved" without a tool call. +- When asked to save or remember something, you MUST write in THIS turn. Never claim "already saved" without actually saving. -### Memory Privacy +### Privacy -- In group chats: do NOT surface personal memory content (private preferences, personal history) -- Memory details (file paths, raw notes) should only be shared in **private/direct chats** -- In groups, use memory to inform your answers, but don't quote or reference it directly +- In group chats: use memory to inform your answers, but don't quote or reference it directly +- Memory details should only be shared in private/direct chats ## Group Chats +You have access to your human's stuff. That doesn't mean you _share_ their stuff. In groups, you're a participant — not their voice, not their proxy. + ### Know When to Speak **Respond when:** @@ -63,6 +60,8 @@ You start fresh each session. Use tools to maintain continuity: **Avoid the triple-tap:** Don't respond multiple times to the same message. One thoughtful response beats three fragments. +Participate, don't dominate. + ### NO_REPLY Format When you have nothing to say, respond with ONLY: NO_REPLY