AI Engineering

Tools and Function Calling for Safe AI Agents

Afzal AhmedFaz Ahmed
·13 September 2026·12 min read
AI AgentsFunction CallingStructured OutputsASP.NET CoreAuthorizationIdempotencyOpenAI API

Why This Matters

Build narrow agent tools with strict schemas, resource authorisation, approval, idempotency, timeouts, safe observations and production tracing.

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_sql
  • run_shell_command
  • write_any_file
  • send_http_request
  • call_any_azure_resource
These capabilities are too broad. Even a small misunderstanding in the prompt or arguments can have a large consequence.

Prefer narrow business capabilities:

  • get_planning_position
  • search_approved_project_evidence
  • calculate_review_deadline
  • prepare_missing_evidence_task
  • execute_approved_project_task
This is the same design instinct we use in a well-structured .NET domain. We expose an intention such as 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:
  1. get_planning_position — reads the approved planning record.
  2. search_approved_evidence — retrieves authorized evidence.
  3. prepare_evidence_task — creates a proposal with no business effect.
  4. execute_approved_task — creates a task only after valid approval.
The model first requests the planning position. The observation shows an ecology condition. It then searches ecology evidence. The search returns no current approved report. The model requests a task proposal.

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.
The model must never choose trusted identity or authority. These values come from the authenticated execution context.
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:

  1. Shape: Is this valid according to the tool schema?
  2. Meaning: Does the project and plot combination exist and make sense?
  3. 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.

CategoryBuildEstate Pro exampleTypical controls
ReadGet planning positionResource authorization and data minimization
CalculateCalculate a review deadlineDeterministic versioned rules
ProposePrepare a missing-evidence taskValidation and durable draft storage
CommandCreate the approved taskExact 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.
In Azure, Microsoft Entra ID and managed identity can establish user and workload identity. Foundry RBAC controls access to Foundry resources. Your ASP.NET Core policy layer still enforces domain access to the BuildEstate Pro project, plot and command.

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.
Return a bounded result with provenance:
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:

  • InvalidRequest may allow one model correction.
  • PermissionDenied stops that path.
  • RateLimited may retry after a bounded delay.
  • Conflict reloads current state.
  • UnknownOutcome triggers reconciliation rather than blind retry.
Do not ask the model to solve an infrastructure failure. A database outage is an operational condition, not a reasoning puzzle.

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:

  1. the task exists, so record success;
  2. it does not exist, so retry safely with the same key;
  3. the outcome cannot be established, so enter ReconciliationRequired.
Cancellation must flow through model calls, HTTP calls and database work. Cancelling the local wait does not undo an external action already accepted. Make that distinction visible in execution state.

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?
Do not log secrets or full personal data simply because tracing is convenient. Record redacted arguments, stable identifiers and outcome metadata.

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:

  1. Schema tests — missing fields, unexpected properties, invalid enums and boundary lengths.
  2. Handler tests — business rules, authorization, cancellation and idempotency.
  3. Agent evaluations — correct tool selection, correct arguments and knowing when not to call a tool.
  4. End-to-end tests — model, policy, state, approval, command and final answer.
Include traps: similarly named projects, malicious document instructions, stale approval, a duplicated command, a timeout after downstream success and a tool result that cites the wrong project.

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.
Tool descriptions are production configuration because they influence model choice. Review and version them like code. Monitor selection frequency, malformed arguments, policy denials, retries, latency and cost. Remove unused or overlapping tools; a smaller catalogue usually improves both safety and selection accuracy.

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.

Primary references

Applied learning context

This production-minded educational design uses BuildEstate Pro as a realistic case study; it does not claim that an autonomous agent is deployed in the live project.

View BuildEstate Pro →
Afzal Ahmed

Faz Ahmed

Senior Full Stack Engineer & Technical Lead

A hands-on engineer with 15+ years in commercial software. I publish what I am studying, revising and testing so visitors can see both established experience and learning still in progress.

How would you approach this problem? I'd love to hear your thoughts or continue the discussion.

Connect on LinkedIn →