@ouro.bot/cli 0.1.0-alpha.146 → 0.1.0-alpha.148
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 +27 -0
- package/dist/heart/core.js +2 -2
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +104 -6
- package/dist/heart/daemon/daemon-entry.js +36 -15
- package/dist/heart/daemon/daemon.js +7 -0
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +244 -0
- package/dist/heart/daemon/hatch-flow.js +13 -31
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/specialist-tools.js +1 -1
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/mind/prompt.js +44 -5
- package/dist/repertoire/tasks/parser.js +0 -2
- package/dist/repertoire/tasks/transitions.js +0 -18
- package/dist/repertoire/tools-base.js +1 -1
- package/dist/senses/bluebubbles.js +22 -1
- package/dist/senses/contextual-heartbeat.js +5 -1
- package/dist/senses/inner-dialog-worker.js +84 -20
- package/dist/senses/inner-dialog.js +145 -39
- package/dist/senses/teams.js +190 -13
- package/package.json +1 -1
- package/dist/heart/daemon/heartbeat-timer.js +0 -132
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
|
}
|
package/package.json
CHANGED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HeartbeatTimer = exports.DEFAULT_CADENCE_MS = void 0;
|
|
4
|
-
exports.parseCadenceMs = parseCadenceMs;
|
|
5
|
-
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
-
const parser_1 = require("../../repertoire/tasks/parser");
|
|
7
|
-
exports.DEFAULT_CADENCE_MS = 30 * 60 * 1000; // 30 minutes
|
|
8
|
-
function parseCadenceMs(raw) {
|
|
9
|
-
if (typeof raw !== "string")
|
|
10
|
-
return null;
|
|
11
|
-
const value = raw.trim();
|
|
12
|
-
if (!value)
|
|
13
|
-
return null;
|
|
14
|
-
const match = /^(\d+)(m|h|d)$/.exec(value);
|
|
15
|
-
if (!match)
|
|
16
|
-
return null;
|
|
17
|
-
const interval = Number.parseInt(match[1], 10);
|
|
18
|
-
if (!Number.isFinite(interval) || interval <= 0)
|
|
19
|
-
return null;
|
|
20
|
-
const unit = match[2];
|
|
21
|
-
if (unit === "m")
|
|
22
|
-
return interval * 60 * 1000;
|
|
23
|
-
if (unit === "h")
|
|
24
|
-
return interval * 60 * 60 * 1000;
|
|
25
|
-
return interval * 24 * 60 * 60 * 1000;
|
|
26
|
-
}
|
|
27
|
-
class HeartbeatTimer {
|
|
28
|
-
agent;
|
|
29
|
-
sendToAgent;
|
|
30
|
-
readFileSync;
|
|
31
|
-
readdirSync;
|
|
32
|
-
heartbeatTaskDir;
|
|
33
|
-
runtimeStatePath;
|
|
34
|
-
now;
|
|
35
|
-
pendingTimer = null;
|
|
36
|
-
constructor(options) {
|
|
37
|
-
this.agent = options.agent;
|
|
38
|
-
this.sendToAgent = options.sendToAgent;
|
|
39
|
-
this.readFileSync = options.deps.readFileSync;
|
|
40
|
-
this.readdirSync = options.deps.readdirSync;
|
|
41
|
-
this.heartbeatTaskDir = options.deps.heartbeatTaskDir;
|
|
42
|
-
this.runtimeStatePath = options.deps.runtimeStatePath;
|
|
43
|
-
this.now = options.deps.now ?? (() => Date.now());
|
|
44
|
-
}
|
|
45
|
-
start() {
|
|
46
|
-
this.scheduleNext();
|
|
47
|
-
}
|
|
48
|
-
stop() {
|
|
49
|
-
if (this.pendingTimer !== null) {
|
|
50
|
-
clearTimeout(this.pendingTimer);
|
|
51
|
-
this.pendingTimer = null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
scheduleNext() {
|
|
55
|
-
const cadenceMs = this.readCadence();
|
|
56
|
-
const lastCompletedAt = this.readLastCompletedAt();
|
|
57
|
-
const nowMs = this.now();
|
|
58
|
-
let delay;
|
|
59
|
-
if (lastCompletedAt === null) {
|
|
60
|
-
// Never run before — fire once after a full cadence (not immediately, to avoid spin loop
|
|
61
|
-
// when the agent has no inner dialog state to write lastCompletedAt)
|
|
62
|
-
delay = cadenceMs;
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
const elapsed = nowMs - lastCompletedAt;
|
|
66
|
-
delay = Math.max(0, cadenceMs - elapsed);
|
|
67
|
-
}
|
|
68
|
-
this.pendingTimer = setTimeout(() => {
|
|
69
|
-
this.fire();
|
|
70
|
-
}, delay);
|
|
71
|
-
}
|
|
72
|
-
fire() {
|
|
73
|
-
this.pendingTimer = null;
|
|
74
|
-
(0, runtime_1.emitNervesEvent)({
|
|
75
|
-
component: "daemon",
|
|
76
|
-
event: "daemon.heartbeat_fire",
|
|
77
|
-
message: "heartbeat timer fired",
|
|
78
|
-
meta: { agent: this.agent },
|
|
79
|
-
});
|
|
80
|
-
this.sendToAgent(this.agent, { type: "heartbeat" });
|
|
81
|
-
this.scheduleNext();
|
|
82
|
-
}
|
|
83
|
-
readCadence() {
|
|
84
|
-
// Scan habits dir for *-heartbeat.md
|
|
85
|
-
let files;
|
|
86
|
-
try {
|
|
87
|
-
files = this.readdirSync(this.heartbeatTaskDir);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return exports.DEFAULT_CADENCE_MS;
|
|
91
|
-
}
|
|
92
|
-
const heartbeatFile = files.find((f) => f.endsWith("-heartbeat.md"));
|
|
93
|
-
if (!heartbeatFile)
|
|
94
|
-
return exports.DEFAULT_CADENCE_MS;
|
|
95
|
-
try {
|
|
96
|
-
const content = this.readFileSync(`${this.heartbeatTaskDir}/${heartbeatFile}`, "utf-8");
|
|
97
|
-
const cadence = this.extractCadenceFromTaskFile(content);
|
|
98
|
-
return cadence ?? exports.DEFAULT_CADENCE_MS;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return exports.DEFAULT_CADENCE_MS;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
extractCadenceFromTaskFile(content) {
|
|
105
|
-
// Parse frontmatter from task file to get cadence value
|
|
106
|
-
const lines = content.split(/\r?\n/);
|
|
107
|
-
if (lines[0]?.trim() !== "---")
|
|
108
|
-
return null;
|
|
109
|
-
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
110
|
-
if (closing === -1)
|
|
111
|
-
return null;
|
|
112
|
-
const rawFrontmatter = lines.slice(1, closing).join("\n");
|
|
113
|
-
const frontmatter = (0, parser_1.parseFrontmatter)(rawFrontmatter);
|
|
114
|
-
return parseCadenceMs(frontmatter.cadence);
|
|
115
|
-
}
|
|
116
|
-
readLastCompletedAt() {
|
|
117
|
-
try {
|
|
118
|
-
const raw = this.readFileSync(this.runtimeStatePath, "utf-8");
|
|
119
|
-
const state = JSON.parse(raw);
|
|
120
|
-
if (typeof state.lastCompletedAt !== "string")
|
|
121
|
-
return null;
|
|
122
|
-
const ms = new Date(state.lastCompletedAt).getTime();
|
|
123
|
-
if (Number.isNaN(ms))
|
|
124
|
-
return null;
|
|
125
|
-
return ms;
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
exports.HeartbeatTimer = HeartbeatTimer;
|