@ouro.bot/cli 0.1.0-alpha.13 → 0.1.0-alpha.131
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/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +147 -205
- package/changelog.json +814 -0
- package/dist/heart/active-work.js +622 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/commitments.js +105 -0
- package/dist/heart/config.js +66 -21
- package/dist/heart/core.js +518 -100
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/auth-flow.js +457 -0
- package/dist/heart/daemon/daemon-cli.js +1516 -195
- package/dist/heart/daemon/daemon-entry.js +43 -2
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +261 -1
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +7 -72
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-path-installer.js +57 -29
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +13 -0
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +50 -2
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +2 -2
- package/dist/heart/daemon/specialist-prompt.js +7 -4
- package/dist/heart/daemon/specialist-tools.js +52 -3
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +64 -21
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +197 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +159 -0
- package/dist/heart/providers/anthropic-token.js +163 -0
- package/dist/heart/providers/anthropic.js +195 -34
- package/dist/heart/providers/azure.js +115 -9
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +49 -14
- package/dist/heart/safe-workspace.js +381 -0
- package/dist/heart/session-activity.js +173 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/streaming.js +108 -24
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/tool-loop.js +194 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +14 -2
- package/dist/mind/bundle-manifest.js +12 -0
- package/dist/mind/context.js +60 -14
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/memory.js +27 -26
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +456 -77
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +12 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/context-pack.js +254 -0
- package/dist/repertoire/coding/feedback.js +301 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +210 -4
- package/dist/repertoire/coding/spawner.js +39 -9
- package/dist/repertoire/coding/tools.js +171 -4
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +198 -0
- package/dist/repertoire/skills.js +3 -26
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +925 -250
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +106 -53
- package/dist/senses/bluebubbles-client.js +210 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +915 -45
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +374 -131
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +388 -83
- package/dist/senses/pipeline.js +444 -0
- package/dist/senses/teams.js +607 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +9 -3
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
package/dist/senses/teams.js
CHANGED
|
@@ -40,10 +40,15 @@ exports.createTeamsCallbacks = createTeamsCallbacks;
|
|
|
40
40
|
exports.resolvePendingConfirmation = resolvePendingConfirmation;
|
|
41
41
|
exports.withConversationLock = withConversationLock;
|
|
42
42
|
exports.handleTeamsMessage = handleTeamsMessage;
|
|
43
|
+
exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
|
|
44
|
+
exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
|
|
43
45
|
exports.startTeamsApp = startTeamsApp;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
44
47
|
const teams_apps_1 = require("@microsoft/teams.apps");
|
|
45
48
|
const teams_dev_1 = require("@microsoft/teams.dev");
|
|
46
49
|
const core_1 = require("../heart/core");
|
|
50
|
+
const tools_1 = require("../repertoire/tools");
|
|
51
|
+
const channel_1 = require("../mind/friends/channel");
|
|
47
52
|
const config_1 = require("../heart/config");
|
|
48
53
|
const prompt_1 = require("../mind/prompt");
|
|
49
54
|
const phrases_1 = require("../mind/phrases");
|
|
@@ -54,12 +59,20 @@ const commands_1 = require("./commands");
|
|
|
54
59
|
const nerves_1 = require("../nerves");
|
|
55
60
|
const runtime_1 = require("../nerves/runtime");
|
|
56
61
|
const store_file_1 = require("../mind/friends/store-file");
|
|
62
|
+
const types_1 = require("../mind/friends/types");
|
|
57
63
|
const resolver_1 = require("../mind/friends/resolver");
|
|
58
64
|
const tokens_1 = require("../mind/friends/tokens");
|
|
59
65
|
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
60
66
|
const identity_1 = require("../heart/identity");
|
|
67
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
68
|
+
const progress_story_1 = require("../heart/progress-story");
|
|
69
|
+
const http = __importStar(require("http"));
|
|
61
70
|
const path = __importStar(require("path"));
|
|
62
71
|
const trust_gate_1 = require("./trust-gate");
|
|
72
|
+
const pipeline_1 = require("./pipeline");
|
|
73
|
+
const teamsFailoverStates = new Map();
|
|
74
|
+
const pending_1 = require("../mind/pending");
|
|
75
|
+
const continuity_1 = require("./continuity");
|
|
63
76
|
// Strip @mention markup from incoming messages.
|
|
64
77
|
// Removes <at>...</at> tags and trims extra whitespace.
|
|
65
78
|
// Fallback safety net -- the SDK's activity.mentions.stripText should handle
|
|
@@ -317,14 +330,29 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
317
330
|
onToolStart: (name, args) => {
|
|
318
331
|
stopPhraseRotation();
|
|
319
332
|
flushTextBuffer();
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
// Emit a placeholder to satisfy the 15s Copilot timeout for initial
|
|
334
|
+
// stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
|
|
335
|
+
// never emit before the timeout and the user sees "this response was
|
|
336
|
+
// stopped". The placeholder is replaced by actual content on next emit.
|
|
337
|
+
// https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
|
|
338
|
+
if (!streamHasContent)
|
|
339
|
+
safeEmit("⏳");
|
|
340
|
+
const argSummary = (0, tools_1.summarizeArgs)(name, args) || Object.keys(args).join(", ");
|
|
341
|
+
safeUpdate((0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
|
|
342
|
+
scope: "shared-work",
|
|
343
|
+
phase: "processing",
|
|
344
|
+
objective: `running ${name} (${argSummary})...`,
|
|
345
|
+
})));
|
|
322
346
|
hadToolRun = true;
|
|
323
347
|
},
|
|
324
348
|
onToolEnd: (name, summary, success) => {
|
|
325
349
|
stopPhraseRotation();
|
|
326
350
|
const msg = (0, format_1.formatToolResult)(name, summary, success);
|
|
327
|
-
safeUpdate(
|
|
351
|
+
safeUpdate((0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
|
|
352
|
+
scope: "shared-work",
|
|
353
|
+
phase: "processing",
|
|
354
|
+
objective: msg,
|
|
355
|
+
})));
|
|
328
356
|
},
|
|
329
357
|
onKick: () => {
|
|
330
358
|
stopPhraseRotation();
|
|
@@ -335,7 +363,11 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
335
363
|
stopPhraseRotation();
|
|
336
364
|
if (stopped)
|
|
337
365
|
return;
|
|
338
|
-
const msg = (0,
|
|
366
|
+
const msg = (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
|
|
367
|
+
scope: "shared-work",
|
|
368
|
+
phase: "errored",
|
|
369
|
+
outcomeText: (0, format_1.formatError)(error),
|
|
370
|
+
}));
|
|
339
371
|
if (severity === "transient") {
|
|
340
372
|
safeUpdate(msg);
|
|
341
373
|
}
|
|
@@ -424,6 +456,34 @@ function getFriendStore() {
|
|
|
424
456
|
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
425
457
|
return new store_file_1.FileFriendStore(friendsPath);
|
|
426
458
|
}
|
|
459
|
+
function createTeamsCommandRegistry() {
|
|
460
|
+
const registry = (0, commands_1.createCommandRegistry)();
|
|
461
|
+
(0, commands_1.registerDefaultCommands)(registry);
|
|
462
|
+
return registry;
|
|
463
|
+
}
|
|
464
|
+
function handleTeamsSlashCommand(text, registry, friendId, conversationId, stream, emitResponse = true) {
|
|
465
|
+
const parsed = (0, commands_1.parseSlashCommand)(text);
|
|
466
|
+
if (!parsed)
|
|
467
|
+
return null;
|
|
468
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
469
|
+
if (!dispatchResult.handled || !dispatchResult.result) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (dispatchResult.result.action === "new") {
|
|
473
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(friendId, "teams", conversationId));
|
|
474
|
+
if (emitResponse) {
|
|
475
|
+
stream.emit("session cleared");
|
|
476
|
+
}
|
|
477
|
+
return "new";
|
|
478
|
+
}
|
|
479
|
+
if (dispatchResult.result.action === "response") {
|
|
480
|
+
if (emitResponse) {
|
|
481
|
+
stream.emit(dispatchResult.result.message || "");
|
|
482
|
+
}
|
|
483
|
+
return "response";
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
427
487
|
// Handle an incoming Teams message
|
|
428
488
|
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
|
|
429
489
|
const turnKey = teamsTurnKey(conversationId);
|
|
@@ -434,148 +494,262 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
434
494
|
// before sync I/O (session load, trim) blocks the event loop.
|
|
435
495
|
stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
|
|
436
496
|
await new Promise(r => setImmediate(r));
|
|
437
|
-
// Resolve
|
|
497
|
+
// Resolve identity provider early for friend resolution + slash command session path
|
|
438
498
|
const store = getFriendStore();
|
|
439
499
|
const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
|
|
440
500
|
const externalId = teamsContext?.aadObjectId || conversationId;
|
|
441
|
-
|
|
501
|
+
// Build FriendResolver for the pipeline
|
|
502
|
+
const resolver = new resolver_1.FriendResolver(store, {
|
|
503
|
+
provider,
|
|
504
|
+
externalId,
|
|
505
|
+
tenantId: teamsContext?.tenantId,
|
|
506
|
+
displayName: teamsContext?.displayName || "Unknown",
|
|
507
|
+
channel: "teams",
|
|
508
|
+
});
|
|
509
|
+
// Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
|
|
510
|
+
const resolvedContext = await resolver.resolve();
|
|
511
|
+
const friendId = resolvedContext.friend.id;
|
|
512
|
+
const registry = createTeamsCommandRegistry();
|
|
513
|
+
// Check for slash commands (before pipeline -- these are transport-level concerns)
|
|
514
|
+
if (handleTeamsSlashCommand(text, registry, friendId, conversationId, stream)) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// ── Teams adapter concerns: controller, callbacks, session path ──────────
|
|
518
|
+
const controller = new AbortController();
|
|
519
|
+
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
520
|
+
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
|
|
521
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
522
|
+
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
523
|
+
const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
|
|
524
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
|
|
525
|
+
// Build Teams-specific toolContext fields for injection into the pipeline
|
|
526
|
+
const teamsToolContext = teamsContext ? {
|
|
442
527
|
graphToken: teamsContext.graphToken,
|
|
443
528
|
adoToken: teamsContext.adoToken,
|
|
444
529
|
githubToken: teamsContext.githubToken,
|
|
445
530
|
signin: teamsContext.signin,
|
|
446
|
-
friendStore: store,
|
|
447
531
|
summarize: (0, core_1.createSummarize)(),
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
532
|
+
tenantId: teamsContext.tenantId,
|
|
533
|
+
botApi: teamsContext.botApi,
|
|
534
|
+
} : {};
|
|
535
|
+
let currentText = text;
|
|
536
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
537
|
+
while (true) {
|
|
538
|
+
let drainedSteeringFollowUps = [];
|
|
539
|
+
// Build runAgentOptions with Teams-specific fields
|
|
540
|
+
const agentOptions = {
|
|
541
|
+
traceId,
|
|
542
|
+
toolContext: teamsToolContext,
|
|
543
|
+
mcpManager,
|
|
544
|
+
drainSteeringFollowUps: () => {
|
|
545
|
+
drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
|
|
546
|
+
.map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
|
|
547
|
+
return drainedSteeringFollowUps;
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
if (channelConfig.skipConfirmation)
|
|
551
|
+
agentOptions.skipConfirmation = true;
|
|
552
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
553
|
+
// Capture terminal errors — failover message replaces the error card if it triggers
|
|
554
|
+
let capturedTerminalError = null;
|
|
555
|
+
const teamsFailoverState = (() => {
|
|
556
|
+
if (!teamsFailoverStates.has(conversationId)) {
|
|
557
|
+
teamsFailoverStates.set(conversationId, { pending: null });
|
|
558
|
+
}
|
|
559
|
+
return teamsFailoverStates.get(conversationId);
|
|
560
|
+
})();
|
|
561
|
+
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
562
|
+
const failoverAwareCallbacks = {
|
|
563
|
+
...callbacks,
|
|
564
|
+
onError: (error, severity) => {
|
|
565
|
+
if (severity === "terminal" && teamsFailoverState) {
|
|
566
|
+
capturedTerminalError = error;
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
callbacks.onError(error, severity);
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
/* v8 ignore stop */
|
|
573
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
455
574
|
channel: "teams",
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
575
|
+
sessionKey: conversationId,
|
|
576
|
+
capabilities: teamsCapabilities,
|
|
577
|
+
messages: [{ role: "user", content: currentText }],
|
|
578
|
+
continuityIngressTexts: [currentText],
|
|
579
|
+
callbacks: failoverAwareCallbacks,
|
|
580
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
581
|
+
sessionLoader: {
|
|
582
|
+
loadOrCreate: async () => {
|
|
583
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
584
|
+
const messages = existing?.messages && existing.messages.length > 0
|
|
585
|
+
? existing.messages
|
|
586
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", { mcpManager }, resolvedContext) }];
|
|
587
|
+
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
588
|
+
return {
|
|
589
|
+
messages,
|
|
590
|
+
sessionPath: sessPath,
|
|
591
|
+
state: existing?.state,
|
|
592
|
+
};
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
pendingDir,
|
|
596
|
+
friendStore: store,
|
|
463
597
|
provider,
|
|
464
598
|
externalId,
|
|
465
599
|
tenantId: teamsContext?.tenantId,
|
|
466
|
-
|
|
600
|
+
isGroupChat: false,
|
|
601
|
+
groupHasFamilyMember: false,
|
|
602
|
+
hasExistingGroupWithFamily: false,
|
|
603
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
604
|
+
drainPending: pending_1.drainPending,
|
|
605
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
|
|
606
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
607
|
+
...opts,
|
|
608
|
+
toolContext: {
|
|
609
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
610
|
+
signin: async () => undefined,
|
|
611
|
+
...opts?.toolContext,
|
|
612
|
+
summarize: teamsToolContext.summarize,
|
|
613
|
+
},
|
|
614
|
+
}),
|
|
615
|
+
postTurn: context_1.postTurn,
|
|
616
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
617
|
+
signal: controller.signal,
|
|
618
|
+
runAgentOptions: agentOptions,
|
|
619
|
+
failoverState: teamsFailoverState,
|
|
467
620
|
});
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
621
|
+
/* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
|
|
622
|
+
if (result.failoverMessage) {
|
|
623
|
+
stream.emit(result.failoverMessage);
|
|
624
|
+
}
|
|
625
|
+
else if (capturedTerminalError) {
|
|
626
|
+
callbacks.onError(capturedTerminalError, "terminal");
|
|
627
|
+
}
|
|
628
|
+
/* v8 ignore stop */
|
|
629
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
630
|
+
if (!result.gateResult.allowed) {
|
|
631
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
632
|
+
stream.emit(result.gateResult.autoReply);
|
|
471
633
|
}
|
|
472
634
|
return;
|
|
473
635
|
}
|
|
636
|
+
// Flush any remaining accumulated text at end of turn
|
|
637
|
+
await callbacks.flush();
|
|
638
|
+
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
639
|
+
// This must happen after the stream is done so the OAuth card renders properly.
|
|
640
|
+
if (teamsContext && result.messages) {
|
|
641
|
+
const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
642
|
+
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
643
|
+
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
644
|
+
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
645
|
+
await teamsContext.signin(teamsContext.adoConnectionName);
|
|
646
|
+
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
647
|
+
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
648
|
+
}
|
|
649
|
+
if (result.turnOutcome !== "superseded") {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const supersedingIndex = drainedSteeringFollowUps
|
|
653
|
+
.map((followUp) => followUp.effect)
|
|
654
|
+
.lastIndexOf("clear_and_supersede");
|
|
655
|
+
if (supersedingIndex < 0) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
|
|
659
|
+
const replayTail = drainedSteeringFollowUps
|
|
660
|
+
.slice(supersedingIndex + 1)
|
|
661
|
+
.map((followUp) => followUp.text.trim())
|
|
662
|
+
.filter((followUpText) => followUpText.length > 0)
|
|
663
|
+
.join("\n");
|
|
664
|
+
if (replayTail) {
|
|
665
|
+
currentText = replayTail;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (handleTeamsSlashCommand(supersedingFollowUp.text, registry, friendId, conversationId, stream, false)) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
currentText = supersedingFollowUp.text;
|
|
474
672
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
673
|
+
}
|
|
674
|
+
// Internal port for the secondary bot App (not exposed externally).
|
|
675
|
+
// The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
|
|
676
|
+
const SECONDARY_INTERNAL_PORT = 3979;
|
|
677
|
+
// Collect all unique OAuth connection names across top-level config and tenant overrides.
|
|
678
|
+
/* v8 ignore start -- runtime Teams SDK config; no unit-testable surface @preserve */
|
|
679
|
+
function allOAuthConnectionNames() {
|
|
680
|
+
const oauthConfig = (0, config_1.getOAuthConfig)();
|
|
681
|
+
const names = new Set();
|
|
682
|
+
if (oauthConfig.graphConnectionName)
|
|
683
|
+
names.add(oauthConfig.graphConnectionName);
|
|
684
|
+
if (oauthConfig.adoConnectionName)
|
|
685
|
+
names.add(oauthConfig.adoConnectionName);
|
|
686
|
+
if (oauthConfig.githubConnectionName)
|
|
687
|
+
names.add(oauthConfig.githubConnectionName);
|
|
688
|
+
if (oauthConfig.tenantOverrides) {
|
|
689
|
+
for (const ov of Object.values(oauthConfig.tenantOverrides)) {
|
|
690
|
+
if (ov.graphConnectionName)
|
|
691
|
+
names.add(ov.graphConnectionName);
|
|
692
|
+
if (ov.adoConnectionName)
|
|
693
|
+
names.add(ov.adoConnectionName);
|
|
694
|
+
if (ov.githubConnectionName)
|
|
695
|
+
names.add(ov.githubConnectionName);
|
|
492
696
|
}
|
|
493
697
|
}
|
|
494
|
-
|
|
495
|
-
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
496
|
-
const existing = (0, context_1.loadSession)(sessPath);
|
|
497
|
-
const messages = existing?.messages && existing.messages.length > 0
|
|
498
|
-
? existing.messages
|
|
499
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
|
|
500
|
-
// Push user message
|
|
501
|
-
messages.push({ role: "user", content: text });
|
|
502
|
-
// Run agent
|
|
503
|
-
const controller = new AbortController();
|
|
504
|
-
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
505
|
-
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
|
|
506
|
-
const traceId = (0, nerves_1.createTraceId)();
|
|
507
|
-
const agentOptions = {};
|
|
508
|
-
agentOptions.traceId = traceId;
|
|
509
|
-
if (toolContext)
|
|
510
|
-
agentOptions.toolContext = toolContext;
|
|
511
|
-
if (channelConfig.skipConfirmation)
|
|
512
|
-
agentOptions.skipConfirmation = true;
|
|
513
|
-
agentOptions.drainSteeringFollowUps = () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text }));
|
|
514
|
-
const result = await (0, core_1.runAgent)(messages, callbacks, "teams", controller.signal, agentOptions);
|
|
515
|
-
// Flush any remaining accumulated text at end of turn
|
|
516
|
-
await callbacks.flush();
|
|
517
|
-
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
518
|
-
// This must happen after the stream is done so the OAuth card renders properly.
|
|
519
|
-
if (teamsContext) {
|
|
520
|
-
const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
521
|
-
if (allContent.includes("AUTH_REQUIRED:graph"))
|
|
522
|
-
await teamsContext.signin("graph");
|
|
523
|
-
if (allContent.includes("AUTH_REQUIRED:ado"))
|
|
524
|
-
await teamsContext.signin("ado");
|
|
525
|
-
if (allContent.includes("AUTH_REQUIRED:github"))
|
|
526
|
-
await teamsContext.signin("github");
|
|
527
|
-
}
|
|
528
|
-
// Trim context and save session
|
|
529
|
-
(0, context_1.postTurn)(messages, sessPath, result.usage);
|
|
530
|
-
// Accumulate token usage on friend record
|
|
531
|
-
if (toolContext?.context?.friend?.id) {
|
|
532
|
-
await (0, tokens_1.accumulateFriendTokens)(store, toolContext.context.friend.id, result.usage);
|
|
533
|
-
}
|
|
534
|
-
// SDK auto-closes the stream after our handler returns (app.process.js)
|
|
698
|
+
return [...names];
|
|
535
699
|
}
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
// Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
|
|
539
|
-
function startTeamsApp() {
|
|
700
|
+
// Create an App instance from a TeamsConfig. Returns { app, mode }.
|
|
701
|
+
function createBotApp(teamsConfig) {
|
|
540
702
|
const mentionStripping = { activity: { mentions: { stripText: true } } };
|
|
541
|
-
const teamsConfig = (0, config_2.getTeamsConfig)();
|
|
542
|
-
let app;
|
|
543
|
-
let mode;
|
|
544
703
|
const oauthConfig = (0, config_1.getOAuthConfig)();
|
|
545
|
-
if (teamsConfig.clientId) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
704
|
+
if (teamsConfig.clientId && teamsConfig.clientSecret) {
|
|
705
|
+
return {
|
|
706
|
+
app: new teams_apps_1.App({
|
|
707
|
+
clientId: teamsConfig.clientId,
|
|
708
|
+
clientSecret: teamsConfig.clientSecret,
|
|
709
|
+
tenantId: teamsConfig.tenantId,
|
|
710
|
+
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
711
|
+
...mentionStripping,
|
|
712
|
+
}),
|
|
713
|
+
mode: "Bot Service (client secret)",
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
else if (teamsConfig.clientId) {
|
|
717
|
+
return {
|
|
718
|
+
app: new teams_apps_1.App({
|
|
719
|
+
clientId: teamsConfig.clientId,
|
|
720
|
+
tenantId: teamsConfig.tenantId,
|
|
721
|
+
...(teamsConfig.managedIdentityClientId ? { managedIdentityClientId: teamsConfig.managedIdentityClientId } : {}),
|
|
722
|
+
oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
|
|
723
|
+
...mentionStripping,
|
|
724
|
+
}),
|
|
725
|
+
mode: "Bot Service (managed identity)",
|
|
726
|
+
};
|
|
555
727
|
}
|
|
556
728
|
else {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
729
|
+
return {
|
|
730
|
+
app: new teams_apps_1.App({
|
|
731
|
+
plugins: [new teams_dev_1.DevtoolsPlugin()],
|
|
732
|
+
...mentionStripping,
|
|
733
|
+
}),
|
|
734
|
+
mode: "DevtoolsPlugin",
|
|
735
|
+
};
|
|
563
736
|
}
|
|
737
|
+
}
|
|
738
|
+
/* v8 ignore stop */
|
|
739
|
+
// Register message, verify-state, and error handlers on an App instance.
|
|
740
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
741
|
+
function registerBotHandlers(app, label) {
|
|
742
|
+
const connectionNames = allOAuthConnectionNames();
|
|
564
743
|
// Override default OAuth verify-state handler. The SDK's built-in handler
|
|
565
744
|
// uses a single defaultConnectionName, which breaks multi-connection setups
|
|
566
745
|
// (graph + ado + github). The verifyState activity only carries a `state`
|
|
567
746
|
// code with no connectionName, so we try each configured connection until
|
|
568
747
|
// one succeeds.
|
|
569
|
-
const allConnectionNames = [
|
|
570
|
-
oauthConfig.graphConnectionName,
|
|
571
|
-
oauthConfig.adoConnectionName,
|
|
572
|
-
oauthConfig.githubConnectionName,
|
|
573
|
-
].filter(Boolean);
|
|
574
748
|
app.on("signin.verify-state", async (ctx) => {
|
|
575
749
|
const { api, activity } = ctx;
|
|
576
750
|
if (!activity.value?.state)
|
|
577
751
|
return { status: 404 };
|
|
578
|
-
for (const cn of
|
|
752
|
+
for (const cn of connectionNames) {
|
|
579
753
|
try {
|
|
580
754
|
await api.users.token.get({
|
|
581
755
|
channelId: activity.channelId,
|
|
@@ -583,12 +757,12 @@ function startTeamsApp() {
|
|
|
583
757
|
connectionName: cn,
|
|
584
758
|
code: activity.value.state,
|
|
585
759
|
});
|
|
586
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
|
|
760
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
|
|
587
761
|
return { status: 200 };
|
|
588
762
|
}
|
|
589
763
|
catch { /* try next */ }
|
|
590
764
|
}
|
|
591
|
-
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message:
|
|
765
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
|
|
592
766
|
return { status: 412 };
|
|
593
767
|
});
|
|
594
768
|
app.on("message", async (ctx) => {
|
|
@@ -598,18 +772,49 @@ function startTeamsApp() {
|
|
|
598
772
|
const turnKey = teamsTurnKey(convId);
|
|
599
773
|
const userId = activity.from?.id || "";
|
|
600
774
|
const channelId = activity.channelId || "msteams";
|
|
601
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message:
|
|
775
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
|
|
602
776
|
// Resolve pending confirmations IMMEDIATELY — before token fetches or
|
|
603
777
|
// the conversation lock. The original message holds the lock while
|
|
604
778
|
// awaiting confirmation, so acquiring it here would deadlock. Token
|
|
605
779
|
// fetches are also unnecessary (and slow) for a simple yes/no reply.
|
|
606
780
|
if (resolvePendingConfirmation(convId, text)) {
|
|
607
|
-
// Don't emit on this stream — the original message's stream is still
|
|
608
|
-
// active. Opening a second streaming response in the same conversation
|
|
609
|
-
// can corrupt the first. The original stream will show tool progress
|
|
610
|
-
// once the confirmation Promise resolves.
|
|
611
781
|
return;
|
|
612
782
|
}
|
|
783
|
+
const commandRegistry = createTeamsCommandRegistry();
|
|
784
|
+
const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
|
|
785
|
+
if (parsedSlashCommand) {
|
|
786
|
+
const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
|
|
787
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
788
|
+
if (dispatchResult.result.action === "response") {
|
|
789
|
+
stream.emit(dispatchResult.result.message || "");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (dispatchResult.result.action === "new") {
|
|
793
|
+
const commandStore = getFriendStore();
|
|
794
|
+
const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
|
|
795
|
+
const commandExternalId = activity.from?.aadObjectId || convId;
|
|
796
|
+
const commandResolver = new resolver_1.FriendResolver(commandStore, {
|
|
797
|
+
provider: commandProvider,
|
|
798
|
+
externalId: commandExternalId,
|
|
799
|
+
tenantId: activity.conversation?.tenantId,
|
|
800
|
+
displayName: activity.from?.name || "Unknown",
|
|
801
|
+
channel: "teams",
|
|
802
|
+
});
|
|
803
|
+
const commandContext = await commandResolver.resolve();
|
|
804
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
|
|
805
|
+
stream.emit("session cleared");
|
|
806
|
+
if (_turnCoordinator.isTurnActive(turnKey)) {
|
|
807
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
808
|
+
conversationId: convId,
|
|
809
|
+
text,
|
|
810
|
+
receivedAt: Date.now(),
|
|
811
|
+
effect: "clear_and_supersede",
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
613
818
|
// If this conversation already has an active turn, steer follow-up input
|
|
614
819
|
// into that turn and avoid starting a second concurrent turn.
|
|
615
820
|
if (!_turnCoordinator.tryBeginTurn(turnKey)) {
|
|
@@ -617,31 +822,35 @@ function startTeamsApp() {
|
|
|
617
822
|
conversationId: convId,
|
|
618
823
|
text,
|
|
619
824
|
receivedAt: Date.now(),
|
|
825
|
+
effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
|
|
620
826
|
});
|
|
621
827
|
return;
|
|
622
828
|
}
|
|
623
829
|
try {
|
|
830
|
+
// Resolve OAuth connection names for this user's tenant (supports per-tenant overrides).
|
|
831
|
+
const tenantId = activity.conversation?.tenantId;
|
|
832
|
+
const tenantOAuth = (0, config_1.resolveOAuthForTenant)(tenantId);
|
|
624
833
|
// Fetch tokens for both OAuth connections independently.
|
|
625
834
|
// Failures are silently caught -- the tool handler will request signin if needed.
|
|
626
835
|
let graphToken;
|
|
627
836
|
let adoToken;
|
|
628
837
|
let githubToken;
|
|
629
838
|
try {
|
|
630
|
-
const graphRes = await api.users.token.get({ userId, connectionName:
|
|
839
|
+
const graphRes = await api.users.token.get({ userId, connectionName: tenantOAuth.graphConnectionName, channelId });
|
|
631
840
|
graphToken = graphRes?.token;
|
|
632
841
|
}
|
|
633
842
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
634
843
|
try {
|
|
635
|
-
const adoRes = await api.users.token.get({ userId, connectionName:
|
|
844
|
+
const adoRes = await api.users.token.get({ userId, connectionName: tenantOAuth.adoConnectionName, channelId });
|
|
636
845
|
adoToken = adoRes?.token;
|
|
637
846
|
}
|
|
638
847
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
639
848
|
try {
|
|
640
|
-
const githubRes = await api.users.token.get({ userId, connectionName:
|
|
849
|
+
const githubRes = await api.users.token.get({ userId, connectionName: tenantOAuth.githubConnectionName, channelId });
|
|
641
850
|
githubToken = githubRes?.token;
|
|
642
851
|
}
|
|
643
852
|
catch { /* no token yet — tool handler will trigger signin */ }
|
|
644
|
-
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken } });
|
|
853
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.token_status", component: "channels", message: "oauth token availability", meta: { graph: !!graphToken, ado: !!adoToken, github: !!githubToken, tenantId } });
|
|
645
854
|
const teamsContext = {
|
|
646
855
|
graphToken,
|
|
647
856
|
adoToken,
|
|
@@ -661,6 +870,11 @@ function startTeamsApp() {
|
|
|
661
870
|
aadObjectId: activity.from?.aadObjectId,
|
|
662
871
|
tenantId: activity.conversation?.tenantId,
|
|
663
872
|
displayName: activity.from?.name,
|
|
873
|
+
graphConnectionName: tenantOAuth.graphConnectionName,
|
|
874
|
+
adoConnectionName: tenantOAuth.adoConnectionName,
|
|
875
|
+
githubConnectionName: tenantOAuth.githubConnectionName,
|
|
876
|
+
/* v8 ignore next -- bot API availability branch; requires live SDK context @preserve */
|
|
877
|
+
botApi: app.id && api ? { id: app.id, conversations: api.conversations } : undefined,
|
|
664
878
|
};
|
|
665
879
|
/* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
|
|
666
880
|
const ctxSend = async (t) => {
|
|
@@ -678,6 +892,227 @@ function startTeamsApp() {
|
|
|
678
892
|
_turnCoordinator.endTurn(turnKey);
|
|
679
893
|
}
|
|
680
894
|
});
|
|
895
|
+
app.event("error", ({ error }) => {
|
|
896
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
897
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
function findAadObjectId(friend) {
|
|
901
|
+
for (const ext of friend.externalIds) {
|
|
902
|
+
if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
|
|
903
|
+
return { aadObjectId: ext.externalId, tenantId: ext.tenantId };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
function resolveTeamsFriendStore(deps) {
|
|
909
|
+
return deps.store
|
|
910
|
+
?? deps.createFriendStore?.()
|
|
911
|
+
?? new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends"));
|
|
912
|
+
}
|
|
913
|
+
function getTeamsConversations(botApi) {
|
|
914
|
+
return botApi.conversations;
|
|
915
|
+
}
|
|
916
|
+
function hasExplicitCrossChatAuthorization(params) {
|
|
917
|
+
return params.intent === "explicit_cross_chat"
|
|
918
|
+
&& types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
|
|
919
|
+
}
|
|
920
|
+
async function sendProactiveTeamsMessageToSession(params, deps) {
|
|
921
|
+
const store = resolveTeamsFriendStore(deps);
|
|
922
|
+
const conversations = getTeamsConversations(deps.botApi);
|
|
923
|
+
let friend;
|
|
924
|
+
try {
|
|
925
|
+
friend = await store.get(params.friendId);
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
friend = null;
|
|
929
|
+
}
|
|
930
|
+
if (!friend) {
|
|
931
|
+
(0, runtime_1.emitNervesEvent)({
|
|
932
|
+
level: "warn",
|
|
933
|
+
component: "senses",
|
|
934
|
+
event: "senses.teams_proactive_no_friend",
|
|
935
|
+
message: "proactive send skipped: friend not found",
|
|
936
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
937
|
+
});
|
|
938
|
+
return { delivered: false, reason: "friend_not_found" };
|
|
939
|
+
}
|
|
940
|
+
if (!hasExplicitCrossChatAuthorization(params) && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
941
|
+
(0, runtime_1.emitNervesEvent)({
|
|
942
|
+
component: "senses",
|
|
943
|
+
event: "senses.teams_proactive_trust_skip",
|
|
944
|
+
message: "proactive send skipped: trust level not allowed",
|
|
945
|
+
meta: {
|
|
946
|
+
friendId: params.friendId,
|
|
947
|
+
trustLevel: friend.trustLevel ?? "unknown",
|
|
948
|
+
intent: params.intent ?? "generic_outreach",
|
|
949
|
+
authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
return { delivered: false, reason: "trust_skip" };
|
|
953
|
+
}
|
|
954
|
+
const aadInfo = findAadObjectId(friend);
|
|
955
|
+
if (!aadInfo) {
|
|
956
|
+
(0, runtime_1.emitNervesEvent)({
|
|
957
|
+
level: "warn",
|
|
958
|
+
component: "senses",
|
|
959
|
+
event: "senses.teams_proactive_no_aad_id",
|
|
960
|
+
message: "proactive send skipped: no AAD object ID found",
|
|
961
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
962
|
+
});
|
|
963
|
+
return { delivered: false, reason: "missing_target" };
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
const conversation = await conversations.create({
|
|
967
|
+
bot: { id: deps.botApi.id },
|
|
968
|
+
members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
|
|
969
|
+
tenantId: aadInfo.tenantId,
|
|
970
|
+
isGroup: false,
|
|
971
|
+
});
|
|
972
|
+
await conversations.activities(conversation.id).create({
|
|
973
|
+
type: "message",
|
|
974
|
+
text: params.text,
|
|
975
|
+
});
|
|
976
|
+
(0, runtime_1.emitNervesEvent)({
|
|
977
|
+
component: "senses",
|
|
978
|
+
event: "senses.teams_proactive_sent",
|
|
979
|
+
message: "proactive teams message sent",
|
|
980
|
+
meta: { friendId: params.friendId, aadObjectId: aadInfo.aadObjectId, sessionKey: params.sessionKey },
|
|
981
|
+
});
|
|
982
|
+
return { delivered: true };
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
(0, runtime_1.emitNervesEvent)({
|
|
986
|
+
level: "error",
|
|
987
|
+
component: "senses",
|
|
988
|
+
event: "senses.teams_proactive_send_error",
|
|
989
|
+
message: "proactive teams send failed",
|
|
990
|
+
meta: {
|
|
991
|
+
friendId: params.friendId,
|
|
992
|
+
aadObjectId: aadInfo.aadObjectId,
|
|
993
|
+
sessionKey: params.sessionKey,
|
|
994
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
return { delivered: false, reason: "send_error" };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function scanPendingTeamsFiles(pendingRoot) {
|
|
1001
|
+
const results = [];
|
|
1002
|
+
let friendIds;
|
|
1003
|
+
try {
|
|
1004
|
+
friendIds = fs.readdirSync(pendingRoot);
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
return results;
|
|
1008
|
+
}
|
|
1009
|
+
for (const friendId of friendIds) {
|
|
1010
|
+
const teamsDir = path.join(pendingRoot, friendId, "teams");
|
|
1011
|
+
let keys;
|
|
1012
|
+
try {
|
|
1013
|
+
keys = fs.readdirSync(teamsDir);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
for (const key of keys) {
|
|
1019
|
+
const keyDir = path.join(teamsDir, key);
|
|
1020
|
+
let files;
|
|
1021
|
+
try {
|
|
1022
|
+
files = fs.readdirSync(keyDir);
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
|
|
1028
|
+
const filePath = path.join(keyDir, file);
|
|
1029
|
+
try {
|
|
1030
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1031
|
+
results.push({ friendId, key, filePath, content });
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
// skip unreadable files
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return results;
|
|
1040
|
+
}
|
|
1041
|
+
async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
|
|
1042
|
+
const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
|
|
1043
|
+
const pendingFiles = scanPendingTeamsFiles(root);
|
|
1044
|
+
const result = { sent: 0, skipped: 0, failed: 0 };
|
|
1045
|
+
for (const { friendId, key, filePath, content } of pendingFiles) {
|
|
1046
|
+
let parsed;
|
|
1047
|
+
try {
|
|
1048
|
+
parsed = JSON.parse(content);
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
result.failed++;
|
|
1052
|
+
try {
|
|
1053
|
+
fs.unlinkSync(filePath);
|
|
1054
|
+
}
|
|
1055
|
+
catch { /* ignore */ }
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const messageText = typeof parsed.content === "string" ? parsed.content : "";
|
|
1059
|
+
if (!messageText.trim()) {
|
|
1060
|
+
result.skipped++;
|
|
1061
|
+
try {
|
|
1062
|
+
fs.unlinkSync(filePath);
|
|
1063
|
+
}
|
|
1064
|
+
catch { /* ignore */ }
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
const sendResult = await sendProactiveTeamsMessageToSession({
|
|
1068
|
+
friendId,
|
|
1069
|
+
sessionKey: key,
|
|
1070
|
+
text: messageText,
|
|
1071
|
+
intent: "generic_outreach",
|
|
1072
|
+
}, {
|
|
1073
|
+
botApi,
|
|
1074
|
+
store,
|
|
1075
|
+
});
|
|
1076
|
+
if (sendResult.delivered) {
|
|
1077
|
+
result.sent++;
|
|
1078
|
+
try {
|
|
1079
|
+
fs.unlinkSync(filePath);
|
|
1080
|
+
}
|
|
1081
|
+
catch { /* ignore */ }
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (sendResult.reason === "friend_not_found" || sendResult.reason === "trust_skip" || sendResult.reason === "missing_target") {
|
|
1085
|
+
result.skipped++;
|
|
1086
|
+
try {
|
|
1087
|
+
fs.unlinkSync(filePath);
|
|
1088
|
+
}
|
|
1089
|
+
catch { /* ignore */ }
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
result.failed++;
|
|
1093
|
+
}
|
|
1094
|
+
if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
|
|
1095
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1096
|
+
component: "senses",
|
|
1097
|
+
event: "senses.teams_proactive_drain_complete",
|
|
1098
|
+
message: "teams proactive drain complete",
|
|
1099
|
+
meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
return result;
|
|
1103
|
+
}
|
|
1104
|
+
// Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
|
|
1105
|
+
// Mode is determined by getTeamsConfig().clientId.
|
|
1106
|
+
// Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
|
|
1107
|
+
//
|
|
1108
|
+
// Dual-bot support: if teamsSecondary is configured with a clientId, a second App
|
|
1109
|
+
// instance starts on an internal port and the primary app proxies requests from
|
|
1110
|
+
// /api/messages-secondary to it. This lets a single App Service serve two bot
|
|
1111
|
+
// registrations (e.g. one per tenant) without SDK modifications.
|
|
1112
|
+
function startTeamsApp() {
|
|
1113
|
+
const teamsConfig = (0, config_2.getTeamsConfig)();
|
|
1114
|
+
const { app, mode } = createBotApp(teamsConfig);
|
|
1115
|
+
registerBotHandlers(app, "primary");
|
|
681
1116
|
if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
|
|
682
1117
|
const handler = (err) => {
|
|
683
1118
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -686,11 +1121,54 @@ function startTeamsApp() {
|
|
|
686
1121
|
handler.__agentHandler = true;
|
|
687
1122
|
process.on("unhandledRejection", handler);
|
|
688
1123
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: msg, meta: {} });
|
|
692
|
-
});
|
|
693
|
-
const port = (0, config_2.getTeamsChannelConfig)().port;
|
|
1124
|
+
/* v8 ignore next -- PORT env branch; runtime-only @preserve */
|
|
1125
|
+
const port = process.env.PORT ? Number(process.env.PORT) : (0, config_2.getTeamsChannelConfig)().port;
|
|
694
1126
|
app.start(port);
|
|
695
|
-
|
|
1127
|
+
// Diagnostic: log tool count at startup to verify deploy
|
|
1128
|
+
const startupTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)("teams"));
|
|
1129
|
+
const toolNames = startupTools.map((t) => t.function.name);
|
|
1130
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Teams bot started on port ${port} with ${mode} (chunked streaming)`, meta: { port, mode, toolCount: toolNames.length, hasProactive: toolNames.includes("teams_send_message") } });
|
|
1131
|
+
// --- Secondary bot (dual-bot support) ---
|
|
1132
|
+
// If teamsSecondary has a clientId, start a second App on an internal port
|
|
1133
|
+
// and proxy /api/messages-secondary on the primary app to it.
|
|
1134
|
+
/* v8 ignore start -- dual-bot proxy wiring; requires live Teams SDK + HTTP @preserve */
|
|
1135
|
+
const secondaryConfig = (0, config_1.getTeamsSecondaryConfig)();
|
|
1136
|
+
if (secondaryConfig.clientId) {
|
|
1137
|
+
const { app: secondaryApp, mode: secondaryMode } = createBotApp(secondaryConfig);
|
|
1138
|
+
registerBotHandlers(secondaryApp, "secondary");
|
|
1139
|
+
secondaryApp.start(SECONDARY_INTERNAL_PORT);
|
|
1140
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.app_started", component: "channels", message: `Secondary bot started on internal port ${SECONDARY_INTERNAL_PORT} with ${secondaryMode}`, meta: { port: SECONDARY_INTERNAL_PORT, mode: secondaryMode } });
|
|
1141
|
+
// Proxy: forward /api/messages-secondary on the primary app's Express
|
|
1142
|
+
// to localhost:SECONDARY_INTERNAL_PORT/api/messages.
|
|
1143
|
+
// The SDK's HttpPlugin exposes .post() bound to its Express instance.
|
|
1144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1145
|
+
const httpPlugin = app.http;
|
|
1146
|
+
httpPlugin.post("/api/messages-secondary", (req, res) => {
|
|
1147
|
+
const body = JSON.stringify(req.body);
|
|
1148
|
+
const proxyReq = http.request({
|
|
1149
|
+
hostname: "127.0.0.1",
|
|
1150
|
+
port: SECONDARY_INTERNAL_PORT,
|
|
1151
|
+
path: "/api/messages",
|
|
1152
|
+
method: "POST",
|
|
1153
|
+
headers: {
|
|
1154
|
+
...req.headers,
|
|
1155
|
+
"content-length": Buffer.byteLength(body).toString(),
|
|
1156
|
+
},
|
|
1157
|
+
}, (proxyRes) => {
|
|
1158
|
+
res.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
|
|
1159
|
+
proxyRes.pipe(res);
|
|
1160
|
+
});
|
|
1161
|
+
proxyReq.on("error", (err) => {
|
|
1162
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.proxy_error", component: "channels", message: `secondary proxy error: ${err.message}`, meta: {} });
|
|
1163
|
+
if (!res.headersSent) {
|
|
1164
|
+
res.writeHead(502);
|
|
1165
|
+
res.end("Bad Gateway");
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
proxyReq.write(body);
|
|
1169
|
+
proxyReq.end();
|
|
1170
|
+
});
|
|
1171
|
+
(0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.proxy_ready", component: "channels", message: "proxy /api/messages-secondary → secondary bot ready", meta: {} });
|
|
1172
|
+
}
|
|
1173
|
+
/* v8 ignore stop */
|
|
696
1174
|
}
|