@peopl-health/nexus 5.10.0-dev.1092 → 5.10.0-dev.1093
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: `
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
});
|