Skip to content

Commit a10ebc0

Browse files
fpfp100jsl517claude
authored
Use Activity helpers and ResolveAgentIdentity for agent telemetry (#211)
* Use Activity helper methods and ResolveAgentIdentity for agent telemetry Replace direct ChannelAccount property access with Activity class helpers (getAgenticInstanceId, getAgenticUser, getAgenticTenantId) in ScopeUtils and TurnContextUtils. Use RuntimeUtility.ResolveAgentIdentity for blueprint ID resolution when authToken is provided, falling back to recipient.agenticAppBlueprintId. Add authToken parameter with overloaded signatures (required + deprecated no-arg) for backward compatibility. Upgrade @microsoft/agents-hosting and agents-activity to ^1.3.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix lint errors: extract private Core methods, clean up overload signatures - Extract deriveAgentDetailsCore and buildInvokeAgentDetailsCore as private methods so internal callers bypass deprecated overloads (fixes @typescript-eslint/no-deprecated lint errors) - Remove `public` from implementation signatures of overloaded methods (only the two declared overloads should be public-facing) - Fix stale JSDoc on getTenantIdPair (removed ChannelData reference) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * comments * Fix agent identity resolution: use ResolveAgentIdentity for agentId, token blueprint claim for agentBlueprintId - agentId now uses ResolveAgentIdentity (works for both app-based and blueprint agents) - agentBlueprintId now reads xms_par_app_azp from token for blueprint agents - Remove deprecated overloads and backward-compat code, authToken is now required - Consolidate deriveAgentDetailsCore into deriveAgentDetails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update CHANGELOG for ScopeUtils breaking changes and Activity helper adoption Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use optional chaining on Activity helper method calls for resilience Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Refactor agent identity resolution and add resolveAuthToken test coverage - Extract resolveEmbodiedAgentIds() into TurnContextUtils as shared helper - Align getTargetAgentBaggagePairs with deriveAgentDetails (no agentId for non-agentic) - Add resolveAuthToken to OutputLoggingMiddleware with per-request/cache dual path - Export isPerRequestExportEnabled from observability public API - Fix CHANGELOG to reflect actual agentId resolution behavior - Add test coverage for resolveAuthToken (per-request, cache, fallback paths) - Remove unused ResolveAgentIdentity mock from scope-utils tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix A365_AUTH_TOKEN_KEY JSDoc and normalize empty strings in resolveEmbodiedAgentIds - Correct JSDoc for A365_AUTH_TOKEN_KEY: token is used for embodied/agentic requests (blueprint ID resolution), not non-agentic requests - Normalize empty strings to undefined in resolveEmbodiedAgentIds to prevent noisy telemetry attributes from empty agentId/agentBlueprintId values Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: jsl517 <pefan@microsoft.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ddf4902 commit a10ebc0

13 files changed

Lines changed: 332 additions & 147 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Breaking Changes (`@microsoft/agents-a365-observability-hosting`)
11+
12+
- **`ScopeUtils.deriveAgentDetails(turnContext, authToken)`** — New required `authToken: string` parameter.
13+
- **`ScopeUtils.populateInferenceScopeFromTurnContext(details, turnContext, authToken, ...)`** — New required `authToken: string` parameter.
14+
- **`ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, turnContext, authToken, ...)`** — New required `authToken: string` parameter.
15+
- **`ScopeUtils.populateExecuteToolScopeFromTurnContext(details, turnContext, authToken, ...)`** — New required `authToken: string` parameter.
16+
- **`ScopeUtils.buildInvokeAgentDetails(details, turnContext, authToken)`** — New required `authToken: string` parameter.
17+
1018
### Added
1119
- **OutputScope**: Tracing scope for outgoing agent messages with caller details, conversation ID, source metadata, and parent span linking.
1220
- **BaggageMiddleware**: Middleware for automatic OpenTelemetry baggage propagation from TurnContext.
1321
- **OutputLoggingMiddleware**: Middleware that creates OutputScope spans for outgoing messages with lazy parent span linking via `A365_PARENT_SPAN_KEY`.
1422
- **ObservabilityHostingManager**: Manager for configuring hosting-layer observability middleware with `ObservabilityHostingOptions`.
1523

1624
### Changed
25+
- `ScopeUtils.deriveAgentDetails` now resolves `agentId` via `activity.getAgenticInstanceId()` for embodied (agentic) agents only. For non-embodied agents, `agentId` is `undefined` since the token's app ID cannot reliably be attributed to the agent.
26+
- `ScopeUtils.deriveAgentDetails` now resolves `agentBlueprintId` from the JWT `xms_par_app_azp` claim via `RuntimeUtility.getAgentIdFromToken()` instead of reading `recipient.agenticAppBlueprintId`.
27+
- `ScopeUtils.deriveAgentDetails` now resolves `agentUPN` via `activity.getAgenticUser()` instead of `recipient.agenticUserId`.
28+
- `ScopeUtils.deriveTenantDetails` now uses `activity.getAgenticTenantId()` instead of `recipient.tenantId`.
29+
- `getTargetAgentBaggagePairs` now uses `activity.getAgenticInstanceId()` instead of `recipient.agenticAppId`.
30+
- `getTenantIdPair` now uses `activity.getAgenticTenantId()` instead of manual `channelData` parsing.
1731
- `InferenceScope.recordInputMessages()` / `recordOutputMessages()` now use JSON array format instead of comma-separated strings.
1832
- `InvokeAgentScope.recordInputMessages()` / `recordOutputMessages()` now use JSON array format instead of comma-separated strings.
1933

packages/agents-a365-observability-hosting/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ export * from './utils/ScopeUtils';
88
export * from './utils/TurnContextUtils';
99
export { AgenticTokenCache, AgenticTokenCacheInstance } from './caching/AgenticTokenCache';
1010
export { BaggageMiddleware } from './middleware/BaggageMiddleware';
11-
export { OutputLoggingMiddleware, A365_PARENT_SPAN_KEY } from './middleware/OutputLoggingMiddleware';
11+
export { OutputLoggingMiddleware, A365_PARENT_SPAN_KEY, A365_AUTH_TOKEN_KEY } from './middleware/OutputLoggingMiddleware';
1212
export { ObservabilityHostingManager } from './middleware/ObservabilityHostingManager';
1313
export type { ObservabilityHostingOptions } from './middleware/ObservabilityHostingManager';

packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,24 @@ import {
99
CallerDetails,
1010
ParentSpanRef,
1111
logger,
12+
isPerRequestExportEnabled,
1213
} from '@microsoft/agents-a365-observability';
1314
import { ScopeUtils } from '../utils/ScopeUtils';
15+
import { AgenticTokenCacheInstance } from '../caching/AgenticTokenCache';
1416

1517
/**
1618
* TurnState key for the parent span reference.
1719
* Set this in `turnState` to link OutputScope spans as children of an InvokeAgentScope.
1820
*/
1921
export const A365_PARENT_SPAN_KEY = 'A365ParentSpanId';
2022

23+
/**
24+
* TurnState key for the auth token.
25+
* Set this in `turnState` so middleware can resolve the agent blueprint ID
26+
* from token claims (used for embodied/agentic requests).
27+
*/
28+
export const A365_AUTH_TOKEN_KEY = 'A365AuthToken';
29+
2130
/**
2231
* Middleware that creates {@link OutputScope} spans for outgoing messages.
2332
* Links to a parent span when {@link A365_PARENT_SPAN_KEY} is set in turnState.
@@ -28,7 +37,8 @@ export const A365_PARENT_SPAN_KEY = 'A365ParentSpanId';
2837
export class OutputLoggingMiddleware implements Middleware {
2938

3039
async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> {
31-
const agentDetails = ScopeUtils.deriveAgentDetails(context);
40+
const authToken = this.resolveAuthToken(context);
41+
const agentDetails = ScopeUtils.deriveAgentDetails(context, authToken);
3242
const tenantDetails = ScopeUtils.deriveTenantDetails(context);
3343

3444
if (!agentDetails || !tenantDetails) {
@@ -47,6 +57,23 @@ export class OutputLoggingMiddleware implements Middleware {
4757
await next();
4858
}
4959

60+
/**
61+
* Resolve the auth token for agent identity resolution.
62+
* When per-request export is enabled, reads from turnState.
63+
* Otherwise, reads from the cached observability token.
64+
*/
65+
private resolveAuthToken(context: TurnContext): string {
66+
if (isPerRequestExportEnabled()) {
67+
return context.turnState.get(A365_AUTH_TOKEN_KEY) as string ?? '';
68+
}
69+
const agentId = context.activity?.getAgenticInstanceId?.() ?? '';
70+
const tenantId = context.activity?.getAgenticTenantId?.() ?? '';
71+
if (agentId && tenantId) {
72+
return AgenticTokenCacheInstance.getObservabilityToken(agentId, tenantId) ?? '';
73+
}
74+
return '';
75+
}
76+
5077
/**
5178
* Creates a send handler that wraps outgoing messages in OutputScope spans.
5279
* Reads parent span ref lazily so the agent handler can set it during `next()`.

packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
InvokeAgentDetails,
1717
ToolCallDetails,
1818
} from '@microsoft/agents-a365-observability';
19+
import { resolveEmbodiedAgentIds } from './TurnContextUtils';
1920

2021
/**
2122
* Unified utilities to populate scope tags from a TurnContext.
@@ -40,26 +41,30 @@ export class ScopeUtils {
4041
* @returns Tenant details if a recipient tenant id is present; otherwise undefined.
4142
*/
4243
public static deriveTenantDetails(turnContext: TurnContext): TenantDetails | undefined {
43-
const tenantId = turnContext?.activity?.recipient?.tenantId;
44+
const tenantId = turnContext?.activity?.getAgenticTenantId?.();
4445
return tenantId ? { tenantId } : undefined;
4546
}
4647

4748
/**
4849
* Derive target agent details from the activity recipient.
50+
* Uses {@link resolveEmbodiedAgentIds} to resolve the agent ID and blueprint ID, which are only
51+
* set for embodied (agentic) agents — see that function for the rationale.
4952
* @param turnContext Activity context
53+
* @param authToken Auth token for resolving agent identity from token claims.
5054
* @returns Agent details built from recipient properties; otherwise undefined.
5155
*/
52-
public static deriveAgentDetails(turnContext: TurnContext): AgentDetails | undefined {
56+
public static deriveAgentDetails(turnContext: TurnContext, authToken: string): AgentDetails | undefined {
5357
const recipient = turnContext?.activity?.recipient;
5458
if (!recipient) return undefined;
59+
const { agentId, agentBlueprintId } = resolveEmbodiedAgentIds(turnContext, authToken);
5560
return {
56-
agentId: recipient.agenticAppId,
61+
agentId,
5762
agentName: recipient.name,
5863
agentAUID: recipient.aadObjectId,
59-
agentBlueprintId: recipient.agenticAppBlueprintId,
60-
agentUPN: recipient.agenticUserId,
64+
agentBlueprintId,
65+
agentUPN: turnContext?.activity?.getAgenticUser?.(),
6166
agentDescription: recipient.role,
62-
tenantId: recipient.tenantId
67+
tenantId: turnContext?.activity?.getAgenticTenantId?.()
6368
} as AgentDetails;
6469
}
6570

@@ -127,17 +132,19 @@ export class ScopeUtils {
127132
* Also records input messages from the context if present.
128133
* @param details The inference call details (model, provider, tokens, etc.).
129134
* @param turnContext The current activity context to derive scope parameters from.
135+
* @param authToken Auth token for resolving agent identity from token claims.
130136
* @param startTime Optional explicit start time (ms epoch, Date, or HrTime).
131137
* @param endTime Optional explicit end time (ms epoch, Date, or HrTime).
132138
* @returns A started `InferenceScope` enriched with context-derived parameters.
133139
*/
134140
static populateInferenceScopeFromTurnContext(
135141
details: InferenceDetails,
136142
turnContext: TurnContext,
143+
authToken: string,
137144
startTime?: TimeInput,
138145
endTime?: TimeInput
139146
): InferenceScope {
140-
const agent = ScopeUtils.deriveAgentDetails(turnContext);
147+
const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken);
141148
const tenant = ScopeUtils.deriveTenantDetails(turnContext);
142149
const conversationId = ScopeUtils.deriveConversationId(turnContext);
143150
const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext);
@@ -161,20 +168,22 @@ export class ScopeUtils {
161168
* Also sets execution type and input messages from the context if present.
162169
* @param details The invoke-agent call details to be augmented and used for the scope.
163170
* @param turnContext The current activity context to derive scope parameters from.
171+
* @param authToken Auth token for resolving agent identity from token claims.
164172
* @param startTime Optional explicit start time (ms epoch, Date, or HrTime).
165173
* @param endTime Optional explicit end time (ms epoch, Date, or HrTime).
166174
* @returns A started `InvokeAgentScope` enriched with context-derived parameters.
167175
*/
168176
static populateInvokeAgentScopeFromTurnContext(
169177
details: InvokeAgentDetails,
170178
turnContext: TurnContext,
179+
authToken: string,
171180
startTime?: TimeInput,
172181
endTime?: TimeInput
173182
): InvokeAgentScope {
174183
const tenant = ScopeUtils.deriveTenantDetails(turnContext);
175184
const callerAgent = ScopeUtils.deriveCallerAgent(turnContext);
176185
const caller = ScopeUtils.deriveCallerDetails(turnContext);
177-
const invokeAgentDetails = ScopeUtils.buildInvokeAgentDetails(details, turnContext);
186+
const invokeAgentDetails = ScopeUtils.buildInvokeAgentDetailsCore(details, turnContext, authToken);
178187

179188
if (!tenant) {
180189
throw new Error('populateInvokeAgentScopeFromTurnContext: Missing tenant details on TurnContext (recipient)');
@@ -189,10 +198,15 @@ export class ScopeUtils {
189198
* Build InvokeAgentDetails by merging provided details with agent info, conversation id and source metadata from the TurnContext.
190199
* @param details Base invoke-agent details to augment
191200
* @param turnContext Activity context
201+
* @param authToken Auth token for resolving agent identity from token claims.
192202
* @returns New InvokeAgentDetails suitable for starting an InvokeAgentScope.
193203
*/
194-
public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext): InvokeAgentDetails {
195-
const agent = ScopeUtils.deriveAgentDetails(turnContext);
204+
public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails {
205+
return ScopeUtils.buildInvokeAgentDetailsCore(details, turnContext, authToken);
206+
}
207+
208+
private static buildInvokeAgentDetailsCore(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails {
209+
const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken);
196210
const srcMetaFromContext = ScopeUtils.deriveSourceMetadataObject(turnContext);
197211
const baseRequest = details.request ?? {};
198212
const baseSource = baseRequest.sourceMetadata ?? {};
@@ -217,6 +231,7 @@ export class ScopeUtils {
217231
* Derives `agentDetails`, `tenantDetails`, `conversationId`, and `sourceMetadata` (channel name/link) from context.
218232
* @param details The tool call details (name, type, args, call id, etc.).
219233
* @param turnContext The current activity context to derive scope parameters from.
234+
* @param authToken Auth token for resolving agent identity from token claims.
220235
* @param startTime Optional explicit start time (ms epoch, Date, or HrTime). Useful when recording a
221236
* tool call after execution has already completed.
222237
* @param endTime Optional explicit end time (ms epoch, Date, or HrTime).
@@ -225,10 +240,11 @@ export class ScopeUtils {
225240
static populateExecuteToolScopeFromTurnContext(
226241
details: ToolCallDetails,
227242
turnContext: TurnContext,
243+
authToken: string,
228244
startTime?: TimeInput,
229245
endTime?: TimeInput
230246
): ExecuteToolScope {
231-
const agent = ScopeUtils.deriveAgentDetails(turnContext);
247+
const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken);
232248
const tenant = ScopeUtils.deriveTenantDetails(turnContext);
233249
const conversationId = ScopeUtils.deriveConversationId(turnContext);
234250
const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext);

packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { TurnContext } from '@microsoft/agents-hosting';
77
import { ExecutionType, OpenTelemetryConstants } from '@microsoft/agents-a365-observability';
88
import {RoleTypes} from '@microsoft/agents-activity';
9+
import { Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime';
910

1011
/**
1112
* TurnContext utility methods.
@@ -64,17 +65,37 @@ export function getExecutionTypePair(turnContext: TurnContext): Array<[string, s
6465
return [[OpenTelemetryConstants.GEN_AI_EXECUTION_TYPE_KEY, executionType]];
6566
}
6667

68+
/**
69+
* Resolves the agent instance ID and blueprint ID for embodied (agentic) agents only.
70+
* For the non-embodied agent case, we cannot reliably determine whether the token contains an app ID,
71+
* or whether the app ID present in the token claims actually corresponds to this agent. Therefore,
72+
* we only set agentId and agentBlueprintId for embodied (agentic) agents.
73+
* @param turnContext Activity context
74+
* @param authToken Auth token for resolving blueprint ID from token claims.
75+
* @returns Object with agentId and agentBlueprintId, both undefined for non-embodied agents.
76+
*/
77+
export function resolveEmbodiedAgentIds(turnContext: TurnContext, authToken: string): { agentId: string | undefined; agentBlueprintId: string | undefined } {
78+
const isAgentic = turnContext?.activity?.isAgenticRequest?.();
79+
const rawAgentId = isAgentic ? turnContext.activity.getAgenticInstanceId?.() : undefined;
80+
const rawBlueprintId = isAgentic ? RuntimeUtility.getAgentIdFromToken(authToken) : undefined;
81+
return {
82+
agentId: rawAgentId || undefined,
83+
agentBlueprintId: rawBlueprintId || undefined,
84+
};
85+
}
86+
6787
/**
6888
* Extracts agent/recipient-related OpenTelemetry baggage pairs from the TurnContext.
6989
* @param turnContext The current TurnContext (activity context)
90+
* @param authToken Optional auth token for resolving agent blueprint ID from token claims.
7091
* @returns Array of [key, value] pairs for agent identity and description
7192
*/
72-
export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[string, string]> {
73-
if (!turnContext || !turnContext.activity?.recipient) {
93+
export function getTargetAgentBaggagePairs(turnContext: TurnContext, authToken?: string): Array<[string, string]> {
94+
if (!turnContext || !turnContext.activity?.recipient) {
7495
return [];
7596
}
76-
const recipient = turnContext.activity.recipient;
77-
const agentId = recipient.agenticAppId;
97+
const recipient = turnContext.activity.recipient;
98+
const { agentId } = authToken ? resolveEmbodiedAgentIds(turnContext, authToken) : { agentId: turnContext.activity?.isAgenticRequest?.() ? turnContext.activity.getAgenticInstanceId?.() : undefined };
7899
const agentName = recipient.name;
79100
const aadObjectId = recipient.aadObjectId;
80101
const agentDescription = recipient.role;
@@ -88,29 +109,12 @@ export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[str
88109
}
89110

90111
/**
91-
* Extracts the tenant ID baggage key-value pair, attempting to retrieve from ChannelData if necessary.
112+
* Extracts the tenant ID baggage key-value pair using the Activity's getAgenticTenantId() helper.
92113
* @param turnContext The current TurnContext (activity context)
93114
* @returns Array of [key, value] for tenant ID
94115
*/
95116
export function getTenantIdPair(turnContext: TurnContext): Array<[string, string]> {
96-
let tenantId = turnContext.activity?.recipient?.tenantId;
97-
98-
99-
// If not found, try to extract from channelData. Accepts both object and JSON string.
100-
if (!tenantId && turnContext.activity?.channelData) {
101-
try {
102-
let channelData: unknown = turnContext.activity.channelData;
103-
if (typeof channelData === 'string') {
104-
channelData = JSON.parse(channelData);
105-
}
106-
if (
107-
typeof channelData === 'object' && channelData !== null) {
108-
tenantId = (channelData as { tenant: { id?: string } })?.tenant?.id;
109-
}
110-
} catch (_err) {
111-
// ignore JSON parse errors
112-
}
113-
}
117+
const tenantId = turnContext.activity?.getAgenticTenantId?.();
114118
return tenantId ? [[OpenTelemetryConstants.TENANT_ID_KEY, tenantId]] : [];
115119
}
116120

packages/agents-a365-observability/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,8 @@ export { OutputScope } from './tracing/scopes/OutputScope';
5656
export { logger, setLogger, getLogger, resetLogger, formatError } from './utils/logging';
5757
export type { ILogger } from './utils/logging';
5858

59+
// Exporter utilities
60+
export { isPerRequestExportEnabled } from './tracing/exporter/utils';
61+
5962
// Configuration
6063
export * from './configuration';

0 commit comments

Comments
 (0)