Table of Contents

Equip an Agent with Built-In Tools for Data Processing and Automation

LM-Kit.NET ships a growing catalog of built-in tools across multiple categories. The catalog is designed for continuous growth, with a strong focus on security and scalability. Built-in tools give your agent fine-grained capabilities: parse JSON, query databases, validate emails, compute statistics, encrypt data, call HTTP APIs, read files, process PDFs, perform OCR, and more, all without writing custom tool code. This guide shows how to register built-in tools, configure IO tools securely, and build practical data processing agents.


Why This Matters

Two enterprise problems that built-in tools solve:

  1. Repetitive tool authoring. Every agent project reinvents the same utilities: JSON parsing, date formatting, regex matching, URL building. Built-in tools provide production-tested implementations for dozens of common operations, saving weeks of development time and eliminating bugs in foundational plumbing.
  2. Data pipeline automation. Business workflows require reading CSVs, validating records, computing summaries, and writing output files. An agent equipped with data, text, and IO tools can orchestrate these steps autonomously, turning a multi-script ETL process into a single natural language instruction.

Prerequisites

Requirement Minimum
.NET SDK 8.0+
VRAM 6+ GB (for a tool-calling model like Qwen 3 8B)
Model Must support tool calling (model.HasToolCalls == true)

Step 1: Create the Project

dotnet new console -n BuiltInToolsAgent
cd BuiltInToolsAgent
dotnet add package LM-Kit.NET

Step 2: Understand the Built-In Tools Ecosystem

All built-in tools are accessible through the BuiltInTools static class. They fall into two groups: safe tools (no file system or network access) and IO tools (require explicit configuration for security). Every built-in tool also exposes rich metadata (category, risk level, side effects) through the IToolMetadata interface, and you can use filtering methods like GetByCategory(), GetByMaxRisk(), and GetReadOnly() to select tools programmatically.

The built-in tool catalog is designed to evolve over time, enabling fine-grained permission control.

Category Description Access
Data Parsing and transformation: JSON, XML, CSV, YAML, HTML, Markdown, databases, spreadsheets, QR codes Safe (Database and Spreadsheet have I/O)
Text String operations: text manipulation, diff, regex, templating, encoding, slugification, fuzzy matching, phonetics Safe
Numeric Computation: calculator, unit conversion, statistics, financial math, random, expressions, IP calc Safe
Security Hashing, encryption, JWT, validation, password generation, checksums Safe
Utility Date/time, cron, URL, color, locale, MIME types, paths, scheduling, time zones, humanization Safe
Document PDF manipulation (split, merge, extract, info, render, unlock), image preprocessing (deskew, crop, resize), text extraction, OCR Has I/O
IO File system operations, process execution, compression, clipboard, environment variables, file watching Requires config
Net HTTP operations, FTP, web search, network diagnostics, SMTP, RSS feeds Requires config

Step 3: Register Safe Tools with an Agent

Register only the tools your agent actually needs. Fewer tools means the model selects the right tool faster and produces better results. Start with a small set of tools per agent, then add more only when required by the workflow.

Important: Never register dozens of tools at once. Each registered tool adds its full JSON schema to the model's context, consuming tokens and degrading tool selection accuracy.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

// ──────────────────────────────────────
// 1. Load a tool-calling model
// ──────────────────────────────────────
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

// ──────────────────────────────────────
// 2. Create agent with two focused tools
// ──────────────────────────────────────
var agent = Agent.CreateBuilder(model)
    .WithPersona("Data Processing Assistant")
    .WithInstruction("You are a helpful assistant with access to a calculator tool. " +
                     "Always use the calculator for arithmetic operations.")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.CalcArithmetic);
    })
    .Build();

// ──────────────────────────────────────
// 3. Run with real-time feedback
// ──────────────────────────────────────
using var executor = new AgentExecutor();

executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};

executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ Calling tool: {e.ToolCall.Name}");
    Console.ResetColor();
};

executor.AfterToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine($"  ✓ Result: {e.ToolCallResult.ResultJson}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    "A store sold 3 items: $59.99, $124.50, and $89.00. " +
    "What is the total revenue and the average price per item?");

