@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
Maintainers
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-coreThe 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-coreinto a Teams-only framework
Install
pnpm add @cuylabs/agent-channel-teams @cuylabs/agent-channel-m365 @microsoft/agents-hosting @microsoft/agents-hosting-extensions-teams expressagent-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-corestill owns the agentic loop, tools, and inferenceagent-channel-m365still owns generic M365 transport andTurnContextbridgingagent-channel-teamsadds Teams-specific dispatch, typed metadata, and helper access- Microsoft
AgentApplicationrouting 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:
TeamsMessageActivityHandlersTeamsConversationUpdateHandlersTeamsCardActionHandlersTeamsConfigHandlersTeamsTaskModuleHandlersTeamsMessageExtensionHandlersTeamsTabHandlersTeamsMeetingHandlers
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:
- docs/README.md for the documentation index
- docs/architecture.md for the bridge boundary and layering
- docs/handler-model.md for handler groups, meeting routing, and
onTurn - docs/teams-helpers.md for
TeamsInfo, typed channel data, and the curated helper surface
Why This Package Exists
This package is the bridge between:
- your
agent-coreexecution 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-coreremains the only execution engineAgentApplication,Meeting,TaskModule, and similar Microsoft router classes are intentionally not part of the public cuylabs model
