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-teams

v7.0.0

Published

Teams-native channel layer for @cuylabs/agent-core built on top of @cuylabs/agent-channel-m365

Downloads

4,032

Readme

@cuylabs/agent-channel-teams

Teams-native channel features for @cuylabs/agent-core, layered above @cuylabs/agent-channel-m365.

This package exists because agent-channel-m365 is intentionally transport-first:

  • it is the right layer for ordinary message turns over the Microsoft 365 Activity Protocol
  • it is not the right layer for every Teams-specific surface

agent-channel-teams adds a Teams-focused layer without forcing your library to adopt Microsoft's AgentApplication architecture.

What It Gives You

agent-channel-teams keeps your stack:

Teams
  -> @microsoft/agents-hosting
    -> @cuylabs/agent-channel-teams
      -> @cuylabs/agent-channel-m365
        -> @cuylabs/agent-core

The value is:

  • parsed Teams activity metadata instead of raw activity.channelData
  • curated Teams helpers (TeamsInfo, parseTeamsChannelData) exposed through the cuylabs package
  • Teams-specific hooks for conversation lifecycle, dialog/task modules, message extensions, tabs, config, card submits, feedback, edits, deletes, reactions, and meeting events
  • Teams response builders for dialog and search-style invoke payloads
  • invoke response helpers for Teams handlers, plus fallback access to generic M365 onInvoke
  • attachment download helpers exposed through the curated Teams helper surface
  • ambient Teams turn context that tools and middleware can read
  • a way for Teams-specific handlers to still call back into agent-core

What It Does Not Do

  • it does not replace agent-channel-m365
  • it does not copy Microsoft's routing model
  • it does not turn agent-core into a Teams-only framework

Install

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

agent-channel-teams depends on @microsoft/agents-hosting-extensions-teams because Teams helpers and typed channel data are part of the supported bridge contract, not an optional add-on.

Bridge Design

agent-channel-teams is the Teams-native facade for the cuylabs stack:

  • agent-core still owns the agentic loop, tools, and inference
  • agent-channel-m365 still owns generic M365 transport and TurnContext bridging
  • agent-channel-teams adds Teams-specific dispatch, typed metadata, and helper access
  • Microsoft AgentApplication routing abstractions are intentionally not the public programming model here

That means you can use Microsoft Teams helpers where they fit, while keeping the application architecture centered on cuylabs handlers and agent-core.

Example

import { createAgent } from "@cuylabs/agent-core";
import {
  createTeamsChannelAdapter,
  createTeamsDialogMessage,
} from "@cuylabs/agent-channel-teams";

const agent = createAgent({ model });

const teams = createTeamsChannelAdapter({
  agent,
  prepareTurn: ({ teams, user }) => ({
    system: `You are helping ${user.userName}.`,
    scopeAttributes: {
      teamsSurface: teams.surface,
      teamsKind: teams.kind,
    },
  }),
  handlers: {
    async taskSubmit(ctx) {
      const prompt = JSON.stringify(ctx.turnContext.activity.value);
      const events = ctx.runAgent(
        `Handle this Teams dialog payload:\n${prompt}`,
      );

      let text = "";
      for await (const event of events) {
        if (event.type === "text-delta") {
          text += event.text;
        }
      }

      return {
        status: 200,
        body: createTeamsDialogMessage(text),
      };
    },
  },
});

Main API

createTeamsChannelAdapter(...)

Creates a Teams-aware adapter. Ordinary message turns still flow through the underlying M365 adapter. Teams-specific activities can be intercepted by handlers.

mountTeamsAgent(...)

Express helper equivalent to mountM365Agent(...), but with the Teams-aware adapter already wired in.

currentTeamsTurnContext()

Read Teams metadata from tools or middleware during a turn.

sendTeamsInvoke(...)

Send a Teams invoke response body without manually constructing an invokeResponse activity.

Invoke handlers can also return a TeamsInvokeResult directly. If a handled invoke returns nothing, the adapter sends an empty 200 acknowledgement. If no Teams handler handles the invoke, the adapter returns an explicit 501.

createTeamsDialog(...) and createTeamsSearchResult(...)

Build common Teams dialog/search invoke bodies without making callers hand-roll raw payload objects.

Interactive Request Cards

