@peopl-health/nexus 5.10.0-dev.1092 → 5.10.0-dev.1094

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.
@@ -79,14 +79,14 @@ const _DOWNSTREAM_SKILL = {
79
79
 
80
80
  const definition = {
81
81
  name: 'submitRoutingDecision',
82
- description: '**Does:** The terminal routing checkpoint — call ONCE per turn to commit the turn plan. Decomposes `trace` (the per-mention evaluation record) and `turn_plan` (the primary action + optional secondary/deferred) into a persisted RoutingDecision (assessment + disposition + provenance), sets the turn\'s outbound signals, and hands the agent the next skill to invoke.\n\n**Required inputs:** `trace.mentions_evaluated[]` — one entry per clinical mention: `case_id_opened_or_updated`, `clinical_reasoning`, `decision.action`, plus `safety_flag_disposition` when `safety_flags_received` is non-empty. `turn_plan.primary` — exactly one `{action, reason, case_id?}`; `turn_plan.secondary[]` — up to 2 `{action, compose_as}`; `turn_plan.deferred[]` — optional `{item?, defer_reason}`.\n\n**Enum coercion:** primary/secondary `action` and secondary `compose_as` are folded (case/accent/separator) and synonym-mapped to canonical values; each repair is disclosed via `auto_repairs`. A genuinely unknown action is rejected.\n\n**Returns:** `decision_id`, `downstream_skill_to_invoke` (emergency | accompaniment | symptom_management | patient_education | null, derived from `primary.action`), and `auto_repairs` (only when non-empty). Fails hard with `decision_already_committed_for_turn` if called twice in one turn.\n\n**Side effects:** persists the RoutingDecision; sets `branch_taken=ai_agent` on the turn trace.',
82
+ description: '**Does:** The terminal routing checkpoint — call ONCE per turn to commit the turn plan. Decomposes `trace` (the per-mention evaluation record) and `turn_plan` (the primary action + optional secondary/deferred) into a persisted RoutingDecision (assessment + disposition + provenance), sets the turn\'s outbound signals, and hands the agent the next skill to invoke.\n\n**Required inputs:** `trace.mentions_evaluated[]` — one entry per clinical mention: `clinical_reasoning`, `decision.action`, `case_id_opened_or_updated` (null when the mention opened no case), plus `safety_flag_disposition` (string or object) when `safety_flags_received` is non-empty. `turn_plan.primary` — exactly one `{action, reason, case_id?}`; `turn_plan.secondary[]` — up to 2 `{action, compose_as}`; `turn_plan.deferred[]` — optional `{item?, defer_reason}`.\n\n**Enum coercion:** primary/secondary `action` and secondary `compose_as` are folded (case/accent/separator) and synonym-mapped to canonical values; each repair is disclosed via `auto_repairs`. A genuinely unknown action is rejected.\n\n**Returns:** `decision_id`, `downstream_skill_to_invoke` (emergency | accompaniment | symptom_management | patient_education | null, derived from `primary.action`), and `auto_repairs` (only when non-empty). Fails hard with `decision_already_committed_for_turn` if called twice in one turn.\n\n**Side effects:** persists the RoutingDecision; sets `branch_taken=ai_agent` on the turn trace.',
83
83
  strict: false,
84
84
  parameters: {
85
85
  type: 'object',
86
86
  properties: {
87
87
  trace: {
88
88
  type: 'object',
89
- description: 'Per-mention evaluation record. `mentions_evaluated[]`: each `{case_id_opened_or_updated, clinical_reasoning, decision:{action}, safety_flags_received?, safety_flag_disposition?}`. A safety_flag_disposition is required for any mention with a non-empty safety_flags_received.',
89
+ description: 'Per-mention evaluation record. `mentions_evaluated[]`: each `{case_id_opened_or_updated?, clinical_reasoning, decision:{action}, safety_flags_received?, safety_flag_disposition?}`. `case_id_opened_or_updated` is null/omitted when the mention opened or updated no case (e.g. a medication, appointment, allergy or emotion mention). `safety_flag_disposition` is required for any mention with a non-empty `safety_flags_received`, and may be a short string (e.g. `acknowledged`, `escalated`) or an object.',
90
90
  },
91
91
  turn_plan: {
92
92
  type: 'object',
@@ -184,8 +184,8 @@ function validateTrace(trace) {
184
184
  for (let i = 0; i < mentionsRaw.length; i += 1) {
185
185
  const mention = mentionsRaw[i];
186
186
  if (!mention || typeof mention !== 'object' || Array.isArray(mention)) return { error: fail('trace.mentions_evaluated[*] must be objects', { index: i }) };
187
- const caseId = (typeof mention.case_id_opened_or_updated === 'string' ? mention.case_id_opened_or_updated.trim() : '');
188
- if (!caseId) return { error: fail(`mentions_evaluated[${i}].case_id_opened_or_updated is required`, { index: i }) };
187
+ // A non-symptom mention (medication, appointment, allergy, emotion) opens no case, so case_id is optional; null records the absence.
188
+ const caseId = (typeof mention.case_id_opened_or_updated === 'string' ? mention.case_id_opened_or_updated.trim() : '') || null;
189
189
  const reasoning = typeof mention.clinical_reasoning === 'string' ? mention.clinical_reasoning.trim() : '';
190
190
  if (!reasoning) return { error: fail(`mentions_evaluated[${i}].clinical_reasoning is required`, { index: i }) };
191
191
  let decision = mention.decision;
@@ -200,8 +200,14 @@ function validateTrace(trace) {
200
200
  return { error: fail(`mentions_evaluated[${i}].safety_flags_received must be a list`, { index: i }) };
201
201
  }
202
202
  if (Array.isArray(flags) && flags.length) {
203
- const disposition = mention.safety_flag_disposition;
204
- if (!disposition || typeof disposition !== 'object' || Array.isArray(disposition)) {
203
+ // Accept the model's most common shape — a plain string ('acknowledged', 'escalated') — as well as an object.
204
+ const disposition = typeof mention.safety_flag_disposition === 'string'
205
+ ? mention.safety_flag_disposition.trim()
206
+ : mention.safety_flag_disposition;
207
+ const dispositionGiven = typeof disposition === 'string'
208
+ ? disposition.length > 0
209
+ : (disposition && typeof disposition === 'object' && !Array.isArray(disposition));
210
+ if (!dispositionGiven) {
205
211
  return { error: fail(`mentions_evaluated[${i}].safety_flag_disposition is required when safety_flags_received is non-empty`, { index: i, mention_id: mention.mention_id }) };
206
212
  }
207
213
  }
@@ -118,14 +118,21 @@ class ClinicalImpression {
118
118
  status: 'completed',
119
119
  subject: patientReference(assessment.patientId),
120
120
  effectiveDateTime: toIso(assessment.performedAt),
121
- finding: assessment.findings.map((finding) => ({
122
- item: reference('Condition', finding.caseId),
123
- extension: [
124
- extension('routing-finding-case-id', { valueString: finding.caseId }),
121
+ finding: assessment.findings.map((finding) => {
122
+ const findingExts = [];
123
+ const projected = {};
124
+ // A caseless mention (caseId null) has no case to point at: no Condition item and no case-id extension, so absence stays absence in the FHIR.
125
+ if (finding.caseId) {
126
+ projected.item = reference('Condition', finding.caseId);
127
+ findingExts.push(extension('routing-finding-case-id', { valueString: finding.caseId }));
128
+ }
129
+ findingExts.push(
125
130
  extension('routing-finding-reasoning', { valueString: finding.clinicalReasoning }),
126
131
  extension('routing-finding-action', { valueString: finding.decisionAction }),
127
- ],
128
- })),
132
+ );
133
+ projected.extension = findingExts;
134
+ return projected;
135
+ }),
129
136
  extension: extensions,
130
137
  };
131
138
  if (assessment.summary) payload.summary = assessment.summary;
@@ -5,7 +5,8 @@ const { BaseDto } = require('./BaseDto');
5
5
  const dateTime = z.iso.datetime({ offset: true });
6
6
 
7
7
  const mentionFindingSchema = z.strictObject({
8
- caseId: z.string(),
8
+ // null when the mention opened/updated no case (medication, appointment, allergy, emotion); absence is stored as absence, not a sentinel.
9
+ caseId: z.string().nullable().default(null),
9
10
  clinicalReasoning: z.string(),
10
11
  decisionAction: z.string(),
11
12
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.10.0-dev.1092",
3
+ "version": "5.10.0-dev.1094",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",