Skip to content

.NET: emit execute_tool <function_name> span for function tools (parity with Python / GenAI conventions) #2015

@zakimaksyutov

Description

@zakimaksyutov

Description

When using Microsoft.Agents.AI with UseOpenTelemetry(sourceName: ...) and an in-proc tool created via AIFunctionFactory.Create(...), I don’t see an execute_tool <function_name> child span in traces.

Repro

    private static void ConfigureOpenTelemetry()
    {
        ResourceBuilder resource = ResourceBuilder.CreateDefault().AddService(serviceName: ServiceName);
        tracerProvider =
            Sdk.CreateTracerProviderBuilder()
                .SetResourceBuilder(resourceBuilder: resource)
                .AddSource(OTelSourceName)
                .AddSource("Microsoft.Agents.AI")
                .AddSource("Microsoft.Extensions.AI")
                .AddHttpClientInstrumentation()
                .AddAzureMonitorTraceExporter(o =>
                {
                    o.ConnectionString = AppInsightsConnectionString;
                })
                .Build();
        loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.ClearProviders();
            builder.AddOpenTelemetry(options =>
            {
                options.SetResourceBuilder(resourceBuilder: resource);
                options.IncludeFormattedMessage = true;
                options.ParseStateValues = true;
                options.AddAzureMonitorLogExporter(o =>
                {
                    o.ConnectionString = AppInsightsConnectionString;
                }); // uses env var connection string
            });
        });
        logger = loggerFactory.CreateLogger<Program>();
    }

    private static async Task DoWork()
    {
        Uri endpoint = new Uri("<redacted>");

        // Wrap the method as an in-proc tool
        AIFunction doneTool = AIFunctionFactory.Create(
            (Func<string, string, Task<string>>)SimpleTools.DoneAsync,
            new AIFunctionFactoryOptions { Name = "mark_done", Description = "Accepts two strings and returns 'Done'." });

        // Uses your Azure CLI sign-in (az login). You can swap to an API key later.
        AIAgent agent = new AzureOpenAIClient(endpoint: endpoint, new AzureCliCredential())
            .GetChatClient("gpt-4.1")
            .CreateAIAgent("You are a concise assistant.",
                tools: new List<AITool> { doneTool })
            .AsBuilder()
            .UseOpenTelemetry(sourceName: OTelSourceName, otel =>
            {
                otel.EnableSensitiveData = true;
            })
            .Build();

        Console.WriteLine("Enter a prompt for the agent (blank line to exit).");

        while (true)
        {
            Console.Write("You> ");
            string? userInput = Console.ReadLine();

            if (string.IsNullOrWhiteSpace(value: userInput))
            {
                break;
            }

            AgentRunResponse reply = await agent.RunAsync(message: userInput);
            Console.WriteLine(value: reply);
        }
    }

public static class SimpleTools
{
    [Description("Returns the literal string 'Done'.")]
    public static Task<string> DoneAsync(
        [Description("First parameter.")] string first,
        [Description("Second parameter.")] string second)
    {
        Console.WriteLine($"SimpleTools.DoneAsync was called with '{first}' and '{second}'");
        return Task.FromResult("Done");
    }
}

Output

Enter a prompt for the agent (blank line to exit).
You> Mark #1 as done
SimpleTools.DoneAsync was called with '#1' and 'done'
#1 has been marked as done.
You>

Tool call is captured in gen_ai.output.messages but not as internal span:

[
  {
    "role": "assistant",
    "parts": [
      {
        "type": "tool_call",
        "id": "call_xscticFKfNJIdNQTjQDwMYgR",
        "name": "mark_done",
        "arguments": {
          "first": "#1",
          "second": "done"
        }
      }
    ],
    "finish_reason": "stop"
  },
  {
    "role": "tool",
    "parts": [
      {
        "type": "tool_call_response",
        "id": "call_xscticFKfNJIdNQTjQDwMYgR",
        "response": "Done"
      }
    ],
    "finish_reason": "stop"
  },
  {
    "role": "assistant",
    "parts": [
      {
        "type": "text",
        "content": "#1 has been marked as done."
      }
    ],
    "finish_reason": "stop"
  }
]
Image

Expected

Child span named execute_tool mark_done (following the GenAI semantic conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/?utm_source=chatgpt.com#execute-tool-span), with args/result attributes when EnableSensitiveData = true.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions