npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cuylabs/agent-channel-m365

v5.2.0

Published

Microsoft 365 Activity Protocol adapter for @cuylabs/agent-core turn sources

Readme

@cuylabs/agent-channel-m365

Microsoft 365 channel adapter for @cuylabs/agent-core. It bridges agent-core chat turns into the Microsoft 365 Agents SDK transport layer for Teams and other Activity Protocol channels, with a clean path to Copilot-hosted scenarios that only need message turns.

How it works

This package bridges two systems that operate at different layers of the stack:

┌───────────────────────────────────────────────────┐
│  Microsoft 365 Channels                           │
│  Teams · Webchat · compatible Activity clients       │
└─────────────────────┬─────────────────────────────┘
                      │ Activity Protocol (HTTP + JWT)
┌─────────────────────▼─────────────────────────────┐
│  @microsoft/agents-hosting                        │
│  CloudAdapter → TurnContext → StreamingResponse    │
└─────────────────────┬─────────────────────────────┘
                      │ handler(context)
┌─────────────────────▼─────────────────────────────┐
│  @cuylabs/agent-channel-m365  ← this package      │
│  Maps Activity → agent.chat() → AgentEvent stream  │
│  → StreamingResponse.queueTextChunk()              │
└─────────────────────┬─────────────────────────────┘
                      │ agent.chat(sessionId, text)
┌─────────────────────▼─────────────────────────────┐
│  @cuylabs/agent-core                              │
│  LLM inference · tools · sessions · multi-agent    │
└───────────────────────────────────────────────────┘

The M365 SDK handles transport (channel auth, Activity protocol, JWT validation). Your agent-core handles AI orchestration (inference, tools, sessions). This package is the bridge between them.

Support Matrix

| Capability | Status | | ----------------------------------------------------------- | ---------------------------------------------------------------- | | Teams / Web Chat message turns | Supported | | Streaming text responses | Supported through StreamingResponse | | Conversation update / welcome messages | Supported | | Custom invoke handling | Supported through onInvoke / sendM365Invoke | | Event, reaction, message update/delete hooks | Supported | | Copilot-hosted plain message turns | Supported when the host calls the bot over the Activity protocol | | Adaptive card workflows / task modules / message extensions | Host-specific, app code required | | In-channel approval and human-input resolution | Not built-in yet; the adapter fails fast with a clear message | | Full Security Copilot plugin surface | Out of scope for this transport adapter |

Install

pnpm add @cuylabs/agent-channel-m365 @microsoft/agents-hosting express

Quick start

import { createAgent } from "@cuylabs/agent-core";
import { anthropic } from "@ai-sdk/anthropic";
import { mountM365Agent } from "@cuylabs/agent-channel-m365";

const agent = createAgent({
  model: anthropic("claude-sonnet-4-20250514"),
  tools: [
    /* your tools */
  ],
});

const { server } = await mountM365Agent(agent, {
  welcomeMessage: "Hello! I'm your AI assistant.",
  feedbackLoop: true,
});
// → M365 Agent listening on port 3978

Set environment variables for M365 auth:

MicrosoftAppId=<your-app-id>
MicrosoftAppPassword=<your-app-secret>
MicrosoftAppTenantId=<your-tenant-id>

Low-level usage

For full control over the Express app and CloudAdapter:

import express from "express";
import {
  CloudAdapter,
  authorizeJWT,
  getAuthConfigWithDefaults,
} from "@microsoft/agents-hosting";
import { createM365ChannelAdapter } from "@cuylabs/agent-channel-m365";

const authConfig = getAuthConfigWithDefaults();
const cloudAdapter = new CloudAdapter(authConfig);
const m365 = createM365ChannelAdapter({ agent });

const app = express();
app.post("/api/messages", (req, res) =>
  cloudAdapter.process(req, res, m365.handler),
);

app.listen(3978);

mountM365Agent() mounts route-scoped JSON parsing and JWT auth only on the configured message route. It no longer installs global middleware on the whole app.

Streaming

The adapter streams tokens to channels that support it:

| Channel | Streaming | Delay | | --------------------------------------------- | --------- | --------------------- | | Teams | Yes | 1000ms between chunks | | Webchat / DirectLine | Yes | 500ms between chunks | | Copilot-hosted non-streaming Activity clients | Buffered | Single response | | Other channels | Buffered | Single response |

Streaming is handled automatically by the M365 StreamingResponse.

If you need richer response metadata, return it from prepareTurn():

