Tool Context Engineering
As applications grow, the number of available tools can overwhelm the agent's context window. Tool Context Engineering is about managing this complexity through collapsing (hierarchical organization) and instruction placement (where guidance appears).
The Problem: Tool Bloat
An agent with 50+ tools faces issues:
- Token waste: Every tool description consumes context
- Decision fatigue: Too many choices slow down the agent
- Irrelevant options: Most tools aren't needed for any given task
Agent with 50 tools:
┌─────────────────────────────────────────────────────────┐
│ WebSearch, CodeSearch, DocSearch, FileRead, FileWrite, │
│ FileDelete, GitCommit, GitPush, GitPull, GitStatus, │
│ DatabaseQuery, DatabaseInsert, DatabaseUpdate, ... │
│ (thousands of tokens just for tool definitions) │
└─────────────────────────────────────────────────────────┘The Solution: Hierarchical Collapsing
Collapse tools into containers that expand on demand:
Before expansion: After user asks about files:
┌──────────────────┐ ┌──────────────────┐
│ SearchTools │ │ SearchTools │
│ FileTools │ ──► │ FileRead │
│ GitTools │ │ FileWrite │
│ DatabaseTools │ │ FileDelete │
└──────────────────┘ │ GitTools │
│ DatabaseTools │
4 tools visible └──────────────────┘
6 tools visibleThis reduces initial token usage while keeping all capabilities accessible.
The Toolkit Attribute
Use [Toolkit("description")] to make a toolkit collapsible:
[Toolkit("File operations for reading, writing, and managing files")]
public class FileSystemToolkit
{
[AIFunction]
public string ReadFile(string path) { /* ... */ }
[AIFunction]
public void WriteFile(string path, string content) { /* ... */ }
[AIFunction]
public void DeleteFile(string path) { /* ... */ }
}How the description works:
- You provide the purpose (e.g., "File operations for reading, writing, and managing files")
- The system auto-generates the function list (e.g., "Container FileSystemToolkit provides access to: ReadFile, WriteFile, DeleteFile")
- The final description combines both:
"{auto-generated function list}. {your description}"
Note: Providing a description enables collapsing. Without a description ([Toolkit] or [Toolkit(Name = "...")]), functions are always visible.
Toolkit with Dual-Context Instructions
The [Toolkit] attribute supports two instruction injection points:
[Toolkit(
"Database operations",
FunctionResult: "Use transactions for multiple operations.",
SystemPrompt: "CRITICAL: Never execute DELETE without a WHERE clause. Always validate inputs."
)]
public class DatabaseToolkit
{
// Functions...
}FunctionResult vs SystemPrompt
These two properties serve different purposes and behave differently:
| Property | Injection Target | When | Persistence | Use For |
|---|---|---|---|---|
FunctionResult | Tool call result | Once on expansion | In message history | Listing functions, one-time guidance |
SystemPrompt | System instructions | Every turn | Configurable | Critical rules, behavioral constraints |
FunctionResult
Returned as the result of "calling" the container. It's a one-time message that stays in conversation history. This is appended to the auto-generated expansion message.
[Toolkit(
"Search operations",
FunctionResult: "Tip: Start with WebSearch for general queries."
)]
public class SearchToolkit { /* ... */ }Agent sees on expansion:
"SearchToolkit expanded. Available functions: WebSearch, CodeSearch, DocSearch
Tip: Start with WebSearch for general queries."SystemPrompt
Injected into the system instructions while the container is expanded. Use for rules that must be enforced every turn.
[Toolkit(
"File operations",
SystemPrompt: @"
FILE OPERATION RULES:
- Always check if a file exists before reading
- Never overwrite without user confirmation
- Use absolute paths only"
)]
public class FileSystemToolkit { /* ... */ }During each turn while expanded:
System: You are a helpful assistant.
FILE OPERATION RULES:
- Always check if a file exists before reading
- Never overwrite without user confirmation
- Use absolute paths only
User: Read the config file.
Agent: (follows the rules because they're in system prompt)Context Lifecycle
Understanding when instructions appear and disappear:
Turn 1: User asks about files
├── Agent calls FileToolkit container
├── FunctionResult returned: "FileToolkit expanded. Available functions: ReadFile, WriteFile..."
├── SystemPrompt injected: "FILE RULES: ..."
└── Turn ends
Turn 2: Agent reads a file
├── SystemPrompt still present (functions still expanded)
├── Agent uses ReadFile
└── Turn ends
Turn 3: User asks about something else
├── SystemPrompt cleared (by default)
├── File functions still available but rules gone
└── Turn ends
[If PersistSystemPromptInjections = true]
Turn 3: User asks about something else
├── SystemPrompt STILL present
├── Accumulating prompts can bloat context
└── Turn endsCollapsingConfig
Configure collapsing behavior in AgentConfig:
var config = new AgentConfig
{
Collapsing = new CollapsingConfig
{
// Master switch for C# tool collapsing
Enabled = true,
// Also collapse client-provided tools
CollapseClientTools = false,
// Max functions listed in container description
MaxFunctionNamesInDescription = 10,
// Keep SystemPrompt injections across turns (default: false)
PersistSystemPromptInjections = false,
// How skill instructions are delivered
SkillInstructionMode = SkillInstructionMode.Both,
// Runtime override: these toolkits never collapse even if they have descriptions
NeverCollapse = new HashSet<string> { "CoreToolkit", "DebugToolkit" },
// Custom instructions for MCP servers
MCPServerInstructions = new Dictionary<string, string>
{
["filesystem"] = "Always use absolute paths.",
["github"] = "Prefer GraphQL API over REST when possible."
},
// Instructions for client-provided tools
ClientToolsInstructions = "These tools interact with the user's IDE."
}
};Configuration Options Explained
Enabled
Enabled = true // Toolkits with descriptions are collapsible
Enabled = false // All functions exposed directly (no containers)CollapseClientTools
CollapseClientTools = true // Group client tools under a container
CollapseClientTools = false // Client tools always visibleMaxFunctionNamesInDescription
Controls how many function names appear in the container description:
MaxFunctionNamesInDescription = 10
// Container shows: "File operations (ReadFile, WriteFile, DeleteFile, ...)"
MaxFunctionNamesInDescription = 3
// Container shows: "File operations (ReadFile, WriteFile, DeleteFile)"
MaxFunctionNamesInDescription = 0
// Container shows: "File operations"PersistSystemPromptInjections
PersistSystemPromptInjections = false // Recommended: Clear after each turn
PersistSystemPromptInjections = true // Keep accumulating (use carefully)Why false is recommended: If multiple containers expand, their SystemPrompts accumulate. Clearing prevents context bloat.
NeverCollapse
Runtime override to prevent specific toolkits from collapsing, even if they have descriptions:
NeverCollapse = new HashSet<string> { "CoreToolkit", "DebugToolkit" }Use cases:
- Core tools that should always be visible (e.g., file operations)
- Debug/development toolkits that you don't want collapsed during testing
- Environment-specific overrides (collapse in production, expand in development)
How it works:
- The container is still generated at compile time (description still provided)
- At runtime,
ToolVisibilityManagertreats toolkits inNeverCollapseas non-containers - Functions are shown directly; the container is hidden
// Toolkit has a description, so container is generated
[Toolkit("File operations for reading, writing, and managing files")]
public class FileToolkit { /* ... */ }
// But at runtime, if "FileToolkit" is in NeverCollapse:
// - Container is hidden
// - ReadFile, WriteFile, DeleteFile are visible directlySkillInstructionMode
Controls how skill instructions are delivered:
public enum SkillInstructionMode
{
PromptMiddlewareOnly, // Instructions only in system prompt
Both // Instructions in system prompt AND function result
}PromptMiddlewareOnly
Most token-efficient. Instructions appear once in system prompt:
System: ... [Skill: Research] 1. Search web first 2. Search code if relevant ...Both (Default)
Backward-compatible. Instructions appear in both places:
System: ... [Skill: Research] 1. Search web first ...
FunctionResult: "Skill activated. Instructions: 1. Search web first ..."Dynamic Instructions with Expressions
Reference methods or properties for runtime-generated instructions:
[Toolkit(
"Search operations",
FunctionResult: nameof(GetAvailableFunctions),
SystemPrompt: nameof(SearchRules)
)]
public class SearchToolkit
{
private static readonly List<string> _enabledProviders = new() { "Web", "Code" };
// Called at expansion time
public static string GetAvailableFunctions()
{
return $"Available search providers: {string.Join(", ", _enabledProviders)}";
}
// Property for static rules
public static string SearchRules => @"
SEARCH RULES:
- Always cite sources
- Prefer recent results";
[AIFunction]
public async Task<string> WebSearch(string query) { /* ... */ }
}MCP Server Instructions
Provide instructions for Model Context Protocol servers:
Collapsing = new CollapsingConfig
{
MCPServerInstructions = new Dictionary<string, string>
{
["filesystem"] = @"
When using filesystem tools:
- Always use absolute paths
- Check file existence before reading
- Request confirmation before deleting",
["github"] = @"
When using GitHub tools:
- Prefer the GraphQL API for bulk operations
- Always include PR descriptions
- Link related issues"
}
}These instructions are injected when the respective MCP server's tools are used.
Best Practices
1. Use FunctionResult for Orientation
FunctionResult: @"
Available functions:
- Query: Read data (SELECT)
- Insert: Add records
- Update: Modify records
- Delete: Remove records (use carefully)
For complex operations, use Query first to understand the schema."2. Use SystemPrompt for Critical Rules
SystemPrompt: @"
MANDATORY DATABASE RULES:
1. NEVER execute DELETE without WHERE clause
2. ALWAYS use parameterized queries
3. LIMIT results to 1000 rows maximum"3. Keep Instructions Concise
// Good: Brief, actionable
SystemPrompt: "Always use transactions for multiple operations. Rollback on any error."
// Bad: Novel-length instructions
SystemPrompt: "When working with databases, it's important to remember that... [500 words]"4. Don't Persist Unless Necessary
// Recommended for most cases
PersistSystemPromptInjections = false
// Only use true when rules MUST persist across many turns
PersistSystemPromptInjections = true5. Group Related Functions Logically
// Good: Coherent grouping
[Toolkit("Git version control operations")]
public class GitToolkit { /* commit, push, pull, status, diff */ }
// Bad: Unrelated functions forced together
[Toolkit("Misc utilities")]
public class MiscToolkit { /* sendEmail, readFile, getCurrentTime, translateText */ }Complete Example
// Agent configuration
var config = new AgentConfig
{
Collapsing = new CollapsingConfig
{
Enabled = true,
MaxFunctionNamesInDescription = 5,
PersistSystemPromptInjections = false,
SkillInstructionMode = SkillInstructionMode.PromptMiddlewareOnly,
MCPServerInstructions = new Dictionary<string, string>
{
["filesystem"] = "Use absolute paths. Confirm before delete."
}
}
};
// Toolkit with collapsing
[Toolkit(
"Database operations for querying and modifying data",
FunctionResult: nameof(GetDatabaseInfo),
SystemPrompt: "CRITICAL: Use transactions for multi-step operations. Never DELETE without WHERE."
)]
public class DatabaseToolkit
{
public static string GetDatabaseInfo() =>
$"Connected to: {_connectionString}. Tables: {string.Join(", ", _tables)}";
[AIFunction]
[AIDescription("Execute a SELECT query")]
public async Task<string> Query(string sql) { /* ... */ }
[AIFunction]
[AIDescription("Insert a new record")]
public async Task<string> Insert(string table, string json) { /* ... */ }
[AIFunction]
[AIDescription("Update existing records")]
[RequiresPermission]
public async Task<string> Update(string table, string where, string json) { /* ... */ }
[AIFunction]
[AIDescription("Delete records (requires WHERE clause)")]
[RequiresPermission]
public async Task<string> Delete(string table, string where) { /* ... */ }
}
// Build agent
var agent = new AgentBuilder()
.WithConfig(config)
.WithTools<DatabaseToolkit>()
.Build();Agent's initial view:
Tools: DatabaseToolkit (Query, Insert, Update, Delete, ...)After expansion:
System: ... CRITICAL: Use transactions for multi-step operations. Never DELETE without WHERE.
Tools: Query, Insert, Update, Delete
Recent message: "DatabaseToolkit expanded. Available functions: Query, Insert, Update, Delete
Connected to: prod-db. Tables: users, orders, products"Summary
| Concept | Purpose |
|---|---|
[Toolkit("description")] | Group toolkit functions into expandable container |
FunctionResult | One-time orientation message on expansion (appended to auto-generated message) |
SystemPrompt | Persistent rules while functions are active |
CollapsingConfig | Global collapsing behavior settings |
CollapsingConfig.NeverCollapse | Runtime override to prevent specific toolkits from collapsing |
PersistSystemPromptInjections | Keep/clear SystemPrompt between turns |
SkillInstructionMode | How skill instructions are delivered |
MCPServerInstructions | Custom guidance for MCP server tools |
Effective context engineering keeps the agent focused, reduces token usage, and ensures critical rules are followed—without overwhelming the context window.