Console.WriteLine($"\n\nFinal answer: {result.Content}");

Step 4: Pick Specific Tools for a Focused Agent

For production agents, register only the tools the agent needs. Fewer tools means less confusion for the model and faster tool selection.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

var agent = Agent.CreateBuilder(model)
    .WithPersona("CSV Data Analyst")
    .WithInstruction("You analyze CSV data. Parse it, compute statistics, validate fields, and output results as JSON.")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.CsvParse);
        tools.Register(BuiltInTools.ValidatorData);
        tools.Register(BuiltInTools.CalcArithmetic);
    })
    .Build();

using var executor = new AgentExecutor();
executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};
executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ {e.ToolCall.Name}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    "Here is a CSV of customer orders:\n" +
    "customer,email,amount,date\n" +
    "Alice,alice@example.com,150.00,2025-01-15\n" +
    "Bob,invalid-email,89.50,2025-01-16\n" +
    "Carol,carol@test.org,220.75,2025-01-17\n\n" +
    "Parse this data, validate all email addresses, and compute the average order amount.");

Console.WriteLine($"\n\nResult: {result.Content}");

Step 5: Configure IO Tools with Security Restrictions

IO tools (FileSystem, Http, Process) are disabled by default. Enable them with explicit restrictions to control what the agent can access.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.Agents.Tools.BuiltIn.IO;
using LMKit.Agents.Tools.BuiltIn.Net;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

var agent = Agent.CreateBuilder(model)
    .WithPersona("File Processing Agent")
    .WithTools(tools =>
    {
        // Safe tools for data processing
        tools.Register(BuiltInTools.JsonParse);
        tools.Register(BuiltInTools.CsvParse);

        // FileSystem: read-only, restricted to specific directories
        tools.Register(BuiltInTools.CreateFileSystemRead(new FileSystemToolOptions
        {
            AllowWrite = false,
            AllowDelete = false,
            AllowedPaths = new HashSet<string> { "/data/input", "/data/output" },
            MaxFileSize = 10 * 1024 * 1024  // 10 MB limit
        }));

        // HTTP: restricted to specific hosts
        tools.Register(BuiltInTools.CreateHttpGet(new HttpToolOptions
        {
            AllowedHosts = new HashSet<string> { "api.example.com", "data.example.com" },
            Timeout = TimeSpan.FromSeconds(15),
            MaxResponseSize = 50_000
        }));
    })
    .Build();

using var executor = new AgentExecutor();
executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};
executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ {e.ToolCall.Name}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    "List all CSV files in /data/input, then read the first one and summarize its contents as JSON.");

Console.WriteLine($"\n\nResult: {result.Content}");

Security defaults for IO tools:

Tool Default Behavior How to Restrict
FileSystem Read-only, no path restrictions AllowedPaths, BlockedPaths, MaxFileSize
Http Blocks localhost/127.0.0.1 AllowedHosts, BlockedHosts, AllowedSchemes
Process Full access ProcessToolOptions
Network Full access NetworkToolOptions

Step 6: Build a Data Transformation Agent

A complete example: an agent that reads CSV data, validates records, computes statistics, and outputs structured JSON.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

var agent = Agent.CreateBuilder(model)
    .WithPersona("Data Quality Engineer")
    .WithInstruction(
        "You process and validate datasets. For each task:\n" +
        "1. Parse the input data using the appropriate tool\n" +
        "2. Validate all fields (emails, dates, numeric ranges)\n" +
        "3. Compute summary statistics on numeric columns\n" +
        "4. Output a JSON report with valid records, invalid records, and statistics")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.CsvParse);
        tools.Register(BuiltInTools.ValidatorData);
        tools.Register(BuiltInTools.CalcArithmetic);
    })
    .Build();

string csvData = @"name,email,revenue,signup_date
Acme Corp,sales@acme.com,125000.50,2024-03-15
BadEmail Inc,not-an-email,89000.00,2024-06-01
Tech Solutions,info@techsol.io,210000.75,2024-01-20
Null Fields,,0,invalid-date
Global Ltd,contact@global.com,175000.25,2024-09-10";