prepareTurn: () => ({
  response: {
    citations: [{ title: "Runbook", url: "https://example.com/runbook" }],
    attachments: [cardAttachment],
    sensitivityLabel: { labelId: "label-1" },
    finalMessage: { type: "message", text: "Done." },
  },
});

Configuration

createM365ChannelAdapter({
  agent,

  // Session mapping
  sessionStrategy: "conversation-id", // or "custom"
  resolveSessionId: (convId, channelId) => `${channelId}:${convId}`,

  // UI features
  showReasoning: false, // Show "Thinking..." in Teams
  showToolUsage: true, // Show "Using tool: X..." in Teams
  generatedByAILabel: true, // "Generated by AI" label
  feedbackLoop: false, // Thumbs up/down in Teams

  // Formatting
  formatToolUpdate: (name) => `Running ${name}...`,
  formatToolError: (name, err) => `${name} failed`,
  formatReasoningUpdate: () => "Analyzing...",
  formatInitialUpdate: () => "Starting...",
  formatStreamError: () =>
    "I encountered an error while processing your request. Please try again.",
  onStreamEnd: (event, request) => {
    console.log("stream ended", {
      conversationId: request.user.conversationId,
      result: event.result,
      responseLength: event.response.length,
      error: event.error?.message,
    });
  },

  // Lifecycle
  welcomeMessage: "Hi there!",
  timeout: 120_000,
  a365Observability: {
    agentId: "agent-456",
    agentDescription: "Organizes email and calendar work",
    agentVersion: "1.0.0",
  },
  resolveMessage: (context) =>
    typeof context.activity.value === "string"
      ? context.activity.value
      : context.activity.text,
  onInvoke: async (context) => {
    await sendM365Invoke(context, { ok: true });
  },
  onEvent: async (context) => {
    /* host event */
  },
  onMessageReaction: async (context) => {
    /* reaction */
  },
  onMessageUpdate: async (context) => {
    /* edit */
  },
  onMessageDelete: async (context) => {
    /* delete */
  },
  onSessionEnd: (sessionId) => {
    /* cleanup */
  },
  onError: (error, convId) => {
    /* logging */
  },
});

Agent 365 Observability

The M365 channel can wrap message turns with Agent 365 observability baggage before it calls agent.chat().

Startup still belongs in your host process:

import { createAgent } from "@cuylabs/agent-core";
import {
  createA365TracingConfig,
  initA365Observability,
} from "@cuylabs/agent-a365-observability";
import { createM365ChannelAdapter } from "@cuylabs/agent-channel-m365";

const observability = await initA365Observability({
  serviceName: "email-agent-service",
  serviceVersion: "1.0.0",
  configuration: {
    exporterEnabled: process.env.ENABLE_A365_OBSERVABILITY_EXPORTER === "true",
  },
  tokenResolver: async (agentId, tenantId) =>
    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",
  }),
});

const m365 = createM365ChannelAdapter({
  agent,
  a365Observability: {
    agentId: "agent-456",
    agentDescription: "Organizes email and calendar work",
    agentVersion: "1.0.0",
  },
});

During each message turn, the adapter derives tenant, conversation, channel, and user values from the TurnContext activity and passes them to runWithA365TurnContext(). Live export still requires Microsoft Agent 365 Frontier preview access and a real token resolver.

If you do not provide onInvoke, the adapter now sends an explicit 501 Not Implemented invoke response instead of relying on implicit CloudAdapter fallback behavior.

Turn Context

The adapter can now enrich each turn with structured context instead of forcing everything into the system prompt.

import {
  createM365ChannelAdapter,
  currentM365TurnContext,
  sendM365Invoke,
} from "@cuylabs/agent-channel-m365";

const m365 = createM365ChannelAdapter({
  agent,
  prepareTurn: ({ user, turnContext }) => ({
    system: `Help ${user.userName}.`,
    scopeName: "teams-message",
    scopeAttributes: {
      channelId: user.channelId,
      tenantId: user.tenantId,
    },
    context: {
      aadObjectId: turnContext.activity.from?.aadObjectId,
    },
  }),
});

// Inside a tool or middleware that imports this package:
const ctx = currentM365TurnContext();

Invoke Handling

Use sendM365Invoke() when your host needs an explicit invoke response:

import {
  createM365ChannelAdapter,
  sendM365Invoke,
} from "@cuylabs/agent-channel-m365";

