@hahnfeld/teams-adapter 1.4.1 → 1.5.1

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,29 +1077,11 @@ 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);
1211
- }
1212
- }
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;
1080
+ msg.setUsage(parts.join(" · "));
1229
1081
  }
1230
- return {};
1082
+ // Usage is the last event of a prompt turn — finalize the card so
1083
+ // the next turn gets a fresh one.
1084
+ await this.composer.finalize(sessionId);
1231
1085
  }
1232
1086
  /**
1233
1087
  * Clean up all per-session state (contexts, composer, output modes).
@@ -1250,9 +1104,7 @@ export class TeamsAdapter extends MessagingAdapter {
1250
1104
  }
1251
1105
  this._sessionContexts.delete(sessionId);
1252
1106
  this._sessionOutputModes.delete(sessionId);
1253
- this._planSending.delete(sessionId);
1254
1107
  this._toolEntryIds.delete(sessionId);
1255
- this.clearTypingPulse(sessionId);
1256
1108
  this.composer.cleanup(sessionId);
1257
1109
  }
1258
1110
  async handleSessionEnd(sessionId, _content) {
@@ -1261,25 +1113,22 @@ export class TeamsAdapter extends MessagingAdapter {
1261
1113
  return;
1262
1114
  const msg = this.composer.get(sessionId);
1263
1115
  if (msg) {
1116
+ msg.closeActiveThinking();
1264
1117
  const current = msg.getFooter();
1265
1118
  msg.setUsage(current ? `${current} · Task completed` : "Task completed");
1266
1119
  }
1267
- const ref = await this.composer.finalize(sessionId);
1120
+ await this.composer.finalize(sessionId);
1268
1121
  this.cleanupSessionState(sessionId);
1269
1122
  }
1270
1123
  async handleError(sessionId, content) {
1271
1124
  const ctx = this._sessionContexts.get(sessionId);
1272
1125
  if (!ctx)
1273
1126
  return;
1127
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1128
+ msg.closeActiveThinking();
1129
+ msg.addInfo("❌", "Error", content.text || "Unknown error");
1274
1130
  await this.composer.finalize(sessionId);
1275
1131
  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
1132
  }
1284
1133
  async handleAttachment(sessionId, content) {
1285
1134
  if (!content.attachment)
@@ -1298,18 +1147,17 @@ export class TeamsAdapter extends MessagingAdapter {
1298
1147
  if (isAttachmentTooLarge(attachment.size)) {
1299
1148
  log.warn({ sessionId, fileName: attachment.fileName, size: attachment.size }, "[TeamsAdapter] File too large");
1300
1149
  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}`);
1150
+ msg.addResource(`📎 ⚠️ File too large (${Math.round(attachment.size / 1024 / 1024)}MB): ${attachment.fileName}`);
1302
1151
  return;
1303
1152
  }
1304
1153
  try {
1305
1154
  const shareUrl = await uploadFileViaGraph(this.graphClient, sessionId, attachment.filePath, attachment.fileName, attachment.mimeType);
1306
- // Append file info inline in the body
1307
1155
  const msg = this.composer.getOrCreate(sessionId, ctx.context);
1308
1156
  if (shareUrl) {
1309
- msg.addText(`\n\n📎 [${attachment.fileName}](${shareUrl})`);
1157
+ msg.addResource(`📎 [${attachment.fileName}](${shareUrl})`);
1310
1158
  }
1311
1159
  else {
1312
- msg.addText(`\n\n📎 ${attachment.fileName} (${Math.round(attachment.size / 1024)}KB)`);
1160
+ msg.addResource(`📎 ${attachment.fileName} (${Math.round(attachment.size / 1024)}KB)`);
1313
1161
  }
1314
1162
  }
1315
1163
  catch (err) {
@@ -1322,10 +1170,8 @@ export class TeamsAdapter extends MessagingAdapter {
1322
1170
  const ctx = this._sessionContexts.get(sessionId);
1323
1171
  if (!ctx)
1324
1172
  return;
1325
- try {
1326
- await this.sendActivityWithRetry(ctx.context, { text: `⚙️ ${content.text}` });
1327
- }
1328
- catch { /* best effort */ }
1173
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1174
+ msg.addInfo("⚙️", "System", content.text);
1329
1175
  }
