@peopl-health/nexus 5.9.0-dev.921 → 6.0.0-dev.611

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.
Files changed (126) hide show
  1. package/lib/clinical/AssistantProcessor.js +5 -34
  2. package/lib/clinical/config/divergenceConfig.js +1 -18
  3. package/lib/clinical/config/llmConfig.js +1 -9
  4. package/lib/clinical/context/ToolRuntimeContext.js +2 -1
  5. package/lib/clinical/helpers/divergenceHelper.js +0 -12
  6. package/lib/clinical/index.js +0 -2
  7. package/lib/clinical/memory/DefaultMemoryManager.js +9 -19
  8. package/lib/clinical/models/turnTraceModel.js +5 -18
  9. package/lib/clinical/providers/AnthropicProvider.js +29 -291
  10. package/lib/clinical/providers/BaseLLMProvider.js +24 -113
  11. package/lib/clinical/providers/GatewayProvider.js +3 -3
  12. package/lib/clinical/providers/OpenAIResponsesProvider.js +4 -46
  13. package/lib/clinical/services/divergenceService.js +5 -22
  14. package/lib/clinical/services/promptComposerService.js +15 -46
  15. package/lib/clinical/services/shadowMessageService.js +12 -28
  16. package/lib/clinical/services/shadowService.js +3 -12
  17. package/lib/clinical/stores/CtcaeCatalog.js +0 -20
  18. package/lib/config/airtableConfig.js +0 -3
  19. package/lib/core/BatchingManager.js +3 -10
  20. package/lib/core/NexusMessaging.js +13 -31
  21. package/lib/eval/EvalProvider.js +2 -3
  22. package/lib/fhir/config/fhirConfig.js +3 -34
  23. package/lib/fhir/constants/projectionSlugs.js +1 -41
  24. package/lib/fhir/helpers/elementHelper.js +0 -21
  25. package/lib/fhir/helpers/fhirHelper.js +2 -5
  26. package/lib/fhir/index.js +0 -24
  27. package/lib/fhir/models/envelopeModel.js +0 -5
  28. package/lib/fhir/projections/registerProjectors.js +0 -16
  29. package/lib/fhir/resources/Condition.js +2 -107
  30. package/lib/fhir/resources/Observation.js +1 -73
  31. package/lib/fhir/resources/Procedure.js +2 -34
  32. package/lib/fhir/resources/Provenance.js +2 -93
  33. package/lib/fhir/services/fhirService.js +2 -2
  34. package/lib/helpers/twilioHelper.js +1 -9
  35. package/lib/index.d.ts +0 -16
  36. package/lib/index.js +0 -9
  37. package/lib/models/messageModel.js +0 -6
  38. package/lib/routes/index.js +1 -21
  39. package/lib/services/airtableService.js +1 -2
  40. package/lib/shared/dtos/ContingencySafetyNet.js +3 -10
  41. package/lib/shared/dtos/DispatchedEscalation.js +8 -3
  42. package/lib/shared/dtos/Reminder.js +2 -2
  43. package/lib/storage/MongoStorage.js +0 -2
  44. package/package.json +1 -1
  45. package/lib/clinical/config/cohortConfig.js +0 -68
  46. package/lib/clinical/config/composerConfig.js +0 -42
  47. package/lib/clinical/config/subAgentsConfig.js +0 -44
  48. package/lib/clinical/flags/bridgeConfig.js +0 -14
  49. package/lib/clinical/flags/composerFlags.js +0 -14
  50. package/lib/clinical/helpers/clinicalFactsHelper.js +0 -108
  51. package/lib/clinical/helpers/clinicalMentionHelper.js +0 -68
  52. package/lib/clinical/helpers/cohortHelper.js +0 -22
  53. package/lib/clinical/helpers/gradeEstimateHelper.js +0 -29
  54. package/lib/clinical/helpers/resultsHelper.js +0 -34
  55. package/lib/clinical/helpers/safetyFlagsHelper.js +0 -10
  56. package/lib/clinical/services/bridgeService.js +0 -55
  57. package/lib/clinical/services/clinicalAirtableService.js +0 -162
  58. package/lib/clinical/services/clinicalExtractionService.js +0 -112
  59. package/lib/clinical/services/composerService.js +0 -59
  60. package/lib/clinical/services/dispatcherService.js +0 -137
  61. package/lib/clinical/services/reminderDispatchService.js +0 -36
  62. package/lib/clinical/services/skillService.js +0 -44
  63. package/lib/clinical/tools/analyzeSymptomPatternsTool.js +0 -279
  64. package/lib/clinical/tools/deliverPatientMessageTool.js +0 -202
  65. package/lib/clinical/tools/extractClinicalInfoTool.js +0 -167
  66. package/lib/clinical/tools/getActiveSymptomLandscapeTool.js +0 -141
  67. package/lib/clinical/tools/getPatientHistoryTool.js +0 -235
  68. package/lib/clinical/tools/getPatientRiskProfileTool.js +0 -71
  69. package/lib/clinical/tools/getRouterContextBundleTool.js +0 -93
  70. package/lib/clinical/tools/lookupCtcaeEntryTool.js +0 -35
  71. package/lib/clinical/tools/openClusterHypothesisTool.js +0 -160
  72. package/lib/clinical/tools/openConditionTool.js +0 -136
  73. package/lib/clinical/tools/recordClinicalImpressionTool.js +0 -251
  74. package/lib/clinical/tools/recordInterventionTool.js +0 -140
  75. package/lib/clinical/tools/registerClinicalTools.js +0 -39
  76. package/lib/clinical/tools/reportCrisisTool.js +0 -211
  77. package/lib/clinical/tools/reportMedicalEscalationTool.js +0 -187
  78. package/lib/clinical/tools/reportResultsReceivedTool.js +0 -153
  79. package/lib/clinical/tools/reportUnresolvedRequestv2Tool.js +0 -177
  80. package/lib/clinical/tools/schedulePatientReminderTool.js +0 -149
  81. package/lib/clinical/tools/setContingencyPlanTool.js +0 -176
  82. package/lib/clinical/tools/skillTool.js +0 -67
  83. package/lib/clinical/tools/submitRoutingDecisionTool.js +0 -291
  84. package/lib/clinical/tools/updateClusterHypothesisTool.js +0 -159
  85. package/lib/controllers/cohortController.js +0 -16
  86. package/lib/controllers/traceController.js +0 -26
  87. package/lib/core/workflowRunner.js +0 -35
  88. package/lib/fhir/config/fhirTablesConfig.js +0 -9
  89. package/lib/fhir/constants/severityPriority.js +0 -8
  90. package/lib/fhir/helpers/compositeHelper.js +0 -88
  91. package/lib/fhir/helpers/fhirReadHelper.js +0 -42
  92. package/lib/fhir/helpers/terminologyHelper.js +0 -260
  93. package/lib/fhir/projections/clusterProjection.js +0 -24
  94. package/lib/fhir/projections/contingencyProjection.js +0 -36
  95. package/lib/fhir/projections/dispatchedEscalationProjection.js +0 -25
  96. package/lib/fhir/projections/interventionProjection.js +0 -42
  97. package/lib/fhir/projections/proctcaeProjection.js +0 -179
  98. package/lib/fhir/projections/reminderProjection.js +0 -20
  99. package/lib/fhir/projections/riskProjection.js +0 -16
  100. package/lib/fhir/projections/routingProjection.js +0 -48
  101. package/lib/fhir/projections/symptomCaseProjection.js +0 -65
  102. package/lib/fhir/resources/CarePlan.js +0 -93
  103. package/lib/fhir/resources/ClinicalImpression.js +0 -118
  104. package/lib/fhir/resources/Communication.js +0 -65
  105. package/lib/fhir/resources/CommunicationRequest.js +0 -182
  106. package/lib/fhir/resources/Device.js +0 -24
  107. package/lib/fhir/resources/Patient.js +0 -18
  108. package/lib/fhir/resources/Questionnaire.js +0 -23
  109. package/lib/fhir/resources/QuestionnaireResponse.js +0 -29
  110. package/lib/fhir/resources/RequestOrchestration.js +0 -46
  111. package/lib/fhir/resources/RiskAssessment.js +0 -99
  112. package/lib/fhir/resources/Task.js +0 -106
  113. package/lib/fhir/resources/interventionExtensions.js +0 -28
  114. package/lib/fhir/services/clusterService.js +0 -77
  115. package/lib/fhir/services/contingencyService.js +0 -117
  116. package/lib/fhir/services/interventionService.js +0 -149
  117. package/lib/fhir/services/resultObservationService.js +0 -52
  118. package/lib/fhir/services/riskService.js +0 -101
  119. package/lib/fhir/services/routingService.js +0 -147
  120. package/lib/fhir/services/snapshotService.js +0 -28
  121. package/lib/fhir/services/symptomCaseService.js +0 -139
  122. package/lib/fhir/services/terminologyService.js +0 -58
  123. package/lib/fhir/services/triageService.js +0 -57
  124. package/lib/fhir/utils/normalizeUtils.js +0 -33
  125. package/lib/shared/dtos/GradedSymptom.js +0 -23
  126. package/lib/shared/dtos/TriageResponse.js +0 -35
@@ -1,136 +0,0 @@
1
- const crypto = require('node:crypto');
2
-
3
- const { readSymptomCases, storeSymptomCase } = require('../../fhir');
4
- const { ManagedSymptom } = require('../../shared/dtos/ManagedSymptom');
5
- const { GRADE_MIN, GRADE_MAX, parseGradeEstimate } = require('../helpers/gradeEstimateHelper');
6
-
7
- const definition = {
8
- name: 'openCondition',
9
- description: '**Does:** Opens a NEW agent-tracked symptom case (Condition + initial ClinicalImpression) for a symptom term with no open case — the longitudinal expediente that groups 1..N episodes of the same `ctcae_term`.\n\n**Required inputs:** `ctcae_term` (English snake_case catalog key — e.g. `fever`, `pain`, `dyspnea`; use `other` only when no catalog term fits), `episode_id` (the intake episode this case wraps), `mention_id` (the intake mention that opened the case), `verbatim_quote` (exact substring of the patient message). Optional `temporality`, `grade_estimate` (`{best_estimate, confidence, possible_range, reasoning}`).\n\n**When to call:** when the turn surfaces a symptom with no open case on this `ctcae_term`. Use `recordClinicalImpression` if a case is already open — do NOT call `openCondition` twice for the same symptom.\n\n**When NOT to call:** for a new episode or subsequent assessment of an EXISTING case — use `recordClinicalImpression`.\n\n**Returns:** `case_id`, `action_taken` (`opened`), `status`, `ctcae_term`, `episode_ids`, `current_episode_id`, `grade_estimate`, `temporality`, `trajectory`. Fails hard with `case_already_open_for_term` + `existing_case_id` when an open case already exists for the term — recover via `recordClinicalImpression`.\n\n**Side effects:** writes a FHIR `Condition` + head `ClinicalImpression` + `Provenance`.',
10
- strict: false,
11
- parameters: {
12
- type: 'object',
13
- properties: {
14
- ctcae_term: {
15
- type: 'string',
16
- description: 'English snake_case key from the CTCAE catalog (e.g. \'fever\', \'pain\', \'dyspnea\'). Use \'other\' only when no catalog term fits.',
17
- },
18
- episode_id: {
19
- type: 'string',
20
- description: 'The intake episode id this case wraps.',
21
- },
22
- mention_id: {
23
- type: 'string',
24
- description: 'The intake mention id that opened the case.',
25
- },
26
- verbatim_quote: {
27
- type: 'string',
28
- description: 'Exact substring of the patient\'s message.',
29
- },
30
- temporality: {
31
- type: 'string',
32
- description: 'Optional. Onset/course descriptor (e.g. \'ongoing\', \'acute\', \'progressive\').',
33
- },
34
- grade_estimate: {
35
- type: 'object',
36
- description: 'Optional. `{best_estimate: int 1-5|null, confidence: low|moderate|high, possible_range: [low, high], reasoning: str}`.',
37
- },
38
- },
39
- required: ['ctcae_term', 'episode_id', 'mention_id', 'verbatim_quote'],
40
- },
41
- };
42
-
43
- const uid = (prefix) => `${prefix}_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`;
44
-
45
- async function handler(args = {}, context = {}) {
46
- try {
47
- const runtime = context?.toolRuntimeContext || null;
48
- if (!runtime?.turnId || !runtime?.patientCode) {
49
- return JSON.stringify({ success: false, error: 'openCondition requires an active turn context (turnId, patientCode).', data: {} });
50
- }
51
- const asString = (value) => (typeof value === 'string' ? value : '');
52
- const ctcaeTerm = asString(args?.ctcae_term).trim().toLowerCase();
53
- const episodeId = asString(args?.episode_id).trim();
54
- const mentionId = asString(args?.mention_id).trim();
55
- const verbatim = asString(args?.verbatim_quote).trim();
56
-
57
- const missing = [['ctcae_term', ctcaeTerm], ['episode_id', episodeId], ['mention_id', mentionId], ['verbatim_quote', verbatim]]
58
- .filter(([, value]) => !value).map(([name]) => name);
59
- if (missing.length) {
60
- return JSON.stringify({ success: false, error: `missing required field(s): ${missing.join(', ')}`, data: {} });
61
- }
62
-
63
- const patientId = runtime.patientCode;
64
- const casesForTerm = await readSymptomCases({ patientId, ctcaeTerm });
65
- const openCase = casesForTerm.find((symptomCase) => ['characterizing', 'monitoring'].includes(symptomCase.condition.clinicalStatus));
66
- if (openCase) {
67
- return JSON.stringify({
68
- success: false,
69
- error: 'case_already_open_for_term',
70
- data: {
71
- existing_case_id: openCase.condition.conditionId,
72
- existing_status: openCase.condition.clinicalStatus,
73
- hint: 'call recordClinicalImpression with event=\'add_evidence\' or \'new_episode\' to attach to the existing case',
74
- },
75
- });
76
- }
77
-
78
- const now = new Date().toISOString();
79
- const caseId = uid('case');
80
- const temporality = typeof args?.temporality === 'string' ? args.temporality : null;
81
- const grade = parseGradeEstimate(args?.grade_estimate);
82
- if (grade.error) {
83
- return JSON.stringify({ success: false, error: 'grade_out_of_range', data: { allowed_range: [GRADE_MIN, GRADE_MAX] } });
84
- }
85
- const managedSymptom = new ManagedSymptom({
86
- condition: {
87
- conditionId: caseId,
88
- patientId,
89
- ctcaeTerm,
90
- clinicalStatus: 'characterizing',
91
- onsetAt: now,
92
- temporality,
93
- evidenceEpisodeIds: [episodeId],
94
- currentEpisodeId: episodeId,
95
- evidenceMentionIds: [mentionId],
96
- },
97
- latestAssessment: {
98
- assessmentId: uid('assessment'),
99
- conditionId: caseId,
100
- patientId,
101
- performedAt: now,
102
- previousId: null,
103
- grade,
104
- trajectory: null,
105
- },
106
- });
107
-
108
- await storeSymptomCase({ patientId, managedSymptom });
109
- const { condition, latestAssessment } = managedSymptom;
110
- return JSON.stringify({
111
- success: true,
112
- data: {
113
- case_id: condition.conditionId,
114
- action_taken: 'opened',
115
- status: condition.clinicalStatus,
116
- ctcae_term: condition.ctcaeTerm,
117
- episode_ids: [...condition.evidenceEpisodeIds],
118
- current_episode_id: condition.currentEpisodeId,
119
- grade_estimate: {
120
- best_estimate: latestAssessment.grade.bestEstimate,
121
- confidence: latestAssessment.grade.confidence,
122
- possible_range: latestAssessment.grade.possibleRange,
123
- },
124
- temporality: condition.temporality,
125
- trajectory: { direction: latestAssessment.trajectory || 'unknown' },
126
- },
127
- });
128
- } catch (err) {
129
- return JSON.stringify({ success: false, error: err?.message || 'openCondition failed', data: {} });
130
- }
131
- }
132
-
133
- module.exports = {
134
- definition,
135
- handler,
136
- };
@@ -1,251 +0,0 @@
1
- const crypto = require('node:crypto');
2
-
3
- const { readSymptomCases, storeSymptomCase } = require('../../fhir');
4
- const { ManagedSymptom } = require('../../shared/dtos/ManagedSymptom');
5
- const { GRADE_MIN, GRADE_MAX, parseGradeEstimate } = require('../helpers/gradeEstimateHelper');
6
-
7
- const definition = {
8
- name: 'recordClinicalImpression',
9
- description: '**Does:** Evolves an existing symptom case by `event`. `add_evidence`: new mention on the current episode (requires `characterizing` + a grade or trajectory). `new_episode`: a new episode for the same `ctcae_term` — attaches it and returns the case to `characterizing`. `resolve_episode`: the current episode resolved — case goes to `monitoring`. `close`: terminal — status `closed`, requires `close_reason`.\n\n**Required inputs:** `case_id` (from `openCondition` or `getActiveSymptomLandscape`), `event`. Per-event: `add_evidence`/`new_episode` require `mention_id` plus at least one of `grade_estimate` or `trajectory_direction`; `new_episode` also requires `episode_id`; `close` requires `close_reason`.\n\n**Returns:** `case_id`, `action_taken`, `status`, `ctcae_term`, `episode_ids`, `current_episode_id`, `mention_ids`, `grade_estimate`, `temporality`, `trajectory`, `decision_posture`, `close_reason`, `abatement_at`. Fails hard on status guards (e.g. `add_evidence` on a non-`characterizing` case), duplicate `mention_id`, or an already-attached `episode_id`.\n\n**Side effects:** appends a FHIR `ClinicalImpression` to the case chain and repoints the head; updates the `Condition` lifecycle.',
10
- strict: false,
11
- parameters: {
12
- type: 'object',
13
- properties: {
14
- case_id: {
15
- type: 'string',
16
- description: 'The case_id returned by openCondition (or from getActiveSymptomLandscape.active_cases[].case_id).',
17
- },
18
- event: {
19
- type: 'string',
20
- enum: ['add_evidence', 'new_episode', 'resolve_episode', 'close'],
21
- description: '`add_evidence`: more info on the current episode. `new_episode`: a new episode for the same term. `resolve_episode`: current episode resolved (case → monitoring). `close`: terminal (requires close_reason).',
22
- },
23
- mention_id: {
24
- type: 'string',
25
- description: 'The new intake mention id. Required for add_evidence and new_episode; optional audit trace for resolve_episode.',
26
- },
27
- episode_id: {
28
- type: 'string',
29
- description: 'The new intake episode id. Required for new_episode.',
30
- },
31
- grade_estimate: {
32
- type: 'object',
33
- description: '`{best_estimate: int 1-5|null, confidence: low|moderate|high, possible_range: [low, high], reasoning: str}`. At least one of grade_estimate or trajectory_direction is required for add_evidence and new_episode.',
34
- },
35
- trajectory_direction: {
36
- type: 'string',
37
- enum: ['improving', 'stable', 'worsening', 'unknown'],
38
- description: 'Per-turn direction vs the prior impression.',
39
- },
40
- decision_posture: {
41
- type: 'string',
42
- enum: ['probe_for_discriminators', 'gather_more_evidence', 'commit_to_grade'],
43
- description: 'Optional. Clinical posture for this impression.',
44
- },
45
- close_reason: {
46
- type: 'string',
47
- description: 'Required for event=close. Why the case is being closed.',
48
- },
49
- temporality: {
50
- type: 'string',
51
- description: 'Optional. Pass only when temporality changed since the last impression.',
52
- },
53
- reasoning: {
54
- type: 'string',
55
- description: 'Optional. General clinical rationale for this impression.',
56
- },
57
- },
58
- required: ['case_id', 'event'],
59
- },
60
- };
61
-
62
- const EVENTS = definition.parameters.properties.event.enum;
63
- const TRAJECTORIES = definition.parameters.properties.trajectory_direction.enum;
64
- const POSTURES = definition.parameters.properties.decision_posture.enum;
65
- const OPEN_STATUSES = ['characterizing', 'monitoring'];
66
-
67
- const uid = (prefix) => `${prefix}_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`;
68
-
69
- function coerceGrade(raw, base) {
70
- if (!raw || typeof raw !== 'object') return base;
71
- return parseGradeEstimate(raw);
72
- }
73
-
74
- function buildAssessment(condition, previous, args) {
75
- const grade = coerceGrade(args?.grade_estimate, previous.grade);
76
- if (grade?.error) return grade;
77
- const trajectory = typeof args?.trajectory_direction === 'string' ? args.trajectory_direction : null;
78
- if (trajectory !== null && !TRAJECTORIES.includes(trajectory)) return { error: 'invalid_trajectory_direction' };
79
- const decisionPosture = typeof args?.decision_posture === 'string' ? args.decision_posture : null;
80
- if (decisionPosture !== null && !POSTURES.includes(decisionPosture)) return { error: 'invalid_decision_posture' };
81
- const reasoning = typeof args?.reasoning === 'string' ? args.reasoning : null;
82
- return {
83
- assessmentId: uid('assessment'),
84
- conditionId: condition.conditionId,
85
- patientId: condition.patientId,
86
- performedAt: new Date().toISOString(),
87
- previousId: previous.assessmentId,
88
- grade,
89
- trajectory,
90
- decisionPosture,
91
- reasoning,
92
- };
93
- }
94
-
95
- function toManaged(existing, conditionPatch, newAssessment) {
96
- const condition = { ...existing.condition, ...conditionPatch };
97
- const priorAssessments = [existing.latestAssessment, ...existing.priorAssessments];
98
- return new ManagedSymptom({
99
- condition,
100
- latestAssessment: newAssessment || existing.latestAssessment,
101
- priorAssessments: newAssessment ? priorAssessments : existing.priorAssessments,
102
- });
103
- }
104
-
105
- function fail(error, data = {}) {
106
- return JSON.stringify({ success: false, error, data });
107
- }
108
-
109
- function failAssessment(error, caseId, extra = {}) {
110
- const allowed = {
111
- grade_out_of_range: { allowed_range: [GRADE_MIN, GRADE_MAX] },
112
- invalid_trajectory_direction: { allowed: TRAJECTORIES },
113
- invalid_decision_posture: { allowed: POSTURES },
114
- }[error] || {};
115
- return fail(error, { case_id: caseId, ...allowed, ...extra });
116
- }
117
-
118
- function applyAddEvidence(existing, args) {
119
- if (existing.condition.clinicalStatus !== 'characterizing') {
120
- return fail('add_evidence requires status=characterizing. If status=monitoring, call new_episode; if closed, open a new case.', { case_id: existing.condition.conditionId, status: existing.condition.clinicalStatus });
121
- }
122
- const mentionId = (typeof args?.mention_id === 'string' ? args.mention_id : '').trim();
123
- if (!mentionId) return fail('mention_id is required for add_evidence');
124
- if (existing.condition.evidenceMentionIds.includes(mentionId)) {
125
- return fail('mention_already_recorded_on_case', { case_id: existing.condition.conditionId, mention_id: mentionId });
126
- }
127
- if (!args?.grade_estimate && !args?.trajectory_direction) {
128
- return fail('add_evidence requires at least one of `grade_estimate` or `trajectory_direction`.', { case_id: existing.condition.conditionId });
129
- }
130
- const assessment = buildAssessment(existing.condition, existing.latestAssessment, args);
131
- if (assessment.error) return failAssessment(assessment.error, existing.condition.conditionId);
132
- const patch = { evidenceMentionIds: [...existing.condition.evidenceMentionIds, mentionId] };
133
- if (typeof args?.temporality === 'string') patch.temporality = args.temporality;
134
- return toManaged(existing, patch, assessment);
135
- }
136
-
137
- function applyNewEpisode(existing, args) {
138
- if (!OPEN_STATUSES.includes(existing.condition.clinicalStatus)) {
139
- return fail('new_episode requires an open case (characterizing or monitoring).', { case_id: existing.condition.conditionId, status: existing.condition.clinicalStatus });
140
- }
141
- const episodeId = (typeof args?.episode_id === 'string' ? args.episode_id : '').trim();
142
- const mentionId = (typeof args?.mention_id === 'string' ? args.mention_id : '').trim();
143
- if (!episodeId || !mentionId) return fail('new_episode requires both episode_id and mention_id');
144
- if (existing.condition.evidenceEpisodeIds.includes(episodeId)) {
145
- return fail('episode_already_attached_to_case', { case_id: existing.condition.conditionId, episode_id: episodeId, current_episode_id: existing.condition.currentEpisodeId });
146
- }
147
- if (!args?.grade_estimate && !args?.trajectory_direction) {
148
- return fail('new_episode requires at least one of `grade_estimate` or `trajectory_direction`.', { case_id: existing.condition.conditionId, episode_id: episodeId });
149
- }
150
- const assessment = buildAssessment(existing.condition, existing.latestAssessment, args);
151
- if (assessment.error) return failAssessment(assessment.error, existing.condition.conditionId, { episode_id: episodeId });
152
- const mentions = [...existing.condition.evidenceMentionIds];
153
- if (!mentions.includes(mentionId)) mentions.push(mentionId);
154
- const patch = {
155
- evidenceEpisodeIds: [...existing.condition.evidenceEpisodeIds, episodeId],
156
- currentEpisodeId: episodeId,
157
- evidenceMentionIds: mentions,
158
- clinicalStatus: 'characterizing',
159
- };
160
- if (typeof args?.temporality === 'string') patch.temporality = args.temporality;
161
- return toManaged(existing, patch, assessment);
162
- }
163
-
164
- function applyResolveEpisode(existing, args) {
165
- if (existing.condition.clinicalStatus !== 'characterizing') {
166
- return fail('resolve_episode requires status=characterizing (there must be an active episode to resolve).', { case_id: existing.condition.conditionId, status: existing.condition.clinicalStatus });
167
- }
168
- const patch = { clinicalStatus: 'monitoring', currentEpisodeId: null };
169
- const mentionId = (typeof args?.mention_id === 'string' ? args.mention_id : '').trim();
170
- if (mentionId && !existing.condition.evidenceMentionIds.includes(mentionId)) {
171
- patch.evidenceMentionIds = [...existing.condition.evidenceMentionIds, mentionId];
172
- }
173
- if (typeof args?.temporality === 'string') patch.temporality = args.temporality;
174
- const reasoning = (typeof args?.reasoning === 'string' ? args.reasoning : '').trim();
175
- if (reasoning) patch.lastTransitionReason = reasoning;
176
- return toManaged(existing, patch, null);
177
- }
178
-
179
- function applyClose(existing, args) {
180
- if (!OPEN_STATUSES.includes(existing.condition.clinicalStatus)) {
181
- return fail('case_already_terminal', { case_id: existing.condition.conditionId, status: existing.condition.clinicalStatus, abatement_at: existing.condition.abatementAt, close_reason: existing.condition.closeReason });
182
- }
183
- const closeReason = (typeof args?.close_reason === 'string' ? args.close_reason : '').trim();
184
- if (!closeReason) return fail('close_reason is required');
185
- const patch = { clinicalStatus: 'closed', abatementAt: new Date().toISOString(), closeReason };
186
- const reasoning = (typeof args?.reasoning === 'string' ? args.reasoning : '').trim();
187
- if (reasoning) patch.lastTransitionReason = reasoning;
188
- return toManaged(existing, patch, null);
189
- }
190
-
191
- const HANDLERS = {
192
- add_evidence: applyAddEvidence,
193
- new_episode: applyNewEpisode,
194
- resolve_episode: applyResolveEpisode,
195
- close: applyClose,
196
- };
197
-
198
- async function handler(args = {}, context = {}) {
199
- try {
200
- const runtime = context?.toolRuntimeContext || null;
201
- if (!runtime?.turnId || !runtime?.patientCode) {
202
- return JSON.stringify({ success: false, error: 'recordClinicalImpression requires an active turn context (turnId, patientCode).', data: {} });
203
- }
204
- const caseId = (typeof args?.case_id === 'string' ? args.case_id : '').trim();
205
- const event = (typeof args?.event === 'string' ? args.event : '').trim().toLowerCase();
206
- if (!caseId) return fail('case_id is required');
207
- if (!EVENTS.includes(event)) {
208
- return fail(`unknown event: '${event}'. Must be one of ${EVENTS.join(' | ')}.`);
209
- }
210
-
211
- const patientId = runtime.patientCode;
212
- const cases = await readSymptomCases({ patientId });
213
- const existing = cases.find((symptomCase) => symptomCase.condition.conditionId === caseId);
214
- if (!existing) return fail('case_not_found_or_wrong_patient', { case_id: caseId });
215
-
216
- const result = HANDLERS[event](existing, args);
217
- if (typeof result === 'string') return result;
218
-
219
- await storeSymptomCase({ patientId, managedSymptom: result });
220
- const { condition, latestAssessment } = result;
221
- return JSON.stringify({
222
- success: true,
223
- data: {
224
- case_id: condition.conditionId,
225
- action_taken: event,
226
- status: condition.clinicalStatus,
227
- ctcae_term: condition.ctcaeTerm,
228
- episode_ids: [...condition.evidenceEpisodeIds],
229
- current_episode_id: condition.currentEpisodeId,
230
- mention_ids: [...condition.evidenceMentionIds],
231
- grade_estimate: {
232
- best_estimate: latestAssessment.grade.bestEstimate,
233
- confidence: latestAssessment.grade.confidence,
234
- possible_range: latestAssessment.grade.possibleRange,
235
- },
236
- temporality: condition.temporality,
237
- trajectory: { direction: latestAssessment.trajectory || 'unknown' },
238
- decision_posture: latestAssessment.decisionPosture,
239
- close_reason: condition.closeReason,
240
- abatement_at: condition.abatementAt,
241
- },
242
- });
243
- } catch (err) {
244
- return JSON.stringify({ success: false, error: err?.message || 'recordClinicalImpression failed', data: {} });
245
- }
246
- }
247
-
248
- module.exports = {
249
- definition,
250
- handler,
251
- };
@@ -1,140 +0,0 @@
1
- const crypto = require('node:crypto');
2
-
3
- const { readSymptomCases, readInterventions, storeIntervention } = require('../../fhir');
4
- const { Intervention } = require('../../shared/dtos/Intervention');
5
-
6
- const OPEN_CASE_STATUSES = ['characterizing', 'monitoring'];
7
- const KINDS = ['otc', 'self_care', 'other', 'education', 'team_escalation', 'follow_up_check'];
8
- const OUTCOMES = ['pending', 'improved', 'no_relief', 'side_effects', 'worsened', 'unknown'];
9
- const ACTIONS = ['create', 'record_outcome'];
10
-
11
- const definition = {
12
- name: 'recordIntervention',
13
- description: '**Does:** Records an agent-delivered intervention against an open symptom case (`action:\'create\'`) or patches the outcome of one already recorded (`action:\'record_outcome\'`). One intervention = one clinical action (OTC advice, self-care, education, a team escalation task, or a follow-up check).\n\n**Create inputs:** `case_id` (an OPEN case from `openCondition`/`getActiveSymptomLandscape`), `intervention_label`, `intervention_kind` (`otc` | `self_care` | `other` | `education` | `team_escalation` | `follow_up_check`); optional `rationale`. Fails hard with `case_not_found_or_wrong_patient` or `case_is_dormant` (recover via `recordClinicalImpression` reactivation), or `invalid intervention_kind`.\n\n**Outcome inputs (`action:\'record_outcome\'`):** `intervention_id` (the handle create returned), `outcome` (`pending` | `improved` | `no_relief` | `side_effects` | `worsened` | `unknown`); optional `outcome_notes`. Single-write: fails with `outcome_already_recorded` if the outcome is already non-pending.\n\n**When to call:** create when you deliver an intervention; record_outcome once the patient reports how it went.\n\n**Returns:** create → `{intervention_id, delivered_at}`; record_outcome → `{intervention_id, outcome}`.\n\n**Side effects:** writes a FHIR Procedure/Communication/Task/CommunicationRequest (per kind) + Provenance.',
14
- strict: false,
15
- parameters: {
16
- type: 'object',
17
- properties: {
18
- action: {
19
- type: 'string',
20
- enum: ACTIONS,
21
- description: '`create` (default) records a new intervention; `record_outcome` patches an existing one\'s outcome. When omitted, presence of `intervention_id` implies `record_outcome`.',
22
- },
23
- case_id: {
24
- type: 'string',
25
- description: 'create: the OPEN case_id this intervention attaches to.',
26
- },
27
- intervention_label: {
28
- type: 'string',
29
- description: 'create: short human label for the intervention.',
30
- },
31
- intervention_kind: {
32
- type: 'string',
33
- enum: KINDS,
34
- description: 'create: `otc`, `self_care`, `other`, `education`, `team_escalation`, or `follow_up_check`.',
35
- },
36
- rationale: {
37
- type: 'string',
38
- description: 'create: optional clinical rationale for the intervention.',
39
- },
40
- intervention_id: {
41
- type: 'string',
42
- description: 'record_outcome: the handle returned when the intervention was created.',
43
- },
44
- outcome: {
45
- type: 'string',
46
- enum: OUTCOMES,
47
- description: 'record_outcome: `pending`, `improved`, `no_relief`, `side_effects`, `worsened`, or `unknown`.',
48
- },
49
- outcome_notes: {
50
- type: 'string',
51
- description: 'record_outcome: optional free-text notes on the outcome.',
52
- },
53
- },
54
- required: [],
55
- },
56
- };
57
-
58
- function fail(error, data = {}) {
59
- return JSON.stringify({ success: false, error, data });
60
- }
61
-
62
- async function recordOutcome(patientId, interventionId, args) {
63
- const outcome = (typeof args?.outcome === 'string' ? args.outcome : '').trim().toLowerCase();
64
- if (!OUTCOMES.includes(outcome)) {
65
- return fail(`invalid outcome: '${outcome}'. Must be one of ${OUTCOMES.join(' | ')}.`, { outcome });
66
- }
67
- const existing = (await readInterventions({ patientId })).find((i) => i.interventionId === interventionId);
68
- if (!existing) return fail('intervention_not_found_or_wrong_patient', { intervention_id: interventionId });
69
- if (existing.outcome && existing.outcome !== 'pending') {
70
- return fail('outcome_already_recorded', { intervention_id: interventionId, outcome: existing.outcome });
71
- }
72
- const outcomeNotes = typeof args?.outcome_notes === 'string' ? (args.outcome_notes.trim() || null) : null;
73
- const patched = new Intervention({ ...existing, outcome, outcomeNotes });
74
- await storeIntervention({ patientId, intervention: patched });
75
- return JSON.stringify({ success: true, data: { intervention_id: patched.interventionId, outcome: patched.outcome } });
76
- }
77
-
78
- async function create(patientId, args) {
79
- const caseId = (typeof args?.case_id === 'string' ? args.case_id : '').trim();
80
- const label = (typeof args?.intervention_label === 'string' ? args.intervention_label : '').trim();
81
- const kind = (typeof args?.intervention_kind === 'string' ? args.intervention_kind : '').trim().toLowerCase();
82
- const rationale = typeof args?.rationale === 'string' ? (args.rationale.trim() || null) : null;
83
-
84
- const missing = [['case_id', caseId], ['intervention_label', label], ['intervention_kind', kind]]
85
- .filter(([, v]) => !v).map(([n]) => n);
86
- if (missing.length) return fail(`missing required field(s): ${missing.join(', ')}`);
87
- if (!KINDS.includes(kind)) return fail(`invalid intervention_kind: '${kind}'. Must be one of ${KINDS.join(' | ')}.`, { intervention_kind: kind });
88
-
89
- const targetCase = (await readSymptomCases({ patientId })).find((c) => c.condition.conditionId === caseId);
90
- if (!targetCase) return fail('case_not_found_or_wrong_patient', { case_id: caseId });
91
- if (!OPEN_CASE_STATUSES.includes(targetCase.condition.clinicalStatus)) {
92
- return fail('case_is_dormant — interventions attach to open cases; if the symptom recurred, reactivate the case via recordClinicalImpression first.', { case_id: caseId, status: targetCase.condition.clinicalStatus });
93
- }
94
-
95
- const deliveredAt = new Date().toISOString();
96
- const intervention = new Intervention({
97
- kind,
98
- interventionId: `intv_${crypto.randomUUID().replace(/-/g, '').slice(0, 12)}`,
99
- patientId,
100
- caseId,
101
- interventionLabel: label,
102
- rationale,
103
- deliveredAt,
104
- });
105
- await storeIntervention({ patientId, intervention });
106
- return JSON.stringify({ success: true, data: { intervention_id: intervention.interventionId, delivered_at: deliveredAt } });
107
- }
108
-
109
- async function handler(args = {}, context = {}) {
110
- try {
111
- const runtime = context?.toolRuntimeContext || null;
112
- if (!runtime?.turnId || !runtime?.patientCode) {
113
- return fail('recordIntervention requires an active turn context (turnId, patientCode).');
114
- }
115
- const patientId = runtime.patientCode;
116
- const interventionId = (typeof args?.intervention_id === 'string' ? args.intervention_id : '').trim();
117
- const action = (typeof args?.action === 'string' ? args.action : '').trim().toLowerCase();
118
- if (action && !ACTIONS.includes(action)) {
119
- return fail(`invalid action: '${action}'. Must be one of ${ACTIONS.join(' | ')}.`);
120
- }
121
- if (action === 'record_outcome' && !interventionId) {
122
- return fail('action=record_outcome requires `intervention_id`.');
123
- }
124
- if (action === 'create' && interventionId) {
125
- return fail('action=create must NOT carry `intervention_id` — use action=record_outcome to patch an outcome.', { intervention_id: interventionId });
126
- }
127
-
128
- if (action === 'record_outcome' || (!action && interventionId)) {
129
- return await recordOutcome(patientId, interventionId, args);
130
- }
131
- return await create(patientId, args);
132
- } catch (err) {
133
- return fail(err?.message || 'recordIntervention failed');
134
- }
135
- }
136
-
137
- module.exports = {
138
- definition,
139
- handler,
140
- };
@@ -1,39 +0,0 @@
1
- const { registerTool } = require('../../services/toolRegistryService');
2
-
3
- const TOOLS = [
4
- require('./extractClinicalInfoTool'),
5
- require('./lookupCtcaeEntryTool'),
6
- require('./reportMedicalEscalationTool'),
7
- require('./reportCrisisTool'),
8
- require('./reportUnresolvedRequestv2Tool'),
9
- require('./reportResultsReceivedTool'),
10
- require('./schedulePatientReminderTool'),
11
- require('./openClusterHypothesisTool'),
12
- require('./updateClusterHypothesisTool'),
13
- require('./submitRoutingDecisionTool'),
14
- require('./analyzeSymptomPatternsTool'),
15
- require('./openConditionTool'),
16
- require('./recordClinicalImpressionTool'),
17
- require('./getActiveSymptomLandscapeTool'),
18
- require('./recordInterventionTool'),
19
- require('./setContingencyPlanTool'),
20
- require('./getPatientRiskProfileTool'),
21
- require('./getPatientHistoryTool'),
22
- require('./getRouterContextBundleTool'),
23
- require('./deliverPatientMessageTool'),
24
- require('./skillTool'),
25
- ];
26
-
27
- function registerClinicalTools() {
28
- for (const tool of TOOLS) {
29
- registerTool({
30
- name: tool.definition.name,
31
- description: tool.definition.description,
32
- parameters: tool.definition.parameters,
33
- strict: tool.definition.strict,
34
- handler: tool.handler,
35
- });
36
- }
37
- }
38
-
39
- module.exports = { registerClinicalTools };