Table of Contents

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:

  1. 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.
  2. 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