@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 +2 -19
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +96 -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 +307 -218
- 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,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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1157
|
+
msg.addResource(`📎 [${attachment.fileName}](${shareUrl})`);
|
|
1310
1158
|
}
|
|
1311
1159
|
else {
|
|
1312
|
-
msg.
|
|
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
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
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 ?
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
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
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|