@ouro.bot/cli 0.0.1-alpha.0 → 0.1.0-alpha.2

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.
Files changed (119) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +20 -0
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +22 -0
  3. package/AdoptionSpecialist.ouro/psyche/identities/basilisk.md +31 -0
  4. package/AdoptionSpecialist.ouro/psyche/identities/jafar.md +31 -0
  5. package/AdoptionSpecialist.ouro/psyche/identities/jormungandr.md +31 -0
  6. package/AdoptionSpecialist.ouro/psyche/identities/kaa.md +31 -0
  7. package/AdoptionSpecialist.ouro/psyche/identities/medusa.md +31 -0
  8. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +31 -0
  9. package/AdoptionSpecialist.ouro/psyche/identities/nagini.md +31 -0
  10. package/AdoptionSpecialist.ouro/psyche/identities/ouroboros.md +31 -0
  11. package/AdoptionSpecialist.ouro/psyche/identities/python.md +31 -0
  12. package/AdoptionSpecialist.ouro/psyche/identities/quetzalcoatl.md +31 -0
  13. package/AdoptionSpecialist.ouro/psyche/identities/sir-hiss.md +31 -0
  14. package/AdoptionSpecialist.ouro/psyche/identities/the-serpent.md +31 -0
  15. package/AdoptionSpecialist.ouro/psyche/identities/the-snake.md +31 -0
  16. package/README.md +224 -6
  17. package/dist/heart/agent-entry.js +17 -0
  18. package/dist/heart/api-error.js +34 -0
  19. package/dist/heart/config.js +296 -0
  20. package/dist/heart/core.js +515 -0
  21. package/dist/heart/daemon/daemon-cli.js +675 -0
  22. package/dist/heart/daemon/daemon-entry.js +74 -0
  23. package/dist/heart/daemon/daemon.js +313 -0
  24. package/dist/heart/daemon/hatch-flow.js +285 -0
  25. package/dist/heart/daemon/hatch-specialist.js +107 -0
  26. package/dist/heart/daemon/health-monitor.js +79 -0
  27. package/dist/heart/daemon/log-tailer.js +146 -0
  28. package/dist/heart/daemon/message-router.js +98 -0
  29. package/dist/heart/daemon/os-cron.js +260 -0
  30. package/dist/heart/daemon/ouro-bot-entry.js +23 -0
  31. package/dist/heart/daemon/ouro-bot-wrapper.js +90 -0
  32. package/dist/heart/daemon/ouro-entry.js +23 -0
  33. package/dist/heart/daemon/ouro-uti.js +212 -0
  34. package/dist/heart/daemon/process-manager.js +237 -0
  35. package/dist/heart/daemon/runtime-logging.js +98 -0
  36. package/dist/heart/daemon/subagent-installer.js +125 -0
  37. package/dist/heart/daemon/task-scheduler.js +240 -0
  38. package/dist/heart/harness.js +26 -0
  39. package/dist/heart/identity.js +281 -0
  40. package/dist/heart/kicks.js +144 -0
  41. package/dist/heart/primitives.js +4 -0
  42. package/dist/heart/providers/anthropic.js +329 -0
  43. package/dist/heart/providers/azure.js +66 -0
  44. package/dist/heart/providers/minimax.js +53 -0
  45. package/dist/heart/providers/openai-codex.js +162 -0
  46. package/dist/heart/streaming.js +412 -0
  47. package/dist/heart/turn-coordinator.js +62 -0
  48. package/dist/inner-worker-entry.js +4 -0
  49. package/dist/mind/associative-recall.js +197 -0
  50. package/dist/mind/bundle-manifest.js +118 -0
  51. package/dist/mind/context.js +302 -0
  52. package/dist/mind/first-impressions.js +43 -0
  53. package/dist/mind/format.js +56 -0
  54. package/dist/mind/friends/channel.js +41 -0
  55. package/dist/mind/friends/resolver.js +84 -0
  56. package/dist/mind/friends/store-file.js +171 -0
  57. package/dist/mind/friends/store.js +4 -0
  58. package/dist/mind/friends/tokens.js +26 -0
  59. package/dist/mind/friends/types.js +21 -0
  60. package/dist/mind/memory.js +388 -0
  61. package/dist/mind/pending.js +93 -0
  62. package/dist/mind/phrases.js +43 -0
  63. package/dist/mind/prompt-refresh.js +20 -0
  64. package/dist/mind/prompt.js +352 -0
  65. package/dist/mind/token-estimate.js +119 -0
  66. package/dist/nerves/cli-logging.js +31 -0
  67. package/dist/nerves/coverage/audit-rules.js +81 -0
  68. package/dist/nerves/coverage/audit.js +200 -0
  69. package/dist/nerves/coverage/cli-main.js +5 -0
  70. package/dist/nerves/coverage/cli.js +51 -0
  71. package/dist/nerves/coverage/contract.js +23 -0
  72. package/dist/nerves/coverage/file-completeness.js +56 -0
  73. package/dist/nerves/coverage/run-artifacts.js +77 -0
  74. package/dist/nerves/coverage/source-scanner.js +34 -0
  75. package/dist/nerves/index.js +152 -0
  76. package/dist/nerves/runtime.js +38 -0
  77. package/dist/repertoire/ado-client.js +211 -0
  78. package/dist/repertoire/ado-context.js +73 -0
  79. package/dist/repertoire/ado-semantic.js +841 -0
  80. package/dist/repertoire/ado-templates.js +146 -0
  81. package/dist/repertoire/coding/index.js +36 -0
  82. package/dist/repertoire/coding/manager.js +489 -0
  83. package/dist/repertoire/coding/monitor.js +60 -0
  84. package/dist/repertoire/coding/reporter.js +45 -0
  85. package/dist/repertoire/coding/spawner.js +102 -0
  86. package/dist/repertoire/coding/tools.js +167 -0
  87. package/dist/repertoire/coding/types.js +2 -0
  88. package/dist/repertoire/data/ado-endpoints.json +122 -0
  89. package/dist/repertoire/data/graph-endpoints.json +212 -0
  90. package/dist/repertoire/github-client.js +64 -0
  91. package/dist/repertoire/graph-client.js +118 -0
  92. package/dist/repertoire/skills.js +156 -0
  93. package/dist/repertoire/tasks/board.js +122 -0
  94. package/dist/repertoire/tasks/index.js +210 -0
  95. package/dist/repertoire/tasks/lifecycle.js +80 -0
  96. package/dist/repertoire/tasks/middleware.js +65 -0
  97. package/dist/repertoire/tasks/parser.js +173 -0
  98. package/dist/repertoire/tasks/scanner.js +132 -0
  99. package/dist/repertoire/tasks/transitions.js +145 -0
  100. package/dist/repertoire/tasks/types.js +2 -0
  101. package/dist/repertoire/tools-base.js +714 -0
  102. package/dist/repertoire/tools-github.js +53 -0
  103. package/dist/repertoire/tools-teams.js +308 -0
  104. package/dist/repertoire/tools.js +199 -0
  105. package/dist/senses/cli-entry.js +15 -0
  106. package/dist/senses/cli.js +604 -0
  107. package/dist/senses/commands.js +98 -0
  108. package/dist/senses/inner-dialog-worker.js +61 -0
  109. package/dist/senses/inner-dialog.js +231 -0
  110. package/dist/senses/session-lock.js +119 -0
  111. package/dist/senses/teams-entry.js +15 -0
  112. package/dist/senses/teams.js +696 -0
  113. package/dist/senses/trust-gate.js +150 -0
  114. package/package.json +34 -11
  115. package/subagents/README.md +73 -0
  116. package/subagents/work-doer.md +233 -0
  117. package/subagents/work-merger.md +624 -0
  118. package/subagents/work-planner.md +373 -0
  119. package/bin/ouro.js +0 -6
@@ -0,0 +1,696 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_FLUSH_INTERVAL_MS = void 0;
37
+ exports.stripMentions = stripMentions;
38
+ exports.splitMessage = splitMessage;
39
+ exports.createTeamsCallbacks = createTeamsCallbacks;
40
+ exports.resolvePendingConfirmation = resolvePendingConfirmation;
41
+ exports.withConversationLock = withConversationLock;
42
+ exports.handleTeamsMessage = handleTeamsMessage;
43
+ exports.startTeamsApp = startTeamsApp;
44
+ const teams_apps_1 = require("@microsoft/teams.apps");
45
+ const teams_dev_1 = require("@microsoft/teams.dev");
46
+ const core_1 = require("../heart/core");
47
+ const config_1 = require("../heart/config");
48
+ const prompt_1 = require("../mind/prompt");
49
+ const phrases_1 = require("../mind/phrases");
50
+ const format_1 = require("../mind/format");
51
+ const config_2 = require("../heart/config");
52
+ const context_1 = require("../mind/context");
53
+ const commands_1 = require("./commands");
54
+ const nerves_1 = require("../nerves");
55
+ const runtime_1 = require("../nerves/runtime");
56
+ const store_file_1 = require("../mind/friends/store-file");
57
+ const resolver_1 = require("../mind/friends/resolver");
58
+ const tokens_1 = require("../mind/friends/tokens");
59
+ const turn_coordinator_1 = require("../heart/turn-coordinator");
60
+ const identity_1 = require("../heart/identity");
61
+ const path = __importStar(require("path"));
62
+ const trust_gate_1 = require("./trust-gate");
63
+ // Strip @mention markup from incoming messages.
64
+ // Removes <at>...</at> tags and trims extra whitespace.
65
+ // Fallback safety net -- the SDK's activity.mentions.stripText should handle
66
+ // this automatically, but this utility is exported for testability.
67
+ function stripMentions(text) {
68
+ if (!text)
69
+ return "";
70
+ return text.replace(/<at>[^<]*<\/at>/g, "").trim();
71
+ }
72
+ // Recovery chunk size for error-recovery splitting. When a full-text send fails
73
+ // (e.g. 413 from Teams), we split into chunks of this size and retry.
74
+ // Not used preemptively — the harness tries to send the full message first.
75
+ const RECOVERY_CHUNK_SIZE = 4000;
76
+ // Default interval (ms) for the periodic flush timer in chunked streaming mode.
77
+ // Text is accumulated in textBuffer and flushed via safeEmit/safeSend at this
78
+ // interval. This replaces per-token streaming, which caused compounding latency:
79
+ //
80
+ // - Teams throttles streaming updates to 1 req/sec with exponential backoff
81
+ // https://learn.microsoft.com/en-us/microsoftteams/platform/bots/streaming-ux
82
+ // - SDK debounces at 500ms internally and re-sends ALL cumulative text each chunk
83
+ // - Per-token streaming generates 100+ HTTP POSTs per response, each throttled
84
+ // - Copilot enforces a 15s timeout for the initial stream.emit()
85
+ // https://learn.microsoft.com/en-us/answers/questions/2288017/m365-custom-engine-agents-timeout-message-after-15
86
+ //
87
+ // At 1000ms (1 req/sec), we stay at the Teams throttle floor while keeping the
88
+ // stream alive well within the 15s Copilot timeout. Tune up if 429s observed.
89
+ exports.DEFAULT_FLUSH_INTERVAL_MS = 1_000;
90
+ // Split text into chunks that fit within maxLen, breaking at paragraph
91
+ // boundaries (\n\n), then line boundaries (\n), then word boundaries.
92
+ // Never loses content — all text is preserved across chunks.
93
+ function splitMessage(text, maxLen) {
94
+ if (text.length <= maxLen)
95
+ return [text];
96
+ const chunks = [];
97
+ let remaining = text;
98
+ while (remaining.length > 0) {
99
+ if (remaining.length <= maxLen) {
100
+ chunks.push(remaining);
101
+ break;
102
+ }
103
+ // Find best split point: paragraph > line > word > hard cut
104
+ const slice = remaining.slice(0, maxLen);
105
+ let splitAt = slice.lastIndexOf("\n\n");
106
+ if (splitAt <= 0)
107
+ splitAt = slice.lastIndexOf("\n");
108
+ if (splitAt <= 0)
109
+ splitAt = slice.lastIndexOf(" ");
110
+ if (splitAt <= 0)
111
+ splitAt = maxLen; // hard cut as last resort
112
+ chunks.push(remaining.slice(0, splitAt));
113
+ remaining = remaining.slice(splitAt).replace(/^[\n ]+/, ""); // trim leading whitespace from next chunk
114
+ }
115
+ return chunks;
116
+ }
117
+ // Create Teams-specific callbacks for the agent loop.
118
+ // The SDK handles cumulative text, debouncing (500ms), and the streaming
119
+ // protocol (streamSequence, streamId, informative/streaming/final types).
120
+ //
121
+ // Chunked streaming (unified mode):
122
+ // Text is always accumulated in textBuffer and flushed periodically (via a
123
+ // flush timer started on first onTextChunk) or at end-of-turn via flush().
124
+ // First flush goes to safeEmit (primary output), subsequent flushes go to
125
+ // safeSend (ctx.send). Tool results, kicks, and errors use safeUpdate
126
+ // (transient status) or safeSend (terminal errors). Reasoning is accumulated
127
+ // and periodically pushed via safeUpdate on the same flush timer tick.
128
+ function createTeamsCallbacks(stream, controller, sendMessage, options) {
129
+ let stopped = false; // set when stream signals cancellation (403)
130
+ let hadToolRun = false;
131
+ let hadRealOutput = false; // true once reasoning/tool output shown; suppresses phrases
132
+ let reasoningBuf = ""; // accumulated reasoning text for status display
133
+ let textBuffer = ""; // accumulated text output for chunked streaming
134
+ let streamHasContent = false; // tracks whether primary output has received content
135
+ let phraseTimer = null;
136
+ let lastPhrase = "";
137
+ let flushTimer = null;
138
+ const flushInterval = options?.flushIntervalMs ?? exports.DEFAULT_FLUSH_INTERVAL_MS;
139
+ // Track whether reasoning has changed since last flush (avoid redundant updates).
140
+ let lastFlushedReasoningLen = 0;
141
+ // Periodic tick: flush text buffer and push reasoning updates.
142
+ function flushTick() {
143
+ flushTextBuffer();
144
+ if (reasoningBuf.length > lastFlushedReasoningLen) {
145
+ safeUpdate(reasoningBuf);
146
+ lastFlushedReasoningLen = reasoningBuf.length;
147
+ }
148
+ }
149
+ // Start the periodic flush timer. Idempotent -- no-op if already running.
150
+ function startFlushTimer() {
151
+ if (flushTimer)
152
+ return;
153
+ flushTimer = setInterval(flushTick, flushInterval);
154
+ }
155
+ // Stop the periodic flush timer. Idempotent.
156
+ function stopFlushTimer() {
157
+ if (flushTimer) {
158
+ clearInterval(flushTimer);
159
+ flushTimer = null;
160
+ }
161
+ }
162
+ // Mark stream as broken and abort the agent loop.
163
+ function markStopped() {
164
+ if (stopped)
165
+ return;
166
+ stopped = true;
167
+ stopPhraseRotation();
168
+ stopFlushTimer();
169
+ controller.abort();
170
+ }
171
+ // Clean up timers when the controller is aborted externally
172
+ controller.signal.addEventListener("abort", () => {
173
+ stopPhraseRotation();
174
+ stopFlushTimer();
175
+ });
176
+ // Handle the result of a stream call: if it returns a Promise (the Teams SDK
177
+ // does async HTTP under the hood even though the interface types it as void),
178
+ // catch its rejection so we detect a dead stream and abort the agent loop.
179
+ function catchAsync(result) {
180
+ if (result && typeof result.catch === "function") {
181
+ result.catch(() => markStopped());
182
+ }
183
+ }
184
+ // Safely emit a text delta to the stream.
185
+ // On error (e.g. 403 from Teams stop button), abort the controller.
186
+ function safeEmit(text) {
187
+ /* v8 ignore next -- defensive guard: stopped set by prior 403; tested via flush abort path @preserve */
188
+ if (stopped)
189
+ return;
190
+ try {
191
+ catchAsync(stream.emit(text));
192
+ streamHasContent = true;
193
+ }
194
+ catch {
195
+ markStopped();
196
+ }
197
+ }
198
+ // Awaitable emit — returns true if the emit succeeded, false if it failed.
199
+ // Used by flush() so it can fall back to sendMessage on async 413/failure.
200
+ async function tryEmit(text) {
201
+ /* v8 ignore next -- defensive guard: stopped set by prior error; tested via flush abort path @preserve */
202
+ if (stopped)
203
+ return false;
204
+ try {
205
+ // stream.emit() is typed as void but the Teams SDK returns a Promise
206
+ // internally (async HTTP). Cast to capture the result for awaiting.
207
+ const result = stream.emit(text);
208
+ streamHasContent = true;
209
+ if (result && typeof result.then === "function") {
210
+ await result;
211
+ }
212
+ return true;
213
+ }
214
+ catch {
215
+ markStopped();
216
+ return false;
217
+ }
218
+ }
219
+ // Safely send a status update to the stream.
220
+ // On error (e.g. 403 from Teams stop button), abort the controller.
221
+ function safeUpdate(text) {
222
+ if (stopped)
223
+ return;
224
+ try {
225
+ catchAsync(stream.update(text));
226
+ }
227
+ catch {
228
+ markStopped();
229
+ }
230
+ }
231
+ // Safely send a separate message via sendMessage (for content after first emit).
232
+ // Serialized via promise chain -- concurrent calls execute sequentially.
233
+ // If any send fails, the chain halts via markStopped().
234
+ let sendChain = Promise.resolve();
235
+ let sendChainBusy = false;
236
+ function safeSend(text) {
237
+ if (stopped || !sendMessage)
238
+ return;
239
+ if (!sendChainBusy) {
240
+ // Chain is idle -- start the send synchronously and mark busy
241
+ sendChainBusy = true;
242
+ try {
243
+ sendChain = sendMessage(text).catch(() => markStopped()).finally(() => { sendChainBusy = false; });
244
+ }
245
+ catch {
246
+ sendChainBusy = false;
247
+ markStopped();
248
+ }
249
+ }
250
+ else {
251
+ // Chain is busy -- queue onto the existing chain
252
+ sendChain = sendChain.then(() => {
253
+ if (stopped)
254
+ return;
255
+ return sendMessage(text);
256
+ }).catch(() => markStopped());
257
+ }
258
+ }
259
+ // Flush accumulated text buffer via safeEmit. The Teams SDK accumulates
260
+ // emitted text into a single streaming message (cumulative), so every
261
+ // periodic flush appends to the same response — not separate messages.
262
+ // No preemptive splitting — sends full text. Error recovery happens in flush().
263
+ function flushTextBuffer() {
264
+ if (!textBuffer)
265
+ return;
266
+ safeEmit(textBuffer);
267
+ textBuffer = "";
268
+ }
269
+ function startPhraseRotation(pool) {
270
+ stopPhraseRotation();
271
+ phraseTimer = setInterval(() => {
272
+ const next = (0, phrases_1.pickPhrase)(pool, lastPhrase);
273
+ lastPhrase = next;
274
+ safeUpdate(next + "...");
275
+ }, 1500);
276
+ }
277
+ function stopPhraseRotation() {
278
+ if (phraseTimer) {
279
+ clearInterval(phraseTimer);
280
+ phraseTimer = null;
281
+ }
282
+ }
283
+ return {
284
+ onModelStart: () => {
285
+ if (hadRealOutput)
286
+ return; // real output already shown; don't overwrite with phrases
287
+ const phrases = (0, phrases_1.getPhrases)();
288
+ const pool = hadToolRun ? phrases.followup : phrases.thinking;
289
+ const first = (0, phrases_1.pickPhrase)(pool);
290
+ lastPhrase = first;
291
+ safeUpdate(first + "...");
292
+ startPhraseRotation(pool);
293
+ },
294
+ onModelStreamStart: () => {
295
+ // No-op: don't stop rotation here — keep cycling phrases through
296
+ // the reasoning phase until actual content arrives.
297
+ },
298
+ onReasoningChunk: (text) => {
299
+ /* v8 ignore next -- defensive guard: stopped set by prior error @preserve */
300
+ if (stopped)
301
+ return;
302
+ stopPhraseRotation();
303
+ hadRealOutput = true;
304
+ reasoningBuf += text;
305
+ startFlushTimer();
306
+ },
307
+ onTextChunk: (text) => {
308
+ if (stopped)
309
+ return;
310
+ stopPhraseRotation();
311
+ textBuffer += text;
312
+ startFlushTimer();
313
+ },
314
+ onClearText: () => {
315
+ textBuffer = "";
316
+ },
317
+ onToolStart: (name, args) => {
318
+ stopPhraseRotation();
319
+ flushTextBuffer();
320
+ const argSummary = Object.values(args).join(", ");
321
+ safeUpdate(`running ${name} (${argSummary})...`);
322
+ hadToolRun = true;
323
+ },
324
+ onToolEnd: (name, summary, success) => {
325
+ stopPhraseRotation();
326
+ const msg = (0, format_1.formatToolResult)(name, summary, success);
327
+ safeUpdate(msg);
328
+ },
329
+ onKick: () => {
330
+ stopPhraseRotation();
331
+ const msg = (0, format_1.formatKick)();
332
+ safeUpdate(msg);
333
+ },
334
+ onError: (error, severity) => {
335
+ stopPhraseRotation();
336
+ if (stopped)
337
+ return;
338
+ const msg = (0, format_1.formatError)(error);
339
+ if (severity === "transient") {
340
+ safeUpdate(msg);
341
+ }
342
+ else {
343
+ safeSend(msg);
344
+ }
345
+ },
346
+ onConfirmAction: options?.conversationId
347
+ ? async (name, args) => {
348
+ const convId = options.conversationId;
349
+ const argsDesc = Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(", ");
350
+ safeUpdate(`Confirm action: ${name} (${argsDesc}) -- reply "yes" to confirm or "no" to cancel`);
351
+ return new Promise((resolve) => {
352
+ _pendingConfirmations.set(convId, resolve);
353
+ // Auto-deny after 2 minutes to prevent indefinite blocking
354
+ // (e.g. when the stream dies and the user never sees the prompt).
355
+ setTimeout(() => {
356
+ if (_pendingConfirmations.has(convId)) {
357
+ _pendingConfirmations.delete(convId);
358
+ resolve("denied");
359
+ }
360
+ }, 120_000);
361
+ });
362
+ }
363
+ : undefined,
364
+ flush: async () => {
365
+ stopFlushTimer();
366
+ if (textBuffer) {
367
+ const text = textBuffer;
368
+ textBuffer = "";
369
+ if (!stopped) {
370
+ // Stream is alive — await the emit so we can catch async 413/failure
371
+ // and fall through to sendMessage recovery.
372
+ const ok = await tryEmit(text);
373
+ if (!ok)
374
+ markStopped();
375
+ }
376
+ if (stopped && sendMessage) {
377
+ // Stream is dead — fall back to sendMessage; split on failure as recovery.
378
+ try {
379
+ await sendMessage(text);
380
+ }
381
+ catch {
382
+ const chunks = splitMessage(text, RECOVERY_CHUNK_SIZE);
383
+ for (const chunk of chunks)
384
+ await sendMessage(chunk);
385
+ }
386
+ }
387
+ }
388
+ else if (!streamHasContent) {
389
+ safeEmit("(completed with tool calls only \u2014 no text response)");
390
+ }
391
+ },
392
+ };
393
+ }
394
+ // Per-conversation pending confirmation resolvers.
395
+ // When a mutate tool needs confirmation, the resolver is stored here.
396
+ // The next message from the same conversation resolves it.
397
+ const _pendingConfirmations = new Map();
398
+ // Confirmation response words (case-insensitive)
399
+ const CONFIRM_WORDS = new Set(["yes", "confirm", "go", "y", "ok", "approve", "proceed"]);
400
+ function resolvePendingConfirmation(convId, text) {
401
+ const resolver = _pendingConfirmations.get(convId);
402
+ if (!resolver)
403
+ return false;
404
+ _pendingConfirmations.delete(convId);
405
+ const word = text.trim().toLowerCase();
406
+ if (CONFIRM_WORDS.has(word)) {
407
+ resolver("confirmed");
408
+ }
409
+ else {
410
+ resolver("denied");
411
+ }
412
+ return true;
413
+ }
414
+ const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
415
+ function teamsTurnKey(conversationId) {
416
+ return `teams:${conversationId}`;
417
+ }
418
+ async function withConversationLock(convId, fn) {
419
+ await _turnCoordinator.withTurnLock(teamsTurnKey(convId), fn);
420
+ }
421
+ // Create a fresh friend store per request so mkdirSync re-runs if directories
422
+ // are deleted while the process is alive.
423
+ function getFriendStore() {
424
+ const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
425
+ return new store_file_1.FileFriendStore(friendsPath);
426
+ }
427
+ // Handle an incoming Teams message
428
+ async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
429
+ const turnKey = teamsTurnKey(conversationId);
430
+ // NOTE: Confirmation resolution is handled in the app.on("message") handler
431
+ // BEFORE the conversation lock. By the time we get here, any pending
432
+ // confirmation has already been resolved and the reply consumed.
433
+ // Send first thinking phrase immediately so the user sees feedback
434
+ // before sync I/O (session load, trim) blocks the event loop.
435
+ stream.update((0, phrases_1.pickPhrase)((0, phrases_1.getPhrases)().thinking) + "...");
436
+ await new Promise(r => setImmediate(r));
437
+ // Resolve context kernel (identity + channel) early so we can use the friend UUID for session path
438
+ const store = getFriendStore();
439
+ const provider = teamsContext?.aadObjectId ? "aad" : "teams-conversation";
440
+ const externalId = teamsContext?.aadObjectId || conversationId;
441
+ const toolContext = teamsContext ? {
442
+ graphToken: teamsContext.graphToken,
443
+ adoToken: teamsContext.adoToken,
444
+ githubToken: teamsContext.githubToken,
445
+ signin: teamsContext.signin,
446
+ friendStore: store,
447
+ summarize: (0, core_1.createSummarize)(),
448
+ } : undefined;
449
+ if (toolContext) {
450
+ const resolver = new resolver_1.FriendResolver(store, {
451
+ provider,
452
+ externalId,
453
+ tenantId: teamsContext?.tenantId,
454
+ displayName: teamsContext?.displayName || "Unknown",
455
+ channel: "teams",
456
+ });
457
+ toolContext.context = await resolver.resolve();
458
+ }
459
+ const friendId = toolContext?.context?.friend?.id || "default";
460
+ if (toolContext?.context?.friend) {
461
+ const trustGate = (0, trust_gate_1.enforceTrustGate)({
462
+ friend: toolContext.context.friend,
463
+ provider,
464
+ externalId,
465
+ tenantId: teamsContext?.tenantId,
466
+ channel: "teams",
467
+ });
468
+ if (!trustGate.allowed) {
469
+ if (trustGate.reason === "stranger_first_reply") {
470
+ stream.emit(trustGate.autoReply);
471
+ }
472
+ return;
473
+ }
474
+ }
475
+ const registry = (0, commands_1.createCommandRegistry)();
476
+ (0, commands_1.registerDefaultCommands)(registry);
477
+ // Check for slash commands
478
+ const parsed = (0, commands_1.parseSlashCommand)(text);
479
+ if (parsed) {
480
+ const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
481
+ if (dispatchResult.handled && dispatchResult.result) {
482
+ if (dispatchResult.result.action === "new") {
483
+ const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
484
+ (0, context_1.deleteSession)(sessPath);
485
+ stream.emit("session cleared");
486
+ return;
487
+ }
488
+ else if (dispatchResult.result.action === "response") {
489
+ stream.emit(dispatchResult.result.message || "");
490
+ return;
491
+ }
492
+ }
493
+ }
494
+ // Load or create session
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)
535
+ }
536
+ // Start the Teams app in DevtoolsPlugin mode (local dev) or Bot Service mode (real Teams).
537
+ // Mode is determined by getTeamsConfig().clientId.
538
+ // Text is always accumulated in textBuffer and flushed periodically (chunked streaming).
539
+ function startTeamsApp() {
540
+ const mentionStripping = { activity: { mentions: { stripText: true } } };
541
+ const teamsConfig = (0, config_2.getTeamsConfig)();
542
+ let app;
543
+ let mode;
544
+ const oauthConfig = (0, config_1.getOAuthConfig)();
545
+ if (teamsConfig.clientId) {
546
+ // Bot Service mode -- real Teams connection with SingleTenant credentials
547
+ app = new teams_apps_1.App({
548
+ clientId: teamsConfig.clientId,
549
+ clientSecret: teamsConfig.clientSecret,
550
+ tenantId: teamsConfig.tenantId,
551
+ oauth: { defaultConnectionName: oauthConfig.graphConnectionName },
552
+ ...mentionStripping,
553
+ });
554
+ mode = "Bot Service";
555
+ }
556
+ else {
557
+ // DevtoolsPlugin mode -- local development with Teams DevtoolsPlugin UI
558
+ app = new teams_apps_1.App({
559
+ plugins: [new teams_dev_1.DevtoolsPlugin()],
560
+ ...mentionStripping,
561
+ });
562
+ mode = "DevtoolsPlugin";
563
+ }
564
+ // Override default OAuth verify-state handler. The SDK's built-in handler
565
+ // uses a single defaultConnectionName, which breaks multi-connection setups
566
+ // (graph + ado + github). The verifyState activity only carries a `state`
567
+ // code with no connectionName, so we try each configured connection until
568
+ // one succeeds.
569
+ const allConnectionNames = [
570
+ oauthConfig.graphConnectionName,
571
+ oauthConfig.adoConnectionName,
572
+ oauthConfig.githubConnectionName,
573
+ ].filter(Boolean);
574
+ app.on("signin.verify-state", async (ctx) => {
575
+ const { api, activity } = ctx;
576
+ if (!activity.value?.state)
577
+ return { status: 404 };
578
+ for (const cn of allConnectionNames) {
579
+ try {
580
+ await api.users.token.get({
581
+ channelId: activity.channelId,
582
+ userId: activity.from.id,
583
+ connectionName: cn,
584
+ code: activity.value.state,
585
+ });
586
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.verify_state", component: "channels", message: `verify-state succeeded for connection "${cn}"`, meta: { connectionName: cn } });
587
+ return { status: 200 };
588
+ }
589
+ catch { /* try next */ }
590
+ }
591
+ (0, runtime_1.emitNervesEvent)({ level: "warn", event: "channel.verify_state", component: "channels", message: "verify-state failed for all connections", meta: {} });
592
+ return { status: 412 };
593
+ });
594
+ app.on("message", async (ctx) => {
595
+ const { stream, activity, api, signin } = ctx;
596
+ const text = activity.text || "";
597
+ const convId = activity.conversation?.id || "unknown";
598
+ const turnKey = teamsTurnKey(convId);
599
+ const userId = activity.from?.id || "";
600
+ const channelId = activity.channelId || "msteams";
601
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: "incoming teams message", meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
602
+ // Resolve pending confirmations IMMEDIATELY — before token fetches or
603
+ // the conversation lock. The original message holds the lock while
604
+ // awaiting confirmation, so acquiring it here would deadlock. Token
605
+ // fetches are also unnecessary (and slow) for a simple yes/no reply.
606
+ 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
+ return;
612
+ }
613
+ // If this conversation already has an active turn, steer follow-up input
614
+ // into that turn and avoid starting a second concurrent turn.
615
+ if (!_turnCoordinator.tryBeginTurn(turnKey)) {
616
+ _turnCoordinator.enqueueFollowUp(turnKey, {
617
+ conversationId: convId,
618
+ text,
619
+ receivedAt: Date.now(),
620
+ });
621
+ return;
622
+ }
623
+ try {
624
+ // Fetch tokens for both OAuth connections independently.
625
+ // Failures are silently caught -- the tool handler will request signin if needed.
626
+ let graphToken;
627
+ let adoToken;
628
+ let githubToken;
629
+ try {
630
+ const graphRes = await api.users.token.get({ userId, connectionName: oauthConfig.graphConnectionName, channelId });
631
+ graphToken = graphRes?.token;
632
+ }
633
+ catch { /* no token yet — tool handler will trigger signin */ }
634
+ try {
635
+ const adoRes = await api.users.token.get({ userId, connectionName: oauthConfig.adoConnectionName, channelId });
636
+ adoToken = adoRes?.token;
637
+ }
638
+ catch { /* no token yet — tool handler will trigger signin */ }
639
+ try {
640
+ const githubRes = await api.users.token.get({ userId, connectionName: oauthConfig.githubConnectionName, channelId });
641
+ githubToken = githubRes?.token;
642
+ }
643
+ 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 } });
645
+ const teamsContext = {
646
+ graphToken,
647
+ adoToken,
648
+ githubToken,
649
+ signin: async (cn) => {
650
+ try {
651
+ const result = await signin({ connectionName: cn });
652
+ (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.signin_result", component: "channels", message: `signin(${cn}): ${result ? "token received" : "no token"}`, meta: { connectionName: cn, hasToken: !!result } });
653
+ return result;
654
+ }
655
+ catch (e) {
656
+ const msg = e instanceof Error ? e.message : String(e);
657
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.signin_error", component: "channels", message: `signin(${cn}) failed`, meta: { connectionName: cn, reason: msg.slice(0, 100) } });
658
+ return undefined;
659
+ }
660
+ },
661
+ aadObjectId: activity.from?.aadObjectId,
662
+ tenantId: activity.conversation?.tenantId,
663
+ displayName: activity.from?.name,
664
+ };
665
+ /* v8 ignore next 5 -- bot-framework integration callback; tested via handleTeamsMessage sendMessage path @preserve */
666
+ const ctxSend = async (t) => {
667
+ // Use send with replyToId (not reply, which adds a blockquote).
668
+ // replyToId anchors the message after the user's message in Copilot Chat.
669
+ await ctx.send({ type: "message", text: t, replyToId: activity.id });
670
+ };
671
+ await handleTeamsMessage(text, stream, convId, teamsContext, ctxSend);
672
+ }
673
+ catch (err) {
674
+ const msg = err instanceof Error ? err.message : String(err);
675
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.handler_error", component: "channels", message: msg.slice(0, 200), meta: {} });
676
+ }
677
+ finally {
678
+ _turnCoordinator.endTurn(turnKey);
679
+ }
680
+ });
681
+ if (!process.listeners("unhandledRejection").some((l) => l.__agentHandler)) {
682
+ const handler = (err) => {
683
+ const msg = err instanceof Error ? err.message : String(err);
684
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "channel.unhandled_rejection", component: "channels", message: msg.slice(0, 200), meta: {} });
685
+ };
686
+ handler.__agentHandler = true;
687
+ process.on("unhandledRejection", handler);
688
+ }
689
+ app.event("error", ({ error }) => {
690
+ const msg = error instanceof Error ? error.message : String(error);
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;
694
+ app.start(port);
695
+ (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 } });
696
+ }