1330
1176
  /** Sanitize metadata strings for safe markdown interpolation. */
1331
1177
  static sanitizeMd(text) {
@@ -1336,31 +1182,25 @@ export class TeamsAdapter extends MessagingAdapter {
1336
1182
  if (!ctx)
1337
1183
  return;
1338
1184
  const modeId = TeamsAdapter.sanitizeMd(String(content.metadata?.modeId ?? ""));
1339
- try {
1340
- await this.sendActivityWithRetry(ctx.context, { text: `⚙️ **Mode:** ${modeId}` });
1341
- }
1342
- catch { /* best effort */ }
1185
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1186
+ msg.addInfo("⚙️", "Mode", modeId);
1343
1187
  }
1344
1188
  async handleConfigUpdate(sessionId, content) {
1345
1189
  const ctx = this._sessionContexts.get(sessionId);
1346
1190
  if (!ctx)
1347
1191
  return;
1348
1192
  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 */ }
1193
+ const detail = key ? TeamsAdapter.sanitizeMd(String(key)) : "updated";
1194
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1195
+ msg.addInfo("⚙️", "Config", detail);
1354
1196
  }
1355
1197
  async handleModelUpdate(sessionId, content) {
1356
1198
  const ctx = this._sessionContexts.get(sessionId);
1357
1199
  if (!ctx)
1358
1200
  return;
1359
1201
  const modelId = TeamsAdapter.sanitizeMd(String(content.metadata?.modelId ?? ""));
1360
- try {
1361
- await this.sendActivityWithRetry(ctx.context, { text: `⚙️ **Model:** ${modelId}` });
1362
- }
1363
- catch { /* best effort */ }
1202
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1203
+ msg.addInfo("⚙️", "Model", modelId);
1364
1204
  }
1365
1205
  async handleUserReplay(sessionId, content) {
1366
1206
  if (!content.text)
@@ -1368,10 +1208,8 @@ export class TeamsAdapter extends MessagingAdapter {
1368
1208
  const ctx = this._sessionContexts.get(sessionId);
1369
1209
  if (!ctx)
1370
1210
  return;
1371
- try {
1372
- await this.sendActivityWithRetry(ctx.context, { text: content.text });
1373
- }
1374
- catch { /* best effort */ }
1211
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1212
+ msg.addText(content.text);
1375
1213
  }
1376
1214
  async handleResource(sessionId, content) {
1377
1215
  if (!content.text)
@@ -1379,10 +1217,8 @@ export class TeamsAdapter extends MessagingAdapter {
1379
1217
  const ctx = this._sessionContexts.get(sessionId);
1380
1218
  if (!ctx)
1381
1219
  return;
1382
- try {
1383
- await this.sendActivityWithRetry(ctx.context, { text: content.text });
1384
- }
1385
- catch { /* best effort */ }
1220
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1221
+ msg.addResource(`📎 ${content.text}`);
1386
1222
  }
1387
1223
  async handleResourceLink(sessionId, content) {
1388
1224
  const ctx = this._sessionContexts.get(sessionId);
@@ -1392,11 +1228,9 @@ export class TeamsAdapter extends MessagingAdapter {
1392
1228
  const rawName = content.metadata?.name;
1393
1229
  const url = rawUrl && /^https?:\/\//i.test(rawUrl) ? rawUrl : undefined;
1394
1230
  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 */ }
1231
+ const text = url ? `📎 [${name || url}](${url})` : `📎 ${content.text || "Resource"}`;
1232
+ const msg = this.composer.getOrCreate(sessionId, ctx.context);
1233
+ msg.addResource(text);
1400
1234
  }
1401
1235
  // ─── sendPermissionRequest ──────────────────────────────────────────────
1402
1236
  async sendPermissionRequest(sessionId, request) {
@@ -1417,15 +1251,17 @@ export class TeamsAdapter extends MessagingAdapter {
1417
1251
  const typeIcon = {
1418
1252
  completed: "✅", error: "❌", permission: "🔐", input_required: "💬", budget_warning: "⚠️",
1419
1253
  };
1254
+ const typeLabel = {
1255
+ completed: "Completed", error: "Error", permission: "Permission", input_required: "Input Required", budget_warning: "Budget Warning",
1256
+ };
1420
1257
  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
- }
1258
+ const label = typeLabel[notification.type] ?? "Notification";
1259
+ const detail = notification.sessionName
1260
+ ? `${notification.sessionName} — ${notification.summary}`
1261
+ : notification.summary;
1262
+ // Build a mini Adaptive Card matching the info Container style
1263
+ const card = TeamsAdapter.buildNotificationCard(icon, label, detail, notification.deepLink);
1426
1264
  // 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
1265
  if (this.notificationChannelId) {
1430
1266
  const ref = this.conversationStore.getAny();
1431
1267
  if (ref && TeamsAdapter.isValidServiceUrl(ref.serviceUrl)) {
@@ -1443,7 +1279,10 @@ export class TeamsAdapter extends MessagingAdapter {
1443
1279
  },
1444
1280
  body: JSON.stringify({
1445
1281
  type: "message",
1446
- text,
1282
+ attachments: [{
1283
+ contentType: "application/vnd.microsoft.card.adaptive",
1284
+ content: card,
1285
+ }],
1447
1286
  from: { id: ref.botId, name: ref.botName },
1448
1287
  }),
1449
1288
  signal: controller.signal,
@@ -1462,13 +1301,12 @@ export class TeamsAdapter extends MessagingAdapter {
1462
1301
  }
1463
1302
  }
1464
1303
  }
1465
- // Session-specific context fallback — last resort, may fail if the
1466
- // TurnContext's HTTP response stream has closed since the turn ended.
1304
+ // Session-specific context fallback
1467
1305
  if (notification.sessionId) {
1468
1306
  const ctx = this._sessionContexts.get(notification.sessionId);
1469
1307
  if (ctx) {
1470
1308
  try {
1471
- await sendText(ctx.context, text);
1309
+ await sendCard(ctx.context, card);
1472
1310
  return;
1473
1311
  }
1474
1312
  catch (err) {
@@ -1478,6 +1316,28 @@ export class TeamsAdapter extends MessagingAdapter {
1478
1316
  }
1479
1317
  log.debug({ type: notification.type, sessionName: notification.sessionName }, "[TeamsAdapter] sendNotification: no delivery path available");
1480
1318
  }
1319
+ /** Build a notification Adaptive Card with the same info Container style. */
1320
+ static buildNotificationCard(emoji, label, detail, deepLink) {
1321
+ // Pre-escape detail, then append raw markdown link (buildLevel2 would double-escape it)
1322
+ const content = deepLink
1323
+ ? `${escapeMd(detail)}\n[Open →](${deepLink})`
1324
+ : escapeMd(detail);
1325
+ return {
1326
+ type: "AdaptiveCard",
1327
+ version: "1.4",
1328
+ body: [
1329
+ {
1330
+ type: "Container",
1331
+ spacing: "Small",
1332
+ items: [
1333
+ buildLevel1(emoji, escapeMd(label)),
1334
+ buildLevel2(content, undefined, true),
1335
+ ],
1336
+ },
1337
+ ],
1338
+ width: "stretch",
1339
+ };
1340
+ }
1481
1341
  // ─── createSessionThread ─────────────────────────────────────────────────
1482
1342
  /**
1483
1343
  * Create a new conversation thread for a session.