@hahnfeld/teams-adapter 1.3.5 → 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.
@@ -1,34 +1,176 @@
1
1
  import { log } from "@openacp/plugin-sdk";
2
- import { sendText } from "./send-utils.js";
3
- const MAX_BODY_LENGTH = 25_000;
4
- const STALL_TIMEOUT = 30_000;
5
- /** Escape asterisks to prevent breaking markdown italic/bold spans. */
6
- function escapeEmphasis(text) {
7
- return text.replace(/\*/g, "\\*");
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
- * Normalize newlines for Teams rendering.
11
- * Teams collapses single \n in markdown — use \n\n for line breaks.
12
- */
13
- function teamsNewlines(text) {
14
- return text.replace(/(?<!\n)\n(?!\n)/g, "\n\n");
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
- title = null;
26
- header = null;
27
- body = "";
28
- footer = null;
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.body;
188
+ return this.entries
189
+ .filter((e) => e.kind === "text")
190
+ .map((e) => e.text)
191
+ .join("");
47
192
  }
48
193
  getFooter() {
49
- return this.footer;
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.title = text;
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
- /** Max header length — keeps the message compact; tool diffs can be huge. */
57
- static MAX_HEADER_LENGTH = 120;
58
- /** Replace the ephemeral header (tool_call, thought, etc.). */
59
- setHeader(text) {
60
- // Truncate to first line, then cap length — headers are status indicators, not content
61
- const firstLine = text.split("\n")[0];
62
- this.header = firstLine.length > SessionMessage.MAX_HEADER_LENGTH
63
- ? firstLine.slice(0, SessionMessage.MAX_HEADER_LENGTH) + "..."
64
- : firstLine;
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
- /** Clear the ephemeral header and flush the update. */
68
- clearHeader() {
69
- if (this.header === null)
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
- this.header = null;
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
- /** Append text to the body (streaming text chunks). */
75
- appendBody(text) {
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.body += text;
79
- this.resetStallTimer();
80
- // Check if body needs splitting
81
- if (this.body.length > MAX_BODY_LENGTH && this.ref) {
82
- this.split();
83
- return;
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
- /** Set the persistent footer (usage, completion). */
88
- setFooter(text) {
89
- this.footer = text;
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
- /** Append to the existing footer (e.g., adding "Task completed" after usage). */
93
- appendFooter(text) {
94
- this.footer = this.footer ? `${this.footer} · ${text}` : text;
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
- * Close any unclosed code fences in the body.
99
- * If the body has an odd number of ``` markers, the footer would
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
- static closeCodeFences(text) {
103
- const fenceCount = (text.match(/^```/gm) || []).length;
104
- if (fenceCount % 2 !== 0) {
105
- return text + "\n```";
106
- }
107
- return text;
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
- /** Compose the four zones into a single markdown string. */
110
- compose() {
111
- const parts = [];
112
- if (this.title) {
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
- /** Request a flush through the rate limiter. */
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 composed = this.compose();
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 composed = this.compose();
146
- if (!composed || composed === this.lastSent)
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
- // First send create the message
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 = composed;
379
+ this.lastSent = cardStr;
159
380
  }
160
381
  else {
161
- // Update existing message via REST
162
- const success = await this.updateViaRest(composed);
382
+ const success = await this.updateCardViaRest(card);
163
383
  if (success) {
164
- this.lastSent = composed;
384
+ this.lastSent = cardStr;
165
385
  }
166
386
  }
167
- // Check if state changed during the flush (new chunks arrived while we were awaiting)
168
- const current = this.compose();
169
- if (current && current !== this.lastSent) {
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 updateViaRest(text) {
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
- text: teamsNewlines(text),
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
- /** Split: finalize current message at body limit, start fresh. */
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
- if (this.body && !this.footer) {
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.header = null;
232
- // Use appendBody so split check is applied
233
- this.appendBody("\n\n---\n_Response was cut short — the model likely reached its output token limit. Send a follow-up message to continue._");
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
- /** Finalize: clear stall timer, clear ephemeral header, do a last flush. */
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
- // Clear ephemeral header on finalize; title and footer persist
246
- this.header = null;
247
- // Final flush
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 sendText(this.context, composed);
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.updateViaRest(composed);
466
+ await this.updateCardViaRest(card);
262
467
  }
263
- this.lastSent = composed;
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
- if (!this.body)
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
- try {
271
- this.body = this.body.replace(pattern, "").trim();
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
- catch { /* leave unchanged */ }
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