Skip to content

Commit ddf4902

Browse files
fpfp100jsl517claudenikhilNava
authored
Add OutputScope, BaggageMiddleware, OutputLoggingMiddleware, and ObservabilityHostingManager (#210)
* Add InputScope, OutputScope, and MessageLoggingMiddleware for message tracing Introduces message-level tracing via InputScope and OutputScope, registered through MessageLoggingMiddleware and ObservabilityMiddlewareRegistrar. Both scopes lazily read A365_PARENT_SPAN_KEY from turnState so the agent handler can link them as children of an InvokeAgentScope. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * comments * Address PR #210 review comments - Remove parseExecutionType (execution type removed in new schema) - Make ObservabilityHostingManager.configure() an instance method with required params - Remove static singleton pattern and getInstance() from ObservabilityHostingManager - Update CHANGELOG with unreleased changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update CHANGELOG to reflect net public API changes vs main Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: jsl517 <pefan@microsoft.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Nikhil Navakiran <nikhil.navakiran@gmail.com>
1 parent adecb7f commit ddf4902

16 files changed

Lines changed: 758 additions & 111 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to the Agent365 TypeScript SDK will be documented in this fi
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
- **OutputScope**: Tracing scope for outgoing agent messages with caller details, conversation ID, source metadata, and parent span linking.
12+
- **BaggageMiddleware**: Middleware for automatic OpenTelemetry baggage propagation from TurnContext.
13+
- **OutputLoggingMiddleware**: Middleware that creates OutputScope spans for outgoing messages with lazy parent span linking via `A365_PARENT_SPAN_KEY`.
14+
- **ObservabilityHostingManager**: Manager for configuring hosting-layer observability middleware with `ObservabilityHostingOptions`.
15+
16+
### Changed
17+
- `InferenceScope.recordInputMessages()` / `recordOutputMessages()` now use JSON array format instead of comma-separated strings.
18+
- `InvokeAgentScope.recordInputMessages()` / `recordOutputMessages()` now use JSON array format instead of comma-separated strings.
19+
820
## [1.1.0] - 2025-12-09
921

1022
### Changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ export * from './utils/BaggageBuilderUtils';
77
export * from './utils/ScopeUtils';
88
export * from './utils/TurnContextUtils';
99
export { AgenticTokenCache, AgenticTokenCacheInstance } from './caching/AgenticTokenCache';
10+
export { BaggageMiddleware } from './middleware/BaggageMiddleware';
11+
export { OutputLoggingMiddleware, A365_PARENT_SPAN_KEY } from './middleware/OutputLoggingMiddleware';
12+
export { ObservabilityHostingManager } from './middleware/ObservabilityHostingManager';
13+
export type { ObservabilityHostingOptions } from './middleware/ObservabilityHostingManager';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { TurnContext, Middleware } from '@microsoft/agents-hosting';
5+
import { ActivityTypes, ActivityEventNames } from '@microsoft/agents-activity';
6+
import { BaggageBuilder } from '@microsoft/agents-a365-observability';
7+
import {
8+
getExecutionTypePair,
9+
getCallerBaggagePairs,
10+
getTargetAgentBaggagePairs,
11+
getTenantIdPair,
12+
getSourceMetadataBaggagePairs,
13+
getConversationIdAndItemLinkPairs,
14+
} from '../utils/TurnContextUtils';
15+
16+
/**
17+
* Middleware that propagates OpenTelemetry baggage context derived from TurnContext.
18+
* Async replies (ContinueConversation) are passed through without baggage setup.
19+
*/
20+
export class BaggageMiddleware implements Middleware {
21+
22+
async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> {
23+
const isAsyncReply =
24+
context.activity?.type === ActivityTypes.Event &&
25+
context.activity?.name === ActivityEventNames.ContinueConversation;
26+
27+
if (isAsyncReply) {
28+
await next();
29+
return;
30+
}
31+
32+
const baggageScope = new BaggageBuilder()
33+
.setPairs(getCallerBaggagePairs(context))
34+
.setPairs(getTargetAgentBaggagePairs(context))
35+
.setPairs(getTenantIdPair(context))
36+
.setPairs(getSourceMetadataBaggagePairs(context))
37+
.setPairs(getConversationIdAndItemLinkPairs(context))
38+
.setPairs(getExecutionTypePair(context))
39+
.build();
40+
41+
await baggageScope.run(async () => {
42+
await next();
43+
});
44+
}
45+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { Middleware } from '@microsoft/agents-hosting';
5+
import { logger } from '@microsoft/agents-a365-observability';
6+
import { BaggageMiddleware } from './BaggageMiddleware';
7+
import { OutputLoggingMiddleware } from './OutputLoggingMiddleware';
8+
9+
/**
10+
* Configuration options for the hosting observability layer.
11+
*/
12+
export interface ObservabilityHostingOptions {
13+
/** Enable baggage propagation middleware. Defaults to true. */
14+
enableBaggage?: boolean;
15+
16+
/** Enable output logging middleware for tracing outgoing messages. Defaults to false. */
17+
enableOutputLogging?: boolean;
18+
}
19+
20+
/**
21+
* Manager for configuring hosting-layer observability middleware.
22+
*
23+
* @example
24+
* ```typescript
25+
* const manager = new ObservabilityHostingManager();
26+
* manager.configure(adapter, { enableOutputLogging: true });
27+
* ```
28+
*/
29+
export class ObservabilityHostingManager {
30+
private _configured = false;
31+
32+
/**
33+
* Registers observability middleware on the adapter.
34+
* Subsequent calls are ignored.
35+
*/
36+
configure(
37+
adapter: { use(...middlewares: Array<Middleware>): void },
38+
options: ObservabilityHostingOptions
39+
): void {
40+
if (this._configured) {
41+
logger.warn('[ObservabilityHostingManager] Already configured. Subsequent configure() calls are ignored.');
42+
return;
43+
}
44+
45+
const enableBaggage = options.enableBaggage !== false;
46+
const enableOutputLogging = options.enableOutputLogging === true;
47+
48+
if (enableBaggage) {
49+
adapter.use(new BaggageMiddleware());
50+
logger.info('[ObservabilityHostingManager] BaggageMiddleware registered.');
51+
}
52+
if (enableOutputLogging) {
53+
adapter.use(new OutputLoggingMiddleware());
54+
logger.info('[ObservabilityHostingManager] OutputLoggingMiddleware registered.');
55+
}
56+
57+
logger.info(`[ObservabilityHostingManager] Configured. Baggage: ${enableBaggage}, OutputLogging: ${enableOutputLogging}.`);
58+
this._configured = true;
59+
}
60+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { TurnContext, Middleware, SendActivitiesHandler } from '@microsoft/agents-hosting';
5+
import {
6+
OutputScope,
7+
AgentDetails,
8+
TenantDetails,
9+
CallerDetails,
10+
ParentSpanRef,
11+
logger,
12+
} from '@microsoft/agents-a365-observability';
13+
import { ScopeUtils } from '../utils/ScopeUtils';
14+
15+
/**
16+
* TurnState key for the parent span reference.
17+
* Set this in `turnState` to link OutputScope spans as children of an InvokeAgentScope.
18+
*/
19+
export const A365_PARENT_SPAN_KEY = 'A365ParentSpanId';
20+
21+
/**
22+
* Middleware that creates {@link OutputScope} spans for outgoing messages.
23+
* Links to a parent span when {@link A365_PARENT_SPAN_KEY} is set in turnState.
24+
*
25+
* **Privacy note:** Outgoing message content is captured verbatim
26+
* as span attributes and exported to the configured telemetry backend.
27+
*/
28+
export class OutputLoggingMiddleware implements Middleware {
29+
30+
async onTurn(context: TurnContext, next: () => Promise<void>): Promise<void> {
31+
const agentDetails = ScopeUtils.deriveAgentDetails(context);
32+
const tenantDetails = ScopeUtils.deriveTenantDetails(context);
33+
34+
if (!agentDetails || !tenantDetails) {
35+
await next();
36+
return;
37+
}
38+
39+
const callerDetails = ScopeUtils.deriveCallerDetails(context);
40+
const conversationId = ScopeUtils.deriveConversationId(context);
41+
const sourceMetadata = ScopeUtils.deriveSourceMetadataObject(context);
42+
43+
context.onSendActivities(
44+
this._createSendHandler(context, agentDetails, tenantDetails, callerDetails, conversationId, sourceMetadata)
45+
);
46+
47+
await next();
48+
}
49+
50+
/**
51+
* Creates a send handler that wraps outgoing messages in OutputScope spans.
52+
* Reads parent span ref lazily so the agent handler can set it during `next()`.
53+
*/
54+
private _createSendHandler(
55+
turnContext: TurnContext,
56+
agentDetails: AgentDetails,
57+
tenantDetails: TenantDetails,
58+
callerDetails?: CallerDetails,
59+
conversationId?: string,
60+
sourceMetadata?: { name?: string; description?: string },
61+
): SendActivitiesHandler {
62+
return async (_ctx, activities, sendNext) => {
63+
const messages = activities
64+
.filter((a) => a.type === 'message' && a.text)
65+
.map((a) => a.text!);
66+
67+
if (messages.length === 0) {
68+
return await sendNext();
69+
}
70+
71+
const parentSpanRef: ParentSpanRef | undefined = turnContext.turnState.get(A365_PARENT_SPAN_KEY);
72+
if (!parentSpanRef) {
73+
logger.warn(
74+
`[OutputLoggingMiddleware] No parent span ref in turnState under '${A365_PARENT_SPAN_KEY}'. OutputScope will not be linked to a parent.`
75+
);
76+
}
77+
78+
const outputScope = OutputScope.start(
79+
{ messages },
80+
agentDetails,
81+
tenantDetails,
82+
callerDetails,
83+
conversationId,
84+
sourceMetadata,
85+
undefined,
86+
parentSpanRef,
87+
);
88+
try {
89+
return await sendNext();
90+
} catch (error) {
91+
outputScope.recordError(
92+
error instanceof Error ? error : new Error(typeof error === 'string' ? error : JSON.stringify(error))
93+
);
94+
throw error;
95+
} finally {
96+
outputScope.dispose();
97+
}
98+
};
99+
}
100+
}

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ import {
1515
InferenceDetails,
1616
InvokeAgentDetails,
1717
ToolCallDetails,
18-
ExecutionType
1918
} from '@microsoft/agents-a365-observability';
20-
import {
21-
getExecutionTypePair,
22-
} from './TurnContextUtils';
2319

2420
/**
2521
* Unified utilities to populate scope tags from a TurnContext.
@@ -198,7 +194,6 @@ export class ScopeUtils {
198194
public static buildInvokeAgentDetails(details: InvokeAgentDetails, turnContext: TurnContext): InvokeAgentDetails {
199195
const agent = ScopeUtils.deriveAgentDetails(turnContext);
200196
const srcMetaFromContext = ScopeUtils.deriveSourceMetadataObject(turnContext);
201-
const executionTypePair = getExecutionTypePair(turnContext);
202197
const baseRequest = details.request ?? {};
203198
const baseSource = baseRequest.sourceMetadata ?? {};
204199
const mergedSourceMetadata = {
@@ -212,7 +207,6 @@ export class ScopeUtils {
212207
conversationId: ScopeUtils.deriveConversationId(turnContext),
213208
request: {
214209
...baseRequest,
215-
executionType: executionTypePair.length > 0 ? (executionTypePair[0][1] as ExecutionType) : baseRequest.executionType,
216210
sourceMetadata: mergedSourceMetadata
217211
}
218212
};
@@ -247,4 +241,5 @@ export class ScopeUtils {
247241
const scope = ExecuteToolScope.start(details, agent, tenant, conversationId, sourceMetadata, undefined, startTime, endTime);
248242
return scope;
249243
}
244+
250245
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export {
4444
InferenceDetails,
4545
InferenceOperationType,
4646
InferenceResponse,
47-
OutputResponse
47+
OutputResponse,
4848
} from './tracing/contracts';
4949

5050
// Scopes

packages/agents-a365-observability/src/tracing/contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export enum ExecutionType {
1919
Unknown = 'Unknown'
2020
}
2121

22+
2223
/**
2324
* Represents different roles that can invoke an agent
2425
*/

packages/agents-a365-observability/src/tracing/scopes/InferenceScope.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,15 @@ export class InferenceScope extends OpenTelemetryScope {
8181
* @param messages Array of input messages
8282
*/
8383
public recordInputMessages(messages: string[]): void {
84-
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, messages.join(','));
84+
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, JSON.stringify(messages));
8585
}
8686

8787
/**
8888
* Records the output messages for telemetry tracking.
8989
* @param messages Array of output messages
9090
*/
9191
public recordOutputMessages(messages: string[]): void {
92-
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY, messages.join(','));
92+
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY, JSON.stringify(messages));
9393
}
9494

9595
/**

packages/agents-a365-observability/src/tracing/scopes/InvokeAgentScope.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,14 @@ export class InvokeAgentScope extends OpenTelemetryScope {
125125
* @param messages Array of input messages
126126
*/
127127
public recordInputMessages(messages: string[]): void {
128-
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, messages.join(','));
128+
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_INPUT_MESSAGES_KEY, JSON.stringify(messages));
129129
}
130130

131131
/**
132132
* Records the output messages for telemetry tracking.
133133
* @param messages Array of output messages
134134
*/
135135
public recordOutputMessages(messages: string[]): void {
136-
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY, messages.join(','));
136+
this.setTagMaybe(OpenTelemetryConstants.GEN_AI_OUTPUT_MESSAGES_KEY, JSON.stringify(messages));
137137
}
138138
}

0 commit comments

Comments
 (0)