@ouro.bot/cli 0.1.0-alpha.793 → 0.1.0-alpha.795
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 +13 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +1065 -1050
- package/dist/senses/sanctuary-media-catalog-contract.js +24 -3
- package/dist/senses/shared-turn.js +43 -17
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@ exports.sanctuaryMediaCatalogRequiredToolCalls = sanctuaryMediaCatalogRequiredTo
|
|
|
4
4
|
const runtime_1 = require("../nerves/runtime");
|
|
5
5
|
const sanctuary_media_optimization_1 = require("./sanctuary-media-optimization");
|
|
6
6
|
const REQUIRED_TOOL_NAMES = ["sanctuary_search_media_catalog"];
|
|
7
|
-
const HOUSEHOLD_JARGON = /\b(?:bounded|catalog
|
|
7
|
+
const HOUSEHOLD_JARGON = /\b(?:bounded|catalog|inventory|endpoint|backend|data shape|pars(?:e|ed|ing)|json|jellyfin|unmanic|sanctuary_search_media_catalog)\b/iu;
|
|
8
|
+
const NEGATED_MEDIA_ACCESS = /\b(?:not|never|none|nothing|zero|inaccessible|invisible|unavailable|broken|failed|failing)\b|\b(?:isn[’']t|won[’']t)\b|\bno\s+access\b|\b(?:lost|lacks?|lacking|without)\s+access\b|\bonly\s+\d|\bfewer\s+than\b|\bsome\s+of\b|\b(?:part|portion|subset)\s+of\b|\bi\s+(?:can(?:not|[’']t)|do\s+not|don[’']t|have\s+no)\b|\bi[’']m\s+not\b/iu;
|
|
8
9
|
const UNSOLICITED_PIVOT = /\b(?:what would you like|what are you in the mood for|would you like me to|do you want me to|want me to|recommend we add)\b/iu;
|
|
9
10
|
const TITLE_LIST_GLUE = new Set(["and", "are", "catalog", "film", "films", "from", "have", "here", "in", "is", "library", "movie", "movies", "on", "shelf", "show", "shows", "the", "these", "titles", "we"]);
|
|
10
11
|
const COUNT_ANSWER_GLUE = new Set(["and", "are", "currently", "episodes", "film", "films", "have", "in", "items", "library", "movie", "movies", "on", "shelf", "show", "shows", "the", "there", "tv", "we"]);
|
|
@@ -91,6 +92,23 @@ function parseCatalogEvidence(result, args) {
|
|
|
91
92
|
function sentenceCount(answer) {
|
|
92
93
|
return answer.split(/[.!?]+(?:\s+|$)/u).filter((part) => part.trim()).length;
|
|
93
94
|
}
|
|
95
|
+
function hasGroundedFirstPersonVisibility(answer, formattedCount) {
|
|
96
|
+
if (NEGATED_MEDIA_ACCESS.test(answer) || answer.split(formattedCount).length - 1 !== 1)
|
|
97
|
+
return false;
|
|
98
|
+
const escapedCount = formattedCount.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
99
|
+
const mediaNoun = "(?:titles?|movies?|films?|shows?|episodes?|items?)";
|
|
100
|
+
const countedObject = `(?:all\\s+|about\\s+|around\\s+|exactly\\s+|roughly\\s+)?${escapedCount}\\s+${mediaNoun}\\b`;
|
|
101
|
+
const contextObject = `(?:it|(?:(?:the|our)\\s+)?(?:full\\s+)?(?:library|shelf))(?:\\s+(?:now|again))?(?=\\s*(?:[.!?;,:—-]|$))`;
|
|
102
|
+
const accessObject = `(?:${contextObject}|${countedObject})`;
|
|
103
|
+
const firstPersonAccess = [
|
|
104
|
+
new RegExp(`\\bi\\s+(?:(?:can\\s+(?:(?:absolutely|clearly|currently|definitely|now|plainly|readily)\\s+)?|(?:currently|now)\\s+)?(?:see|browse|access|view))\\s+${accessObject}`, "iu"),
|
|
105
|
+
new RegExp(`\\bi(?:\\s+have|[’']ve\\s+got)\\s+(?:(?:full\\s+)?access\\s+to\\s+${accessObject}|${countedObject})`, "iu"),
|
|
106
|
+
new RegExp(`\\bi(?:\\s+am|[’']m)\\s+(?:(?:currently|now)\\s+)?(?:looking\\s+at|able\\s+to\\s+(?:see|browse|access|view))\\s+${accessObject}`, "iu"),
|
|
107
|
+
/\b(?:(?:the|our)\s+)?(?:full\s+)?(?:library|shelf)\s+is\s+(?:fully\s+)?(?:visible|accessible)\s+to\s+me\b/iu,
|
|
108
|
+
];
|
|
109
|
+
const groundedCount = new RegExp(`\\b${escapedCount}\\s+${mediaNoun}\\b`, "iu");
|
|
110
|
+
return firstPersonAccess.some((pattern) => pattern.test(answer)) && groundedCount.test(answer);
|
|
111
|
+
}
|
|
94
112
|
function commonAnswerRejection(answer, technicalDetailRequested) {
|
|
95
113
|
if (/\b(?:just|only|actually|merely)\s+(?:a\s+)?(?:bot|ai|assistant)\b|\bi\s+(?:can[’']?t|cannot|don[’']?t|do not)\s+(?:actually\s+)?watch\b|\bif i had to pick\b/iu.test(answer)) {
|
|
96
114
|
return "Answer from the catalog evidence with a confident, personable choice; do not retreat into an AI or 'I cannot watch' disclaimer.";
|
|
@@ -175,14 +193,17 @@ function sanctuaryMediaCatalogRequiredToolCalls(request, advertisedToolNames) {
|
|
|
175
193
|
return "Answer library visibility in no more than two short sentences and 240 characters.";
|
|
176
194
|
if (!/^yes\b/iu.test(answer.trim()))
|
|
177
195
|
return "Lead with a direct yes when the current catalog read succeeds.";
|
|
196
|
+
if (latest) {
|
|
197
|
+
const formattedCount = new Intl.NumberFormat("en-US").format(latest.totalItems);
|
|
198
|
+
if (!hasGroundedFirstPersonVisibility(answer, formattedCount))
|
|
199
|
+
return `Reply in one or two sentences. A valid shape is: Yes—I can see the library now. It contains ${formattedCount} titles.`;
|
|
200
|
+
}
|
|
178
201
|
if (/\b(?:titles? like|such as|including)\b/iu.test(answer))
|
|
179
202
|
return "Do not sample titles for a visibility question; answer whether the shelf is visible and give the current count.";
|
|
180
203
|
if ([...answer.matchAll(/\byes\b/giu)].length > 1)
|
|
181
204
|
return "Give one answer once; do not repeat the yes or restate the response.";
|
|
182
205
|
if (answer.includes("?"))
|
|
183
206
|
return "Answer the visibility question and stop without asking a new question.";
|
|
184
|
-
if (latest && !answer.includes(new Intl.NumberFormat("en-US").format(latest.totalItems)))
|
|
185
|
-
return "Include the current shelf count from the successful catalog evidence.";
|
|
186
207
|
}
|
|
187
208
|
if (kind === "title_lookup" && latest?.query) {
|
|
188
209
|
const exactMatch = latest.titles.some((title) => (0, sanctuary_media_optimization_1.normalizeSanctuaryMediaText)(title) === requestedTitleQuery);
|
|
@@ -187,20 +187,27 @@ function hasAcceptedOutwardSessionAck(events, assistantIndex, toolCallId, toolNa
|
|
|
187
187
|
}
|
|
188
188
|
return false;
|
|
189
189
|
}
|
|
190
|
-
function newOutwardCoordinates(events, existingEventIds) {
|
|
190
|
+
function newOutwardCoordinates(events, existingEventIds, afterEventId) {
|
|
191
|
+
const boundaryIndex = events.findIndex((event) => event.id === afterEventId);
|
|
192
|
+
if (boundaryIndex < 0)
|
|
193
|
+
return [];
|
|
191
194
|
return events.flatMap((event, eventIndex) => {
|
|
192
|
-
if (existingEventIds.has(event.id) || event.role !== "assistant")
|
|
195
|
+
if (eventIndex <= boundaryIndex || existingEventIds.has(event.id) || event.role !== "assistant" || event.provenance?.captureKind === "synthetic")
|
|
193
196
|
return [];
|
|
194
|
-
const outwardTools = event.toolCalls.
|
|
197
|
+
const outwardTools = event.toolCalls.flatMap((call) => {
|
|
195
198
|
if (call.function.name !== "speak" && call.function.name !== "settle")
|
|
196
|
-
return
|
|
197
|
-
|
|
199
|
+
return [];
|
|
200
|
+
if (!hasAcceptedOutwardSessionAck(events, eventIndex, call.id, call.function.name))
|
|
201
|
+
return [];
|
|
202
|
+
const text = stripThinkBlocks(parseToolStringArg(call, call.function.name, call.function.name === "speak" ? "message" : "answer") ?? "");
|
|
203
|
+
return text ? [{ kind: call.function.name, eventId: event.id, text }] : [];
|
|
198
204
|
});
|
|
199
205
|
if (outwardTools.length > 0)
|
|
200
|
-
return outwardTools
|
|
206
|
+
return outwardTools;
|
|
201
207
|
if (event.toolCalls.length > 0)
|
|
202
208
|
return [];
|
|
203
|
-
|
|
209
|
+
const text = stripThinkBlocks(typeof event.content === "string" ? event.content : "");
|
|
210
|
+
return text ? [{ kind: "text", eventId: event.id, text }] : [];
|
|
204
211
|
});
|
|
205
212
|
}
|
|
206
213
|
function newestPlainAssistantText(messages) {
|
|
@@ -210,12 +217,30 @@ function newestPlainAssistantText(messages) {
|
|
|
210
217
|
&& candidate.content.trim().length > 0);
|
|
211
218
|
return message ? assistantContentText(message.content) : null;
|
|
212
219
|
}
|
|
213
|
-
function causalSessionEventIds(events, existingEventIds, attempts) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return attempts.flatMap((attempt
|
|
220
|
+
function causalSessionEventIds(events, existingEventIds, attempts, afterEventId) {
|
|
221
|
+
if (!afterEventId)
|
|
222
|
+
return attempts.flatMap((attempt) => attempt.delivered ? [null] : []);
|
|
223
|
+
const coordinates = newOutwardCoordinates(events, existingEventIds, afterEventId);
|
|
224
|
+
let nextCoordinate = 0;
|
|
225
|
+
return attempts.flatMap((attempt) => {
|
|
226
|
+
if (!attempt.delivered)
|
|
227
|
+
return [];
|
|
228
|
+
const coordinateIndex = coordinates.findIndex((coordinate, index) => index >= nextCoordinate && coordinate.kind === attempt.kind && coordinate.text === attempt.text);
|
|
229
|
+
if (coordinateIndex < 0)
|
|
230
|
+
return [null];
|
|
231
|
+
nextCoordinate = coordinateIndex + 1;
|
|
232
|
+
return [coordinates[coordinateIndex].eventId];
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function currentIngressEventId(events, existingEventIds, userMessage, precommittedIngress, ingressRelations) {
|
|
236
|
+
if (precommittedIngress)
|
|
237
|
+
return precommittedIngress.eventId;
|
|
238
|
+
const reference = ingressRelations?.references[0];
|
|
239
|
+
return events.findLast((event) => (!existingEventIds.has(event.id)
|
|
240
|
+
&& event.role === "user"
|
|
241
|
+
&& event.content === userMessage
|
|
242
|
+
&& event.provenance?.captureKind !== "synthetic"
|
|
243
|
+
&& (!reference || event.relations?.references.includes(reference))))?.id;
|
|
219
244
|
}
|
|
220
245
|
function getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRootOverride) {
|
|
221
246
|
return path.join(agentRootOverride ?? (0, identity_1.getAgentRoot)(agentName), "state", "sessions", friendId, channel, `${(0, config_1.sanitizeKey)(sessionKey)}.json`);
|
|
@@ -329,7 +354,7 @@ async function runSenseTurn(options) {
|
|
|
329
354
|
if (!text)
|
|
330
355
|
return;
|
|
331
356
|
const delivery = { kind, text };
|
|
332
|
-
const attempt = { kind, delivered: false };
|
|
357
|
+
const attempt = { kind, text, delivered: false };
|
|
333
358
|
deliveryAttempts.push(attempt);
|
|
334
359
|
try {
|
|
335
360
|
await options.deliverySink?.onDelivery(delivery);
|
|
@@ -444,6 +469,7 @@ async function runSenseTurn(options) {
|
|
|
444
469
|
};
|
|
445
470
|
}
|
|
446
471
|
const persistedEvents = persistPromise ? await persistPromise : [];
|
|
472
|
+
const ingressEventId = currentIngressEventId(persistedEvents, existingEventIds, userMessage, options.precommittedIngress, options.ingressRelations);
|
|
447
473
|
const finalDeliveryKind = terminalDeliveryKind;
|
|
448
474
|
const acceptedTerminalOutcome = turnResult.turnOutcome === "settled" || turnResult.turnOutcome === "blocked";
|
|
449
475
|
const failoverText = turnResult.turnOutcome === "errored" ? turnResult.failoverMessage?.trim() : undefined;
|
|
@@ -463,7 +489,7 @@ async function runSenseTurn(options) {
|
|
|
463
489
|
else
|
|
464
490
|
pendingResponseText = "";
|
|
465
491
|
if (!hadPendingCallbackText && plainTerminalText)
|
|
466
|
-
recoveredTerminalEventId =
|
|
492
|
+
recoveredTerminalEventId = causalSessionEventIds(persistedEvents, existingEventIds, [{ kind: "text", text: stripThinkBlocks(plainTerminalText), delivered: true }], ingressEventId)[0] ?? undefined;
|
|
467
493
|
}
|
|
468
494
|
else if (turnResult.turnOutcome === "command") {
|
|
469
495
|
// Slash-command text is emitted directly by the pipeline and has no assistant event.
|
|
@@ -491,7 +517,7 @@ async function runSenseTurn(options) {
|
|
|
491
517
|
const recovered = extractOutwardSenseDeliveryText(postTurnSession.messages.slice(preTurnMessageCount));
|
|
492
518
|
finalResponse = recovered ?? emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
|
|
493
519
|
if (recovered)
|
|
494
|
-
responseCausalSessionEventId =
|
|
520
|
+
responseCausalSessionEventId = causalSessionEventIds(persistedEvents, existingEventIds, [{ kind: finalDeliveryKind, text: stripThinkBlocks(recovered), delivered: true }], ingressEventId)[0] ?? undefined;
|
|
495
521
|
}
|
|
496
522
|
else {
|
|
497
523
|
finalResponse = emptyFallback ?? (hadReasoningChunk ? "" : "(agent responded but response was empty)");
|
|
@@ -538,7 +564,7 @@ async function runSenseTurn(options) {
|
|
|
538
564
|
providerInvocationCount,
|
|
539
565
|
toolInvocationCount,
|
|
540
566
|
sessionPath: sessPath,
|
|
541
|
-
...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(persistedEvents, existingEventIds, deliveryAttempts) } : {}),
|
|
567
|
+
...(deliveries.length > 0 ? { causalSessionEventIds: causalSessionEventIds(persistedEvents, existingEventIds, deliveryAttempts, ingressEventId) } : {}),
|
|
542
568
|
...(responseCausalSessionEventId ? { responseCausalSessionEventId } : {}),
|
|
543
569
|
};
|
|
544
570
|
});
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.795",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.795",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|