@hahnfeld/teams-adapter 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/adapter.d.ts CHANGED
@@ -28,8 +28,6 @@ export declare class TeamsAdapter extends MessagingAdapter {
28
28
  /** Track processed activity IDs to handle Teams 15-second retry deduplication */
29
29
  private _processedActivities;
30
30
  private _processedCleanupTimer?;
31
- /** Per-session typing pulse state (timeout + interval handles) */
32
- private _typingPulses;
33
31
  /** Per-session active tool entry ID for updateToolResult dispatch */
34
32
  private _toolEntryIds;
35
33
  /** Messages buffered during assistant initialization — replayed once ready. Capped to prevent unbounded growth. */
@@ -82,10 +80,6 @@ export declare class TeamsAdapter extends MessagingAdapter {
82
80
  private setupAssistant;
83
81
  respawnAssistant(): Promise<void>;
84
82
  restartAssistant(): Promise<void>;
85
- /** Send a typing indicator to the user. Non-critical — failures are silently ignored. */
86
- private sendTyping;
87
- /** Clear the typing pulse for a session once a response arrives. */
88
- private clearTypingPulse;
89
83
  /**
90
84
  * Acquire a bot framework token for proactive messaging via the MSA/AAD endpoint.
91
85
  * Required when posting to the Bot Connector REST API outside of a turn context.
@@ -99,13 +93,6 @@ export declare class TeamsAdapter extends MessagingAdapter {
99
93
  */
100
94
  private static readonly TRUSTED_SERVICE_URL_PATTERNS;
101
95
  static isValidServiceUrl(url: string): boolean;
102
- /** AI-generated content entity — attached to all outbound messages for the Teams "AI generated" badge */
103
- private static readonly AI_ENTITY;
104
- /**
105
- * Send a Teams activity with exponential backoff retry on transient failures.
106
- * Handles HTTP 429 (rate limited), 502, 504 per Microsoft best practices.
107
- */
108
- private sendActivityWithRetry;
109
96
  private resolveMode;
110
97
  /**
111
98
  * Primary outbound dispatch — routes agent messages to Teams.
@@ -120,14 +107,8 @@ export declare class TeamsAdapter extends MessagingAdapter {
120
107
  protected handleToolUpdate(sessionId: string, content: OutgoingMessage, _verbosity: DisplayVerbosity): Promise<void>;
121
108
  /** Set the session title on the composer if not already set. */
122
109
  private ensureSessionTitle;
123
- /** Per-session plan send mutex to prevent TOCTOU race on first plan message. */
124
- private _planSending;
125
110
  protected handlePlan(sessionId: string, content: OutgoingMessage, _verbosity: DisplayVerbosity): Promise<void>;
126
111
  protected handleUsage(sessionId: string, content: OutgoingMessage, _verbosity: DisplayVerbosity): Promise<void>;
127
- /** Suggested quick-reply actions (Teams restricts these to 1:1 personal chat only) */
128
- private static readonly QUICK_ACTIONS;
129
- /** Return QUICK_ACTIONS only if the conversation is 1:1 personal chat (Teams requirement) */
130
- private getQuickActions;
131
112
  /**
132
113
  * Clean up all per-session state (contexts, composer, output modes).
133
114
  */
@@ -146,6 +127,8 @@ export declare class TeamsAdapter extends MessagingAdapter {
146
127
  protected handleResourceLink(sessionId: string, content: OutgoingMessage): Promise<void>;
147
128
  sendPermissionRequest(sessionId: string, request: PermissionRequest): Promise<void>;
148
129
  sendNotification(notification: NotificationMessage): Promise<void>;
130
+ /** Build a notification Adaptive Card with the same info Container style. */
131
+ private static buildNotificationCard;
149
132
  /**
150
133
  * Create a new conversation thread for a session.
151
134
  *
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -5,7 +5,7 @@ import { CloudAdapter, ConfigurationBotFrameworkAuthentication, } from "botbuild
5
5
  import { PasswordServiceClientCredentialFactory } from "botframework-connector";
6
6
  import { log, MessagingAdapter, BaseRenderer } from "@openacp/plugin-sdk";
7
7
  import { DEFAULT_BOT_PORT } from "./types.js";
8
- import { SessionMessageManager } from "./message-composer.js";
8
+ import { SessionMessageManager, buildLevel1, buildLevel2, escapeMd } from "./message-composer.js";
9
9
  import { ConversationRateLimiter } from "./rate-limiter.js";
10
10
  import { PermissionHandler } from "./permissions.js";
11
11
  import { handleCommand, setupCardActionCallbacks } from "./commands/index.js";
@@ -13,12 +13,8 @@ import { spawnAssistant } from "./assistant.js";
13
13
  import { downloadTeamsFile, isAttachmentTooLarge, uploadFileViaGraph } from "./media.js";
14
14
  import { GraphFileClient } from "./graph.js";
15
15
  import { ConversationStore } from "./conversation-store.js";
16
- import { sendText, sendCard, sendActivity } from "./send-utils.js";
17
- import { formatTokens, formatToolSummary, formatPlan } from "./formatting.js";
18
- /** Max retry attempts for transient Teams API failures */
19
- const MAX_RETRIES = 3;
20
- /** Base delay (ms) for exponential backoff */
21
- const BASE_RETRY_DELAY = 1000;
16
+ import { sendText, sendCard } from "./send-utils.js";
17
+ import { formatTokens, formatToolSummary } from "./formatting.js";
22
18
  export class TeamsAdapter extends MessagingAdapter {
23
19
  name = "teams";
24
20
  renderer = new BaseRenderer();
@@ -51,8 +47,6 @@ export class TeamsAdapter extends MessagingAdapter {
51
47
  /** Track processed activity IDs to handle Teams 15-second retry deduplication */
52
48
  _processedActivities = new Map();
53
49
  _processedCleanupTimer;
54
- /** Per-session typing pulse state (timeout + interval handles) */
55
- _typingPulses = new Map();
56
50
  /** Per-session active tool entry ID for updateToolResult dispatch */
57
51
  _toolEntryIds = new Map();
58
52
  /** Messages buffered during assistant initialization — replayed once ready. Capped to prevent unbounded growth. */
@@ -164,13 +158,6 @@ export class TeamsAdapter extends MessagingAdapter {
164
158
  this._sessionContexts.clear();
165
159
  this._sessionOutputModes.clear();
166
160
  this._processedActivities.clear();
167
- for (const pulse of this._typingPulses.values()) {
168
- if (pulse.timeout)
169
- clearTimeout(pulse.timeout);
170
- if (pulse.interval)
171
- clearInterval(pulse.interval);
172
- }
173
- this._typingPulses.clear();
174
161
  this.rateLimiter.destroy();
175
162
  this.conversationStore.destroy();
176
163
  this.permissionHandler.dispose();
@@ -369,29 +356,11 @@ export class TeamsAdapter extends MessagingAdapter {
369
356
  }
370
357
  return;
371
358
  }
372
- if (sessionId !== "unknown") {
373
- this.composer.cleanup(sessionId);
374
- }
375
- // Pulse typing indicator after 5s delay, then every 8s until response arrives.
376
- // Cleared automatically when handleText/handleThought/etc. fires for a session.
377
- const typingPulseKey = sessionId !== "unknown" ? sessionId : threadId;
378
- const existingPulse = this._typingPulses.get(typingPulseKey);
379
- if (!existingPulse) {
380
- const pulseState = {};
381
- this._typingPulses.set(typingPulseKey, pulseState);
382
- pulseState.timeout = setTimeout(() => {
383
- // Use the stored context for this thread
384
- const ctx = this._sessionContexts.get(typingPulseKey);
385
- if (ctx)
386
- this.sendTyping(ctx.context);
387
- // Continue pulsing every 8s until cleared
388
- pulseState.interval = setInterval(() => {
389
- const c = this._sessionContexts.get(typingPulseKey);
390
- if (c)
391
- this.sendTyping(c.context);
392
- }, 8_000);
393
- }, 5_000);
394
- }
359
+ // Don't cleanup the existing card here — if the agent is still working on
360
+ // the previous prompt, its remaining events need the current card. The card
361
+ // finalizes naturally via handleSessionEnd or handleError. The new prompt is
362
+ // queued by core and its events will create a fresh card after the current
363
+ // turn completes.
395
364
  const existingSessionBeforeSend = this.core.sessionManager.getSessionByThread("teams", threadId);
396
365
  if (!existingSessionBeforeSend) {
397
366
  const defaultAgent = this.core.configManager.get()?.defaultAgent ?? "claude";
@@ -663,6 +632,18 @@ export class TeamsAdapter extends MessagingAdapter {
663
632
  await sendText(context, `❌ Unknown agent: ${agentName}`);
664
633
  return;
665
634
  }
635
+ // Destroy any existing session in this conversation before creating a new one
636
+ const conversationId = context.activity.conversation?.id?.split(";")[0];
637
+ if (conversationId) {
638
+ const existing = this.core.sessionManager.getSessionByThread("teams", conversationId);
639
+ if (existing) {
640
+ await this.composer.finalize(existing.id);
641
+ try {
642
+ await existing.destroy();
643
+ }
644
+ catch { /* best effort */ }
645
+ }
646
+ }
666
647
  // Send acknowledgment immediately, then create session in the background.
667
648
  // Session creation spawns an agent process (~30s) which would timeout the invoke.
668
649
  await sendText(context, `🔄 Creating session with **${agentName}**...`);
@@ -858,22 +839,6 @@ export class TeamsAdapter extends MessagingAdapter {
858
839
  async restartAssistant() {
859
840
  await this.respawnAssistant();
860
841
  }
861
- // ─── Typing indicator ────────────────────────────────────────────────────
862
- /** Send a typing indicator to the user. Non-critical — failures are silently ignored. */
863
- sendTyping(context) {
864
- sendActivity(context, { type: "typing" }).catch(() => { });
865
- }
866
- /** Clear the typing pulse for a session once a response arrives. */
867
- clearTypingPulse(key) {
868
- const pulse = this._typingPulses.get(key);
869
- if (pulse) {
870
- if (pulse.timeout)
871
- clearTimeout(pulse.timeout);
872
- if (pulse.interval)
873
- clearInterval(pulse.interval);
874
- this._typingPulses.delete(key);
875
- }
876
- }
877
842
  // ─── Bot token for proactive messaging ────────────────────────────────────
878
843
  /**
879
844
  * Acquire a bot framework token for proactive messaging via the MSA/AAD endpoint.
@@ -935,50 +900,6 @@ export class TeamsAdapter extends MessagingAdapter {
935
900
  static isValidServiceUrl(url) {
936
901
  return TeamsAdapter.TRUSTED_SERVICE_URL_PATTERNS.some((pattern) => pattern.test(url));
937
902
  }
938
- /** AI-generated content entity — attached to all outbound messages for the Teams "AI generated" badge */
939
- static AI_ENTITY = {
940
- type: "https://schema.org/Message",
941
- "@type": "Message",
942
- "@context": "https://schema.org",
943
- additionalType: ["AIGeneratedContent"],
944
- };
945
- /**
946
- * Send a Teams activity with exponential backoff retry on transient failures.
947
- * Handles HTTP 429 (rate limited), 502, 504 per Microsoft best practices.
948
- */
949
- async sendActivityWithRetry(context, activity) {
950
- // Attach AI-generated content label to all message activities.
951
- // Clone the activity to avoid mutating the caller's object (and duplicating on retries).
952
- if (!activity.type || activity.type === "message") {
953
- const existing = activity.entities ?? [];
954
- // Skip if an AIGeneratedContent entity is already present (e.g., citation entities)
955
- const hasAiLabel = existing.some((e) => Array.isArray(e?.additionalType) && e.additionalType.includes("AIGeneratedContent"));
956
- if (!hasAiLabel) {
957
- activity = { ...activity, entities: [...existing, TeamsAdapter.AI_ENTITY] };
958
- }
959
- }
960
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
961
- try {
962
- return await sendActivity(context, activity);
963
- }
964
- catch (err) {
965
- const statusCode = err?.statusCode;
966
- // Teams docs require retrying 412, 429, 502, and 504
967
- const isRetryable = statusCode === 412 || statusCode === 429 || statusCode === 502 || statusCode === 504;
968
- if (!isRetryable || attempt === MAX_RETRIES)
969
- throw err;
970
- // Parse Retry-After header if available, otherwise use exponential backoff + jitter
971
- const retryAfterRaw = err?.headers?.["retry-after"];
972
- const retryAfterSec = retryAfterRaw ? parseInt(retryAfterRaw, 10) : NaN;
973
- const delayMs = !isNaN(retryAfterSec) && retryAfterSec > 0
974
- ? retryAfterSec * 1000
975
- : BASE_RETRY_DELAY * Math.pow(2, attempt) + Math.random() * 500;
976
- log.warn({ statusCode, attempt: attempt + 1, delayMs }, "[TeamsAdapter] Rate limited or transient error, retrying");
977
- await new Promise((r) => setTimeout(r, delayMs));
978
- }
979
- }
980
- throw new Error("unreachable");
981
- }
982
903
  // ─── Helper: resolve context ─────────────────────────────────────────────
983
904
  resolveMode(sessionId) {
984
905
  // Check session-level override
@@ -1076,61 +997,54 @@ export class TeamsAdapter extends MessagingAdapter {
1076
997
  }
1077
998
  // ─── Handler overrides ───────────────────────────────────────────────────
1078
999
  async handleThought(sessionId, content, _verbosity) {
1079
- this.clearTypingPulse(sessionId);
1080
1000
  const ctx = this._sessionContexts.get(sessionId);
1081
1001
  if (!ctx)
1082
1002
  return;
1083
1003
  const msg = this.composer.getOrCreate(sessionId, ctx.context);
1084
1004
  this.ensureSessionTitle(sessionId, msg);
1085
1005
  const summary = content.text?.split("\n")[0] || "";
1086
- msg.addThought(summary);
1006
+ msg.addThinking(summary);
1087
1007
  }
1088
1008
  async handleText(sessionId, content) {
1089
- this.clearTypingPulse(sessionId);
1090
1009
  const ctx = this._sessionContexts.get(sessionId);
1091
1010
  if (!ctx)
1092
1011
  return;
1093
- if (!this.composer.has(sessionId))
1094
- this.sendTyping(ctx.context);
1095
1012
  const msg = this.composer.getOrCreate(sessionId, ctx.context);
1096
1013
  this.ensureSessionTitle(sessionId, msg);
1014
+ msg.closeActiveThinking();
1097
1015
  if (content.text)
1098
1016
  msg.addText(content.text);
1099
1017
  }
1100
1018
  async handleToolCall(sessionId, content, _verbosity) {
1101
- this.clearTypingPulse(sessionId);
1102
1019
  const ctx = this._sessionContexts.get(sessionId);
1103
1020
  if (!ctx)
1104
1021
  return;
1105
1022
  const msg = this.composer.getOrCreate(sessionId, ctx.context);
1106
1023
  this.ensureSessionTitle(sessionId, msg);
1024
+ msg.closeActiveThinking();
1107
1025
  const meta = (content.metadata ?? {});
1108
1026
  const toolName = meta.name || content.text || "Tool";
1109
1027
  const summary = formatToolSummary(toolName, meta.rawInput, meta.displaySummary);
1110
- const entryId = msg.addToolStart(toolName, summary);
1028
+ const entryId = msg.addTimedStart("🔧", summary);
1111
1029
  this._toolEntryIds.set(sessionId, entryId);
1112
1030
  }
1113
1031
  async handleToolUpdate(sessionId, content, _verbosity) {
1114
- this.clearTypingPulse(sessionId);
1115
1032
  const ctx = this._sessionContexts.get(sessionId);
1116
1033
  if (!ctx)
1117
1034
  return;
1118
1035
  const msg = this.composer.getOrCreate(sessionId, ctx.context);
1119
1036
  const meta = (content.metadata ?? {});
1120
1037
  const toolName = meta.name || content.text || "";
1121
- // Only update if we have meaningful content
1122
1038
  if (!toolName && !meta.displaySummary)
1123
1039
  return;
1124
1040
  const summary = formatToolSummary(toolName || "Tool", meta.rawInput, meta.displaySummary);
1125
- // Look up and use the stored entry id to create tool-result entry (progress stays)
1126
1041
  const entryId = this._toolEntryIds.get(sessionId);
1127
1042
  if (entryId) {
1128
- msg.addToolResult(entryId, summary);
1043
+ msg.addTimedResult(entryId, summary);
1129
1044
  this._toolEntryIds.delete(sessionId);
1130
1045
  }
1131
1046
  else {
1132
- // Fallback: create a standalone tool result
1133
- msg.addToolResult("", summary);
1047
+ msg.addTimedResult("", summary);
1134
1048
  }
1135
1049
  }
1136
1050
  /** Set the session title on the composer if not already set. */
@@ -1140,57 +1054,15 @@ export class TeamsAdapter extends MessagingAdapter {
1140
1054
  if (name)
1141
1055
  msg.setTitle(name);
1142
1056
  }
1143
- /** Per-session plan send mutex to prevent TOCTOU race on first plan message. */
1144
- _planSending = new Set();
1145
1057
  async handlePlan(sessionId, content, _verbosity) {
1146
- this.clearTypingPulse(sessionId);
1147
1058
  const ctx = this._sessionContexts.get(sessionId);
1148
- const planEntries = content.metadata?.entries ?? [];
1149
1059
  if (!ctx)
1150
1060
  return;
1151
- const { context } = ctx;
1152
- const conversationId = context.activity.conversation?.id;
1153
- const entries = planEntries;
1154
- const text = formatPlan(entries, "high");
1155
- const planRef = this.composer.getPlanRef(sessionId);
1156
- if (planRef) {
1157
- // Update existing plan message via rate limiter
1158
- await this.rateLimiter.enqueue(conversationId, async () => {
1159
- const token = await this.acquireBotToken();
1160
- if (!token)
1161
- return;
1162
- const url = `${planRef.serviceUrl}/v3/conversations/${encodeURIComponent(planRef.conversationId)}/activities/${encodeURIComponent(planRef.activityId)}`;
1163
- await fetch(url, {
1164
- method: "PUT",
1165
- headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
1166
- body: JSON.stringify({ type: "message", text: text.replace(/(?<!\n)\n(?!\n)/g, "\n\n"), textFormat: "markdown" }),
1167
- });
1168
- }, `plan:${sessionId}`);
1169
- }
1170
- else {
1171
- // Prevent TOCTOU: if another plan event is already sending the first message, skip
1172
- if (this._planSending.has(sessionId))
1173
- return;
1174
- this._planSending.add(sessionId);
1175
- try {
1176
- const result = await this.rateLimiter.enqueue(conversationId, async () => {
1177
- return sendText(context, text);
1178
- }, `plan:${sessionId}`);
1179
- if (result?.id) {
1180
- this.composer.setPlanRef(sessionId, {
1181
- activityId: result.id,
1182
- conversationId,
1183
- serviceUrl: context.activity.serviceUrl,
1184
- });
1185
- }
1186
- }
1187
- catch (err) {
1188
- log.warn({ err, sessionId }, "[TeamsAdapter] handlePlan: send failed");
1189
- }
1190
- finally {
1191
- this._planSending.delete(sessionId);
1192
- }
1193
- }
1061
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1062
+ this.ensureSessionTitle(sessionId, msg);
1063
+ msg.closeActiveThinking();
1064
+ const planEntries = content.metadata?.entries ?? [];
1065
+ msg.setPlan(planEntries.map((e) => ({ content: e.content, status: e.status })));
1194
1066
  }
1195
1067
  async handleUsage(sessionId, content, _verbosity) {
1196
1068
  const ctx = this._sessionContexts.get(sessionId);
@@ -1205,30 +1077,9 @@ export class TeamsAdapter extends MessagingAdapter {
1205
1077
  parts.push(`${(meta.duration / 1000).toFixed(1)}s`);
1206
1078
  if (meta.cost != null)
1207
1079
  parts.push(`$${meta.cost.toFixed(4)}`);
1208
- parts.push("Task completed");
1209
- const footerText = parts.join(" · ");
1210
- msg.setUsage(footerText);
1080
+ msg.setUsage(parts.join(" · "));
1211
1081
  }
1212
1082
  }
1213
- /** Suggested quick-reply actions (Teams restricts these to 1:1 personal chat only) */
1214
- static QUICK_ACTIONS = {
1215
- suggestedActions: {
1216
- actions: [
1217
- { type: "imBack", title: "➕ New Session", value: "/new" },
1218
- { type: "imBack", title: "📊 Status", value: "/status" },
1219
- { type: "imBack", title: "📋 Sessions", value: "/sessions" },
1220
- { type: "imBack", title: "📋 Menu", value: "/menu" },
1221
- ],
1222
- },
1223
- };
1224
- /** Return QUICK_ACTIONS only if the conversation is 1:1 personal chat (Teams requirement) */
1225
- getQuickActions(context) {
1226
- const convType = context.activity.conversation;
1227
- if (convType?.conversationType === "personal") {
1228
- return TeamsAdapter.QUICK_ACTIONS;
1229
- }
1230
- return {};
1231
- }
1232
1083
  /**
1233
1084
  * Clean up all per-session state (contexts, composer, output modes).
1234
1085
  */
@@ -1250,9 +1101,7 @@ export class TeamsAdapter extends MessagingAdapter {
1250
1101
  }
1251
1102
  this._sessionContexts.delete(sessionId);
1252
1103
  this._sessionOutputModes.delete(sessionId);
1253
- this._planSending.delete(sessionId);
1254
1104
  this._toolEntryIds.delete(sessionId);
1255
- this.clearTypingPulse(sessionId);
1256
1105
  this.composer.cleanup(sessionId);
1257
1106
  }
1258
1107
  async handleSessionEnd(sessionId, _content) {
@@ -1261,25 +1110,22 @@ export class TeamsAdapter extends MessagingAdapter {
1261
1110
  return;
1262
1111
  const msg = this.composer.get(sessionId);
1263
1112
  if (msg) {
1113
+ msg.closeActiveThinking();
1264
1114
  const current = msg.getFooter();
1265
1115
  msg.setUsage(current ? `${current} · Task completed` : "Task completed");
1266
1116
  }
1267
- const ref = await this.composer.finalize(sessionId);
1117
+ await this.composer.finalize(sessionId);
1268
1118
  this.cleanupSessionState(sessionId);
1269
1119
  }
1270
1120
  async handleError(sessionId, content) {
1271
1121
  const ctx = this._sessionContexts.get(sessionId);
1272
1122
  if (!ctx)
1273
1123
  return;
1124
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1125
+ msg.closeActiveThinking();
1126
+ msg.addInfo("❌", "Error", content.text || "Unknown error");
1274
1127
  await this.composer.finalize(sessionId);
1275
1128
  this.cleanupSessionState(sessionId);
1276
- try {
1277
- await this.sendActivityWithRetry(ctx.context, {
1278
- text: `❌ **Error:** ${content.text}`,
1279
- ...this.getQuickActions(ctx.context),
1280
- });
1281
- }
1282
- catch { /* best effort */ }
1283
1129
  }
1284
1130
  async handleAttachment(sessionId, content) {
1285
1131
  if (!content.attachment)
@@ -1298,18 +1144,17 @@ export class TeamsAdapter extends MessagingAdapter {
1298
1144
  if (isAttachmentTooLarge(attachment.size)) {
1299
1145
  log.warn({ sessionId, fileName: attachment.fileName, size: attachment.size }, "[TeamsAdapter] File too large");
1300
1146
  const msg = this.composer.getOrCreate(sessionId, ctx.context);
1301
- msg.addText(`\n\n⚠️ File too large to send (${Math.round(attachment.size / 1024 / 1024)}MB): ${attachment.fileName}`);
1147
+ msg.addResource(`📎 ⚠️ File too large (${Math.round(attachment.size / 1024 / 1024)}MB): ${attachment.fileName}`);
1302
1148
  return;
1303
1149
  }
1304
1150
  try {
1305
1151
  const shareUrl = await uploadFileViaGraph(this.graphClient, sessionId, attachment.filePath, attachment.fileName, attachment.mimeType);
1306
- // Append file info inline in the body
1307
1152
  const msg = this.composer.getOrCreate(sessionId, ctx.context);
1308
1153
  if (shareUrl) {
1309
- msg.addText(`\n\n📎 [${attachment.fileName}](${shareUrl})`);
1154
+ msg.addResource(`📎 [${attachment.fileName}](${shareUrl})`);
1310
1155
  }
1311
1156
  else {
1312
- msg.addText(`\n\n📎 ${attachment.fileName} (${Math.round(attachment.size / 1024)}KB)`);
1157
+ msg.addResource(`📎 ${attachment.fileName} (${Math.round(attachment.size / 1024)}KB)`);
1313
1158
  }
1314
1159
  }
1315
1160
  catch (err) {
@@ -1322,10 +1167,8 @@ export class TeamsAdapter extends MessagingAdapter {
1322
1167
  const ctx = this._sessionContexts.get(sessionId);
1323
1168
  if (!ctx)
1324
1169
  return;
1325
- try {
1326
- await this.sendActivityWithRetry(ctx.context, { text: `⚙️ ${content.text}` });
1327
- }
1328
- catch { /* best effort */ }
1170
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1171
+ msg.addInfo("⚙️", "System", content.text);
1329
1172
  }
1330
1173
  /** Sanitize metadata strings for safe markdown interpolation. */
1331
1174
  static sanitizeMd(text) {
@@ -1336,31 +1179,25 @@ export class TeamsAdapter extends MessagingAdapter {
1336
1179
  if (!ctx)
1337
1180
  return;
1338
1181
  const modeId = TeamsAdapter.sanitizeMd(String(content.metadata?.modeId ?? ""));
1339
- try {
1340
- await this.sendActivityWithRetry(ctx.context, { text: `⚙️ **Mode:** ${modeId}` });
1341
- }
1342
- catch { /* best effort */ }
1182
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1183
+ msg.addInfo("⚙️", "Mode", modeId);
1343
1184
  }
1344
1185
  async handleConfigUpdate(sessionId, content) {
1345
1186
  const ctx = this._sessionContexts.get(sessionId);
1346
1187
  if (!ctx)
1347
1188
  return;
1348
1189
  const key = content.metadata?.key;
1349
- const detail = key ? ` \`${TeamsAdapter.sanitizeMd(String(key))}\`` : "";
1350
- try {
1351
- await this.sendActivityWithRetry(ctx.context, { text: `⚙️ **Config updated**${detail}` });
1352
- }
1353
- catch { /* best effort */ }
1190
+ const detail = key ? TeamsAdapter.sanitizeMd(String(key)) : "updated";
1191
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1192
+ msg.addInfo("⚙️", "Config", detail);
1354
1193
  }
1355
1194
  async handleModelUpdate(sessionId, content) {
1356
1195
  const ctx = this._sessionContexts.get(sessionId);
1357
1196
  if (!ctx)
1358
1197
  return;
1359
1198
  const modelId = TeamsAdapter.sanitizeMd(String(content.metadata?.modelId ?? ""));
1360
- try {
1361
- await this.sendActivityWithRetry(ctx.context, { text: `⚙️ **Model:** ${modelId}` });
1362
- }
1363
- catch { /* best effort */ }
1199
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1200
+ msg.addInfo("⚙️", "Model", modelId);
1364
1201
  }
1365
1202
  async handleUserReplay(sessionId, content) {
1366
1203
  if (!content.text)
@@ -1368,10 +1205,8 @@ export class TeamsAdapter extends MessagingAdapter {
1368
1205
  const ctx = this._sessionContexts.get(sessionId);
1369
1206
  if (!ctx)
1370
1207
  return;
1371
- try {
1372
- await this.sendActivityWithRetry(ctx.context, { text: content.text });
1373
- }
1374
- catch { /* best effort */ }
1208
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1209
+ msg.addText(content.text);
1375
1210
  }
1376
1211
  async handleResource(sessionId, content) {
1377
1212
  if (!content.text)
@@ -1379,10 +1214,8 @@ export class TeamsAdapter extends MessagingAdapter {
1379
1214
  const ctx = this._sessionContexts.get(sessionId);
1380
1215
  if (!ctx)
1381
1216
  return;
1382
- try {
1383
- await this.sendActivityWithRetry(ctx.context, { text: content.text });
1384
- }
1385
- catch { /* best effort */ }
1217
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1218
+ msg.addResource(`📎 ${content.text}`);
1386
1219
  }
1387
1220
  async handleResourceLink(sessionId, content) {
1388
1221
  const ctx = this._sessionContexts.get(sessionId);
@@ -1392,11 +1225,9 @@ export class TeamsAdapter extends MessagingAdapter {
1392
1225
  const rawName = content.metadata?.name;
1393
1226
  const url = rawUrl && /^https?:\/\//i.test(rawUrl) ? rawUrl : undefined;
1394
1227
  const name = rawName?.replace(/[\[\]\(\)]/g, "") || undefined;
1395
- const text = url ? `📎 [${name || url}](${url})` : content.text;
1396
- try {
1397
- await this.sendActivityWithRetry(ctx.context, { text });
1398
- }
1399
- catch { /* best effort */ }
1228
+ const text = url ? `📎 [${name || url}](${url})` : `📎 ${content.text || "Resource"}`;
1229
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1230
+ msg.addResource(text);
1400
1231
  }
1401
1232
  // ─── sendPermissionRequest ──────────────────────────────────────────────
1402
1233
  async sendPermissionRequest(sessionId, request) {
@@ -1417,15 +1248,17 @@ export class TeamsAdapter extends MessagingAdapter {
1417
1248
  const typeIcon = {
1418
1249
  completed: "✅", error: "❌", permission: "🔐", input_required: "💬", budget_warning: "⚠️",
1419
1250
  };
1251
+ const typeLabel = {
1252
+ completed: "Completed", error: "Error", permission: "Permission", input_required: "Input Required", budget_warning: "Budget Warning",
1253
+ };
1420
1254
  const icon = typeIcon[notification.type] ?? "ℹ️";
1421
- const name = notification.sessionName ? ` **${notification.sessionName}**` : "";
1422
- let text = `${icon}${name}: ${notification.summary}`;
1423
- if (notification.deepLink) {
1424
- text += `\n${notification.deepLink}`;
1425
- }
1255
+ const label = typeLabel[notification.type] ?? "Notification";
1256
+ const detail = notification.sessionName
1257
+ ? `${notification.sessionName} — ${notification.summary}`
1258
+ : notification.summary;
1259
+ // Build a mini Adaptive Card matching the info Container style
1260
+ const card = TeamsAdapter.buildNotificationCard(icon, label, detail, notification.deepLink);
1426
1261
  // Post to the notification channel via Bot Framework REST API.
1427
- // Use any stored ref for the serviceUrl and bot identity, but target
1428
- // the notification channel ID directly as the conversation.
1429
1262
  if (this.notificationChannelId) {
1430
1263
  const ref = this.conversationStore.getAny();
1431
1264
  if (ref && TeamsAdapter.isValidServiceUrl(ref.serviceUrl)) {
@@ -1443,7 +1276,10 @@ export class TeamsAdapter extends MessagingAdapter {
1443
1276
  },
1444
1277
  body: JSON.stringify({
1445
1278
  type: "message",
1446
- text,
1279
+ attachments: [{
1280
+ contentType: "application/vnd.microsoft.card.adaptive",
1281
+ content: card,
1282
+ }],
1447
1283
  from: { id: ref.botId, name: ref.botName },
1448
1284
  }),
1449
1285
  signal: controller.signal,
@@ -1462,13 +1298,12 @@ export class TeamsAdapter extends MessagingAdapter {
1462
1298
  }
1463
1299
  }
1464
1300
  }
1465
- // Session-specific context fallback — last resort, may fail if the
1466
- // TurnContext's HTTP response stream has closed since the turn ended.
1301
+ // Session-specific context fallback
1467
1302
  if (notification.sessionId) {
1468
1303
  const ctx = this._sessionContexts.get(notification.sessionId);
1469
1304
  if (ctx) {
1470
1305
  try {
1471
- await sendText(ctx.context, text);
1306
+ await sendCard(ctx.context, card);
1472
1307
  return;
1473
1308
  }
1474
1309
  catch (err) {
@@ -1478,6 +1313,28 @@ export class TeamsAdapter extends MessagingAdapter {
1478
1313
  }
1479
1314
  log.debug({ type: notification.type, sessionName: notification.sessionName }, "[TeamsAdapter] sendNotification: no delivery path available");
1480
1315
  }
1316
+ /** Build a notification Adaptive Card with the same info Container style. */
1317
+ static buildNotificationCard(emoji, label, detail, deepLink) {
1318
+ // Pre-escape detail, then append raw markdown link (buildLevel2 would double-escape it)
1319
+ const content = deepLink
1320
+ ? `${escapeMd(detail)}\n[Open →](${deepLink})`
1321
+ : escapeMd(detail);
1322
+ return {
1323
+ type: "AdaptiveCard",
1324
+ version: "1.4",
1325
+ body: [
1326
+ {
1327
+ type: "Container",
1328
+ spacing: "Small",
1329
+ items: [
1330
+ buildLevel1(emoji, escapeMd(label)),
1331
+ buildLevel2(content, undefined, true),
1332
+ ],
1333
+ },
1334
+ ],
1335
+ width: "stretch",
1336
+ };
1337
+ }
1481
1338
  // ─── createSessionThread ─────────────────────────────────────────────────
1482
1339
  /**
1483
1340
  * Create a new conversation thread for a session.