createTeamsApprovalRequestCard(...), createTeamsHumanInputRequestCard(...), and parseTeamsInputRequestSubmit(...) provide the Teams UI pieces for approval and human-input requests. They are intentionally state-free: agent-server should own pending request state and turn resolution, while Teams renders cards and passes submit payloads back to server.respondToInputRequest(...).

import {
  createTeamsApprovalRequestCard,
  parseTeamsInputRequestSubmit,
} from "@cuylabs/agent-channel-teams";

server.subscribe(async (notification) => {
  if (notification.type !== "input/request") return;
  if (notification.request.kind !== "approval") return;

  await sendCardToTeams(
    createTeamsApprovalRequestCard(notification.request),
  );
});

const submit = parseTeamsInputRequestSubmit(ctx.turnContext.activity.value);
if (submit) {
  server.respondToInputRequest(submit.requestId, submit.payload);
}

TeamsInfo and Typed Channel Data

The curated Teams helper surface is available directly from the main barrel. If you prefer a grouped import path, the same exports are also available from the ./extensions subpath.

These helpers do not require AgentApplication; they work with the same TurnContext already used by the cuylabs adapter layer.

import {
  TeamsInfo,
  TeamsAttachmentDownloader,
  parseTeamsChannelData,
} from "@cuylabs/agent-channel-teams";

const teams = createTeamsChannelAdapter({
  agent,
  handlers: {
    async dialogSubmit(ctx) {
      const members = await TeamsInfo.getPagedMembers(ctx.turnContext);
      const names = members.members.map((m) => m.name).join(", ");
      await ctx.sendInvoke(createTeamsDialogMessage(`Team members: ${names}`));
    },
  },
});

// Zod-validated typed channel data
const channelData = parseTeamsChannelData(ctx.turnContext.activity.channelData);
console.log(channelData.team?.id, channelData.tenant?.id);

parseTeamsChannelData() is intentionally strict. If the incoming Teams payload does not match the expected SDK shape, parsing fails fast instead of silently dropping typed data.

The same helpers are also grouped under:

import {
  TeamsInfo,
  parseTeamsChannelData,
} from "@cuylabs/agent-channel-teams/extensions";

Type-only imports are available from the main barrel:

import type {
  TeamDetails,
  TeamsChannelData,
  TeamsMeetingInfo,
} from "@cuylabs/agent-channel-teams";

Handler and prepareTurn() contexts also expose parsed channel data directly:

const teams = createTeamsChannelAdapter({
  agent,
  prepareTurn: ({ teams, channelData }) => ({
    system: `Tenant: ${channelData?.tenant?.id ?? teams.tenantId ?? "unknown"}`,
  }),
  handlers: {
    meetingStart(ctx) {
      console.log(ctx.channelData?.meeting?.id ?? ctx.teams.meetingId);
    },
  },
});

Meeting Handlers

Meeting events are dispatched through the same handlers object as other Teams activities. Meeting events are Event-type activities (except meetingStageView and meetingSmartReply which are Invoke-type).

const teams = createTeamsChannelAdapter({
  agent,
  handlers: {
    meetingStart(ctx) {
      console.log("Meeting started:", ctx.teams.meetingId);
    },
    meetingEnd(ctx) {
      console.log("Meeting ended:", ctx.teams.meetingId);
    },
    participantsJoin(ctx) {
      console.log("Participants joined");
    },
    participantsLeave(ctx) {
      console.log("Participants left");
    },
  },
});

All 17 meeting event kinds from Microsoft's SDK are supported:

| Handler | Activity type | Teams event name | | ------------------------- | ------------- | ------------------------- | | meetingStart | Event | meetingStart | | meetingEnd | Event | meetingEnd | | participantsJoin | Event | meetingParticipantJoin | | participantsLeave | Event | meetingParticipantLeave | | meetingRoomJoin | Event | meetingRoomJoin | | meetingRoomLeave | Event | meetingRoomLeave | | meetingReaction | Event | meetingReaction | | meetingPollResponse | Event | meetingPollResponse | | meetingAppsInstalled | Event | meetingAppsInstalled | | meetingAppsUninstalled | Event | meetingAppsUninstalled | | meetingRecordingStarted | Event | meetingRecordingStarted | | meetingRecordingStopped | Event | meetingRecordingStopped | | meetingFocusChange | Event | meetingFocusChange | | meetingScreenShareStart | Event | meetingScreenShareStart | | meetingScreenShareStop | Event | meetingScreenShareStop | | meetingStageView | Invoke | meetingStageView | | meetingSmartReply | Invoke | meetingSmartReply |

