@ouro.bot/cli 0.1.0-alpha.42 → 0.1.0-alpha.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.json +7 -0
- package/dist/heart/core.js +90 -24
- package/dist/mind/context.js +12 -4
- package/dist/mind/first-impressions.js +14 -1
- package/dist/mind/prompt.js +3 -3
- package/dist/repertoire/tools-base.js +4 -1
- package/dist/senses/bluebubbles.js +24 -2
- package/dist/senses/cli.js +12 -2
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/inner-dialog.js +1 -0
- package/dist/senses/pipeline.js +18 -1
- package/dist/senses/teams.js +165 -83
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.43",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Continuity-aware onboarding now stays contextual: when an active task goes idle, onboarding guidance can reappear during genuine lulls instead of staying hidden just because no-handoff state was previously persisted.",
|
|
8
|
+
"Teams active-turn controls are safer: `/new` still clears the current session during long-running turns, and superseding follow-ups keep the user's replacement ask instead of dropping it."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
{
|
|
5
12
|
"version": "0.1.0-alpha.42",
|
|
6
13
|
"changes": [
|
package/dist/heart/core.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasToolIntent = exports.buildSystem = exports.toResponsesTools = exports.toResponsesInput = exports.streamResponsesApi = exports.streamChatCompletion = exports.getToolsForChannel = exports.summarizeArgs = exports.execTool = exports.tools = void 0;
|
|
3
4
|
exports.createProviderRegistry = createProviderRegistry;
|
|
4
5
|
exports.resetProviderRuntime = resetProviderRuntime;
|
|
5
6
|
exports.getModel = getModel;
|
|
@@ -110,6 +111,53 @@ function getProviderDisplayLabel() {
|
|
|
110
111
|
};
|
|
111
112
|
return providerLabelBuilders[getProvider()]();
|
|
112
113
|
}
|
|
114
|
+
// Re-export tools, execTool, summarizeArgs from ./tools for backward compat
|
|
115
|
+
var tools_2 = require("../repertoire/tools");
|
|
116
|
+
Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_2.tools; } });
|
|
117
|
+
Object.defineProperty(exports, "execTool", { enumerable: true, get: function () { return tools_2.execTool; } });
|
|
118
|
+
Object.defineProperty(exports, "summarizeArgs", { enumerable: true, get: function () { return tools_2.summarizeArgs; } });
|
|
119
|
+
Object.defineProperty(exports, "getToolsForChannel", { enumerable: true, get: function () { return tools_2.getToolsForChannel; } });
|
|
120
|
+
// Re-export streaming functions for backward compat
|
|
121
|
+
var streaming_1 = require("./streaming");
|
|
122
|
+
Object.defineProperty(exports, "streamChatCompletion", { enumerable: true, get: function () { return streaming_1.streamChatCompletion; } });
|
|
123
|
+
Object.defineProperty(exports, "streamResponsesApi", { enumerable: true, get: function () { return streaming_1.streamResponsesApi; } });
|
|
124
|
+
Object.defineProperty(exports, "toResponsesInput", { enumerable: true, get: function () { return streaming_1.toResponsesInput; } });
|
|
125
|
+
Object.defineProperty(exports, "toResponsesTools", { enumerable: true, get: function () { return streaming_1.toResponsesTools; } });
|
|
126
|
+
// Re-export prompt functions for backward compat
|
|
127
|
+
var prompt_2 = require("../mind/prompt");
|
|
128
|
+
Object.defineProperty(exports, "buildSystem", { enumerable: true, get: function () { return prompt_2.buildSystem; } });
|
|
129
|
+
function parseFinalAnswerPayload(argumentsText) {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(argumentsText);
|
|
132
|
+
if (typeof parsed === "string") {
|
|
133
|
+
return { answer: parsed };
|
|
134
|
+
}
|
|
135
|
+
if (!parsed || typeof parsed !== "object") {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
const answer = typeof parsed.answer === "string" ? parsed.answer : undefined;
|
|
139
|
+
const rawIntent = parsed.intent;
|
|
140
|
+
const intent = rawIntent === "complete" || rawIntent === "blocked" || rawIntent === "direct_reply"
|
|
141
|
+
? rawIntent
|
|
142
|
+
: undefined;
|
|
143
|
+
return { answer, intent };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function getFinalAnswerRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp) {
|
|
150
|
+
if (mustResolveBeforeHandoff && !intent) {
|
|
151
|
+
return "your final_answer is missing required intent. when you must keep going until done or blocked, call final_answer again with answer plus intent=complete, blocked, or direct_reply.";
|
|
152
|
+
}
|
|
153
|
+
if (mustResolveBeforeHandoff && intent === "direct_reply" && !sawSteeringFollowUp) {
|
|
154
|
+
return "your final_answer used intent=direct_reply without a newer steering follow-up. continue the unresolved work, or call final_answer again with intent=complete or blocked when appropriate.";
|
|
155
|
+
}
|
|
156
|
+
return "your final_answer was incomplete or malformed. call final_answer again with your complete response.";
|
|
157
|
+
}
|
|
158
|
+
// Re-export kick utilities for backward compat
|
|
159
|
+
var kicks_1 = require("./kicks");
|
|
160
|
+
Object.defineProperty(exports, "hasToolIntent", { enumerable: true, get: function () { return kicks_1.hasToolIntent; } });
|
|
113
161
|
function upsertSystemPrompt(messages, systemText) {
|
|
114
162
|
const systemMessage = { role: "system", content: systemText };
|
|
115
163
|
if (messages[0]?.role === "system") {
|
|
@@ -310,6 +358,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
310
358
|
let lastUsage;
|
|
311
359
|
let overflowRetried = false;
|
|
312
360
|
let retryCount = 0;
|
|
361
|
+
let outcome = "complete";
|
|
362
|
+
let sawSteeringFollowUp = false;
|
|
363
|
+
let mustResolveBeforeHandoffActive = options?.mustResolveBeforeHandoff === true;
|
|
313
364
|
// Prevent MaxListenersExceeded warning — each iteration adds a listener
|
|
314
365
|
try {
|
|
315
366
|
require("events").setMaxListeners(50, signal);
|
|
@@ -328,6 +379,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
328
379
|
const activeTools = toolChoiceRequired ? [...baseTools, tools_1.finalAnswerTool] : baseTools;
|
|
329
380
|
const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
|
|
330
381
|
if (steeringFollowUps.length > 0) {
|
|
382
|
+
const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
|
|
383
|
+
if (hasSupersedingFollowUp) {
|
|
384
|
+
mustResolveBeforeHandoffActive = false;
|
|
385
|
+
options?.setMustResolveBeforeHandoff?.(false);
|
|
386
|
+
outcome = "superseded";
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
if (steeringFollowUps.some((followUp) => followUp.effect === "set_no_handoff")) {
|
|
390
|
+
mustResolveBeforeHandoffActive = true;
|
|
391
|
+
options?.setMustResolveBeforeHandoff?.(true);
|
|
392
|
+
}
|
|
393
|
+
sawSteeringFollowUp = true;
|
|
331
394
|
for (const followUp of steeringFollowUps) {
|
|
332
395
|
messages.push({ role: "user", content: followUp.text });
|
|
333
396
|
}
|
|
@@ -335,8 +398,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
335
398
|
}
|
|
336
399
|
// Yield so pending I/O (stdin Ctrl-C) can be processed between iterations
|
|
337
400
|
await new Promise((r) => setImmediate(r));
|
|
338
|
-
if (signal?.aborted)
|
|
401
|
+
if (signal?.aborted) {
|
|
402
|
+
outcome = "aborted";
|
|
339
403
|
break;
|
|
404
|
+
}
|
|
340
405
|
try {
|
|
341
406
|
callbacks.onModelStart();
|
|
342
407
|
const result = await providerRuntime.streamTurn({
|
|
@@ -381,22 +446,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
381
446
|
const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
|
|
382
447
|
if (isSoleFinalAnswer) {
|
|
383
448
|
// Extract answer from the tool call arguments.
|
|
384
|
-
// Supports: {"answer":"text"}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
else if (parsed.answer != null) {
|
|
392
|
-
answer = parsed.answer;
|
|
393
|
-
}
|
|
394
|
-
// else: valid JSON but no answer field — answer stays undefined (retry)
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
// JSON parsing failed (e.g. truncated output) — answer stays undefined (retry)
|
|
398
|
-
}
|
|
399
|
-
if (answer != null) {
|
|
449
|
+
// Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
|
|
450
|
+
const { answer, intent } = parseFinalAnswerPayload(result.toolCalls[0].arguments);
|
|
451
|
+
const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
|
|
452
|
+
const validTerminalIntent = intent === "complete" || intent === "blocked";
|
|
453
|
+
const validClosure = answer != null
|
|
454
|
+
&& (!mustResolveBeforeHandoffActive || validDirectReply || validTerminalIntent);
|
|
455
|
+
if (validClosure) {
|
|
400
456
|
if (result.finalAnswerStreamed) {
|
|
401
457
|
// The streaming layer already parsed and emitted the answer
|
|
402
458
|
// progressively via FinalAnswerParser. Skip clearing and
|
|
@@ -409,19 +465,26 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
409
465
|
// Never truncate -- channel adapters handle splitting long messages.
|
|
410
466
|
callbacks.onTextChunk(answer);
|
|
411
467
|
}
|
|
412
|
-
// Keep the full assistant message (with tool_calls) for debuggability,
|
|
413
|
-
// plus a synthetic tool response so the conversation stays valid on resume.
|
|
414
468
|
messages.push(msg);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
469
|
+
if (validDirectReply) {
|
|
470
|
+
const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
|
|
471
|
+
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
|
|
472
|
+
providerRuntime.appendToolOutput(result.toolCalls[0].id, resumeWork);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const delivered = "(delivered)";
|
|
476
|
+
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
|
|
477
|
+
providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
|
|
478
|
+
outcome = intent === "blocked" ? "blocked" : "complete";
|
|
479
|
+
done = true;
|
|
480
|
+
}
|
|
418
481
|
}
|
|
419
482
|
else {
|
|
420
483
|
// Answer is undefined -- the model's final_answer was incomplete or
|
|
421
484
|
// malformed. Clear any partial streamed text or noise, then push the
|
|
422
485
|
// assistant msg + error tool result and let the model try again.
|
|
423
486
|
callbacks.onClearText?.();
|
|
424
|
-
const retryError =
|
|
487
|
+
const retryError = getFinalAnswerRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp);
|
|
425
488
|
messages.push(msg);
|
|
426
489
|
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: retryError });
|
|
427
490
|
providerRuntime.appendToolOutput(result.toolCalls[0].id, retryError);
|
|
@@ -485,6 +548,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
485
548
|
// Abort is not an error — just stop cleanly
|
|
486
549
|
if (signal?.aborted) {
|
|
487
550
|
stripLastToolCalls(messages);
|
|
551
|
+
outcome = "aborted";
|
|
488
552
|
break;
|
|
489
553
|
}
|
|
490
554
|
// Context overflow: trim aggressively and retry once
|
|
@@ -519,6 +583,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
519
583
|
});
|
|
520
584
|
if (aborted) {
|
|
521
585
|
stripLastToolCalls(messages);
|
|
586
|
+
outcome = "aborted";
|
|
522
587
|
break;
|
|
523
588
|
}
|
|
524
589
|
providerRuntime.resetTurnState(messages);
|
|
@@ -534,6 +599,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
534
599
|
meta: {},
|
|
535
600
|
});
|
|
536
601
|
stripLastToolCalls(messages);
|
|
602
|
+
outcome = "errored";
|
|
537
603
|
done = true;
|
|
538
604
|
}
|
|
539
605
|
}
|
|
@@ -544,5 +610,5 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
544
610
|
message: "runAgent turn completed",
|
|
545
611
|
meta: { done },
|
|
546
612
|
});
|
|
547
|
-
return { usage: lastUsage };
|
|
613
|
+
return { usage: lastUsage, outcome };
|
|
548
614
|
}
|
package/dist/mind/context.js
CHANGED
|
@@ -227,7 +227,7 @@ function repairSessionMessages(messages) {
|
|
|
227
227
|
});
|
|
228
228
|
return result;
|
|
229
229
|
}
|
|
230
|
-
function saveSession(filePath, messages, lastUsage) {
|
|
230
|
+
function saveSession(filePath, messages, lastUsage, state) {
|
|
231
231
|
const violations = validateSessionMessages(messages);
|
|
232
232
|
if (violations.length > 0) {
|
|
233
233
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -243,6 +243,9 @@ function saveSession(filePath, messages, lastUsage) {
|
|
|
243
243
|
const envelope = { version: 1, messages };
|
|
244
244
|
if (lastUsage)
|
|
245
245
|
envelope.lastUsage = lastUsage;
|
|
246
|
+
if (state?.mustResolveBeforeHandoff === true) {
|
|
247
|
+
envelope.state = { mustResolveBeforeHandoff: true };
|
|
248
|
+
}
|
|
246
249
|
fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
|
|
247
250
|
}
|
|
248
251
|
function loadSession(filePath) {
|
|
@@ -263,13 +266,18 @@ function loadSession(filePath) {
|
|
|
263
266
|
});
|
|
264
267
|
messages = repairSessionMessages(messages);
|
|
265
268
|
}
|
|
266
|
-
|
|
269
|
+
const state = data?.state && typeof data.state === "object" && data.state !== null
|
|
270
|
+
&& typeof data.state.mustResolveBeforeHandoff === "boolean"
|
|
271
|
+
&& data.state.mustResolveBeforeHandoff === true
|
|
272
|
+
? { mustResolveBeforeHandoff: true }
|
|
273
|
+
: undefined;
|
|
274
|
+
return { messages, lastUsage: data.lastUsage, state };
|
|
267
275
|
}
|
|
268
276
|
catch {
|
|
269
277
|
return null;
|
|
270
278
|
}
|
|
271
279
|
}
|
|
272
|
-
function postTurn(messages, sessPath, usage, hooks) {
|
|
280
|
+
function postTurn(messages, sessPath, usage, hooks, state) {
|
|
273
281
|
if (hooks?.beforeTrim) {
|
|
274
282
|
try {
|
|
275
283
|
hooks.beforeTrim([...messages]);
|
|
@@ -289,7 +297,7 @@ function postTurn(messages, sessPath, usage, hooks) {
|
|
|
289
297
|
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
290
298
|
const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
|
|
291
299
|
messages.splice(0, messages.length, ...trimmed);
|
|
292
|
-
saveSession(sessPath, messages, usage);
|
|
300
|
+
saveSession(sessPath, messages, usage, state);
|
|
293
301
|
}
|
|
294
302
|
function deleteSession(filePath) {
|
|
295
303
|
try {
|
|
@@ -11,9 +11,22 @@ exports.ONBOARDING_TOKEN_THRESHOLD = 100_000;
|
|
|
11
11
|
function isOnboarding(friend) {
|
|
12
12
|
return (friend.totalTokens ?? 0) < exports.ONBOARDING_TOKEN_THRESHOLD;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function hasLiveContinuityPressure(state) {
|
|
15
|
+
if (!state)
|
|
16
|
+
return false;
|
|
17
|
+
if (typeof state.currentObligation === "string" && state.currentObligation.trim().length > 0)
|
|
18
|
+
return true;
|
|
19
|
+
if (state.mustResolveBeforeHandoff === true)
|
|
20
|
+
return true;
|
|
21
|
+
if (state.hasQueuedFollowUp === true)
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
function getFirstImpressions(friend, state) {
|
|
15
26
|
if (!isOnboarding(friend))
|
|
16
27
|
return "";
|
|
28
|
+
if (hasLiveContinuityPressure(state))
|
|
29
|
+
return "";
|
|
17
30
|
(0, runtime_1.emitNervesEvent)({
|
|
18
31
|
component: "mind",
|
|
19
32
|
event: "mind.first_impressions",
|
package/dist/mind/prompt.js
CHANGED
|
@@ -423,7 +423,7 @@ tool_choice is set to "required" -- i must call a tool on every turn.
|
|
|
423
423
|
\`final_answer\` must be the ONLY tool call in that turn. do not combine it with other tool calls.
|
|
424
424
|
do NOT call no-op tools just before \`final_answer\`. if i am done, i call \`final_answer\` directly.`;
|
|
425
425
|
}
|
|
426
|
-
function contextSection(context) {
|
|
426
|
+
function contextSection(context, options) {
|
|
427
427
|
if (!context)
|
|
428
428
|
return "";
|
|
429
429
|
const lines = ["## friend context"];
|
|
@@ -459,7 +459,7 @@ function contextSection(context) {
|
|
|
459
459
|
lines.push("when i learn something that might invalidate an existing note, i check related notes and update or override any that are stale.");
|
|
460
460
|
lines.push("i save ANYTHING i learn about my friend immediately with save_friend_note -- names, preferences, what they do, what they care about. when in doubt, save it. saving comes BEFORE responding: i call save_friend_note first, then final_answer on the next turn.");
|
|
461
461
|
// Onboarding instructions (only below token threshold -- drop once exceeded)
|
|
462
|
-
const impressions = (0, first_impressions_1.getFirstImpressions)(friend);
|
|
462
|
+
const impressions = (0, first_impressions_1.getFirstImpressions)(friend, options);
|
|
463
463
|
if (impressions) {
|
|
464
464
|
lines.push(impressions);
|
|
465
465
|
}
|
|
@@ -553,7 +553,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
553
553
|
}),
|
|
554
554
|
memoryFriendToolContractSection(),
|
|
555
555
|
toolBehaviorSection(options),
|
|
556
|
-
contextSection(context),
|
|
556
|
+
contextSection(context, options),
|
|
557
557
|
]
|
|
558
558
|
.filter(Boolean)
|
|
559
559
|
.join("\n\n");
|
|
@@ -690,7 +690,10 @@ exports.finalAnswerTool = {
|
|
|
690
690
|
description: "respond to the user with your message. call this tool when you are ready to deliver your response.",
|
|
691
691
|
parameters: {
|
|
692
692
|
type: "object",
|
|
693
|
-
properties: {
|
|
693
|
+
properties: {
|
|
694
|
+
answer: { type: "string" },
|
|
695
|
+
intent: { type: "string", enum: ["complete", "blocked", "direct_reply"] },
|
|
696
|
+
},
|
|
694
697
|
required: ["answer"],
|
|
695
698
|
},
|
|
696
699
|
},
|
|
@@ -147,6 +147,10 @@ function extractMessageText(content) {
|
|
|
147
147
|
.filter(Boolean)
|
|
148
148
|
.join("\n");
|
|
149
149
|
}
|
|
150
|
+
function isHistoricalLaneMetadataLine(line) {
|
|
151
|
+
return /^\[(conversation scope|recent active lanes|routing control):?/i.test(line)
|
|
152
|
+
|| /^- (top_level|thread:[^:]+):/i.test(line);
|
|
153
|
+
}
|
|
150
154
|
function extractHistoricalLaneSummary(messages) {
|
|
151
155
|
const seen = new Set();
|
|
152
156
|
const summaries = [];
|
|
@@ -171,7 +175,7 @@ function extractHistoricalLaneSummary(messages) {
|
|
|
171
175
|
.split("\n")
|
|
172
176
|
.slice(1)
|
|
173
177
|
.map((line) => line.trim())
|
|
174
|
-
.find(
|
|
178
|
+
.find((line) => line.length > 0 && !isHistoricalLaneMetadataLine(line))
|
|
175
179
|
?.slice(0, 80) ?? "(no recent text)";
|
|
176
180
|
summaries.push({
|
|
177
181
|
key: laneKey,
|
|
@@ -230,6 +234,23 @@ function buildInboundContent(event, existingMessages) {
|
|
|
230
234
|
...event.inputPartsForAgent,
|
|
231
235
|
];
|
|
232
236
|
}
|
|
237
|
+
function getBlueBubblesContinuityIngressTexts(event) {
|
|
238
|
+
if (event.kind !== "message")
|
|
239
|
+
return [];
|
|
240
|
+
const text = event.textForAgent.trim();
|
|
241
|
+
if (text.length > 0)
|
|
242
|
+
return [text];
|
|
243
|
+
const fallbackText = (event.inputPartsForAgent ?? [])
|
|
244
|
+
.map((part) => {
|
|
245
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
246
|
+
return part.text.trim();
|
|
247
|
+
}
|
|
248
|
+
return "";
|
|
249
|
+
})
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join("\n");
|
|
252
|
+
return fallbackText ? [fallbackText] : [];
|
|
253
|
+
}
|
|
233
254
|
function createReplyTargetController(event) {
|
|
234
255
|
const defaultTargetLabel = event.kind === "message" && event.threadOriginatorGuid?.trim() ? "current_lane" : "top_level";
|
|
235
256
|
let selection = event.kind === "message" && event.threadOriginatorGuid?.trim()
|
|
@@ -510,9 +531,10 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
510
531
|
channel: "bluebubbles",
|
|
511
532
|
capabilities: bbCapabilities,
|
|
512
533
|
messages: [userMessage],
|
|
534
|
+
continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
|
|
513
535
|
callbacks,
|
|
514
536
|
friendResolver: { resolve: () => Promise.resolve(context) },
|
|
515
|
-
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath }) },
|
|
537
|
+
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath, state: existing?.state }) },
|
|
516
538
|
pendingDir,
|
|
517
539
|
friendStore: store,
|
|
518
540
|
provider: "imessage-handle",
|
package/dist/senses/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MarkdownStreamer = exports.InputController = exports.Spinner = void 0;
|
|
37
37
|
exports.formatPendingPrefix = formatPendingPrefix;
|
|
38
|
+
exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
|
|
38
39
|
exports.handleSigint = handleSigint;
|
|
39
40
|
exports.addHistory = addHistory;
|
|
40
41
|
exports.renderMarkdown = renderMarkdown;
|
|
@@ -80,6 +81,10 @@ function formatPendingPrefix(messages, agentName) {
|
|
|
80
81
|
: `[message from ${msg.from}: ${msg.content}]`)
|
|
81
82
|
.join("\n");
|
|
82
83
|
}
|
|
84
|
+
function getCliContinuityIngressTexts(input) {
|
|
85
|
+
const trimmed = input.trim();
|
|
86
|
+
return trimmed ? [trimmed] : [];
|
|
87
|
+
}
|
|
83
88
|
// spinner that only touches stderr, cleans up after itself
|
|
84
89
|
// exported for direct testability (stop-without-start branch)
|
|
85
90
|
class Spinner {
|
|
@@ -729,6 +734,7 @@ async function main(agentName, options) {
|
|
|
729
734
|
}
|
|
730
735
|
// Load existing session or start fresh
|
|
731
736
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
737
|
+
let sessionState = existing?.state;
|
|
732
738
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
733
739
|
? existing.messages
|
|
734
740
|
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
|
|
@@ -748,9 +754,10 @@ async function main(agentName, options) {
|
|
|
748
754
|
channel: "cli",
|
|
749
755
|
capabilities: cliCapabilities,
|
|
750
756
|
messages: [{ role: "user", content: userInput }],
|
|
757
|
+
continuityIngressTexts: getCliContinuityIngressTexts(userInput),
|
|
751
758
|
callbacks,
|
|
752
759
|
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
753
|
-
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages, sessionPath: sessPath }) },
|
|
760
|
+
sessionLoader: { loadOrCreate: () => Promise.resolve({ messages, sessionPath: sessPath, state: sessionState }) },
|
|
754
761
|
pendingDir,
|
|
755
762
|
friendStore,
|
|
756
763
|
provider: "local",
|
|
@@ -766,7 +773,10 @@ async function main(agentName, options) {
|
|
|
766
773
|
summarize,
|
|
767
774
|
},
|
|
768
775
|
}),
|
|
769
|
-
postTurn:
|
|
776
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
777
|
+
(0, context_1.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
|
|
778
|
+
sessionState = state;
|
|
779
|
+
},
|
|
770
780
|
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
771
781
|
signal,
|
|
772
782
|
runAgentOptions: {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeContinuityClauses = normalizeContinuityClauses;
|
|
4
|
+
exports.resolveMustResolveBeforeHandoff = resolveMustResolveBeforeHandoff;
|
|
5
|
+
exports.classifySteeringFollowUpEffect = classifySteeringFollowUpEffect;
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
const NO_HANDOFF_CLAUSES = new Set([
|
|
8
|
+
"dont return control until complete or blocked",
|
|
9
|
+
"do not return control until complete or blocked",
|
|
10
|
+
"dont hand back control until complete or blocked",
|
|
11
|
+
"do not hand back control until complete or blocked",
|
|
12
|
+
"keep going until youre done",
|
|
13
|
+
"keep going until you are done",
|
|
14
|
+
"keep working until youre done",
|
|
15
|
+
"keep working until you are done",
|
|
16
|
+
"dont stop until youre done",
|
|
17
|
+
"do not stop until youre done",
|
|
18
|
+
"only come back if blocked",
|
|
19
|
+
"only return if blocked",
|
|
20
|
+
"only respond if blocked",
|
|
21
|
+
"work autonomously on this",
|
|
22
|
+
"work on this autonomously",
|
|
23
|
+
"handle this autonomously",
|
|
24
|
+
]);
|
|
25
|
+
const CANCEL_SUPERSEDE_CLAUSES = new Set([
|
|
26
|
+
"stop",
|
|
27
|
+
"cancel that",
|
|
28
|
+
"cancel this",
|
|
29
|
+
"never mind",
|
|
30
|
+
"nevermind",
|
|
31
|
+
"forget it",
|
|
32
|
+
"ignore that",
|
|
33
|
+
"ignore this",
|
|
34
|
+
"hold off",
|
|
35
|
+
"ill take it from here",
|
|
36
|
+
"i will take it from here",
|
|
37
|
+
"lets do something else",
|
|
38
|
+
"stop working on that",
|
|
39
|
+
"stop working on this",
|
|
40
|
+
"dont do that",
|
|
41
|
+
"do not do that",
|
|
42
|
+
]);
|
|
43
|
+
function normalizeContinuityClauses(text) {
|
|
44
|
+
return text
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.split(/[\n.!?;]+/)
|
|
47
|
+
.map((clause) => clause.replace(/[^a-z\s]/g, "").replace(/\s+/g, " ").trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
function resolveMustResolveBeforeHandoff(initialValue, ingressTexts) {
|
|
51
|
+
let current = initialValue;
|
|
52
|
+
for (const text of ingressTexts ?? []) {
|
|
53
|
+
for (const clause of normalizeContinuityClauses(text)) {
|
|
54
|
+
if (CANCEL_SUPERSEDE_CLAUSES.has(clause)) {
|
|
55
|
+
current = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (NO_HANDOFF_CLAUSES.has(clause)) {
|
|
59
|
+
current = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
(0, runtime_1.emitNervesEvent)({
|
|
64
|
+
component: "senses",
|
|
65
|
+
event: "senses.continuity_state_resolved",
|
|
66
|
+
message: "resolved continuity handoff state from ingress text",
|
|
67
|
+
meta: {
|
|
68
|
+
initialValue,
|
|
69
|
+
finalValue: current,
|
|
70
|
+
ingressCount: ingressTexts?.length ?? 0,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
function classifySteeringFollowUpEffect(text) {
|
|
76
|
+
const clauses = normalizeContinuityClauses(text);
|
|
77
|
+
let effect = "none";
|
|
78
|
+
if (clauses.some((clause) => CANCEL_SUPERSEDE_CLAUSES.has(clause))) {
|
|
79
|
+
effect = "clear_and_supersede";
|
|
80
|
+
}
|
|
81
|
+
else if (clauses.some((clause) => NO_HANDOFF_CLAUSES.has(clause))) {
|
|
82
|
+
effect = "set_no_handoff";
|
|
83
|
+
}
|
|
84
|
+
(0, runtime_1.emitNervesEvent)({
|
|
85
|
+
component: "senses",
|
|
86
|
+
event: "senses.continuity_follow_up_classified",
|
|
87
|
+
message: "classified steering follow-up continuity effect",
|
|
88
|
+
meta: {
|
|
89
|
+
effect,
|
|
90
|
+
clauseCount: clauses.length,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
return effect;
|
|
94
|
+
}
|
|
@@ -248,6 +248,7 @@ async function runInnerDialogTurn(options) {
|
|
|
248
248
|
channel: "inner",
|
|
249
249
|
capabilities: innerCapabilities,
|
|
250
250
|
messages: [userMessage],
|
|
251
|
+
continuityIngressTexts: [],
|
|
251
252
|
callbacks,
|
|
252
253
|
friendResolver: { resolve: () => Promise.resolve(selfContext) },
|
|
253
254
|
sessionLoader,
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.handleInboundTurn = handleInboundTurn;
|
|
9
9
|
const runtime_1 = require("../nerves/runtime");
|
|
10
|
+
const continuity_1 = require("./continuity");
|
|
10
11
|
// ── Pipeline ──────────────────────────────────────────────────────
|
|
11
12
|
async function handleInboundTurn(input) {
|
|
12
13
|
// Step 1: Resolve friend
|
|
@@ -54,6 +55,11 @@ async function handleInboundTurn(input) {
|
|
|
54
55
|
// Step 3: Load/create session
|
|
55
56
|
const session = await input.sessionLoader.loadOrCreate();
|
|
56
57
|
const sessionMessages = session.messages;
|
|
58
|
+
let mustResolveBeforeHandoff = (0, continuity_1.resolveMustResolveBeforeHandoff)(session.state?.mustResolveBeforeHandoff === true, input.continuityIngressTexts);
|
|
59
|
+
const currentObligation = input.continuityIngressTexts
|
|
60
|
+
?.map((text) => text.trim())
|
|
61
|
+
.filter((text) => text.length > 0)
|
|
62
|
+
.at(-1);
|
|
57
63
|
// Step 4: Drain pending messages
|
|
58
64
|
const pending = input.drainPending(input.pendingDir);
|
|
59
65
|
// Assemble messages: session messages + pending (formatted) + inbound user messages
|
|
@@ -92,6 +98,11 @@ async function handleInboundTurn(input) {
|
|
|
92
98
|
const existingToolContext = input.runAgentOptions?.toolContext;
|
|
93
99
|
const runAgentOptions = {
|
|
94
100
|
...input.runAgentOptions,
|
|
101
|
+
currentObligation,
|
|
102
|
+
mustResolveBeforeHandoff,
|
|
103
|
+
setMustResolveBeforeHandoff: (value) => {
|
|
104
|
+
mustResolveBeforeHandoff = value;
|
|
105
|
+
},
|
|
95
106
|
toolContext: {
|
|
96
107
|
/* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
|
|
97
108
|
signin: async () => undefined,
|
|
@@ -102,7 +113,12 @@ async function handleInboundTurn(input) {
|
|
|
102
113
|
};
|
|
103
114
|
const result = await input.runAgent(sessionMessages, input.callbacks, input.channel, input.signal, runAgentOptions);
|
|
104
115
|
// Step 6: postTurn
|
|
105
|
-
|
|
116
|
+
const nextState = result.outcome === "complete" || result.outcome === "blocked" || result.outcome === "superseded"
|
|
117
|
+
? undefined
|
|
118
|
+
: mustResolveBeforeHandoff
|
|
119
|
+
? { mustResolveBeforeHandoff: true }
|
|
120
|
+
: undefined;
|
|
121
|
+
input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
|
|
106
122
|
// Step 7: Token accumulation
|
|
107
123
|
await input.accumulateFriendTokens(input.friendStore, resolvedContext.friend.id, result.usage);
|
|
108
124
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -118,6 +134,7 @@ async function handleInboundTurn(input) {
|
|
|
118
134
|
resolvedContext,
|
|
119
135
|
gateResult,
|
|
120
136
|
usage: result.usage,
|
|
137
|
+
turnOutcome: result.outcome,
|
|
121
138
|
sessionPath: session.sessionPath,
|
|
122
139
|
messages: sessionMessages,
|
|
123
140
|
};
|
package/dist/senses/teams.js
CHANGED
|
@@ -68,6 +68,7 @@ const path = __importStar(require("path"));
|
|
|
68
68
|
const trust_gate_1 = require("./trust-gate");
|
|
69
69
|
const pipeline_1 = require("./pipeline");
|
|
70
70
|
const pending_1 = require("../mind/pending");
|
|
71
|
+
const continuity_1 = require("./continuity");
|
|
71
72
|
// Strip @mention markup from incoming messages.
|
|
72
73
|
// Removes <at>...</at> tags and trims extra whitespace.
|
|
73
74
|
// Fallback safety net -- the SDK's activity.mentions.stripText should handle
|
|
@@ -439,6 +440,34 @@ function getFriendStore() {
|
|
|
439
440
|
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
440
441
|
return new store_file_1.FileFriendStore(friendsPath);
|
|
441
442
|
}
|
|
443
|
+
function createTeamsCommandRegistry() {
|
|
444
|
+
const registry = (0, commands_1.createCommandRegistry)();
|
|
445
|
+
(0, commands_1.registerDefaultCommands)(registry);
|
|
446
|
+
return registry;
|
|
447
|
+
}
|
|
448
|
+
function handleTeamsSlashCommand(text, registry, friendId, conversationId, stream, emitResponse = true) {
|
|
449
|
+
const parsed = (0, commands_1.parseSlashCommand)(text);
|
|
450
|
+
if (!parsed)
|
|
451
|
+
return null;
|
|
452
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
453
|
+
if (!dispatchResult.handled || !dispatchResult.result) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
if (dispatchResult.result.action === "new") {
|
|
457
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(friendId, "teams", conversationId));
|
|
458
|
+
if (emitResponse) {
|
|
459
|
+
stream.emit("session cleared");
|
|
460
|
+
}
|
|
461
|
+
return "new";
|
|
462
|
+
}
|
|
463
|
+
if (dispatchResult.result.action === "response") {
|
|
464
|
+
if (emitResponse) {
|
|
465
|
+
stream.emit(dispatchResult.result.message || "");
|
|
466
|
+
}
|
|
467
|
+
return "response";
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
442
471
|
// Handle an incoming Teams message
|
|
443
472
|
async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage) {
|
|
444
473
|
const turnKey = teamsTurnKey(conversationId);
|
|
@@ -464,24 +493,10 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
464
493
|
// Pre-resolve friend for session path + slash commands (pipeline will re-use the cached result)
|
|
465
494
|
const resolvedContext = await resolver.resolve();
|
|
466
495
|
const friendId = resolvedContext.friend.id;
|
|
467
|
-
const registry = (
|
|
468
|
-
(0, commands_1.registerDefaultCommands)(registry);
|
|
496
|
+
const registry = createTeamsCommandRegistry();
|
|
469
497
|
// Check for slash commands (before pipeline -- these are transport-level concerns)
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const dispatchResult = registry.dispatch(parsed.command, { channel: "teams" });
|
|
473
|
-
if (dispatchResult.handled && dispatchResult.result) {
|
|
474
|
-
if (dispatchResult.result.action === "new") {
|
|
475
|
-
const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
|
|
476
|
-
(0, context_1.deleteSession)(sessPath);
|
|
477
|
-
stream.emit("session cleared");
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
else if (dispatchResult.result.action === "response") {
|
|
481
|
-
stream.emit(dispatchResult.result.message || "");
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
498
|
+
if (handleTeamsSlashCommand(text, registry, friendId, conversationId, stream)) {
|
|
499
|
+
return;
|
|
485
500
|
}
|
|
486
501
|
// ── Teams adapter concerns: controller, callbacks, session path ──────────
|
|
487
502
|
const controller = new AbortController();
|
|
@@ -501,76 +516,107 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
501
516
|
tenantId: teamsContext.tenantId,
|
|
502
517
|
botApi: teamsContext.botApi,
|
|
503
518
|
} : {};
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
capabilities: teamsCapabilities,
|
|
516
|
-
messages: [{ role: "user", content: text }],
|
|
517
|
-
callbacks,
|
|
518
|
-
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
519
|
-
sessionLoader: {
|
|
520
|
-
loadOrCreate: async () => {
|
|
521
|
-
const existing = (0, context_1.loadSession)(sessPath);
|
|
522
|
-
const messages = existing?.messages && existing.messages.length > 0
|
|
523
|
-
? existing.messages
|
|
524
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, resolvedContext) }];
|
|
525
|
-
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
526
|
-
return { messages, sessionPath: sessPath };
|
|
519
|
+
let currentText = text;
|
|
520
|
+
while (true) {
|
|
521
|
+
let drainedSteeringFollowUps = [];
|
|
522
|
+
// Build runAgentOptions with Teams-specific fields
|
|
523
|
+
const agentOptions = {
|
|
524
|
+
traceId,
|
|
525
|
+
toolContext: teamsToolContext,
|
|
526
|
+
drainSteeringFollowUps: () => {
|
|
527
|
+
drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
|
|
528
|
+
.map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
|
|
529
|
+
return drainedSteeringFollowUps;
|
|
527
530
|
},
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
531
|
+
};
|
|
532
|
+
if (channelConfig.skipConfirmation)
|
|
533
|
+
agentOptions.skipConfirmation = true;
|
|
534
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
535
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
536
|
+
channel: "teams",
|
|
537
|
+
capabilities: teamsCapabilities,
|
|
538
|
+
messages: [{ role: "user", content: currentText }],
|
|
539
|
+
continuityIngressTexts: [currentText],
|
|
540
|
+
callbacks,
|
|
541
|
+
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|
|
542
|
+
sessionLoader: {
|
|
543
|
+
loadOrCreate: async () => {
|
|
544
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
545
|
+
const messages = existing?.messages && existing.messages.length > 0
|
|
546
|
+
? existing.messages
|
|
547
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", undefined, resolvedContext) }];
|
|
548
|
+
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
549
|
+
return { messages, sessionPath: sessPath, state: existing?.state };
|
|
550
|
+
},
|
|
546
551
|
},
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
552
|
+
pendingDir,
|
|
553
|
+
friendStore: store,
|
|
554
|
+
provider,
|
|
555
|
+
externalId,
|
|
556
|
+
tenantId: teamsContext?.tenantId,
|
|
557
|
+
isGroupChat: false,
|
|
558
|
+
groupHasFamilyMember: false,
|
|
559
|
+
hasExistingGroupWithFamily: false,
|
|
560
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
561
|
+
drainPending: pending_1.drainPending,
|
|
562
|
+
runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
|
|
563
|
+
...opts,
|
|
564
|
+
toolContext: {
|
|
565
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
566
|
+
signin: async () => undefined,
|
|
567
|
+
...opts?.toolContext,
|
|
568
|
+
summarize: teamsToolContext.summarize,
|
|
569
|
+
},
|
|
570
|
+
}),
|
|
571
|
+
postTurn: context_1.postTurn,
|
|
572
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
573
|
+
signal: controller.signal,
|
|
574
|
+
runAgentOptions: agentOptions,
|
|
575
|
+
});
|
|
576
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
577
|
+
if (!result.gateResult.allowed) {
|
|
578
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
579
|
+
stream.emit(result.gateResult.autoReply);
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
557
582
|
}
|
|
558
|
-
|
|
583
|
+
// Flush any remaining accumulated text at end of turn
|
|
584
|
+
await callbacks.flush();
|
|
585
|
+
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
586
|
+
// This must happen after the stream is done so the OAuth card renders properly.
|
|
587
|
+
if (teamsContext && result.messages) {
|
|
588
|
+
const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
589
|
+
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
590
|
+
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
591
|
+
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
592
|
+
await teamsContext.signin(teamsContext.adoConnectionName);
|
|
593
|
+
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
594
|
+
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
595
|
+
}
|
|
596
|
+
if (result.turnOutcome !== "superseded") {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const supersedingIndex = drainedSteeringFollowUps
|
|
600
|
+
.map((followUp) => followUp.effect)
|
|
601
|
+
.lastIndexOf("clear_and_supersede");
|
|
602
|
+
if (supersedingIndex < 0) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const supersedingFollowUp = drainedSteeringFollowUps[supersedingIndex];
|
|
606
|
+
const replayTail = drainedSteeringFollowUps
|
|
607
|
+
.slice(supersedingIndex + 1)
|
|
608
|
+
.map((followUp) => followUp.text.trim())
|
|
609
|
+
.filter((followUpText) => followUpText.length > 0)
|
|
610
|
+
.join("\n");
|
|
611
|
+
if (replayTail) {
|
|
612
|
+
currentText = replayTail;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (handleTeamsSlashCommand(supersedingFollowUp.text, registry, friendId, conversationId, stream, false)) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
currentText = supersedingFollowUp.text;
|
|
559
619
|
}
|
|
560
|
-
// Flush any remaining accumulated text at end of turn
|
|
561
|
-
await callbacks.flush();
|
|
562
|
-
// After the agent loop, check if any tool returned AUTH_REQUIRED and trigger signin.
|
|
563
|
-
// This must happen after the stream is done so the OAuth card renders properly.
|
|
564
|
-
if (teamsContext && result.messages) {
|
|
565
|
-
const allContent = result.messages.map(m => typeof m.content === "string" ? m.content : "").join("\n");
|
|
566
|
-
if (allContent.includes("AUTH_REQUIRED:graph") && teamsContext.graphConnectionName)
|
|
567
|
-
await teamsContext.signin(teamsContext.graphConnectionName);
|
|
568
|
-
if (allContent.includes("AUTH_REQUIRED:ado") && teamsContext.adoConnectionName)
|
|
569
|
-
await teamsContext.signin(teamsContext.adoConnectionName);
|
|
570
|
-
if (allContent.includes("AUTH_REQUIRED:github") && teamsContext.githubConnectionName)
|
|
571
|
-
await teamsContext.signin(teamsContext.githubConnectionName);
|
|
572
|
-
}
|
|
573
|
-
// SDK auto-closes the stream after our handler returns (app.process.js)
|
|
574
620
|
}
|
|
575
621
|
// Internal port for the secondary bot App (not exposed externally).
|
|
576
622
|
// The primary app proxies /api/messages-secondary → localhost:SECONDARY_PORT/api/messages.
|
|
@@ -681,6 +727,41 @@ function registerBotHandlers(app, label) {
|
|
|
681
727
|
if (resolvePendingConfirmation(convId, text)) {
|
|
682
728
|
return;
|
|
683
729
|
}
|
|
730
|
+
const commandRegistry = createTeamsCommandRegistry();
|
|
731
|
+
const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
|
|
732
|
+
if (parsedSlashCommand) {
|
|
733
|
+
const dispatchResult = commandRegistry.dispatch(parsedSlashCommand.command, { channel: "teams" });
|
|
734
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
735
|
+
if (dispatchResult.result.action === "response") {
|
|
736
|
+
stream.emit(dispatchResult.result.message || "");
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (dispatchResult.result.action === "new") {
|
|
740
|
+
const commandStore = getFriendStore();
|
|
741
|
+
const commandProvider = activity.from?.aadObjectId ? "aad" : "teams-conversation";
|
|
742
|
+
const commandExternalId = activity.from?.aadObjectId || convId;
|
|
743
|
+
const commandResolver = new resolver_1.FriendResolver(commandStore, {
|
|
744
|
+
provider: commandProvider,
|
|
745
|
+
externalId: commandExternalId,
|
|
746
|
+
tenantId: activity.conversation?.tenantId,
|
|
747
|
+
displayName: activity.from?.name || "Unknown",
|
|
748
|
+
channel: "teams",
|
|
749
|
+
});
|
|
750
|
+
const commandContext = await commandResolver.resolve();
|
|
751
|
+
(0, context_1.deleteSession)((0, config_2.sessionPath)(commandContext.friend.id, "teams", convId));
|
|
752
|
+
stream.emit("session cleared");
|
|
753
|
+
if (_turnCoordinator.isTurnActive(turnKey)) {
|
|
754
|
+
_turnCoordinator.enqueueFollowUp(turnKey, {
|
|
755
|
+
conversationId: convId,
|
|
756
|
+
text,
|
|
757
|
+
receivedAt: Date.now(),
|
|
758
|
+
effect: "clear_and_supersede",
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
684
765
|
// If this conversation already has an active turn, steer follow-up input
|
|
685
766
|
// into that turn and avoid starting a second concurrent turn.
|
|
686
767
|
if (!_turnCoordinator.tryBeginTurn(turnKey)) {
|
|
@@ -688,6 +769,7 @@ function registerBotHandlers(app, label) {
|
|
|
688
769
|
conversationId: convId,
|
|
689
770
|
text,
|
|
690
771
|
receivedAt: Date.now(),
|
|
772
|
+
effect: (0, continuity_1.classifySteeringFollowUpEffect)(text),
|
|
691
773
|
});
|
|
692
774
|
return;
|
|
693
775
|
}
|