AI agents often work by calling external tools. An agent might query a database, read application logs, create a support ticket, call an API, or retrieve information from a cloud service.
In a small application, tracking these operations may seem straightforward. However, enterprise applications can have multiple agents, dozens of tools, and several connected services. A single user request may trigger many tool calls before the agent produces a response.
If something goes wrong, developers need to know which tool was called, what input was provided, how long it took, and whether it succeeded.
Without this information, troubleshooting becomes difficult. A slow response could be caused by the AI model, an external API, a database query, or repeated tool calls.
This article explains how to track AI agent tool calls in an ASP.NET Core application using C#, structured logging, and a consistent execution model.
Why Tool Call Tracking Matters
Consider an AI assistant used by an enterprise support team.
A user asks:
"Find the latest payment failure for customer 1052 and create a support ticket."
To complete this request, the agent might:
- Call a customer lookup tool.
- Query the payment service.
- Retrieve the latest failed transaction.
- Call a support ticket API.
- Return the ticket reference to the user.
The entire process may look like one operation to the user, but several independent tool calls happen behind the scenes.
If the ticket is not created, developers need to identify the exact failure.
Possible problems include:
- The customer lookup API returned an error.
- The payment service timed out.
- The agent selected an incorrect transaction.
- The ticket API rejected the request.
- The same tool was called repeatedly.
- An external service returned an unexpected response.
Tool call tracking provides the information needed to investigate these problems.
It also helps teams measure execution time, identify expensive integrations, monitor failure rates, and detect unexpected agent behavior.
Understand What Should Be Tracked
A useful tracking system should capture more than the tool name and execution status.
Each tool call should have a unique identifier and enough context to connect it with the original user request.
The following fields provide a practical starting point:
| Field | Purpose |
|---|---|
| ToolCallId | Identifies an individual tool call |
| AgentId | Identifies the agent |
| ConversationId | Connects calls to a conversation |
| ToolName | Identifies the tool being executed |
| StartedAtUtc | Records when execution started |
| CompletedAtUtc | Records when execution finished |
| DurationMs | Measures execution time |
| Status | Records success, failure, or timeout |
| ErrorType | Identifies the failure category |
| RetryCount | Tracks execution attempts |
| CorrelationId | Connects related application operations |
For example, a payment lookup and a ticket creation call should have separate tool call IDs but share the same conversation and correlation IDs.
This makes it possible to investigate the complete execution flow without confusing individual operations.
Do not automatically store every tool input and output. These values may contain customer information, credentials, personal data, or confidential business records. Store only the information needed for troubleshooting, and apply appropriate redaction and access controls.
Create a Tool Call Data Model
Let's implement a tracking system using ASP.NET Core and Entity Framework Core.
Start by defining the execution status:
public enum ToolCallStatus
{
Started = 0,
Completed = 1,
Failed = 2,
TimedOut = 3,
Cancelled = 4
}Next, create an entity to store tool execution details:
public class AgentToolCall
{
public Guid Id { get; set; }
public string AgentId { get; set; } = string.Empty;
public string ConversationId { get; set; } = string.Empty;
public string CorrelationId { get; set; } = string.Empty;
public string ToolName { get; set; } = string.Empty;
public ToolCallStatus Status { get; set; }
public DateTime StartedAtUtc { get; set; }
public DateTime? CompletedAtUtc { get; set; }
public long? DurationMs { get; set; }
public int RetryCount { get; set; }
public string? ErrorType { get; set; }
public string? ErrorMessage { get; set; }
}The Id property identifies an individual tool call. The CorrelationId connects the call to other operations within the same request.
For example, if a user request triggers three different tools, each tool call gets a separate ID while sharing the same correlation ID.
Register the entity in the database context:
public class AppDbContext : DbContext
{
public AppDbContext(
DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<AgentToolCall> AgentToolCalls =>
Set<AgentToolCall>();
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<AgentToolCall>()
.HasIndex(x => x.CorrelationId);
modelBuilder.Entity<AgentToolCall>()
.HasIndex(x => new
{
x.AgentId,
x.StartedAtUtc
});
}
}These indexes help when retrieving execution history for a conversation or reviewing activity from a particular agent.
For a high-volume application, consider table partitioning or a dedicated observability store rather than keeping every execution record in the primary application database indefinitely.
Build a Tool Call Tracking Service
Instead of writing tracking logic separately for every tool, create a reusable service.
The service should record when execution starts and update the record when the operation completes or fails.
public class ToolCallTracker
{
private readonly AppDbContext _context;
public ToolCallTracker(AppDbContext context)
{
_context = context;
}
public async Task<Guid> StartAsync(
string agentId,
string conversationId,
string correlationId,
string toolName,
int retryCount,
CancellationToken cancellationToken)
{
var toolCall = new AgentToolCall
{
Id = Guid.NewGuid(),
AgentId = agentId,
ConversationId = conversationId,
CorrelationId = correlationId,
ToolName = toolName,
Status = ToolCallStatus.Started,
StartedAtUtc = DateTime.UtcNow,
RetryCount = retryCount
};
_context.AgentToolCalls.Add(toolCall);
await _context.SaveChangesAsync(
cancellationToken);
return toolCall.Id;
}
public async Task CompleteAsync(
Guid toolCallId,
ToolCallStatus status,
string? errorType,
string? errorMessage,
CancellationToken cancellationToken)
{
var toolCall = await _context.AgentToolCalls
.FirstOrDefaultAsync(
x => x.Id == toolCallId,
cancellationToken);
if (toolCall == null)
{
return;
}
var completedAt = DateTime.UtcNow;
toolCall.Status = status;
toolCall.CompletedAtUtc = completedAt;
toolCall.DurationMs = (long)(
completedAt - toolCall.StartedAtUtc
).TotalMilliseconds;
toolCall.ErrorType = errorType;
toolCall.ErrorMessage = errorMessage;
await _context.SaveChangesAsync(
cancellationToken);
}
}The service creates a record before executing the tool and updates it after execution.
Using a separate tracking service keeps monitoring concerns out of individual tool implementations.
In a production system, avoid creating a new database context concurrently with another operation on the same context. Also consider what should happen if the tracking database is unavailable. A durable telemetry queue or independent observability pipeline can prevent monitoring failures from silently interrupting important business operations.

Join the conversation! Your thoughts help the community grow.