@cuylabs/agent-a365-observability 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,19 @@
2
2
 
3
3
  Microsoft Agent 365 observability adapter for `@cuylabs/agent-core`.
4
4
 
5
- This package keeps the Microsoft-specific parts outside `agent-core`:
5
+ This package connects agent-core's portable OpenTelemetry spans to Microsoft's
6
+ Agent 365 observability SDK without putting Microsoft SDK types in agent-core.
6
7
 
7
- - starts Microsoft Agent 365 Observability
8
- - wires the Agent 365 exporter token resolver
9
- - wraps each request in Agent 365 baggage so spans include tenant and agent identity
10
- - returns a small tracing config fragment for stable agent metadata
8
+ It does four things:
9
+
10
+ - starts Microsoft Agent 365 Observability;
11
+ - wires the Agent 365 exporter token resolver;
12
+ - returns a small `createAgent({ tracing })` config fragment for stable agent
13
+ metadata;
14
+ - wraps each request in Agent 365 baggage so spans include tenant, agent,
15
+ conversation, channel, and caller identity.
16
+
17
+ For the deeper design, read [docs/README.md](./docs/README.md).
11
18
 
12
19
  ## Install
13
20
 
@@ -31,7 +38,7 @@ const observability = await initA365Observability({
31
38
  serviceVersion: "1.0.0",
32
39
  configuration: {
33
40
  exporterEnabled: true,
34
- logLevel: "info|warn|error",
41
+ logLevel: "info",
35
42
  },
36
43
  tokenResolver: async (agentId, tenantId) => {
37
44
  return getObservabilityToken(agentId, tenantId);
@@ -52,7 +59,6 @@ await runWithA365Context(
52
59
  {
53
60
  tenantId: "tenant-123",
54
61
  agentId: "agent-456",
55
- agentName: "email-assistant",
56
62
  conversationId: "conversation-789",
57
63
  sessionId: "session-789",
58
64
  channelName: "msteams",
@@ -79,7 +85,6 @@ await runWithA365TurnContext(
79
85
  context, // TurnContext from CloudAdapter
80
86
  {
81
87
  agentId: "agent-456",
82
- agentName: "email-assistant",
83
88
  agentDescription: "Organizes email and calendar work",
84
89
  agentVersion: "1.0.0",
85
90
  sessionId,
@@ -100,7 +105,6 @@ const m365 = createM365ChannelAdapter({
100
105
  agent,
101
106
  a365Observability: {
102
107
  agentId: "agent-456",
103
- agentName: "email-assistant",
104
108
  agentDescription: "Organizes email and calendar work",
105
109
  agentVersion: "1.0.0",
106
110
  },
@@ -112,13 +116,22 @@ startup so the Microsoft exporter and token resolver are registered.
112
116
 
113
117
  ## Shape
114
118
 
115
- Use `agent-core` for portable OpenTelemetry spans:
119
+ The integration has three separate pieces:
120
+
121
+ 1. `initA365Observability(...)` starts Microsoft's exporter and baggage span
122
+ processor once at host startup.
123
+ 2. `createA365TracingConfig(...)` feeds agent-core's `createAgent({ tracing })`
124
+ contract.
125
+ 3. `runWithA365Context(...)` or `runWithA365TurnContext(...)` binds
126
+ per-request Agent 365 baggage before `agent.chat()` runs.
127
+
128
+ Use agent-core for portable OpenTelemetry spans:
116
129
 
117
130
  - `invoke_agent` agent spans
118
131
  - `execute_tool` tool spans
119
132
  - AI SDK model spans nested under the agent turn
120
133
 
121
- Use this package for Agent 365-specific context:
134
+ Use this package for Agent 365-specific context on those spans:
122
135
 
123
136
  - `microsoft.tenant.id`
124
137
  - `gen_ai.agent.id`
@@ -129,6 +142,18 @@ Use this package for Agent 365-specific context:
129
142
 
130
143
  For multi-tenant agents, prefer `runWithA365Context()` over static tracing attributes. Static attributes are useful for stable agent metadata; baggage is the right place for per-request tenant and conversation identity.
131
144
 
145
+ Use `extraBaggage` only for additional dimensions that are not part of the
146
+ standard Agent 365 identity set. Structured fields such as `tenantId`,
147
+ `agentId`, `conversationId`, and `userId` win over conflicting `extraBaggage`
148
+ keys.
149
+
150
+ Microsoft's baggage processor does not overwrite span attributes that
151
+ agent-core already set. In normal `@cuylabs/agent-channel-m365` usage this is
152
+ fine because the channel's default session strategy uses the M365
153
+ `conversation.id` as the agent-core session ID. If you use custom M365 session
154
+ mapping, `gen_ai.conversation.id` reflects the custom agent-core session ID.
155
+ See [docs/agent-core-otel.md](./docs/agent-core-otel.md) for details.
156
+
132
157
  ## Phoenix vs Agent 365
133
158
 
134
159
  Phoenix is a normal OTLP destination. You pass a span processor/exporter into `agent-core` tracing, and `agent-core` owns the tracer provider lifecycle for that example.
@@ -156,8 +181,42 @@ const agent = createAgent({
156
181
 
157
182
  Set `useGenAIOpenTelemetry: false` only when you intentionally want to use global AI SDK telemetry integrations or another model-span integration.
158
183
 
184
+ ## TurnContext Mapping
185
+
186
+ `runWithA365TurnContext(...)` follows Microsoft's
187
+ `agents-a365-observability-hosting` helper behavior for M365 Activity fields.
188
+ Some mappings look surprising if read as generic Bot Framework fields:
189
+
190
+ - `activity.serviceUrl` maps to `microsoft.conversation.item.link`.
191
+ - `activity.recipient.role` maps to `gen_ai.agent.description`.
192
+ - `activity.from.agenticUserId` maps to `user.email`.
193
+ - `activity.channelIdSubChannel` maps to `microsoft.channel.link`.
194
+
195
+ Pass explicit `runWithA365TurnContext` options when your host has more precise
196
+ values, such as a real Teams message deep link for `conversationItemLink`.
197
+ Caller-agent attributes and host-owned fields such as `agentEmail`,
198
+ `agentPlatformId`, and `callerClientIp` are not inferred from TurnContext; pass
199
+ them explicitly through `runWithA365Context()` or the TurnContext options when
200
+ you need those dimensions.
201
+
159
202
  ## Exporter Modes
160
203
 
161
204
  For batch export, provide `tokenResolver` in `initA365Observability()`.
162
205
 
163
206
  For per-request export, pass `exportToken` to `runWithA365Context()` and enable per-request export in the Microsoft SDK configuration or environment.
207
+
208
+ ## v0 Limits
209
+
210
+ v0 does not expose:
211
+
212
+ - a separate Microsoft `OutputScope` span;
213
+ - agent-to-agent HTTP trace propagation wrappers;
214
+ - real-time threat protection or chat-history submission middleware.
215
+
216
+ Those remain additive adapter features if an Agent 365 deployment needs them.
217
+
218
+ ## Examples
219
+
220
+ Examples live in the repository under
221
+ `packages/agent-a365-observability/examples`. They are monorepo-local reference
222
+ files and are not shipped in the npm tarball.
package/dist/index.d.ts CHANGED
@@ -14,19 +14,6 @@ type A365ObservabilityConfiguration = {
14
14
  logLevel?: string;
15
15
  authenticationScopes?: string[];
16
16
  };
17
- type InitA365ObservabilityOptions = {
18
- serviceName: string;
19
- serviceVersion?: string;
20
- serviceNamespace?: string;
21
- tokenResolver?: A365TokenResolver;
22
- clusterCategory?: string;
23
- exporterOptions?: Record<string, unknown>;
24
- customLogger?: A365Logger;
25
- configuration?: A365ObservabilityConfiguration;
26
- };
27
- type A365ObservabilityHandle = {
28
- shutdown(): Promise<void>;
29
- };
30
17
  type A365RequestContext = {
31
18
  tenantId?: string;
32
19
  agentId?: string;
@@ -68,8 +55,11 @@ type A365RequestContext = {
68
55
  type A365ActivityAccountLike = {
69
56
  id?: string;
70
57
  name?: string;
58
+ role?: string;
71
59
  aadObjectId?: string;
72
60
  agenticAppId?: string;
61
+ agenticAppBlueprintId?: string;
62
+ agenticUserId?: string;
73
63
  tenantId?: string;
74
64
  userPrincipalName?: string;
75
65
  email?: string;
@@ -79,6 +69,7 @@ type A365ActivityLike = {
79
69
  id?: string;
80
70
  serviceUrl?: string;
81
71
  channelId?: string;
72
+ channelIdSubChannel?: string;
82
73
  from?: A365ActivityAccountLike;
83
74
  recipient?: A365ActivityAccountLike;
84
75
  conversation?: {
@@ -89,6 +80,9 @@ type A365ActivityLike = {
89
80
  [key: string]: unknown;
90
81
  };
91
82
  channelData?: unknown;
83
+ isAgenticRequest?: () => boolean;
84
+ getAgenticInstanceId?: () => string | undefined;
85
+ getAgenticTenantId?: () => string | undefined;
92
86
  [key: string]: unknown;
93
87
  };
94
88
  type A365TurnContextLike = {
@@ -103,8 +97,8 @@ type A365TurnContextOptions = Omit<A365RequestContext, "extraBaggage"> & {
103
97
  */
104
98
  serviceUrl?: string;
105
99
  /**
106
- * Use `activity.recipient.id` as `agentId` when `agentId` and
107
- * `activity.recipient.agenticAppId` are both unavailable.
100
+ * Use `activity.recipient.id` as `agentId` when `agentId` and Agent 365
101
+ * activity identity helpers are unavailable.
108
102
  *
109
103
  * Keep this disabled when the recipient ID is only a Bot Framework ID and not
110
104
  * the Agent 365 app ID.
@@ -135,6 +129,35 @@ type A365TracingConfig = {
135
129
  useGlobalTelemetryIntegrations?: boolean;
136
130
  spanAttributes?: Record<string, SpanAttributeValue>;
137
131
  };
132
+
133
+ type A365ObservabilityModuleLoader = () => Promise<unknown>;
134
+ type A365ObservabilityRuntimeOptions = {
135
+ /**
136
+ * Test seam for loading `@microsoft/agents-a365-observability`.
137
+ *
138
+ * Production code normally leaves this unset so the package is loaded lazily
139
+ * only when observability is used.
140
+ */
141
+ getObservabilityModule?: A365ObservabilityModuleLoader;
142
+ };
143
+ type InitA365ObservabilityOptions = A365ObservabilityRuntimeOptions & {
144
+ serviceName: string;
145
+ serviceVersion?: string;
146
+ serviceNamespace?: string;
147
+ tokenResolver?: A365TokenResolver;
148
+ clusterCategory?: string;
149
+ exporterOptions?: Record<string, unknown>;
150
+ customLogger?: A365Logger;
151
+ configuration?: A365ObservabilityConfiguration;
152
+ };
153
+ type A365ObservabilityHandle = {
154
+ shutdown(): Promise<void>;
155
+ };
156
+
157
+ declare class A365ObservabilityModuleLoadError extends Error {
158
+ constructor(message: string, options?: ErrorOptions);
159
+ }
160
+
138
161
  declare const A365_BAGGAGE_KEYS: {
139
162
  readonly tenantId: "microsoft.tenant.id";
140
163
  readonly agentId: "gen_ai.agent.id";
@@ -166,13 +189,18 @@ declare const A365_BAGGAGE_KEYS: {
166
189
  readonly serverAddress: "server.address";
167
190
  readonly serverPort: "server.port";
168
191
  };
169
- declare function createA365TracingConfig(options?: A365TracingConfigOptions): A365TracingConfig;
192
+
170
193
  declare function buildA365BaggagePairs(context: A365RequestContext): Record<string, string>;
194
+
195
+ declare function createA365TracingConfig(options?: A365TracingConfigOptions): A365TracingConfig;
196
+
171
197
  declare function createA365ContextFromTurnContext(turnContext: A365TurnContextLike, options?: A365TurnContextOptions): A365RequestContext;
172
- declare function runWithA365TurnContext<T>(turnContext: A365TurnContextLike, fn: () => T): Promise<Awaited<T>>;
173
- declare function runWithA365TurnContext<T>(turnContext: A365TurnContextLike, options: A365TurnContextOptions, fn: () => T): Promise<Awaited<T>>;
198
+
199
+ declare function runWithA365TurnContext<T>(turnContext: A365TurnContextLike, fn: () => T, runtimeOptions?: A365ObservabilityRuntimeOptions): Promise<Awaited<T>>;
200
+ declare function runWithA365TurnContext<T>(turnContext: A365TurnContextLike, options: A365TurnContextOptions, fn: () => T, runtimeOptions?: A365ObservabilityRuntimeOptions): Promise<Awaited<T>>;
201
+
174
202
  declare function initA365Observability(options: InitA365ObservabilityOptions): Promise<A365ObservabilityHandle>;
175
- declare function runWithA365Context<T>(requestContext: A365RequestContext, fn: () => T): Promise<Awaited<T>>;
176
- declare function updateA365ExportToken(token: string): Promise<boolean>;
203
+ declare function runWithA365Context<T>(requestContext: A365RequestContext, fn: () => T, options?: A365ObservabilityRuntimeOptions): Promise<Awaited<T>>;
204
+ declare function updateA365ExportToken(token: string, options?: A365ObservabilityRuntimeOptions): Promise<boolean>;
177
205
 
178
- export { type A365ActivityAccountLike, type A365ActivityLike, type A365Logger, type A365ObservabilityConfiguration, type A365ObservabilityHandle, type A365RequestContext, type A365TokenResolver, type A365TracingConfig, type A365TracingConfigOptions, type A365TurnContextLike, type A365TurnContextOptions, A365_BAGGAGE_KEYS, type InitA365ObservabilityOptions, type SpanAttributeValue, buildA365BaggagePairs, createA365ContextFromTurnContext, createA365TracingConfig, initA365Observability, runWithA365Context, runWithA365TurnContext, updateA365ExportToken };
206
+ export { type A365ActivityAccountLike, type A365ActivityLike, type A365Logger, type A365ObservabilityConfiguration, type A365ObservabilityHandle, A365ObservabilityModuleLoadError, type A365ObservabilityModuleLoader, type A365ObservabilityRuntimeOptions, type A365RequestContext, type A365TokenResolver, type A365TracingConfig, type A365TracingConfigOptions, type A365TurnContextLike, type A365TurnContextOptions, A365_BAGGAGE_KEYS, type InitA365ObservabilityOptions, type SpanAttributeValue, buildA365BaggagePairs, createA365ContextFromTurnContext, createA365TracingConfig, initA365Observability, runWithA365Context, runWithA365TurnContext, updateA365ExportToken };
package/dist/index.js CHANGED
@@ -1,4 +1,12 @@
1
- // src/index.ts
1
+ // src/errors.ts
2
+ var A365ObservabilityModuleLoadError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = "A365ObservabilityModuleLoadError";
6
+ }
7
+ };
8
+
9
+ // src/baggage-keys.ts
2
10
  var A365_BAGGAGE_KEYS = {
3
11
  tenantId: "microsoft.tenant.id",
4
12
  agentId: "gen_ai.agent.id",
@@ -30,28 +38,89 @@ var A365_BAGGAGE_KEYS = {
30
38
  serverAddress: "server.address",
31
39
  serverPort: "server.port"
32
40
  };
33
- var OBSERVABILITY_PACKAGE = "@microsoft/agents-a365-observability";
34
- function createA365TracingConfig(options = {}) {
35
- const spanAttributes = {
36
- ...options.spanAttributes ?? {}
37
- };
38
- if (isNonEmpty(options.tenantId)) {
39
- spanAttributes[A365_BAGGAGE_KEYS.tenantId] = options.tenantId.trim();
41
+
42
+ // src/internal/parsing.ts
43
+ function setPair(target, key, value) {
44
+ if (value === null || value === void 0) {
45
+ return;
40
46
  }
41
- return {
42
- ...isNonEmpty(options.agentId) ? { agentId: options.agentId.trim() } : {},
43
- ...isNonEmpty(options.agentDescription) ? { agentDescription: options.agentDescription.trim() } : {},
44
- ...isNonEmpty(options.agentVersion) ? { agentVersion: options.agentVersion.trim() } : {},
45
- ...options.useGenAIOpenTelemetry !== void 0 ? { useGenAIOpenTelemetry: options.useGenAIOpenTelemetry } : {},
46
- ...options.telemetryIntegrations ? { telemetryIntegrations: options.telemetryIntegrations } : {},
47
- ...options.useGlobalTelemetryIntegrations !== void 0 ? {
48
- useGlobalTelemetryIntegrations: options.useGlobalTelemetryIntegrations
49
- } : {},
50
- ...Object.keys(spanAttributes).length > 0 ? { spanAttributes } : {}
51
- };
47
+ const stringValue = String(value).trim();
48
+ if (!stringValue) {
49
+ return;
50
+ }
51
+ target[key] = stringValue;
52
+ }
53
+ function isNonEmpty(value) {
54
+ return typeof value === "string" && value.trim().length > 0;
55
+ }
56
+ function firstNonEmpty(...values) {
57
+ for (const value of values) {
58
+ if (value === null || value === void 0) {
59
+ continue;
60
+ }
61
+ const stringValue = String(value).trim();
62
+ if (stringValue) {
63
+ return stringValue;
64
+ }
65
+ }
66
+ return void 0;
52
67
  }
68
+ function readPath(value, path) {
69
+ let current = value;
70
+ for (const segment of path) {
71
+ if (!isRecord(current)) {
72
+ return void 0;
73
+ }
74
+ current = current[segment];
75
+ }
76
+ return firstNonEmpty(
77
+ typeof current === "string" || typeof current === "number" || typeof current === "boolean" ? current : void 0
78
+ );
79
+ }
80
+ function parseServiceEndpoint(serviceUrl) {
81
+ if (!isNonEmpty(serviceUrl)) {
82
+ return void 0;
83
+ }
84
+ try {
85
+ const url = new URL(serviceUrl);
86
+ return {
87
+ serverAddress: url.hostname,
88
+ ...url.port ? { serverPort: Number(url.port) } : {}
89
+ };
90
+ } catch {
91
+ return void 0;
92
+ }
93
+ }
94
+ function callOptionalStringMethod(method) {
95
+ if (!method) {
96
+ return void 0;
97
+ }
98
+ try {
99
+ return firstNonEmpty(method());
100
+ } catch {
101
+ return void 0;
102
+ }
103
+ }
104
+ function callOptionalBooleanMethod(method) {
105
+ if (!method) {
106
+ return false;
107
+ }
108
+ try {
109
+ return method() === true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ function isRecord(value) {
115
+ return typeof value === "object" && value !== null;
116
+ }
117
+
118
+ // src/baggage.ts
53
119
  function buildA365BaggagePairs(context) {
54
120
  const pairs = {};
121
+ for (const [key, value] of Object.entries(context.extraBaggage ?? {})) {
122
+ setPair(pairs, key, value);
123
+ }
55
124
  setPair(pairs, A365_BAGGAGE_KEYS.tenantId, context.tenantId);
56
125
  setPair(pairs, A365_BAGGAGE_KEYS.agentId, context.agentId);
57
126
  setPair(pairs, A365_BAGGAGE_KEYS.agentName, context.agentName);
@@ -101,23 +170,46 @@ function buildA365BaggagePairs(context) {
101
170
  setPair(pairs, A365_BAGGAGE_KEYS.serviceName, context.serviceName);
102
171
  setPair(pairs, A365_BAGGAGE_KEYS.serverAddress, context.serverAddress);
103
172
  setPair(pairs, A365_BAGGAGE_KEYS.serverPort, context.serverPort);
104
- for (const [key, value] of Object.entries(context.extraBaggage ?? {})) {
105
- setPair(pairs, key, value);
106
- }
107
173
  return pairs;
108
174
  }
175
+
176
+ // src/tracing-config.ts
177
+ function createA365TracingConfig(options = {}) {
178
+ const spanAttributes = {
179
+ ...options.spanAttributes ?? {}
180
+ };
181
+ if (isNonEmpty(options.tenantId)) {
182
+ spanAttributes[A365_BAGGAGE_KEYS.tenantId] = options.tenantId.trim();
183
+ }
184
+ return {
185
+ ...isNonEmpty(options.agentId) ? { agentId: options.agentId.trim() } : {},
186
+ ...isNonEmpty(options.agentDescription) ? { agentDescription: options.agentDescription.trim() } : {},
187
+ ...isNonEmpty(options.agentVersion) ? { agentVersion: options.agentVersion.trim() } : {},
188
+ ...options.useGenAIOpenTelemetry !== void 0 ? { useGenAIOpenTelemetry: options.useGenAIOpenTelemetry } : {},
189
+ ...options.telemetryIntegrations ? { telemetryIntegrations: options.telemetryIntegrations } : {},
190
+ ...options.useGlobalTelemetryIntegrations !== void 0 ? {
191
+ useGlobalTelemetryIntegrations: options.useGlobalTelemetryIntegrations
192
+ } : {},
193
+ ...Object.keys(spanAttributes).length > 0 ? { spanAttributes } : {}
194
+ };
195
+ }
196
+
197
+ // src/turn-context.ts
109
198
  function createA365ContextFromTurnContext(turnContext, options = {}) {
110
199
  const { serviceUrl, useRecipientIdAsAgentId, ...requestOptions } = options;
111
200
  const activity = turnContext.activity ?? {};
112
201
  const channelData = activity.channelData;
113
202
  const endpoint = parseServiceEndpoint(serviceUrl ?? activity.serviceUrl);
203
+ const isAgenticRequest = callOptionalBooleanMethod(activity.isAgenticRequest);
114
204
  const agentId = firstNonEmpty(
115
205
  requestOptions.agentId,
206
+ isAgenticRequest ? callOptionalStringMethod(activity.getAgenticInstanceId) : void 0,
116
207
  activity.recipient?.agenticAppId,
117
208
  useRecipientIdAsAgentId ? activity.recipient?.id : void 0
118
209
  );
119
210
  const tenantId = firstNonEmpty(
120
211
  requestOptions.tenantId,
212
+ callOptionalStringMethod(activity.getAgenticTenantId),
121
213
  activity.recipient?.tenantId,
122
214
  activity.conversation?.tenantId,
123
215
  readPath(channelData, ["tenant", "id"]),
@@ -133,12 +225,43 @@ function createA365ContextFromTurnContext(turnContext, options = {}) {
133
225
  activity.recipient?.name
134
226
  )
135
227
  } : {},
228
+ // Mirrors Microsoft's A365 observability-hosting helper, which maps
229
+ // recipient.role to gen_ai.agent.description.
230
+ ...firstNonEmpty(requestOptions.agentDescription, activity.recipient?.role) ? {
231
+ agentDescription: firstNonEmpty(
232
+ requestOptions.agentDescription,
233
+ activity.recipient?.role
234
+ )
235
+ } : {},
236
+ ...firstNonEmpty(requestOptions.agentAuid, activity.recipient?.aadObjectId) ? {
237
+ agentAuid: firstNonEmpty(
238
+ requestOptions.agentAuid,
239
+ activity.recipient?.aadObjectId
240
+ )
241
+ } : {},
242
+ ...firstNonEmpty(
243
+ requestOptions.agentBlueprintId,
244
+ activity.recipient?.agenticAppBlueprintId
245
+ ) ? {
246
+ agentBlueprintId: firstNonEmpty(
247
+ requestOptions.agentBlueprintId,
248
+ activity.recipient?.agenticAppBlueprintId
249
+ )
250
+ } : {},
136
251
  ...firstNonEmpty(requestOptions.conversationId, activity.conversation?.id) ? {
137
252
  conversationId: firstNonEmpty(
138
253
  requestOptions.conversationId,
139
254
  activity.conversation?.id
140
255
  )
141
256
  } : {},
257
+ // Mirrors Microsoft's A365 observability-hosting helper, which maps the
258
+ // Activity serviceUrl to microsoft.conversation.item.link.
259
+ ...firstNonEmpty(requestOptions.conversationItemLink, activity.serviceUrl) ? {
260
+ conversationItemLink: firstNonEmpty(
261
+ requestOptions.conversationItemLink,
262
+ activity.serviceUrl
263
+ )
264
+ } : {},
142
265
  ...firstNonEmpty(
143
266
  requestOptions.sessionId,
144
267
  requestOptions.conversationId,
@@ -156,6 +279,12 @@ function createA365ContextFromTurnContext(turnContext, options = {}) {
156
279
  activity.channelId
157
280
  )
158
281
  } : {},
282
+ ...firstNonEmpty(requestOptions.channelLink, activity.channelIdSubChannel) ? {
283
+ channelLink: firstNonEmpty(
284
+ requestOptions.channelLink,
285
+ activity.channelIdSubChannel
286
+ )
287
+ } : {},
159
288
  ...firstNonEmpty(
160
289
  requestOptions.userId,
161
290
  activity.from?.aadObjectId,
@@ -174,10 +303,13 @@ function createA365ContextFromTurnContext(turnContext, options = {}) {
174
303
  ...firstNonEmpty(requestOptions.userName, activity.from?.name) ? {
175
304
  userName: firstNonEmpty(requestOptions.userName, activity.from?.name)
176
305
  } : {},
306
+ // Mirrors Microsoft's A365 observability-hosting helper, which treats
307
+ // agenticUserId as the caller UPN/email value.
177
308
  ...firstNonEmpty(
178
309
  requestOptions.userEmail,
179
310
  activity.from?.email,
180
311
  activity.from?.userPrincipalName,
312
+ activity.from?.agenticUserId,
181
313
  readPath(channelData, ["from", "userPrincipalName"]),
182
314
  readPath(turnContext.identity, ["preferred_username"]),
183
315
  readPath(turnContext.identity, ["upn"])
@@ -186,28 +318,30 @@ function createA365ContextFromTurnContext(turnContext, options = {}) {
186
318
  requestOptions.userEmail,
187
319
  activity.from?.email,
188
320
  activity.from?.userPrincipalName,
321
+ activity.from?.agenticUserId,
189
322
  readPath(channelData, ["from", "userPrincipalName"]),
190
323
  readPath(turnContext.identity, ["preferred_username"]),
191
324
  readPath(turnContext.identity, ["upn"])
192
325
  )
193
326
  } : {},
327
+ ...firstNonEmpty(
328
+ requestOptions.callerAgentBlueprintId,
329
+ activity.from?.agenticAppBlueprintId
330
+ ) ? {
331
+ callerAgentBlueprintId: firstNonEmpty(
332
+ requestOptions.callerAgentBlueprintId,
333
+ activity.from?.agenticAppBlueprintId
334
+ )
335
+ } : {},
194
336
  ...requestOptions.serverAddress ? { serverAddress: requestOptions.serverAddress } : endpoint?.serverAddress ? { serverAddress: endpoint.serverAddress } : {},
195
337
  ...requestOptions.serverPort !== void 0 ? { serverPort: requestOptions.serverPort } : endpoint?.serverPort !== void 0 ? { serverPort: endpoint.serverPort } : {}
196
338
  };
197
339
  }
198
- async function runWithA365TurnContext(turnContext, optionsOrFn, maybeFn) {
199
- const options = typeof optionsOrFn === "function" ? {} : optionsOrFn ?? {};
200
- const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
201
- if (!fn) {
202
- throw new Error("runWithA365TurnContext requires a callback.");
203
- }
204
- return await runWithA365Context(
205
- createA365ContextFromTurnContext(turnContext, options),
206
- fn
207
- );
208
- }
340
+
341
+ // src/lifecycle.ts
342
+ var OBSERVABILITY_PACKAGE = "@microsoft/agents-a365-observability";
209
343
  async function initA365Observability(options) {
210
- const module = await loadObservabilityModule();
344
+ const module = await loadObservabilityModule(options);
211
345
  const configProvider = createConfigurationProvider(
212
346
  module,
213
347
  options.configuration
@@ -227,7 +361,7 @@ async function initA365Observability(options) {
227
361
  builder2.withExporterOptions?.(options.exporterOptions);
228
362
  }
229
363
  if (options.customLogger) {
230
- builder2.withCustomLogger?.(options.customLogger);
364
+ builder2.withCustomLogger?.(normalizeCustomLogger(options.customLogger));
231
365
  }
232
366
  if (configProvider) {
233
367
  builder2.withConfigurationProvider?.(configProvider);
@@ -238,8 +372,8 @@ async function initA365Observability(options) {
238
372
  shutdown: () => module.ObservabilityManager.shutdown()
239
373
  };
240
374
  }
241
- async function runWithA365Context(requestContext, fn) {
242
- const module = await loadObservabilityModule();
375
+ async function runWithA365Context(requestContext, fn, options = {}) {
376
+ const module = await loadObservabilityModule(options);
243
377
  const runWithBaggage = () => {
244
378
  const scope = new module.BaggageBuilder().setPairs(buildA365BaggagePairs(requestContext)).build();
245
379
  return scope.run(fn);
@@ -247,15 +381,18 @@ async function runWithA365Context(requestContext, fn) {
247
381
  const result = requestContext.exportToken && module.runWithExportToken ? module.runWithExportToken(requestContext.exportToken, runWithBaggage) : runWithBaggage();
248
382
  return await result;
249
383
  }
250
- async function updateA365ExportToken(token) {
251
- const module = await loadObservabilityModule();
384
+ async function updateA365ExportToken(token, options = {}) {
385
+ const module = await loadObservabilityModule(options);
252
386
  return module.updateExportToken?.(token) ?? false;
253
387
  }
254
- async function loadObservabilityModule() {
388
+ async function loadObservabilityModule(options = {}) {
255
389
  try {
390
+ if (options.getObservabilityModule) {
391
+ return await options.getObservabilityModule();
392
+ }
256
393
  return await import(OBSERVABILITY_PACKAGE);
257
394
  } catch (error) {
258
- throw new Error(
395
+ throw new A365ObservabilityModuleLoadError(
259
396
  `Unable to load ${OBSERVABILITY_PACKAGE}. Install @microsoft/agents-a365-observability and @microsoft/agents-a365-runtime before using @cuylabs/agent-a365-observability.`,
260
397
  { cause: error }
261
398
  );
@@ -285,61 +422,30 @@ function createConfigurationProvider(module, configuration) {
285
422
  getConfiguration: () => new module.ObservabilityConfiguration(overrides)
286
423
  };
287
424
  }
288
- function setPair(target, key, value) {
289
- if (value === null || value === void 0) {
290
- return;
291
- }
292
- const stringValue = String(value).trim();
293
- if (!stringValue) {
294
- return;
295
- }
296
- target[key] = stringValue;
297
- }
298
- function isNonEmpty(value) {
299
- return typeof value === "string" && value.trim().length > 0;
300
- }
301
- function firstNonEmpty(...values) {
302
- for (const value of values) {
303
- if (value === null || value === void 0) {
304
- continue;
305
- }
306
- const stringValue = String(value).trim();
307
- if (stringValue) {
308
- return stringValue;
309
- }
310
- }
311
- return void 0;
425
+ function normalizeCustomLogger(customLogger) {
426
+ return {
427
+ ...customLogger,
428
+ event: customLogger.event ?? (() => {
429
+ })
430
+ };
312
431
  }
313
- function readPath(value, path) {
314
- let current = value;
315
- for (const segment of path) {
316
- if (!isRecord(current)) {
317
- return void 0;
318
- }
319
- current = current[segment];
432
+
433
+ // src/turn-runner.ts
434
+ async function runWithA365TurnContext(turnContext, optionsOrFn, maybeFnOrRuntime, maybeRuntime) {
435
+ const options = typeof optionsOrFn === "function" ? {} : optionsOrFn ?? {};
436
+ const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFnOrRuntime;
437
+ const runtimeOptions = typeof optionsOrFn === "function" ? typeof maybeFnOrRuntime === "function" ? void 0 : maybeFnOrRuntime : maybeRuntime;
438
+ if (typeof fn !== "function") {
439
+ throw new Error("runWithA365TurnContext requires a callback.");
320
440
  }
321
- return firstNonEmpty(
322
- typeof current === "string" || typeof current === "number" || typeof current === "boolean" ? current : void 0
441
+ return await runWithA365Context(
442
+ createA365ContextFromTurnContext(turnContext, options),
443
+ fn,
444
+ runtimeOptions
323
445
  );
324
446
  }
325
- function parseServiceEndpoint(serviceUrl) {
326
- if (!isNonEmpty(serviceUrl)) {
327
- return void 0;
328
- }
329
- try {
330
- const url = new URL(serviceUrl);
331
- return {
332
- serverAddress: url.hostname,
333
- ...url.port ? { serverPort: Number(url.port) } : {}
334
- };
335
- } catch {
336
- return void 0;
337
- }
338
- }
339
- function isRecord(value) {
340
- return typeof value === "object" && value !== null;
341
- }
342
447
  export {
448
+ A365ObservabilityModuleLoadError,
343
449
  A365_BAGGAGE_KEYS,
344
450
  buildA365BaggagePairs,
345
451
  createA365ContextFromTurnContext,
package/docs/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Agent 365 Observability Docs
2
+
3
+ `@cuylabs/agent-a365-observability` connects agent-core's portable
4
+ OpenTelemetry spans to Microsoft's Agent 365 observability SDK.
5
+
6
+ Read these files in this order:
7
+
8
+ 1. [architecture.md](./architecture.md)
9
+ Explains the layer split and why this package exists outside agent-core.
10
+ 2. [agent-core-otel.md](./agent-core-otel.md)
11
+ Explains what agent-core emits and how this package feeds that telemetry.
12
+ 3. [microsoft-a365-observability.md](./microsoft-a365-observability.md)
13
+ Explains what Microsoft's SDK owns: exporter startup, baggage enrichment,
14
+ export tokens, and trace propagation utilities.
15
+ 4. [lifecycle-and-limits.md](./lifecycle-and-limits.md)
16
+ Explains startup order, per-request baggage, session/conversation behavior,
17
+ and v0 limits.
18
+
19
+ The short version:
20
+
21
+ - agent-core owns platform-neutral spans for agent turns, tools, and model
22
+ calls.
23
+ - `agent-channel-m365` owns Activity ingress and turn wrapping for M365 hosts.
24
+ - Microsoft's Agent 365 observability SDK owns exporter registration and
25
+ baggage-to-span enrichment.
26
+ - this package starts Microsoft observability and wraps each agent turn with
27
+ the Agent 365 baggage that Microsoft expects.
@@ -0,0 +1,68 @@
1
+ # Agent-Core OpenTelemetry Integration
2
+
3
+ This package builds on agent-core's existing tracing support. It does not
4
+ replace agent-core telemetry and it does not create agent-core spans itself.
5
+
6
+ ## What Agent-Core Emits
7
+
8
+ agent-core's OpenTelemetry middleware emits:
9
+
10
+ - `invoke_agent` spans for agent turns;
11
+ - `execute_tool` spans for tool calls;
12
+ - AI SDK GenAI spans for model calls when AI SDK telemetry is enabled.
13
+
14
+ The root agent span includes common GenAI attributes such as:
15
+
16
+ - `gen_ai.operation.name`
17
+ - `gen_ai.agent.name`
18
+ - `gen_ai.agent.id`
19
+ - `gen_ai.agent.description`
20
+ - `gen_ai.agent.version`
21
+ - `gen_ai.input.messages`
22
+ - `gen_ai.output.messages`
23
+ - `gen_ai.usage.input_tokens`
24
+ - `gen_ai.usage.output_tokens`
25
+
26
+ `createA365TracingConfig(...)` returns the agent-core tracing fragment for
27
+ stable agent metadata. Per-request metadata belongs in baggage via
28
+ `runWithA365Context(...)`.
29
+
30
+ ## What This Package Adds
31
+
32
+ This package wraps the turn in Agent 365 baggage before agent-core starts its
33
+ spans. Microsoft's baggage processor then copies that baggage into span
34
+ attributes.
35
+
36
+ Common copied attributes include:
37
+
38
+ - `microsoft.tenant.id`
39
+ - `microsoft.session.id`
40
+ - `microsoft.a365.agent.blueprint.id`
41
+ - `microsoft.channel.name`
42
+ - `microsoft.channel.link`
43
+ - `user.id`
44
+ - `user.name`
45
+ - `user.email`
46
+
47
+ For `invoke_agent` spans, Microsoft's processor also copies Agent 365
48
+ agent/caller dimensions when agent-core has not already set the same key.
49
+
50
+ ## Attribute Precedence
51
+
52
+ Microsoft's baggage processor does not overwrite attributes that already exist
53
+ on a span.
54
+
55
+ That matters for a few keys:
56
+
57
+ - agent-core always sets `gen_ai.agent.name` from `createAgent({ name })`;
58
+ - agent-core sets `gen_ai.conversation.id` from the agent-core session ID;
59
+ - `createA365TracingConfig({ agentId, agentDescription, agentVersion })`
60
+ causes agent-core to set those keys before baggage is copied.
61
+
62
+ For normal `@cuylabs/agent-channel-m365` usage this is fine because the default
63
+ session strategy uses the M365 `conversation.id` as the agent-core session ID.
64
+ If you use a custom M365 session mapping, `gen_ai.conversation.id` will reflect
65
+ the custom agent-core session ID, not the original M365 conversation ID.
66
+
67
+ Use `microsoft.session.id` for the Agent 365 session dimension and keep custom
68
+ session mapping in mind when analyzing exported spans.
@@ -0,0 +1,57 @@
1
+ # Architecture
2
+
3
+ This package exists to keep agent-core platform-neutral while allowing an
4
+ M365-hosted agent to export Agent 365-shaped telemetry.
5
+
6
+ ## Layer Split
7
+
8
+ | Layer | Owns | Does not own |
9
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
10
+ | `@cuylabs/agent-core` | Agent runtime, model loop, tool execution, OpenTelemetry span emission | Microsoft exporter startup, Agent 365 baggage, token resolution |
11
+ | `@cuylabs/agent-channel-m365` | M365 Activity/CloudAdapter ingress, session mapping, optional per-turn observability wrapper | Exporter lifecycle or OpenTelemetry provider registration |
12
+ | `@microsoft/agents-a365-observability` | Agent 365 exporter, baggage builder, baggage span processor, per-request export token context | agent-core tracing config or channel message handling |
13
+ | `@cuylabs/agent-a365-observability` | Glue that starts Microsoft observability, produces agent-core tracing config, and wraps turns with A365 baggage | Model provider, chat transport, Microsoft SDK internals |
14
+
15
+ The important boundary is that agent-core never imports Microsoft SDK types.
16
+ Microsoft-specific telemetry behavior lives in this adapter and the Microsoft
17
+ SDK.
18
+
19
+ ## Runtime Flow
20
+
21
+ At startup:
22
+
23
+ 1. the host calls `initA365Observability(...)`;
24
+ 2. this package loads Microsoft's observability SDK;
25
+ 3. Microsoft registers its OpenTelemetry provider/export processor;
26
+ 4. Microsoft registers a span processor that copies baggage into span
27
+ attributes.
28
+
29
+ Per agent:
30
+
31
+ 1. the host calls `createAgent({ tracing: createA365TracingConfig(...) })`;
32
+ 2. agent-core installs its normal OpenTelemetry middleware;
33
+ 3. agent-core emits `invoke_agent`, `execute_tool`, and AI SDK model spans.
34
+
35
+ Per turn:
36
+
37
+ 1. the host or M365 channel calls `runWithA365Context(...)` or
38
+ `runWithA365TurnContext(...)`;
39
+ 2. this package builds Agent 365 baggage from tenant, agent, conversation,
40
+ channel, and caller identity;
41
+ 3. agent-core runs the turn inside that baggage scope;
42
+ 4. Microsoft's span processor copies relevant baggage onto each span at span
43
+ start;
44
+ 5. Microsoft's exporter sends the final span shape to Agent 365.
45
+
46
+ ## Why Not Put This In Agent-Core?
47
+
48
+ Agent 365 observability has Microsoft-specific concepts:
49
+
50
+ - Agent 365 tenant and agent identity baggage;
51
+ - Microsoft exporter token resolution;
52
+ - Microsoft per-request export token context;
53
+ - M365 `TurnContext` identity helpers;
54
+ - Agent 365 trace propagation utilities.
55
+
56
+ Putting those in agent-core would make core platform-specific. This adapter
57
+ keeps the runtime portable while still supporting Agent 365 deployment.
@@ -0,0 +1,118 @@
1
+ # Lifecycle And Limits
2
+
3
+ This package is intentionally small for v0. It initializes Microsoft
4
+ observability once, then wraps each turn with Agent 365 baggage.
5
+
6
+ ## Startup Order
7
+
8
+ Call `initA365Observability(...)` during host startup, before the first
9
+ agent-core turn emits spans.
10
+
11
+ ```ts
12
+ const observability = await initA365Observability({
13
+ serviceName: "email-agent-service",
14
+ tokenResolver: async (agentId, tenantId) =>
15
+ getObservabilityToken(agentId, tenantId),
16
+ });
17
+ ```
18
+
19
+ The returned handle shuts down the Microsoft observability manager:
20
+
21
+ ```ts
22
+ await observability.shutdown();
23
+ ```
24
+
25
+ Microsoft's current `ObservabilityManager` API exposes shutdown, not a separate
26
+ public flush method. When Microsoft's builder creates its own OpenTelemetry
27
+ `NodeSDK`, `shutdown()` delegates to that SDK. When Microsoft's builder detects
28
+ an existing global tracer provider, it attaches its processors to that provider
29
+ and does not own provider shutdown; the host that created the global provider is
30
+ responsible for flushing or shutting it down.
31
+
32
+ ## Per-Request Baggage
33
+
34
+ Use `runWithA365Context(...)` when you already have tenant, agent, channel, and
35
+ caller identity:
36
+
37
+ ```ts
38
+ await runWithA365Context(
39
+ {
40
+ tenantId,
41
+ agentId,
42
+ sessionId,
43
+ conversationId,
44
+ channelName: "msteams",
45
+ },
46
+ () => agent.send(sessionId, message),
47
+ );
48
+ ```
49
+
50
+ Use `runWithA365TurnContext(...)` when you have a Microsoft `TurnContext`.
51
+ The helper reads common Activity fields and optional Agent 365 Activity methods
52
+ such as `getAgenticTenantId()` and `getAgenticInstanceId()`.
53
+
54
+ ## Extra Baggage
55
+
56
+ Use `extraBaggage` for additional span attributes that are not part of the
57
+ standard Agent 365 baggage set:
58
+
59
+ ```ts
60
+ await runWithA365Context(
61
+ {
62
+ tenantId,
63
+ agentId,
64
+ extraBaggage: {
65
+ "deployment.environment.name": "prod",
66
+ "custom.routing.bucket": "soc",
67
+ },
68
+ },
69
+ () => agent.send(sessionId, message),
70
+ );
71
+ ```
72
+
73
+ Structured Agent 365 fields win over `extraBaggage` conflicts. For example, if
74
+ `tenantId` is set and `extraBaggage` also contains `microsoft.tenant.id`, the
75
+ structured `tenantId` value is used. This prevents ad hoc baggage from
76
+ accidentally or deliberately spoofing core tenant, agent, conversation, and user
77
+ identity dimensions.
78
+
79
+ ## Per-Request Export Tokens
80
+
81
+ Batch export usually uses `tokenResolver` from `initA365Observability(...)`.
82
+
83
+ For Microsoft's per-request export mode, pass `exportToken` in the request
84
+ context:
85
+
86
+ ```ts
87
+ await runWithA365Context(
88
+ {
89
+ tenantId,
90
+ agentId,
91
+ exportToken,
92
+ },
93
+ () => agent.send(sessionId, message),
94
+ );
95
+ ```
96
+
97
+ ## M365 Session Mapping
98
+
99
+ `@cuylabs/agent-channel-m365` defaults to using M365 `conversation.id` as the
100
+ agent-core session ID. That keeps agent-core's `gen_ai.conversation.id` aligned
101
+ with Agent 365 conversation identity.
102
+
103
+ If you use a custom session mapping, agent-core will set
104
+ `gen_ai.conversation.id` to your custom session ID. Microsoft's baggage
105
+ processor will not overwrite that attribute with Agent 365 baggage. This is a
106
+ known v0 limit and should be considered when analyzing spans.
107
+
108
+ ## v0 Limits
109
+
110
+ v0 does not expose:
111
+
112
+ - a separate Microsoft `OutputScope` span;
113
+ - agent-to-agent HTTP trace propagation wrappers;
114
+ - custom span processors for overriding agent-core attributes;
115
+ - real-time threat protection or chat-history submission middleware.
116
+
117
+ Those features are additive and should be introduced only when a real Agent 365
118
+ deployment needs them.
@@ -0,0 +1,86 @@
1
+ # Microsoft Agent 365 Observability SDK
2
+
3
+ This package delegates exporter and baggage behavior to Microsoft's
4
+ `@microsoft/agents-a365-observability` SDK.
5
+
6
+ ## What Microsoft Owns
7
+
8
+ Microsoft's SDK owns:
9
+
10
+ - OpenTelemetry provider/export processor registration;
11
+ - the Agent 365 span exporter;
12
+ - batch export and per-request export token modes;
13
+ - `BaggageBuilder` and `BaggageScope`;
14
+ - the span processor that copies baggage into span attributes;
15
+ - Agent 365 scope classes such as `InvokeAgentScope`, `ExecuteToolScope`,
16
+ `InferenceScope`, and `OutputScope`;
17
+ - trace propagation helpers for agent-to-agent HTTP calls.
18
+
19
+ This package intentionally does not reimplement those pieces.
20
+
21
+ ## Why This Package Still Exists
22
+
23
+ Microsoft's SDK is platform-level observability infrastructure. It does not
24
+ know how a cuylabs agent is configured or how agent-core emits spans.
25
+
26
+ This package answers the integration question:
27
+
28
+ > How does an agent-core agent participate in Microsoft Agent 365
29
+ > observability without making agent-core depend on Microsoft SDK types?
30
+
31
+ The answer is:
32
+
33
+ 1. initialize Microsoft's SDK once at host startup;
34
+ 2. pass stable tracing config into `createAgent()`;
35
+ 3. wrap each request with Agent 365 baggage before `agent.chat()` runs.
36
+
37
+ ## Scope Classes
38
+
39
+ Microsoft exposes explicit scope classes for several span shapes. agent-core
40
+ already emits equivalent agent and tool spans, and AI SDK telemetry covers model
41
+ spans.
42
+
43
+ The main shape difference is `OutputScope`, which creates a separate
44
+ `output_messages` span. agent-core currently records output messages on the
45
+ `invoke_agent` span through `gen_ai.output.messages`.
46
+
47
+ The v0 adapter does not emit a separate `OutputScope`. If a future Agent 365
48
+ dashboard requires the separate `output_messages` span, this should be added as
49
+ an adapter feature or an agent-core telemetry extension point, not by importing
50
+ Microsoft scope classes into agent-core.
51
+
52
+ ## Trace Propagation
53
+
54
+ Microsoft also exports helpers such as:
55
+
56
+ - `runWithParentSpanRef`
57
+ - `extractContextFromHeaders`
58
+ - `injectContextToHeaders`
59
+
60
+ Those are useful for agent-to-agent HTTP trace continuation. v0 does not wrap
61
+ them. Consumers that need A2A trace propagation can import them directly from
62
+ Microsoft's SDK until a real cuylabs integration requires a first-class helper.
63
+
64
+ ## TurnContext Mapping Parity
65
+
66
+ `runWithA365TurnContext(...)` intentionally mirrors Microsoft's
67
+ `agents-a365-observability-hosting` TurnContext helpers for fields that can
68
+ look surprising outside the Agent 365 activity model:
69
+
70
+ - `activity.serviceUrl` maps to `microsoft.conversation.item.link`.
71
+ - `activity.recipient.role` maps to `gen_ai.agent.description`.
72
+ - `activity.from.agenticUserId` maps to `user.email`.
73
+ - `activity.channelIdSubChannel` maps to `microsoft.channel.link`.
74
+
75
+ Those mappings come from Microsoft's hosting helper behavior, not from generic
76
+ Bot Framework naming. Prefer explicit options when your host has more precise
77
+ values, for example a real Teams message deep link for
78
+ `conversationItemLink`.
79
+
80
+ Caller-agent attributes and some host-owned agent attributes are not
81
+ extractable from a generic TurnContext shape. Fields such as `agentEmail`,
82
+ `agentPlatformId`, `sessionDescription`, `callerClientIp`, `callerAgentId`,
83
+ `callerAgentName`, `callerAgentAuid`, `callerAgentEmail`,
84
+ `callerAgentPlatformId`, and `callerAgentVersion` are only populated when the
85
+ host passes them explicitly through `runWithA365Context(...)` or the options
86
+ argument to `runWithA365TurnContext(...)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuylabs/agent-a365-observability",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Microsoft Agent 365 observability adapter for @cuylabs/agent-core",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,18 +14,19 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
- "examples",
17
+ "docs",
18
18
  "README.md"
19
19
  ],
20
20
  "devDependencies": {
21
21
  "@ai-sdk/openai": "4.0.0-beta.38",
22
22
  "@types/node": "^22.0.0",
23
23
  "ai": "7.0.0-beta.111",
24
+ "dotenv": "^17.2.3",
24
25
  "tsup": "^8.0.0",
25
26
  "typescript": "^5.7.0",
26
27
  "vitest": "^4.0.18",
27
28
  "zod": "^3.25.76 || ^4.1.8",
28
- "@cuylabs/agent-core": "^3.0.0"
29
+ "@cuylabs/agent-core": "^3.1.0"
29
30
  },
30
31
  "peerDependencies": {
31
32
  "@cuylabs/agent-core": "^3.0.0",
@@ -1,115 +0,0 @@
1
- /**
2
- * 01 — Microsoft Agent 365 Observability
3
- *
4
- * Similar goal to agent-core's Phoenix/Zipkin examples, but A365 owns the
5
- * OpenTelemetry exporter setup and requires per-request tenant/agent identity
6
- * through baggage.
7
- *
8
- * Simplified span hierarchy:
9
- *
10
- * invoke_agent calculator-bot (agent-core turn span)
11
- * ├── invoke_agent gpt-4o-mini (AI SDK v7 / @ai-sdk/otel)
12
- * │ └── chat gpt-4o-mini (model request)
13
- * ├── execute_tool calculator (agent-core tool span)
14
- * └── invoke_agent gpt-4o-mini (AI SDK final model span)
15
- *
16
- * Required env:
17
- * ENABLE_A365_OBSERVABILITY_EXPORTER=true
18
- * A365_AGENT_ID=...
19
- * A365_TENANT_ID=...
20
- * A365_OBSERVABILITY_TOKEN=... # demo token resolver only
21
- *
22
- * Run:
23
- * npx tsx examples/01-a365-observability.ts
24
- */
25
-
26
- import {
27
- createA365TracingConfig,
28
- initA365Observability,
29
- runWithA365Context,
30
- } from "@cuylabs/agent-a365-observability";
31
- import { createAgent, Tool } from "@cuylabs/agent-core";
32
- import { openai } from "@ai-sdk/openai";
33
- import { z } from "zod";
34
-
35
- const agentId = requiredEnv("A365_AGENT_ID");
36
- const tenantId = requiredEnv("A365_TENANT_ID");
37
-
38
- const observability = await initA365Observability({
39
- serviceName: "agent-a365-observability-example",
40
- serviceVersion: "1.0.0",
41
- configuration: {
42
- exporterEnabled: process.env.ENABLE_A365_OBSERVABILITY_EXPORTER === "true",
43
- logLevel: process.env.A365_OBSERVABILITY_LOG_LEVEL ?? "info|warn|error",
44
- },
45
- tokenResolver: async () => requiredEnv("A365_OBSERVABILITY_TOKEN"),
46
- });
47
-
48
- const calculator = Tool.define("calculator", {
49
- description: "Evaluate a math expression",
50
- parameters: z.object({
51
- expression: z.string().describe("Expression like '2 + 3 * 4'"),
52
- }),
53
- execute: async ({ expression }) => ({
54
- title: "Calculator",
55
- output: String(eval(expression)), // demo only
56
- }),
57
- });
58
-
59
- const agent = createAgent({
60
- name: "calculator-bot",
61
- model: openai("gpt-4o-mini"),
62
- tools: [calculator],
63
- systemPrompt:
64
- "You have a calculator tool. Use it to answer math questions. Be concise.",
65
- tracing: createA365TracingConfig({
66
- agentId,
67
- agentDescription: "Calculator example for Microsoft Agent 365 Observability",
68
- agentVersion: "1.0.0",
69
- }),
70
- });
71
-
72
- try {
73
- await runWithA365Context(
74
- {
75
- tenantId,
76
- agentId,
77
- agentName: "calculator-bot",
78
- agentDescription:
79
- "Calculator example for Microsoft Agent 365 Observability",
80
- agentVersion: "1.0.0",
81
- sessionId: "demo",
82
- conversationId: "demo",
83
- channelName: "local",
84
- },
85
- async () => {
86
- for await (const event of agent.chat(
87
- "demo",
88
- "What is 123 * 456 + 789?",
89
- )) {
90
- if (event.type === "text-delta") {
91
- process.stdout.write(event.text);
92
- }
93
- if (event.type === "tool-start") {
94
- console.log(`\n${event.toolName}(${JSON.stringify(event.input)})`);
95
- }
96
- if (event.type === "tool-result") {
97
- console.log(` ${event.result}`);
98
- }
99
- }
100
- },
101
- );
102
-
103
- console.log("\nSpans emitted with Agent 365 baggage.");
104
- } finally {
105
- await agent.close();
106
- await observability.shutdown();
107
- }
108
-
109
- function requiredEnv(name: string): string {
110
- const value = process.env[name];
111
- if (!value) {
112
- throw new Error(`${name} is required`);
113
- }
114
- return value;
115
- }