@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 +2 -19
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +93 -236
- package/dist/adapter.js.map +1 -1
- package/dist/commands/new-session.d.ts.map +1 -1
- package/dist/commands/new-session.js +68 -11
- package/dist/commands/new-session.js.map +1 -1
- package/dist/commands/session.d.ts +1 -2
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +8 -12
- package/dist/commands/session.js.map +1 -1
- package/dist/message-composer.d.ts +66 -58
- package/dist/message-composer.d.ts.map +1 -1
- package/dist/message-composer.js +301 -216
- package/dist/message-composer.js.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +54 -98
- package/dist/permissions.js.map +1 -1
- package/package.json +1 -1
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
|
*
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAQ7D,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EAGnB,WAAW,EAEX,gBAAgB,EAChB,mBAAmB,EAIpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAO,gBAAgB,EAAgB,MAAM,qBAAqB,CAAC;AAC1E,OAAO,KAAK,EAAiC,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
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
|
|
17
|
-
import { formatTokens, formatToolSummary
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
1043
|
+
msg.addTimedResult(entryId, summary);
|
|
1129
1044
|
this._toolEntryIds.delete(sessionId);
|
|
1130
1045
|
}
|
|
1131
1046
|
else {
|
|
1132
|
-
|
|
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
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1154
|
+
msg.addResource(`📎 [${attachment.fileName}](${shareUrl})`);
|
|
1310
1155
|
}
|
|
1311
1156
|
else {
|
|
1312
|
-
msg.
|
|
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
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
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 ?
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
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
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|