The Agentic Leap — Part 2: Tools and Function Calling for Safe AI Agents
In Module 1, we established a simple principle: a model may choose what it wants to do next, but trusted application code decides what it is allowed to do.
Tools are where that principle becomes real.
A model by itself can generate text. A tool allows it to request information or propose an action. In a Microsoft Foundry and ASP.NET Core solution, that tool might search approved project evidence, calculate a planning deadline or prepare a task proposal.
The important word is request. A tool call produced by a model is not an instruction that your application must obey. Treat it like untrusted input arriving at an API boundary: parse it, validate it, authorize it and execute it only when policy allows.
A model can request a capability. It cannot grant itself permission to use that capability.We will build this idea gradually, using C# and the continuing BuildEstate Pro case study.
1. What is function calling?
Function calling—also called tool calling—allows an application to describe available capabilities to a model. Instead of answering only with prose, the model can return a structured request containing a tool name and arguments.
For example, the user asks:
“What is the current planning position for Plot 14?”The model might request:
{
"name": "get_planning_position",
"arguments": {
"projectId": "8cb3f432-4e13-4e68-a30e-cf86483bea21",
"plotId": "81ad84aa-dc7c-4c81-a9d3-ddb5c36f509d"
}
}
Your application receives that structured data. It does not give the model a direct connection to SQL Server, Azure resources or internal services. ASP.NET Core resolves the authenticated user, checks project access, validates the identifiers and invokes a controlled handler. The sanitized result is then returned to the model as an observation.
User goal
-> model requests a tool
-> application validates the schema
-> application checks identity and authorization
-> trusted handler executes
-> bounded result returns to the model
-> model decides whether to continue or finish
This is the safe mental model for every tool in the series.
2. Function calling is not remote code execution
A dangerous design exposes tools such as:
execute_sqlrun_shell_commandwrite_any_filesend_http_requestcall_any_azure_resource
Prefer narrow business capabilities:
get_planning_positionsearch_approved_project_evidencecalculate_review_deadlineprepare_missing_evidence_taskexecute_approved_project_task
ApprovePlanningTask, not a generic method that lets a caller update any table and column.
A narrow tool is easier to describe, authorize, test, monitor and revoke. It also gives the model fewer ways to make an expensive mistake.
3. Our real-life case study
The BuildEstate Pro planning manager asks:
“Review Plot 14. If ecology evidence is missing, prepare the appropriate task.”The agent receives four possible tools:
get_planning_position— reads the approved planning record.search_approved_evidence— retrieves authorized evidence.prepare_evidence_task— creates a proposal with no business effect.execute_approved_task— creates a task only after valid approval.
At this point, nothing has been created in the business system. The application stores a proposal and asks a qualified user to review it. Only a separate, approval-bound command can create the task.
This separation is deliberately calm and boring. “Boring” is a compliment around business consequences: the path is understandable, reviewable and recoverable.
4. Design a small C# contract
Start with a request type that contains only model-selectable business inputs:
public sealed record GetPlanningPositionArgs(
Guid ProjectId,
Guid PlotId);
public sealed record PlanningPositionResult(
Guid ProjectId,
Guid PlotId,
string Status,
IReadOnlyList<string> Conditions,
string EvidenceVersion,
DateTimeOffset RetrievedAtUtc);
Notice what is missing from the arguments:
- user ID;
- tenant ID;
- access role;
- connection string;
- approval status.
public sealed record AgentExecutionContext(
Guid ExecutionId,
Guid UserId,
Guid TenantId,
IReadOnlySet<string> GrantedScopes);
This mirrors normal ASP.NET Core practice. Claims come from the authenticated principal, not from a request body saying "isAdmin": true.
5. Schema validation is necessary but not sufficient
Strict tool schemas improve reliability. They can require fields, reject additional properties, define enums and constrain lengths. However, a valid JSON shape does not prove that the data is true or permitted.
Think in three gates:
- Shape: Is this valid according to the tool schema?
- Meaning: Does the project and plot combination exist and make sense?
- Permission: May this authenticated user perform this operation now?
public async Task<ToolResult> HandleAsync(
GetPlanningPositionArgs args,
AgentExecutionContext context,
CancellationToken ct)
{
if (args.ProjectId == Guid.Empty || args.PlotId == Guid.Empty)
return ToolResult.Invalid("Project and plot are required.");
var plot = await plotRepository.FindAsync(
context.TenantId, args.ProjectId, args.PlotId, ct);
if (plot is null)
return ToolResult.NotFound("The requested plot was not found.");
var allowed = await authorization.CanReadPlanningAsync(
context.UserId, plot, ct);
if (!allowed)
return ToolResult.Denied("Planning access is not permitted.");
return ToolResult.Success(MapPosition(plot));
}
Structured Outputs and strict schemas reduce malformed calls. They do not replace these domain checks.
6. Four useful categories of tools
Classifying tools makes risk easier to understand.
| Category | BuildEstate Pro example | Typical controls |
|---|---|---|
| Read | Get planning position | Resource authorization and data minimization |
| Calculate | Calculate a review deadline | Deterministic versioned rules |
| Propose | Prepare a missing-evidence task | Validation and durable draft storage |
| Command | Create the approved task | Exact approval, idempotency and reconciliation |
Read tools
Read tools should return only the data required for the decision. A model asking for one planning position does not need an entire EF Core entity graph, audit history or every document in the project.
Calculation tools
If a rule is deterministic, implement it in C#:
public static DateOnly CalculateReviewDeadline(
DateOnly conditionDate,
int workingDays) =>
BusinessCalendar.AddWorkingDays(conditionDate, workingDays);
The agent can decide when this calculation helps, but it should not invent how working days are counted.
Proposal tools
A proposal captures intent without changing the authoritative business state. It can contain the suggested title, reason, supporting evidence and expected consequence.
Command tools
A command changes business state. It requires the strongest controls and should normally be unavailable until the workflow holds a valid approval.
7. Separate proposal from execution
Avoid a tool called create_or_suggest_task. Nobody can tell whether it changes data.
Use two explicit contracts:
public sealed record PrepareEvidenceTaskArgs(
Guid ProjectId,
Guid PlotId,
string EvidenceType,
string Reason,
DateOnly? SuggestedDueDate);
public sealed record ExecuteApprovedTaskArgs(
Guid ProposalId,
Guid ApprovalId);
The proposal result might be:
public sealed record TaskProposalResult(
Guid ProposalId,
string DisplayTitle,
string Consequence,
string ProposalHash,
DateTimeOffset ExpiresAtUtc);
The review screen shows the exact proposal. Approval binds to its hash, affected resource, evidence version, approver and expiry. If someone edits the title, due date or project, the proposal has changed and normally needs fresh approval.
The agent must not approve its own work. A second model acting as a “reviewer agent” is still not a qualified human approver unless your business policy explicitly permits automated approval for that low-risk action.
8. Authorization belongs at execution time
Filtering the tool list is useful. If a user cannot create tasks, do not show the command tool to the model. This reduces accidental selection.
But hiding the tool is not enough. The handler must authorize every call because:
- model output is untrusted;
- permissions may have changed during a long run;
- a tool request may be replayed;
- an identifier may belong to another tenant;
- a compromised client may call the endpoint directly.
For downstream services, choose deliberately between service identity and on-behalf-of user identity. Keep tokens short-lived, minimally scoped and bound to the intended audience. Never send model-provider API keys to a business tool.
9. Safe tool results
Tool results become new model input, so they are another trust boundary.
Do not return:
- raw exception stack traces;
- connection strings or access tokens;
- complete database entities;
- arbitrary HTML;
- huge document collections;
- instructions copied from untrusted evidence as though they were policy.
public sealed record EvidenceObservation(
string EvidenceId,
string SourceTitle,
string ApprovedVersion,
string Extract,
DateTimeOffset RetrievedAtUtc,
string DataClassification);
If an uploaded document contains “ignore your rules,” the model receives it as quoted evidence linked to a source. It does not become a system instruction and cannot add tools or permissions.
10. Give errors a calm, structured meaning
Returning arbitrary exception text encourages the model to improvise. Use a small error taxonomy:
public enum ToolErrorCode
{
InvalidRequest,
PermissionDenied,
NotFound,
Conflict,
RateLimited,
TransientFailure,
UnknownOutcome
}
public sealed record ToolError(
ToolErrorCode Code,
string SafeMessage,
bool CanRetry,
TimeSpan? RetryAfter = null);
The orchestrator can now behave predictably:
InvalidRequestmay allow one model correction.PermissionDeniedstops that path.RateLimitedmay retry after a bounded delay.Conflictreloads current state.UnknownOutcometriggers reconciliation rather than blind retry.
11. Retries, timeouts and cancellation
Retries are safe only when the operation is retryable. Reading a project record can usually be retried after a transient timeout. Creating a task may have succeeded even when the response was lost.
Every command should carry an idempotency key:
var key = $"agent:{executionId}:proposal:{proposalId}";
var result = await taskClient.CreateAsync(
command,
idempotencyKey: key,
cancellationToken: ct);
If the call times out, query the downstream system using the same key. There are three honest outcomes:
- the task exists, so record success;
- it does not exist, so retry safely with the same key;
- the outcome cannot be established, so enter
ReconciliationRequired.
12. Microsoft.Extensions.AI and Foundry
Microsoft.Extensions.AI offers provider-neutral .NET abstractions for chat clients and tool calling. A tool can be represented as an AIFunction, supplied to an IChatClient, and invoked through the tool-calling client pipeline.
A simplified example looks like this:
AIFunction getPlanningPosition = AIFunctionFactory.Create(
async (Guid projectId, Guid plotId, CancellationToken ct) =>
await planningTools.GetPositionAsync(projectId, plotId, ct),
name: "get_planning_position",
description: "Reads the authorized planning position for one project plot.");
var options = new ChatOptions
{
Tools = [getPlanningPosition]
};
var response = await chatClient.GetResponseAsync(
messages,
options,
cancellationToken);
The exact integration changes depending on the provider and library version, but the architectural rule remains stable: the function body calls a protected application service that repeats validation and authorization.
Microsoft Foundry can also expose built-in tools, custom functions, OpenAPI capabilities and MCP servers. A managed toolbox improves discovery, authentication and versioning, but discovery does not equal permission. Curate what the agent receives and keep domain authorization in the service that owns the data or action.
13. Observe the complete tool call
Useful tracing should answer:
- Which execution requested the tool?
- Which tool and schema version were used?
- Which model deployment requested it?
- Did schema validation pass?
- What policy decision was made?
- How long did the handler take?
- What result classification returned?
- Which evidence or business record was involved?
- Was the call a retry or duplicate?
With Foundry observability, Application Insights and OpenTelemetry, you can correlate model activity with your ASP.NET Core spans. Keep a separate business audit for consequential actions; a diagnostic trace is not automatically a compliant audit record.
14. Test tools without relying on a model
Most tool behaviour can be tested as ordinary C#.
[Fact]
public async Task GetPosition_denies_a_project_from_another_tenant()
{
var context = TestContext.ForTenant(TenantIds.North);
var args = new GetPlanningPositionArgs(
ProjectIds.SouthProject,
PlotIds.Plot14);
var result = await handler.HandleAsync(args, context, default);
result.Error!.Code.Should().Be(ToolErrorCode.PermissionDenied);
}
Test four layers:
- Schema tests — missing fields, unexpected properties, invalid enums and boundary lengths.
- Handler tests — business rules, authorization, cancellation and idempotency.
- Agent evaluations — correct tool selection, correct arguments and knowing when not to call a tool.
- End-to-end tests — model, policy, state, approval, command and final answer.
15. Govern the tool catalogue
Every production tool needs:
- a named owner;
- a risk and data classification;
- a versioned schema and description;
- an expected latency and failure contract;
- an authorization policy;
- a kill switch;
- evaluation coverage;
- a retirement process.
16. Production review checklist
- Tools expose narrow business intentions, not generic infrastructure.
- Model arguments never provide trusted identity or privileges.
- Strict schemas and semantic validation are both present.
- Resource authorization is repeated inside every handler.
- Reads, calculations, proposals and commands are visibly distinct.
- Commands require exact approval where appropriate.
- Side effects are idempotent and reconcilable.
- Timeouts, retry rules and cancellation are explicit.
- Results are bounded, sanitized and linked to provenance.
- Errors are structured and safe to show to a model.
- Traces avoid secrets and unnecessary personal data.
- Tool selection and handler behaviour are evaluated separately.
- Operators can disable one risky capability quickly.
17. What you should take away
A tool is a carefully controlled doorway from probabilistic model output into trusted software.
The model may choose the doorway and suggest arguments. ASP.NET Core checks who is asking, which resource is involved, whether the arguments make sense and whether the consequence is permitted. Microsoft Foundry and Microsoft.Extensions.AI can make tool integration easier, but your application retains responsibility for business truth and authority.
Keep this sentence close:
Tool calling gives a model options, not permissions.In Module 3: Agentic Architecture Patterns, we will decide when one bounded agent is enough and when routers, planners, workers, critics or human review genuinely earn their additional complexity.
