@ouro.bot/cli 0.1.0-alpha.38 → 0.1.0-alpha.381
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -14
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
- package/changelog.json +2303 -2
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +140 -0
- package/dist/arc/episodes.js +117 -0
- package/dist/arc/intentions.js +133 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +237 -0
- package/dist/arc/packets.js +193 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +65 -0
- package/dist/heart/active-work.js +832 -0
- package/dist/heart/agent-entry.js +58 -3
- package/dist/heart/attachments/image-normalize.js +194 -0
- package/dist/heart/attachments/materialize.js +97 -0
- package/dist/heart/attachments/originals.js +88 -0
- package/dist/heart/attachments/render.js +29 -0
- package/dist/heart/attachments/sources/adapter.js +2 -0
- package/dist/heart/attachments/sources/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +16 -0
- package/dist/heart/attachments/store.js +103 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +378 -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/bundle-state.js +168 -0
- package/dist/heart/commitments.js +111 -0
- package/dist/heart/config-registry.js +304 -0
- package/dist/heart/config.js +111 -128
- package/dist/heart/core.js +803 -259
- package/dist/heart/cross-chat-delivery.js +131 -0
- package/dist/heart/daemon/agent-config-check.js +376 -0
- package/dist/heart/daemon/agent-discovery.js +79 -3
- package/dist/heart/daemon/agent-service.js +360 -0
- package/dist/heart/daemon/agentic-repair.js +205 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/cli-defaults.js +560 -0
- package/dist/heart/daemon/cli-exec.js +3220 -0
- package/dist/heart/daemon/cli-help.js +319 -0
- package/dist/heart/daemon/cli-parse.js +1060 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +560 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/daemon-cli.js +29 -1498
- package/dist/heart/daemon/daemon-entry.js +356 -3
- package/dist/heart/daemon/daemon-health.js +141 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +157 -12
- package/dist/heart/daemon/daemon-tombstone.js +236 -0
- package/dist/heart/daemon/daemon.js +774 -58
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +419 -0
- package/dist/heart/daemon/health-monitor.js +79 -1
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/interactive-repair.js +182 -0
- package/dist/heart/daemon/launchd.js +46 -9
- package/dist/heart/daemon/log-tailer.js +82 -12
- package/dist/heart/daemon/logs-prune.js +105 -0
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/ouro-bot-entry.js +1 -1
- package/dist/heart/daemon/process-manager.js +201 -0
- package/dist/heart/daemon/provider-discovery.js +137 -0
- package/dist/heart/daemon/pulse.js +475 -0
- package/dist/heart/daemon/run-hooks.js +2 -0
- package/dist/heart/daemon/runtime-logging.js +67 -16
- package/dist/heart/daemon/runtime-metadata.js +73 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sense-manager.js +119 -30
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +307 -0
- package/dist/heart/daemon/stale-bundle-prune.js +96 -0
- package/dist/heart/daemon/startup-tui.js +237 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/thoughts.js +510 -0
- package/dist/heart/daemon/up-progress.js +135 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/habits/habit-migration.js +181 -0
- package/dist/heart/habits/habit-parser.js +140 -0
- package/dist/heart/habits/habit-scheduler.js +371 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -126
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +10 -8
- package/dist/heart/{daemon → hatch}/specialist-tools.js +30 -10
- package/dist/heart/identity.js +153 -65
- package/dist/heart/kept-notes.js +357 -0
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mcp/mcp-server.js +653 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +59 -0
- package/dist/heart/outlook/outlook-http-hooks.js +64 -0
- package/dist/heart/outlook/outlook-http-response.js +7 -0
- package/dist/heart/outlook/outlook-http-routes.js +232 -0
- package/dist/heart/outlook/outlook-http-static.js +99 -0
- package/dist/heart/outlook/outlook-http-transport.js +116 -0
- package/dist/heart/outlook/outlook-http.js +99 -0
- package/dist/heart/outlook/outlook-read.js +28 -0
- package/dist/heart/outlook/outlook-types.js +27 -0
- package/dist/heart/outlook/outlook-view.js +195 -0
- package/dist/heart/outlook/readers/agent-machine.js +359 -0
- package/dist/heart/outlook/readers/continuity-readers.js +332 -0
- package/dist/heart/outlook/readers/runtime-readers.js +660 -0
- package/dist/heart/outlook/readers/sessions.js +232 -0
- package/dist/heart/outlook/readers/shared.js +111 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-attempt.js +133 -0
- package/dist/heart/provider-binding-resolver.js +239 -0
- package/dist/heart/provider-credentials.js +379 -0
- package/dist/heart/provider-failover.js +266 -0
- package/dist/heart/provider-models.js +81 -0
- package/dist/heart/provider-ping.js +237 -0
- package/dist/heart/provider-state.js +216 -0
- package/dist/heart/provider-visibility.js +186 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +193 -55
- package/dist/heart/providers/azure.js +103 -12
- package/dist/heart/providers/error-classification.js +63 -0
- package/dist/heart/providers/github-copilot.js +145 -0
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +29 -7
- package/dist/heart/providers/openai-codex.js +39 -29
- package/dist/heart/runtime-credentials.js +181 -0
- package/dist/heart/session-activity.js +190 -0
- package/dist/heart/session-events.js +855 -0
- package/dist/heart/session-transcript.js +167 -0
- package/dist/heart/start-of-turn-packet.js +345 -0
- package/dist/heart/streaming.js +36 -27
- package/dist/heart/sync.js +332 -0
- package/dist/heart/target-resolution.js +127 -0
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/tool-activity-callbacks.js +36 -0
- package/dist/heart/tool-description.js +135 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +351 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +1 -1
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +301 -0
- package/dist/heart/versioning/ouro-version-manager.js +295 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +12 -2
- package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
- package/dist/mind/bundle-manifest.js +7 -1
- package/dist/mind/context.js +141 -94
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +84 -96
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/first-impressions.js +14 -1
- package/dist/mind/friends/channel.js +48 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/resolver.js +38 -1
- package/dist/mind/friends/store-file.js +39 -3
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +9 -1
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/note-search.js +268 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/pending.js +74 -7
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +966 -183
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +7 -1
- package/dist/nerves/coverage/audit-rules.js +15 -6
- package/dist/nerves/coverage/audit.js +28 -2
- package/dist/nerves/coverage/cli.js +1 -1
- package/dist/nerves/coverage/file-completeness.js +83 -5
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +224 -4
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/redact.js +79 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/outlook-ui/assets/index-BAcU08c-.css +1 -0
- package/dist/outlook-ui/assets/index-D7l3l4vY.js +61 -0
- package/dist/outlook-ui/index.html +15 -0
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +11 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +365 -0
- package/dist/repertoire/bundle-templates.js +72 -0
- package/dist/repertoire/bw-installer.js +79 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +330 -0
- package/dist/repertoire/coding/feedback.js +197 -30
- package/dist/repertoire/coding/manager.js +158 -9
- package/dist/repertoire/coding/spawner.js +55 -9
- package/dist/repertoire/coding/tools.js +170 -7
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +107 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +371 -0
- package/dist/repertoire/mcp-client.js +255 -0
- package/dist/repertoire/mcp-manager.js +305 -0
- package/dist/repertoire/mcp-tools.js +63 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +15 -24
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tasks/board.js +43 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +28 -10
- package/dist/repertoire/tasks/lifecycle.js +2 -2
- package/dist/repertoire/tasks/parser.js +3 -2
- package/dist/repertoire/tasks/scanner.js +194 -37
- package/dist/repertoire/tasks/transitions.js +16 -79
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-attachments.js +317 -0
- package/dist/repertoire/tools-base.js +42 -690
- package/dist/repertoire/tools-bluebubbles.js +1 -0
- package/dist/repertoire/tools-bridge.js +141 -0
- package/dist/repertoire/tools-bundle.js +984 -0
- package/dist/repertoire/tools-config.js +185 -0
- package/dist/repertoire/tools-continuity.js +248 -0
- package/dist/repertoire/tools-credential.js +182 -0
- package/dist/repertoire/tools-files.js +342 -0
- package/dist/repertoire/tools-flight.js +224 -0
- package/dist/repertoire/tools-flow.js +105 -0
- package/dist/repertoire/tools-github.js +1 -7
- package/dist/repertoire/tools-notes.js +376 -0
- package/dist/repertoire/tools-session.js +739 -0
- package/dist/repertoire/tools-shell.js +120 -0
- package/dist/repertoire/tools-stripe.js +180 -0
- package/dist/repertoire/tools-surface.js +243 -0
- package/dist/repertoire/tools-teams.js +12 -62
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-user-profile.js +144 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools.js +144 -120
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/user-profile.js +118 -0
- package/dist/repertoire/vault-setup.js +241 -0
- package/dist/repertoire/vault-unlock.js +364 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/attention-queue.js +116 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +260 -9
- package/dist/senses/bluebubbles/entry.js +70 -0
- package/dist/senses/bluebubbles/inbound-log.js +113 -0
- package/dist/senses/bluebubbles/index.js +1620 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +43 -12
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +46 -6
- package/dist/senses/bluebubbles/replay.js +129 -0
- package/dist/senses/bluebubbles/runtime-state.js +109 -0
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +605 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +83 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +240 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +574 -254
- package/dist/senses/commands.js +66 -3
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog-worker.js +112 -19
- package/dist/senses/inner-dialog.js +630 -82
- package/dist/senses/pipeline.js +602 -0
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/shared-turn.js +205 -0
- package/dist/senses/surface-tool.js +68 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +585 -231
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +28 -7
- package/skills/agent-commerce.md +106 -0
- package/skills/browser-navigation.md +110 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -0
- package/skills/configure-dev-tools.md +101 -0
- package/skills/travel-planning.md +134 -0
- package/dist/heart/daemon/ouro-path-installer.js +0 -178
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/dist/mind/associative-recall.js +0 -197
- package/dist/senses/bluebubbles-entry.js +0 -11
- package/dist/senses/bluebubbles.js +0 -736
- package/dist/senses/debug-activity.js +0 -127
- package/subagents/README.md +0 -60
- package/subagents/work-doer.md +0 -235
- package/subagents/work-merger.md +0 -618
- package/subagents/work-planner.md +0 -382
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
- /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
- /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
- /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
- /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
package/dist/senses/teams.js
CHANGED
|
@@ -34,12 +34,16 @@ 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
|
-
exports.resolvePendingConfirmation = resolvePendingConfirmation;
|
|
41
44
|
exports.withConversationLock = withConversationLock;
|
|
42
45
|
exports.handleTeamsMessage = handleTeamsMessage;
|
|
46
|
+
exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
|
|
43
47
|
exports.drainAndSendPendingTeams = drainAndSendPendingTeams;
|
|
44
48
|
exports.startTeamsApp = startTeamsApp;
|
|
45
49
|
const fs = __importStar(require("fs"));
|
|
@@ -57,14 +61,34 @@ const context_1 = require("../mind/context");
|
|
|
57
61
|
const commands_1 = require("./commands");
|
|
58
62
|
const nerves_1 = require("../nerves");
|
|
59
63
|
const runtime_1 = require("../nerves/runtime");
|
|
64
|
+
const proactive_content_guard_1 = require("./proactive-content-guard");
|
|
60
65
|
const store_file_1 = require("../mind/friends/store-file");
|
|
66
|
+
const types_1 = require("../mind/friends/types");
|
|
61
67
|
const resolver_1 = require("../mind/friends/resolver");
|
|
62
68
|
const tokens_1 = require("../mind/friends/tokens");
|
|
63
69
|
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
64
70
|
const identity_1 = require("../heart/identity");
|
|
71
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
72
|
+
const progress_story_1 = require("../heart/progress-story");
|
|
73
|
+
const tool_activity_callbacks_1 = require("../heart/tool-activity-callbacks");
|
|
74
|
+
const commands_2 = require("./commands");
|
|
65
75
|
const http = __importStar(require("http"));
|
|
66
76
|
const path = __importStar(require("path"));
|
|
67
77
|
const trust_gate_1 = require("./trust-gate");
|
|
78
|
+
const pipeline_1 = require("./pipeline");
|
|
79
|
+
const teamsFailoverStates = new Map();
|
|
80
|
+
const pending_1 = require("../mind/pending");
|
|
81
|
+
const continuity_1 = require("./continuity");
|
|
82
|
+
// AIGeneratedContent entity and feedbackLoopEnabled channelData for all outbound
|
|
83
|
+
// Teams messages. Required by Teams AI UX best practices.
|
|
84
|
+
function aiLabelEntities() {
|
|
85
|
+
return [{
|
|
86
|
+
type: "https://schema.org/Message",
|
|
87
|
+
"@type": "Message",
|
|
88
|
+
"@context": "https://schema.org",
|
|
89
|
+
additionalType: ["AIGeneratedContent"],
|
|
90
|
+
}];
|
|
91
|
+
}
|
|
68
92
|
// Strip @mention markup from incoming messages.
|
|
69
93
|
// Removes <at>...</at> tags and trims extra whitespace.
|
|
70
94
|
// Fallback safety net -- the SDK's activity.mentions.stripText should handle
|
|
@@ -119,6 +143,46 @@ function splitMessage(text, maxLen) {
|
|
|
119
143
|
}
|
|
120
144
|
return chunks;
|
|
121
145
|
}
|
|
146
|
+
// Sanitize user-provided feedback comments: truncate, strip control chars and newlines.
|
|
147
|
+
function sanitizeFeedbackComment(comment) {
|
|
148
|
+
const cleaned = comment.replace(/[\x00-\x1f\n\r]/g, "");
|
|
149
|
+
return cleaned.length > 200 ? cleaned.slice(0, 200) : cleaned;
|
|
150
|
+
}
|
|
151
|
+
// Build synthetic message text from a Teams feedback reaction.
|
|
152
|
+
function buildFeedbackSyntheticText(reaction, comment) {
|
|
153
|
+
const emoji = reaction === "like" ? "thumbs-up" : "thumbs-down";
|
|
154
|
+
if (comment) {
|
|
155
|
+
const sanitized = sanitizeFeedbackComment(comment);
|
|
156
|
+
return `[reacted with ${emoji} to your message: "${sanitized}"]`;
|
|
157
|
+
}
|
|
158
|
+
return `[reacted with ${emoji} to your message]`;
|
|
159
|
+
}
|
|
160
|
+
// Build a welcome Adaptive Card with prompt starters for new bot installs.
|
|
161
|
+
function buildWelcomeCard() {
|
|
162
|
+
const promptStarters = [
|
|
163
|
+
"What can you help me with?",
|
|
164
|
+
"Tell me about yourself",
|
|
165
|
+
"What's on my calendar today?",
|
|
166
|
+
"Summarize my recent emails",
|
|
167
|
+
];
|
|
168
|
+
return {
|
|
169
|
+
type: "AdaptiveCard",
|
|
170
|
+
version: "1.5",
|
|
171
|
+
body: [
|
|
172
|
+
{
|
|
173
|
+
type: "TextBlock",
|
|
174
|
+
text: "Hey! I'm here and ready to help. Try one of these to get started, or just ask me anything.",
|
|
175
|
+
wrap: true,
|
|
176
|
+
size: "Medium",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
actions: promptStarters.map((prompt) => ({
|
|
180
|
+
type: "Action.Submit",
|
|
181
|
+
title: prompt,
|
|
182
|
+
data: { msteams: { type: "messageBack", text: prompt, displayText: prompt } },
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
122
186
|
// Create Teams-specific callbacks for the agent loop.
|
|
123
187
|
// The SDK handles cumulative text, debouncing (500ms), and the streaming
|
|
124
188
|
// protocol (streamSequence, streamId, informative/streaming/final types).
|
|
@@ -131,12 +195,16 @@ function splitMessage(text, maxLen) {
|
|
|
131
195
|
// (transient status) or safeSend (terminal errors). Reasoning is accumulated
|
|
132
196
|
// and periodically pushed via safeUpdate on the same flush timer tick.
|
|
133
197
|
function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
198
|
+
const MIN_INITIAL_CHARS = 20;
|
|
134
199
|
let stopped = false; // set when stream signals cancellation (403)
|
|
135
200
|
let hadToolRun = false;
|
|
136
201
|
let hadRealOutput = false; // true once reasoning/tool output shown; suppresses phrases
|
|
137
202
|
let reasoningBuf = ""; // accumulated reasoning text for status display
|
|
203
|
+
let totalEmitted = 0; // cumulative chars emitted via safeEmit (for >4000 finalization)
|
|
204
|
+
let streamFinalized = false; // true after stream.close() — subsequent flushes go to safeSend
|
|
138
205
|
let textBuffer = ""; // accumulated text output for chunked streaming
|
|
139
206
|
let streamHasContent = false; // tracks whether primary output has received content
|
|
207
|
+
let firstContentEmitted = false; // true after first content push — disables MIN_INITIAL_CHARS threshold
|
|
140
208
|
let phraseTimer = null;
|
|
141
209
|
let lastPhrase = "";
|
|
142
210
|
let flushTimer = null;
|
|
@@ -186,15 +254,16 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
186
254
|
result.catch(() => markStopped());
|
|
187
255
|
}
|
|
188
256
|
}
|
|
189
|
-
// Safely emit a text delta to the stream.
|
|
257
|
+
// Safely emit a text delta to the stream with AI labels.
|
|
190
258
|
// On error (e.g. 403 from Teams stop button), abort the controller.
|
|
191
259
|
function safeEmit(text) {
|
|
192
260
|
/* v8 ignore next -- defensive guard: stopped set by prior 403; tested via flush abort path @preserve */
|
|
193
261
|
if (stopped)
|
|
194
262
|
return;
|
|
195
263
|
try {
|
|
196
|
-
catchAsync(stream.emit(text));
|
|
264
|
+
catchAsync(stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } }));
|
|
197
265
|
streamHasContent = true;
|
|
266
|
+
totalEmitted += text.length;
|
|
198
267
|
}
|
|
199
268
|
catch {
|
|
200
269
|
markStopped();
|
|
@@ -209,7 +278,7 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
209
278
|
try {
|
|
210
279
|
// stream.emit() is typed as void but the Teams SDK returns a Promise
|
|
211
280
|
// internally (async HTTP). Cast to capture the result for awaiting.
|
|
212
|
-
const result = stream.emit(text);
|
|
281
|
+
const result = stream.emit({ text, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
213
282
|
streamHasContent = true;
|
|
214
283
|
if (result && typeof result.then === "function") {
|
|
215
284
|
await result;
|
|
@@ -265,11 +334,49 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
265
334
|
// emitted text into a single streaming message (cumulative), so every
|
|
266
335
|
// periodic flush appends to the same response — not separate messages.
|
|
267
336
|
// No preemptive splitting — sends full text. Error recovery happens in flush().
|
|
337
|
+
// Hybrid MIN_INITIAL_CHARS: hold back until >= MIN_INITIAL_CHARS accumulated
|
|
338
|
+
// before the first content emit, so phrase rotation shows while real content
|
|
339
|
+
// buffers. After first emit, flush normally (no threshold).
|
|
268
340
|
function flushTextBuffer() {
|
|
269
341
|
if (!textBuffer)
|
|
270
342
|
return;
|
|
343
|
+
if (!firstContentEmitted && textBuffer.length < MIN_INITIAL_CHARS)
|
|
344
|
+
return;
|
|
345
|
+
// Proactive >4000 finalization: if cumulative emitted + buffer >= RECOVERY_CHUNK_SIZE,
|
|
346
|
+
// finalize the stream and send overflow via safeSend (follow-up message).
|
|
347
|
+
if (!streamFinalized && totalEmitted + textBuffer.length >= RECOVERY_CHUNK_SIZE) {
|
|
348
|
+
const remaining = RECOVERY_CHUNK_SIZE - totalEmitted;
|
|
349
|
+
/* v8 ignore next 2 -- defensive: remaining always > 0 because finalization runs once @preserve */
|
|
350
|
+
if (remaining > 0)
|
|
351
|
+
safeEmit(textBuffer.slice(0, remaining));
|
|
352
|
+
try {
|
|
353
|
+
stream.close();
|
|
354
|
+
}
|
|
355
|
+
catch { /* stream may already be dead */ }
|
|
356
|
+
streamFinalized = true;
|
|
357
|
+
/* v8 ignore next -- defensive ternary: remaining always > 0 at first finalization @preserve */
|
|
358
|
+
const overflow = textBuffer.slice(remaining > 0 ? remaining : 0);
|
|
359
|
+
textBuffer = "";
|
|
360
|
+
if (overflow)
|
|
361
|
+
safeSend(overflow);
|
|
362
|
+
if (!firstContentEmitted) {
|
|
363
|
+
firstContentEmitted = true;
|
|
364
|
+
stopPhraseRotation();
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (streamFinalized) {
|
|
369
|
+
// After finalization, all content goes to safeSend (follow-up messages)
|
|
370
|
+
safeSend(textBuffer);
|
|
371
|
+
textBuffer = "";
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
271
374
|
safeEmit(textBuffer);
|
|
272
375
|
textBuffer = "";
|
|
376
|
+
if (!firstContentEmitted) {
|
|
377
|
+
firstContentEmitted = true;
|
|
378
|
+
stopPhraseRotation();
|
|
379
|
+
}
|
|
273
380
|
}
|
|
274
381
|
function startPhraseRotation(pool) {
|
|
275
382
|
stopPhraseRotation();
|
|
@@ -312,32 +419,45 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
312
419
|
onTextChunk: (text) => {
|
|
313
420
|
if (stopped)
|
|
314
421
|
return;
|
|
315
|
-
|
|
422
|
+
// Don't stop phrase rotation here — let it continue until first content
|
|
423
|
+
// emit (handled in flushTextBuffer when MIN_INITIAL_CHARS threshold met).
|
|
316
424
|
textBuffer += text;
|
|
317
425
|
startFlushTimer();
|
|
318
426
|
},
|
|
319
427
|
onClearText: () => {
|
|
320
428
|
textBuffer = "";
|
|
321
429
|
},
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
430
|
+
...(() => {
|
|
431
|
+
const toolCbs = (0, tool_activity_callbacks_1.createToolActivityCallbacks)({
|
|
432
|
+
onDescription: (text) => safeUpdate(text),
|
|
433
|
+
/* v8 ignore next -- onResult only called in debug mode; tested via tool-activity-callbacks.test.ts @preserve */
|
|
434
|
+
onResult: (text) => safeUpdate(text),
|
|
435
|
+
/* v8 ignore next -- onFailure tested via onToolEnd failure test @preserve */
|
|
436
|
+
onFailure: (text) => safeUpdate(text),
|
|
437
|
+
isDebug: commands_2.getDebugMode,
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
onToolStart: (name, args) => {
|
|
441
|
+
stopPhraseRotation();
|
|
442
|
+
// Force-flush any accumulated text, bypassing MIN_INITIAL_CHARS threshold
|
|
443
|
+
firstContentEmitted = true;
|
|
444
|
+
flushTextBuffer();
|
|
445
|
+
// Emit a placeholder to satisfy the 15s Copilot timeout for initial
|
|
446
|
+
// stream.emit(). Without this, long tool chains (e.g. ADO batch ops)
|
|
447
|
+
// never emit before the timeout and the user sees "this response was
|
|
448
|
+
// stopped". The placeholder is replaced by actual content on next emit.
|
|
449
|
+
// https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
|
|
450
|
+
if (!streamHasContent)
|
|
451
|
+
safeEmit("\u23f3");
|
|
452
|
+
toolCbs.onToolStart(name, args);
|
|
453
|
+
hadToolRun = true;
|
|
454
|
+
},
|
|
455
|
+
onToolEnd: (name, summary, success) => {
|
|
456
|
+
stopPhraseRotation();
|
|
457
|
+
toolCbs.onToolEnd(name, summary, success);
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
})(),
|
|
341
461
|
onKick: () => {
|
|
342
462
|
stopPhraseRotation();
|
|
343
463
|
const msg = (0, format_1.formatKick)();
|
|
@@ -347,7 +467,11 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
347
467
|
stopPhraseRotation();
|
|
348
468
|
if (stopped)
|
|
349
469
|
return;
|
|
350
|
-
const msg = (0,
|
|
470
|
+
const msg = (0, progress_story_1.renderProgressStory)((0, progress_story_1.buildProgressStory)({
|
|
471
|
+
scope: "shared-work",
|
|
472
|
+
phase: "errored",
|
|
473
|
+
outcomeText: (0, format_1.formatError)(error),
|
|
474
|
+
}));
|
|
351
475
|
if (severity === "transient") {
|
|
352
476
|
safeUpdate(msg);
|
|
353
477
|
}
|
|
@@ -355,38 +479,27 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
355
479
|
safeSend(msg);
|
|
356
480
|
}
|
|
357
481
|
},
|
|
358
|
-
onConfirmAction: options?.conversationId
|
|
359
|
-
? async (name, args) => {
|
|
360
|
-
const convId = options.conversationId;
|
|
361
|
-
const argsDesc = Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
362
|
-
safeUpdate(`Confirm action: ${name} (${argsDesc}) -- reply "yes" to confirm or "no" to cancel`);
|
|
363
|
-
return new Promise((resolve) => {
|
|
364
|
-
_pendingConfirmations.set(convId, resolve);
|
|
365
|
-
// Auto-deny after 2 minutes to prevent indefinite blocking
|
|
366
|
-
// (e.g. when the stream dies and the user never sees the prompt).
|
|
367
|
-
setTimeout(() => {
|
|
368
|
-
if (_pendingConfirmations.has(convId)) {
|
|
369
|
-
_pendingConfirmations.delete(convId);
|
|
370
|
-
resolve("denied");
|
|
371
|
-
}
|
|
372
|
-
}, 120_000);
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
: undefined,
|
|
376
482
|
flush: async () => {
|
|
377
483
|
stopFlushTimer();
|
|
484
|
+
stopPhraseRotation();
|
|
378
485
|
if (textBuffer) {
|
|
486
|
+
// Bypass MIN_INITIAL_CHARS threshold — flush delivers all remaining content
|
|
487
|
+
firstContentEmitted = true;
|
|
379
488
|
const text = textBuffer;
|
|
380
489
|
textBuffer = "";
|
|
381
|
-
if (
|
|
490
|
+
if (streamFinalized && sendMessage) {
|
|
491
|
+
// Stream already finalized (>4000 path) — send remaining content as follow-up
|
|
492
|
+
safeSend(text);
|
|
493
|
+
}
|
|
494
|
+
else if (!stopped) {
|
|
382
495
|
// Stream is alive — await the emit so we can catch async 413/failure
|
|
383
496
|
// and fall through to sendMessage recovery.
|
|
384
497
|
const ok = await tryEmit(text);
|
|
385
498
|
if (!ok)
|
|
386
499
|
markStopped();
|
|
387
500
|
}
|
|
388
|
-
if (stopped && sendMessage) {
|
|
389
|
-
// Stream is dead — fall back to sendMessage; split on failure as recovery.
|
|
501
|
+
if (stopped && !streamFinalized && sendMessage) {
|
|
502
|
+
// Stream is dead (not from finalization) — fall back to sendMessage; split on failure as recovery.
|
|
390
503
|
try {
|
|
391
504
|
await sendMessage(text);
|
|
392
505
|
}
|
|
@@ -397,32 +510,12 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
|
|
|
397
510
|
}
|
|
398
511
|
}
|
|
399
512
|
}
|
|
400
|
-
else if (!streamHasContent) {
|
|
401
|
-
safeEmit("(completed with tool calls only
|
|
513
|
+
else if (!streamHasContent && !options?.suppressEmptyStreamMessage) {
|
|
514
|
+
safeEmit("(completed with tool calls only — no text response)");
|
|
402
515
|
}
|
|
403
516
|
},
|
|
404
517
|
};
|
|
405
518
|
}
|
|
406
|
-
// Per-conversation pending confirmation resolvers.
|
|
407
|
-
// When a mutate tool needs confirmation, the resolver is stored here.
|
|
408
|
-
// The next message from the same conversation resolves it.
|
|
409
|
-
const _pendingConfirmations = new Map();
|
|
410
|
-
// Confirmation response words (case-insensitive)
|
|
411
|
-
const CONFIRM_WORDS = new Set(["yes", "confirm", "go", "y", "ok", "approve", "proceed"]);
|
|
412
|
-
function resolvePendingConfirmation(convId, text) {
|
|
413
|
-
const resolver = _pendingConfirmations.get(convId);
|
|
414
|
-
if (!resolver)
|
|
415
|
-
return false;
|
|
416
|
-
_pendingConfirmations.delete(convId);
|
|
417
|
-
const word = text.trim().toLowerCase();
|
|
418
|
-
if (CONFIRM_WORDS.has(word)) {
|
|
419
|
-
resolver("confirmed");
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
resolver("denied");
|
|
423
|
-
}
|
|
424
|
-
return true;
|
|
425
|
-
}
|
|
426
519
|
const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
|
|
427
520
|
function teamsTurnKey(conversationId) {
|
|
428
521
|
return `teams:${conversationId}`;
|
|
@@ -436,118 +529,233 @@ function getFriendStore() {
|
|
|
436
529
|
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
437
530
|
return new store_file_1.FileFriendStore(friendsPath);
|
|
438
531
|
}
|
|
532
|
+
function createTeamsCommandRegistry() {
|
|
533
|
+
const registry = (0, commands_1.createCommandRegistry)();
|
|
534
|
+
(0, commands_1.registerDefaultCommands)(registry);
|
|
535
|
+
return registry;
|
|
536
|
+
}
|
|
537
|
+
/* v8 ignore start -- superseding follow-up slash command handler; tested via startTeamsApp integration tests @preserve */
|
|
538
|
+
function handleTeamsSlashCommand(text, registry, friendId, conversationId, stream, emitResponse = true) {
|
|
539
|
+
const parsed = (0, commands_1.parseSlashCommand)(text);
|
|
540
|
+
if (!parsed)
|
|
541
|
+
return null;
|
|
542
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
543
|
+
if (!dispatchResult.handled || !dispatchResult.result) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
if (dispatchResult.result.action === "new") {
|
|
547
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(friendId, "teams", conversationId));
|
|
548
|
+
if (emitResponse) {
|
|
549
|
+
stream.emit("session cleared");
|
|
550
|
+
}
|
|
551
|
+
return "new";
|
|
552
|
+
}
|
|
553
|
+
if (dispatchResult.result.action === "response") {
|
|
554
|
+
if (emitResponse) {
|
|
555
|
+
stream.emit(dispatchResult.result.message || "");
|
|
556
|
+
}
|
|
557
|
+
return "response";
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
/* v8 ignore stop */
|
|
439
562
|
// Handle an incoming Teams message
|
|
440
|
-
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
|
|
563
|
+
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides) {
|
|
441
564
|
const turnKey = teamsTurnKey(conversationId);
|
|
442
565
|
// NOTE: Confirmation resolution is handled in the app.on("message") handler
|
|
443
566
|
// BEFORE the conversation lock. By the time we get here, any pending
|
|
444
567
|
// confirmation has already been resolved and the reply consumed.
|
|
445
568
|
// Send first thinking phrase immediately so the user sees feedback
|
|
446
569
|
// before sync I/O (session load, trim) blocks the event loop.
|
|
447
|
-
|
|
570
|
+
// Skip for reaction signals — they should be processed quietly.
|
|
571
|
+
if (!reactionOverrides) {
|
|
572
|
+
stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
|
|
573
|
+
}
|
|
448
574
|
await new Promise(r => setImmediate(r));
|
|
449
|
-
// Resolve
|
|
575
|
+
// Resolve identity provider early for friend resolution + slash command session path
|
|
450
576
|
const store = getFriendStore();
|
|
451
577
|
const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
|
|
452
578
|
const externalId = teamsContext?.aadObjectId || conversationId;
|
|
453
|
-
|
|
579
|
+
// Build FriendResolver for the pipeline
|
|
580
|
+
const resolver = new resolver_1.FriendResolver(store, {
|
|
581
|
+
provider,
|
|
582
|
+
externalId,
|
|
583
|
+
tenantId: teamsContext?.tenantId,
|
|
584
|
+
displayName: teamsContext?.displayName || "Unknown",
|
|
585
|
+
channel: "teams",
|
|
586
|
+
});
|
|
587
|
+
// Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
|
|
588
|
+
const resolvedContext = await resolver.resolve();
|
|
589
|
+
const friendId = resolvedContext.friend.id;
|
|
590
|
+
// ── Teams adapter concerns: controller, callbacks, session path ──────────
|
|
591
|
+
const controller = new AbortController();
|
|
592
|
+
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
593
|
+
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs, ...(reactionOverrides?.suppressEmptyStreamMessage ? { suppressEmptyStreamMessage: true } : {}) });
|
|
594
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
595
|
+
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
596
|
+
const teamsCapabilities = (0, channel_1.getChannelCapabilities)("teams");
|
|
597
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
|
|
598
|
+
// Build Teams-specific toolContext fields for injection into the pipeline
|
|
599
|
+
const teamsToolContext = teamsContext ? {
|
|
454
600
|
graphToken: teamsContext.graphToken,
|
|
455
601
|
adoToken: teamsContext.adoToken,
|
|
456
602
|
githubToken: teamsContext.githubToken,
|
|
457
603
|
signin: teamsContext.signin,
|
|
458
|
-
|
|
459
|
-
summarize: (0, core_1.createSummarize)(),
|
|
604
|
+
summarize: (0, core_1.createSummarize)("human"),
|
|
460
605
|
tenantId: teamsContext.tenantId,
|
|
461
606
|
botApi: teamsContext.botApi,
|
|
462
|
-
} :
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
607
|
+
} : {};
|
|
608
|
+
let currentText = text;
|
|
609
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
610
|
+
while (true) {
|
|
611
|
+
let drainedSteeringFollowUps = [];
|
|
612
|
+
// Build runAgentOptions with Teams-specific fields
|
|
613
|
+
const agentOptions = {
|
|
614
|
+
traceId,
|
|
615
|
+
toolContext: teamsToolContext,
|
|
616
|
+
mcpManager,
|
|
617
|
+
drainSteeringFollowUps: () => {
|
|
618
|
+
drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
|
|
619
|
+
.map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
|
|
620
|
+
return drainedSteeringFollowUps;
|
|
621
|
+
},
|
|
622
|
+
...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
|
|
623
|
+
};
|
|
624
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
625
|
+
// Capture terminal errors — failover message replaces the error card if it triggers
|
|
626
|
+
let capturedTerminalError = null;
|
|
627
|
+
const teamsFailoverState = (() => {
|
|
628
|
+
if (!teamsFailoverStates.has(conversationId)) {
|
|
629
|
+
teamsFailoverStates.set(conversationId, { pending: null });
|
|
630
|
+
}
|
|
631
|
+
return teamsFailoverStates.get(conversationId);
|
|
632
|
+
})();
|
|
633
|
+
/* v8 ignore start -- failover-aware callback wrapper: tested via pipeline integration @preserve */
|
|
634
|
+
const failoverAwareCallbacks = {
|
|
635
|
+
...callbacks,
|
|
636
|
+
onError: (error, severity) => {
|
|
637
|
+
if (severity === "terminal" && teamsFailoverState) {
|
|
638
|
+
capturedTerminalError = error;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
callbacks.onError(error, severity);
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
/* v8 ignore stop */
|
|
645
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
469
646
|
channel: "teams",
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
647
|
+
sessionKey: conversationId,
|
|
648
|
+
capabilities: teamsCapabilities,
|
|
649
|
+
messages: [{ role: "user", content: currentText }],
|
|
650
|
+
continuityIngressTexts: [currentText],
|
|
651
|
+
callbacks: failoverAwareCallbacks,
|
|
652
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
653
|
+
sessionLoader: {
|
|
654
|
+
loadOrCreate: async () => {
|
|
655
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
656
|
+
const messages = existing?.messages && existing.messages.length > 0
|
|
657
|
+
? existing.messages
|
|
658
|
+
: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("teams", {}, resolvedContext)) }];
|
|
659
|
+
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
660
|
+
return {
|
|
661
|
+
messages,
|
|
662
|
+
sessionPath: sessPath,
|
|
663
|
+
state: existing?.state,
|
|
664
|
+
events: existing?.events,
|
|
665
|
+
};
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
pendingDir,
|
|
669
|
+
friendStore: store,
|
|
477
670
|
provider,
|
|
478
671
|
externalId,
|
|
479
672
|
tenantId: teamsContext?.tenantId,
|
|
480
|
-
|
|
673
|
+
isGroupChat: false,
|
|
674
|
+
groupHasFamilyMember: false,
|
|
675
|
+
hasExistingGroupWithFamily: false,
|
|
676
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
677
|
+
drainPending: pending_1.drainPending,
|
|
678
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
|
|
679
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
680
|
+
...opts,
|
|
681
|
+
toolContext: {
|
|
682
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
683
|
+
signin: async () => undefined,
|
|
684
|
+
...opts?.toolContext,
|
|
685
|
+
summarize: teamsToolContext.summarize,
|
|
686
|
+
},
|
|
687
|
+
}),
|
|
688
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
689
|
+
const prepared = (0, context_1.postTurnTrim)(turnMessages, usage, hooks);
|
|
690
|
+
(0, context_1.deferPostTurnPersist)(sessionPathArg, prepared, usage, state);
|
|
691
|
+
},
|
|
692
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
693
|
+
signal: controller.signal,
|
|
694
|
+
runAgentOptions: agentOptions,
|
|
695
|
+
failoverState: teamsFailoverState,
|
|
481
696
|
});
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
const registry = (0, commands_1.createCommandRegistry)();
|
|
490
|
-
(0, commands_1.registerDefaultCommands)(registry);
|
|
491
|
-
// Check for slash commands
|
|
492
|
-
const parsed = (0, commands_1.parseSlashCommand)(text);
|
|
493
|
-
if (parsed) {
|
|
494
|
-
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
495
|
-
if (dispatchResult.handled && dispatchResult.result) {
|
|
496
|
-
if (dispatchResult.result.action === "new") {
|
|
497
|
-
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
697
|
+
// ── Handle pipeline-intercepted commands ────────────────────────
|
|
698
|
+
if (result.turnOutcome === "command") {
|
|
699
|
+
if (result.commandAction === "new") {
|
|
498
700
|
(0, context_1.deleteSession)(sessPath);
|
|
499
701
|
stream.emit("session cleared");
|
|
500
|
-
return;
|
|
501
702
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
703
|
+
// For "response" commands: pipeline already emitted the response via onTextChunk
|
|
704
|
+
await callbacks.flush();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
/* v8 ignore start -- failover display: tested via pipeline integration tests @preserve */
|
|
708
|
+
if (result.failoverMessage) {
|
|
709
|
+
stream.emit(result.failoverMessage);
|
|
710
|
+
}
|
|
711
|
+
else if (capturedTerminalError) {
|
|
712
|
+
callbacks.onError(capturedTerminalError, "terminal");
|
|
713
|
+
}
|
|
714
|
+
/* v8 ignore stop */
|
|
715
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
716
|
+
if (!result.gateResult.allowed) {
|
|
717
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
718
|
+
stream.emit(result.gateResult.autoReply);
|
|
505
719
|
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// Flush any remaining accumulated text at end of turn
|
|
723
|
+
await callbacks.flush();
|
|
724
|
+
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
725
|
+
// This must happen after the stream is done so the OAuth card renders properly.
|
|
726
|
+
if (teamsContext && result.messages) {
|
|
727
|
+
const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
728
|
+
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
729
|
+
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
730
|
+
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
731
|
+
await teamsContext.signin(teamsContext.adoConnectionName);
|
|
732
|
+
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
733
|
+
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
734
|
+
}
|
|
735
|
+
if (result.turnOutcome !== "superseded") {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const supersedingIndex = drainedSteeringFollowUps
|
|
739
|
+
.map((followUp) => followUp.effect)
|
|
740
|
+
.lastIndexOf("clear_and_supersede");
|
|
741
|
+
if (supersedingIndex < 0) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
|
|
745
|
+
const replayTail = drainedSteeringFollowUps
|
|
746
|
+
.slice(supersedingIndex + 1)
|
|
747
|
+
.map((followUp) => followUp.text.trim())
|
|
748
|
+
.filter((followUpText) => followUpText.length > 0)
|
|
749
|
+
.join("\n");
|
|
750
|
+
if (replayTail) {
|
|
751
|
+
currentText = replayTail;
|
|
752
|
+
continue;
|
|
506
753
|
}
|
|
754
|
+
if (handleTeamsSlashCommand(supersedingFollowUp.text, createTeamsCommandRegistry(), friendId, conversationId, stream, false)) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
currentText = supersedingFollowUp.text;
|
|
507
758
|
}
|
|
508
|
-
// Load or create session
|
|
509
|
-
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
510
|
-
const existing = (0, context_1.loadSession)(sessPath);
|
|
511
|
-
const messages = existing?.messages && existing.messages.length > 0
|
|
512
|
-
? existing.messages
|
|
513
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, toolContext?.context) }];
|
|
514
|
-
// Repair any orphaned tool calls from a previous aborted turn
|
|
515
|
-
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
516
|
-
// Push user message
|
|
517
|
-
messages.push({ role: "user", content: text });
|
|
518
|
-
// Run agent
|
|
519
|
-
const controller = new AbortController();
|
|
520
|
-
const channelConfig = (0, config_2.getTeamsChannelConfig)();
|
|
521
|
-
const callbacks = createTeamsCallbacks(stream, controller, sendMessage, { conversationId, flushIntervalMs: channelConfig.flushIntervalMs });
|
|
522
|
-
const traceId = (0, nerves_1.createTraceId)();
|
|
523
|
-
const agentOptions = {};
|
|
524
|
-
agentOptions.traceId = traceId;
|
|
525
|
-
if (toolContext)
|
|
526
|
-
agentOptions.toolContext = toolContext;
|
|
527
|
-
if (channelConfig.skipConfirmation)
|
|
528
|
-
agentOptions.skipConfirmation = true;
|
|
529
|
-
agentOptions.drainSteeringFollowUps = () => _turnCoordinator.drainFollowUps(turnKey).map((m) => ({ text: m.text }));
|
|
530
|
-
const result = await (0, core_1.runAgent)(messages, callbacks, "teams", controller.signal, agentOptions);
|
|
531
|
-
// Flush any remaining accumulated text at end of turn
|
|
532
|
-
await callbacks.flush();
|
|
533
|
-
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
534
|
-
// This must happen after the stream is done so the OAuth card renders properly.
|
|
535
|
-
if (teamsContext) {
|
|
536
|
-
const allContent = messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
537
|
-
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
538
|
-
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
539
|
-
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
540
|
-
await teamsContext.signin(teamsContext.adoConnectionName);
|
|
541
|
-
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
542
|
-
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
543
|
-
}
|
|
544
|
-
// Trim context and save session
|
|
545
|
-
(0, context_1.postTurn)(messages, sessPath, result.usage);
|
|
546
|
-
// Accumulate token usage on friend record
|
|
547
|
-
if (toolContext?.context?.friend?.id) {
|
|
548
|
-
await (0, tokens_1.accumulateFriendTokens)(store, toolContext.context.friend.id, result.usage);
|
|
549
|
-
}
|
|
550
|
-
// SDK auto-closes the stream after our handler returns (app.process.js)
|
|
551
759
|
}
|
|
552
760
|
// Internal port for the secondary bot App (not exposed externally).
|
|
553
761
|
// The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
|
|
@@ -623,7 +831,7 @@ function registerBotHandlers(app, label) {
|
|
|
623
831
|
// (graph + ado + github). The verifyState activity only carries a `state`
|
|
624
832
|
// code with no connectionName, so we try each configured connection until
|
|
625
833
|
// one succeeds.
|
|
626
|
-
app.on("signin.verify-state", async (ctx) => {
|
|
834
|
+
app.on("signin.verify-state", (async (ctx) => {
|
|
627
835
|
const { api, activity } = ctx;
|
|
628
836
|
if (!activity.value?.state)
|
|
629
837
|
return { status: 404 };
|
|
@@ -642,7 +850,73 @@ function registerBotHandlers(app, label) {
|
|
|
642
850
|
}
|
|
643
851
|
(0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: `[${label}] verify-state failed for all connections`, meta: {} });
|
|
644
852
|
return { status: 412 };
|
|
853
|
+
}));
|
|
854
|
+
// Handle Teams feedback reactions (thumbs up/down on AI-generated messages).
|
|
855
|
+
// SDK routes message/submitAction with actionName "feedback" to this event.
|
|
856
|
+
/* v8 ignore start -- Teams SDK invoke handler; requires live SDK context @preserve */
|
|
857
|
+
app.on("message.submit.feedback", async (ctx) => {
|
|
858
|
+
const { stream, activity } = ctx;
|
|
859
|
+
const reaction = activity.value?.actionValue?.reaction;
|
|
860
|
+
const comment = activity.value?.actionValue?.feedback;
|
|
861
|
+
const convId = activity.conversation?.id || "unknown";
|
|
862
|
+
const turnKey = teamsTurnKey(convId);
|
|
863
|
+
// Validate payload — graceful no-op for malformed invocations
|
|
864
|
+
if (activity.value?.actionName !== "feedback" || !reaction) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const syntheticText = buildFeedbackSyntheticText(reaction, comment);
|
|
868
|
+
// Turn coordination: if a turn is active, enqueue as steering follow-up
|
|
869
|
+
if (!_turnCoordinator.tryBeginTurn(turnKey)) {
|
|
870
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
871
|
+
conversationId: convId,
|
|
872
|
+
text: syntheticText,
|
|
873
|
+
receivedAt: Date.now(),
|
|
874
|
+
effect: (0, continuity_1.classifySteeringFollowUpEffect)(syntheticText),
|
|
875
|
+
});
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
const teamsContext = {
|
|
880
|
+
signin: async () => undefined,
|
|
881
|
+
aadObjectId: activity.from?.aadObjectId,
|
|
882
|
+
tenantId: activity.conversation?.tenantId,
|
|
883
|
+
displayName: activity.from?.name,
|
|
884
|
+
};
|
|
885
|
+
const ctxSend = async (t) => {
|
|
886
|
+
await ctx.send({ type: "message", text: t, replyToId: activity.replyToId, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
887
|
+
};
|
|
888
|
+
await handleTeamsMessage(syntheticText, stream, convId, teamsContext, ctxSend, { isReactionSignal: true, suppressEmptyStreamMessage: true });
|
|
889
|
+
}
|
|
890
|
+
catch (err) {
|
|
891
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
892
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.feedback_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
|
|
893
|
+
}
|
|
894
|
+
finally {
|
|
895
|
+
_turnCoordinator.endTurn(turnKey);
|
|
896
|
+
}
|
|
645
897
|
});
|
|
898
|
+
/* v8 ignore stop */
|
|
899
|
+
// Handle bot install — send welcome Adaptive Card with prompt starters.
|
|
900
|
+
/* v8 ignore start -- Teams SDK install handler; requires live SDK context @preserve */
|
|
901
|
+
app.on("install.add", async (ctx) => {
|
|
902
|
+
try {
|
|
903
|
+
const card = buildWelcomeCard();
|
|
904
|
+
await ctx.send({
|
|
905
|
+
type: "message",
|
|
906
|
+
attachments: [{
|
|
907
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
908
|
+
content: card,
|
|
909
|
+
}],
|
|
910
|
+
entities: aiLabelEntities(),
|
|
911
|
+
channelData: { feedbackLoopEnabled: true },
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
916
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.welcome_handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
/* v8 ignore stop */
|
|
646
920
|
app.on("message", async (ctx) => {
|
|
647
921
|
const { stream, activity, api, signin } = ctx;
|
|
648
922
|
const text = activity.text || "";
|
|
@@ -651,12 +925,40 @@ function registerBotHandlers(app, label) {
|
|
|
651
925
|
const userId = activity.from?.id || "";
|
|
652
926
|
const channelId = activity.channelId || "msteams";
|
|
653
927
|
(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) } });
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
928
|
+
const commandRegistry = createTeamsCommandRegistry();
|
|
929
|
+
const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
|
|
930
|
+
if (parsedSlashCommand) {
|
|
931
|
+
const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
|
|
932
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
933
|
+
if (dispatchResult.result.action === "response") {
|
|
934
|
+
stream.emit(dispatchResult.result.message || "");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (dispatchResult.result.action === "new") {
|
|
938
|
+
const commandStore = getFriendStore();
|
|
939
|
+
const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
|
|
940
|
+
const commandExternalId = activity.from?.aadObjectId || convId;
|
|
941
|
+
const commandResolver = new resolver_1.FriendResolver(commandStore, {
|
|
942
|
+
provider: commandProvider,
|
|
943
|
+
externalId: commandExternalId,
|
|
944
|
+
tenantId: activity.conversation?.tenantId,
|
|
945
|
+
displayName: activity.from?.name || "Unknown",
|
|
946
|
+
channel: "teams",
|
|
947
|
+
});
|
|
948
|
+
const commandContext = await commandResolver.resolve();
|
|
949
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
|
|
950
|
+
stream.emit("session cleared");
|
|
951
|
+
if (_turnCoordinator.isTurnActive(turnKey)) {
|
|
952
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
953
|
+
conversationId: convId,
|
|
954
|
+
text,
|
|
955
|
+
receivedAt: Date.now(),
|
|
956
|
+
effect: "clear_and_supersede",
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
660
962
|
}
|
|
661
963
|
// If this conversation already has an active turn, steer follow-up input
|
|
662
964
|
// into that turn and avoid starting a second concurrent turn.
|
|
@@ -665,6 +967,7 @@ function registerBotHandlers(app, label) {
|
|
|
665
967
|
conversationId: convId,
|
|
666
968
|
text,
|
|
667
969
|
receivedAt: Date.now(),
|
|
970
|
+
effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
|
|
668
971
|
});
|
|
669
972
|
return;
|
|
670
973
|
}
|
|
@@ -722,7 +1025,7 @@ function registerBotHandlers(app, label) {
|
|
|
722
1025
|
const ctxSend = async (t) => {
|
|
723
1026
|
// Use send with replyToId (not reply, which adds a blockquote).
|
|
724
1027
|
// replyToId anchors the message after the user's message in Copilot Chat.
|
|
725
|
-
await ctx.send({ type: "message", text: t, replyToId: activity.id });
|
|
1028
|
+
await ctx.send({ type: "message", text: t, replyToId: activity.id, entities: aiLabelEntities(), channelData: { feedbackLoopEnabled: true } });
|
|
726
1029
|
};
|
|
727
1030
|
await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
|
|
728
1031
|
}
|
|
@@ -739,7 +1042,6 @@ function registerBotHandlers(app, label) {
|
|
|
739
1042
|
(0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.app_error", component: "channels", message: `[${label}] ${msg}`, meta: {} });
|
|
740
1043
|
});
|
|
741
1044
|
}
|
|
742
|
-
const TEAMS_PROACTIVE_ALLOWED_TRUST = new Set(["family", "friend"]);
|
|
743
1045
|
function findAadObjectId(friend) {
|
|
744
1046
|
for (const ext of friend.externalIds) {
|
|
745
1047
|
if (ext.provider === "aad" && !ext.externalId.startsWith("group:")) {
|
|
@@ -748,6 +1050,108 @@ function findAadObjectId(friend) {
|
|
|
748
1050
|
}
|
|
749
1051
|
return undefined;
|
|
750
1052
|
}
|
|
1053
|
+
function resolveTeamsFriendStore(deps) {
|
|
1054
|
+
return deps.store
|
|
1055
|
+
?? deps.createFriendStore?.()
|
|
1056
|
+
?? new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends"));
|
|
1057
|
+
}
|
|
1058
|
+
function getTeamsConversations(botApi) {
|
|
1059
|
+
return botApi.conversations;
|
|
1060
|
+
}
|
|
1061
|
+
function hasExplicitCrossChatAuthorization(params) {
|
|
1062
|
+
return params.intent === "explicit_cross_chat"
|
|
1063
|
+
&& types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
|
|
1064
|
+
}
|
|
1065
|
+
async function sendProactiveTeamsMessageToSession(params, deps) {
|
|
1066
|
+
const store = resolveTeamsFriendStore(deps);
|
|
1067
|
+
const conversations = getTeamsConversations(deps.botApi);
|
|
1068
|
+
let friend;
|
|
1069
|
+
try {
|
|
1070
|
+
friend = await store.get(params.friendId);
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
friend = null;
|
|
1074
|
+
}
|
|
1075
|
+
if (!friend) {
|
|
1076
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1077
|
+
level: "warn",
|
|
1078
|
+
component: "senses",
|
|
1079
|
+
event: "senses.teams_proactive_no_friend",
|
|
1080
|
+
message: "proactive send skipped: friend not found",
|
|
1081
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
1082
|
+
});
|
|
1083
|
+
return { delivered: false, reason: "friend_not_found" };
|
|
1084
|
+
}
|
|
1085
|
+
if (!hasExplicitCrossChatAuthorization(params) && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
1086
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1087
|
+
component: "senses",
|
|
1088
|
+
event: "senses.teams_proactive_trust_skip",
|
|
1089
|
+
message: "proactive send skipped: trust level not allowed",
|
|
1090
|
+
meta: {
|
|
1091
|
+
friendId: params.friendId,
|
|
1092
|
+
trustLevel: friend.trustLevel ?? "unknown",
|
|
1093
|
+
intent: params.intent ?? "generic_outreach",
|
|
1094
|
+
authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
return { delivered: false, reason: "trust_skip" };
|
|
1098
|
+
}
|
|
1099
|
+
const aadInfo = findAadObjectId(friend);
|
|
1100
|
+
if (!aadInfo) {
|
|
1101
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1102
|
+
level: "warn",
|
|
1103
|
+
component: "senses",
|
|
1104
|
+
event: "senses.teams_proactive_no_aad_id",
|
|
1105
|
+
message: "proactive send skipped: no AAD object ID found",
|
|
1106
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
1107
|
+
});
|
|
1108
|
+
return { delivered: false, reason: "missing_target" };
|
|
1109
|
+
}
|
|
1110
|
+
const internalContentBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(params.text);
|
|
1111
|
+
if (internalContentBlockReason) {
|
|
1112
|
+
(0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
|
|
1113
|
+
friendId: params.friendId,
|
|
1114
|
+
sessionKey: params.sessionKey,
|
|
1115
|
+
reason: internalContentBlockReason,
|
|
1116
|
+
source: "session_send",
|
|
1117
|
+
});
|
|
1118
|
+
return { delivered: false, reason: "internal_content_blocked" };
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const conversation = await conversations.create({
|
|
1122
|
+
bot: { id: deps.botApi.id },
|
|
1123
|
+
members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
|
|
1124
|
+
tenantId: aadInfo.tenantId,
|
|
1125
|
+
isGroup: false,
|
|
1126
|
+
});
|
|
1127
|
+
await conversations.activities(conversation.id).create({
|
|
1128
|
+
type: "message",
|
|
1129
|
+
text: params.text,
|
|
1130
|
+
});
|
|
1131
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1132
|
+
component: "senses",
|
|
1133
|
+
event: "senses.teams_proactive_sent",
|
|
1134
|
+
message: "proactive teams message sent",
|
|
1135
|
+
meta: { friendId: params.friendId, aadObjectId: aadInfo.aadObjectId, sessionKey: params.sessionKey },
|
|
1136
|
+
});
|
|
1137
|
+
return { delivered: true };
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1141
|
+
level: "error",
|
|
1142
|
+
component: "senses",
|
|
1143
|
+
event: "senses.teams_proactive_send_error",
|
|
1144
|
+
message: "proactive teams send failed",
|
|
1145
|
+
meta: {
|
|
1146
|
+
friendId: params.friendId,
|
|
1147
|
+
aadObjectId: aadInfo.aadObjectId,
|
|
1148
|
+
sessionKey: params.sessionKey,
|
|
1149
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
return { delivered: false, reason: "send_error" };
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
751
1155
|
function scanPendingTeamsFiles(pendingRoot) {
|
|
752
1156
|
const results = [];
|
|
753
1157
|
let friendIds;
|
|
@@ -793,8 +1197,7 @@ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
|
|
|
793
1197
|
const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
|
|
794
1198
|
const pendingFiles = scanPendingTeamsFiles(root);
|
|
795
1199
|
const result = { sent: 0, skipped: 0, failed: 0 };
|
|
796
|
-
const
|
|
797
|
-
for (const { friendId, filePath, content } of pendingFiles) {
|
|
1200
|
+
for (const { friendId, key, filePath, content } of pendingFiles) {
|
|
798
1201
|
let parsed;
|
|
799
1202
|
try {
|
|
800
1203
|
parsed = JSON.parse(content);
|
|
@@ -816,95 +1219,46 @@ async function drainAndSendPendingTeams(store, botApi, pendingRoot) {
|
|
|
816
1219
|
catch { /* ignore */ }
|
|
817
1220
|
continue;
|
|
818
1221
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
friend = await store.get(friendId);
|
|
822
|
-
}
|
|
823
|
-
catch {
|
|
824
|
-
friend = null;
|
|
825
|
-
}
|
|
826
|
-
if (!friend) {
|
|
1222
|
+
const internalBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(messageText);
|
|
1223
|
+
if (internalBlockReason) {
|
|
827
1224
|
result.skipped++;
|
|
828
1225
|
try {
|
|
829
1226
|
fs.unlinkSync(filePath);
|
|
830
1227
|
}
|
|
831
1228
|
catch { /* ignore */ }
|
|
832
|
-
(0,
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
message: "proactive send skipped: friend not found",
|
|
837
|
-
meta: { friendId },
|
|
1229
|
+
(0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
|
|
1230
|
+
friendId,
|
|
1231
|
+
reason: internalBlockReason,
|
|
1232
|
+
source: "pending_drain",
|
|
838
1233
|
});
|
|
839
1234
|
continue;
|
|
840
1235
|
}
|
|
841
|
-
|
|
842
|
-
|
|
1236
|
+
const sendResult = await sendProactiveTeamsMessageToSession({
|
|
1237
|
+
friendId,
|
|
1238
|
+
sessionKey: key,
|
|
1239
|
+
text: messageText,
|
|
1240
|
+
intent: "generic_outreach",
|
|
1241
|
+
}, {
|
|
1242
|
+
botApi,
|
|
1243
|
+
store,
|
|
1244
|
+
});
|
|
1245
|
+
if (sendResult.delivered) {
|
|
1246
|
+
result.sent++;
|
|
843
1247
|
try {
|
|
844
1248
|
fs.unlinkSync(filePath);
|
|
845
1249
|
}
|
|
846
1250
|
catch { /* ignore */ }
|
|
847
|
-
(0, runtime_1.emitNervesEvent)({
|
|
848
|
-
component: "senses",
|
|
849
|
-
event: "senses.teams_proactive_trust_skip",
|
|
850
|
-
message: "proactive send skipped: trust level not allowed",
|
|
851
|
-
meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
|
|
852
|
-
});
|
|
853
1251
|
continue;
|
|
854
1252
|
}
|
|
855
|
-
|
|
856
|
-
if (!aadInfo) {
|
|
1253
|
+
if (sendResult.reason === "friend_not_found" || sendResult.reason === "trust_skip" || sendResult.reason === "missing_target") {
|
|
857
1254
|
result.skipped++;
|
|
858
1255
|
try {
|
|
859
1256
|
fs.unlinkSync(filePath);
|
|
860
1257
|
}
|
|
861
1258
|
catch { /* ignore */ }
|
|
862
|
-
(0, runtime_1.emitNervesEvent)({
|
|
863
|
-
level: "warn",
|
|
864
|
-
component: "senses",
|
|
865
|
-
event: "senses.teams_proactive_no_aad_id",
|
|
866
|
-
message: "proactive send skipped: no AAD object ID found",
|
|
867
|
-
meta: { friendId },
|
|
868
|
-
});
|
|
869
1259
|
continue;
|
|
870
1260
|
}
|
|
871
|
-
|
|
872
|
-
const conversation = await conversations.create({
|
|
873
|
-
bot: { id: botApi.id },
|
|
874
|
-
members: [{ id: aadInfo.aadObjectId, role: "user", name: friend.name || aadInfo.aadObjectId }],
|
|
875
|
-
tenantId: aadInfo.tenantId,
|
|
876
|
-
isGroup: false,
|
|
877
|
-
});
|
|
878
|
-
await conversations.activities(conversation.id).create({
|
|
879
|
-
type: "message",
|
|
880
|
-
text: messageText,
|
|
881
|
-
});
|
|
882
|
-
result.sent++;
|
|
883
|
-
try {
|
|
884
|
-
fs.unlinkSync(filePath);
|
|
885
|
-
}
|
|
886
|
-
catch { /* ignore */ }
|
|
887
|
-
(0, runtime_1.emitNervesEvent)({
|
|
888
|
-
component: "senses",
|
|
889
|
-
event: "senses.teams_proactive_sent",
|
|
890
|
-
message: "proactive teams message sent",
|
|
891
|
-
meta: { friendId, aadObjectId: aadInfo.aadObjectId },
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
catch (error) {
|
|
895
|
-
result.failed++;
|
|
896
|
-
(0, runtime_1.emitNervesEvent)({
|
|
897
|
-
level: "error",
|
|
898
|
-
component: "senses",
|
|
899
|
-
event: "senses.teams_proactive_send_error",
|
|
900
|
-
message: "proactive teams send failed",
|
|
901
|
-
meta: {
|
|
902
|
-
friendId,
|
|
903
|
-
aadObjectId: aadInfo.aadObjectId,
|
|
904
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
905
|
-
},
|
|
906
|
-
});
|
|
907
|
-
}
|
|
1261
|
+
result.failed++;
|
|
908
1262
|
}
|
|
909
1263
|
if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
|
|
910
1264
|
(0, runtime_1.emitNervesEvent)({
|