@peopl-health/nexus 5.71.6 → 5.71.8

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.
@@ -77,7 +77,7 @@ function gradeEstimateSchema(description) {
77
77
  source: {
78
78
  type: 'string',
79
79
  enum: GRADE_SOURCES,
80
- description: 'Where this grade came from. Required whenever you set best_estimate. `proctcae_composite`: you are carrying over the number the PRO-CTCAE triage already computed — say this even when you agree with it. When what the patient tells you this turn contradicts that number (they can do today what the questionnaire said they could not), what they tell you now wins: grade it yourself as `agent_adjudicated`. `agent_adjudicated`: you read the grade_scale clauses yourself against what the patient said. `carried_forward`: the case\'s previous grade, unchanged, with no new evidence this turn.',
80
+ description: 'Where this grade came from. Required whenever you set best_estimate. `proctcae_composite`: you are carrying over the number the PRO-CTCAE triage already computed — say this even when you agree with it. When what the patient tells you this turn contradicts that number (they can do today what the questionnaire said they could not), what they tell you now wins: when the term has a grade_scale, grade it yourself as `agent_adjudicated`. `agent_adjudicated`: you read the grade_scale clauses yourself against what the patient said. `carried_forward`: the case\'s previous grade, unchanged, with no new evidence this turn.',
81
81
  },
82
82
  matched_grade: {
83
83
  type: ['integer', 'null'],
@@ -377,6 +377,7 @@ class BaseLLMProvider {
377
377
  'Si una contingencia activa ya cubre la preocupación, no la cruces con otro seguimiento. Si programas, primero usa schedulePatientReminder con reminder_type seguimiento y la misma utility_basis concreta; luego registra hold con el mismo next_check_at.',
378
378
  '',
379
379
  'Si escribes, que sea para ELLA: lo que te contó, lo que quedó abierto, lo que quieres saber ahora.',
380
+ 'Tu texto llega dentro de una plantilla que ya empieza con «Hola {su nombre},»: no saludes ni escribas su nombre, entra directo a lo que quieres decirle, breve (menos de 500 caracteres) y con un emoji como máximo. Es un contacto sobre su cuidado: no le sumes invitaciones a charlas, sesiones ni actividades.',
380
381
  'LÍMITE UTILITY: escribe sólo cuando puedas anclar el contacto en algo concreto que la paciente contó, preguntó o acordó, o en una interacción de cuidado todavía vigente. La inactividad por sí sola no basta. Si escribes, registra esa ancla en utility_basis; si sólo queda un saludo general para generar engagement, registra hold con hold_reason no_utility_anchor y utility_basis null.',
381
382
  'No repitas un mensaje que ya le enviaste, y no menciones automatizaciones, listas ni seguimientos programados.',
382
383
  );
@@ -70,7 +70,7 @@ function gradeAnchorNotice({ ctcaeTerm, grade, anchor }) {
70
70
  const named = leaned === 'self_care' ? 'self-care' : 'instrumental';
71
71
  if (rulesOutFunctionalPart(leaned, anchor)) {
72
72
  return `Grade ${grade} of ${ctcaeTerm} can only stand on the non-functional part of its clause: the patient is not limited in ${named} ADL. `
73
- + '`reasoning` must name that criterion; if it is not met, record the grade below.';
73
+ + '`reasoning` must name that criterion; if it is not met, record the grade below and name in `improvement_basis` what rules this grade out.';
74
74
  }
75
75
  return `Grade ${grade} of ${ctcaeTerm} was recorded without the ${named} ADL limitation its clause mentions. `
76
76
  + 'That is fine when another part of the clause is met or the whole picture makes it the most likely grade — `reasoning` must name which. '
@@ -52,9 +52,9 @@ const GUARDS = [
52
52
  }),
53
53
  ];
54
54
 
55
- function gradeClaimError({ ctcaeTerm, grade, stated = null, previous = null, restatesPriorEvidence = true, continuesEpisode = false, recordsOngoingRead = false, trace = null }) {
55
+ function gradeClaimError({ ctcaeTerm, grade, stated = null, previous = null, restatesPriorEvidence = true, recordsOngoingRead = false, trace = null }) {
56
56
  const resolved = grade || {};
57
- const claim = { ctcaeTerm, grade: resolved, stated: stated || resolved, previous, restatesPriorEvidence, continuesEpisode, recordsOngoingRead, trace };
57
+ const claim = { ctcaeTerm, grade: resolved, stated: stated || resolved, previous, restatesPriorEvidence, recordsOngoingRead, trace };
58
58
  for (const guard of GUARDS) {
59
59
  const error = guard(claim);
60
60
  if (error) return error;
@@ -121,7 +121,6 @@ function buildAssessment(condition, previous, args, runtime, restatesPriorEviden
121
121
  stated: parseGradeEstimate(args.grade_estimate),
122
122
  previous: previous.grade,
123
123
  restatesPriorEvidence,
124
- continuesEpisode,
125
124
  recordsOngoingRead: ONGOING_READ_EVENTS.includes(normalisedEvent(args)),
126
125
  trace: runtime?.trace || null,
127
126
  });
@@ -175,6 +174,10 @@ function requireCeilingGap(args, event) {
175
174
  return error ? fail(error) : null;
176
175
  }
177
176
 
177
+ function sameGradeAndAnchor(a, b) {
178
+ return !!a && !!b && a.bestEstimate === b.bestEstimate && a.functionalAnchor === b.functionalAnchor;
179
+ }
180
+
178
181
  function sameGrade(a, b) {
179
182
  return JSON.stringify(projectGradeRead(a)) === JSON.stringify(projectGradeRead(b));
180
183
  }
@@ -351,7 +354,9 @@ async function handler(args = {}, context = {}) {
351
354
 
352
355
  const { condition, latestAssessment } = result;
353
356
  const consistency = inputConsistency(args);
354
- const freshRead = ONGOING_READ_EVENTS.includes(event) && hasGradeSignal(args?.grade_estimate) && !isCarriedForward(args.grade_estimate);
357
+ // A hold of the same grade on the same anchor was already advised; repeating it re-asks the patient every turn.
358
+ const heldAsAdvised = event === 'add_evidence' && sameGradeAndAnchor(existing.latestAssessment?.grade, latestAssessment.grade);
359
+ const freshRead = ONGOING_READ_EVENTS.includes(event) && hasGradeSignal(args?.grade_estimate) && !isCarriedForward(args.grade_estimate) && !heldAsAdvised;
355
360
  const gradeNotice = freshRead ? gradeClaimNotice({ ctcaeTerm: condition.ctcaeTerm, grade: latestAssessment.grade }) : null;
356
361
  return JSON.stringify({
357
362
  success: true,
@@ -38,7 +38,7 @@ const { ProcessingPipeline } = require('../core/ProcessingPipeline');
38
38
  const { preProcessMessages, configureOpenAIProvider, AssistantProcessor, TURN_KIND } = require('../clinical');
39
39
  const { deliveryOutcome } = require('../clinical/AssistantProcessor');
40
40
  const { UNASSIGNED, alertOpsForClaimedMessage, coverIfUnassigned, coverUnansweredPatient } = require('../clinical/services/pausedSafetyNetService');
41
- const { buildOutreachCarrier } = require('../clinical/services/outreachCarrierService');
41
+ const { buildOutreachCarrier, validateAgentText } = require('../clinical/services/outreachCarrierService');
42
42
  const { routineOutreachCollisionReason, isWithinTriageProtectionWindow } = require('../clinical/services/outreachSweepService');
43
43
  const {
44
44
  readPendingReminderForPatient,
@@ -1037,6 +1037,14 @@ class NexusMessaging {
1037
1037
  }
1038
1038
  const accepted = Boolean(runResult.output) && delivery?.success === true && delivery?.deferred !== true;
1039
1039
  const delivered = ['delivered', 'read'].includes(String(delivery?.status || '').toLowerCase());
1040
+ let carrierRejection = null;
1041
+ if (dryRun && decision.decision === 'write') {
1042
+ try {
1043
+ validateAgentText(runResult.output, firstName);
1044
+ } catch (error) {
1045
+ carrierRejection = error.message;
1046
+ }
1047
+ }
1040
1048
  if (!dryRun && (decision.decision === 'write') !== accepted) {
1041
1049
  logger.warn('[processOutreach] decision and delivery disagree', {
1042
1050
  code, decision: decision.decision, accepted, status: delivery?.status || null,
@@ -1049,6 +1057,7 @@ class NexusMessaging {
1049
1057
  holdReason: decision.holdReason || null,
1050
1058
  nextCheckAt: decision.nextCheckAt || null,
1051
1059
  draft: dryRun ? (runResult.output || '') : null,
1060
+ carrierRejection,
1052
1061
  accepted,
1053
1062
  delivered,
1054
1063
  deferred: Boolean(delivery?.deferred),
package/lib/index.d.ts CHANGED
@@ -475,6 +475,8 @@ declare module '@peopl-health/nexus' {
475
475
  nextCheckAt: string | null;
476
476
  /** Drafted patient-facing text. Present only on a dry run. */
477
477
  draft?: string | null;
478
+ /** Why the out-of-window template would refuse the draft. Present only on a dry run. */
479
+ carrierRejection?: string | null;
478
480
  /** Whether the provider accepted the fixed-template send. This is not proof of delivery. */
479
481
  accepted?: boolean;
480
482
  /** Whether the provider status is already terminal delivered/read. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.71.6",
3
+ "version": "5.71.8",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",