using var executor = new AgentExecutor();
executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};
executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ {e.ToolCall.Name}");
    Console.ResetColor();
};

var report = await executor.ExecuteAsync(agent,
    $"Process this customer dataset and generate a quality report:\n\n{csvData}");

Console.WriteLine($"\n\nReport: {report.Content}");

Step 7: Process JSON Data with the Json Tool

The Json tool supports parsing, querying with dot-notation paths, formatting, minifying, validating, and extracting keys/values.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

var agent = Agent.CreateBuilder(model)
    .WithPersona("JSON Data Processor")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.JsonParse);
        tools.Register(BuiltInTools.CalcArithmetic);
    })
    .Build();

string apiResponse = @"{
    ""status"": ""success"",
    ""data"": {
        ""users"": [
            {""name"": ""Alice"", ""score"": 92.5, ""active"": true},
            {""name"": ""Bob"", ""score"": 87.3, ""active"": false},
            {""name"": ""Carol"", ""score"": 95.1, ""active"": true}
        ],
        ""metadata"": {""total"": 3, ""page"": 1}
    }
}";

using var executor = new AgentExecutor();
executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};
executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ {e.ToolCall.Name}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    $"From this API response, extract the list of active users and calculate their average score:\n\n{apiResponse}");

Console.WriteLine($"\n\nResult: {result.Content}");

Step 8: Validate and Transform Data with Text Tools

Combine Regex, Template, Encoding, and Slug tools for text processing workflows.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

var agent = Agent.CreateBuilder(model)
    .WithPersona("Text Processing Specialist")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.SlugGenerate);
        tools.Register(BuiltInTools.RegexMatch);
    })
    .Build();

using var executor = new AgentExecutor();
executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};
executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ {e.ToolCall.Name}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    "I have these product names that need URL-friendly slugs:\n" +
    "1. 'Café Délicieux™ Premium Blend'\n" +
    "2. 'Über-Fresh Organic Juice (500ml)'\n" +
    "3. '日本茶 Green Tea Extract'\n\n" +
    "For each: generate a URL slug, then check if any contain non-ASCII characters using regex.");

Console.WriteLine($"\n\nResult: {result.Content}");

Step 9: Use Security Tools for Data Compliance

The Security category provides hashing, encryption, validation, and JWT operations for compliance workflows.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

var agent = Agent.CreateBuilder(model)
    .WithPersona("Data Security Auditor")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.HashCompute);
        tools.Register(BuiltInTools.ValidatorData);
        tools.Register(BuiltInTools.PasswordAnalyze);
    })
    .Build();

using var executor = new AgentExecutor();
executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};
executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ {e.ToolCall.Name}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    "Audit this data for security compliance:\n" +
    "1. Validate these emails: 'admin@company.com', 'bad@@email', 'user@test.org'\n" +
    "2. Hash the valid emails with SHA-256 for anonymized storage\n" +
    "3. Generate a secure 16-character password for the new admin account");

Console.WriteLine($"\n\nResult: {result.Content}");

Step 10: Use Document Tools for PDF and Image Processing

The Document category provides tools for PDF manipulation, image preprocessing, text extraction, and OCR. These tools operate on files and enable document automation workflows.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;
using LMKit.TextGeneration.Chat;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

var agent = Agent.CreateBuilder(model)
    .WithPersona("Document Processing Agent")
    .WithInstruction(
        "You process documents and images. You can split PDFs, " +
        "extract text, and get PDF metadata.")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.PdfSplit);
        tools.Register(BuiltInTools.PdfMetadata);
        tools.Register(BuiltInTools.DocumentTextExtract);
    })
    .Build();

using var executor = new AgentExecutor();
executor.AfterTextCompletion += (sender, e) =>
{
    if (e.SegmentType == TextSegmentType.UserVisible)
        Console.Write(e.Text);
};
executor.BeforeToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"\n  ⚙ {e.ToolCall.Name}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    "Extract pages 1-3 from 'report.pdf' into a new file called 'summary.pdf', " +
    "then get the text content from page 1.");

Console.WriteLine($"\n\nResult: {result.Content}");

