@hahnfeld/teams-adapter 1.3.4 → 1.4.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/README.md +42 -9
- package/dist/adapter.d.ts +6 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +90 -15
- package/dist/adapter.js.map +1 -1
- package/dist/message-composer.d.ts +72 -43
- package/dist/message-composer.d.ts.map +1 -1
- package/dist/message-composer.js +441 -144
- package/dist/message-composer.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +88 -35
- package/dist/plugin.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/message-composer.js
CHANGED
|
@@ -1,34 +1,176 @@
|
|
|
1
1
|
import { log } from "@openacp/plugin-sdk";
|
|
2
|
-
import {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { CardFactory } from "@microsoft/agents-hosting";
|
|
3
|
+
const MAX_ROOT_TEXT_LENGTH = 25_000;
|
|
4
|
+
/** How long to wait with no activity before warning about truncation. */
|
|
5
|
+
const STALL_TIMEOUT = 120_000;
|
|
6
|
+
// ─── Brand Colors ────────────────────────────────────────────────────────────
|
|
7
|
+
// Primary blue #01426a | Dark blue #00274a | Purple #463c8f
|
|
8
|
+
// Cyan #5de3f7 | Green #a3cd6a | Magenta #ce0c88
|
|
9
|
+
// Text default #2a2a2a | Muted #676767 | White #ffffff
|
|
10
|
+
const BRAND = {
|
|
11
|
+
blue: "#01426a",
|
|
12
|
+
blueDark: "#00274a",
|
|
13
|
+
purple: "#463c8f",
|
|
14
|
+
cyan: "#5de3f7",
|
|
15
|
+
green: "#a3cd6a",
|
|
16
|
+
magenta: "#ce0c88",
|
|
17
|
+
textDefault: "#2a2a2a",
|
|
18
|
+
textMuted: "#676767",
|
|
19
|
+
white: "#ffffff",
|
|
20
|
+
surfaceMuted: "#f7f7f7",
|
|
21
|
+
};
|
|
22
|
+
// ─── ID generation ─────────────────────────────────────────────────────────────
|
|
23
|
+
let _idCounter = 0;
|
|
24
|
+
function nextId() {
|
|
25
|
+
return `e${++_idCounter}_${Date.now().toString(36)}`;
|
|
8
26
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
// ─── Adaptive Card Builder ────────────────────────────────────────────────────
|
|
28
|
+
function buildCardBody(entries) {
|
|
29
|
+
const blocks = [];
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
switch (entry.kind) {
|
|
33
|
+
case "title":
|
|
34
|
+
blocks.push({
|
|
35
|
+
type: "TextBlock",
|
|
36
|
+
text: `**${escapeMd(entry.text)}**`,
|
|
37
|
+
weight: "Bolder",
|
|
38
|
+
size: "Medium",
|
|
39
|
+
fontType: "Monospace",
|
|
40
|
+
spacing: "None",
|
|
41
|
+
});
|
|
42
|
+
break;
|
|
43
|
+
case "tool-start": {
|
|
44
|
+
const elapsed = formatElapsed(now - entry.startedAt);
|
|
45
|
+
blocks.push({
|
|
46
|
+
type: "Container",
|
|
47
|
+
items: [
|
|
48
|
+
{
|
|
49
|
+
type: "TextBlock",
|
|
50
|
+
text: `🔄 ${escapeMd(entry.toolName)}… (${elapsed})`,
|
|
51
|
+
hexColor: BRAND.blue,
|
|
52
|
+
weight: "Bolder",
|
|
53
|
+
size: "Small",
|
|
54
|
+
fontType: "Monospace",
|
|
55
|
+
spacing: "None",
|
|
56
|
+
},
|
|
57
|
+
...entry.children.map((c) => ({
|
|
58
|
+
type: "TextBlock",
|
|
59
|
+
text: ` ${c.text}`,
|
|
60
|
+
size: "Small",
|
|
61
|
+
hexColor: BRAND.cyan,
|
|
62
|
+
fontType: "Monospace",
|
|
63
|
+
spacing: "None",
|
|
64
|
+
})),
|
|
65
|
+
],
|
|
66
|
+
style: "emphasis",
|
|
67
|
+
padding: "Small",
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "tool-result": {
|
|
72
|
+
const elapsed = formatElapsed(entry.endedAt - entry.startedAt);
|
|
73
|
+
blocks.push({
|
|
74
|
+
type: "Container",
|
|
75
|
+
items: [
|
|
76
|
+
{
|
|
77
|
+
type: "TextBlock",
|
|
78
|
+
text: `📄 ${escapeMd(entry.result)} (${elapsed})`,
|
|
79
|
+
hexColor: BRAND.purple,
|
|
80
|
+
weight: "Bolder",
|
|
81
|
+
size: "Small",
|
|
82
|
+
fontType: "Monospace",
|
|
83
|
+
spacing: "None",
|
|
84
|
+
},
|
|
85
|
+
...entry.children.map((c) => ({
|
|
86
|
+
type: "TextBlock",
|
|
87
|
+
text: ` ${c.text}`,
|
|
88
|
+
size: "Small",
|
|
89
|
+
hexColor: BRAND.textDefault,
|
|
90
|
+
fontType: "Monospace",
|
|
91
|
+
wrap: true,
|
|
92
|
+
spacing: "None",
|
|
93
|
+
})),
|
|
94
|
+
],
|
|
95
|
+
style: "good",
|
|
96
|
+
padding: "Small",
|
|
97
|
+
});
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "text":
|
|
101
|
+
blocks.push({
|
|
102
|
+
type: "TextBlock",
|
|
103
|
+
text: entry.text,
|
|
104
|
+
size: "Small",
|
|
105
|
+
hexColor: BRAND.textDefault,
|
|
106
|
+
fontType: "Monospace",
|
|
107
|
+
wrap: true,
|
|
108
|
+
spacing: "None",
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case "thought":
|
|
112
|
+
blocks.push({
|
|
113
|
+
type: "TextBlock",
|
|
114
|
+
text: `💭 ${entry.text}`,
|
|
115
|
+
italic: true,
|
|
116
|
+
size: "Small",
|
|
117
|
+
hexColor: BRAND.textMuted,
|
|
118
|
+
fontType: "Monospace",
|
|
119
|
+
spacing: "None",
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
case "usage":
|
|
123
|
+
blocks.push({
|
|
124
|
+
type: "TextBlock",
|
|
125
|
+
text: `*${escapeMd(entry.text)}*`,
|
|
126
|
+
italic: true,
|
|
127
|
+
size: "Small",
|
|
128
|
+
hexColor: BRAND.blue,
|
|
129
|
+
fontType: "Monospace",
|
|
130
|
+
spacing: "None",
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
case "divider":
|
|
134
|
+
blocks.push({
|
|
135
|
+
type: "TextBlock",
|
|
136
|
+
text: "─".repeat(30),
|
|
137
|
+
size: "Small",
|
|
138
|
+
hexColor: BRAND.textMuted,
|
|
139
|
+
fontType: "Monospace",
|
|
140
|
+
spacing: "None",
|
|
141
|
+
});
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return blocks;
|
|
146
|
+
}
|
|
147
|
+
function formatElapsed(ms) {
|
|
148
|
+
if (ms < 1000)
|
|
149
|
+
return `${ms}ms`;
|
|
150
|
+
if (ms < 60_000)
|
|
151
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
152
|
+
return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
|
|
153
|
+
}
|
|
154
|
+
function escapeMd(text) {
|
|
155
|
+
return text.replace(/\[/g, "\\[").replace(/\*/g, "\\*").replace(/\]/g, "\\]");
|
|
15
156
|
}
|
|
16
|
-
|
|
17
|
-
* A single main message with title/header/body/footer zones.
|
|
18
|
-
*/
|
|
157
|
+
// ─── SessionMessage ────────────────────────────────────────────────────────────
|
|
19
158
|
export class SessionMessage {
|
|
20
159
|
context;
|
|
21
160
|
conversationId;
|
|
22
161
|
sessionId;
|
|
23
162
|
rateLimiter;
|
|
24
163
|
acquireBotToken;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
164
|
+
entries = [];
|
|
165
|
+
titleId = null;
|
|
166
|
+
usageId = null;
|
|
167
|
+
/** The active tool whose children accumulate streaming output */
|
|
168
|
+
toolActive = null;
|
|
29
169
|
ref = null;
|
|
30
170
|
lastSent = "";
|
|
31
171
|
stallTimer;
|
|
172
|
+
/** Interval handle for periodic elapsed-time updates on running tools */
|
|
173
|
+
tickInterval;
|
|
32
174
|
constructor(context, conversationId, sessionId, rateLimiter, acquireBotToken) {
|
|
33
175
|
this.context = context;
|
|
34
176
|
this.conversationId = conversationId;
|
|
@@ -43,111 +185,190 @@ export class SessionMessage {
|
|
|
43
185
|
return this.ref;
|
|
44
186
|
}
|
|
45
187
|
getBody() {
|
|
46
|
-
return this.
|
|
188
|
+
return this.entries
|
|
189
|
+
.filter((e) => e.kind === "text")
|
|
190
|
+
.map((e) => e.text)
|
|
191
|
+
.join("");
|
|
47
192
|
}
|
|
48
193
|
getFooter() {
|
|
49
|
-
|
|
194
|
+
const usage = this.entries.find((e) => e.kind === "usage");
|
|
195
|
+
return usage ? usage.text : null;
|
|
50
196
|
}
|
|
197
|
+
// ─── Entry API ───────────────────────────────────────────────────────────
|
|
51
198
|
/** Set the persistent session title (bold, survives finalize). */
|
|
52
199
|
setTitle(text) {
|
|
53
|
-
this.
|
|
200
|
+
if (this.titleId) {
|
|
201
|
+
const entry = this.findEntry(this.titleId);
|
|
202
|
+
if (entry && entry.kind === "title")
|
|
203
|
+
entry.text = text;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
this.titleId = nextId();
|
|
207
|
+
this.entries.unshift({ id: this.titleId, kind: "title", text });
|
|
208
|
+
}
|
|
54
209
|
this.requestFlush();
|
|
55
210
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Add a tool-progress entry (🔄 Running...). Sets toolActive so subsequent
|
|
213
|
+
* addText() calls route children here. Returns entry id for tracking.
|
|
214
|
+
*/
|
|
215
|
+
addToolStart(toolName, _params) {
|
|
216
|
+
const id = nextId();
|
|
217
|
+
const startedAt = Date.now();
|
|
218
|
+
this.entries.push({ id, kind: "tool-start", toolName, startedAt, children: [] });
|
|
219
|
+
this.toolActive = id;
|
|
220
|
+
this.resetStallTimer();
|
|
221
|
+
this.startTickInterval();
|
|
65
222
|
this.requestFlush();
|
|
223
|
+
return id;
|
|
66
224
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Add a tool-result entry (📄 Result...). Creates a NEW entry — does NOT
|
|
227
|
+
* replace the tool-progress entry. Both coexist as historical record.
|
|
228
|
+
* Clears toolActive so subsequent addText() goes to root body.
|
|
229
|
+
*/
|
|
230
|
+
addToolResult(id, result) {
|
|
231
|
+
const progressEntry = this.entries.find((e) => e.id === id);
|
|
232
|
+
if (!progressEntry || progressEntry.kind !== "tool-start") {
|
|
233
|
+
// Progress entry not found — create a standalone result
|
|
234
|
+
const startedAt = Date.now();
|
|
235
|
+
this.entries.push({ id: nextId(), kind: "tool-result", toolName: "", result, startedAt, endedAt: startedAt, children: [] });
|
|
236
|
+
this.toolActive = null;
|
|
237
|
+
this.stopTickInterval();
|
|
238
|
+
this.requestFlush();
|
|
70
239
|
return;
|
|
71
|
-
|
|
240
|
+
}
|
|
241
|
+
const startedAt = progressEntry.startedAt;
|
|
242
|
+
const endedAt = Date.now();
|
|
243
|
+
const children = [...progressEntry.children];
|
|
244
|
+
// Keep the tool-start as historical record; add tool-result after it
|
|
245
|
+
this.entries.push({ id: nextId(), kind: "tool-result", toolName: progressEntry.toolName, result, startedAt, endedAt, children });
|
|
246
|
+
if (this.toolActive === id)
|
|
247
|
+
this.toolActive = null;
|
|
248
|
+
this.stopTickInterval();
|
|
72
249
|
this.requestFlush();
|
|
73
250
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
251
|
+
/** Add text — goes to toolActive children if a tool is running, else root text entry. */
|
|
252
|
+
addText(text) {
|
|
76
253
|
if (!text)
|
|
77
254
|
return;
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
255
|
+
if (this.toolActive) {
|
|
256
|
+
const entry = this.findEntry(this.toolActive);
|
|
257
|
+
if (entry && (entry.kind === "tool-start" || entry.kind === "tool-result")) {
|
|
258
|
+
entry.children.push({ text });
|
|
259
|
+
this.resetStallTimer();
|
|
260
|
+
this.requestFlush();
|
|
261
|
+
// Check for overflow (root text limit)
|
|
262
|
+
if (this.ref)
|
|
263
|
+
this.checkSplit();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Root-level text — append to last root text entry or create new
|
|
268
|
+
const lastText = this.entries.filter((e) => e.kind === "text").at(-1);
|
|
269
|
+
if (lastText) {
|
|
270
|
+
lastText.text += text;
|
|
84
271
|
}
|
|
272
|
+
else {
|
|
273
|
+
this.entries.push({ id: nextId(), kind: "text", text });
|
|
274
|
+
}
|
|
275
|
+
this.resetStallTimer();
|
|
85
276
|
this.requestFlush();
|
|
277
|
+
// Check for overflow after root text grows
|
|
278
|
+
if (this.ref)
|
|
279
|
+
this.checkSplit();
|
|
86
280
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Always append a new thought entry (thoughts persist, never replaced).
|
|
283
|
+
* Multiple thinking blocks can coexist — each is a historical record.
|
|
284
|
+
*/
|
|
285
|
+
addThought(text) {
|
|
286
|
+
this.entries.push({ id: nextId(), kind: "thought", text });
|
|
90
287
|
this.requestFlush();
|
|
91
288
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
289
|
+
/** Set or replace the usage entry (only one at a time). */
|
|
290
|
+
setUsage(text) {
|
|
291
|
+
if (this.usageId) {
|
|
292
|
+
const entry = this.findEntry(this.usageId);
|
|
293
|
+
if (entry && entry.kind === "usage")
|
|
294
|
+
entry.text = text;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
this.usageId = nextId();
|
|
298
|
+
this.entries.push({ id: this.usageId, kind: "usage", text });
|
|
299
|
+
}
|
|
95
300
|
this.requestFlush();
|
|
96
301
|
}
|
|
302
|
+
/** Add a divider entry. */
|
|
303
|
+
appendDivider() {
|
|
304
|
+
this.entries.push({ id: nextId(), kind: "divider" });
|
|
305
|
+
this.requestFlush();
|
|
306
|
+
}
|
|
307
|
+
// ─── Entry helpers ────────────────────────────────────────────────────────
|
|
308
|
+
findEntry(id) {
|
|
309
|
+
for (const entry of this.entries) {
|
|
310
|
+
if (entry.id === id)
|
|
311
|
+
return entry;
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
removeEntry(id) {
|
|
316
|
+
this.entries = this.entries.filter((e) => e.id !== id);
|
|
317
|
+
}
|
|
318
|
+
// ─── Periodic tick for elapsed time updates ───────────────────────────────
|
|
97
319
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* be swallowed into the code block — append a closing fence.
|
|
320
|
+
* Start a 1-second interval that updates elapsed time on running tool-progress
|
|
321
|
+
* entries. Called when a tool starts, stopped when tool completes or finalize.
|
|
101
322
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
323
|
+
startTickInterval() {
|
|
324
|
+
if (this.tickInterval)
|
|
325
|
+
return;
|
|
326
|
+
this.tickInterval = setInterval(() => {
|
|
327
|
+
// Only tick if there's an active tool-progress entry
|
|
328
|
+
const hasRunningTool = this.entries.some((e) => e.kind === "tool-start");
|
|
329
|
+
if (!hasRunningTool) {
|
|
330
|
+
this.stopTickInterval();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.requestFlush();
|
|
334
|
+
}, 1_000);
|
|
335
|
+
if (this.tickInterval.unref)
|
|
336
|
+
this.tickInterval.unref();
|
|
108
337
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
parts.push(`**${escapeEmphasis(this.title)}**`);
|
|
114
|
-
}
|
|
115
|
-
if (this.header) {
|
|
116
|
-
parts.push(`*${escapeEmphasis(this.header)}*`);
|
|
117
|
-
}
|
|
118
|
-
if (this.title || this.header) {
|
|
119
|
-
parts.push("---");
|
|
338
|
+
stopTickInterval() {
|
|
339
|
+
if (this.tickInterval) {
|
|
340
|
+
clearInterval(this.tickInterval);
|
|
341
|
+
this.tickInterval = undefined;
|
|
120
342
|
}
|
|
121
|
-
if (this.body) {
|
|
122
|
-
parts.push(SessionMessage.closeCodeFences(this.body));
|
|
123
|
-
}
|
|
124
|
-
if (this.footer) {
|
|
125
|
-
if (this.body)
|
|
126
|
-
parts.push("---");
|
|
127
|
-
parts.push(`*${escapeEmphasis(this.footer)}*`);
|
|
128
|
-
}
|
|
129
|
-
return parts.join("\n\n");
|
|
130
343
|
}
|
|
131
|
-
|
|
344
|
+
// ─── Card building ─────────────────────────────────────────────────────────
|
|
345
|
+
buildCard() {
|
|
346
|
+
const body = buildCardBody(this.entries);
|
|
347
|
+
return {
|
|
348
|
+
type: "AdaptiveCard",
|
|
349
|
+
version: "1.4",
|
|
350
|
+
body: [
|
|
351
|
+
...(body.length > 0 ? body : [{ type: "TextBlock", text: "…" }]),
|
|
352
|
+
],
|
|
353
|
+
// Use full available width
|
|
354
|
+
width: "stretch",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// ─── Flush / Rate limiting ─────────────────────────────────────────────────
|
|
132
358
|
requestFlush() {
|
|
133
|
-
const
|
|
134
|
-
if (!composed)
|
|
135
|
-
return;
|
|
136
|
-
if (composed === this.lastSent)
|
|
137
|
-
return;
|
|
138
|
-
// Coalescing key: activityId for updates, session for new sends
|
|
359
|
+
const card = this.buildCard();
|
|
139
360
|
const key = this.ref ? `update:${this.ref.activityId}` : `new:${this.sessionId}`;
|
|
140
361
|
this.rateLimiter.enqueue(this.conversationId, () => this.flush(), key).catch((err) => {
|
|
141
362
|
log.warn({ err, sessionId: this.sessionId }, "[SessionMessage] flush failed");
|
|
142
363
|
});
|
|
143
364
|
}
|
|
144
365
|
async flush() {
|
|
145
|
-
const
|
|
146
|
-
|
|
366
|
+
const card = this.buildCard();
|
|
367
|
+
const cardStr = JSON.stringify(card);
|
|
368
|
+
if (!cardStr || cardStr === this.lastSent)
|
|
147
369
|
return;
|
|
148
370
|
if (!this.ref) {
|
|
149
|
-
|
|
150
|
-
const result = await sendText(this.context, composed);
|
|
371
|
+
const result = await sendCard(this.context, card);
|
|
151
372
|
if (result?.id) {
|
|
152
373
|
this.ref = {
|
|
153
374
|
activityId: result.id,
|
|
@@ -155,22 +376,21 @@ export class SessionMessage {
|
|
|
155
376
|
serviceUrl: this.context.activity.serviceUrl,
|
|
156
377
|
};
|
|
157
378
|
}
|
|
158
|
-
this.lastSent =
|
|
379
|
+
this.lastSent = cardStr;
|
|
159
380
|
}
|
|
160
381
|
else {
|
|
161
|
-
|
|
162
|
-
const success = await this.updateViaRest(composed);
|
|
382
|
+
const success = await this.updateCardViaRest(card);
|
|
163
383
|
if (success) {
|
|
164
|
-
this.lastSent =
|
|
384
|
+
this.lastSent = cardStr;
|
|
165
385
|
}
|
|
166
386
|
}
|
|
167
|
-
//
|
|
168
|
-
const current = this.
|
|
169
|
-
if (current
|
|
387
|
+
// Content arrived while flushing — request another flush
|
|
388
|
+
const current = this.buildCard();
|
|
389
|
+
if (JSON.stringify(current) !== this.lastSent) {
|
|
170
390
|
this.requestFlush();
|
|
171
391
|
}
|
|
172
392
|
}
|
|
173
|
-
async
|
|
393
|
+
async updateCardViaRest(card) {
|
|
174
394
|
if (!this.ref)
|
|
175
395
|
return false;
|
|
176
396
|
const token = await this.acquireBotToken();
|
|
@@ -186,69 +406,54 @@ export class SessionMessage {
|
|
|
186
406
|
},
|
|
187
407
|
body: JSON.stringify({
|
|
188
408
|
type: "message",
|
|
189
|
-
|
|
190
|
-
textFormat: "markdown",
|
|
409
|
+
attachments: [CardFactory.adaptiveCard(card)],
|
|
191
410
|
}),
|
|
192
411
|
});
|
|
193
412
|
if (!response.ok) {
|
|
194
|
-
log.warn({ status: response.status, sessionId: this.sessionId }, "[SessionMessage] REST update failed");
|
|
413
|
+
log.warn({ status: response.status, sessionId: this.sessionId }, "[SessionMessage] REST card update failed");
|
|
195
414
|
return false;
|
|
196
415
|
}
|
|
197
416
|
return true;
|
|
198
417
|
}
|
|
199
418
|
catch (err) {
|
|
200
|
-
log.warn({ err, sessionId: this.sessionId }, "[SessionMessage] REST update error");
|
|
419
|
+
log.warn({ err, sessionId: this.sessionId }, "[SessionMessage] REST card update error");
|
|
201
420
|
return false;
|
|
202
421
|
}
|
|
203
422
|
}
|
|
204
|
-
|
|
205
|
-
split() {
|
|
206
|
-
const finalBody = this.body.slice(0, MAX_BODY_LENGTH);
|
|
207
|
-
const overflow = this.body.slice(MAX_BODY_LENGTH);
|
|
208
|
-
log.info({ sessionId: this.sessionId, finalLen: finalBody.length, overflowLen: overflow.length }, "[SessionMessage] splitting");
|
|
209
|
-
// Clear ephemeral header on the finalized message; footer stays per spec
|
|
210
|
-
this.header = null;
|
|
211
|
-
this.body = finalBody;
|
|
212
|
-
// Final flush of the current message (with footer preserved)
|
|
213
|
-
const composed = this.compose();
|
|
214
|
-
if (this.ref && composed !== this.lastSent) {
|
|
215
|
-
this.rateLimiter.enqueue(this.conversationId, () => this.updateViaRest(composed).then(() => { }), `update:${this.ref.activityId}`).catch(() => { });
|
|
216
|
-
}
|
|
217
|
-
// Reset for a new message — footer carries over to new message
|
|
218
|
-
this.ref = null;
|
|
219
|
-
this.lastSent = "";
|
|
220
|
-
this.body = overflow;
|
|
221
|
-
if (overflow) {
|
|
222
|
-
this.requestFlush();
|
|
223
|
-
}
|
|
224
|
-
}
|
|
423
|
+
// ─── Stall timer ───────────────────────────────────────────────────────────
|
|
225
424
|
resetStallTimer() {
|
|
226
425
|
if (this.stallTimer)
|
|
227
426
|
clearTimeout(this.stallTimer);
|
|
228
427
|
this.stallTimer = setTimeout(() => {
|
|
229
|
-
|
|
428
|
+
const hasContent = this.entries.some((e) => (e.kind === "text" && e.text.length > 0) ||
|
|
429
|
+
(e.kind === "tool-start" && e.children.length > 0));
|
|
430
|
+
const hasUsage = this.entries.some((e) => e.kind === "usage");
|
|
431
|
+
if (hasContent && !hasUsage) {
|
|
230
432
|
log.warn({ sessionId: this.sessionId }, "[SessionMessage] Stream stalled — adding cutoff notice");
|
|
231
|
-
this.
|
|
232
|
-
|
|
233
|
-
|
|
433
|
+
this.entries.push({ id: nextId(), kind: "divider" });
|
|
434
|
+
this.entries.push({
|
|
435
|
+
id: nextId(),
|
|
436
|
+
kind: "text",
|
|
437
|
+
text: "\n\n---\n_Response was cut short — the model likely reached its output token limit. Send a follow-up message to continue._",
|
|
438
|
+
});
|
|
439
|
+
this.requestFlush();
|
|
234
440
|
}
|
|
235
441
|
}, STALL_TIMEOUT);
|
|
236
442
|
if (this.stallTimer.unref)
|
|
237
443
|
this.stallTimer.unref();
|
|
238
444
|
}
|
|
239
|
-
|
|
445
|
+
// ─── Finalize ─────────────────────────────────────────────────────────────
|
|
240
446
|
async finalize() {
|
|
447
|
+
this.stopTickInterval();
|
|
241
448
|
if (this.stallTimer) {
|
|
242
449
|
clearTimeout(this.stallTimer);
|
|
243
450
|
this.stallTimer = undefined;
|
|
244
451
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const composed = this.compose();
|
|
249
|
-
if (composed && composed !== this.lastSent) {
|
|
452
|
+
const card = this.buildCard();
|
|
453
|
+
const cardStr = JSON.stringify(card);
|
|
454
|
+
if (cardStr && cardStr !== this.lastSent) {
|
|
250
455
|
if (!this.ref) {
|
|
251
|
-
const result = await
|
|
456
|
+
const result = await sendCard(this.context, card);
|
|
252
457
|
if (result?.id) {
|
|
253
458
|
this.ref = {
|
|
254
459
|
activityId: result.id,
|
|
@@ -258,24 +463,107 @@ export class SessionMessage {
|
|
|
258
463
|
}
|
|
259
464
|
}
|
|
260
465
|
else {
|
|
261
|
-
await this.
|
|
466
|
+
await this.updateCardViaRest(card);
|
|
262
467
|
}
|
|
263
|
-
this.lastSent =
|
|
468
|
+
this.lastSent = cardStr;
|
|
264
469
|
}
|
|
265
470
|
return this.ref;
|
|
266
471
|
}
|
|
472
|
+
/** Legacy compat — strip pattern from root text entries. */
|
|
267
473
|
async stripPattern(pattern) {
|
|
268
|
-
|
|
474
|
+
for (const entry of this.entries) {
|
|
475
|
+
if (entry.kind === "text") {
|
|
476
|
+
try {
|
|
477
|
+
entry.text = entry.text.replace(pattern, "").trim();
|
|
478
|
+
}
|
|
479
|
+
catch { /* leave unchanged */ }
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Check if root text exceeds limit and split into a new message.
|
|
485
|
+
* Called from addText when a message is already sent (this.ref != null).
|
|
486
|
+
*/
|
|
487
|
+
checkSplit() {
|
|
488
|
+
const rootText = this.entries
|
|
489
|
+
.filter((e) => e.kind === "text")
|
|
490
|
+
.map((e) => e.text)
|
|
491
|
+
.join("");
|
|
492
|
+
if (rootText.length <= MAX_ROOT_TEXT_LENGTH)
|
|
269
493
|
return;
|
|
270
|
-
|
|
271
|
-
|
|
494
|
+
this.split();
|
|
495
|
+
}
|
|
496
|
+
/** For split: finalize current message at limit, start fresh. */
|
|
497
|
+
split() {
|
|
498
|
+
const rootText = this.entries
|
|
499
|
+
.filter((e) => e.kind === "text")
|
|
500
|
+
.map((e) => e.text)
|
|
501
|
+
.join("");
|
|
502
|
+
if (rootText.length <= MAX_ROOT_TEXT_LENGTH)
|
|
503
|
+
return;
|
|
504
|
+
let accLen = 0;
|
|
505
|
+
const entriesToKeep = [];
|
|
506
|
+
const entriesToOverflow = [];
|
|
507
|
+
for (const entry of this.entries) {
|
|
508
|
+
if (entry.kind === "text") {
|
|
509
|
+
const text = entry.text;
|
|
510
|
+
if (accLen + text.length <= MAX_ROOT_TEXT_LENGTH) {
|
|
511
|
+
entriesToKeep.push(entry);
|
|
512
|
+
accLen += text.length;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
entriesToOverflow.push(entry);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
entriesToKeep.push(entry);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
log.info({ sessionId: this.sessionId, kept: entriesToKeep.length, overflow: entriesToOverflow.length }, "[SessionMessage] splitting");
|
|
523
|
+
this.entries = entriesToKeep;
|
|
524
|
+
if (entriesToOverflow.length > 0) {
|
|
525
|
+
const overflowText = entriesToOverflow
|
|
526
|
+
.filter((e) => e.kind === "text")
|
|
527
|
+
.map((e) => e.text)
|
|
528
|
+
.join("");
|
|
529
|
+
if (overflowText) {
|
|
530
|
+
this.entries.push({ id: nextId(), kind: "text", text: overflowText });
|
|
531
|
+
}
|
|
272
532
|
}
|
|
273
|
-
|
|
533
|
+
if (this.ref) {
|
|
534
|
+
const card = this.buildCard();
|
|
535
|
+
this.rateLimiter.enqueue(this.conversationId, () => this.updateCardViaRest(card).then(() => { }), `update:${this.ref.activityId}`).catch(() => { });
|
|
536
|
+
}
|
|
537
|
+
this.ref = null;
|
|
538
|
+
this.lastSent = "";
|
|
539
|
+
this.requestFlush();
|
|
540
|
+
}
|
|
541
|
+
// ─── Legacy API (no-ops for compat) ────────────────────────────────────────
|
|
542
|
+
/** @deprecated — use addThought instead */
|
|
543
|
+
setHeader(_text) { }
|
|
544
|
+
/** @deprecated — use addToolResult instead */
|
|
545
|
+
setHeaderResult(_text) { }
|
|
546
|
+
/** @deprecated — use addText instead */
|
|
547
|
+
appendBody(text) {
|
|
548
|
+
this.addText(text);
|
|
549
|
+
}
|
|
550
|
+
/** @deprecated — use setUsage instead */
|
|
551
|
+
setFooter(text) {
|
|
552
|
+
this.setUsage(text);
|
|
553
|
+
}
|
|
554
|
+
/** @deprecated — use setUsage instead */
|
|
555
|
+
appendFooter(text) {
|
|
556
|
+
const current = this.getFooter();
|
|
557
|
+
this.setUsage(current ? `${current} · ${text}` : text);
|
|
274
558
|
}
|
|
559
|
+
/** @deprecated — header zone is gone */
|
|
560
|
+
clearHeader() { }
|
|
561
|
+
/** @deprecated — thoughts persist, use addThought */
|
|
562
|
+
removeThought() { }
|
|
563
|
+
/** @deprecated — use addToolStart + addToolResult */
|
|
564
|
+
updateToolResult(_id, _result) { }
|
|
275
565
|
}
|
|
276
|
-
|
|
277
|
-
* Manages SessionMessage instances and plan refs across sessions.
|
|
278
|
-
*/
|
|
566
|
+
// ─── SessionMessageManager ─────────────────────────────────────────────────────
|
|
279
567
|
export class SessionMessageManager {
|
|
280
568
|
rateLimiter;
|
|
281
569
|
acquireBotToken;
|
|
@@ -303,7 +591,6 @@ export class SessionMessageManager {
|
|
|
303
591
|
has(sessionId) {
|
|
304
592
|
return this.messages.has(sessionId);
|
|
305
593
|
}
|
|
306
|
-
/** Finalize and remove a session's message and plan ref. */
|
|
307
594
|
async finalize(sessionId) {
|
|
308
595
|
const msg = this.messages.get(sessionId);
|
|
309
596
|
if (!msg)
|
|
@@ -312,7 +599,6 @@ export class SessionMessageManager {
|
|
|
312
599
|
this.planRefs.delete(sessionId);
|
|
313
600
|
return msg.finalize();
|
|
314
601
|
}
|
|
315
|
-
/** Get or set the plan message ref for a session. */
|
|
316
602
|
getPlanRef(sessionId) {
|
|
317
603
|
return this.planRefs.get(sessionId);
|
|
318
604
|
}
|
|
@@ -328,4 +614,15 @@ export class SessionMessageManager {
|
|
|
328
614
|
this.planRefs.delete(sessionId);
|
|
329
615
|
}
|
|
330
616
|
}
|
|
617
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
618
|
+
async function sendCard(context, card) {
|
|
619
|
+
const activity = {
|
|
620
|
+
type: "message",
|
|
621
|
+
attachments: [CardFactory.adaptiveCard(card)],
|
|
622
|
+
};
|
|
623
|
+
if (typeof context.send === "function") {
|
|
624
|
+
return context.send(activity);
|
|
625
|
+
}
|
|
626
|
+
return context.sendActivity(activity);
|
|
627
|
+
}
|
|
331
628
|
//# sourceMappingURL=message-composer.js.map
|