Skip to content

Manual Tool Calling in Microsoft Agent Framework

Move beyond the MAF "black box" by executing tool calls manually. Learn how to gain the observability and control needed for advanced, custom agentic workflows.

Mago Brown - Unsplash

Table of Contents

This post demonstrates how to make manual tool calls using the Microsoft Agent Framework (MAF).

When you expose tools to your agents using the Microsoft Agent Framework, and the LLM/agent selects one, MAF handles tool parsing, execution and results processing for you. This is convenient and easy to use, but you sacrifice flexibility. In more advanced scenarios, with workflows, intermediate feedback, and tracing, you often need access to the raw tool response.

Let’s explore how you can gain more control and visibility by executing tools manually.

Microsoft Agent Framework Tool Calling

When you create an Agent in MAF and provide it with tools, this is the execution cycle:

  1. Invocation: The caller invokes the agent with a message.
  2. Selection: The underlying LLM processes the message and selects a tool.
  3. Response: The tool call is returned as part of the LLM response.
  4. Execution: MAF parses the tool call, executes it, and records the tool results.
  5. Re-Invocation: MAF invokes the agent/LLM again passing the tool results.
  6. Final Result: The final agent/LLM response is returned to the caller.

All of this happens internally. The caller only sees the final interpretation of the results, not the internal tool call itself.

Preventing Automatic Tool Execution

To execute tools manually, we need to prevent MAF from doing it automatically.

If we provide an agent with a tool declaration but without a method to call, the LLM will still select the tool and return it in the response, but MAF won't be able to execute any code. We can achieve this using the AsDeclarationOnly() extension method.

public static List<AITool> GetDeclarationOnlyTools()
{
    return Tools.Select(toolMeta => toolMeta.Value.AsDeclarationOnly()).Cast<AITool>().ToList();
}

public static List<AITool> GetTools()
{
    return Tools.Select(toolMeta => toolMeta.Value).Cast<AITool>().ToList();
}

By passing these "Declaration Only" tools to your agent, they will be selected by the LLM but never automatically executed by the framework.

Executing Tools Manually

In a previous post, we created a custom agent by sub-classing the MAF DelegatingAgent.

public class ManualToolCallAgent(AIAgent agent) : DelegatingAIAgent(agent)
{
    protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
        IEnumerable<ChatMessage> messages,
        AgentSession? thread = null,
        AgentRunOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var agentSession = await InnerAgent.GetNewSessionAsync(cancellationToken);
        
        var tools = new Dictionary<string, FunctionCallContent>();

        await foreach (var agentResponse in InnerAgent.RunStreamingAsync(messages, agentSession, options, cancellationToken))
        {
            tools.AddToolCalls(agentResponse.Contents);

            yield return agentResponse;
        }

        var toolResults = new List<AIContent>();

        foreach (var functionCallContent in tools)
        {
            var function = AgentTools.Get(functionCallContent.Key);

            var result = await function.InvokeAsync(new AIFunctionArguments(functionCallContent.Value.Arguments), cancellationToken);

            toolResults.Add(new FunctionResultContent(
                functionCallContent.Value.CallId,
                result));
        }

        var toolMessage = new ChatMessage(ChatRole.Tool, toolResults);
        
        await foreach (var update in InnerAgent.RunStreamingAsync([toolMessage], agentSession, cancellationToken: cancellationToken))
        {      
            yield return update;
        }
    }
}

Capturing Tool Calls

We can use a helper extension method to store the function calls in a dictionary keyed by their name:

public static void AddToolCalls(this Dictionary<string, FunctionCallContent> tools, IList<AIContent> contents)
{
    foreach (var content in contents)
    {
        if (content is FunctionCallContent callContent)
        {
            tools[callContent.Name] = callContent;
        }
    }
}

We then loop through the list of tool calls and execute (invoke) them manually, storing the results. The tool call results are then passed back to the LLM so it can process them and produce the final result.

Key Considerations

  • Tool Lookup: Since the LLM returns a definition without the method reference, you need a way to look up the executable tool (e.g., a static AgentTools registry or storing them within the Agent class).
  • Error Handling: In a production environment, manual tool invocation should be wrapped in try/catch blocks to log exceptions and return appropriate error messages to the consumer.
  • User Feedback: Manual calling is an excellent opportunity to provide intermediate progress updates to the user while long-running tools are executing.
  • Function Call Tracking: You will get an exception if you don’t provide a tool call result, with the corresponding CallId, to the LLM.

Until Next Time

Manual tool calling may seem trivial, but it opens up endless possibilities in terms of intermediate feedback, tracing, and granular workflow execution. Moving away from the MAF black- box execution model gives you the observability required to build agentic systems.

In upcoming posts we will explore how manual tool calling can be leveraged in MAF workflows.

You can find the code sample here

Comments

Latest