The Document category includes tools for PDF operations (split, merge, extract, info, render, unlock), image preprocessing (deskew, crop, resize), format conversion (Markdown to PDF, EML to PDF, Markdown to/from HTML, Markdown to/from DOCX, EML/MBOX to Markdown), text extraction from multiple formats, and OCR. See the Process PDFs and Images with Built-In Document Tools guide for detailed examples.


Step 11: Monitor Tool Invocations

Track which tools the agent calls, how often, and what results they return. This is essential for debugging, auditing, and cost tracking.

using System.Text;
using LMKit.Model;
using LMKit.Agents;
using LMKit.Agents.Tools.BuiltIn;

LMKit.Licensing.LicenseManager.SetLicenseKey("");

Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;

Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("qwen3:8b",
    downloadingProgress: (_, len, read) =>
    {
        if (len.HasValue) Console.Write($"\r  Downloading: {(double)read / len.Value * 100:F1}%   ");
        return true;
    },
    loadingProgress: p => { Console.Write($"\r  Loading: {p * 100:F0}%   "); return true; });
Console.WriteLine("\n");

using var executor = new AgentExecutor();

var agent = Agent.CreateBuilder(model)
    .WithPersona("Data Analyst")
    .WithInstruction("Use the calculator tool for all arithmetic computations.")
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.CalcArithmetic);
    })
    .Build();

int toolCallCount = 0;

// Monitor every tool call via the executor events
executor.BeforeToolInvocation += (sender, e) =>
{
    toolCallCount++;
    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"  [TOOL] {e.ToolCall.Name}({e.ToolCall.ArgumentsJson})");
    Console.ResetColor();
};

executor.AfterToolInvocation += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"         → {e.ToolCallResult.ResultJson}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(agent,
    "What is the compound interest on a $10,000 investment at 5.5% annual rate over 3 years?");
Console.WriteLine($"\nFinal answer: {result.Content}");
Console.WriteLine($"Tools called: {toolCallCount}");

Step 12: Inspect Tool Metadata

Every built-in tool implements the IToolMetadata interface, which exposes structured information about a tool's behavior, risk profile, and side effects. You can use this metadata to make informed decisions about which tools to register or to build automated policies.

IToolMetadata properties:

Property Type Description
Category string Tool category (e.g., "Data", "IO", "Net", "Security")
SideEffect ToolSideEffect What kind of side effect the tool produces
RiskLevel ToolRiskLevel How risky the tool is to execute (Low, Medium, High, Critical)
DefaultApproval ToolApprovalMode Whether the tool requires approval by default
IsIdempotent bool True if calling the tool multiple times with the same input yields the same result
IsReadOnly bool True if the tool does not modify any external state

ToolSideEffect values:

Value Meaning
None Pure computation, no external interaction
LocalRead Reads from the local file system or environment
LocalWrite Writes to the local file system or environment
NetworkRead Reads from the network (HTTP GET, DNS lookup)
NetworkWrite Writes to the network (HTTP POST, SMTP send)
Irreversible Performs an action that cannot be undone (e.g., delete, send email)

ToolRiskLevel values:

Value Meaning
Low Safe, no side effects (Calculator, DateTime, Json)
Medium Local reads or idempotent network reads (FileSystem read, Http GET)
High Local writes or network writes (FileSystem write, Http POST)
Critical Irreversible actions (Process execution, SMTP send, file delete)

You can access metadata on any ToolInfo object returned by the tool registry:

foreach (var toolInfo in agent.Tools)
{
    Console.WriteLine($"{toolInfo.Name}: Category={toolInfo.Category}, " +
                      $"Risk={toolInfo.RiskLevel}, ReadOnly={toolInfo.IsReadOnly}");
}

Step 13: Filter Tools by Metadata

The BuiltInTools class provides filtering methods that let you select tools based on their metadata. This is useful when you want to register only tools that meet certain safety criteria.

using LMKit.Agents;
using LMKit.Agents.Tools;
using LMKit.Agents.Tools.BuiltIn;

// Get all read-only tools (safe for untrusted environments)
IReadOnlyList<ITool> readOnlyTools = BuiltInTools.GetReadOnly();

