@cuylabs/agent-a365-observability
v7.4.0
Published
Microsoft Agent 365 observability adapter for @cuylabs/agent-core
Maintainers
Readme
@cuylabs/agent-a365-observability
Microsoft Agent 365 observability adapter for @cuylabs/agent-core.
This package connects agent-core's portable OpenTelemetry spans to Microsoft's Agent 365 observability SDK without putting Microsoft SDK types in agent-core.
It does four things:
- starts Microsoft Agent 365 Observability;
- wires the Agent 365 exporter token resolver;
- returns a small
createAgent({ tracing })config fragment for stable agent metadata; - wraps each request in Agent 365 baggage so spans include tenant, agent, conversation, channel, and caller identity.
For the deeper design, read docs/README.md. For the full SDK and auth flow, read docs/sdk-and-auth-flow.md.
Install
pnpm add @cuylabs/agent-a365-observability @microsoft/agents-a365-observability @microsoft/agents-a365-runtimeInstall @microsoft/agents-a365-observability-hosting as well when using the
M365/OBO token-cache helpers.
Dependency roles:
| Package | Required when | Notes |
| ---------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| @microsoft/agents-a365-observability | Always, when exporting to Agent 365 | Owns Microsoft's ObservabilityManager, exporter, baggage processor, trace propagation utilities, and optional OutputScope. |
| @microsoft/agents-a365-runtime | Always with Microsoft's observability SDK | Provides Microsoft runtime configuration types used by the SDK. |
| @microsoft/agents-a365-observability-hosting | Only for M365/OBO token-cache helpers | Provides AgenticTokenCache. It is an optional peer and is lazy-loaded only by OBO helpers. S2S App Service agents do not need it. |
Usage
import { createAgent } from "@cuylabs/agent-core";
import {
createA365ObservedTurnSource,
createA365S2STokenResolverFromEnv,
createA365TracingConfig,
initA365S2SObservability,
initA365Observability,
runWithA365Context,
runWithA365OutputMessages,
runWithA365TurnContext,
} from "@cuylabs/agent-a365-observability";
const observability = await initA365Observability({
serviceName: "email-agent-service",
serviceVersion: "1.0.0",
configuration: {
exporterEnabled: true,
logLevel: "info",
},
tokenResolver: async (agentId, tenantId) => {
return getObservabilityToken(agentId, tenantId);
},
});
const agent = createAgent({
name: "email-assistant",
model,
tracing: createA365TracingConfig({
agentId: "agent-456",
agentDescription: "Organizes email and calendar work",
agentVersion: "1.0.0",
}),
});
await runWithA365Context(
{
tenantId: "tenant-123",
agentId: "agent-456",
conversationId: "conversation-789",
sessionId: "session-789",
channelName: "msteams",
},
async () => {
for await (const event of agent.chat("session-789", "Find urgent email")) {
// stream events to your channel
}
},
);
await agent.close();
await observability.shutdown();When you are adapting a custom channel around AgentTurnSource, use
createA365ObservedTurnSource(...) instead of hand-rolling async-generator
wrapping. The helper keeps the full event stream inside the Agent 365 baggage
scope while letting the channel own its own metadata mapping:
const observedSource = createA365ObservedTurnSource({
source: agent,
context: ({ sessionId }) => ({
tenantId: "tenant-123",
agentId: "agent-456",
conversationId: sessionId,
sessionId,
channelName: "slack",
userId: "slack-user-id",
}),
});M365 / Teams bot turns
When your bot uses @microsoft/agents-hosting TurnContext, use the
TurnContext helper so tenant, agent, conversation, channel, and user identity
are derived from the incoming activity:
await runWithA365TurnContext(
context, // TurnContext from CloudAdapter
{
agentId: "agent-456",
agentDescription: "Organizes email and calendar work",
agentVersion: "1.0.0",
sessionId,
},
async () => {
for await (const event of agent.chat(sessionId, context.activity.text)) {
// stream events to the channel
}
},
);If you use @cuylabs/channel-m365-agent-core, this wrapper is built into the channel
adapter:
const m365 = createM365ChannelAdapter({
agent,
a365Observability: {
agentId: "agent-456",
agentDescription: "Organizes email and calendar work",
agentVersion: "1.0.0",
},
});The host process still needs to call initA365Observability(...) once during
startup so the Microsoft exporter and token resolver are registered.
Shape
This package is the @cuylabs/agent-core equivalent of a framework-specific
Agent 365 observability extension. Microsoft's OpenAI Agents and LangChain
extension packages patch those harnesses directly so their model/framework
spans flow through the Agent 365 ObservabilityManager. This package does the
same kind of adapter work at the agent-core layer: it starts the Microsoft
ObservabilityManager, feeds agent-core tracing metadata, and wraps each
agent-core turn with Agent 365 baggage.
The integration has four separate pieces:
initA365Observability(...)starts Microsoft's exporter and baggage span processor once at host startup.createA365TracingConfig(...)feeds agent-core'screateAgent({ tracing })contract.runWithA365Context(...)orrunWithA365TurnContext(...)binds per-request Agent 365 baggage beforeagent.chat()runs.createA365ObservedTurnSource(...)wraps a channel-neutralAgentTurnSourcewhen a host needs streaming-safe A365 baggage around every emitted event.createA365S2STokenResolver(...)provides a reusable service-to-service token resolver for Agent 365 Observability API export.
The source follows those same boundaries: auth/ owns Agent 365 S2S token
resolution, context/ owns baggage and TurnContext mapping, runtime/ owns
Microsoft SDK startup and lazy loading, and tracing/ owns the agent-core
tracing config adapter.
Use agent-core and AI SDK v7 for portable OpenTelemetry spans:
invoke_agentagent spans- AI SDK
chatmodel spans nested under the agent turn - AI SDK
execute_tooltool spans when tools run
Use this package for Agent 365-specific context on those spans:
microsoft.tenant.idgen_ai.agent.idmicrosoft.session.idgen_ai.conversation.id- channel and caller attributes
- A2A caller-agent attributes such as
microsoft.a365.caller.agent.id
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.
Use extraBaggage only for additional dimensions that are not part of the
standard Agent 365 identity set. Structured fields such as tenantId,
agentId, conversationId, and userId win over conflicting extraBaggage
keys.
Microsoft's baggage processor does not overwrite span attributes that
agent-core already set. In normal @cuylabs/channel-m365-agent-core usage this is
fine because the channel's default session strategy uses the M365
conversation.id as the agent-core session ID. If you use custom M365 session
mapping, gen_ai.conversation.id reflects the custom agent-core session ID.
See docs/agent-core-otel.md for details.
Phoenix vs Agent 365
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.
Agent 365 is different. The Microsoft SDK owns the exporter and adds an Agent 365 span processor that copies baggage into span attributes. agent-core still emits the agent and AI SDK spans, while this package starts the Microsoft exporter and wraps each request with the tenant, agent, conversation, channel, and caller baggage that Agent 365 expects.
AI SDK Telemetry
agent-core uses AI SDK v7 telemetry and enables @ai-sdk/otel's GenAIOpenTelemetry integration by default. That gives you current GenAI-shaped model spans such as invoke_agent, agent_step, chat, and execute_tool, with attributes like gen_ai.operation.name, gen_ai.provider.name, model IDs, and token usage.
This package does not replace that model telemetry. It adds the Microsoft Agent 365 layer: exporter startup, token resolution, and baggage that flows microsoft.tenant.id, gen_ai.agent.id, conversation, channel, and caller identity onto the spans.
createA365TracingConfig() defaults emitToolSpans to false. AI SDK v7 already emits execute_tool spans with the Agent 365 GenAI tool keys, including gen_ai.tool.name, gen_ai.tool.call.id, gen_ai.tool.call.arguments, and gen_ai.tool.call.result when content recording is enabled. Disabling agent-core's extra tool-span layer avoids duplicate execute_tool spans for one tool call while preserving model and tool observability from the AI SDK integration.
If you need to plug in another AI SDK telemetry integration, pass it through createA365TracingConfig():
const agent = createAgent({
name: "email-assistant",
model,
tracing: createA365TracingConfig({
agentId: "agent-456",
telemetryIntegrations: [myIntegration],
}),
});Set useGenAIOpenTelemetry: false only when you intentionally want to use global AI SDK telemetry integrations or another model-span integration.
Set emitToolSpans: true only when your agent execution path does not use AI SDK v7 tool telemetry and you need agent-core to emit tool spans itself.
TurnContext Mapping
runWithA365TurnContext(...) follows Microsoft's
agents-a365-observability-hosting helper behavior for M365 Activity fields.
Some mappings look surprising if read as generic Bot Framework fields:
activity.serviceUrlmaps tomicrosoft.conversation.item.link.activity.recipient.rolemaps togen_ai.agent.description.activity.from.agenticUserIdmaps touser.email.activity.channelIdSubChannelmaps tomicrosoft.channel.link.
Pass explicit runWithA365TurnContext options when your host has more precise
values, such as a real Teams message deep link for conversationItemLink.
Caller-agent attributes and host-owned fields such as agentEmail,
agentPlatformId, and callerClientIp are not inferred from TurnContext; pass
them explicitly through runWithA365Context() or the TurnContext options when
you need those dimensions.
Exporter Modes
For batch export, provide tokenResolver in initA365Observability().
For non-OBO service-to-service export, use initA365S2SObservability() when
the host should use the Agent 365 CLI generated environment values. The helper
understands both A365_OBSERVABILITY_* names and generated
agent365Observability__* names such as agent365Observability__tenantId,
agent365Observability__agentId, agent365Observability__clientId, and
agent365Observability__clientSecret.
await initA365S2SObservability({
serviceName: "email-agent-service",
serviceVersion: "1.0.0",
});The S2S initializer creates the token resolver, enables the Microsoft S2S exporter endpoint, and enables the exporter unless configuration overrides it.
Use createA365S2STokenResolverFromEnv() when you want the resolver only:
const tokenResolver = createA365S2STokenResolverFromEnv();
await initA365Observability({
serviceName: "email-agent-service",
tokenResolver,
exporterOptions: { useS2SEndpoint: true },
configuration: { exporterEnabled: true },
});Use createA365S2STokenResolver() when you want to pass values explicitly:
import {
createA365S2STokenResolver,
initA365Observability,
} from "@cuylabs/agent-a365-observability";
const tokenResolver = createA365S2STokenResolver({
tenantId: process.env.A365_OBSERVABILITY_TENANT_ID!,
agentId: process.env.A365_OBSERVABILITY_AGENT_ID!,
blueprintClientId: process.env.A365_OBSERVABILITY_CLIENT_ID!,
blueprintClientSecret: process.env.A365_OBSERVABILITY_CLIENT_SECRET,
useManagedIdentity:
process.env.A365_OBSERVABILITY_USE_MANAGED_IDENTITY === "true",
});
await initA365Observability({
serviceName: "email-agent-service",
tokenResolver,
exporterOptions: { useS2SEndpoint: true },
configuration: { exporterEnabled: true },
});Hosts that want Azure managed identity without taking an Azure dependency in
this package can pass managedIdentityAssertionProvider; the provider should
return an assertion token for api://AzureADTokenExchange.
For per-request export, pass exportToken to runWithA365Context() and enable
per-request export in startup configuration:
await initA365Observability({
serviceName: "email-agent-service",
tokenResolver,
configuration: {
exporterEnabled: true,
perRequest: {
enabled: true,
maxTraces: 1000,
maxSpansPerTrace: 5000,
maxConcurrentExports: 20,
flushGraceMs: 250,
maxTraceAgeMs: 30 * 60 * 1000,
},
},
});Microsoft's current per-request processor reads its tuning options from environment-backed configuration. This package applies these explicit startup values to the Microsoft environment variables before the SDK starts.
Trace Propagation
Use the trace propagation helpers when one agent calls another over HTTP or when async work needs to keep a parent span:
const headers = await injectA365TraceContextToHeaders({});
await fetch(agentUrl, { method: "POST", headers });
await runWithA365ExtractedTraceContext(req.headers, () =>
runWithA365Context(context, () => agent.chat(sessionId, text)),
);For queue or callback flows where you persisted the parent span ids, use
runWithA365ParentSpanRef(...).
Optional Output Spans
agent-core records output messages on the agent turn span. If an Agent 365
deployment needs Microsoft's separate output_messages span shape, wrap the
outgoing delivery path:
await runWithA365OutputMessages(
{
tenantId,
agentId,
conversationId,
channelName: "msteams",
messages: ["Done."],
},
() => sendMessage("Done."),
);This is opt-in because outgoing message content is recorded as telemetry.
OBO Token Cache
For M365/OBO hosts, use the OBO helpers around Microsoft's
AgenticTokenCache. This is separate from the S2S setup flow.
This path requires the optional
@microsoft/agents-a365-observability-hosting peer because AgenticTokenCache
lives in Microsoft's hosting package. The adapter does not load that package
for S2S startup, S2S token resolution, baggage wrapping, trace propagation, or
output spans. It is loaded only when an OBO helper is called.
await refreshA365OboObservabilityToken({
agentId,
tenantId,
turnContext,
authorization,
scopes: ["api://9b975845-388f-4429-889e-eab1ef63949c/.default"],
});
await initA365Observability({
serviceName: "email-agent-service",
tokenResolver: createA365OboTokenResolver(),
configuration: { exporterEnabled: true },
});Optional Features
The adapter now exposes the official additive pieces that do not duplicate agent-core telemetry:
- S2S Observability token resolution, including Agent 365 CLI env aliases;
- Microsoft trace context propagation helpers;
- per-request export token and processor configuration;
- optional Microsoft
OutputScopeoutput-message spans; - OBO token cache helpers for M365 per-user authorization.
Real-time threat protection and chat-history submission are outside observability and should live in separate Agent 365 tooling or security packages.
Examples
Examples live in the repository under
packages/agent-a365-observability/examples. They are monorepo-local reference
files and are not shipped in the npm tarball.
