From 269ea459a2eee158c4b566e4365194b49d71a8a4 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Tue, 24 Feb 2026 18:10:51 -0800 Subject: [PATCH 1/8] 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 --- .../src/utils/ScopeUtils.ts | 86 ++++++++++-- .../src/utils/TurnContextUtils.ts | 25 +--- pnpm-lock.yaml | 128 +++++++++--------- pnpm-workspace.yaml | 4 +- .../hosting/BaggageBuilderUtils.test.ts | 17 ++- .../hosting/TurnContextUtils.test.ts | 13 +- .../extension/hosting/scope-utils.test.ts | 34 +++-- 7 files changed, 185 insertions(+), 122 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts index 1f8531c3..1c37c69a 100644 --- a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts @@ -17,6 +17,7 @@ import { ToolCallDetails, ExecutionType } from '@microsoft/agents-a365-observability'; +import { Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime'; import { getExecutionTypePair, } from './TurnContextUtils'; @@ -44,26 +45,35 @@ export class ScopeUtils { * @returns Tenant details if a recipient tenant id is present; otherwise undefined. */ public static deriveTenantDetails(turnContext: TurnContext): TenantDetails | undefined { - const tenantId = turnContext?.activity?.recipient?.tenantId; + const tenantId = turnContext?.activity?.getAgenticTenantId(); return tenantId ? { tenantId } : undefined; } /** * Derive target agent details from the activity recipient. + * Uses {@link RuntimeUtility.ResolveAgentIdentity} to resolve the agent identity (Blueprint vs App) + * when an auth token is provided; otherwise falls back to recipient.agenticAppBlueprintId. * @param turnContext Activity context + * @param authToken Auth token for resolving agent identity from token claims. * @returns Agent details built from recipient properties; otherwise undefined. */ - public static deriveAgentDetails(turnContext: TurnContext): AgentDetails | undefined { + public static deriveAgentDetails(turnContext: TurnContext, authToken: string): AgentDetails | undefined; + /** @deprecated Provide `authToken` for proper blueprint ID resolution via ResolveAgentIdentity. */ + public static deriveAgentDetails(turnContext: TurnContext): AgentDetails | undefined; + public static deriveAgentDetails(turnContext: TurnContext, authToken?: string): AgentDetails | undefined { const recipient = turnContext?.activity?.recipient; if (!recipient) return undefined; + const agentBlueprintId = authToken + ? RuntimeUtility.ResolveAgentIdentity(turnContext, authToken) + : recipient.agenticAppBlueprintId; return { - agentId: recipient.agenticAppId, + agentId: turnContext?.activity?.getAgenticInstanceId(), agentName: recipient.name, agentAUID: recipient.aadObjectId, - agentBlueprintId: recipient.agenticAppBlueprintId, - agentUPN: recipient.agenticUserId, + agentBlueprintId, + agentUPN: turnContext?.activity?.getAgenticUser(), agentDescription: recipient.role, - tenantId: recipient.tenantId + tenantId: turnContext?.activity?.getAgenticTenantId() } as AgentDetails; } @@ -131,6 +141,7 @@ export class ScopeUtils { * Also records input messages from the context if present. * @param details The inference call details (model, provider, tokens, etc.). * @param turnContext The current activity context to derive scope parameters from. + * @param authToken Auth token for resolving agent identity from token claims. * @param startTime Optional explicit start time (ms epoch, Date, or HrTime). * @param endTime Optional explicit end time (ms epoch, Date, or HrTime). * @returns A started `InferenceScope` enriched with context-derived parameters. @@ -138,10 +149,25 @@ export class ScopeUtils { static populateInferenceScopeFromTurnContext( details: InferenceDetails, turnContext: TurnContext, + authToken: string, + startTime?: TimeInput, + endTime?: TimeInput + ): InferenceScope; + /** @deprecated Provide `authToken` for proper agent identity resolution. */ + static populateInferenceScopeFromTurnContext( + details: InferenceDetails, + turnContext: TurnContext, + ): InferenceScope; + static populateInferenceScopeFromTurnContext( + details: InferenceDetails, + turnContext: TurnContext, + authToken?: string, startTime?: TimeInput, endTime?: TimeInput ): InferenceScope { - const agent = ScopeUtils.deriveAgentDetails(turnContext); + const agent = authToken + ? ScopeUtils.deriveAgentDetails(turnContext, authToken) + : ScopeUtils.deriveAgentDetails(turnContext); const tenant = ScopeUtils.deriveTenantDetails(turnContext); const conversationId = ScopeUtils.deriveConversationId(turnContext); const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext); @@ -165,6 +191,7 @@ export class ScopeUtils { * Also sets execution type and input messages from the context if present. * @param details The invoke-agent call details to be augmented and used for the scope. * @param turnContext The current activity context to derive scope parameters from. + * @param authToken Auth token for resolving agent identity from token claims. * @param startTime Optional explicit start time (ms epoch, Date, or HrTime). * @param endTime Optional explicit end time (ms epoch, Date, or HrTime). * @returns A started `InvokeAgentScope` enriched with context-derived parameters. @@ -172,13 +199,28 @@ export class ScopeUtils { static populateInvokeAgentScopeFromTurnContext( details: InvokeAgentDetails, turnContext: TurnContext, + authToken: string, + startTime?: TimeInput, + endTime?: TimeInput + ): InvokeAgentScope; + /** @deprecated Provide `authToken` for proper agent identity resolution. */ + static populateInvokeAgentScopeFromTurnContext( + details: InvokeAgentDetails, + turnContext: TurnContext, + ): InvokeAgentScope; + static populateInvokeAgentScopeFromTurnContext( + details: InvokeAgentDetails, + turnContext: TurnContext, + authToken?: string, startTime?: TimeInput, endTime?: TimeInput ): InvokeAgentScope { const tenant = ScopeUtils.deriveTenantDetails(turnContext); const callerAgent = ScopeUtils.deriveCallerAgent(turnContext); const caller = ScopeUtils.deriveCallerDetails(turnContext); - const invokeAgentDetails = ScopeUtils.buildInvokeAgentDetails(details, turnContext); + const invokeAgentDetails = authToken + ? ScopeUtils.buildInvokeAgentDetails(details, turnContext, authToken) + : ScopeUtils.buildInvokeAgentDetails(details, turnContext); if (!tenant) { throw new Error('populateInvokeAgentScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); @@ -193,10 +235,16 @@ export class ScopeUtils { * Build InvokeAgentDetails by merging provided details with agent info, conversation id and source metadata from the TurnContext. * @param details Base invoke-agent details to augment * @param turnContext Activity context + * @param authToken Auth token for resolving agent identity from token claims. * @returns New InvokeAgentDetails suitable for starting an InvokeAgentScope. */ - public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext): InvokeAgentDetails { - const agent = ScopeUtils.deriveAgentDetails(turnContext); + public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails; + /** @deprecated Provide `authToken` for proper agent identity resolution. */ + public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext): InvokeAgentDetails; + public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken?: string): InvokeAgentDetails { + const agent = authToken + ? ScopeUtils.deriveAgentDetails(turnContext, authToken) + : ScopeUtils.deriveAgentDetails(turnContext); const srcMetaFromContext = ScopeUtils.deriveSourceMetadataObject(turnContext); const executionTypePair = getExecutionTypePair(turnContext); const baseRequest = details.request ?? {}; @@ -223,6 +271,7 @@ export class ScopeUtils { * Derives `agentDetails`, `tenantDetails`, `conversationId`, and `sourceMetadata` (channel name/link) from context. * @param details The tool call details (name, type, args, call id, etc.). * @param turnContext The current activity context to derive scope parameters from. + * @param authToken Auth token for resolving agent identity from token claims. * @param startTime Optional explicit start time (ms epoch, Date, or HrTime). Useful when recording a * tool call after execution has already completed. * @param endTime Optional explicit end time (ms epoch, Date, or HrTime). @@ -231,10 +280,25 @@ export class ScopeUtils { static populateExecuteToolScopeFromTurnContext( details: ToolCallDetails, turnContext: TurnContext, + authToken: string, + startTime?: TimeInput, + endTime?: TimeInput + ): ExecuteToolScope; + /** @deprecated Provide `authToken` for proper agent identity resolution. */ + static populateExecuteToolScopeFromTurnContext( + details: ToolCallDetails, + turnContext: TurnContext, + ): ExecuteToolScope; + static populateExecuteToolScopeFromTurnContext( + details: ToolCallDetails, + turnContext: TurnContext, + authToken?: string, startTime?: TimeInput, endTime?: TimeInput ): ExecuteToolScope { - const agent = ScopeUtils.deriveAgentDetails(turnContext); + const agent = authToken + ? ScopeUtils.deriveAgentDetails(turnContext, authToken) + : ScopeUtils.deriveAgentDetails(turnContext); const tenant = ScopeUtils.deriveTenantDetails(turnContext); const conversationId = ScopeUtils.deriveConversationId(turnContext); const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext); diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 981d4ead..3662adbc 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -70,11 +70,11 @@ export function getExecutionTypePair(turnContext: TurnContext): Array<[string, s * @returns Array of [key, value] pairs for agent identity and description */ export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[string, string]> { - if (!turnContext || !turnContext.activity?.recipient) { + if (!turnContext || !turnContext.activity?.recipient) { return []; } - const recipient = turnContext.activity.recipient; - const agentId = recipient.agenticAppId; + const recipient = turnContext.activity.recipient; + const agentId = turnContext.activity?.getAgenticInstanceId(); const agentName = recipient.name; const aadObjectId = recipient.aadObjectId; const agentDescription = recipient.role; @@ -93,24 +93,7 @@ export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[str * @returns Array of [key, value] for tenant ID */ export function getTenantIdPair(turnContext: TurnContext): Array<[string, string]> { - let tenantId = turnContext.activity?.recipient?.tenantId; - - - // If not found, try to extract from channelData. Accepts both object and JSON string. - if (!tenantId && turnContext.activity?.channelData) { - try { - let channelData: unknown = turnContext.activity.channelData; - if (typeof channelData === 'string') { - channelData = JSON.parse(channelData); - } - if ( - typeof channelData === 'object' && channelData !== null) { - tenantId = (channelData as { tenant: { id?: string } })?.tenant?.id; - } - } catch (_err) { - // ignore JSON parse errors - } - } + const tenantId = turnContext.activity?.getAgenticTenantId(); return tenantId ? [[OpenTelemetryConstants.TENANT_ID_KEY, tenantId]] : []; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23cccb37..7cf4f654 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,11 +31,11 @@ catalogs: specifier: ^1.1.1 version: 1.1.1 '@microsoft/agents-activity': - specifier: ^1.1.0-alpha.85 - version: 1.1.0-alpha.85 + specifier: ^1.3.1 + version: 1.3.1 '@microsoft/agents-hosting': - specifier: ^1.1.0-alpha.85 - version: 1.1.0-alpha.85 + specifier: ^1.3.1 + version: 1.3.1 '@modelcontextprotocol/sdk': specifier: ^1.25.2 version: 1.25.2 @@ -69,6 +69,9 @@ catalogs: '@opentelemetry/semantic-conventions': specifier: ^1.37.0 version: 1.38.0 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 '@types/jest': specifier: ^30.0.0 version: 30.0.0 @@ -173,10 +176,10 @@ importers: version: link:../agents-a365-runtime '@microsoft/agents-activity': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 devDependencies: '@eslint/js': specifier: 'catalog:' @@ -393,7 +396,7 @@ importers: version: link:../agents-a365-runtime '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 '@opentelemetry/api': specifier: 'catalog:' version: 1.9.0 @@ -439,7 +442,7 @@ importers: version: 4.13.0 '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 jsonwebtoken: specifier: 'catalog:' version: 9.0.3 @@ -488,7 +491,7 @@ importers: version: link:../agents-a365-runtime '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.1.13) @@ -546,7 +549,7 @@ importers: version: link:../agents-a365-tooling '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.1.13) @@ -604,7 +607,7 @@ importers: version: link:../agents-a365-tooling '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 hono: specifier: ^4.11.7 version: 4.11.7 @@ -662,7 +665,7 @@ importers: version: link:../agents-a365-tooling '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 '@openai/agents': specifier: 'catalog:' version: 0.4.2(@cfworker/json-schema@4.1.1)(hono@4.11.7)(ws@8.18.3)(zod@4.1.13) @@ -732,7 +735,7 @@ importers: version: link:../packages/agents-a365-runtime '@microsoft/agents-hosting': specifier: 'catalog:' - version: 1.1.0-alpha.85 + version: 1.3.1 '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@4.1.13) @@ -852,11 +855,11 @@ importers: specifier: workspace:* version: link:../../packages/agents-a365-tooling-extensions-langchain '@microsoft/agents-activity': - specifier: ^1.1.0-alpha.85 - version: 1.1.0-alpha.85 + specifier: ^1.3.1 + version: 1.3.1 '@microsoft/agents-hosting': - specifier: ^1.1.0-alpha.85 - version: 1.1.0-alpha.85 + specifier: ^1.3.1 + version: 1.3.1 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -889,8 +892,8 @@ importers: specifier: ^0.2.18 version: 0.2.23 '@types/express': - specifier: ^4.17.21 - version: 4.17.25 + specifier: 'catalog:' + version: 5.0.6 '@types/node': specifier: ^20.14.9 version: 20.19.25 @@ -953,10 +956,18 @@ packages: resolution: {integrity: sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==} engines: {node: '>=0.8.0'} + '@azure/msal-common@16.0.4': + resolution: {integrity: sha512-0KZ9/wbUyZN65JLAx5bGNfWjkD0kRMUgM99oSpZFg7wEOb3XcKIiHrFnIpgyc8zZ70fHodyh8JKEOel1oN24Gw==} + engines: {node: '>=0.8.0'} + '@azure/msal-node@3.8.3': resolution: {integrity: sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==} engines: {node: '>=16'} + '@azure/msal-node@5.0.4': + resolution: {integrity: sha512-WbA77m68noCw4qV+1tMm5nodll34JCDF0KmrSrp9LskS0bGbgHt98ZRxq69BQK5mjMqDD5ThHJOrrGSfzPybxw==} + engines: {node: '>=20'} + '@babel/cli@7.28.6': resolution: {integrity: sha512-6EUNcuBbNkj08Oj4gAZ+BUU8yLCgKzgVX4gaTh09Ya2C8ICM4P+G30g4m3akRxSYAp3A/gnWchrNst7px4/nUQ==} engines: {node: '>=6.9.0'} @@ -1944,12 +1955,12 @@ packages: peerDependencies: '@langchain/core': ^1.0.0 - '@microsoft/agents-activity@1.1.0-alpha.85': - resolution: {integrity: sha512-iMzeYFJZPSrXkhHpHesKQ1gjvCm6uyPlH0ojsNf8Z3EODCeiFsxHOahoLyzCuqzcWBYhNCcQ45aarNbeJ84HgA==} + '@microsoft/agents-activity@1.3.1': + resolution: {integrity: sha512-4k44NrfEqXiSg49ofj8geV8ylPocqDLtZKKt0PFL9BvFV0n57X3y1s/fEbsf7Fkl3+P/R2XLyMB5atEGf/eRGg==} engines: {node: '>=20.0.0'} - '@microsoft/agents-hosting@1.1.0-alpha.85': - resolution: {integrity: sha512-1Ii92EJSaQTuqjOBWUqoioClum+6Dh82uBmzoExMi3ZDJ9UTV6Kqg4W1h4PYhUZPmqIP3HLSVNvty84Gx/mqYg==} + '@microsoft/agents-hosting@1.3.1': + resolution: {integrity: sha512-570oJr93l1RcCNNaMVpOm+PgQkRgno/F65nH1aCWLIKLnw0o7iPoj+8Z5b7mnLMidg9lldVSCcf0dBxqTGE1/w==} engines: {node: '>=20.0.0'} '@microsoft/m365agentsplayground@0.2.23': @@ -2329,11 +2340,11 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@4.19.7': - resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -2356,9 +2367,6 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -2377,14 +2385,11 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -2651,8 +2656,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.13.2: - resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} babel-jest@30.2.0: resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} @@ -3628,8 +3633,8 @@ packages: jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jwks-rsa@3.2.0: - resolution: {integrity: sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==} + jwks-rsa@3.2.2: + resolution: {integrity: sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==} engines: {node: '>=14'} jws@4.0.1: @@ -4718,12 +4723,20 @@ snapshots: '@azure/msal-common@15.13.2': {} + '@azure/msal-common@16.0.4': {} + '@azure/msal-node@3.8.3': dependencies: '@azure/msal-common': 15.13.2 jsonwebtoken: 9.0.3 uuid: 10.0.0 + '@azure/msal-node@5.0.4': + dependencies: + '@azure/msal-common': 16.0.4 + jsonwebtoken: 9.0.3 + uuid: 10.0.0 + '@babel/cli@7.28.6(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -6034,7 +6047,7 @@ snapshots: transitivePeerDependencies: - ws - '@microsoft/agents-activity@1.1.0-alpha.85': + '@microsoft/agents-activity@1.3.1': dependencies: debug: 4.4.3(supports-color@5.5.0) uuid: 10.0.0 @@ -6042,15 +6055,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@microsoft/agents-hosting@1.1.0-alpha.85': + '@microsoft/agents-hosting@1.3.1': dependencies: '@azure/core-auth': 1.10.1 - '@azure/msal-node': 3.8.3 - '@microsoft/agents-activity': 1.1.0-alpha.85 - axios: 1.13.2 + '@azure/msal-node': 5.0.4 + '@microsoft/agents-activity': 1.3.1 + axios: 1.13.5 jsonwebtoken: 9.0.3 - jwks-rsa: 3.2.0 + jwks-rsa: 3.2.2 object-path: 0.11.8 + zod: 4.1.13 transitivePeerDependencies: - debug - supports-color @@ -6553,19 +6567,18 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@4.19.7': + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 20.19.25 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 - '@types/express@4.17.25': + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.7 - '@types/qs': 6.14.0 - '@types/serve-static': 1.15.10 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 '@types/http-errors@2.0.5': {} @@ -6591,8 +6604,6 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 20.19.25 - '@types/mime@1.3.5': {} - '@types/ms@2.1.0': {} '@types/node-fetch@2.6.13': @@ -6612,20 +6623,14 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.19.25 - '@types/send@1.2.1': dependencies: '@types/node': 20.19.25 - '@types/serve-static@1.15.10': + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 '@types/node': 20.19.25 - '@types/send': 0.17.6 '@types/stack-utils@2.0.3': {} @@ -6881,7 +6886,7 @@ snapshots: asynckit@0.4.0: {} - axios@1.13.2: + axios@1.13.5: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 @@ -8085,9 +8090,8 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jwks-rsa@3.2.0: + jwks-rsa@3.2.2: dependencies: - '@types/express': 4.17.25 '@types/jsonwebtoken': 9.0.10 debug: 4.4.3(supports-color@5.5.0) jose: 4.15.9 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 223985a7..a24be527 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,8 +24,8 @@ catalog: "@langchain/mcp-adapters": "^1.1.1" # Microsoft 365 Agents SDK packages - "@microsoft/agents-hosting": "^1.1.0-alpha.85" - "@microsoft/agents-activity": "^1.1.0-alpha.85" + "@microsoft/agents-hosting": "^1.3.1" + "@microsoft/agents-activity": "^1.3.1" # Hono - required peer dependency for MCP SDK and OpenAI agents "hono": "^4.11.7" diff --git a/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts b/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts index f6d65821..3aaf516b 100644 --- a/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts +++ b/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts @@ -9,9 +9,12 @@ import { BaggageBuilder, OpenTelemetryConstants } from '@microsoft/agents-a365-o describe('BaggageBuilderUtils', () => { const mockTurnContext = { activity: { - from: { id: 'user1', name: 'User One', agenticUserId: 'agentic-user-1', tenantId: 'tenant1', aadObjectId: 'aad-object-1', agenticAppBlueprintId: 'blueprint-123', role: 'user' }, - recipient: { id: 'agent1', name: 'Agent One', agenticAppId: 'agent-app-1', agenticUserId: 'agentic-agent-1', tenantId: 'tenant1', aadObjectId: 'aad-object-2', role: 'agent' }, - channelData: {}, + from: { id: 'user1', name: 'User One', agenticUserId: 'agentic-user-1', tenantId: 'tenant1', role: 'user' }, + recipient: { id: 'agent1', name: 'Agent One', agenticAppId: 'agent-app-1', agenticUserId: 'agentic-agent-1', tenantId: 'tenant1', role: 'agent' }, + conversation: { id: 'conv-1', tenantId: 'tenant1' }, + getAgenticInstanceId: () => 'agent-app-1', + getAgenticUser: () => 'agentic-agent-1', + getAgenticTenantId: () => 'tenant1', }, } as any; @@ -41,16 +44,16 @@ describe('BaggageBuilderUtils', () => { expect(result).toBe(builder); // Validate every expected OpenTelemetry baggage key and value const asObj = Object.fromEntries(capturedPairs); - expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY]).toBe('aad-object-1'); + expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY]).toBeUndefined(); expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_NAME_KEY]).toBe('User One'); expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_UPN_KEY]).toBe('agentic-user-1'); expect(asObj[OpenTelemetryConstants.GEN_AI_CALLER_TENANT_ID_KEY]).toBe('tenant1'); expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe('agent-app-1'); expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY]).toBe('Agent One'); - expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY]).toBe('aad-object-2'); + expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY]).toBeUndefined(); expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_DESCRIPTION_KEY]).toBe('agent'); - expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY]).toBe('blueprint-123'); - expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_UPN_KEY]).toBe(undefined); + expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY]).toBeUndefined(); + expect(asObj[OpenTelemetryConstants.GEN_AI_AGENT_UPN_KEY]).toBeUndefined(); expect(asObj[OpenTelemetryConstants.TENANT_ID_KEY]).toBe('tenant1'); }); diff --git a/tests/observability/extension/hosting/TurnContextUtils.test.ts b/tests/observability/extension/hosting/TurnContextUtils.test.ts index f9dfcc9f..04806c26 100644 --- a/tests/observability/extension/hosting/TurnContextUtils.test.ts +++ b/tests/observability/extension/hosting/TurnContextUtils.test.ts @@ -16,10 +16,13 @@ import { OpenTelemetryConstants, ExecutionType } from '@microsoft/agents-a365-ob describe('TurnContextUtils', () => { const mockTurnContext = { activity: { - from: { id: 'user1', name: 'User One', agenticUserId: 'agentic-user-1', tenantId: 'tenant1', role: 'agenticUser', agenticAppBlueprintId: 'blueprint-123' }, - recipient: { id: 'agent1', name: 'Agent One', agenticAppId: 'agent-app-1', agenticUserId: 'agentic-agent-1', tenantId: 'tenant1', role: 'agenticUser', aadObjectId: 'aad-object-1' }, - channelData: {}, + from: { id: 'user1', name: 'User One', agenticUserId: 'agentic-user-1', tenantId: 'tenant1', role: 'agenticUser' }, + recipient: { id: 'agent1', name: 'Agent One', agenticAppId: 'agent-app-1', agenticUserId: 'agentic-agent-1', tenantId: 'tenant1', role: 'agenticUser' }, + conversation: { id: 'conv-1', tenantId: 'tenant1' }, text: 'Hello world', + getAgenticInstanceId: () => 'agent-app-1', + getAgenticUser: () => 'agentic-agent-1', + getAgenticTenantId: () => 'tenant1', }, } as any; @@ -45,8 +48,8 @@ describe('TurnContextUtils', () => { const obj = Object.fromEntries(pairs); expect(obj[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe('agent-app-1'); expect(obj[OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY]).toBe('Agent One'); - expect(obj[OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY]).toBe('aad-object-1'); - expect(obj[OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY]).toBe(undefined); + expect(obj[OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY]).toBeUndefined(); + expect(obj[OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY]).toBeUndefined(); }); it('should get tenant id pair', () => { diff --git a/tests/observability/extension/hosting/scope-utils.test.ts b/tests/observability/extension/hosting/scope-utils.test.ts index a5ad82f7..00e14be6 100644 --- a/tests/observability/extension/hosting/scope-utils.test.ts +++ b/tests/observability/extension/hosting/scope-utils.test.ts @@ -20,7 +20,10 @@ function makeTurnContext( text: text ?? 'hello world', channelId: channelName ?? 'web', channelIdSubChannel: channelLink ?? 'https://example/channel', - conversation: { id: conversationId ?? 'conv-001' } + conversation: { id: conversationId ?? 'conv-001', tenantId: 'tenant-123' }, + getAgenticInstanceId: () => 'agent-1', + getAgenticUser: () => 'agent-upn@contoso.com', + getAgenticTenantId: () => 'tenant-123', } }; @@ -35,9 +38,9 @@ function makeTurnContext( }; base.activity.recipient = { agenticAppId: 'agent-1', + agenticAppBlueprintId: 'agent-blueprint-1', name: 'Agent One', aadObjectId: 'agent-oid', - agenticAppBlueprintId: 'agent-blueprint-1', agenticUserId: 'agent-upn@contoso.com', role: 'assistant', tenantId: 'tenant-123' @@ -83,35 +86,35 @@ describe('ScopeUtils.populateFromTurnContext', () => { describe('error conditions', () => { test('populateInferenceScopeFromTurnContext throws when agent details are missing', () => { const details: any = { operationName: 'inference', model: 'm', providerName: 'prov' }; - const ctx = makeCtx({ activity: { /* no recipient */ } as any }); + const ctx = makeCtx({ activity: { /* no recipient */ getAgenticTenantId: () => 't1' } as any }); expect(() => ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx)) .toThrow('populateInferenceScopeFromTurnContext: Missing agent details on TurnContext (recipient)'); }); test('populateInferenceScopeFromTurnContext throws when tenant details are missing', () => { const details: any = { operationName: 'inference', model: 'm', providerName: 'prov' }; - const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' } } as any }); // agent ok, no tenantId + const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // agent ok, no tenantId expect(() => ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx)) .toThrow('populateInferenceScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); }); test('populateExecuteToolScopeFromTurnContext throws when agent details are missing', () => { const details: any = { toolName: 'tool' }; - const ctx = makeCtx({ activity: { /* no recipient */ } as any }); + const ctx = makeCtx({ activity: { /* no recipient */ getAgenticTenantId: () => 't1' } as any }); expect(() => ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx)) .toThrow('populateExecuteToolScopeFromTurnContext: Missing agent details on TurnContext (recipient)'); }); test('populateExecuteToolScopeFromTurnContext throws when tenant details are missing', () => { const details: any = { toolName: 'tool' }; - const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' } } as any }); // agent ok, no tenantId + const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // agent ok, no tenantId expect(() => ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx)) .toThrow('populateExecuteToolScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); }); test('populateInvokeAgentScopeFromTurnContext throws when tenant details are missing', () => { const details: InvokeAgentDetails = { agentId: 'aid' } as any; - const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' } } as any }); // no tenantId + const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // no tenantId expect(() => ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, ctx)) .toThrow('populateInvokeAgentScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); }); @@ -175,23 +178,23 @@ function makeCtx(partial: Partial): TurnContext { return partial as unknown as TurnContext; } -test('deriveTenantDetails prefers recipient.tenantId', () => { - const ctx = makeCtx({ activity: { recipient: { tenantId: 't-rec' }, from: { tenantId: 't-from' } } as any }); +test('deriveTenantDetails returns tenantId from getAgenticTenantId()', () => { + const ctx = makeCtx({ activity: { getAgenticTenantId: () => 't-rec' } as any }); expect(ScopeUtils.deriveTenantDetails(ctx)).toEqual({ tenantId: 't-rec' }); }); -test('deriveTenantDetails returns undefined when only from.tenantId is present', () => { - const ctx = makeCtx({ activity: { from: { tenantId: 't-from' } } as any }); +test('deriveTenantDetails returns undefined when getAgenticTenantId() returns undefined', () => { + const ctx = makeCtx({ activity: { getAgenticTenantId: () => undefined } as any }); expect(ScopeUtils.deriveTenantDetails(ctx)).toBeUndefined(); }); test('deriveAgentDetails maps recipient fields to AgentDetails', () => { - const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid', name: 'A', aadObjectId: 'auid', agenticAppBlueprintId: 'bp1', agenticUserId: 'upn1', role: 'bot', tenantId: 't1' } } as any }); + const ctx = makeCtx({ activity: { recipient: { name: 'A', aadObjectId: 'auid', role: 'bot' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => 'upn1', getAgenticTenantId: () => 't1' } as any }); expect(ScopeUtils.deriveAgentDetails(ctx)).toEqual({ agentId: 'aid', agentName: 'A', agentAUID: 'auid', - agentBlueprintId: 'bp1', + agentBlueprintId: undefined, agentUPN: 'upn1', agentDescription: 'bot', tenantId: 't1', @@ -263,10 +266,13 @@ test('buildInvokeAgentDetails merges agent (recipient), conversationId, sourceMe }; const ctx = makeCtx({ activity: { - recipient: { agenticAppId: 'rec-agent', name: 'Rec', aadObjectId: 'auid', role: 'bot', tenantId: 'tX' }, + recipient: { name: 'Rec', role: 'bot' }, conversation: { id: 'c-2' }, channelId: 'web', channelIdSubChannel: 'inbox', + getAgenticInstanceId: () => 'rec-agent', + getAgenticUser: () => undefined, + getAgenticTenantId: () => 'tX', } as any }); From 2b13c9e1d853ba77aa0cb207bde26f2b1a710cd8 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Tue, 24 Feb 2026 18:46:05 -0800 Subject: [PATCH 2/8] 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 --- .../src/utils/ScopeUtils.ts | 28 +++++++++---------- .../src/utils/TurnContextUtils.ts | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts index 1c37c69a..9cefaa97 100644 --- a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts @@ -60,7 +60,11 @@ export class ScopeUtils { public static deriveAgentDetails(turnContext: TurnContext, authToken: string): AgentDetails | undefined; /** @deprecated Provide `authToken` for proper blueprint ID resolution via ResolveAgentIdentity. */ public static deriveAgentDetails(turnContext: TurnContext): AgentDetails | undefined; - public static deriveAgentDetails(turnContext: TurnContext, authToken?: string): AgentDetails | undefined { + static deriveAgentDetails(turnContext: TurnContext, authToken?: string): AgentDetails | undefined { + return ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); + } + + private static deriveAgentDetailsCore(turnContext: TurnContext, authToken?: string): AgentDetails | undefined { const recipient = turnContext?.activity?.recipient; if (!recipient) return undefined; const agentBlueprintId = authToken @@ -165,9 +169,7 @@ export class ScopeUtils { startTime?: TimeInput, endTime?: TimeInput ): InferenceScope { - const agent = authToken - ? ScopeUtils.deriveAgentDetails(turnContext, authToken) - : ScopeUtils.deriveAgentDetails(turnContext); + const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); const tenant = ScopeUtils.deriveTenantDetails(turnContext); const conversationId = ScopeUtils.deriveConversationId(turnContext); const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext); @@ -218,9 +220,7 @@ export class ScopeUtils { const tenant = ScopeUtils.deriveTenantDetails(turnContext); const callerAgent = ScopeUtils.deriveCallerAgent(turnContext); const caller = ScopeUtils.deriveCallerDetails(turnContext); - const invokeAgentDetails = authToken - ? ScopeUtils.buildInvokeAgentDetails(details, turnContext, authToken) - : ScopeUtils.buildInvokeAgentDetails(details, turnContext); + const invokeAgentDetails = ScopeUtils.buildInvokeAgentDetailsCore(details, turnContext, authToken); if (!tenant) { throw new Error('populateInvokeAgentScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); @@ -241,10 +241,12 @@ export class ScopeUtils { public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails; /** @deprecated Provide `authToken` for proper agent identity resolution. */ public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext): InvokeAgentDetails; - public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken?: string): InvokeAgentDetails { - const agent = authToken - ? ScopeUtils.deriveAgentDetails(turnContext, authToken) - : ScopeUtils.deriveAgentDetails(turnContext); + static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken?: string): InvokeAgentDetails { + return ScopeUtils.buildInvokeAgentDetailsCore(details, turnContext, authToken); + } + + private static buildInvokeAgentDetailsCore(details: InvokeAgentDetails, turnContext: TurnContext, authToken?: string): InvokeAgentDetails { + const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); const srcMetaFromContext = ScopeUtils.deriveSourceMetadataObject(turnContext); const executionTypePair = getExecutionTypePair(turnContext); const baseRequest = details.request ?? {}; @@ -296,9 +298,7 @@ export class ScopeUtils { startTime?: TimeInput, endTime?: TimeInput ): ExecuteToolScope { - const agent = authToken - ? ScopeUtils.deriveAgentDetails(turnContext, authToken) - : ScopeUtils.deriveAgentDetails(turnContext); + const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); const tenant = ScopeUtils.deriveTenantDetails(turnContext); const conversationId = ScopeUtils.deriveConversationId(turnContext); const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext); diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 3662adbc..3c1ac425 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -88,7 +88,7 @@ export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[str } /** - * Extracts the tenant ID baggage key-value pair, attempting to retrieve from ChannelData if necessary. + * Extracts the tenant ID baggage key-value pair using the Activity's getAgenticTenantId() helper. * @param turnContext The current TurnContext (activity context) * @returns Array of [key, value] for tenant ID */ From 0f93ad097682221ca79112a57419e55d7129742d Mon Sep 17 00:00:00 2001 From: jsl517 Date: Thu, 26 Feb 2026 17:57:00 -0800 Subject: [PATCH 3/8] comments --- .../src/utils/ScopeUtils.ts | 64 ++++--------------- 1 file changed, 12 insertions(+), 52 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts index 9cefaa97..50624f73 100644 --- a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts @@ -51,27 +51,26 @@ export class ScopeUtils { /** * Derive target agent details from the activity recipient. - * Uses {@link RuntimeUtility.ResolveAgentIdentity} to resolve the agent identity (Blueprint vs App) - * when an auth token is provided; otherwise falls back to recipient.agenticAppBlueprintId. + * Uses {@link RuntimeUtility.ResolveAgentIdentity} to resolve the agent ID (instance ID for blueprint agents, + * appid/azp from token for app-based agents). For blueprint agents, the blueprint ID is resolved + * from the token's xms_par_app_azp claim via {@link RuntimeUtility.getAgentIdFromToken}. * @param turnContext Activity context * @param authToken Auth token for resolving agent identity from token claims. * @returns Agent details built from recipient properties; otherwise undefined. */ - public static deriveAgentDetails(turnContext: TurnContext, authToken: string): AgentDetails | undefined; - /** @deprecated Provide `authToken` for proper blueprint ID resolution via ResolveAgentIdentity. */ - public static deriveAgentDetails(turnContext: TurnContext): AgentDetails | undefined; - static deriveAgentDetails(turnContext: TurnContext, authToken?: string): AgentDetails | undefined { + public static deriveAgentDetails(turnContext: TurnContext, authToken: string): AgentDetails | undefined { return ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); } - private static deriveAgentDetailsCore(turnContext: TurnContext, authToken?: string): AgentDetails | undefined { + private static deriveAgentDetailsCore(turnContext: TurnContext, authToken: string): AgentDetails | undefined { const recipient = turnContext?.activity?.recipient; if (!recipient) return undefined; - const agentBlueprintId = authToken - ? RuntimeUtility.ResolveAgentIdentity(turnContext, authToken) - : recipient.agenticAppBlueprintId; + const agentId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken); + const agentBlueprintId = turnContext?.activity?.isAgenticRequest() + ? RuntimeUtility.getAgentIdFromToken(authToken) + : undefined; return { - agentId: turnContext?.activity?.getAgenticInstanceId(), + agentId, agentName: recipient.name, agentAUID: recipient.aadObjectId, agentBlueprintId, @@ -156,18 +155,6 @@ export class ScopeUtils { authToken: string, startTime?: TimeInput, endTime?: TimeInput - ): InferenceScope; - /** @deprecated Provide `authToken` for proper agent identity resolution. */ - static populateInferenceScopeFromTurnContext( - details: InferenceDetails, - turnContext: TurnContext, - ): InferenceScope; - static populateInferenceScopeFromTurnContext( - details: InferenceDetails, - turnContext: TurnContext, - authToken?: string, - startTime?: TimeInput, - endTime?: TimeInput ): InferenceScope { const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); const tenant = ScopeUtils.deriveTenantDetails(turnContext); @@ -204,18 +191,6 @@ export class ScopeUtils { authToken: string, startTime?: TimeInput, endTime?: TimeInput - ): InvokeAgentScope; - /** @deprecated Provide `authToken` for proper agent identity resolution. */ - static populateInvokeAgentScopeFromTurnContext( - details: InvokeAgentDetails, - turnContext: TurnContext, - ): InvokeAgentScope; - static populateInvokeAgentScopeFromTurnContext( - details: InvokeAgentDetails, - turnContext: TurnContext, - authToken?: string, - startTime?: TimeInput, - endTime?: TimeInput ): InvokeAgentScope { const tenant = ScopeUtils.deriveTenantDetails(turnContext); const callerAgent = ScopeUtils.deriveCallerAgent(turnContext); @@ -238,14 +213,11 @@ export class ScopeUtils { * @param authToken Auth token for resolving agent identity from token claims. * @returns New InvokeAgentDetails suitable for starting an InvokeAgentScope. */ - public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails; - /** @deprecated Provide `authToken` for proper agent identity resolution. */ - public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext): InvokeAgentDetails; - static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken?: string): InvokeAgentDetails { + public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails { return ScopeUtils.buildInvokeAgentDetailsCore(details, turnContext, authToken); } - private static buildInvokeAgentDetailsCore(details: InvokeAgentDetails, turnContext: TurnContext, authToken?: string): InvokeAgentDetails { + private static buildInvokeAgentDetailsCore(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails { const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); const srcMetaFromContext = ScopeUtils.deriveSourceMetadataObject(turnContext); const executionTypePair = getExecutionTypePair(turnContext); @@ -285,18 +257,6 @@ export class ScopeUtils { authToken: string, startTime?: TimeInput, endTime?: TimeInput - ): ExecuteToolScope; - /** @deprecated Provide `authToken` for proper agent identity resolution. */ - static populateExecuteToolScopeFromTurnContext( - details: ToolCallDetails, - turnContext: TurnContext, - ): ExecuteToolScope; - static populateExecuteToolScopeFromTurnContext( - details: ToolCallDetails, - turnContext: TurnContext, - authToken?: string, - startTime?: TimeInput, - endTime?: TimeInput ): ExecuteToolScope { const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); const tenant = ScopeUtils.deriveTenantDetails(turnContext); From cdde6f4abe347f585f1cec993883bd7bd393ebf9 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Thu, 26 Feb 2026 18:16:53 -0800 Subject: [PATCH 4/8] 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 --- .../src/utils/ScopeUtils.ts | 10 +--- .../extension/hosting/scope-utils.test.ts | 57 ++++++++++++------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts index 50624f73..9e18e28c 100644 --- a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts @@ -59,10 +59,6 @@ export class ScopeUtils { * @returns Agent details built from recipient properties; otherwise undefined. */ public static deriveAgentDetails(turnContext: TurnContext, authToken: string): AgentDetails | undefined { - return ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); - } - - private static deriveAgentDetailsCore(turnContext: TurnContext, authToken: string): AgentDetails | undefined { const recipient = turnContext?.activity?.recipient; if (!recipient) return undefined; const agentId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken); @@ -156,7 +152,7 @@ export class ScopeUtils { startTime?: TimeInput, endTime?: TimeInput ): InferenceScope { - const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); + const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken); const tenant = ScopeUtils.deriveTenantDetails(turnContext); const conversationId = ScopeUtils.deriveConversationId(turnContext); const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext); @@ -218,7 +214,7 @@ export class ScopeUtils { } private static buildInvokeAgentDetailsCore(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails { - const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); + const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken); const srcMetaFromContext = ScopeUtils.deriveSourceMetadataObject(turnContext); const executionTypePair = getExecutionTypePair(turnContext); const baseRequest = details.request ?? {}; @@ -258,7 +254,7 @@ export class ScopeUtils { startTime?: TimeInput, endTime?: TimeInput ): ExecuteToolScope { - const agent = ScopeUtils.deriveAgentDetailsCore(turnContext, authToken); + const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken); const tenant = ScopeUtils.deriveTenantDetails(turnContext); const conversationId = ScopeUtils.deriveConversationId(turnContext); const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext); diff --git a/tests/observability/extension/hosting/scope-utils.test.ts b/tests/observability/extension/hosting/scope-utils.test.ts index 00e14be6..c0442a1b 100644 --- a/tests/observability/extension/hosting/scope-utils.test.ts +++ b/tests/observability/extension/hosting/scope-utils.test.ts @@ -3,11 +3,28 @@ // Licensed under the MIT License. // ------------------------------------------------------------------------------ +// Mock RuntimeUtility methods so tests don't depend on real JWT parsing +jest.mock('@microsoft/agents-a365-runtime', () => { + const actual = jest.requireActual('@microsoft/agents-a365-runtime'); + return { + ...actual, + Utility: { + ...actual.Utility, + ResolveAgentIdentity: (context: any, _authToken: string) => + context.activity?.isAgenticRequest?.() + ? context.activity?.getAgenticInstanceId?.() || '' + : 'test-app-id', + getAgentIdFromToken: () => 'test-blueprint-id', + }, + }; +}); + import { ScopeUtils } from '../../../../packages/agents-a365-observability-hosting/src/utils/ScopeUtils'; import { InferenceScope, InvokeAgentScope, ExecuteToolScope, OpenTelemetryConstants, ExecutionType, OpenTelemetryScope, InvokeAgentDetails } from '@microsoft/agents-a365-observability'; import { RoleTypes } from '@microsoft/agents-activity'; import type { TurnContext } from '@microsoft/agents-hosting'; +const testAuthToken = 'mock-auth-token'; function makeTurnContext( text?: string, @@ -21,6 +38,7 @@ function makeTurnContext( channelId: channelName ?? 'web', channelIdSubChannel: channelLink ?? 'https://example/channel', conversation: { id: conversationId ?? 'conv-001', tenantId: 'tenant-123' }, + isAgenticRequest: () => true, getAgenticInstanceId: () => 'agent-1', getAgenticUser: () => 'agent-upn@contoso.com', getAgenticTenantId: () => 'tenant-123', @@ -61,7 +79,7 @@ describe('ScopeUtils.populateFromTurnContext', () => { test('build InferenceScope based on turn context', () => { const details = { operationName: 'inference', model: 'gpt-4o', providerName: 'openai' } as any; const ctx = makeTurnContext('input text', 'web', 'https://web', 'conv-A'); - const scope = ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx) as InferenceScope; + const scope = ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx, testAuthToken) as InferenceScope; expect(scope).toBeInstanceOf(InferenceScope); const calls = spy.mock.calls.map(args => [args[0], args[1]]); expect(calls).toEqual( @@ -72,7 +90,7 @@ describe('ScopeUtils.populateFromTurnContext', () => { [OpenTelemetryConstants.GEN_AI_AGENT_NAME_KEY, 'Agent One'], [OpenTelemetryConstants.GEN_AI_AGENT_AUID_KEY, 'agent-oid'], [OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY, 'agent-1'], - [OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, 'agent-blueprint-1'], + [OpenTelemetryConstants.GEN_AI_AGENT_BLUEPRINT_ID_KEY, 'test-blueprint-id'], [OpenTelemetryConstants.GEN_AI_AGENT_UPN_KEY, 'agent-upn@contoso.com'], [OpenTelemetryConstants.GEN_AI_AGENT_DESCRIPTION_KEY, 'assistant'], [OpenTelemetryConstants.TENANT_ID_KEY, 'tenant-123'], @@ -87,35 +105,35 @@ describe('ScopeUtils.populateFromTurnContext', () => { test('populateInferenceScopeFromTurnContext throws when agent details are missing', () => { const details: any = { operationName: 'inference', model: 'm', providerName: 'prov' }; const ctx = makeCtx({ activity: { /* no recipient */ getAgenticTenantId: () => 't1' } as any }); - expect(() => ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx)) + expect(() => ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx, testAuthToken)) .toThrow('populateInferenceScopeFromTurnContext: Missing agent details on TurnContext (recipient)'); }); test('populateInferenceScopeFromTurnContext throws when tenant details are missing', () => { const details: any = { operationName: 'inference', model: 'm', providerName: 'prov' }; - const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // agent ok, no tenantId - expect(() => ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx)) + const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, isAgenticRequest: () => false, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // agent ok, no tenantId + expect(() => ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx, testAuthToken)) .toThrow('populateInferenceScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); }); test('populateExecuteToolScopeFromTurnContext throws when agent details are missing', () => { const details: any = { toolName: 'tool' }; const ctx = makeCtx({ activity: { /* no recipient */ getAgenticTenantId: () => 't1' } as any }); - expect(() => ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx)) + expect(() => ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx, testAuthToken)) .toThrow('populateExecuteToolScopeFromTurnContext: Missing agent details on TurnContext (recipient)'); }); test('populateExecuteToolScopeFromTurnContext throws when tenant details are missing', () => { const details: any = { toolName: 'tool' }; - const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // agent ok, no tenantId - expect(() => ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx)) + const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, isAgenticRequest: () => false, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // agent ok, no tenantId + expect(() => ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx, testAuthToken)) .toThrow('populateExecuteToolScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); }); test('populateInvokeAgentScopeFromTurnContext throws when tenant details are missing', () => { const details: InvokeAgentDetails = { agentId: 'aid' } as any; - const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // no tenantId - expect(() => ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, ctx)) + const ctx = makeCtx({ activity: { recipient: { agenticAppId: 'aid' }, isAgenticRequest: () => false, getAgenticInstanceId: () => 'aid', getAgenticUser: () => undefined, getAgenticTenantId: () => undefined } as any }); // no tenantId + expect(() => ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, ctx, testAuthToken)) .toThrow('populateInvokeAgentScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); }); }); @@ -124,7 +142,7 @@ describe('ScopeUtils.populateFromTurnContext', () => { const details = { operationName: 'invoke', model: 'n/a', providerName: 'internal' } as any; const ctx = makeTurnContext('invoke message', 'teams', 'https://teams', 'conv-B'); ctx.activity.from!.role = RoleTypes.AgenticUser; - const scope = ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, ctx) as InvokeAgentScope; + const scope = ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, ctx, testAuthToken) as InvokeAgentScope; expect(scope).toBeInstanceOf(InvokeAgentScope); const calls = spy.mock.calls.map(args => [args[0], args[1]]); expect(calls).toEqual( @@ -154,7 +172,7 @@ describe('ScopeUtils.populateFromTurnContext', () => { test('build ExecuteToolScope based on turn context', () => { const details = { toolName: 'search', arguments: '{}' } as any; const ctx = makeTurnContext(undefined, 'cli', 'https://cli', 'conv-C'); - const scope = ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx) as ExecuteToolScope; + const scope = ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx, testAuthToken) as ExecuteToolScope; expect(scope).toBeInstanceOf(ExecuteToolScope); const calls = spy.mock.calls.map(args => [args[0], args[1]]); expect(calls).toEqual( @@ -189,9 +207,9 @@ test('deriveTenantDetails returns undefined when getAgenticTenantId() returns un }); test('deriveAgentDetails maps recipient fields to AgentDetails', () => { - const ctx = makeCtx({ activity: { recipient: { name: 'A', aadObjectId: 'auid', role: 'bot' }, getAgenticInstanceId: () => 'aid', getAgenticUser: () => 'upn1', getAgenticTenantId: () => 't1' } as any }); - expect(ScopeUtils.deriveAgentDetails(ctx)).toEqual({ - agentId: 'aid', + const ctx = makeCtx({ activity: { recipient: { name: 'A', aadObjectId: 'auid', role: 'bot' }, isAgenticRequest: () => false, getAgenticInstanceId: () => 'aid', getAgenticUser: () => 'upn1', getAgenticTenantId: () => 't1' } as any }); + expect(ScopeUtils.deriveAgentDetails(ctx, testAuthToken)).toEqual({ + agentId: 'test-app-id', agentName: 'A', agentAUID: 'auid', agentBlueprintId: undefined, @@ -203,7 +221,7 @@ test('deriveAgentDetails maps recipient fields to AgentDetails', () => { test('deriveAgentDetails returns undefined without recipient', () => { const ctx = makeCtx({ activity: {} as any }); - expect(ScopeUtils.deriveAgentDetails(ctx)).toBeUndefined(); + expect(ScopeUtils.deriveAgentDetails(ctx, testAuthToken)).toBeUndefined(); }); test('deriveCallerAgent maps from fields to caller AgentDetails', () => { @@ -270,14 +288,15 @@ test('buildInvokeAgentDetails merges agent (recipient), conversationId, sourceMe conversation: { id: 'c-2' }, channelId: 'web', channelIdSubChannel: 'inbox', + isAgenticRequest: () => false, getAgenticInstanceId: () => 'rec-agent', getAgenticUser: () => undefined, getAgenticTenantId: () => 'tX', } as any }); - const result = ScopeUtils.buildInvokeAgentDetails(invokeAgentDetails, ctx); - expect(result.agentId).toBe('rec-agent'); + const result = ScopeUtils.buildInvokeAgentDetails(invokeAgentDetails, ctx, testAuthToken); + expect(result.agentId).toBe('test-app-id'); expect(result.conversationId).toBe('c-2'); expect(result.request?.sourceMetadata).toEqual({ id: 'orig-id', name: 'web', description: 'inbox' }); }); @@ -288,7 +307,7 @@ test('buildInvokeAgentDetails keeps base request when TurnContext has no overrid request: { content: 'hi', executionType: ExecutionType.HumanToAgent, sourceMetadata: { description: 'keep', name: 'keep-name' }}, }; const ctx = makeCtx({ activity: {} as any }); - const result = ScopeUtils.buildInvokeAgentDetails(invokeAgentDetails, ctx); + const result = ScopeUtils.buildInvokeAgentDetails(invokeAgentDetails, ctx, testAuthToken); expect(result.agentId).toBe('base-agent'); expect(result.conversationId).toBeUndefined(); expect(result.request?.sourceMetadata).toEqual({ description: 'keep', name: 'keep-name' }); From 17760da90da3e8f97bdddee3726a164ff9ef71c8 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Fri, 27 Feb 2026 14:27:34 -0800 Subject: [PATCH 5/8] Update CHANGELOG for ScopeUtils breaking changes and Activity helper adoption Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 334eadff..7488c42b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to the Agent365 TypeScript SDK will be documented in this fi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Breaking Changes (`@microsoft/agents-a365-observability-hosting`) + +- **`ScopeUtils.deriveAgentDetails(turnContext, authToken)`** — New required `authToken: string` parameter. +- **`ScopeUtils.populateInferenceScopeFromTurnContext(details, turnContext, authToken, ...)`** — New required `authToken: string` parameter. +- **`ScopeUtils.populateInvokeAgentScopeFromTurnContext(details, turnContext, authToken, ...)`** — New required `authToken: string` parameter. +- **`ScopeUtils.populateExecuteToolScopeFromTurnContext(details, turnContext, authToken, ...)`** — New required `authToken: string` parameter. +- **`ScopeUtils.buildInvokeAgentDetails(details, turnContext, authToken)`** — New required `authToken: string` parameter. + +### Changed (`@microsoft/agents-a365-observability-hosting`) + +- `ScopeUtils.deriveAgentDetails` now resolves `agentId` via `RuntimeUtility.ResolveAgentIdentity()` (works for both app-based and blueprint-based agents) instead of reading `recipient.agenticAppId` directly. +- `ScopeUtils.deriveAgentDetails` now resolves `agentBlueprintId` from the JWT `xms_par_app_azp` claim via `RuntimeUtility.getAgentIdFromToken()` instead of reading `recipient.agenticAppBlueprintId`. +- `ScopeUtils.deriveAgentDetails` now resolves `agentUPN` via `activity.getAgenticUser()` instead of `recipient.agenticUserId`. +- `ScopeUtils.deriveTenantDetails` now uses `activity.getAgenticTenantId()` instead of `recipient.tenantId`. +- `getTargetAgentBaggagePairs` now uses `activity.getAgenticInstanceId()` instead of `recipient.agenticAppId`. +- `getTenantIdPair` now uses `activity.getAgenticTenantId()` instead of manual `channelData` parsing. + ## [1.1.0] - 2025-12-09 ### Changed From 9e8b3ff3b523db0c8655650e2fcbb77416f34aaa Mon Sep 17 00:00:00 2001 From: jsl517 Date: Mon, 2 Mar 2026 11:06:48 -0800 Subject: [PATCH 6/8] Use optional chaining on Activity helper method calls for resilience Co-Authored-By: Claude Opus 4.6 --- .../src/middleware/OutputLoggingMiddleware.ts | 2 +- .../src/utils/ScopeUtils.ts | 8 ++++---- .../src/utils/TurnContextUtils.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts b/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts index b474a307..93d4f2dc 100644 --- a/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts +++ b/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts @@ -35,7 +35,7 @@ export const A365_AUTH_TOKEN_KEY = 'A365AuthToken'; export class OutputLoggingMiddleware implements Middleware { async onTurn(context: TurnContext, next: () => Promise): Promise { - const authToken = context.activity?.isAgenticRequest() + const authToken = context.activity?.isAgenticRequest?.() ? '' : context.turnState.get(A365_AUTH_TOKEN_KEY) as string ?? ''; const agentDetails = ScopeUtils.deriveAgentDetails(context, authToken); diff --git a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts index 1bd4c000..d5c634a1 100644 --- a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts @@ -41,7 +41,7 @@ export class ScopeUtils { * @returns Tenant details if a recipient tenant id is present; otherwise undefined. */ public static deriveTenantDetails(turnContext: TurnContext): TenantDetails | undefined { - const tenantId = turnContext?.activity?.getAgenticTenantId(); + const tenantId = turnContext?.activity?.getAgenticTenantId?.(); return tenantId ? { tenantId } : undefined; } @@ -58,7 +58,7 @@ export class ScopeUtils { const recipient = turnContext?.activity?.recipient; if (!recipient) return undefined; const agentId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken); - const agentBlueprintId = turnContext?.activity?.isAgenticRequest() + const agentBlueprintId = turnContext?.activity?.isAgenticRequest?.() ? RuntimeUtility.getAgentIdFromToken(authToken) : undefined; return { @@ -66,9 +66,9 @@ export class ScopeUtils { agentName: recipient.name, agentAUID: recipient.aadObjectId, agentBlueprintId, - agentUPN: turnContext?.activity?.getAgenticUser(), + agentUPN: turnContext?.activity?.getAgenticUser?.(), agentDescription: recipient.role, - tenantId: turnContext?.activity?.getAgenticTenantId() + tenantId: turnContext?.activity?.getAgenticTenantId?.() } as AgentDetails; } diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 3c1ac425..5e0c488c 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -74,7 +74,7 @@ export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[str return []; } const recipient = turnContext.activity.recipient; - const agentId = turnContext.activity?.getAgenticInstanceId(); + const agentId = turnContext.activity?.getAgenticInstanceId?.() ?? recipient.agenticAppId; const agentName = recipient.name; const aadObjectId = recipient.aadObjectId; const agentDescription = recipient.role; @@ -93,7 +93,7 @@ export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[str * @returns Array of [key, value] for tenant ID */ export function getTenantIdPair(turnContext: TurnContext): Array<[string, string]> { - const tenantId = turnContext.activity?.getAgenticTenantId(); + const tenantId = turnContext.activity?.getAgenticTenantId?.(); return tenantId ? [[OpenTelemetryConstants.TENANT_ID_KEY, tenantId]] : []; } From c6643c079d729a6df4a65b949a2341b36c744a77 Mon Sep 17 00:00:00 2001 From: jsl517 Date: Tue, 3 Mar 2026 12:18:43 -0800 Subject: [PATCH 7/8] 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 --- CHANGELOG.md | 2 +- .../src/middleware/OutputLoggingMiddleware.ts | 23 +++++- .../src/utils/ScopeUtils.ts | 12 +-- .../src/utils/TurnContextUtils.ts | 23 +++++- .../agents-a365-observability/src/index.ts | 3 + .../hosting/BaggageBuilderUtils.test.ts | 1 + .../hosting/TurnContextUtils.test.ts | 1 + .../hosting/baggage-middleware.test.ts | 2 +- .../hosting/output-logging-middleware.test.ts | 79 ++++++++++++++++++- .../extension/hosting/scope-utils.test.ts | 10 +-- 10 files changed, 133 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b386649..f83fabf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ObservabilityHostingManager**: Manager for configuring hosting-layer observability middleware with `ObservabilityHostingOptions`. ### Changed -- `ScopeUtils.deriveAgentDetails` now resolves `agentId` via `RuntimeUtility.ResolveAgentIdentity()` (works for both app-based and blueprint-based agents) instead of reading `recipient.agenticAppId` directly. +- `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. - `ScopeUtils.deriveAgentDetails` now resolves `agentBlueprintId` from the JWT `xms_par_app_azp` claim via `RuntimeUtility.getAgentIdFromToken()` instead of reading `recipient.agenticAppBlueprintId`. - `ScopeUtils.deriveAgentDetails` now resolves `agentUPN` via `activity.getAgenticUser()` instead of `recipient.agenticUserId`. - `ScopeUtils.deriveTenantDetails` now uses `activity.getAgenticTenantId()` instead of `recipient.tenantId`. diff --git a/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts b/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts index 93d4f2dc..54195d72 100644 --- a/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts +++ b/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts @@ -9,8 +9,10 @@ import { CallerDetails, ParentSpanRef, logger, + isPerRequestExportEnabled, } from '@microsoft/agents-a365-observability'; import { ScopeUtils } from '../utils/ScopeUtils'; +import { AgenticTokenCacheInstance } from '../caching/AgenticTokenCache'; /** * TurnState key for the parent span reference. @@ -35,9 +37,7 @@ export const A365_AUTH_TOKEN_KEY = 'A365AuthToken'; export class OutputLoggingMiddleware implements Middleware { async onTurn(context: TurnContext, next: () => Promise): Promise { - const authToken = context.activity?.isAgenticRequest?.() - ? '' - : context.turnState.get(A365_AUTH_TOKEN_KEY) as string ?? ''; + const authToken = this.resolveAuthToken(context); const agentDetails = ScopeUtils.deriveAgentDetails(context, authToken); const tenantDetails = ScopeUtils.deriveTenantDetails(context); @@ -57,6 +57,23 @@ export class OutputLoggingMiddleware implements Middleware { await next(); } + /** + * Resolve the auth token for agent identity resolution. + * When per-request export is enabled, reads from turnState. + * Otherwise, reads from the cached observability token. + */ + private resolveAuthToken(context: TurnContext): string { + if (isPerRequestExportEnabled()) { + return context.turnState.get(A365_AUTH_TOKEN_KEY) as string ?? ''; + } + const agentId = context.activity?.getAgenticInstanceId?.() ?? ''; + const tenantId = context.activity?.getAgenticTenantId?.() ?? ''; + if (agentId && tenantId) { + return AgenticTokenCacheInstance.getObservabilityToken(agentId, tenantId) ?? ''; + } + return ''; + } + /** * Creates a send handler that wraps outgoing messages in OutputScope spans. * Reads parent span ref lazily so the agent handler can set it during `next()`. diff --git a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts index d5c634a1..356e24dc 100644 --- a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts @@ -16,7 +16,7 @@ import { InvokeAgentDetails, ToolCallDetails, } from '@microsoft/agents-a365-observability'; -import { Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime'; +import { resolveEmbodiedAgentIds } from './TurnContextUtils'; /** * Unified utilities to populate scope tags from a TurnContext. @@ -47,9 +47,8 @@ export class ScopeUtils { /** * Derive target agent details from the activity recipient. - * Uses {@link RuntimeUtility.ResolveAgentIdentity} to resolve the agent ID (instance ID for blueprint agents, - * appid/azp from token for app-based agents). For blueprint agents, the blueprint ID is resolved - * from the token's xms_par_app_azp claim via {@link RuntimeUtility.getAgentIdFromToken}. + * Uses {@link resolveEmbodiedAgentIds} to resolve the agent ID and blueprint ID, which are only + * set for embodied (agentic) agents — see that function for the rationale. * @param turnContext Activity context * @param authToken Auth token for resolving agent identity from token claims. * @returns Agent details built from recipient properties; otherwise undefined. @@ -57,10 +56,7 @@ export class ScopeUtils { public static deriveAgentDetails(turnContext: TurnContext, authToken: string): AgentDetails | undefined { const recipient = turnContext?.activity?.recipient; if (!recipient) return undefined; - const agentId = RuntimeUtility.ResolveAgentIdentity(turnContext, authToken); - const agentBlueprintId = turnContext?.activity?.isAgenticRequest?.() - ? RuntimeUtility.getAgentIdFromToken(authToken) - : undefined; + const { agentId, agentBlueprintId } = resolveEmbodiedAgentIds(turnContext, authToken); return { agentId, agentName: recipient.name, diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 5e0c488c..3b42e2b8 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -6,6 +6,7 @@ import { TurnContext } from '@microsoft/agents-hosting'; import { ExecutionType, OpenTelemetryConstants } from '@microsoft/agents-a365-observability'; import {RoleTypes} from '@microsoft/agents-activity'; +import { Utility as RuntimeUtility } from '@microsoft/agents-a365-runtime'; /** * TurnContext utility methods. @@ -64,17 +65,35 @@ export function getExecutionTypePair(turnContext: TurnContext): Array<[string, s return [[OpenTelemetryConstants.GEN_AI_EXECUTION_TYPE_KEY, executionType]]; } +/** + * Resolves the agent instance ID and blueprint ID for embodied (agentic) agents only. + * For the non-embodied agent case, we cannot reliably determine whether the token contains an app ID, + * or whether the app ID present in the token claims actually corresponds to this agent. Therefore, + * we only set agentId and agentBlueprintId for embodied (agentic) agents. + * @param turnContext Activity context + * @param authToken Auth token for resolving blueprint ID from token claims. + * @returns Object with agentId and agentBlueprintId, both undefined for non-embodied agents. + */ +export function resolveEmbodiedAgentIds(turnContext: TurnContext, authToken: string): { agentId: string | undefined; agentBlueprintId: string | undefined } { + const isAgentic = turnContext?.activity?.isAgenticRequest?.(); + return { + agentId: isAgentic ? turnContext.activity.getAgenticInstanceId?.() : undefined, + agentBlueprintId: isAgentic ? RuntimeUtility.getAgentIdFromToken(authToken) : undefined, + }; +} + /** * Extracts agent/recipient-related OpenTelemetry baggage pairs from the TurnContext. * @param turnContext The current TurnContext (activity context) + * @param authToken Optional auth token for resolving agent blueprint ID from token claims. * @returns Array of [key, value] pairs for agent identity and description */ -export function getTargetAgentBaggagePairs(turnContext: TurnContext): Array<[string, string]> { +export function getTargetAgentBaggagePairs(turnContext: TurnContext, authToken?: string): Array<[string, string]> { if (!turnContext || !turnContext.activity?.recipient) { return []; } const recipient = turnContext.activity.recipient; - const agentId = turnContext.activity?.getAgenticInstanceId?.() ?? recipient.agenticAppId; + const { agentId } = authToken ? resolveEmbodiedAgentIds(turnContext, authToken) : { agentId: turnContext.activity?.isAgenticRequest?.() ? turnContext.activity.getAgenticInstanceId?.() : undefined }; const agentName = recipient.name; const aadObjectId = recipient.aadObjectId; const agentDescription = recipient.role; diff --git a/packages/agents-a365-observability/src/index.ts b/packages/agents-a365-observability/src/index.ts index b2131b64..b45f86d7 100644 --- a/packages/agents-a365-observability/src/index.ts +++ b/packages/agents-a365-observability/src/index.ts @@ -56,5 +56,8 @@ export { OutputScope } from './tracing/scopes/OutputScope'; export { logger, setLogger, getLogger, resetLogger, formatError } from './utils/logging'; export type { ILogger } from './utils/logging'; +// Exporter utilities +export { isPerRequestExportEnabled } from './tracing/exporter/utils'; + // Configuration export * from './configuration'; diff --git a/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts b/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts index 3aaf516b..5cd977d2 100644 --- a/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts +++ b/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts @@ -12,6 +12,7 @@ describe('BaggageBuilderUtils', () => { from: { id: 'user1', name: 'User One', agenticUserId: 'agentic-user-1', tenantId: 'tenant1', role: 'user' }, recipient: { id: 'agent1', name: 'Agent One', agenticAppId: 'agent-app-1', agenticUserId: 'agentic-agent-1', tenantId: 'tenant1', role: 'agent' }, conversation: { id: 'conv-1', tenantId: 'tenant1' }, + isAgenticRequest: () => true, getAgenticInstanceId: () => 'agent-app-1', getAgenticUser: () => 'agentic-agent-1', getAgenticTenantId: () => 'tenant1', diff --git a/tests/observability/extension/hosting/TurnContextUtils.test.ts b/tests/observability/extension/hosting/TurnContextUtils.test.ts index 04806c26..d2abf2b6 100644 --- a/tests/observability/extension/hosting/TurnContextUtils.test.ts +++ b/tests/observability/extension/hosting/TurnContextUtils.test.ts @@ -20,6 +20,7 @@ describe('TurnContextUtils', () => { recipient: { id: 'agent1', name: 'Agent One', agenticAppId: 'agent-app-1', agenticUserId: 'agentic-agent-1', tenantId: 'tenant1', role: 'agenticUser' }, conversation: { id: 'conv-1', tenantId: 'tenant1' }, text: 'Hello world', + isAgenticRequest: () => true, getAgenticInstanceId: () => 'agent-app-1', getAgenticUser: () => 'agentic-agent-1', getAgenticTenantId: () => 'tenant1', diff --git a/tests/observability/extension/hosting/baggage-middleware.test.ts b/tests/observability/extension/hosting/baggage-middleware.test.ts index 22337033..10490d2b 100644 --- a/tests/observability/extension/hosting/baggage-middleware.test.ts +++ b/tests/observability/extension/hosting/baggage-middleware.test.ts @@ -102,7 +102,7 @@ describe('BaggageMiddleware', () => { expect(capturedBaggage[OpenTelemetryConstants.GEN_AI_CALLER_ID_KEY]).toBe('user-oid'); expect(capturedBaggage[OpenTelemetryConstants.TENANT_ID_KEY]).toBe('tenant-123'); - expect(capturedBaggage[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBe('agent-1'); + expect(capturedBaggage[OpenTelemetryConstants.GEN_AI_AGENT_ID_KEY]).toBeUndefined(); expect(capturedBaggage[OpenTelemetryConstants.GEN_AI_EXECUTION_SOURCE_NAME_KEY]).toBe('web'); expect(capturedBaggage[OpenTelemetryConstants.GEN_AI_CONVERSATION_ID_KEY]).toBe('conv-001'); }); diff --git a/tests/observability/extension/hosting/output-logging-middleware.test.ts b/tests/observability/extension/hosting/output-logging-middleware.test.ts index e28c52ea..ff883ebc 100644 --- a/tests/observability/extension/hosting/output-logging-middleware.test.ts +++ b/tests/observability/extension/hosting/output-logging-middleware.test.ts @@ -1,6 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// Mock isPerRequestExportEnabled before importing the middleware +const mockIsPerRequestExportEnabled = jest.fn().mockReturnValue(false); +jest.mock('@microsoft/agents-a365-observability', () => { + const actual = jest.requireActual('@microsoft/agents-a365-observability'); + return { + ...actual, + isPerRequestExportEnabled: (...args: unknown[]) => mockIsPerRequestExportEnabled(...args), + }; +}); + +// Mock AgenticTokenCacheInstance +const mockGetObservabilityToken = jest.fn().mockReturnValue(null); +jest.mock('../../../../packages/agents-a365-observability-hosting/src/caching/AgenticTokenCache', () => ({ + AgenticTokenCacheInstance: { + getObservabilityToken: (...args: [string, string]) => mockGetObservabilityToken(...args), + }, +})); + import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals'; import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { trace, context as otelContext } from '@opentelemetry/api'; @@ -8,7 +26,7 @@ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-ho import { RoleTypes, ActivityTypes, ActivityEventNames } from '@microsoft/agents-activity'; import type { TurnContext, SendActivitiesHandler } from '@microsoft/agents-hosting'; -import { OutputLoggingMiddleware, A365_PARENT_SPAN_KEY } from '../../../../packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware'; +import { OutputLoggingMiddleware, A365_PARENT_SPAN_KEY, A365_AUTH_TOKEN_KEY } from '../../../../packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware'; import { OpenTelemetryConstants, ParentSpanRef } from '@microsoft/agents-a365-observability'; function makeMockTurnContext(options?: { @@ -105,6 +123,8 @@ describe('OutputLoggingMiddleware', () => { beforeEach(() => { exporter.reset(); + mockIsPerRequestExportEnabled.mockReturnValue(false); + mockGetObservabilityToken.mockReturnValue(null); }); afterAll(async () => { @@ -286,4 +306,61 @@ describe('OutputLoggingMiddleware', () => { } }); }); + + describe('resolveAuthToken', () => { + it('should read token from turnState when per-request export is enabled', async () => { + mockIsPerRequestExportEnabled.mockReturnValue(true); + const middleware = new OutputLoggingMiddleware(); + const ctx = makeMockTurnContext({ text: 'Hello' }); + ctx.turnState.set(A365_AUTH_TOKEN_KEY, 'per-request-token'); + + await middleware.onTurn(ctx, async () => { + ctx.turnState.set(A365_PARENT_SPAN_KEY, { traceId: '0af7651916cd43dd8448eb211c80319c', spanId: 'b7ad6b7169203331', traceFlags: 1 }); + await ctx.simulateSend([{ type: 'message', text: 'Reply' }]); + }); + + expect(mockIsPerRequestExportEnabled).toHaveBeenCalled(); + expect(mockGetObservabilityToken).not.toHaveBeenCalled(); + }); + + it('should read token from cache when per-request export is disabled', async () => { + mockIsPerRequestExportEnabled.mockReturnValue(false); + mockGetObservabilityToken.mockReturnValue('cached-obs-token'); + const middleware = new OutputLoggingMiddleware(); + const ctx = makeMockTurnContext({ text: 'Hello', recipientId: 'agent-1', recipientTenantId: 'tenant-123' }); + + await middleware.onTurn(ctx, async () => { + ctx.turnState.set(A365_PARENT_SPAN_KEY, { traceId: '0af7651916cd43dd8448eb211c80319c', spanId: 'b7ad6b7169203331', traceFlags: 1 }); + await ctx.simulateSend([{ type: 'message', text: 'Reply' }]); + }); + + expect(mockGetObservabilityToken).toHaveBeenCalledWith('agent-1', 'tenant-123'); + }); + + it('should skip cache when per-request export is disabled and agentId is empty', async () => { + mockIsPerRequestExportEnabled.mockReturnValue(false); + const middleware = new OutputLoggingMiddleware(); + const ctx: any = { + activity: { + text: 'Hello', + channelId: 'web', + conversation: { id: 'conv-001' }, + from: { role: RoleTypes.User, aadObjectId: 'user-oid', name: 'User', tenantId: 't' }, + recipient: { name: 'Agent', aadObjectId: 'oid', role: 'assistant' }, + getAgenticTenantId: () => 'tenant-123', + getAgenticUser: () => 'agent@contoso.com', + getAgenticInstanceId: () => '', + isAgenticRequest: () => false, + }, + turnState: new Map(), + onSendActivities: jest.fn(), + }; + + let nextCalled = false; + await middleware.onTurn(ctx, async () => { nextCalled = true; }); + + expect(nextCalled).toBe(true); + expect(mockGetObservabilityToken).not.toHaveBeenCalled(); + }); + }); }); diff --git a/tests/observability/extension/hosting/scope-utils.test.ts b/tests/observability/extension/hosting/scope-utils.test.ts index 41a4daba..5e36e867 100644 --- a/tests/observability/extension/hosting/scope-utils.test.ts +++ b/tests/observability/extension/hosting/scope-utils.test.ts @@ -3,17 +3,13 @@ // Licensed under the MIT License. // ------------------------------------------------------------------------------ -// Mock RuntimeUtility methods so tests don't depend on real JWT parsing +// Mock RuntimeUtility.getAgentIdFromToken so tests don't depend on real JWT parsing jest.mock('@microsoft/agents-a365-runtime', () => { const actual = jest.requireActual('@microsoft/agents-a365-runtime'); return { ...actual, Utility: { ...actual.Utility, - ResolveAgentIdentity: (context: any, _authToken: string) => - context.activity?.isAgenticRequest?.() - ? context.activity?.getAgenticInstanceId?.() || '' - : 'test-app-id', getAgentIdFromToken: () => 'test-blueprint-id', }, }; @@ -208,7 +204,7 @@ test('deriveTenantDetails returns undefined when getAgenticTenantId() returns un test('deriveAgentDetails maps recipient fields to AgentDetails', () => { const ctx = makeCtx({ activity: { recipient: { name: 'A', aadObjectId: 'auid', role: 'bot' }, isAgenticRequest: () => false, getAgenticInstanceId: () => 'aid', getAgenticUser: () => 'upn1', getAgenticTenantId: () => 't1' } as any }); expect(ScopeUtils.deriveAgentDetails(ctx, testAuthToken)).toEqual({ - agentId: 'test-app-id', + agentId: undefined, agentName: 'A', agentAUID: 'auid', agentBlueprintId: undefined, @@ -295,7 +291,7 @@ test('buildInvokeAgentDetails merges agent (recipient), conversationId, sourceMe }); const result = ScopeUtils.buildInvokeAgentDetails(invokeAgentDetails, ctx, testAuthToken); - expect(result.agentId).toBe('test-app-id'); + expect(result.agentId).toBeUndefined(); expect(result.conversationId).toBe('c-2'); expect(result.request?.sourceMetadata).toEqual({ id: 'orig-id', name: 'web', description: 'inbox' }); }); From 42333fdd35b2692c50ee6ee444ecb28e313bacce Mon Sep 17 00:00:00 2001 From: jsl517 Date: Tue, 3 Mar 2026 12:35:59 -0800 Subject: [PATCH 8/8] 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 --- .../src/middleware/OutputLoggingMiddleware.ts | 4 ++-- .../src/utils/TurnContextUtils.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts b/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts index 54195d72..aed44788 100644 --- a/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts +++ b/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts @@ -22,8 +22,8 @@ export const A365_PARENT_SPAN_KEY = 'A365ParentSpanId'; /** * TurnState key for the auth token. - * Set this in `turnState` so middleware can resolve agent identity from token claims - * when the request is not an agentic request. + * Set this in `turnState` so middleware can resolve the agent blueprint ID + * from token claims (used for embodied/agentic requests). */ export const A365_AUTH_TOKEN_KEY = 'A365AuthToken'; diff --git a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts index 3b42e2b8..af55ccde 100644 --- a/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/TurnContextUtils.ts @@ -76,9 +76,11 @@ export function getExecutionTypePair(turnContext: TurnContext): Array<[string, s */ export function resolveEmbodiedAgentIds(turnContext: TurnContext, authToken: string): { agentId: string | undefined; agentBlueprintId: string | undefined } { const isAgentic = turnContext?.activity?.isAgenticRequest?.(); + const rawAgentId = isAgentic ? turnContext.activity.getAgenticInstanceId?.() : undefined; + const rawBlueprintId = isAgentic ? RuntimeUtility.getAgentIdFromToken(authToken) : undefined; return { - agentId: isAgentic ? turnContext.activity.getAgenticInstanceId?.() : undefined, - agentBlueprintId: isAgentic ? RuntimeUtility.getAgentIdFromToken(authToken) : undefined, + agentId: rawAgentId || undefined, + agentBlueprintId: rawBlueprintId || undefined, }; }