The event-name mappings are exported for hosts that need explicit routing or observability constants:

import {
  TEAMS_MEETING_EVENT_NAMES,
  TEAMS_MEETING_INVOKE_NAMES,
} from "@cuylabs/agent-channel-teams";

Handler Capability Groups

For larger apps, the handler interfaces are also grouped by capability:

  • TeamsMessageActivityHandlers
  • TeamsConversationUpdateHandlers
  • TeamsCardActionHandlers
  • TeamsConfigHandlers
  • TeamsTaskModuleHandlers
  • TeamsMessageExtensionHandlers
  • TeamsTabHandlers
  • TeamsMeetingHandlers

TeamsActivityHandlers composes these groups into the full Teams surface.

onTurn Middleware Hook

For advanced use cases where you need raw TurnContext access before cuylabs processing — for example, to use Microsoft SDK helpers directly or to short-circuit certain activity types — use the onTurn hook:

const teams = createTeamsChannelAdapter({
  agent,
  onTurn: async (context, next) => {
    console.log(
      `Activity: ${context.activity.type} / ${context.activity.name}`,
    );

    // Call next() to continue into the normal handler pipeline
    await next();
  },
  handlers: {
    // ...
  },
});

You can short-circuit by not calling next():

onTurn: async (context, next) => {
  if (shouldSkip(context)) {
    // Don't call next() — cuylabs processing is skipped entirely
    return;
  }
  await next();
},

Targeted Activities (Group Chat / Channel)

Teams supports an activityTreatment: targeted entity that makes a reply visible only to one recipient inside a group conversation. Surfaced here in two places, both built on the ActivityTreatments.Targeted entity added in @microsoft/agents-activity 1.5:

import {
  isTargetedTeamsActivity,
  makeTargetedTeamsActivity,
  parseTeamsActivity,
} from "@cuylabs/agent-channel-teams";

// Read on inbound: as a derived field on TeamsActivityInfo
const teams = parseTeamsActivity(turnContext);
if (teams.targeted) {
  // The user explicitly asked the bot privately inside a group context
}

// Or directly from any TurnContext
isTargetedTeamsActivity(turnContext);

// Mark on outbound — only valid for group conversations
const reply = MessageFactory.text("This response is just for you.");
makeTargetedTeamsActivity(reply);
await turnContext.sendActivity(reply);

The helper is idempotent and can be called before TurnContext.sendActivity() applies the conversation reference. Teams only honors this treatment in group chat and channel contexts.

Proactive Messaging

@cuylabs/agent-channel-teams/proactive re-exports the M365 proactive bridge and adds a Teams-aware capture helper that bundles the Conversation together with parsed Teams metadata (tenant, surface, team, channel, meeting) so apps can route stored references later:

import {
  captureTeamsConversationReference,
  continueM365Conversation,
} from "@cuylabs/agent-channel-teams/proactive";

const capture = captureTeamsConversationReference(turnContext);
await store.put(capture.teams.conversationId, capture);

// Later:
await continueM365Conversation(adapter, capture.conversation, async (ctx) => {
  await ctx.sendActivity("Background work finished.");
});

The upstream Proactive class (which is bound to AgentApplication) is intentionally not used; this package exposes the underlying Conversation / builder primitives plus a thin wrapper around CloudAdapter.continueConversation. See @cuylabs/agent-channel-m365 README for the M365-level details and telemetry coverage.

Docs

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

Why This Package Exists

This package is the bridge between:

  • your agent-core execution model
  • the generic M365 transport layer
  • the richer Teams-native surfaces you will want later

That means you can start with ordinary Teams bot chat now, and then add dialogs, message actions, or search-oriented Teams invokes without changing your core agent architecture.

The important boundary is:

  • Microsoft SDK helpers and DTOs are available where they fit naturally
  • agent-core remains the only execution engine
  • AgentApplication, Meeting, TaskModule, and similar Microsoft router classes are intentionally not part of the public cuylabs model