@cuylabs/agent-a365-observability
v4.4.0
Published
Microsoft Agent 365 observability adapter for @cuylabs/agent-core
Downloads
999
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.
Install
pnpm add @cuylabs/agent-a365-observability @microsoft/agents-a365-observability @microsoft/agents-a365-runtimeUsage
import { createAgent } from "@cuylabs/agent-core";
import {
createA365TracingConfig,
initA365Observability,
runWithA365Context,
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();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/agent-channel-m365, 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
The integration has three 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.
Use agent-core for portable OpenTelemetry spans:
invoke_agentagent spansexecute_tooltool spans- AI SDK model spans nested under the agent turn
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/agent-channel-m365 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, tool, 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.
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.
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 per-request export, pass exportToken to runWithA365Context() and enable per-request export in the Microsoft SDK configuration or environment.
v0 Limits
v0 does not expose:
- a separate Microsoft
OutputScopespan; - agent-to-agent HTTP trace propagation wrappers;
- real-time threat protection or chat-history submission middleware.
Those remain additive adapter features if an Agent 365 deployment needs them.
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.
