@cuylabs/agent-a365-observability 2.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 +70 -11
- package/dist/index.d.ts +49 -21
- package/dist/index.js +197 -91
- package/docs/README.md +27 -0
- package/docs/agent-core-otel.md +68 -0
- package/docs/architecture.md +57 -0
- package/docs/lifecycle-and-limits.md +118 -0
- package/docs/microsoft-a365-observability.md +86 -0
- package/package.json +5 -4
- package/examples/01-a365-observability.ts +0 -115
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
declare function runWithA365TurnContext<T>(turnContext: A365TurnContextLike,
|
|
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/
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
322
|
-
|
|
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
|
+
"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,21 +14,22 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"
|
|
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": "^
|
|
29
|
+
"@cuylabs/agent-core": "^3.1.0"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
|
-
"@cuylabs/agent-core": "^
|
|
32
|
+
"@cuylabs/agent-core": "^3.0.0",
|
|
32
33
|
"ai": "7.0.0-beta.111",
|
|
33
34
|
"@microsoft/agents-a365-observability": ">=0.2.0-preview.5 <1.0.0",
|
|
34
35
|
"@microsoft/agents-a365-runtime": ">=0.2.0-preview.5 <1.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
|
-
}
|