From 913e956c08ccb136301c01c470cb2eef77fd672e Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 23 Nov 2025 18:42:07 -0600 Subject: [PATCH 1/7] feat: add Tool: prompt syntax for tool-specific instructions - Add extractToolSection() to markdown.ts for parsing Tool: sections - Update stripScopedInstructionSections() to remove Tool sections from general instructions - Add readToolInstructions() helper to systemMessage.ts - Modify getToolsForModel() to accept and inject tool-specific instructions into descriptions - Add comprehensive tests for extractToolSection and stripScopedInstructionSections - Document Tool prompt syntax in docs/instruction-files.md Tool sections work like Mode and Model sections: context (workspace/project) is checked first, then global (~/.mux/AGENTS.md). Instructions are appended to the tool's description, allowing customization without cluttering general instructions. --- docs/instruction-files.md | 34 +++++ src/common/utils/tools/tools.ts | 53 ++++++- src/node/services/aiService.ts | 13 +- src/node/services/systemMessage.ts | 90 ++++++++++- src/node/utils/main/markdown.test.ts | 214 ++++++++++++++++++++++++++- src/node/utils/main/markdown.ts | 19 ++- 6 files changed, 409 insertions(+), 14 deletions(-) diff --git a/docs/instruction-files.md b/docs/instruction-files.md index 7f0caf589f..7c15a0c1b2 100644 --- a/docs/instruction-files.md +++ b/docs/instruction-files.md @@ -89,6 +89,40 @@ Be terse and to the point. Use status reporting tools every few minutes. ``` +## Tool Prompts + +Like modes and models, mux reads headings titled `Tool: ` to add instructions to specific tool descriptions. This allows you to customize how the AI uses particular tools without cluttering general instructions. + +Rules: + +- Workspace instructions are evaluated before global instructions; the first matching section wins. +- Tool names must match exactly (case-insensitive). +- Tool sections are removed from ``; matching content is injected into the tool's description. +- Only tools available for the active model are augmented (e.g., `web_search` for Anthropic models). + + + +Example: + +```markdown +## Tool: bash + +When running commands: + +- Always use absolute paths for clarity +- Prefer single-line commands over multi-line scripts +- Add `set -e` to scripts that should fail fast + +## Tool: file_edit_replace_string + +When editing files: + +- Include enough context (2-3 lines) around the replacement to make matches unique +- Verify the edit succeeded by reading the file afterwards +``` + +Available tools include: `bash`, `file_read`, `file_edit_replace_string`, `file_edit_insert`, `propose_plan`, `todo_write`, `todo_read`, `status_set`, and `web_search` (Anthropic/OpenAI models only). + ## Practical layout ``` diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts index a6f8f46da9..76d7cadbd0 100644 --- a/src/common/utils/tools/tools.ts +++ b/src/common/utils/tools/tools.ts @@ -1,4 +1,4 @@ -import { type Tool } from "ai"; +import { type Tool, tool } from "ai"; import { createFileReadTool } from "@/node/services/tools/file_read"; import { createBashTool } from "@/node/services/tools/bash"; import { createFileEditReplaceStringTool } from "@/node/services/tools/file_edit_replace_string"; @@ -36,6 +36,29 @@ export interface ToolConfiguration { */ export type ToolFactory = (config: ToolConfiguration) => Tool; +/** + * Augment a tool's description with additional instructions from "Tool: " sections + * Creates a wrapper tool that delegates to the base tool but with an enhanced description. + * @param baseTool The original tool to augment + * @param additionalInstructions Additional instructions to append to the description + * @returns A new tool with the augmented description + */ +function augmentToolDescription(baseTool: Tool, additionalInstructions: string): Tool { + // Access the tool as a record to get its properties + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const baseToolRecord = baseTool as any as Record; + const augmentedDescription = `${String(baseToolRecord.description ?? "")}\n\n${additionalInstructions}`; + + // Return a new tool with the augmented description + return tool({ + description: augmentedDescription, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + inputSchema: baseToolRecord.inputSchema as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + execute: baseToolRecord.execute as any, + }); +} + /** * Get tools available for a specific model with configuration * @@ -46,13 +69,15 @@ export type ToolFactory = (config: ToolConfiguration) => Tool; * @param config Required configuration for tools * @param workspaceId Workspace ID for init state tracking (required for runtime tools) * @param initStateManager Init state manager for runtime tools to wait for initialization + * @param toolInstructions Optional map of tool names to additional instructions from "Tool: " sections * @returns Promise resolving to record of tools available for the model */ export async function getToolsForModel( modelString: string, config: ToolConfiguration, workspaceId: string, - initStateManager: InitStateManager + initStateManager: InitStateManager, + toolInstructions?: Record ): Promise> { const [provider, modelId] = modelString.split(":"); @@ -89,21 +114,23 @@ export async function getToolsForModel( // Try to add provider-specific web search tools if available // Lazy-load providers to avoid loading all AI SDKs at startup + let allTools = baseTools; try { switch (provider) { case "anthropic": { const { anthropic } = await import("@ai-sdk/anthropic"); - return { + allTools = { ...baseTools, web_search: anthropic.tools.webSearch_20250305({ maxUses: 1000 }), }; + break; } case "openai": { // Only add web search for models that support it if (modelId.includes("gpt-5") || modelId.includes("gpt-4")) { const { openai } = await import("@ai-sdk/openai"); - return { + allTools = { ...baseTools, web_search: openai.tools.webSearch({ searchContextSize: "high", @@ -119,9 +146,23 @@ export async function getToolsForModel( // - https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#native-tools } } catch (error) { - // If tools aren't available, just return base tools + // If tools aren't available, just use base tools log.error(`No web search tools available for ${provider}:`, error); } - return baseTools; + // Apply tool-specific instructions if provided + if (toolInstructions) { + const augmentedTools: Record = {}; + for (const [toolName, baseTool] of Object.entries(allTools)) { + const instructions = toolInstructions[toolName]; + if (instructions) { + augmentedTools[toolName] = augmentToolDescription(baseTool, instructions); + } else { + augmentedTools[toolName] = baseTool; + } + } + return augmentedTools; + } + + return allTools; } diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index fc29972565..6cb748f913 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -31,7 +31,7 @@ import { import { applyCacheControl } from "@/common/utils/ai/cacheStrategy"; import type { HistoryService } from "./historyService"; import type { PartialService } from "./partialService"; -import { buildSystemMessage } from "./systemMessage"; +import { buildSystemMessage, readToolInstructions } from "./systemMessage"; import { getTokenizerForModel } from "@/node/utils/main/tokenizer"; import { buildProviderOptions } from "@/common/utils/ai/providerOptions"; import type { ThinkingLevel } from "@/common/types/thinking"; @@ -746,6 +746,14 @@ export class AIService extends EventEmitter { const streamToken = this.streamManager.generateStreamToken(); const runtimeTempDir = await this.streamManager.createTempDirForStream(streamToken, runtime); + // Extract tool-specific instructions from AGENTS.md files + const toolInstructions = await readToolInstructions( + metadata, + runtime, + workspacePath, + modelString + ); + // Get model-specific tools with workspace path (correct for local or remote) const allTools = await getToolsForModel( modelString, @@ -756,7 +764,8 @@ export class AIService extends EventEmitter { runtimeTempDir, }, workspaceId, - this.initStateManager + this.initStateManager, + toolInstructions ); // Apply tool policy to filter tools (if policy provided) diff --git a/src/node/services/systemMessage.ts b/src/node/services/systemMessage.ts index 3a0b6e40c3..6817c02bad 100644 --- a/src/node/services/systemMessage.ts +++ b/src/node/services/systemMessage.ts @@ -6,10 +6,12 @@ import { import { extractModeSection, extractModelSection, + extractToolSection, stripScopedInstructionSections, } from "@/node/utils/main/markdown"; import type { Runtime } from "@/node/runtime/Runtime"; import { getMuxHome } from "@/common/constants/paths"; +import { getAvailableTools } from "@/common/utils/tools/toolDefinitions"; // NOTE: keep this in sync with the docs/models.md file @@ -77,6 +79,85 @@ function getSystemDirectory(): string { return getMuxHome(); } +/** + * Extract tool-specific instructions from instruction sources. + * Searches context (workspace/project) first, then falls back to global instructions. + * + * @param globalInstructions Global instructions from ~/.mux/AGENTS.md + * @param contextInstructions Context instructions from workspace/project AGENTS.md + * @param modelString Active model identifier to determine available tools + * @returns Map of tool names to their additional instructions + */ +export function extractToolInstructions( + globalInstructions: string | null, + contextInstructions: string | null, + modelString: string +): Record { + const availableTools = getAvailableTools(modelString); + const toolInstructions: Record = {}; + + for (const toolName of availableTools) { + // Try context instructions first, then global + const content = + (contextInstructions && extractToolSection(contextInstructions, toolName)) ?? + (globalInstructions && extractToolSection(globalInstructions, toolName)) ?? + null; + + if (content) { + toolInstructions[toolName] = content; + } + } + + return toolInstructions; +} + +/** + * Read instruction sources and extract tool-specific instructions. + * Convenience wrapper that combines readInstructionSources and extractToolInstructions. + * + * @param metadata - Workspace metadata (contains projectPath) + * @param runtime - Runtime for reading workspace files (supports SSH) + * @param workspacePath - Workspace directory path + * @param modelString - Active model identifier to determine available tools + * @returns Map of tool names to their additional instructions + */ +export async function readToolInstructions( + metadata: WorkspaceMetadata, + runtime: Runtime, + workspacePath: string, + modelString: string +): Promise> { + const [globalInstructions, contextInstructions] = await readInstructionSources( + metadata, + runtime, + workspacePath + ); + + return extractToolInstructions(globalInstructions, contextInstructions, modelString); +} + +/** + * Read instruction sets from global and context sources. + * Internal helper for buildSystemMessage and extractToolInstructions. + * + * @param metadata - Workspace metadata (contains projectPath) + * @param runtime - Runtime for reading workspace files (supports SSH) + * @param workspacePath - Workspace directory path + * @returns Tuple of [globalInstructions, contextInstructions] + */ +async function readInstructionSources( + metadata: WorkspaceMetadata, + runtime: Runtime, + workspacePath: string +): Promise<[string | null, string | null]> { + const globalInstructions = await readInstructionSet(getSystemDirectory()); + const workspaceInstructions = await readInstructionSetFromRuntime(runtime, workspacePath); + const contextInstructions = + workspaceInstructions ?? (await readInstructionSet(metadata.projectPath)); + + return [globalInstructions, contextInstructions]; +} + /** * Builds a system message for the AI model by combining instruction sources. * @@ -108,10 +189,11 @@ export async function buildSystemMessage( if (!workspacePath) throw new Error("Invalid workspace path: workspacePath is required"); // Read instruction sets - const globalInstructions = await readInstructionSet(getSystemDirectory()); - const workspaceInstructions = await readInstructionSetFromRuntime(runtime, workspacePath); - const contextInstructions = - workspaceInstructions ?? (await readInstructionSet(metadata.projectPath)); + const [globalInstructions, contextInstructions] = await readInstructionSources( + metadata, + runtime, + workspacePath + ); // Combine: global + context (workspace takes precedence over project) after stripping scoped sections const sanitizeScopedInstructions = (input?: string | null): string | undefined => { diff --git a/src/node/utils/main/markdown.test.ts b/src/node/utils/main/markdown.test.ts index b0129a4c3c..ebd7131b6f 100644 --- a/src/node/utils/main/markdown.test.ts +++ b/src/node/utils/main/markdown.test.ts @@ -1,4 +1,4 @@ -import { extractModeSection } from "./markdown"; +import { extractModeSection, extractToolSection, stripScopedInstructionSections } from "./markdown"; describe("extractModeSection", () => { describe("basic extraction", () => { @@ -259,3 +259,215 @@ const x = 1; }); }); }); + +describe("extractToolSection", () => { + describe("basic extraction", () => { + it("should extract content under Tool: bash heading", () => { + const markdown = ` +# General Instructions +Some general content + +# Tool: bash +Use bash conservatively +Prefer single commands + +# Other Section +Other content +`.trim(); + + const result = extractToolSection(markdown, "bash"); + expect(result).toBe("Use bash conservatively\nPrefer single commands"); + }); + + it("should return null when tool section doesn't exist", () => { + const markdown = ` +# General Instructions +Some content + +# Other Section +Other content +`.trim(); + + const result = extractToolSection(markdown, "bash"); + expect(result).toBeNull(); + }); + + it("should return null for empty markdown", () => { + expect(extractToolSection("", "bash")).toBeNull(); + }); + + it("should return null for empty tool name", () => { + expect(extractToolSection("# Tool: bash\nContent", "")).toBeNull(); + }); + }); + + describe("case insensitivity", () => { + it("should match case-insensitive heading", () => { + const markdown = "# TOOL: BASH\nContent here"; + const result = extractToolSection(markdown, "bash"); + expect(result).toBe("Content here"); + }); + + it("should match mixed case heading", () => { + const markdown = "# ToOl: BaSh\nContent here"; + const result = extractToolSection(markdown, "bash"); + expect(result).toBe("Content here"); + }); + + it("should match with case-insensitive tool name parameter", () => { + const markdown = "# Tool: bash\nContent here"; + const result = extractToolSection(markdown, "BASH"); + expect(result).toBe("Content here"); + }); + }); + + describe("multiple tools", () => { + it("should extract specific tool section", () => { + const markdown = ` +# Tool: bash +Bash instructions + +# Tool: file_read +File read instructions + +# Tool: propose_plan +Plan instructions +`.trim(); + + expect(extractToolSection(markdown, "bash")).toBe("Bash instructions"); + expect(extractToolSection(markdown, "file_read")).toBe("File read instructions"); + expect(extractToolSection(markdown, "propose_plan")).toBe("Plan instructions"); + }); + + it("should return only first matching section", () => { + const markdown = ` +# Tool: bash +First bash section + +# Other Section +Other content + +# Tool: bash +Second bash section (should be ignored) +`.trim(); + + const result = extractToolSection(markdown, "bash"); + expect(result).toBe("First bash section"); + expect(result).not.toContain("Second bash section"); + }); + }); + + describe("tool names with underscores", () => { + it("should handle file_read tool", () => { + const markdown = "# Tool: file_read\nRead instructions"; + expect(extractToolSection(markdown, "file_read")).toBe("Read instructions"); + }); + + it("should handle file_edit_replace_string tool", () => { + const markdown = "# Tool: file_edit_replace_string\nReplace instructions"; + expect(extractToolSection(markdown, "file_edit_replace_string")).toBe("Replace instructions"); + }); + }); +}); + +describe("stripScopedInstructionSections", () => { + it("should strip Mode sections", () => { + const markdown = ` +# General +General content + +# Mode: plan +Plan content + +# More General +More general content +`.trim(); + + const result = stripScopedInstructionSections(markdown); + expect(result).toContain("General content"); + expect(result).toContain("More general content"); + expect(result).not.toContain("Plan content"); + }); + + it("should strip Model sections", () => { + const markdown = ` +# General +General content + +# Model: gpt-4 +Model-specific content + +# More General +More general content +`.trim(); + + const result = stripScopedInstructionSections(markdown); + expect(result).toContain("General content"); + expect(result).toContain("More general content"); + expect(result).not.toContain("Model-specific content"); + }); + + it("should strip Tool sections", () => { + const markdown = ` +# General +General content + +# Tool: bash +Tool-specific content + +# More General +More general content +`.trim(); + + const result = stripScopedInstructionSections(markdown); + expect(result).toContain("General content"); + expect(result).toContain("More general content"); + expect(result).not.toContain("Tool-specific content"); + }); + + it("should strip all scoped sections together", () => { + const markdown = ` +# General +General content + +# Mode: plan +Plan content + +# Model: gpt-4 +Model content + +# Tool: bash +Tool content + +# More General +More general content +`.trim(); + + const result = stripScopedInstructionSections(markdown); + expect(result).toContain("General content"); + expect(result).toContain("More general content"); + expect(result).not.toContain("Plan content"); + expect(result).not.toContain("Model content"); + expect(result).not.toContain("Tool content"); + }); + + it("should return empty string for markdown with only scoped sections", () => { + const markdown = ` +# Mode: plan +Plan content + +# Model: gpt-4 +Model content + +# Tool: bash +Tool content +`.trim(); + + const result = stripScopedInstructionSections(markdown); + expect(result.trim()).toBe(""); + }); + + it("should handle empty markdown", () => { + expect(stripScopedInstructionSections("")).toBe(""); + }); +}); diff --git a/src/node/utils/main/markdown.ts b/src/node/utils/main/markdown.ts index ba4ee75427..14cc88c0ff 100644 --- a/src/node/utils/main/markdown.ts +++ b/src/node/utils/main/markdown.ts @@ -130,11 +130,28 @@ export function extractModelSection(markdown: string, modelId: string): string | }); } +/** + * Extract the content under a heading titled "Tool: " (case-insensitive). + */ +export function extractToolSection(markdown: string, toolName: string): string | null { + if (!markdown || !toolName) return null; + + const expectedHeading = `tool: ${toolName}`.toLowerCase(); + return extractSectionByHeading( + markdown, + (headingText) => headingText.toLowerCase() === expectedHeading + ); +} + export function stripScopedInstructionSections(markdown: string): string { if (!markdown) return markdown; return removeSectionsByHeading(markdown, (headingText) => { const normalized = headingText.trim().toLowerCase(); - return normalized.startsWith("mode:") || normalized.startsWith("model:"); + return ( + normalized.startsWith("mode:") || + normalized.startsWith("model:") || + normalized.startsWith("tool:") + ); }); } From c8a6e4169849dc7a8636041cab6813afa8afafd8 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 23 Nov 2025 18:43:55 -0600 Subject: [PATCH 2/7] docs: consolidate scoped instruction rules to reduce duplication --- docs/instruction-files.md | 110 ++++++++++++++------------------------ 1 file changed, 41 insertions(+), 69 deletions(-) diff --git a/docs/instruction-files.md b/docs/instruction-files.md index 7c15a0c1b2..538c2adfaa 100644 --- a/docs/instruction-files.md +++ b/docs/instruction-files.md @@ -11,117 +11,89 @@ Priority within each location: `AGENTS.md` → `AGENT.md` → `CLAUDE.md` (first > **Note:** mux strips HTML-style markdown comments (``) from instruction files before sending them to the model. Use these comments for editor-only metadata—they will not reach the agent. -## Mode Prompts +## Scoped Instructions -> Use mode-specific sections to optimize context and customize the behavior specific modes. +mux supports **Scoped Instructions** that activate only in specific contexts. You define them using special headings in your instruction files: -mux reads mode context from sections inside your instruction files. Add a heading titled: +- `Mode: ` — Active only in specific interaction modes (e.g., plan, exec). +- `Model: ` — Active only for specific models (e.g., GPT-4, Claude). +- `Tool: ` — Appended to the description of specific tools. -- `Mode: ` (case-insensitive), at any heading level (`#` .. `######`) +### General Rules -Rules: +- **Precedence**: Workspace instructions (`/AGENTS.md`) are checked first, then global instructions (`~/.mux/AGENTS.md`). +- **First Match Wins**: Only the *first* matching section found is used. Overriding global defaults is as simple as defining the same section in your workspace. +- **Isolation**: These sections are **stripped** from the general `` block. Their content is injected only where it belongs (e.g., into a specific tool's description or a special XML tag). +- **Boundaries**: A section's content includes everything until the next heading of the same or higher level. -- Workspace instructions are checked first, then global instructions -- The first matching section wins (at most one section is used) -- The section's content is everything until the next heading of the same or higher level -- Mode sections are stripped from the general `` block; only the active mode's content is re-sent via its `` tag. -- Missing sections are ignored (no error) +--- - +### Mode Prompts -Example (in either `~/.mux/AGENTS.md` or `my-project/AGENTS.md`): +Use mode-specific sections to optimize context and customize behavior for specific workflow stages. The active mode's content is injected via a `` tag. + +**Syntax**: `Mode: ` (case-insensitive) + +**Example**: ```markdown # General Instructions - - Be concise -- Prefer TDD ## Mode: Plan - When planning: - -- Focus on goals, constraints, and trade-offs +- Focus on goals and trade-offs - Propose alternatives with pros/cons -- Defer implementation detail unless asked ## Mode: Compact - -When compacting conversation history: - -- Preserve key decisions and their rationale -- Keep code snippets that are still relevant -- Maintain context about ongoing tasks -- Be extremely concise—prioritize information density +- Preserve key decisions +- Be extremely concise ``` -### Available modes - -- **exec** - Default mode for normal operations -- **plan** - Activated when the user toggles plan mode in the UI -- **compact** - Automatically used during `/compact` operations to guide how the AI summarizes conversation history - -Customizing the `compact` mode is particularly useful for controlling what information is preserved during automatic history compaction. - -## Model Prompts +**Available modes**: +- **exec** (default) — Normal operations. +- **plan** — Active in Plan Mode. +- **compact** — Used during `/compact` to guide history summarization. -Similar to modes, mux reads headings titled `Model: ` to scope instructions to specific models or families. The `` is matched against the full model identifier (for example, `openai:gpt-5.1-codex`). +### Model Prompts -Rules: +Scope instructions to specific models or families using regex matching. The matched content is injected via a `` tag. -- Workspace instructions are evaluated before global instructions; the first matching section wins. -- Regexes are case-insensitive by default. Use `/pattern/flags` syntax to opt into custom flags (e.g., `/openai:.*codex/i`). -- Invalid regex patterns are ignored instead of breaking the parse. -- Model sections are also removed from ``; only the first regex match (if any) is injected via its `` tag. -- Only the content under the first matching heading is injected. +**Syntax**: `Model: ` +- Regexes are case-insensitive by default. +- Use `/pattern/flags` for custom flags (e.g., `/openai:.*codex/i`). - - -Example: +**Example**: ```markdown ## Model: sonnet - Be terse and to the point. -## Model: openai:.\*codex - +## Model: openai:.*codex Use status reporting tools every few minutes. ``` -## Tool Prompts +### Tool Prompts -Like modes and models, mux reads headings titled `Tool: ` to add instructions to specific tool descriptions. This allows you to customize how the AI uses particular tools without cluttering general instructions. +Customize how the AI uses specific tools by appending instructions to their descriptions. -Rules: - -- Workspace instructions are evaluated before global instructions; the first matching section wins. +**Syntax**: `Tool: ` - Tool names must match exactly (case-insensitive). -- Tool sections are removed from ``; matching content is injected into the tool's description. -- Only tools available for the active model are augmented (e.g., `web_search` for Anthropic models). - - +- Only tools available for the active model are augmented. -Example: +**Example**: ```markdown ## Tool: bash - -When running commands: - -- Always use absolute paths for clarity -- Prefer single-line commands over multi-line scripts -- Add `set -e` to scripts that should fail fast +- Always use absolute paths +- Add `set -e` to scripts ## Tool: file_edit_replace_string - -When editing files: - -- Include enough context (2-3 lines) around the replacement to make matches unique -- Verify the edit succeeded by reading the file afterwards +- Include 2-3 lines of context +- Verify edits by reading the file afterwards ``` -Available tools include: `bash`, `file_read`, `file_edit_replace_string`, `file_edit_insert`, `propose_plan`, `todo_write`, `todo_read`, `status_set`, and `web_search` (Anthropic/OpenAI models only). +**Available tools**: `bash`, `file_read`, `file_edit_replace_string`, `file_edit_insert`, `propose_plan`, `todo_write`, `todo_read`, `status_set`, `web_search`. ## Practical layout From 06b877e4d79f9a004c31279a305a93347c50926e Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 23 Nov 2025 18:47:17 -0600 Subject: [PATCH 3/7] docs: update Tool prompt syntax examples --- docs/instruction-files.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/instruction-files.md b/docs/instruction-files.md index 538c2adfaa..f9fdb81139 100644 --- a/docs/instruction-files.md +++ b/docs/instruction-files.md @@ -85,12 +85,13 @@ Customize how the AI uses specific tools by appending instructions to their desc ```markdown ## Tool: bash -- Always use absolute paths -- Add `set -e` to scripts +- Use `rg` instead of `grep` for file searching ## Tool: file_edit_replace_string -- Include 2-3 lines of context -- Verify edits by reading the file afterwards +- Run `prettier --write` after editing files + +# Tool: status_set +- Set status url to the Pull Request once opened ``` **Available tools**: `bash`, `file_read`, `file_edit_replace_string`, `file_edit_insert`, `propose_plan`, `todo_write`, `todo_read`, `status_set`, `web_search`. From 0b346af2b47d60fc6fb4bcf3cd84cf259690aa12 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 23 Nov 2025 18:49:47 -0600 Subject: [PATCH 4/7] fix: lint and formatting issues --- docs/instruction-files.md | 16 ++++++++++++++-- src/common/utils/tools/tools.ts | 8 +++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/instruction-files.md b/docs/instruction-files.md index f9fdb81139..3264ce7311 100644 --- a/docs/instruction-files.md +++ b/docs/instruction-files.md @@ -22,7 +22,7 @@ mux supports **Scoped Instructions** that activate only in specific contexts. Yo ### General Rules - **Precedence**: Workspace instructions (`/AGENTS.md`) are checked first, then global instructions (`~/.mux/AGENTS.md`). -- **First Match Wins**: Only the *first* matching section found is used. Overriding global defaults is as simple as defining the same section in your workspace. +- **First Match Wins**: Only the _first_ matching section found is used. Overriding global defaults is as simple as defining the same section in your workspace. - **Isolation**: These sections are **stripped** from the general `` block. Their content is injected only where it belongs (e.g., into a specific tool's description or a special XML tag). - **Boundaries**: A section's content includes everything until the next heading of the same or higher level. @@ -38,19 +38,24 @@ Use mode-specific sections to optimize context and customize behavior for specif ```markdown # General Instructions + - Be concise ## Mode: Plan + When planning: + - Focus on goals and trade-offs - Propose alternatives with pros/cons ## Mode: Compact + - Preserve key decisions - Be extremely concise ``` **Available modes**: + - **exec** (default) — Normal operations. - **plan** — Active in Plan Mode. - **compact** — Used during `/compact` to guide history summarization. @@ -60,6 +65,7 @@ When planning: Scope instructions to specific models or families using regex matching. The matched content is injected via a `` tag. **Syntax**: `Model: ` + - Regexes are case-insensitive by default. - Use `/pattern/flags` for custom flags (e.g., `/openai:.*codex/i`). @@ -67,9 +73,11 @@ Scope instructions to specific models or families using regex matching. The matc ```markdown ## Model: sonnet + Be terse and to the point. -## Model: openai:.*codex +## Model: openai:.\*codex + Use status reporting tools every few minutes. ``` @@ -78,6 +86,7 @@ Use status reporting tools every few minutes. Customize how the AI uses specific tools by appending instructions to their descriptions. **Syntax**: `Tool: ` + - Tool names must match exactly (case-insensitive). - Only tools available for the active model are augmented. @@ -85,12 +94,15 @@ Customize how the AI uses specific tools by appending instructions to their desc ```markdown ## Tool: bash + - Use `rg` instead of `grep` for file searching ## Tool: file_edit_replace_string + - Run `prettier --write` after editing files # Tool: status_set + - Set status url to the Pull Request once opened ``` diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts index 76d7cadbd0..54cceb0616 100644 --- a/src/common/utils/tools/tools.ts +++ b/src/common/utils/tools/tools.ts @@ -47,14 +47,16 @@ function augmentToolDescription(baseTool: Tool, additionalInstructions: string): // Access the tool as a record to get its properties // eslint-disable-next-line @typescript-eslint/no-explicit-any const baseToolRecord = baseTool as any as Record; - const augmentedDescription = `${String(baseToolRecord.description ?? "")}\n\n${additionalInstructions}`; + const originalDescription = + typeof baseToolRecord.description === "string" ? baseToolRecord.description : ""; + const augmentedDescription = `${originalDescription}\n\n${additionalInstructions}`; // Return a new tool with the augmented description return tool({ description: augmentedDescription, - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment inputSchema: baseToolRecord.inputSchema as any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment execute: baseToolRecord.execute as any, }); } From 61875bdb7db6696640eb1e9bfdf3f4c2ffb9a1f7 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 23 Nov 2025 18:55:51 -0600 Subject: [PATCH 5/7] fix: mutate tool in place to preserve provider metadata --- src/common/utils/tools/tools.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts index 54cceb0616..1017be496a 100644 --- a/src/common/utils/tools/tools.ts +++ b/src/common/utils/tools/tools.ts @@ -1,4 +1,4 @@ -import { type Tool, tool } from "ai"; +import { type Tool } from "ai"; import { createFileReadTool } from "@/node/services/tools/file_read"; import { createBashTool } from "@/node/services/tools/bash"; import { createFileEditReplaceStringTool } from "@/node/services/tools/file_edit_replace_string"; @@ -38,10 +38,11 @@ export type ToolFactory = (config: ToolConfiguration) => Tool; /** * Augment a tool's description with additional instructions from "Tool: " sections - * Creates a wrapper tool that delegates to the base tool but with an enhanced description. + * Mutates the base tool in place to append the instructions to its description. + * This preserves any provider-specific metadata or internal state on the tool object. * @param baseTool The original tool to augment * @param additionalInstructions Additional instructions to append to the description - * @returns A new tool with the augmented description + * @returns The same tool instance with the augmented description */ function augmentToolDescription(baseTool: Tool, additionalInstructions: string): Tool { // Access the tool as a record to get its properties @@ -51,14 +52,10 @@ function augmentToolDescription(baseTool: Tool, additionalInstructions: string): typeof baseToolRecord.description === "string" ? baseToolRecord.description : ""; const augmentedDescription = `${originalDescription}\n\n${additionalInstructions}`; - // Return a new tool with the augmented description - return tool({ - description: augmentedDescription, - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - inputSchema: baseToolRecord.inputSchema as any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - execute: baseToolRecord.execute as any, - }); + // Mutate the description in place to preserve other properties (e.g. provider metadata) + baseToolRecord.description = augmentedDescription; + + return baseTool; } /** From 0f4f92a3e59a76114212ee70bd2dbcdf44095ca8 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 23 Nov 2025 20:01:51 -0600 Subject: [PATCH 6/7] docs: add status_set tool instruction example --- docs/AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 262cbb4a89..91554e07d9 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -155,3 +155,7 @@ gh pr view --json mergeable,mergeStateStatus | jq '.' - When Plan Mode is requested, assume the user wants the actual completed plan; do not merely describe how you would devise one. - Attach a net LoC estimate (product code only) to each recommended approach. + +## Tool: status_set + +- Set status url to the Pull Request once opened \ No newline at end of file From dbd3cf2ff48afb6163de5c4d66645588e360347b Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 24 Nov 2025 10:35:42 -0600 Subject: [PATCH 7/7] fix: format AGENTS.md --- docs/AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 91554e07d9..b5ad51f6ab 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -158,4 +158,4 @@ gh pr view --json mergeable,mergeStateStatus | jq '.' ## Tool: status_set -- Set status url to the Pull Request once opened \ No newline at end of file +- Set status url to the Pull Request once opened