@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.
@@ -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 read|inventory|endpoint|backend|data shape|pars(?:e|ed|ing)|json|sanctuary_search_media_catalog)\b/iu;
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.filter((call) => {
197
+ const outwardTools = event.toolCalls.flatMap((call) => {
195
198
  if (call.function.name !== "speak" && call.function.name !== "settle")
196
- return false;
197
- return hasAcceptedOutwardSessionAck(events, eventIndex, call.id, call.function.name);
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.map((call) => ({ kind: call.function.name, eventId: event.id }));
206
+ return outwardTools;
201
207
  if (event.toolCalls.length > 0)
202
208
  return [];
203
- return typeof event.content === "string" && event.content.trim() ? [{ kind: "text", eventId: event.id }] : [];
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
- const coordinates = newOutwardCoordinates(events, existingEventIds);
215
- const aligned = coordinates.length === attempts.length && coordinates.every((coordinate, index) => coordinate.kind === attempts[index].kind)
216
- ? coordinates.map((coordinate) => coordinate.eventId)
217
- : attempts.map(() => null);
218
- return attempts.flatMap((attempt, index) => attempt.delivered ? [aligned[index] ?? null] : []);
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 = newOutwardCoordinates(persistedEvents, existingEventIds).at(-1)?.eventId;
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 = newOutwardCoordinates(persistedEvents, existingEventIds).at(-1)?.eventId;
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
  });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.793",
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.793",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.793",
3
+ "version": "0.1.0-alpha.795",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },