@peopl-health/nexus 5.11.0-dev.1124 → 5.11.0-dev.1126
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.
- package/lib/clinical/AssistantProcessor.js +7 -3
- package/lib/clinical/context/ToolRuntimeContext.js +10 -4
- package/lib/clinical/providers/AnthropicProvider.js +9 -5
- package/lib/clinical/providers/BaseLLMProvider.js +9 -4
- package/lib/clinical/providers/OpenAIResponsesProvider.js +7 -7
- package/lib/clinical/providers/OpenRouterProvider.js +7 -1
- package/lib/clinical/services/bridgeService.js +22 -7
- package/lib/clinical/services/clinicalAirtableService.js +19 -11
- package/lib/clinical/services/shadowService.js +1 -1
- package/lib/clinical/tools/extractClinicalInfoTool.js +1 -0
- package/lib/clinical/tools/schedulePatientReminderTool.js +12 -4
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ const { overlayCohortPreset } = require('./helpers/cohortHelper');
|
|
|
8
8
|
const { forkShadowTurn } = require('./services/shadowService');
|
|
9
9
|
const { diverge } = require('./services/divergenceService');
|
|
10
10
|
const { getAssistantById } = require('./services/assistantResolver');
|
|
11
|
-
const { maybeSendBridge } = require('./services/bridgeService');
|
|
11
|
+
const { maybeSendBridge, clearBridgeState } = require('./services/bridgeService');
|
|
12
12
|
const { getBridgePresetId } = require('./flags/bridgeConfig');
|
|
13
13
|
const { ToolRuntimeContext } = require('./context/ToolRuntimeContext');
|
|
14
14
|
|
|
@@ -45,7 +45,11 @@ class AssistantProcessor {
|
|
|
45
45
|
|
|
46
46
|
if (thread.isInCohort && !runOptions.toolRuntimeContext && runOptions.runId && thread.code) {
|
|
47
47
|
try {
|
|
48
|
-
runOptions.toolRuntimeContext = new ToolRuntimeContext({
|
|
48
|
+
runOptions.toolRuntimeContext = new ToolRuntimeContext({
|
|
49
|
+
patientCode: thread.code,
|
|
50
|
+
turnId: runOptions.runId,
|
|
51
|
+
shouldContinue: runOptions.shouldContinue,
|
|
52
|
+
});
|
|
49
53
|
} catch (error) {
|
|
50
54
|
logger.warn('[AssistantProcessor] turn will run untraced', { code: thread.code, error: error.message });
|
|
51
55
|
}
|
|
@@ -83,7 +87,6 @@ class AssistantProcessor {
|
|
|
83
87
|
outbound: output,
|
|
84
88
|
completed: runResult?.completed,
|
|
85
89
|
predictionTimeMs,
|
|
86
|
-
shouldContinue: runOptions.shouldContinue,
|
|
87
90
|
});
|
|
88
91
|
} catch (error) {
|
|
89
92
|
logger.warn('[AssistantProcessor] trace not stored', {
|
|
@@ -136,6 +139,7 @@ class AssistantProcessor {
|
|
|
136
139
|
if (!this.sendMessage) throw new Error('sendMessage function not configured');
|
|
137
140
|
if (!result?.output) return null;
|
|
138
141
|
await this.sendMessage({ code, body: result.output, processed: true, origin: 'assistant', tools_executed: result.tools_executed, prompt: result.prompt, preset: result.preset, response_id: result.response_id, turnId: result.turnId, traceId: result.traceId });
|
|
142
|
+
clearBridgeState(code);
|
|
139
143
|
return result.output;
|
|
140
144
|
}
|
|
141
145
|
}
|
|
@@ -8,6 +8,7 @@ class ToolRuntimeContext {
|
|
|
8
8
|
repos = {},
|
|
9
9
|
contextSource = null,
|
|
10
10
|
shadow = false,
|
|
11
|
+
shouldContinue = null,
|
|
11
12
|
} = {}) {
|
|
12
13
|
if (typeof patientCode !== 'string' || !patientCode) {
|
|
13
14
|
throw new Error('ToolRuntimeContext requires a non-empty patientCode');
|
|
@@ -22,6 +23,7 @@ class ToolRuntimeContext {
|
|
|
22
23
|
this.patientCode = patientCode;
|
|
23
24
|
this.turnId = turnId;
|
|
24
25
|
this.sessionId = sessionId;
|
|
26
|
+
this.shouldContinue = shouldContinue;
|
|
25
27
|
this.repos = repos;
|
|
26
28
|
this.contextSource = contextSource;
|
|
27
29
|
this.enabledSkills = [];
|
|
@@ -42,13 +44,17 @@ class ToolRuntimeContext {
|
|
|
42
44
|
return toolData;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
isLive() {
|
|
48
|
+
return typeof this.shouldContinue !== 'function' || this.shouldContinue();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async store({ outbound = null, completed = false, predictionTimeMs = null } = {}) {
|
|
46
52
|
this.trace.setSignals({ activatedSkills: [...this.activatedSkills].sort() });
|
|
47
53
|
this.trace.setOutbound(outbound);
|
|
48
|
-
this.
|
|
54
|
+
const superseded = !this.isLive();
|
|
55
|
+
this.trace.setStatus(superseded ? 'superseded' : (completed ? 'completed' : 'incomplete'));
|
|
49
56
|
this.trace.setTimings({ predictionTimeMs });
|
|
50
|
-
|
|
51
|
-
if (!superseded) await this.trace.save();
|
|
57
|
+
await this.trace.save();
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
setSafetyArtifact(artifact) {
|
|
@@ -11,7 +11,7 @@ const { logger } = require('../../utils/logger');
|
|
|
11
11
|
|
|
12
12
|
const runtimeConfig = require('../../config/runtimeConfig');
|
|
13
13
|
|
|
14
|
-
const { BaseLLMProvider, CONVERSATION_CONTINUATION, DELIVERY_NUDGE, MAX_DELIVERY_NUDGES, TERMINAL_DELIVERY_TOOLS } = require('./BaseLLMProvider');
|
|
14
|
+
const { BaseLLMProvider, CONVERSATION_CONTINUATION, DELIVERY_NUDGE, MAX_DELIVERY_NUDGES, TERMINAL_DELIVERY_TOOLS, isSuperseded } = require('./BaseLLMProvider');
|
|
15
15
|
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
16
16
|
|
|
17
17
|
const DEFAULT_MAX_FUNCTION_ROUNDS = 12;
|
|
@@ -69,7 +69,7 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
69
69
|
async _invokeModel({
|
|
70
70
|
instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
|
|
71
71
|
modelConfig, assistant, phiProcessor = null, trace = null, onFirstIteration = null,
|
|
72
|
-
resolvedPromptId = null, resolvedPresetId = null
|
|
72
|
+
resolvedPromptId = null, resolvedPresetId = null, shouldContinue = null
|
|
73
73
|
}, options = {}) {
|
|
74
74
|
const executeTools = options.toolExecutor || handleFunctionCalls;
|
|
75
75
|
let totalRetries = 0;
|
|
@@ -170,7 +170,7 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
170
170
|
&& (response.content || []).some(block => block.type === 'tool_use');
|
|
171
171
|
try {
|
|
172
172
|
const decision = onFirstIteration({ narrationText, hasToolCalls });
|
|
173
|
-
if (decision?.
|
|
173
|
+
if (decision?.dedupNote) bridgeNote = decision.dedupNote;
|
|
174
174
|
} catch (err) {
|
|
175
175
|
logger.warn('[AnthropicProvider] bridge callback failed', { error: err.message });
|
|
176
176
|
}
|
|
@@ -187,6 +187,10 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
187
187
|
let deliveryBreak = false;
|
|
188
188
|
const maxIterations = this.maxFunctionRounds + MAX_DELIVERY_NUDGES + LOOP_ITERATION_HEADROOM;
|
|
189
189
|
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
190
|
+
if (isSuperseded(shouldContinue)) {
|
|
191
|
+
logger.info('[AnthropicProvider] turn superseded — aborting tool loop', { iteration });
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
190
194
|
const content = finalResponse.content || [];
|
|
191
195
|
const toolUses = content.filter(block => block.type === 'tool_use');
|
|
192
196
|
const ended = finalResponse.stop_reason !== 'tool_use' || !toolUses.length;
|
|
@@ -251,9 +255,9 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
// Budget exhausted mid-work: the loop stopped while the model still wanted tools, so the reply may be empty. Make it
|
|
254
|
-
// visible — but not on a delivery break, which also
|
|
258
|
+
// visible — but not on a supersede or delivery break, which also exit with pending calls and are not exhaustion.
|
|
255
259
|
const pendingTools = (finalResponse.content || []).filter(block => block.type === 'tool_use');
|
|
256
|
-
if (finalResponse.stop_reason === 'tool_use' && pendingTools.length && !deliveryBreak) {
|
|
260
|
+
if (finalResponse.stop_reason === 'tool_use' && pendingTools.length && !deliveryBreak && !isSuperseded(shouldContinue)) {
|
|
257
261
|
logger.warn('[AnthropicProvider] Tool-iteration budget exhausted with pending tool calls', { maxFunctionRounds: this.maxFunctionRounds, pending: pendingTools.map(b => b.name) });
|
|
258
262
|
trace?.setSignals?.({ toolBudgetExhausted: true });
|
|
259
263
|
}
|
|
@@ -18,6 +18,10 @@ const TERMINAL_DELIVERY_TOOLS = new Set(['DeliverPatientMessage']);
|
|
|
18
18
|
const DELIVERY_NUDGE = 'sistema: el turno no puede cerrar sin un mensaje para el paciente. Llama ahora a DeliverPatientMessage con message_text en español. No repitas herramientas clínicas ya ejecutadas en este turno.';
|
|
19
19
|
const MAX_DELIVERY_NUDGES = 1;
|
|
20
20
|
|
|
21
|
+
function isSuperseded(shouldContinue) {
|
|
22
|
+
return typeof shouldContinue === 'function' && !shouldContinue();
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
class BaseLLMProvider {
|
|
22
26
|
constructor(options = {}) {
|
|
23
27
|
if (new.target === BaseLLMProvider) {
|
|
@@ -332,8 +336,8 @@ class BaseLLMProvider {
|
|
|
332
336
|
return result;
|
|
333
337
|
}
|
|
334
338
|
|
|
335
|
-
if (config.
|
|
336
|
-
logger.info('[runConversation]
|
|
339
|
+
if (isSuperseded(config.shouldContinue)) {
|
|
340
|
+
logger.info('[runConversation] Superseded — returning without retry', { attempt, shadow: config.shadow });
|
|
337
341
|
return result;
|
|
338
342
|
}
|
|
339
343
|
|
|
@@ -344,8 +348,8 @@ class BaseLLMProvider {
|
|
|
344
348
|
await new Promise(r => setTimeout(r, 500));
|
|
345
349
|
} catch (error) {
|
|
346
350
|
logger.error('[runConversation] Attempt failed', { attempt, error: error.message });
|
|
347
|
-
if (config.
|
|
348
|
-
logger.info('[runConversation]
|
|
351
|
+
if (isSuperseded(config.shouldContinue)) {
|
|
352
|
+
logger.info('[runConversation] Superseded — abandoning after error', { attempt, shadow: config.shadow });
|
|
349
353
|
return { status: 'cancelled', output_text: '', tools_executed: [], retries: 0 };
|
|
350
354
|
}
|
|
351
355
|
if (attempt === maxRetries) throw error;
|
|
@@ -528,4 +532,5 @@ module.exports = {
|
|
|
528
532
|
DELIVERY_NUDGE,
|
|
529
533
|
MAX_DELIVERY_NUDGES,
|
|
530
534
|
TERMINAL_DELIVERY_TOOLS,
|
|
535
|
+
isSuperseded,
|
|
531
536
|
};
|
|
@@ -9,7 +9,7 @@ const { logger } = require('../../utils/logger');
|
|
|
9
9
|
|
|
10
10
|
const runtimeConfig = require('../../config/runtimeConfig');
|
|
11
11
|
|
|
12
|
-
const { BaseLLMProvider } = require('./BaseLLMProvider');
|
|
12
|
+
const { BaseLLMProvider, isSuperseded } = require('./BaseLLMProvider');
|
|
13
13
|
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
14
14
|
|
|
15
15
|
const DEFAULT_MAX_FUNCTION_ROUNDS = 12;
|
|
@@ -69,7 +69,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
69
69
|
shadow = false, shouldContinue = null, onFirstIteration = null
|
|
70
70
|
}, options = {}) {
|
|
71
71
|
const executeTools = options.toolExecutor || handleFunctionCalls;
|
|
72
|
-
const
|
|
72
|
+
const superseded = () => isSuperseded(shouldContinue);
|
|
73
73
|
let totalRetries = 0;
|
|
74
74
|
const allToolsExecuted = [];
|
|
75
75
|
const allToolCalls = [];
|
|
@@ -127,7 +127,7 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
127
127
|
const hasToolCalls = Boolean(assistant) && (response.output || []).some(item => item.type === 'function_call');
|
|
128
128
|
try {
|
|
129
129
|
const decision = onFirstIteration({ narrationText, hasToolCalls });
|
|
130
|
-
if (decision?.
|
|
130
|
+
if (decision?.dedupNote) bridgeNote = decision.dedupNote;
|
|
131
131
|
} catch (err) {
|
|
132
132
|
logger.warn('[OpenAIResponsesProvider] bridge callback failed', { error: err.message });
|
|
133
133
|
}
|
|
@@ -140,8 +140,8 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
140
140
|
apiCallConfig.tool_choice = followUpToolChoice;
|
|
141
141
|
|
|
142
142
|
for (let round = 1; round <= this.maxFunctionRounds; round++) {
|
|
143
|
-
if (
|
|
144
|
-
logger.info('[OpenAIResponsesProvider]
|
|
143
|
+
if (superseded()) {
|
|
144
|
+
logger.info('[OpenAIResponsesProvider] turn superseded — aborting tool loop', { round, shadow });
|
|
145
145
|
break;
|
|
146
146
|
}
|
|
147
147
|
const functionCalls = finalResponse.output.filter(item => item.type === 'function_call');
|
|
@@ -167,9 +167,9 @@ class OpenAIResponsesProvider extends BaseLLMProvider {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
// Budget exhausted mid-work: the loop stopped while the model still wanted tools, so the reply may be empty. Make it
|
|
170
|
-
// visible — but not on a
|
|
170
|
+
// visible — but not on a supersede or delivery break, which also exit with pending calls and are not exhaustion.
|
|
171
171
|
const pendingCalls = (finalResponse.output || []).filter(item => item.type === 'function_call');
|
|
172
|
-
if (pendingCalls.length && !
|
|
172
|
+
if (pendingCalls.length && !superseded() && !deliveryBreak) {
|
|
173
173
|
logger.warn('[OpenAIResponsesProvider] Tool-iteration budget exhausted with pending tool calls', { maxFunctionRounds: this.maxFunctionRounds, pending: pendingCalls.map(c => c.name) });
|
|
174
174
|
trace?.setSignals?.({ toolBudgetExhausted: true });
|
|
175
175
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const { OpenAI } = require('openai');
|
|
2
2
|
|
|
3
|
+
const { logger } = require('../../utils/logger');
|
|
3
4
|
const { retryWithBackoff } = require('../../utils/retryUtils');
|
|
4
5
|
const { withTiming } = require('../../utils/tracingDecorator');
|
|
5
6
|
const { safeParse } = require('../../utils/jsonUtils');
|
|
6
7
|
|
|
7
8
|
const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
|
|
9
|
+
const { isSuperseded } = require('./BaseLLMProvider');
|
|
8
10
|
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
9
11
|
|
|
10
12
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
@@ -37,7 +39,7 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
|
|
|
37
39
|
|
|
38
40
|
async _invokeModel({
|
|
39
41
|
instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
|
|
40
|
-
modelConfig, assistant, phiProcessor = null, trace = null
|
|
42
|
+
modelConfig, assistant, phiProcessor = null, trace = null, shouldContinue = null
|
|
41
43
|
}, options = {}) {
|
|
42
44
|
const executeTools = options.toolExecutor || handleFunctionCalls;
|
|
43
45
|
let totalRetries = 0;
|
|
@@ -74,6 +76,10 @@ class OpenRouterProvider extends OpenAIResponsesProvider {
|
|
|
74
76
|
apiCallConfig.tool_choice = followUpToolChoice;
|
|
75
77
|
|
|
76
78
|
for (let round = 1; round <= this.maxFunctionRounds; round++) {
|
|
79
|
+
if (isSuperseded(shouldContinue)) {
|
|
80
|
+
logger.info('[OpenRouterProvider] turn superseded — aborting tool loop', { round });
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
77
83
|
const message = finalResponse.choices?.[0]?.message;
|
|
78
84
|
const toolCalls = message?.tool_calls || [];
|
|
79
85
|
if (!toolCalls.length) break;
|
|
@@ -25,9 +25,9 @@ function buildDedupNote(bridgeText) {
|
|
|
25
25
|
function maybeSendBridge({ narrationText, hasToolCalls, code, turnId = null, traceId = null, shouldContinue, sendMessage }) {
|
|
26
26
|
if (!isBridgeEnabled()) return { sent: false };
|
|
27
27
|
|
|
28
|
-
const skip = (reason) => {
|
|
28
|
+
const skip = (reason, extra = {}) => {
|
|
29
29
|
logger.info('[bridge] skipped', { code, reason });
|
|
30
|
-
return { sent: false };
|
|
30
|
+
return { sent: false, ...extra };
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
if (typeof shouldContinue === 'function' && !shouldContinue()) return skip('superseded');
|
|
@@ -36,20 +36,35 @@ function maybeSendBridge({ narrationText, hasToolCalls, code, turnId = null, tra
|
|
|
36
36
|
const text = sanitizeBridgeText(narrationText);
|
|
37
37
|
if (!text) return skip('no_usable_narration');
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
const pending = bridgeTimestamps.get(code);
|
|
40
|
+
if (pending !== undefined) {
|
|
41
|
+
return skip('recent_bridge_in_thread', pending.delivered ? { dedupNote: pending.dedupNote } : {});
|
|
42
|
+
}
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
const dedupNote = buildDedupNote(text);
|
|
45
|
+
const entry = { dedupNote, delivered: false };
|
|
46
|
+
bridgeTimestamps.set(code, entry);
|
|
47
|
+
const failed = (err) => {
|
|
48
|
+
bridgeTimestamps.delete(code);
|
|
49
|
+
logger.warn('[bridge] send failed', { code, error: err.message });
|
|
50
|
+
};
|
|
42
51
|
try {
|
|
43
52
|
Promise.resolve(sendMessage({ code, body: text, processed: true, origin: 'assistant', raw: { bridge: true }, turnId, traceId }))
|
|
44
|
-
.
|
|
53
|
+
.then(() => { entry.delivered = true; })
|
|
54
|
+
.catch(failed);
|
|
45
55
|
} catch (err) {
|
|
46
|
-
|
|
56
|
+
failed(err);
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
logger.info('[bridge] sent', { code });
|
|
50
|
-
return { sent: true, text, dedupNote
|
|
60
|
+
return { sent: true, text, dedupNote };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clearBridgeState(code) {
|
|
64
|
+
bridgeTimestamps.delete(code);
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
module.exports = {
|
|
54
68
|
maybeSendBridge,
|
|
69
|
+
clearBridgeState,
|
|
55
70
|
};
|
|
@@ -21,16 +21,21 @@ const INTAKE_SOURCE_TO_AIRTABLE = {
|
|
|
21
21
|
team_relay: 'doctor',
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
async function auditedWrite({ trace, code, table, write }) {
|
|
24
|
+
async function auditedWrite({ trace, code, table, write, isLive = null }) {
|
|
25
25
|
const entry = { table, at: new Date().toISOString(), ok: false };
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
if (isLive && !isLive()) {
|
|
27
|
+
entry.skipped = 'superseded';
|
|
28
|
+
logger.info('[clinicalAirtableService] Airtable write skipped — turn superseded', { table, code });
|
|
29
|
+
} else {
|
|
30
|
+
try {
|
|
31
|
+
const result = await write();
|
|
32
|
+
entry.ok = true;
|
|
33
|
+
const record = Array.isArray(result) ? result[0] : result;
|
|
34
|
+
if (record?.id) entry.recordId = record.id;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
entry.error = err?.message || String(err);
|
|
37
|
+
logger.error('[clinicalAirtableService] Airtable write failed', { table, code, error: entry.error });
|
|
38
|
+
}
|
|
34
39
|
}
|
|
35
40
|
if (trace?.setSignals) {
|
|
36
41
|
const existing = Array.isArray(trace.signals?.airtableWrites) ? trace.signals.airtableWrites : [];
|
|
@@ -92,10 +97,11 @@ async function recordEmergency({ code, trace, symptom, suspectedCondition = '',
|
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
async function recordClinicalIntake({ code, trace, messageRaw, source = 'self' }) {
|
|
100
|
+
async function recordClinicalIntake({ code, trace, messageRaw, source = 'self', isLive = null }) {
|
|
96
101
|
return auditedWrite({
|
|
97
102
|
trace,
|
|
98
103
|
code,
|
|
104
|
+
isLive,
|
|
99
105
|
table: 'intake',
|
|
100
106
|
write: async () => {
|
|
101
107
|
const patientId = await findIntakePatientRecordId(code);
|
|
@@ -110,12 +116,13 @@ async function recordClinicalIntake({ code, trace, messageRaw, source = 'self' }
|
|
|
110
116
|
});
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
async function recordReminder({ code, trace, actionType, reminderId, reminderDateUtc, reminderType, description }) {
|
|
119
|
+
async function recordReminder({ code, trace, actionType, reminderId, reminderDateUtc, reminderType, description, isLive = null }) {
|
|
114
120
|
const type = REMINDER_TYPE_TO_AIRTABLE[reminderType] || reminderType;
|
|
115
121
|
if (actionType === 'update') {
|
|
116
122
|
return auditedWrite({
|
|
117
123
|
trace,
|
|
118
124
|
code,
|
|
125
|
+
isLive,
|
|
119
126
|
table: 'reminder',
|
|
120
127
|
write: async () => {
|
|
121
128
|
const updated = await airtableService.updateRecordByFilter(
|
|
@@ -133,6 +140,7 @@ async function recordReminder({ code, trace, actionType, reminderId, reminderDat
|
|
|
133
140
|
return auditedWrite({
|
|
134
141
|
trace,
|
|
135
142
|
code,
|
|
143
|
+
isLive,
|
|
136
144
|
table: 'reminder',
|
|
137
145
|
write: () => airtableService.addLinkedRecord(
|
|
138
146
|
Monitoreo_ID,
|
|
@@ -27,7 +27,7 @@ async function runShadowTurn({
|
|
|
27
27
|
const shadowAssistant = resolveAssistant(shadowThread);
|
|
28
28
|
|
|
29
29
|
const context = new ToolRuntimeContext({
|
|
30
|
-
patientCode: code, turnId: runId, sessionId, shadow: true,
|
|
30
|
+
patientCode: code, turnId: runId, sessionId, shadow: true, shouldContinue,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
const runOptions = {
|
|
@@ -107,11 +107,18 @@ async function handler(args = {}, context = {}) {
|
|
|
107
107
|
};
|
|
108
108
|
trace.setSignals({ reminderIntents: [...existing, intent] });
|
|
109
109
|
|
|
110
|
+
const isLive = runtime.isLive?.bind(runtime);
|
|
111
|
+
const superseded = Boolean(isLive) && !isLive();
|
|
112
|
+
|
|
110
113
|
if (runtime.patientCode) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
114
|
+
if (superseded) {
|
|
115
|
+
logger.info('[schedulePatientReminder] reminder projection skipped — turn superseded', { turnId });
|
|
116
|
+
} else {
|
|
117
|
+
try {
|
|
118
|
+
await dispatchReminder({ intent, patientId: runtime.patientCode });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.warn('[schedulePatientReminder] reminder FHIR projection failed; ack unaffected', { turnId, error: err?.message });
|
|
121
|
+
}
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
124
|
|
|
@@ -124,6 +131,7 @@ async function handler(args = {}, context = {}) {
|
|
|
124
131
|
reminderDateUtc: fireMoment.toISOString(),
|
|
125
132
|
reminderType,
|
|
126
133
|
description: storedDescription,
|
|
134
|
+
isLive,
|
|
127
135
|
});
|
|
128
136
|
}
|
|
129
137
|
|