Intercept and Control Tool Invocations
When an LLM decides to call a tool, you might need to inspect the call before it executes, log it for auditing, block it based on security rules, or modify the result after execution. LM-Kit.NET exposes BeforeToolInvocation and AfterToolInvocation events on MultiTurnConversation and Agent that give you full control over the tool execution lifecycle. This tutorial builds a system with pre-invocation filtering, post-invocation logging, and security-based tool blocking.
Why Tool Interception Matters
Two enterprise problems that tool invocation control solves:
- Security auditing in regulated environments. Every tool call (web search, file access, API call) must be logged with the exact arguments for compliance. Before/after events provide a clean interception point without modifying the tools themselves or the agent's reasoning logic.
- Cost control for expensive operations. Blocking or rate-limiting tool calls that hit paid APIs (web search, external services) prevents unexpected costs from runaway agent loops. A single misconfigured agent can make hundreds of API calls in minutes without guardrails.
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 8.0+ |
| VRAM | 4+ GB |
| Disk | ~3 GB free for model download |
Step 1: Create the Project
dotnet new console -n ToolInterceptQuickstart
cd ToolInterceptQuickstart
dotnet add package LM-Kit.NET
Step 2: Basic Tool Invocation Logging
using System.Text;
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Events;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
// ──────────────────────────────────────
// 1. Load model
// ──────────────────────────────────────
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:4b",
loadingProgress: p => { Console.Write($"\rLoading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// ──────────────────────────────────────
// 2. Create a conversation with tools
// ──────────────────────────────────────
var chat = new MultiTurnConversation(model)
{
SystemPrompt = "You are a helpful assistant with access to tools.",
MaximumCompletionTokens = 512
};
chat.Tools.Add(BuiltInTools.Calculator);
chat.Tools.Add(BuiltInTools.DateTime);
// ──────────────────────────────────────
// 3. Log every tool call before execution
// ──────────────────────────────────────
chat.BeforeToolInvocation += (sender, e) =>
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n [TOOL CALL] {e.ToolCall.FunctionName}");
Console.WriteLine($" [ARGS] {e.ToolCall.Arguments}");
Console.ResetColor();
};
// ──────────────────────────────────────
// 4. Log every tool result after execution
// ──────────────────────────────────────
chat.AfterToolInvocation += (sender, e) =>
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [RESULT] {e.ToolCallResult.Content}");
Console.ResetColor();
};
chat.AfterTextCompletion += (_, e) =>
{
if (e.SegmentType == TextSegmentType.UserVisible)
Console.Write(e.Text);
};
// ──────────────────────────────────────
// 5. Run a prompt that triggers tool usage
// ──────────────────────────────────────
Console.Write("Assistant: ");
chat.Submit("What is 15% of $2,340? Also, what day of the week is it today?");
Console.WriteLine("\n");
Step 3: Blocking Tool Calls (Security Filtering)
Use the Cancel property on BeforeToolInvocationEventArgs to prevent a tool from executing. This is useful for enforcing security rules or disabling specific tools during sensitive sessions:
// Block specific tool calls based on rules
chat.BeforeToolInvocation += (sender, e) =>
{
// Example: block web search during sensitive sessions
if (e.ToolCall.FunctionName == "web_search")
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($" [BLOCKED] Web search is disabled in this session.");
Console.ResetColor();
e.Cancel = true; // prevents the tool from executing
}
};
When a tool call is cancelled, the model receives a cancellation signal and adapts its response accordingly. It may attempt to answer without the tool or explain that the requested operation is unavailable.
Step 4: Rate Limiting Tool Calls
Prevent runaway agents from making too many tool calls by counting invocations and blocking once a threshold is reached:
int toolCallCount = 0;
const int MaxToolCallsPerSession = 10;
chat.BeforeToolInvocation += (sender, e) =>
{
toolCallCount++;
if (toolCallCount > MaxToolCallsPerSession)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($" [RATE LIMITED] Tool call #{toolCallCount} blocked (max {MaxToolCallsPerSession}).");
Console.ResetColor();
e.Cancel = true;
return;
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [TOOL #{toolCallCount}/{MaxToolCallsPerSession}] {e.ToolCall.FunctionName}");
Console.ResetColor();
};
Step 5: Building an Audit Log
Capture every tool invocation with timestamps for compliance reporting:
var auditLog = new List<(DateTime Timestamp, string Tool, string Args, string Result)>();
chat.BeforeToolInvocation += (sender, e) =>
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [AUDIT] Calling {e.ToolCall.FunctionName}...");
Console.ResetColor();
};
chat.AfterToolInvocation += (sender, e) =>
{
auditLog.Add((
DateTime.UtcNow,
e.ToolCallResult.ToolCall.FunctionName,
e.ToolCallResult.ToolCall.Arguments,
e.ToolCallResult.Content
));
};
// After the session, export the audit log
Console.WriteLine("\n── Audit Log ──");
foreach (var entry in auditLog)
{
Console.WriteLine($" {entry.Timestamp:HH:mm:ss} {entry.Tool}({entry.Args}) → {entry.Result}");
}
Step 6: Agent-Level Tool Interception
The same BeforeToolInvocation and AfterToolInvocation events are available on the Agent class. This works identically to conversation-level interception but applies to the agent's entire execution lifecycle:
var agent = Agent.CreateBuilder(model)
.WithInstruction("You are a research assistant.")
.WithTools(tools =>
{
tools.Register(BuiltInTools.Calculator);
tools.Register(BuiltInTools.DateTime);
})
.WithMaxIterations(5)
.Build();
// Agent-level events work the same way
agent.BeforeToolInvocation += (sender, e) =>
{
Console.WriteLine($" [AGENT TOOL] {e.ToolCall.FunctionName}: {e.ToolCall.Arguments}");
};
agent.AfterToolInvocation += (sender, e) =>
{
Console.WriteLine($" [AGENT RESULT] {e.ToolCallResult.Content}");
};
var result = await agent.ExecuteAsync("Calculate the compound interest on $10,000 at 5% for 3 years.");
Console.WriteLine($"\nAgent: {result.Content}");
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Events not firing | No tools registered on the conversation | Add tools via chat.Tools.Add() before submitting |
| Cancelled tool causes bad response | Model expects tool result but gets none | The model receives a cancellation signal and adapts its response |
| Audit log missing entries | Event handler not attached before first Submit |
Attach handlers immediately after creating the conversation |
| Agent events not firing | Attached to wrong object | Use agent.BeforeToolInvocation, not the underlying conversation |
Next Steps
- Create an AI Agent with Tools: build agents with the
IToolinterface for richer tool integration. - Build a Function-Calling Agent: route natural language prompts to typed C# methods.
- Build a Resilient Production Agent: error handling, retries, and observability for production agents.
- Build a Content Moderation Filter: use tool interception as part of a moderation pipeline.