// Get tools by category
IReadOnlyList<ITool> dataTools = BuiltInTools.GetByCategory("Data");
IReadOnlyList<ITool> textTools = BuiltInTools.GetByCategory("Text");

// Get tools up to a maximum risk level
IReadOnlyList<ITool> lowRiskTools = BuiltInTools.GetByMaxRisk(ToolRiskLevel.Low);
IReadOnlyList<ITool> mediumOrLower = BuiltInTools.GetByMaxRisk(ToolRiskLevel.Medium);

Register filtered tools with an agent:

// Build a safe agent that can only use low-risk, read-only tools
var safeAgent = Agent.CreateBuilder(model)
    .WithPersona("Safe Data Processor")
    .WithTools(tools =>
    {
        foreach (var tool in BuiltInTools.GetByMaxRisk(ToolRiskLevel.Low))
            tools.Register(tool);
    })
    .Build();

// Build an agent with only Data and Text category tools
var dataTextAgent = Agent.CreateBuilder(model)
    .WithPersona("Data and Text Specialist")
    .WithTools(tools =>
    {
        foreach (var tool in BuiltInTools.GetByCategory("Data"))
            tools.Register(tool);
        foreach (var tool in BuiltInTools.GetByCategory("Text"))
            tools.Register(tool);
    })
    .Build();

Tip: Combining GetByMaxRisk() with GetByCategory() gives you fine-grained control. For example, register all low-risk Data tools for a sandboxed analytics agent.


Step 14: Configure Permission Policies

A ToolPermissionPolicy governs which tools are allowed, denied, or require approval before execution. Attach a policy to the agent builder with WithPermissionPolicy() or set it directly on a ToolRegistry. The policy is evaluated before every tool call, and the result is one of three values in ToolPermissionResult: Allowed, Denied, or ApprovalRequired.

Fluent API methods on ToolPermissionPolicy:

Method Description
Allow(toolName) Explicitly allow a specific tool by name
Deny(toolName) Explicitly deny a specific tool by name
AllowCategory(category) Allow all tools in a category
DenyCategory(category) Deny all tools in a category
RequireApproval(toolName) Require user approval for a specific tool
RequireApprovalForCategory(category) Require user approval for all tools in a category
SetMaxRiskLevel(riskLevel) Deny any tool above the specified risk level

Example: Safe Chat Agent

A chatbot that can only use pure-computation tools. No file access, no network, no writes.

var policy = new ToolPermissionPolicy()
    .SetMaxRiskLevel(ToolRiskLevel.Low)
    .DenyCategory("IO")
    .DenyCategory("Net");

var chatAgent = Agent.CreateBuilder(model)
    .WithPersona("Safe Chat Assistant")
    .WithPermissionPolicy(policy)
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.CalcArithmetic);
        tools.Register(BuiltInTools.DateTimeNow);
        tools.Register(BuiltInTools.JsonParse);
    })
    .Build();

Example: Developer Assistant

A developer helper that can read files and make HTTP GET requests, but requires approval for any write operation.

var policy = new ToolPermissionPolicy()
    .SetMaxRiskLevel(ToolRiskLevel.High)
    .AllowCategory("Data")
    .AllowCategory("Text")
    .AllowCategory("Numeric")
    .RequireApprovalForCategory("IO")
    .RequireApprovalForCategory("Net")
    .Deny("process_execute");

var devAgent = Agent.CreateBuilder(model)
    .WithPersona("Developer Assistant")
    .WithPermissionPolicy(policy)
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.JsonParse);
        tools.Register(BuiltInTools.RegexMatch);
        tools.Register(BuiltInTools.CalcArithmetic);
        tools.Register(BuiltInTools.CreateFileSystemRead(new FileSystemToolOptions
        {
            AllowWrite = true,
            AllowedPaths = new HashSet<string> { "/project/src" }
        }));
        tools.Register(BuiltInTools.CreateHttpGet(new HttpToolOptions
        {
            AllowedHosts = new HashSet<string> { "api.github.com" }
        }));
    })
    .Build();

Example: Ops Bot

