Skip to content

Commit af9d7ea

Browse files
authored
🤖 feat: add Tool: <tool_name> prompt syntax (#708)
Generated with `mux` Adds support for `Tool: <tool_name>` syntax in instruction files to inject tool-specific instructions into tool descriptions. ## Changes - **Markdown parsing**: Added `extractToolSection()` to parse `Tool: <name>` sections and updated `stripScopedInstructionSections()`. - **System message**: Added `extractToolInstructions()` and `readToolInstructions()` to read and extract tool instructions from global and context sources. - **Tooling**: Modified `getToolsForModel()` to accept tool instructions and added `augmentToolDescription()` to append instructions to tool descriptions. - **Integration**: Updated `aiService` to wire everything together. - **Documentation**: Updated `docs/instruction-files.md` with new syntax rules and examples. - **Tests**: Added comprehensive tests for extraction and stripping logic. ## Behavior - Works like `Mode:` and `Model:` prompts. - Context (workspace) instructions override/augment global instructions (first match wins per tool). - Instructions are stripped from the main system message and appended to the specific tool's description.
1 parent a7808d3 commit af9d7ea

File tree

7 files changed

+434
-51
lines changed

7 files changed

+434
-51
lines changed

docs/AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,7 @@ gh pr view <number> --json mergeable,mergeStateStatus | jq '.'
155155

156156
- When Plan Mode is requested, assume the user wants the actual completed plan; do not merely describe how you would devise one.
157157
- Attach a net LoC estimate (product code only) to each recommended approach.
158+
159+
## Tool: status_set
160+
161+
- Set status url to the Pull Request once opened

docs/instruction-files.md

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,73 +11,65 @@ Priority within each location: `AGENTS.md` → `AGENT.md` → `CLAUDE.md` (first
1111

1212
> **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.
1313
14-
## Mode Prompts
14+
## Scoped Instructions
1515

16-
> Use mode-specific sections to optimize context and customize the behavior specific modes.
16+
mux supports **Scoped Instructions** that activate only in specific contexts. You define them using special headings in your instruction files:
1717

18-
mux reads mode context from sections inside your instruction files. Add a heading titled:
18+
- `Mode: <mode>` — Active only in specific interaction modes (e.g., plan, exec).
19+
- `Model: <regex>` — Active only for specific models (e.g., GPT-4, Claude).
20+
- `Tool: <tool_name>` — Appended to the description of specific tools.
1921

20-
- `Mode: <mode>` (case-insensitive), at any heading level (`#` .. `######`)
22+
### General Rules
2123

22-
Rules:
24+
- **Precedence**: Workspace instructions (`<workspace>/AGENTS.md`) are checked first, then global instructions (`~/.mux/AGENTS.md`).
25+
- **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.
26+
- **Isolation**: These sections are **stripped** from the general `<custom-instructions>` block. Their content is injected only where it belongs (e.g., into a specific tool's description or a special XML tag).
27+
- **Boundaries**: A section's content includes everything until the next heading of the same or higher level.
2328

24-
- Workspace instructions are checked first, then global instructions
25-
- The first matching section wins (at most one section is used)
26-
- The section's content is everything until the next heading of the same or higher level
27-
- Mode sections are stripped from the general `<custom-instructions>` block; only the active mode's content is re-sent via its `<mode>` tag.
28-
- Missing sections are ignored (no error)
29+
---
2930

30-
<!-- Note to developers: This behavior is implemented in src/services/systemMessage.ts (search for extractModeSection). Keep this documentation in sync with code changes. -->
31+
### Mode Prompts
3132

32-
Example (in either `~/.mux/AGENTS.md` or `my-project/AGENTS.md`):
33+
Use mode-specific sections to optimize context and customize behavior for specific workflow stages. The active mode's content is injected via a `<mode>` tag.
34+
35+
**Syntax**: `Mode: <mode>` (case-insensitive)
36+
37+
**Example**:
3338

3439
```markdown
3540
# General Instructions
3641

3742
- Be concise
38-
- Prefer TDD
3943

4044
## Mode: Plan
4145

4246
When planning:
4347

44-
- Focus on goals, constraints, and trade-offs
48+
- Focus on goals and trade-offs
4549
- Propose alternatives with pros/cons
46-
- Defer implementation detail unless asked
4750

4851
## Mode: Compact
4952

50-
When compacting conversation history:
51-
52-
- Preserve key decisions and their rationale
53-
- Keep code snippets that are still relevant
54-
- Maintain context about ongoing tasks
55-
- Be extremely concise—prioritize information density
53+
- Preserve key decisions
54+
- Be extremely concise
5655
```
5756

58-
### Available modes
59-
60-
- **exec** - Default mode for normal operations
61-
- **plan** - Activated when the user toggles plan mode in the UI
62-
- **compact** - Automatically used during `/compact` operations to guide how the AI summarizes conversation history
63-
64-
Customizing the `compact` mode is particularly useful for controlling what information is preserved during automatic history compaction.
57+
**Available modes**:
6558

66-
## Model Prompts
59+
- **exec** (default) — Normal operations.
60+
- **plan** — Active in Plan Mode.
61+
- **compact** — Used during `/compact` to guide history summarization.
6762

68-
Similar to modes, mux reads headings titled `Model: <regex>` to scope instructions to specific models or families. The `<regex>` is matched against the full model identifier (for example, `openai:gpt-5.1-codex`).
63+
### Model Prompts
6964

70-
Rules:
65+
Scope instructions to specific models or families using regex matching. The matched content is injected via a `<model-...>` tag.
7166

72-
- Workspace instructions are evaluated before global instructions; the first matching section wins.
73-
- Regexes are case-insensitive by default. Use `/pattern/flags` syntax to opt into custom flags (e.g., `/openai:.*codex/i`).
74-
- Invalid regex patterns are ignored instead of breaking the parse.
75-
- Model sections are also removed from `<custom-instructions>`; only the first regex match (if any) is injected via its `<model-…>` tag.
76-
- Only the content under the first matching heading is injected.
67+
**Syntax**: `Model: <regex>`
7768

78-
<!-- Developers: See extractModelSection in src/node/utils/main/markdown.ts for the implementation. -->
69+
- Regexes are case-insensitive by default.
70+
- Use `/pattern/flags` for custom flags (e.g., `/openai:.*codex/i`).
7971

80-
Example:
72+
**Example**:
8173

8274
```markdown
8375
## Model: sonnet
@@ -89,6 +81,33 @@ Be terse and to the point.
8981
Use status reporting tools every few minutes.
9082
```
9183

84+
### Tool Prompts
85+
86+
Customize how the AI uses specific tools by appending instructions to their descriptions.
87+
88+
**Syntax**: `Tool: <tool_name>`
89+
90+
- Tool names must match exactly (case-insensitive).
91+
- Only tools available for the active model are augmented.
92+
93+
**Example**:
94+
95+
```markdown
96+
## Tool: bash
97+
98+
- Use `rg` instead of `grep` for file searching
99+
100+
## Tool: file_edit_replace_string
101+
102+
- Run `prettier --write` after editing files
103+
104+
# Tool: status_set
105+
106+
- Set status url to the Pull Request once opened
107+
```
108+
109+
**Available tools**: `bash`, `file_read`, `file_edit_replace_string`, `file_edit_insert`, `propose_plan`, `todo_write`, `todo_read`, `status_set`, `web_search`.
110+
92111
## Practical layout
93112

94113
```

src/common/utils/tools/tools.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ export interface ToolConfiguration {
3636
*/
3737
export type ToolFactory = (config: ToolConfiguration) => Tool;
3838

39+
/**
40+
* Augment a tool's description with additional instructions from "Tool: <name>" sections
41+
* Mutates the base tool in place to append the instructions to its description.
42+
* This preserves any provider-specific metadata or internal state on the tool object.
43+
* @param baseTool The original tool to augment
44+
* @param additionalInstructions Additional instructions to append to the description
45+
* @returns The same tool instance with the augmented description
46+
*/
47+
function augmentToolDescription(baseTool: Tool, additionalInstructions: string): Tool {
48+
// Access the tool as a record to get its properties
49+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50+
const baseToolRecord = baseTool as any as Record<string, unknown>;
51+
const originalDescription =
52+
typeof baseToolRecord.description === "string" ? baseToolRecord.description : "";
53+
const augmentedDescription = `${originalDescription}\n\n${additionalInstructions}`;
54+
55+
// Mutate the description in place to preserve other properties (e.g. provider metadata)
56+
baseToolRecord.description = augmentedDescription;
57+
58+
return baseTool;
59+
}
60+
3961
/**
4062
* Get tools available for a specific model with configuration
4163
*
@@ -46,13 +68,15 @@ export type ToolFactory = (config: ToolConfiguration) => Tool;
4668
* @param config Required configuration for tools
4769
* @param workspaceId Workspace ID for init state tracking (required for runtime tools)
4870
* @param initStateManager Init state manager for runtime tools to wait for initialization
71+
* @param toolInstructions Optional map of tool names to additional instructions from "Tool: <name>" sections
4972
* @returns Promise resolving to record of tools available for the model
5073
*/
5174
export async function getToolsForModel(
5275
modelString: string,
5376
config: ToolConfiguration,
5477
workspaceId: string,
55-
initStateManager: InitStateManager
78+
initStateManager: InitStateManager,
79+
toolInstructions?: Record<string, string>
5680
): Promise<Record<string, Tool>> {
5781
const [provider, modelId] = modelString.split(":");
5882

@@ -89,21 +113,23 @@ export async function getToolsForModel(
89113

90114
// Try to add provider-specific web search tools if available
91115
// Lazy-load providers to avoid loading all AI SDKs at startup
116+
let allTools = baseTools;
92117
try {
93118
switch (provider) {
94119
case "anthropic": {
95120
const { anthropic } = await import("@ai-sdk/anthropic");
96-
return {
121+
allTools = {
97122
...baseTools,
98123
web_search: anthropic.tools.webSearch_20250305({ maxUses: 1000 }),
99124
};
125+
break;
100126
}
101127

102128
case "openai": {
103129
// Only add web search for models that support it
104130
if (modelId.includes("gpt-5") || modelId.includes("gpt-4")) {
105131
const { openai } = await import("@ai-sdk/openai");
106-
return {
132+
allTools = {
107133
...baseTools,
108134
web_search: openai.tools.webSearch({
109135
searchContextSize: "high",
@@ -119,9 +145,23 @@ export async function getToolsForModel(
119145
// - https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#native-tools
120146
}
121147
} catch (error) {
122-
// If tools aren't available, just return base tools
148+
// If tools aren't available, just use base tools
123149
log.error(`No web search tools available for ${provider}:`, error);
124150
}
125151

126-
return baseTools;
152+
// Apply tool-specific instructions if provided
153+
if (toolInstructions) {
154+
const augmentedTools: Record<string, Tool> = {};
155+
for (const [toolName, baseTool] of Object.entries(allTools)) {
156+
const instructions = toolInstructions[toolName];
157+
if (instructions) {
158+
augmentedTools[toolName] = augmentToolDescription(baseTool, instructions);
159+
} else {
160+
augmentedTools[toolName] = baseTool;
161+
}
162+
}
163+
return augmentedTools;
164+
}
165+
166+
return allTools;
127167
}

src/node/services/aiService.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
import { applyCacheControl } from "@/common/utils/ai/cacheStrategy";
3232
import type { HistoryService } from "./historyService";
3333
import type { PartialService } from "./partialService";
34-
import { buildSystemMessage } from "./systemMessage";
34+
import { buildSystemMessage, readToolInstructions } from "./systemMessage";
3535
import { getTokenizerForModel } from "@/node/utils/main/tokenizer";
3636
import { buildProviderOptions } from "@/common/utils/ai/providerOptions";
3737
import type { ThinkingLevel } from "@/common/types/thinking";
@@ -746,6 +746,14 @@ export class AIService extends EventEmitter {
746746
const streamToken = this.streamManager.generateStreamToken();
747747
const runtimeTempDir = await this.streamManager.createTempDirForStream(streamToken, runtime);
748748

749+
// Extract tool-specific instructions from AGENTS.md files
750+
const toolInstructions = await readToolInstructions(
751+
metadata,
752+
runtime,
753+
workspacePath,
754+
modelString
755+
);
756+
749757
// Get model-specific tools with workspace path (correct for local or remote)
750758
const allTools = await getToolsForModel(
751759
modelString,
@@ -756,7 +764,8 @@ export class AIService extends EventEmitter {
756764
runtimeTempDir,
757765
},
758766
workspaceId,
759-
this.initStateManager
767+
this.initStateManager,
768+
toolInstructions
760769
);
761770

762771
// Apply tool policy to filter tools (if policy provided)

src/node/services/systemMessage.ts

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66
import {
77
extractModeSection,
88
extractModelSection,
9+
extractToolSection,
910
stripScopedInstructionSections,
1011
} from "@/node/utils/main/markdown";
1112
import type { Runtime } from "@/node/runtime/Runtime";
1213
import { getMuxHome } from "@/common/constants/paths";
14+
import { getAvailableTools } from "@/common/utils/tools/toolDefinitions";
1315

1416
// NOTE: keep this in sync with the docs/models.md file
1517

@@ -77,6 +79,85 @@ function getSystemDirectory(): string {
7779
return getMuxHome();
7880
}
7981

82+
/**
83+
* Extract tool-specific instructions from instruction sources.
84+
* Searches context (workspace/project) first, then falls back to global instructions.
85+
*
86+
* @param globalInstructions Global instructions from ~/.mux/AGENTS.md
87+
* @param contextInstructions Context instructions from workspace/project AGENTS.md
88+
* @param modelString Active model identifier to determine available tools
89+
* @returns Map of tool names to their additional instructions
90+
*/
91+
export function extractToolInstructions(
92+
globalInstructions: string | null,
93+
contextInstructions: string | null,
94+
modelString: string
95+
): Record<string, string> {
96+
const availableTools = getAvailableTools(modelString);
97+
const toolInstructions: Record<string, string> = {};
98+
99+
for (const toolName of availableTools) {
100+
// Try context instructions first, then global
101+
const content =
102+
(contextInstructions && extractToolSection(contextInstructions, toolName)) ??
103+
(globalInstructions && extractToolSection(globalInstructions, toolName)) ??
104+
null;
105+
106+
if (content) {
107+
toolInstructions[toolName] = content;
108+
}
109+
}
110+
111+
return toolInstructions;
112+
}
113+
114+
/**
115+
* Read instruction sources and extract tool-specific instructions.
116+
* Convenience wrapper that combines readInstructionSources and extractToolInstructions.
117+
*
118+
* @param metadata - Workspace metadata (contains projectPath)
119+
* @param runtime - Runtime for reading workspace files (supports SSH)
120+
* @param workspacePath - Workspace directory path
121+
* @param modelString - Active model identifier to determine available tools
122+
* @returns Map of tool names to their additional instructions
123+
*/
124+
export async function readToolInstructions(
125+
metadata: WorkspaceMetadata,
126+
runtime: Runtime,
127+
workspacePath: string,
128+
modelString: string
129+
): Promise<Record<string, string>> {
130+
const [globalInstructions, contextInstructions] = await readInstructionSources(
131+
metadata,
132+
runtime,
133+
workspacePath
134+
);
135+
136+
return extractToolInstructions(globalInstructions, contextInstructions, modelString);
137+
}
138+
139+
/**
140+
* Read instruction sets from global and context sources.
141+
* Internal helper for buildSystemMessage and extractToolInstructions.
142+
*
143+
* @param metadata - Workspace metadata (contains projectPath)
144+
* @param runtime - Runtime for reading workspace files (supports SSH)
145+
* @param workspacePath - Workspace directory path
146+
* @returns Tuple of [globalInstructions, contextInstructions]
147+
*/
148+
async function readInstructionSources(
149+
metadata: WorkspaceMetadata,
150+
runtime: Runtime,
151+
workspacePath: string
152+
): Promise<[string | null, string | null]> {
153+
const globalInstructions = await readInstructionSet(getSystemDirectory());
154+
const workspaceInstructions = await readInstructionSetFromRuntime(runtime, workspacePath);
155+
const contextInstructions =
156+
workspaceInstructions ?? (await readInstructionSet(metadata.projectPath));
157+
158+
return [globalInstructions, contextInstructions];
159+
}
160+
80161
/**
81162
* Builds a system message for the AI model by combining instruction sources.
82163
*
@@ -108,10 +189,11 @@ export async function buildSystemMessage(
108189
if (!workspacePath) throw new Error("Invalid workspace path: workspacePath is required");
109190

110191
// Read instruction sets
111-
const globalInstructions = await readInstructionSet(getSystemDirectory());
112-
const workspaceInstructions = await readInstructionSetFromRuntime(runtime, workspacePath);
113-
const contextInstructions =
114-
workspaceInstructions ?? (await readInstructionSet(metadata.projectPath));
192+
const [globalInstructions, contextInstructions] = await readInstructionSources(
193+
metadata,
194+
runtime,
195+
workspacePath
196+
);
115197

116198
// Combine: global + context (workspace takes precedence over project) after stripping scoped sections
117199
const sanitizeScopedInstructions = (input?: string | null): string | undefined => {

0 commit comments

Comments
 (0)