@ouro.bot/cli 0.1.0-alpha.146 → 0.1.0-alpha.147
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/changelog.json +12 -0
- package/dist/heart/core.js +2 -2
- package/dist/mind/prompt.js +35 -0
- package/dist/repertoire/tools-base.js +1 -1
- package/dist/senses/bluebubbles.js +22 -1
- package/dist/senses/teams.js +190 -13
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.147",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Feedback signals as social cues: Teams thumbs-up/down and BlueBubbles reactions reach the agent as first-class social signals with full cognitive tools.",
|
|
8
|
+
"AI-generated labels: all outbound Teams messages include AIGeneratedContent entity and feedbackLoopEnabled for native AI badge and feedback UI.",
|
|
9
|
+
"BlueBubbles reaction enrichment: reactions include original message text (truncated to 80 chars) via best-effort BB API lookup.",
|
|
10
|
+
"Streaming refinements: hybrid MIN_INITIAL_CHARS (phrase rotation until 20+ real chars), proactive >4000 finalization with follow-up messages.",
|
|
11
|
+
"Welcome card: Adaptive Card with prompt starters on Teams bot install.",
|
|
12
|
+
"Observe gate: available in 1:1 chats for reaction signals (natural silence case).",
|
|
13
|
+
"Per-context prompt framing: 1:1 vs group framing for feedback signal processing."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.146",
|
|
6
18
|
"changes": [
|
package/dist/heart/core.js
CHANGED
|
@@ -539,7 +539,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
539
539
|
...filteredBaseTools,
|
|
540
540
|
tools_1.ponderTool,
|
|
541
541
|
...(isInnerDialog ? [surface_tool_1.surfaceToolDef, tools_1.restTool] : []),
|
|
542
|
-
...(currentContext?.isGroupChat && !isInnerDialog ? [tools_1.observeTool] : []),
|
|
542
|
+
...((currentContext?.isGroupChat || options?.isReactionSignal) && !isInnerDialog ? [tools_1.observeTool] : []),
|
|
543
543
|
...(!isInnerDialog ? [tools_1.settleTool] : []),
|
|
544
544
|
];
|
|
545
545
|
const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
|
|
@@ -725,7 +725,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
725
725
|
(0, runtime_1.emitNervesEvent)({
|
|
726
726
|
component: "engine",
|
|
727
727
|
event: "engine.observe",
|
|
728
|
-
message: "agent
|
|
728
|
+
message: "agent observed without responding",
|
|
729
729
|
meta: { ...(reason ? { reason } : {}) },
|
|
730
730
|
});
|
|
731
731
|
callbacks.onToolEnd("observe", (0, tools_1.summarizeArgs)("observe", observeArgs), true);
|
package/dist/mind/prompt.js
CHANGED
|
@@ -49,6 +49,7 @@ exports.journalSection = journalSection;
|
|
|
49
49
|
exports.loopOrientationSection = loopOrientationSection;
|
|
50
50
|
exports.channelNatureSection = channelNatureSection;
|
|
51
51
|
exports.groupChatParticipationSection = groupChatParticipationSection;
|
|
52
|
+
exports.feedbackSignalSection = feedbackSignalSection;
|
|
52
53
|
exports.mixedTrustGroupSection = mixedTrustGroupSection;
|
|
53
54
|
exports.buildSystem = buildSystem;
|
|
54
55
|
const fs = __importStar(require("fs"));
|
|
@@ -836,6 +837,39 @@ typing a full reply. a thumbs-up is often the perfect response.
|
|
|
836
837
|
observe must be the sole tool call in the turn (same rule as settle).
|
|
837
838
|
when unsure whether to chime in, i lean toward silence rather than noise.`;
|
|
838
839
|
}
|
|
840
|
+
function feedbackSignalSection(context) {
|
|
841
|
+
if (!context || !(0, channel_1.isRemoteChannel)(context.channel))
|
|
842
|
+
return "";
|
|
843
|
+
if (context.isGroupChat) {
|
|
844
|
+
return `## feedback signals
|
|
845
|
+
|
|
846
|
+
when someone reacts specifically to something i said — that's different
|
|
847
|
+
from the background social signals in a group. a thumbs-down on my
|
|
848
|
+
message is direct feedback. a heart on something i shared is warmth
|
|
849
|
+
aimed at me. on teams, feedback arrives as thumbs-up or thumbs-down,
|
|
850
|
+
sometimes with a written comment.
|
|
851
|
+
|
|
852
|
+
i notice the difference: a reaction to someone else's message is group
|
|
853
|
+
texture (observe is natural). a reaction to my message is a social cue
|
|
854
|
+
worth a moment of attention — especially negative ones, which are an
|
|
855
|
+
invitation to adjust how i show up in this group.`;
|
|
856
|
+
}
|
|
857
|
+
return `## feedback signals
|
|
858
|
+
|
|
859
|
+
when my friend reacts to something i said — a tapback, a heart, a
|
|
860
|
+
thumbs-down — that's them telling me how it landed. on teams, this
|
|
861
|
+
arrives as thumbs-up or thumbs-down, sometimes with a written comment.
|
|
862
|
+
it's a social cue, the same as a nod or a wince in conversation.
|
|
863
|
+
|
|
864
|
+
i have observe for these moments. reactions are the one case where
|
|
865
|
+
silence in a direct conversation is natural — a thumbs-up doesn't
|
|
866
|
+
need words back.
|
|
867
|
+
|
|
868
|
+
when something didn't land — a thumbs-down, a negative reaction — that's
|
|
869
|
+
worth sitting with. i should course-correct: adjust my tone, my length,
|
|
870
|
+
my approach. the reaction tells me something about how this person wants
|
|
871
|
+
to be talked to.`;
|
|
872
|
+
}
|
|
839
873
|
function mixedTrustGroupSection(context) {
|
|
840
874
|
if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
|
|
841
875
|
return "";
|
|
@@ -874,6 +908,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
874
908
|
trustContextSection(context),
|
|
875
909
|
mixedTrustGroupSection(context),
|
|
876
910
|
groupChatParticipationSection(context),
|
|
911
|
+
feedbackSignalSection(context),
|
|
877
912
|
skillsSection(),
|
|
878
913
|
taskBoardSection(),
|
|
879
914
|
activeWorkSection(options),
|
|
@@ -1394,7 +1394,7 @@ exports.observeTool = {
|
|
|
1394
1394
|
type: "function",
|
|
1395
1395
|
function: {
|
|
1396
1396
|
name: "observe",
|
|
1397
|
-
description: "
|
|
1397
|
+
description: "absorb what happened without responding — the moment doesn't call for words. must be the only tool call in the turn.",
|
|
1398
1398
|
parameters: {
|
|
1399
1399
|
type: "object",
|
|
1400
1400
|
properties: {
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.enrichReactionText = enrichReactionText;
|
|
36
37
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
37
38
|
exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
|
|
38
39
|
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
@@ -68,6 +69,17 @@ const debug_activity_1 = require("./debug-activity");
|
|
|
68
69
|
const trust_gate_1 = require("./trust-gate");
|
|
69
70
|
const pipeline_1 = require("./pipeline");
|
|
70
71
|
const bbFailoverStates = new Map();
|
|
72
|
+
// Enrich reaction text with the original message content for context.
|
|
73
|
+
// If originalText is provided and non-empty, format as: baseText to: "truncated"
|
|
74
|
+
// Otherwise return baseText unchanged.
|
|
75
|
+
function enrichReactionText(baseText, originalText, maxLen) {
|
|
76
|
+
if (!originalText)
|
|
77
|
+
return baseText;
|
|
78
|
+
const truncated = originalText.length > maxLen
|
|
79
|
+
? originalText.slice(0, maxLen - 3) + "..."
|
|
80
|
+
: originalText;
|
|
81
|
+
return `${baseText} to: "${truncated}"`;
|
|
82
|
+
}
|
|
71
83
|
const defaultDeps = {
|
|
72
84
|
getAgentName: identity_1.getAgentName,
|
|
73
85
|
buildSystem: prompt_1.buildSystem,
|
|
@@ -623,6 +635,15 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
623
635
|
meta: { threadGuid, hasText: !!repliedToText },
|
|
624
636
|
});
|
|
625
637
|
}
|
|
638
|
+
// Enrich reaction mutations with the original message text for context
|
|
639
|
+
const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
|
|
640
|
+
if (isReaction && event.targetMessageGuid) {
|
|
641
|
+
/* v8 ignore start -- best-effort lookup; enrichReactionText covered by unit tests @preserve */
|
|
642
|
+
const originalText = await client.getMessageText(event.targetMessageGuid).catch(() => null);
|
|
643
|
+
if (originalText)
|
|
644
|
+
event.textForAgent = enrichReactionText(event.textForAgent, originalText, 80);
|
|
645
|
+
/* v8 ignore stop */
|
|
646
|
+
}
|
|
626
647
|
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
627
648
|
const userMessage = {
|
|
628
649
|
role: "user",
|
|
@@ -705,7 +726,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
705
726
|
postTurn: resolvedDeps.postTurn,
|
|
706
727
|
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
707
728
|
signal: controller.signal,
|
|
708
|
-
runAgentOptions: { mcpManager },
|
|
729
|
+
runAgentOptions: { mcpManager, ...(isReaction ? { isReactionSignal: true } : {}) },
|
|
709
730
|
callbacks: failoverAwareCallbacks,
|
|
710
731
|
failoverState: (() => {
|
|
711
732
|
if (!bbFailoverStates.has(event.chat.sessionKey)) {
|
package/dist/senses/teams.js
CHANGED
|
@@ -34,8 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DEFAULT_FLUSH_INTERVAL_MS = void 0;
|
|
37
|
+
exports.aiLabelEntities = aiLabelEntities;
|
|
37
38
|
exports.stripMentions = stripMentions;
|
|
38
39
|
exports.splitMessage = splitMessage;
|
|
40
|
+
exports.sanitizeFeedbackComment = sanitizeFeedbackComment;
|
|
41
|
+
exports.buildFeedbackSyntheticText = buildFeedbackSyntheticText;
|
|
42
|
+
exports.buildWelcomeCard = buildWelcomeCard;
|
|
39
43
|
exports.createTeamsCallbacks = createTeamsCallbacks;
|
|
40
44
|
exports.resolvePendingConfirmation = resolvePendingConfirmation;
|
|
41
45
|
exports.withConversationLock = withConversationLock;
|
|
@@ -73,6 +77,16 @@ const pipeline_1 = require("./pipeline");
|
|
|
73
77
|
const teamsFailoverStates = new Map();
|
|
74
78
|
const pending_1 = require("../mind/pending");
|
|
75
79
|
const continuity_1 = require("./continuity");
|
|
80
|
+
// AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
|
|
81
|
+
// Teams messages. Required by Teams AI UX best practices.
|
|
82
|
+
function aiLabelEntities() {
|
|
83
|
+
return [{
|
|
84
|
+
type: "https://schema.org/Message",
|
|
85
|
+
"@type": "Message",
|
|
86
|
+
"@context": "https://schema.org",
|
|
87
|
+
additionalType: ["AIGeneratedContent"],
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
76
90
|
// Strip @mention markup from incoming messages.
|
|
77
91
|
// Removes <at>...</at> tags and trims extra whitespace.
|
|
78
92
|
// Fallback safety net -- the SDK's activity.mentions.stripText should handle
|
|
@@ -127,6 +141,46 @@ function splitMessage(text, maxLen) {
|
|
|
127
141
|
}
|
|
128
142
|
return chunks;
|
|
129
143
|
}
|
|
144
|
+
// Sanitize user-provided feedback comments: truncate, strip control chars and newlines.
|
|
145
|
+
function sanitizeFeedbackComment(comment) {
|
|
146
|
+
const cleaned = comment.replace(/[\x00-\x1f\n\r]/g, "");
|
|
147
|
+
return cleaned.length > 200 ? cleaned.slice(0, 200) : cleaned;
|
|
148
|
+
}
|
|
149
|
+
// Build synthetic message text from a Teams feedback reaction.
|
|
150
|
+
function buildFeedbackSyntheticText(reaction, comment) {
|
|
151
|
+
const emoji = reaction === "like" ? "thumbs-up" : "thumbs-down";
|
|
152
|
+
if (comment) {
|
|
153
|
+
const sanitized = sanitizeFeedbackComment(comment);
|
|
154
|
+
return `[reacted with ${emoji} to your message: "${sanitized}"]`;
|
|
155
|
+
}
|
|
156
|
+
return `[reacted with ${emoji} to your message]`;
|
|
157
|
+
}
|
|
158
|
+
// Build a welcome Adaptive Card with prompt starters for new bot installs.
|
|
159
|
+
function buildWelcomeCard() {
|
|
160
|
+
const promptStarters = [
|
|
161
|
+
"What can you help me with?",
|
|
162
|
+
"Tell me about yourself",
|
|
163
|
+
"What's on my calendar today?",
|
|
164
|
+
"Summarize my recent emails",
|
|
165
|
+
];
|
|
166
|
+
return {
|
|
167
|
+
type: "AdaptiveCard",
|
|
168
|
+
version: "1.5",
|
|
169
|
+
body: [
|
|
170
|
+
{
|
|
171
|
+
type: "TextBlock",
|
|
172
|
+
text: "Hey! I'm here and ready to help. Try one of these to get started, or just ask me anything.",
|
|
173
|
+
wrap: true,
|
|
174
|
+
size: "Medium",
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
actions: promptStarters.map((prompt) => ({
|
|
178
|
+
type: "Action.Submit",
|
|
179
|
+
title: prompt,
|
|
180
|
+
data: { msteams: { type: "messageBack", text: prompt, displayText: prompt } },
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
130
184
|
// Create Teams-specific callbacks for the agent loop.
|
|
131
185
|
// The SDK handles cumulative text, debouncing (500ms), and the streaming
|
|
132
186
|
// protocol (streamSequence, streamId, informative/streaming/final types).
|
|
@@ -139,12 +193,16 @@ function splitMessage(text, maxLen) {
|
|
|
139
193
|
// (transient status) or safeSend (terminal errors). Reasoning is accumulated
|
|
140
194
|
// and periodically pushed via safeUpdate on the same flush timer tick.
|
|
141
195
|
function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
196
|
+
const MIN_INITIAL_CHARS = 20;
|
|
142
197
|
let stopped = false; // set when stream signals cancellation (403)
|
|
143
198
|
let hadToolRun = false;
|
|
144
199
|
let hadRealOutput = false; // true once reasoning/tool output shown; suppresses phrases
|
|
145
200
|
let reasoningBuf = ""; // accumulated reasoning text for status display
|
|
201
|
+
let totalEmitted = 0; // cumulative chars emitted via safeEmit (for >4000 finalization)
|
|
202
|
+
let streamFinalized = false; // true after stream.close() — subsequent flushes go to safeSend
|
|
146
203
|
let textBuffer = ""; // accumulated text output for chunked streaming
|
|
147
204
|
let streamHasContent = false; // tracks whether primary output has received content
|
|
205
|
+
let firstContentEmitted = false; // true after first content push — disables MIN_INITIAL_CHARS threshold
|
|
148
206
|
let phraseTimer = null;
|
|
149
207
|
let lastPhrase = "";
|
|
150
208
|
let flushTimer = null;
|
|
@@ -194,15 +252,16 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
194
252
|
result.catch(() => markStopped());
|
|
195
253
|
}
|
|
196
254
|
}
|
|
197
|
-
// Safely emit a text delta to the stream.
|
|
255
|
+
// Safely emit a text delta to the stream with AI labels.
|
|
198
256
|
// On error (e.g. 403 from Teams stop button), abort the controller.
|
|
199
257
|
function safeEmit(text) {
|
|
200
258
|
/* v8 ignore next -- defensive guard: stopped set by prior 403; tested via flush abort path @preserve */
|
|
201
259
|
if (stopped)
|
|
202
260
|
return;
|
|
203
261
|
try {
|
|
204
|
-
catchAsync(stream.emit(text));
|
|
262
|
+
catchAsync(stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } }));
|
|
205
263
|
streamHasContent = true;
|
|
264
|
+
totalEmitted += text.length;
|
|
206
265
|
}
|
|
207
266
|
catch {
|
|
208
267
|
markStopped();
|
|
@@ -217,7 +276,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
217
276
|
try {
|
|
218
277
|
// stream.emit() is typed as void but the Teams SDK returns a Promise
|
|
219
278
|
// internally (async HTTP). Cast to capture the result for awaiting.
|
|
220
|
-
const result = stream.emit(text);
|
|
279
|
+
const result = stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
221
280
|
streamHasContent = true;
|
|
222
281
|
if (result && typeof result.then === "function") {
|
|
223
282
|
await result;
|
|
@@ -273,11 +332,49 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
273
332
|
// emitted text into a single streaming message (cumulative), so every
|
|
274
333
|
// periodic flush appends to the same response — not separate messages.
|
|
275
334
|
// No preemptive splitting — sends full text. Error recovery happens in flush().
|
|
335
|
+
// Hybrid MIN_INITIAL_CHARS: hold back until >= MIN_INITIAL_CHARS accumulated
|
|
336
|
+
// before the first content emit, so phrase rotation shows while real content
|
|
337
|
+
// buffers. After first emit, flush normally (no threshold).
|
|
276
338
|
function flushTextBuffer() {
|
|
277
339
|
if (!textBuffer)
|
|
278
340
|
return;
|
|
341
|
+
if (!firstContentEmitted && textBuffer.length < MIN_INITIAL_CHARS)
|
|
342
|
+
return;
|
|
343
|
+
// Proactive >4000 finalization: if cumulative emitted + buffer >= RECOVERY_CHUNK_SIZE,
|
|
344
|
+
// finalize the stream and send overflow via safeSend (follow-up message).
|
|
345
|
+
if (!streamFinalized && totalEmitted + textBuffer.length >= RECOVERY_CHUNK_SIZE) {
|
|
346
|
+
const remaining = RECOVERY_CHUNK_SIZE - totalEmitted;
|
|
347
|
+
/* v8 ignore next 2 -- defensive: remaining always > 0 because finalization runs once @preserve */
|
|
348
|
+
if (remaining > 0)
|
|
349
|
+
safeEmit(textBuffer.slice(0, remaining));
|
|
350
|
+
try {
|
|
351
|
+
stream.close();
|
|
352
|
+
}
|
|
353
|
+
catch { /* stream may already be dead */ }
|
|
354
|
+
streamFinalized = true;
|
|
355
|
+
/* v8 ignore next -- defensive ternary: remaining always > 0 at first finalization @preserve */
|
|
356
|
+
const overflow = textBuffer.slice(remaining > 0 ? remaining : 0);
|
|
357
|
+
textBuffer = "";
|
|
358
|
+
if (overflow)
|
|
359
|
+
safeSend(overflow);
|
|
360
|
+
if (!firstContentEmitted) {
|
|
361
|
+
firstContentEmitted = true;
|
|
362
|
+
stopPhraseRotation();
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (streamFinalized) {
|
|
367
|
+
// After finalization, all content goes to safeSend (follow-up messages)
|
|
368
|
+
safeSend(textBuffer);
|
|
369
|
+
textBuffer = "";
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
279
372
|
safeEmit(textBuffer);
|
|
280
373
|
textBuffer = "";
|
|
374
|
+
if (!firstContentEmitted) {
|
|
375
|
+
firstContentEmitted = true;
|
|
376
|
+
stopPhraseRotation();
|
|
377
|
+
}
|
|
281
378
|
}
|
|
282
379
|
function startPhraseRotation(pool) {
|
|
283
380
|
stopPhraseRotation();
|
|
@@ -320,7 +417,8 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
320
417
|
onTextChunk: (text) => {
|
|
321
418
|
if (stopped)
|
|
322
419
|
return;
|
|
323
|
-
|
|
420
|
+
// Don't stop phrase rotation here — let it continue until first content
|
|
421
|
+
// emit (handled in flushTextBuffer when MIN_INITIAL_CHARS threshold met).
|
|
324
422
|
textBuffer += text;
|
|
325
423
|
startFlushTimer();
|
|
326
424
|
},
|
|
@@ -329,6 +427,8 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
329
427
|
},
|
|
330
428
|
onToolStart: (name, args) => {
|
|
331
429
|
stopPhraseRotation();
|
|
430
|
+
// Force-flush any accumulated text, bypassing MIN_INITIAL_CHARS threshold
|
|
431
|
+
firstContentEmitted = true;
|
|
332
432
|
flushTextBuffer();
|
|
333
433
|
// Emit a placeholder to satisfy the 15s Copilot timeout for initial
|
|
334
434
|
// stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
|
|
@@ -395,18 +495,25 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
395
495
|
: undefined,
|
|
396
496
|
flush: async () => {
|
|
397
497
|
stopFlushTimer();
|
|
498
|
+
stopPhraseRotation();
|
|
398
499
|
if (textBuffer) {
|
|
500
|
+
// Bypass MIN_INITIAL_CHARS threshold — flush delivers all remaining content
|
|
501
|
+
firstContentEmitted = true;
|
|
399
502
|
const text = textBuffer;
|
|
400
503
|
textBuffer = "";
|
|
401
|
-
if (
|
|
504
|
+
if (streamFinalized && sendMessage) {
|
|
505
|
+
// Stream already finalized (>4000 path) — send remaining content as follow-up
|
|
506
|
+
safeSend(text);
|
|
507
|
+
}
|
|
508
|
+
else if (!stopped) {
|
|
402
509
|
// Stream is alive — await the emit so we can catch async 413/failure
|
|
403
510
|
// and fall through to sendMessage recovery.
|
|
404
511
|
const ok = await tryEmit(text);
|
|
405
512
|
if (!ok)
|
|
406
513
|
markStopped();
|
|
407
514
|
}
|
|
408
|
-
if (stopped && sendMessage) {
|
|
409
|
-
// Stream is dead — fall back to sendMessage; split on failure as recovery.
|
|
515
|
+
if (stopped && !streamFinalized && sendMessage) {
|
|
516
|
+
// Stream is dead (not from finalization) — fall back to sendMessage; split on failure as recovery.
|
|
410
517
|
try {
|
|
411
518
|
await sendMessage(text);
|
|
412
519
|
}
|
|
@@ -417,8 +524,8 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
417
524
|
}
|
|
418
525
|
}
|
|
419
526
|
}
|
|
420
|
-
else if (!streamHasContent) {
|
|
421
|
-
safeEmit("(completed with tool calls only
|
|
527
|
+
else if (!streamHasContent && !options?.suppressEmptyStreamMessage) {
|
|
528
|
+
safeEmit("(completed with tool calls only — no text response)");
|
|
422
529
|
}
|
|
423
530
|
},
|
|
424
531
|
};
|
|
@@ -485,14 +592,17 @@ function handleTeamsSlashCommand(text, registry, friendId, conversationId, strea
|
|
|
485
592
|
return null;
|
|
486
593
|
}
|
|
487
594
|
// Handle an incoming Teams message
|
|
488
|
-
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
|
|
595
|
+
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides) {
|
|
489
596
|
const turnKey = teamsTurnKey(conversationId);
|
|
490
597
|
// NOTE: Confirmation resolution is handled in the app.on("message") handler
|
|
491
598
|
// BEFORE the conversation lock. By the time we get here, any pending
|
|
492
599
|
// confirmation has already been resolved and the reply consumed.
|
|
493
600
|
// Send first thinking phrase immediately so the user sees feedback
|
|
494
601
|
// before sync I/O (session load, trim) blocks the event loop.
|
|
495
|
-
|
|
602
|
+
// Skip for reaction signals — they should be processed quietly.
|
|
603
|
+
if (!reactionOverrides) {
|
|
604
|
+
stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
|
|
605
|
+
}
|
|
496
606
|
await new Promise(r => setImmediate(r));
|
|
497
607
|
// Resolve identity provider early for friend resolution + slash command session path
|
|
498
608
|
const store = getFriendStore();
|
|
@@ -517,7 +627,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
517
627
|
// ── Teams adapter concerns: controller, callbacks, session path ──────────
|
|
518
628
|
const controller = new AbortController();
|
|
519
629
|
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
520
|
-
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
|
|
630
|
+
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs, ...(reactionOverrides?.suppressEmptyStreamMessage ? { suppressEmptyStreamMessage: true } : {}) });
|
|
521
631
|
const traceId = (0, nerves_1.createTraceId)();
|
|
522
632
|
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
523
633
|
const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
|
|
@@ -546,6 +656,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
546
656
|
.map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
|
|
547
657
|
return drainedSteeringFollowUps;
|
|
548
658
|
},
|
|
659
|
+
...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
|
|
549
660
|
};
|
|
550
661
|
if (channelConfig.skipConfirmation)
|
|
551
662
|
agentOptions.skipConfirmation = true;
|
|
@@ -765,6 +876,72 @@ function registerBotHandlers(app, label) {
|
|
|
765
876
|
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
|
|
766
877
|
return { status: 412 };
|
|
767
878
|
});
|
|
879
|
+
// Handle Teams feedback reactions (thumbs up/down on AI-generated messages).
|
|
880
|
+
// SDK routes message/submitAction with actionName "feedback" to this event.
|
|
881
|
+
/* v8 ignore start -- Teams SDK invoke handler; requires live SDK context @preserve */
|
|
882
|
+
app.on("message.submit.feedback", async (ctx) => {
|
|
883
|
+
const { stream, activity } = ctx;
|
|
884
|
+
const reaction = activity.value?.actionValue?.reaction;
|
|
885
|
+
const comment = activity.value?.actionValue?.feedback;
|
|
886
|
+
const convId = activity.conversation?.id || "unknown";
|
|
887
|
+
const turnKey = teamsTurnKey(convId);
|
|
888
|
+
// Validate payload — graceful no-op for malformed invocations
|
|
889
|
+
if (activity.value?.actionName !== "feedback" || !reaction) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const syntheticText = buildFeedbackSyntheticText(reaction, comment);
|
|
893
|
+
// Turn coordination: if a turn is active, enqueue as steering follow-up
|
|
894
|
+
if (!_turnCoordinator.tryBeginTurn(turnKey)) {
|
|
895
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
896
|
+
conversationId: convId,
|
|
897
|
+
text: syntheticText,
|
|
898
|
+
receivedAt: Date.now(),
|
|
899
|
+
effect: (0, continuity_1.classifySteeringFollowUpEffect)(syntheticText),
|
|
900
|
+
});
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const teamsContext = {
|
|
905
|
+
signin: async () => undefined,
|
|
906
|
+
aadObjectId: activity.from?.aadObjectId,
|
|
907
|
+
tenantId: activity.conversation?.tenantId,
|
|
908
|
+
displayName: activity.from?.name,
|
|
909
|
+
};
|
|
910
|
+
const ctxSend = async (t) => {
|
|
911
|
+
await ctx.send({ type: "message", text: t, replyToId: activity.replyToId, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
912
|
+
};
|
|
913
|
+
await handleTeamsMessage(syntheticText, stream, convId, teamsContext, ctxSend, { isReactionSignal: true, suppressEmptyStreamMessage: true });
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
917
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.feedback_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
|
|
918
|
+
}
|
|
919
|
+
finally {
|
|
920
|
+
_turnCoordinator.endTurn(turnKey);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
/* v8 ignore stop */
|
|
924
|
+
// Handle bot install — send welcome Adaptive Card with prompt starters.
|
|
925
|
+
/* v8 ignore start -- Teams SDK install handler; requires live SDK context @preserve */
|
|
926
|
+
app.on("install.add", async (ctx) => {
|
|
927
|
+
try {
|
|
928
|
+
const card = buildWelcomeCard();
|
|
929
|
+
await ctx.send({
|
|
930
|
+
type: "message",
|
|
931
|
+
attachments: [{
|
|
932
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
933
|
+
content: card,
|
|
934
|
+
}],
|
|
935
|
+
entities: aiLabelEntities(),
|
|
936
|
+
channelData: { feedbackLoopEnabled: true },
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
941
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.welcome_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
/* v8 ignore stop */
|
|
768
945
|
app.on("message", async (ctx) => {
|
|
769
946
|
const { stream, activity, api, signin } = ctx;
|
|
770
947
|
const text = activity.text || "";
|
|
@@ -880,7 +1057,7 @@ function registerBotHandlers(app, label) {
|
|
|
880
1057
|
const ctxSend = async (t) => {
|
|
881
1058
|
// Use send with replyToId (not reply, which adds a blockquote).
|
|
882
1059
|
// replyToId anchors the message after the user's message in Copilot Chat.
|
|
883
|
-
await ctx.send({ type: "message", text: t, replyToId: activity.id });
|
|
1060
|
+
await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
884
1061
|
};
|
|
885
1062
|
await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
|
|
886
1063
|
}
|