An operations bot that can do everything, but requires human approval for critical actions (sending emails, running processes).

var policy = new ToolPermissionPolicy()
    .AllowCategory("Data")
    .AllowCategory("Text")
    .AllowCategory("Numeric")
    .AllowCategory("Utility")
    .AllowCategory("Security")
    .AllowCategory("IO")
    .AllowCategory("Net")
    .RequireApproval("smtp_send")
    .RequireApproval("process_execute")
    .RequireApproval("ftp_upload");

var opsAgent = Agent.CreateBuilder(model)
    .WithPersona("Operations Bot")
    .WithPermissionPolicy(policy)
    .WithTools(tools =>
    {
        tools.Register(BuiltInTools.CalcArithmetic);
        tools.Register(BuiltInTools.JsonParse);
        tools.Register(BuiltInTools.HttpGet);
        tools.Register(BuiltInTools.CreateSmtpSend("smtp.company.com", 587, "bot@company.com", "password"));
    })
    .Build();

Step 15: Handle Approval Requests

When a permission policy returns ApprovalRequired for a tool call, the runtime raises the ToolApprovalRequired event. You must handle this event to implement human-in-the-loop approval workflows. If no handler is subscribed, the tool call is denied by default.

using var executor = new AgentExecutor();

executor.ToolApprovalRequired += (sender, e) =>
{
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine($"\n  [APPROVAL REQUIRED] Tool: {e.ToolCall.Name}");
    Console.WriteLine($"  Risk: {e.RiskLevel}, Side Effect: {e.SideEffect}");
    Console.WriteLine($"  Arguments: {e.ToolCall.ArgumentsJson}");
    Console.Write("  Allow this call? (y/n): ");
    Console.ResetColor();

    string? response = Console.ReadLine();
    e.Approved = response?.Trim().Equals("y", StringComparison.OrdinalIgnoreCase) == true;

    if (!e.Approved)
        e.DenialReason = "denied by operator";
};

executor.BeforeToolInvocation += (sender, e) =>
{
    // The PermissionResult property shows the policy evaluation outcome
    Console.ForegroundColor = ConsoleColor.DarkGray;
    Console.WriteLine($"  [PERMISSION] {e.Tool?.Name}: {e.PermissionResult}");
    Console.ResetColor();
};

var result = await executor.ExecuteAsync(opsAgent, "Send a status report email to team@company.com");

When the agent attempts to call smtp_send, the ToolApprovalRequired event fires. If the user types "y", the tool executes. If the user types "n" (or anything else), the tool call is blocked and the model receives a denial result so it can adapt its approach.

You can also evaluate permissions programmatically through the ToolRegistry:

// Check permission for a specific tool before the agent runs
ToolPermissionResult result = agent.Tools.EvaluatePermission("smtp_send");
Console.WriteLine($"SMTP permission: {result}");
// Output: "SMTP permission: ApprovalRequired"

Model Selection

Model ID VRAM Tool Calling Best For
qwen3:4b ~4 GB Yes Lightweight tool agent, simple data tasks
qwen3:8b ~6.5 GB Yes Balanced speed and accuracy for multi-tool workflows
qwen3:14b ~11 GB Yes Complex reasoning across many tools
gptoss:20b ~15 GB Yes Advanced multi-step data pipelines
glm4.7-flash ~18 GB Yes Strongest 30B-class MoE, agentic tasks

Verify tool-calling support after loading: Console.WriteLine($"Tool calling: {model.HasToolCalls}");


Common Issues

Problem Cause Fix
Agent ignores available tools Too many tools registered Register only the tools needed for the task
FileSystem tool returns "access denied" Default is read-only, paths not allowed Set AllowWrite = true and add paths to AllowedPaths
HTTP tool blocks requests Localhost blocked by default Add target hosts to AllowedHosts
Agent calls wrong tool Tool descriptions overlap Use fewer, more targeted tools per agent
JSON parsing fails Input not valid JSON Use the Validator tool first to check format
Slow response with many tools Model evaluates all tool schemas Start with a focused tool set and add tools incrementally while monitoring latency and accuracy

Next Steps

Share