diff --git a/CHANGELOG.md b/CHANGELOG.md index ccacef0d..f83fabf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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. + ### Added - **OutputScope**: Tracing scope for outgoing agent messages with caller details, conversation ID, source metadata, and parent span linking. - **BaggageMiddleware**: Middleware for automatic OpenTelemetry baggage propagation from TurnContext. @@ -14,6 +22,12 @@ 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 `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`. +- `getTargetAgentBaggagePairs` now uses `activity.getAgenticInstanceId()` instead of `recipient.agenticAppId`. +- `getTenantIdPair` now uses `activity.getAgenticTenantId()` instead of manual `channelData` parsing. - `InferenceScope.recordInputMessages()` / `recordOutputMessages()` now use JSON array format instead of comma-separated strings. - `InvokeAgentScope.recordInputMessages()` / `recordOutputMessages()` now use JSON array format instead of comma-separated strings. diff --git a/packages/agents-a365-observability-hosting/src/index.ts b/packages/agents-a365-observability-hosting/src/index.ts index 5f7113e5..ed0173a3 100644 --- a/packages/agents-a365-observability-hosting/src/index.ts +++ b/packages/agents-a365-observability-hosting/src/index.ts @@ -8,6 +8,6 @@ export * from './utils/ScopeUtils'; export * from './utils/TurnContextUtils'; export { AgenticTokenCache, AgenticTokenCacheInstance } from './caching/AgenticTokenCache'; export { BaggageMiddleware } from './middleware/BaggageMiddleware'; -export { OutputLoggingMiddleware, A365_PARENT_SPAN_KEY } from './middleware/OutputLoggingMiddleware'; +export { OutputLoggingMiddleware, A365_PARENT_SPAN_KEY, A365_AUTH_TOKEN_KEY } from './middleware/OutputLoggingMiddleware'; export { ObservabilityHostingManager } from './middleware/ObservabilityHostingManager'; export type { ObservabilityHostingOptions } from './middleware/ObservabilityHostingManager'; diff --git a/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts b/packages/agents-a365-observability-hosting/src/middleware/OutputLoggingMiddleware.ts index bc2225ed..aed44788 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. @@ -18,6 +20,13 @@ import { ScopeUtils } from '../utils/ScopeUtils'; */ export const A365_PARENT_SPAN_KEY = 'A365ParentSpanId'; +/** + * TurnState key for the auth token. + * 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'; + /** * Middleware that creates {@link OutputScope} spans for outgoing messages. * 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'; export class OutputLoggingMiddleware implements Middleware { async onTurn(context: TurnContext, next: () => Promise): Promise { - const agentDetails = ScopeUtils.deriveAgentDetails(context); + const authToken = this.resolveAuthToken(context); + const agentDetails = ScopeUtils.deriveAgentDetails(context, authToken); const tenantDetails = ScopeUtils.deriveTenantDetails(context); if (!agentDetails || !tenantDetails) { @@ -47,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 e1b46201..356e24dc 100644 --- a/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts +++ b/packages/agents-a365-observability-hosting/src/utils/ScopeUtils.ts @@ -16,6 +16,7 @@ import { InvokeAgentDetails, ToolCallDetails, } from '@microsoft/agents-a365-observability'; +import { resolveEmbodiedAgentIds } from './TurnContextUtils'; /** * Unified utilities to populate scope tags from a TurnContext. @@ -40,26 +41,30 @@ 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 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. */ - 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 { agentId, agentBlueprintId } = resolveEmbodiedAgentIds(turnContext, authToken); return { - agentId: recipient.agenticAppId, + agentId, 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; } @@ -127,6 +132,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. @@ -134,10 +140,11 @@ export class ScopeUtils { static populateInferenceScopeFromTurnContext( details: InferenceDetails, turnContext: TurnContext, + authToken: string, startTime?: TimeInput, endTime?: TimeInput ): InferenceScope { - const agent = ScopeUtils.deriveAgentDetails(turnContext); + const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken); const tenant = ScopeUtils.deriveTenantDetails(turnContext); const conversationId = ScopeUtils.deriveConversationId(turnContext); const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(turnContext); @@ -161,6 +168,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. @@ -168,13 +176,14 @@ export class ScopeUtils { 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 = ScopeUtils.buildInvokeAgentDetailsCore(details, turnContext, authToken); if (!tenant) { throw new Error('populateInvokeAgentScopeFromTurnContext: Missing tenant details on TurnContext (recipient)'); @@ -189,10 +198,15 @@ 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 { + return ScopeUtils.buildInvokeAgentDetailsCore(details, turnContext, authToken); + } + + private static buildInvokeAgentDetailsCore(details: InvokeAgentDetails, turnContext: TurnContext, authToken: string): InvokeAgentDetails { + const agent = ScopeUtils.deriveAgentDetails(turnContext, authToken); const srcMetaFromContext = ScopeUtils.deriveSourceMetadataObject(turnContext); const baseRequest = details.request ?? {}; const baseSource = baseRequest.sourceMetadata ?? {}; @@ -217,6 +231,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). @@ -225,10 +240,11 @@ export class ScopeUtils { static populateExecuteToolScopeFromTurnContext( details: ToolCallDetails, turnContext: TurnContext, + authToken: string, startTime?: TimeInput, endTime?: TimeInput ): ExecuteToolScope { - const agent = ScopeUtils.deriveAgentDetails(turnContext); + const agent = ScopeUtils.deriveAgentDetails(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 981d4ead..af55ccde 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,37 @@ 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?.(); + const rawAgentId = isAgentic ? turnContext.activity.getAgenticInstanceId?.() : undefined; + const rawBlueprintId = isAgentic ? RuntimeUtility.getAgentIdFromToken(authToken) : undefined; + return { + agentId: rawAgentId || undefined, + agentBlueprintId: rawBlueprintId || 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]> { - if (!turnContext || !turnContext.activity?.recipient) { +export function getTargetAgentBaggagePairs(turnContext: TurnContext, authToken?: string): Array<[string, string]> { + if (!turnContext || !turnContext.activity?.recipient) { return []; } - const recipient = turnContext.activity.recipient; - const agentId = recipient.agenticAppId; + const recipient = turnContext.activity.recipient; + 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; @@ -88,29 +109,12 @@ 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 */ 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/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/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..5cd977d2 100644 --- a/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts +++ b/tests/observability/extension/hosting/BaggageBuilderUtils.test.ts @@ -9,9 +9,13 @@ 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' }, + isAgenticRequest: () => true, + getAgenticInstanceId: () => 'agent-app-1', + getAgenticUser: () => 'agentic-agent-1', + getAgenticTenantId: () => 'tenant1', }, } as any; @@ -41,16 +45,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..d2abf2b6 100644 --- a/tests/observability/extension/hosting/TurnContextUtils.test.ts +++ b/tests/observability/extension/hosting/TurnContextUtils.test.ts @@ -16,10 +16,14 @@ 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', + isAgenticRequest: () => true, + getAgenticInstanceId: () => 'agent-app-1', + getAgenticUser: () => 'agentic-agent-1', + getAgenticTenantId: () => 'tenant1', }, } as any; @@ -45,8 +49,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/baggage-middleware.test.ts b/tests/observability/extension/hosting/baggage-middleware.test.ts index f2d1907b..10490d2b 100644 --- a/tests/observability/extension/hosting/baggage-middleware.test.ts +++ b/tests/observability/extension/hosting/baggage-middleware.test.ts @@ -21,6 +21,7 @@ function makeMockTurnContext(options?: { activityType?: string; activityName?: string; }): TurnContext { + const recipientTenantId = options?.recipientTenantId ?? 'tenant-123'; const ctx: any = { activity: { type: options?.activityType, @@ -42,8 +43,12 @@ function makeMockTurnContext(options?: { agenticAppBlueprintId: 'blueprint-1', agenticUserId: 'agent@contoso.com', role: 'assistant', - tenantId: options?.recipientTenantId ?? 'tenant-123', + tenantId: recipientTenantId, }, + getAgenticTenantId: () => recipientTenantId, + getAgenticUser: () => 'agent@contoso.com', + getAgenticInstanceId: () => options?.recipientId ?? 'agent-1', + isAgenticRequest: () => false, }, turnState: new Map(), }; @@ -97,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 448384bb..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?: { @@ -24,6 +42,7 @@ function makeMockTurnContext(options?: { }): TurnContext & { _sendHandlers: SendActivitiesHandler[]; turnState: Map; simulateSend: (activities: Array<{ type?: string; text?: string }>) => Promise> } { const sendHandlers: SendActivitiesHandler[] = []; + const recipientTenantId = options?.recipientTenantId ?? 'tenant-123'; const ctx: any = { activity: { type: options?.activityType, @@ -45,8 +64,12 @@ function makeMockTurnContext(options?: { agenticAppBlueprintId: 'blueprint-1', agenticUserId: 'agent@contoso.com', role: 'assistant', - tenantId: options?.recipientTenantId ?? 'tenant-123', + tenantId: recipientTenantId, }, + getAgenticTenantId: () => recipientTenantId, + getAgenticUser: () => 'agent@contoso.com', + getAgenticInstanceId: () => options?.recipientId ?? 'agent-1', + isAgenticRequest: () => false, }, turnState: new Map(), onSendActivities(handler: SendActivitiesHandler) { @@ -100,6 +123,8 @@ describe('OutputLoggingMiddleware', () => { beforeEach(() => { exporter.reset(); + mockIsPerRequestExportEnabled.mockReturnValue(false); + mockGetObservabilityToken.mockReturnValue(null); }); afterAll(async () => { @@ -145,14 +170,15 @@ describe('OutputLoggingMiddleware', () => { const middleware = new OutputLoggingMiddleware(); // Missing agent details (no recipient) - const ctx1: any = { activity: { text: 'Hello' }, onSendActivities: jest.fn() }; + const ctx1: any = { activity: { text: 'Hello', isAgenticRequest: () => false, getAgenticTenantId: () => undefined, getAgenticUser: () => undefined }, turnState: new Map(), onSendActivities: jest.fn() }; let nextCalled = false; await middleware.onTurn(ctx1, async () => { nextCalled = true; }); expect(nextCalled).toBe(true); // Missing tenant details (recipient but no tenantId) const ctx2: any = { - activity: { text: 'Hello', recipient: { agenticAppId: 'agent-1', name: 'Agent' } }, + activity: { text: 'Hello', recipient: { agenticAppId: 'agent-1', name: 'Agent' }, getAgenticTenantId: () => undefined, getAgenticUser: () => undefined, isAgenticRequest: () => false }, + turnState: new Map(), onSendActivities: jest.fn(), }; nextCalled = false; @@ -280,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 73b5e69d..5e36e867 100644 --- a/tests/observability/extension/hosting/scope-utils.test.ts +++ b/tests/observability/extension/hosting/scope-utils.test.ts @@ -3,11 +3,24 @@ // Licensed under the MIT License. // ------------------------------------------------------------------------------ +// 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, + 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, @@ -20,7 +33,11 @@ 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' }, + isAgenticRequest: () => true, + getAgenticInstanceId: () => 'agent-1', + getAgenticUser: () => 'agent-upn@contoso.com', + getAgenticTenantId: () => 'tenant-123', } }; @@ -35,9 +52,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' @@ -58,7 +75,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( @@ -69,7 +86,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'], @@ -83,36 +100,36 @@ 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 }); - expect(() => ScopeUtils.populateInferenceScopeFromTurnContext(details, ctx)) + const ctx = makeCtx({ activity: { /* no recipient */ getAgenticTenantId: () => 't1' } as any }); + 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' } } 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 */ } as any }); - expect(() => ScopeUtils.populateExecuteToolScopeFromTurnContext(details, ctx)) + const ctx = makeCtx({ activity: { /* no recipient */ getAgenticTenantId: () => 't1' } as any }); + 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' } } 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' } } 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)'); }); }); @@ -121,7 +138,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( @@ -150,7 +167,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( @@ -174,23 +191,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 }); - 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: undefined, agentName: 'A', agentAUID: 'auid', - agentBlueprintId: 'bp1', + agentBlueprintId: undefined, agentUPN: 'upn1', agentDescription: 'bot', tenantId: 't1', @@ -199,7 +216,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', () => { @@ -262,15 +279,19 @@ 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', + 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).toBeUndefined(); expect(result.conversationId).toBe('c-2'); expect(result.request?.sourceMetadata).toEqual({ id: 'orig-id', name: 'web', description: 'inbox' }); }); @@ -281,7 +302,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' });