Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Resources;
Expand Down Expand Up @@ -42,8 +43,8 @@ public Agent365Exporter(
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));

if (_options.TokenResolver == null)
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver must be provided.");
if (_options.TokenResolver == null && _options.ContextualTokenResolver == null)
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver or ContextualTokenResolver must be provided.");

_httpClient = httpClient ?? HttpClientFactory.CreateWithTimeout(options.ExporterTimeoutMilliseconds);
_resource = resource ?? ResourceBuilder.CreateEmpty().Build();
Expand Down Expand Up @@ -72,7 +73,9 @@ public override ExportResult Export(in Batch<Activity> batch)
groups: groups,
resource: _resource,
options: _options,
tokenResolver: (agentId, tenantId) => _options.TokenResolver!(agentId, tenantId),
tokenResolver: (agentId, tenantId) => _options.TokenResolver != null
? _options.TokenResolver(agentId, tenantId)
: Task.FromResult<string?>(null),
sendAsync: request => _httpClient.SendAsync(request)
).GetAwaiter().GetResult();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public Agent365ExporterAsync(
this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
this._options = options ?? throw new ArgumentNullException(nameof(options));

if (_options.TokenResolver == null)
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver must be provided.");
if (_options.TokenResolver == null && _options.ContextualTokenResolver == null)
throw new ArgumentNullException(nameof(options.TokenResolver), "Agent365ExporterOptions.TokenResolver or ContextualTokenResolver must be provided.");

this._httpClient = httpClient ?? HttpClientFactory.CreateWithTimeout(options.ExporterTimeoutMilliseconds);
this._resource = resource ?? ResourceBuilder.CreateEmpty().Build();
Expand Down Expand Up @@ -74,7 +74,9 @@ await _core.ExportBatchCoreAsync(
groups: groups,
resource: this._resource,
options: this._options,
tokenResolver: (agentId, tenantId) => this._options.TokenResolver!(agentId, tenantId),
tokenResolver: (agentId, tenantId) => this._options.TokenResolver != null
? this._options.TokenResolver(agentId, tenantId)
: Task.FromResult<string?>(null),
sendAsync: request => this._httpClient.SendAsync(request, cancellationToken)
).ConfigureAwait(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class Agent365ExporterCore
InvokeAgentScope.OperationName,
ExecuteToolScope.OperationName,
OutputScope.OperationName,
OpenTelemetryConstants.ApplyGuardrailOperationName,
"chat",
nameof(InferenceOperationType.Chat),
};
Expand Down Expand Up @@ -193,7 +194,23 @@ public async Task<ExportResult> ExportBatchCoreAsync(
string? token = null;
try
{
token = await tokenResolver(agentId, tenantId).ConfigureAwait(false);
// Prefer ContextualTokenResolver when set; extract agentic user ID from the
// first activity in the group (1:1 relationship between agent and agentic user).
if (options.ContextualTokenResolver != null)
{
var agenticUserId = activities.Count > 0
? activities[0].GetAttributeOrBaggage(OpenTelemetryConstants.AgentAUIDKey)
: null;
var identity = new AgentIdentity(agentId, agenticUserId);

var context = new TokenResolverContext(identity, tenantId);
token = await options.ContextualTokenResolver(context).ConfigureAwait(false);
}
else
{
token = await tokenResolver(agentId, tenantId).ConfigureAwait(false);
}

this._logger?.LogDebug("Agent365ExporterCore: Obtained token for agent {AgentId} tenant {TenantId}.", agentId, tenantId);
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters
/// </summary>
public delegate Task<string?> AsyncAuthTokenResolver(string agentId, string tenantId);

/// <summary>
/// Async delegate used by the exporter to obtain an auth token using rich context.
/// Provides additional fields (e.g. <see cref="TokenResolverContext.Identity"/>)
/// beyond what <see cref="AsyncAuthTokenResolver"/> offers.
/// Must be fast and non-blocking (use internal caching elsewhere).
/// Return null/empty to omit the Authorization header.
Comment thread
nikhilNava marked this conversation as resolved.
/// </summary>
public delegate Task<string?> AsyncContextualTokenResolver(TokenResolverContext context);

/// <summary>
/// Delegate used by the exporter to resolve the endpoint host or URL for a given tenant id.
/// The return value may be a bare host name (e.g. "agent365.svc.cloud.microsoft") or a full URL
Expand Down Expand Up @@ -46,10 +55,20 @@ public Agent365ExporterOptions()
public string ClusterCategory { get; set; } = "production";

/// <summary>
/// Async delegate used to resolve the auth token. REQUIRED.
/// Async delegate used to resolve the auth token.
/// Either this or <see cref="ContextualTokenResolver"/> must be set.
/// When both are set, <see cref="ContextualTokenResolver"/> takes precedence.
/// </summary>
public AsyncAuthTokenResolver? TokenResolver { get; set; }

/// <summary>
/// Async delegate used to resolve the auth token with rich context, which may include the
/// agentic user ID associated with the export batch context.
/// Takes precedence over <see cref="TokenResolver"/> when set.
/// The exporter does not guarantee separate batching or resolver invocation per agentic user ID.
/// </summary>
public AsyncContextualTokenResolver? ContextualTokenResolver { get; set; }

/// <summary>
/// Delegate used to resolve the endpoint host or URL for a given tenant id.
/// Defaults to returning <see cref="DefaultEndpointHost"/>.
Expand Down
38 changes: 38 additions & 0 deletions src/Observability/Runtime/Tracing/Exporters/AgentIdentity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters
{
/// <summary>
/// Represents the identity of an agent and its acting user.
/// <para>
/// In the AI teammate scenario, <see cref="AgenticUserId"/> is 1:1 with <see cref="AgentId"/>.
/// In the S2S scenario, <see cref="AgenticUserId"/> will be null.
/// </para>
/// </summary>
public class AgentIdentity
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentIdentity"/> class.
/// </summary>
/// <param name="agentId">The agent identifier.</param>
/// <param name="agenticUserId">The agentic user identifier (AAD Object ID), or null in S2S scenarios.</param>
public AgentIdentity(string agentId, string? agenticUserId = null)
{
AgentId = agentId;
AgenticUserId = agenticUserId;
}

/// <summary>
/// Gets the agent identifier.
/// </summary>
public string AgentId { get; }

/// <summary>
/// Gets the agentic user identifier (AAD Object ID).
/// In the AI teammate scenario, this value is 1:1 with <see cref="AgentId"/>.
/// Will be null in the S2S scenario.
/// </summary>
public string? AgenticUserId { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters
{
/// <summary>
/// Provides contextual information to the token resolver delegate.
/// <para>
/// <see cref="Identity"/> provides first-class access to agent identity fields (agent ID,
/// agentic user ID). <see cref="TenantId"/> and <see cref="Identity"/>
/// together identify the cache key.
/// </para>
/// </summary>
public class TokenResolverContext
{
/// <summary>
/// Initializes a new instance of the <see cref="TokenResolverContext"/> class.
/// </summary>
/// <param name="identity">The agent identity associated with this request.</param>
/// <param name="tenantId">The tenant identifier (cache key).</param>
public TokenResolverContext(AgentIdentity identity, string tenantId)
{
Identity = identity;
TenantId = tenantId;
}

/// <summary>
/// Gets the agent identity associated with this token resolution request.
/// Contains the agent ID and agentic user ID (AAD Object ID) as first-class properties.
/// </summary>
public AgentIdentity Identity { get; }

/// <summary>
/// Gets the tenant identifier (part of the cache key).
/// </summary>
public string TenantId { get; }
}
}
Loading
Loading