const m365 = createM365ChannelAdapter({
  agent,
  onInvoke: async (context) => {
    await sendM365Invoke(context, { ok: true });
  },
});

You can also pass a generic source instead of a direct Agent, as long as it implements AgentTurnSource from @cuylabs/agent-core:

interface AgentTurnSource {
  chat(
    sessionId: string,
    message: string,
    options?: {
      abort?: AbortSignal;
      system?: string;
    },
  ): AsyncGenerator<AgentEvent>;
}

Telemetry (OpenTelemetry)

The Microsoft 365 Agents SDK 1.5+ ships @microsoft/agents-telemetry, which emits OpenTelemetry spans for transport work that lives behind this channel:

  • agents.adapter.process — every inbound activity
  • agents.adapter.send_activities / update_activity / delete_activity
  • agents.adapter.continue_conversation — proactive sends (see below)
  • agents.connector.* — REST calls to the Bot Framework connector
  • agents.storage.* — bot state storage operations
  • agents.authentication.* / agents.authorization.azure_bot_obo_token — token acquisition and on-behalf-of flows
  • agents.dialogs.* — dialog stack lifecycle
  • agents.app.*AgentApplication lifecycle (only emitted if you use it)

This is intentionally transport-only telemetry. It is not LLM telemetry — LLM call spans come from @cuylabs/agent-core and the AI SDK provider you wire up. Together they give you end-to-end traces from inbound activity through inference and back out to the channel.

@microsoft/agents-hosting 1.5+ already depends on @microsoft/agents-telemetry; the opt-in part is installing the OpenTelemetry API packages and configuring an SDK/exporter. To enable full tracing and log integration:

pnpm add @opentelemetry/api @opentelemetry/api-logs

Then configure an OpenTelemetry SDK with whatever exporter you use — the existing @cuylabs/agent-a365-observability startup already initializes a NodeSDK, so the SDK transport spans are picked up automatically once the OTel API packages are on the install path. To export to Application Insights / Azure Monitor, layer @cuylabs/agent-azure-monitor (or the underlying @azure/monitor-opentelemetry-exporter) on the same NodeSDK.

Disable specific span categories without touching code:

AGENTS_TELEMETRY_DISABLED_SPAN_CATEGORIES="STORAGE,DIALOGS"

See the upstream package README for the full list of categories and exporter recipes.

Proactive Messaging

@microsoft/agents-hosting 1.5+ ships a proactive messaging stack (Conversation, ConversationBuilder, ConversationReferenceBuilder, CreateConversationOptions, and a high-level Proactive class). The high-level class is bound to AgentApplication, which this package intentionally does not adopt.

@cuylabs/agent-channel-m365/proactive exposes the underlying primitives plus a thin continueM365Conversation wrapper around CloudAdapter.continueConversation, so you can capture a reference during a live turn and replay it later without pulling in AgentApplication:

import {
  captureM365ConversationReference,
  continueM365Conversation,
  restoreM365ConversationReference,
} from "@cuylabs/agent-channel-m365/proactive";

// During a live turn — store this in the persistence backend of your choice
const conversation = captureM365ConversationReference(turnContext);
const conversationId = conversation.reference.conversation!.id!;
await proactiveStore.put(conversationId, conversation.toJson());

// Later, anywhere with access to the same CloudAdapter:
const stored = await proactiveStore.get(conversationId);
const restored = restoreM365ConversationReference(stored);
await continueM365Conversation(cloudAdapter, restored, async (ctx) => {
  await ctx.sendActivity("Your async task finished.");
});

Spans for replay are emitted under agents.adapter.continue_conversation once OpenTelemetry is configured.

Configuration Notes

  • azureRegion (Agents SDK 1.5+) — AuthConfiguration now accepts an optional azureRegion field that flows into the MSAL token provider for sovereign-cloud / regional auth. Set it through your normal AuthConfiguration construction (env-driven via getAuthConfigWithDefaults).
  • OAuth env variable rename (Agents SDK 1.5+) — AzureBotAuthorizationOptions properties were renamed to align with the .NET / Python SDKs. Legacy env vars still resolve via deprecated aliases, but new deployments should adopt the new names; consult the upstream #872 for the mapping.
  • adapter.process headers fix (Agents SDK 1.5+) — fixes a sporadic "Cannot set headers after they are sent" crash when a custom logic callback responded before CloudAdapter did. processWithAdapter in ./express-compat benefits automatically once the peer dep is bumped.

Docs

The package README is the quick-start view. For the deeper guides, use the docs set: