@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.
- package/lib/clinical/AssistantProcessor.js +5 -34
- package/lib/clinical/config/divergenceConfig.js +1 -18
- package/lib/clinical/config/llmConfig.js +1 -9
- package/lib/clinical/context/ToolRuntimeContext.js +2 -1
- package/lib/clinical/helpers/divergenceHelper.js +0 -12
- package/lib/clinical/index.js +0 -2
- package/lib/clinical/memory/DefaultMemoryManager.js +9 -19
- package/lib/clinical/models/turnTraceModel.js +5 -18
- package/lib/clinical/providers/AnthropicProvider.js +29 -291
- package/lib/clinical/providers/BaseLLMProvider.js +24 -113
- package/lib/clinical/providers/GatewayProvider.js +3 -3
- package/lib/clinical/providers/OpenAIResponsesProvider.js +4 -46
- package/lib/clinical/services/divergenceService.js +5 -22
- package/lib/clinical/services/promptComposerService.js +15 -46
- package/lib/clinical/services/shadowMessageService.js +12 -28
- package/lib/clinical/services/shadowService.js +3 -12
- package/lib/clinical/stores/CtcaeCatalog.js +0 -20
- package/lib/config/airtableConfig.js +0 -3
- package/lib/core/BatchingManager.js +3 -10
- package/lib/core/NexusMessaging.js +13 -31
- package/lib/eval/EvalProvider.js +2 -3
- package/lib/fhir/config/fhirConfig.js +3 -34
- package/lib/fhir/constants/projectionSlugs.js +1 -41
- package/lib/fhir/helpers/elementHelper.js +0 -21
- package/lib/fhir/helpers/fhirHelper.js +2 -5
- package/lib/fhir/index.js +0 -24
- package/lib/fhir/models/envelopeModel.js +0 -5
- package/lib/fhir/projections/registerProjectors.js +0 -16
- package/lib/fhir/resources/Condition.js +2 -107
- package/lib/fhir/resources/Observation.js +1 -73
- package/lib/fhir/resources/Procedure.js +2 -34
- package/lib/fhir/resources/Provenance.js +2 -93
- package/lib/fhir/services/fhirService.js +2 -2
- package/lib/helpers/twilioHelper.js +1 -9
- package/lib/index.d.ts +0 -16
- package/lib/index.js +0 -9
- package/lib/models/messageModel.js +0 -6
- package/lib/routes/index.js +1 -21
- package/lib/services/airtableService.js +1 -2
- package/lib/shared/dtos/ContingencySafetyNet.js +3 -10
- package/lib/shared/dtos/DispatchedEscalation.js +8 -3
- package/lib/shared/dtos/Reminder.js +2 -2
- package/lib/storage/MongoStorage.js +0 -2
- package/package.json +1 -1
- package/lib/clinical/config/cohortConfig.js +0 -68
- package/lib/clinical/config/composerConfig.js +0 -42
- package/lib/clinical/config/subAgentsConfig.js +0 -44
- package/lib/clinical/flags/bridgeConfig.js +0 -14
- package/lib/clinical/flags/composerFlags.js +0 -14
- package/lib/clinical/helpers/clinicalFactsHelper.js +0 -108
- package/lib/clinical/helpers/clinicalMentionHelper.js +0 -68
- package/lib/clinical/helpers/cohortHelper.js +0 -22
- package/lib/clinical/helpers/gradeEstimateHelper.js +0 -29
- package/lib/clinical/helpers/resultsHelper.js +0 -34
- package/lib/clinical/helpers/safetyFlagsHelper.js +0 -10
- package/lib/clinical/services/bridgeService.js +0 -55
- package/lib/clinical/services/clinicalAirtableService.js +0 -162
- package/lib/clinical/services/clinicalExtractionService.js +0 -112
- package/lib/clinical/services/composerService.js +0 -59
- package/lib/clinical/services/dispatcherService.js +0 -137
- package/lib/clinical/services/reminderDispatchService.js +0 -36
- package/lib/clinical/services/skillService.js +0 -44
- package/lib/clinical/tools/analyzeSymptomPatternsTool.js +0 -279
- package/lib/clinical/tools/deliverPatientMessageTool.js +0 -202
- package/lib/clinical/tools/extractClinicalInfoTool.js +0 -167
- package/lib/clinical/tools/getActiveSymptomLandscapeTool.js +0 -141
- package/lib/clinical/tools/getPatientHistoryTool.js +0 -235
- package/lib/clinical/tools/getPatientRiskProfileTool.js +0 -71
- package/lib/clinical/tools/getRouterContextBundleTool.js +0 -93
- package/lib/clinical/tools/lookupCtcaeEntryTool.js +0 -35
- package/lib/clinical/tools/openClusterHypothesisTool.js +0 -160
- package/lib/clinical/tools/openConditionTool.js +0 -136
- package/lib/clinical/tools/recordClinicalImpressionTool.js +0 -251
- package/lib/clinical/tools/recordInterventionTool.js +0 -140
- package/lib/clinical/tools/registerClinicalTools.js +0 -39
- package/lib/clinical/tools/reportCrisisTool.js +0 -211
- package/lib/clinical/tools/reportMedicalEscalationTool.js +0 -187
- package/lib/clinical/tools/reportResultsReceivedTool.js +0 -153
- package/lib/clinical/tools/reportUnresolvedRequestv2Tool.js +0 -177
- package/lib/clinical/tools/schedulePatientReminderTool.js +0 -149
- package/lib/clinical/tools/setContingencyPlanTool.js +0 -176
- package/lib/clinical/tools/skillTool.js +0 -67
- package/lib/clinical/tools/submitRoutingDecisionTool.js +0 -291
- package/lib/clinical/tools/updateClusterHypothesisTool.js +0 -159
- package/lib/controllers/cohortController.js +0 -16
- package/lib/controllers/traceController.js +0 -26
- package/lib/core/workflowRunner.js +0 -35
- package/lib/fhir/config/fhirTablesConfig.js +0 -9
- package/lib/fhir/constants/severityPriority.js +0 -8
- package/lib/fhir/helpers/compositeHelper.js +0 -88
- package/lib/fhir/helpers/fhirReadHelper.js +0 -42
- package/lib/fhir/helpers/terminologyHelper.js +0 -260
- package/lib/fhir/projections/clusterProjection.js +0 -24
- package/lib/fhir/projections/contingencyProjection.js +0 -36
- package/lib/fhir/projections/dispatchedEscalationProjection.js +0 -25
- package/lib/fhir/projections/interventionProjection.js +0 -42
- package/lib/fhir/projections/proctcaeProjection.js +0 -179
- package/lib/fhir/projections/reminderProjection.js +0 -20
- package/lib/fhir/projections/riskProjection.js +0 -16
- package/lib/fhir/projections/routingProjection.js +0 -48
- package/lib/fhir/projections/symptomCaseProjection.js +0 -65
- package/lib/fhir/resources/CarePlan.js +0 -93
- package/lib/fhir/resources/ClinicalImpression.js +0 -118
- package/lib/fhir/resources/Communication.js +0 -65
- package/lib/fhir/resources/CommunicationRequest.js +0 -182
- package/lib/fhir/resources/Device.js +0 -24
- package/lib/fhir/resources/Patient.js +0 -18
- package/lib/fhir/resources/Questionnaire.js +0 -23
- package/lib/fhir/resources/QuestionnaireResponse.js +0 -29
- package/lib/fhir/resources/RequestOrchestration.js +0 -46
- package/lib/fhir/resources/RiskAssessment.js +0 -99
- package/lib/fhir/resources/Task.js +0 -106
- package/lib/fhir/resources/interventionExtensions.js +0 -28
- package/lib/fhir/services/clusterService.js +0 -77
- package/lib/fhir/services/contingencyService.js +0 -117
- package/lib/fhir/services/interventionService.js +0 -149
- package/lib/fhir/services/resultObservationService.js +0 -52
- package/lib/fhir/services/riskService.js +0 -101
- package/lib/fhir/services/routingService.js +0 -147
- package/lib/fhir/services/snapshotService.js +0 -28
- package/lib/fhir/services/symptomCaseService.js +0 -139
- package/lib/fhir/services/terminologyService.js +0 -58
- package/lib/fhir/services/triageService.js +0 -57
- package/lib/fhir/utils/normalizeUtils.js +0 -33
- package/lib/shared/dtos/GradedSymptom.js +0 -23
- package/lib/shared/dtos/TriageResponse.js +0 -35
|
@@ -1,60 +1,28 @@
|
|
|
1
1
|
const { Anthropic } = require('@anthropic-ai/sdk');
|
|
2
2
|
|
|
3
|
-
const { composePrompt } = require('../services/promptComposerService');
|
|
4
3
|
const { retryWithBackoff } = require('../../utils/retryUtils');
|
|
5
4
|
const { withTiming } = require('../../utils/tracingDecorator');
|
|
6
|
-
const { isPlainObject, safeParse } = require('../../utils/jsonUtils');
|
|
7
5
|
|
|
8
6
|
const { logger } = require('../../utils/logger');
|
|
9
7
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const { BaseLLMProvider, CONVERSATION_CONTINUATION, DELIVERY_NUDGE, MAX_DELIVERY_NUDGES, TERMINAL_DELIVERY_TOOLS } = require('./BaseLLMProvider');
|
|
8
|
+
const { BaseLLMProvider } = require('./BaseLLMProvider');
|
|
13
9
|
const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
|
|
14
10
|
|
|
15
|
-
const DEFAULT_MAX_FUNCTION_ROUNDS =
|
|
16
|
-
|
|
17
|
-
function resolveMaxFunctionRounds(explicit) {
|
|
18
|
-
if (Number.isInteger(explicit) && explicit > 0) return explicit;
|
|
19
|
-
const raw = runtimeConfig.get('MAX_FUNCTION_ROUNDS');
|
|
20
|
-
const n = parseInt(raw, 10);
|
|
21
|
-
if (n > 0) return n;
|
|
22
|
-
if (raw !== undefined) {
|
|
23
|
-
logger.warn('[AnthropicProvider] Invalid MAX_FUNCTION_ROUNDS, falling back to default', { value: raw, default: DEFAULT_MAX_FUNCTION_ROUNDS });
|
|
24
|
-
}
|
|
25
|
-
return DEFAULT_MAX_FUNCTION_ROUNDS;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Iterations that don't count against the tool budget: terminal-only delivery rounds (exempt at the toolRounds check) plus the loop's own end/break passes. Bounds the loop against a misbehaving model.
|
|
29
|
-
const LOOP_ITERATION_HEADROOM = 3;
|
|
30
|
-
const CACHE_TTLS = ['5m', '1h'];
|
|
31
|
-
const DEFAULT_CACHE_TTL = '1h';
|
|
32
|
-
const DEFAULT_THINKING_MODE = 'until_commit';
|
|
33
|
-
// Turns that end the model's clinical reasoning for the turn; after one, until_commit stops thinking. Diverges from the oracle,
|
|
34
|
-
// which excludes submitRoutingDecision because its palliative/emergency branches do more work post-routing. Nexus has no careplan
|
|
35
|
-
// or palliative tools (registerClinicalTools.js) — submitRoutingDecision is the terminal routing checkpoint here, so it belongs.
|
|
36
|
-
const TERMINAL_COMMIT_TOOLS = new Set(['submitRoutingDecision', 'reportMedicalEscalation', 'reportCrisis', 'reportUnresolvedRequestv2']);
|
|
11
|
+
const DEFAULT_MAX_FUNCTION_ROUNDS = 5;
|
|
37
12
|
|
|
38
13
|
class AnthropicProvider extends BaseLLMProvider {
|
|
39
14
|
constructor(options = {}) {
|
|
40
15
|
super(options);
|
|
41
|
-
const { apiKey, client, maxFunctionRounds, defaultModels = {} } = options;
|
|
16
|
+
const { apiKey, client, maxFunctionRounds = DEFAULT_MAX_FUNCTION_ROUNDS, defaultModels = {} } = options;
|
|
42
17
|
|
|
43
18
|
if (!client && !apiKey) {
|
|
44
19
|
throw new Error('AnthropicProvider requires an API key or a preconfigured client');
|
|
45
20
|
}
|
|
46
21
|
|
|
47
22
|
this.client = client || new Anthropic({ apiKey });
|
|
48
|
-
this.maxFunctionRounds =
|
|
49
|
-
const cacheTtl = runtimeConfig.get('ANTHROPIC_CACHE_TTL', DEFAULT_CACHE_TTL);
|
|
50
|
-
if (!CACHE_TTLS.includes(cacheTtl)) {
|
|
51
|
-
logger.warn('[AnthropicProvider] Invalid ANTHROPIC_CACHE_TTL, falling back to default', { cacheTtl, default: DEFAULT_CACHE_TTL });
|
|
52
|
-
}
|
|
53
|
-
this.cacheTtl = CACHE_TTLS.includes(cacheTtl) ? cacheTtl : DEFAULT_CACHE_TTL;
|
|
23
|
+
this.maxFunctionRounds = maxFunctionRounds;
|
|
54
24
|
this.defaults = {
|
|
55
25
|
maxOutputTokens: 400,
|
|
56
|
-
structuredModel: 'claude-haiku-4-5',
|
|
57
|
-
structuredMaxTokens: 2048,
|
|
58
26
|
...defaultModels,
|
|
59
27
|
};
|
|
60
28
|
|
|
@@ -64,7 +32,7 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
64
32
|
|
|
65
33
|
async _invokeModel({
|
|
66
34
|
instructions, input, toolSchemas = [], toolChoice = 'auto', followUpToolChoice = 'auto',
|
|
67
|
-
modelConfig, assistant, phiProcessor = null, trace = null
|
|
35
|
+
modelConfig, assistant, phiProcessor = null, trace = null
|
|
68
36
|
}, options = {}) {
|
|
69
37
|
const executeTools = options.toolExecutor || handleFunctionCalls;
|
|
70
38
|
let totalRetries = 0;
|
|
@@ -73,48 +41,23 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
73
41
|
const accumulatedUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
74
42
|
const addUsage = (usage) => {
|
|
75
43
|
if (!usage) return;
|
|
76
|
-
|
|
77
|
-
accumulatedUsage.input_tokens += inputTokens;
|
|
44
|
+
accumulatedUsage.input_tokens += usage.input_tokens || 0;
|
|
78
45
|
accumulatedUsage.output_tokens += usage.output_tokens || 0;
|
|
79
|
-
accumulatedUsage.total_tokens +=
|
|
46
|
+
accumulatedUsage.total_tokens += (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
80
47
|
};
|
|
81
48
|
|
|
82
49
|
const { system, messages } = this._toAnthropicMessages(instructions, input);
|
|
83
|
-
const apiCallConfig = { ...this._mapModelConfig(modelConfig) };
|
|
84
|
-
// Captured before prepThinking() starts toggling apiCallConfig.thinking per call, so both the per-iteration gate and the
|
|
85
|
-
// tool_choice coercion read the same preset value rather than whichever iteration's state happens to be current.
|
|
86
|
-
const baseThinking = apiCallConfig.thinking;
|
|
87
|
-
const thinkingEnabled = baseThinking?.type === 'enabled';
|
|
88
|
-
const initialChoice = this._mapToolChoice(toolChoice);
|
|
89
|
-
// Only a request that actually carried the forced choice was coerced; with zero tools tool_choice was never sent.
|
|
90
|
-
const toolChoiceCoerced = toolSchemas.length > 0 && thinkingEnabled && (initialChoice.type === 'tool' || initialChoice.type === 'any');
|
|
91
|
-
if (system) apiCallConfig.system = system;
|
|
50
|
+
const apiCallConfig = { ...this._mapModelConfig(modelConfig), system };
|
|
92
51
|
if (toolSchemas.length > 0) {
|
|
93
52
|
apiCallConfig.tools = this._mapTools(toolSchemas);
|
|
94
|
-
apiCallConfig.tool_choice = this.
|
|
53
|
+
apiCallConfig.tool_choice = this._mapToolChoice(toolChoice);
|
|
95
54
|
}
|
|
96
55
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
modelCallNum += 1;
|
|
102
|
-
if (baseThinking && this._thinkingForIter(modelCallNum, thinkingMode, terminalCommitSeen)) apiCallConfig.thinking = baseThinking;
|
|
103
|
-
else delete apiCallConfig.thinking;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const betas = Array.isArray(modelConfig?.betas) ? modelConfig.betas.filter(b => typeof b === 'string' && b.length) : [];
|
|
107
|
-
const makeAPICall = (msgs) => {
|
|
108
|
-
// interleaved-thinking requires thinking enabled on the call; drop it on iters the gate turned thinking off.
|
|
109
|
-
const active = apiCallConfig.thinking ? betas : betas.filter(b => !b.startsWith('interleaved-thinking'));
|
|
110
|
-
const requestOptions = active.length ? { headers: { 'anthropic-beta': active.join(',') } } : undefined;
|
|
111
|
-
return retryWithBackoff(
|
|
112
|
-
() => this.client.messages.create({ messages: msgs, ...apiCallConfig }, requestOptions),
|
|
113
|
-
{ providerName: this.constructor.name }
|
|
114
|
-
);
|
|
115
|
-
};
|
|
56
|
+
const makeAPICall = (msgs) => retryWithBackoff(
|
|
57
|
+
() => this.client.messages.create({ messages: msgs, ...apiCallConfig }),
|
|
58
|
+
{ providerName: this.constructor.name }
|
|
59
|
+
);
|
|
116
60
|
|
|
117
|
-
prepThinking();
|
|
118
61
|
const { result: { result: response, retries }, durationMs } = await withTiming(() => makeAPICall(messages));
|
|
119
62
|
totalRetries += retries;
|
|
120
63
|
addUsage(response.usage);
|
|
@@ -122,78 +65,12 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
122
65
|
let finalResponse = response;
|
|
123
66
|
const currentMessages = [...messages];
|
|
124
67
|
|
|
125
|
-
// Coercion is silent-by-default; the event that matters is the model then *declining* the forced tool under 'auto'.
|
|
126
|
-
if (toolChoiceCoerced) {
|
|
127
|
-
const toolUses = (response.content || []).filter(block => block.type === 'tool_use');
|
|
128
|
-
// A named force ('tool') is satisfied only by THAT tool; 'any'/'required' by any tool_use. Partial compliance
|
|
129
|
-
// (e.g. DeliverPatientMessage fires but the forced reportUnresolvedRequestv2 does not) must count as skipped.
|
|
130
|
-
const firedTool = initialChoice.type === 'tool'
|
|
131
|
-
? toolUses.some(block => block.name === initialChoice.name)
|
|
132
|
-
: toolUses.length > 0;
|
|
133
|
-
trace?.setSignals?.({ toolChoiceCoerced: true, forcedToolSkipped: !firedTool });
|
|
134
|
-
if (!firedTool) {
|
|
135
|
-
logger.warn('[AnthropicProvider] Forced tool skipped after coercion to auto under thinking', { requested: initialChoice.type, tool: initialChoice.name || null });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let bridgeNote = null;
|
|
140
|
-
if (onFirstIteration) {
|
|
141
|
-
const narrationText = (response.content || [])
|
|
142
|
-
.filter(block => block.type === 'text')
|
|
143
|
-
.map(block => block.text || '')
|
|
144
|
-
.join(' ')
|
|
145
|
-
.trim();
|
|
146
|
-
const hasToolCalls = Boolean(assistant) && toolSchemas.length > 0
|
|
147
|
-
&& response.stop_reason === 'tool_use'
|
|
148
|
-
&& (response.content || []).some(block => block.type === 'tool_use');
|
|
149
|
-
try {
|
|
150
|
-
const decision = onFirstIteration({ narrationText, hasToolCalls });
|
|
151
|
-
if (decision?.sent && decision.dedupNote) bridgeNote = decision.dedupNote;
|
|
152
|
-
} catch (err) {
|
|
153
|
-
logger.warn('[AnthropicProvider] bridge callback failed', { error: err.message });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
68
|
if (assistant && toolSchemas.length > 0) {
|
|
158
|
-
|
|
159
|
-
// reaches here from a caller that sets one at run level (e.g. EvalProvider).
|
|
160
|
-
apiCallConfig.tool_choice = this._resolveToolChoice(followUpToolChoice, thinkingEnabled);
|
|
161
|
-
const canDeliver = toolSchemas.some(s => TERMINAL_DELIVERY_TOOLS.has(s.function?.name));
|
|
162
|
-
|
|
163
|
-
let toolRounds = 0;
|
|
164
|
-
let deliveryNudges = 0;
|
|
165
|
-
const maxIterations = this.maxFunctionRounds + MAX_DELIVERY_NUDGES + LOOP_ITERATION_HEADROOM;
|
|
166
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
167
|
-
const content = finalResponse.content || [];
|
|
168
|
-
const toolUses = content.filter(block => block.type === 'tool_use');
|
|
169
|
-
const ended = finalResponse.stop_reason !== 'tool_use' || !toolUses.length;
|
|
170
|
-
const terminalOnly = toolUses.length > 0 && toolUses.every(block => TERMINAL_DELIVERY_TOOLS.has(block.name));
|
|
171
|
-
const budgetSpent = toolRounds >= this.maxFunctionRounds && !terminalOnly;
|
|
172
|
-
|
|
173
|
-
// Nudge on an actual delivery, not narration: a text block is not a patient message, so a turn ending in
|
|
174
|
-
// narration without DeliverPatientMessage still nudges. Rides on the last user turn (empty completions carry no content to echo).
|
|
175
|
-
if ((ended || budgetSpent) && canDeliver && !this._deliveryCommitted(allToolsExecuted) && deliveryNudges < MAX_DELIVERY_NUDGES) {
|
|
176
|
-
const lastUserTurn = currentMessages[currentMessages.length - 1];
|
|
177
|
-
if (lastUserTurn?.role === 'user') {
|
|
178
|
-
deliveryNudges++;
|
|
179
|
-
logger.warn('[AnthropicProvider] Turn will not deliver — nudging DeliverPatientMessage', { deliveryNudges, ended, budgetSpent });
|
|
180
|
-
const turnContent = Array.isArray(lastUserTurn.content)
|
|
181
|
-
? lastUserTurn.content
|
|
182
|
-
: [{ type: 'text', text: String(lastUserTurn.content ?? '') }];
|
|
183
|
-
lastUserTurn.content = [...turnContent, { type: 'text', text: DELIVERY_NUDGE }];
|
|
184
|
-
// The nudge is a model call like any other: gate thinking on it so the per-iteration counter stays honest.
|
|
185
|
-
prepThinking();
|
|
186
|
-
const { result: { result: nudged, retries: nudgeRetries }, durationMs: nudgeDurationMs } = await withTiming(() => makeAPICall(currentMessages));
|
|
187
|
-
totalRetries += nudgeRetries;
|
|
188
|
-
addUsage(nudged.usage);
|
|
189
|
-
producingIteration = trace?.addIteration({ response: nudged, durationMs: nudgeDurationMs, apiCallConfig }) || null;
|
|
190
|
-
finalResponse = nudged;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
69
|
+
apiCallConfig.tool_choice = this._mapToolChoice(followUpToolChoice);
|
|
194
70
|
|
|
195
|
-
|
|
196
|
-
|
|
71
|
+
for (let round = 1; round <= this.maxFunctionRounds; round++) {
|
|
72
|
+
const toolUses = (finalResponse.content || []).filter(block => block.type === 'tool_use');
|
|
73
|
+
if (finalResponse.stop_reason !== 'tool_use' || !toolUses.length) break;
|
|
197
74
|
|
|
198
75
|
const functionCalls = toolUses.map(block => ({
|
|
199
76
|
name: block.name,
|
|
@@ -203,150 +80,43 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
203
80
|
allToolCalls.push(...toolUses.map(block => ({ name: block.name, arguments: block.input ?? {}, call_id: block.id })));
|
|
204
81
|
const { outputs, toolsExecuted } = await executeTools(functionCalls, assistant, phiProcessor, producingIteration);
|
|
205
82
|
|
|
206
|
-
const userContent = outputs.map(o => ({ type: 'tool_result', tool_use_id: o.call_id, content: o.output }));
|
|
207
|
-
if (bridgeNote) {
|
|
208
|
-
userContent.push({ type: 'text', text: bridgeNote });
|
|
209
|
-
bridgeNote = null;
|
|
210
|
-
}
|
|
211
83
|
currentMessages.push(
|
|
212
84
|
{ role: 'assistant', content: finalResponse.content },
|
|
213
|
-
{ role: 'user', content:
|
|
85
|
+
{ role: 'user', content: outputs.map(o => ({ type: 'tool_result', tool_use_id: o.call_id, content: o.output })) }
|
|
214
86
|
);
|
|
215
87
|
allToolsExecuted.push(...toolsExecuted);
|
|
216
|
-
if (toolsExecuted.some(t => TERMINAL_COMMIT_TOOLS.has(t?.tool_name))) terminalCommitSeen = true;
|
|
217
88
|
|
|
218
|
-
prepThinking();
|
|
219
89
|
const { result: { result: followUp, retries: followUpRetries }, durationMs: followUpDurationMs } = await withTiming(() => makeAPICall(currentMessages));
|
|
220
90
|
totalRetries += followUpRetries;
|
|
221
91
|
addUsage(followUp.usage);
|
|
222
92
|
producingIteration = trace?.addIteration({ response: followUp, durationMs: followUpDurationMs, apiCallConfig }) || null;
|
|
223
93
|
finalResponse = followUp;
|
|
224
94
|
}
|
|
225
|
-
|
|
226
|
-
// Budget exhausted mid-work: the loop stopped while the model still wanted tools, so the reply may be empty. Make it visible.
|
|
227
|
-
const pendingTools = (finalResponse.content || []).filter(block => block.type === 'tool_use');
|
|
228
|
-
if (finalResponse.stop_reason === 'tool_use' && pendingTools.length) {
|
|
229
|
-
logger.warn('[AnthropicProvider] Tool-iteration budget exhausted with pending tool calls', { maxFunctionRounds: this.maxFunctionRounds, pending: pendingTools.map(b => b.name) });
|
|
230
|
-
trace?.setSignals?.({ toolBudgetExhausted: true });
|
|
231
|
-
}
|
|
232
95
|
}
|
|
233
96
|
|
|
234
97
|
return { response: this._normalizeResponse(finalResponse), toolCalls: allToolCalls, toolsExecuted: allToolsExecuted, retries: totalRetries, usage: accumulatedUsage };
|
|
235
98
|
}
|
|
236
99
|
|
|
237
|
-
async runStructured({ promptId = null, presetId = null, instructions = null, variables = null, input = [], text = null, model = null } = {}) {
|
|
238
|
-
let modelConfig = {};
|
|
239
|
-
let resolvedInstructions = instructions;
|
|
240
|
-
if (promptId || presetId) {
|
|
241
|
-
const composed = await composePrompt({ promptId, presetId, variables });
|
|
242
|
-
modelConfig = composed.modelConfig || {};
|
|
243
|
-
if (!resolvedInstructions) resolvedInstructions = composed.resolvedPrompt;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const { system, messages } = this._toAnthropicMessages(resolvedInstructions, input);
|
|
247
|
-
|
|
248
|
-
const apiConfig = {
|
|
249
|
-
model: model || (modelConfig.model && modelConfig.model.replace(/^anthropic\//, '')) || this.defaults.structuredModel,
|
|
250
|
-
max_tokens: modelConfig.max_output_tokens || this.defaults.structuredMaxTokens,
|
|
251
|
-
};
|
|
252
|
-
if (system) apiConfig.system = system;
|
|
253
|
-
|
|
254
|
-
const spec = text || modelConfig.text;
|
|
255
|
-
let tool = null;
|
|
256
|
-
if (spec) {
|
|
257
|
-
const format = spec.format ? spec.format : spec;
|
|
258
|
-
if (!format.schema) throw new Error('runStructured: format spec has no schema');
|
|
259
|
-
tool = { name: format.name || 'structuredOutput', input_schema: format.schema };
|
|
260
|
-
if (format.strict !== undefined) tool.strict = format.strict;
|
|
261
|
-
apiConfig.tools = [tool];
|
|
262
|
-
apiConfig.tool_choice = { type: 'tool', name: tool.name };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const { result } = await retryWithBackoff(
|
|
266
|
-
() => this.client.messages.create({ messages, ...apiConfig }),
|
|
267
|
-
{ providerName: this.constructor.name },
|
|
268
|
-
);
|
|
269
|
-
return tool ? this._normalizeStructured(result, tool) : this._normalizeResponse(result);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
100
|
_toAnthropicMessages(instructions, input) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const stableParts = [];
|
|
276
|
-
if (instructions) stableParts.push(instructions);
|
|
277
|
-
const runtimeParts = [];
|
|
278
|
-
|
|
279
|
-
const outputsByCallId = new Map();
|
|
280
|
-
for (const item of input || []) {
|
|
281
|
-
if (item.type === 'function_call_output') outputsByCallId.set(item.call_id, item.output);
|
|
282
|
-
}
|
|
101
|
+
const systemParts = [];
|
|
102
|
+
if (instructions) systemParts.push(instructions);
|
|
283
103
|
|
|
284
104
|
const messages = [];
|
|
285
|
-
let pendingUses = [];
|
|
286
|
-
let pendingResults = [];
|
|
287
|
-
const flushTools = () => {
|
|
288
|
-
if (!pendingUses.length) return;
|
|
289
|
-
messages.push({ role: 'assistant', content: pendingUses });
|
|
290
|
-
messages.push({ role: 'user', content: pendingResults });
|
|
291
|
-
pendingUses = [];
|
|
292
|
-
pendingResults = [];
|
|
293
|
-
};
|
|
294
|
-
|
|
295
105
|
for (const item of input || []) {
|
|
296
106
|
const type = item.type || 'message';
|
|
297
|
-
if (type
|
|
298
|
-
|
|
299
|
-
pendingUses.push({ type: 'tool_use', id: item.call_id, name: item.name, input: this._toToolInput(item.arguments) });
|
|
300
|
-
pendingResults.push({ type: 'tool_result', tool_use_id: item.call_id, content: this._toToolContent(outputsByCallId.get(item.call_id)) });
|
|
107
|
+
if (type !== 'message') {
|
|
108
|
+
logger.debug('[AnthropicProvider] Skipping non-message input item', { type });
|
|
301
109
|
continue;
|
|
302
110
|
}
|
|
303
|
-
if (type === 'function_call_output') continue;
|
|
304
|
-
flushTools();
|
|
305
111
|
const content = item.content;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (item.role === 'system') {
|
|
309
|
-
if (content) stableParts.push(content);
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (item.role === 'developer') {
|
|
313
|
-
if (content) runtimeParts.push(content);
|
|
112
|
+
if (item.role === 'developer' || item.role === 'system') {
|
|
113
|
+
if (content) systemParts.push(content);
|
|
314
114
|
continue;
|
|
315
115
|
}
|
|
316
116
|
messages.push({ role: item.role === 'assistant' ? 'assistant' : 'user', content });
|
|
317
117
|
}
|
|
318
|
-
flushTools();
|
|
319
|
-
|
|
320
|
-
if (!messages.length || messages[messages.length - 1].role === 'assistant') {
|
|
321
|
-
messages.push({ role: 'user', content: CONVERSATION_CONTINUATION });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const system = [];
|
|
325
|
-
if (stableParts.length) {
|
|
326
|
-
system.push({
|
|
327
|
-
type: 'text',
|
|
328
|
-
text: stableParts.join('\n\n'),
|
|
329
|
-
cache_control: { type: 'ephemeral', ttl: this.cacheTtl },
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
// Deliberately no cache_control: caching a prefix that changes every turn only pays write costs.
|
|
333
|
-
if (runtimeParts.length) {
|
|
334
|
-
system.push({ type: 'text', text: runtimeParts.join('\n\n') });
|
|
335
|
-
}
|
|
336
118
|
|
|
337
|
-
return { system:
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
_toToolInput(args) {
|
|
341
|
-
if (isPlainObject(args)) return args;
|
|
342
|
-
const parsed = safeParse(args, null);
|
|
343
|
-
if (isPlainObject(parsed)) return parsed;
|
|
344
|
-
logger.warn('[AnthropicProvider] Unusable tool arguments in history, sending empty input');
|
|
345
|
-
return {};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
_toToolContent(output) {
|
|
349
|
-
return typeof output === 'string' ? output : JSON.stringify(output ?? '');
|
|
119
|
+
return { system: systemParts.join('\n\n'), messages };
|
|
350
120
|
}
|
|
351
121
|
|
|
352
122
|
_mapModelConfig(modelConfig) {
|
|
@@ -358,8 +128,6 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
358
128
|
delete mapped.text;
|
|
359
129
|
delete mapped.reasoning;
|
|
360
130
|
delete mapped.max_output_tokens;
|
|
361
|
-
delete mapped.betas;
|
|
362
|
-
delete mapped.thinking_mode;
|
|
363
131
|
if (mapped.max_tokens == null) mapped.max_tokens = this.defaults.maxOutputTokens;
|
|
364
132
|
return mapped;
|
|
365
133
|
}
|
|
@@ -367,35 +135,16 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
367
135
|
_mapTools(toolSchemas) {
|
|
368
136
|
return toolSchemas.map(schema => {
|
|
369
137
|
const fn = schema.function || schema;
|
|
370
|
-
|
|
371
|
-
// exceeds Anthropic's complexity limit. runStructured keeps strict — single schema.
|
|
372
|
-
return {
|
|
138
|
+
const tool = {
|
|
373
139
|
name: fn.name,
|
|
374
140
|
description: fn.description,
|
|
375
141
|
input_schema: fn.parameters || { type: 'object', properties: {} },
|
|
376
142
|
};
|
|
143
|
+
if (fn.strict !== undefined) tool.strict = fn.strict;
|
|
144
|
+
return tool;
|
|
377
145
|
});
|
|
378
146
|
}
|
|
379
147
|
|
|
380
|
-
_thinkingForIter(iterationNum, mode, terminalCommitSeen) {
|
|
381
|
-
if (mode === 'always') return true;
|
|
382
|
-
if (mode === 'first_iter') return iterationNum === 1;
|
|
383
|
-
return iterationNum === 1 || !terminalCommitSeen;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Anthropic rejects a forced tool_choice ('tool'/'any') while extended thinking is enabled, so under thinking a forced
|
|
387
|
-
// choice is coerced to 'auto'. This deliberately prefers thinking over the tool guarantee: the model is already strongly
|
|
388
|
-
// prompted to call the tool. Adaptive thinking ({type:'adaptive'}) is intentionally NOT coerced — it is accepted with a
|
|
389
|
-
// forced choice on the first-party API (only Bedrock requires thinking off), so the guard fires on 'enabled' only.
|
|
390
|
-
_resolveToolChoice(toolChoice, thinkingEnabled) {
|
|
391
|
-
const mapped = this._mapToolChoice(toolChoice);
|
|
392
|
-
if (thinkingEnabled && (mapped.type === 'tool' || mapped.type === 'any')) {
|
|
393
|
-
logger.debug('[AnthropicProvider] Forced tool_choice coerced to auto under extended thinking', { requested: mapped.type, tool: mapped.name || null });
|
|
394
|
-
return { type: 'auto' };
|
|
395
|
-
}
|
|
396
|
-
return mapped;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
148
|
_mapToolChoice(toolChoice) {
|
|
400
149
|
if (!toolChoice || toolChoice === 'auto') return { type: 'auto' };
|
|
401
150
|
if (toolChoice === 'none') return { type: 'none' };
|
|
@@ -411,16 +160,6 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
411
160
|
.map(block => block.text || '')
|
|
412
161
|
.join('')
|
|
413
162
|
.trim();
|
|
414
|
-
return this._envelope(response, outputText);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
_normalizeStructured(response, tool) {
|
|
418
|
-
const toolUse = (response.content || []).find(block => block.type === 'tool_use' && block.name === tool.name);
|
|
419
|
-
const outputText = toolUse ? JSON.stringify(toolUse.input ?? {}) : '';
|
|
420
|
-
return this._envelope(response, outputText);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
_envelope(response, outputText) {
|
|
424
163
|
const output = outputText
|
|
425
164
|
? [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: outputText }] }]
|
|
426
165
|
: [];
|
|
@@ -432,12 +171,11 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
432
171
|
}
|
|
433
172
|
: undefined;
|
|
434
173
|
|
|
435
|
-
const statusByStopReason = { refusal: 'refused', max_tokens: 'incomplete' };
|
|
436
174
|
return {
|
|
437
175
|
id: response.id,
|
|
438
176
|
object: 'response',
|
|
439
177
|
model: response.model,
|
|
440
|
-
status:
|
|
178
|
+
status: response.stop_reason === 'refusal' ? 'refused' : 'completed',
|
|
441
179
|
stop_reason: response.stop_reason,
|
|
442
180
|
output,
|
|
443
181
|
output_text: outputText,
|