@nexus-cortex/core 4.108.22 → 4.109.0
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/.env.defaults +43 -0
- package/dist/config/RuntimeConfigRegistry.d.ts.map +1 -1
- package/dist/config/RuntimeConfigRegistry.js +4 -0
- package/dist/config/RuntimeConfigRegistry.js.map +1 -1
- package/dist/config/SettingsLoader.d.ts.map +1 -1
- package/dist/config/SettingsLoader.js +4 -0
- package/dist/config/SettingsLoader.js.map +1 -1
- package/dist/config/SettingsSchema.d.ts +4 -0
- package/dist/config/SettingsSchema.d.ts.map +1 -1
- package/dist/config/SettingsSchema.js +36 -0
- package/dist/config/SettingsSchema.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts.map +1 -1
- package/dist/config/effectiveConfig.js +4 -0
- package/dist/config/effectiveConfig.js.map +1 -1
- package/dist/orchestrator/APIClient.d.ts.map +1 -1
- package/dist/orchestrator/APIClient.js +8 -1
- package/dist/orchestrator/APIClient.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +10 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +171 -109
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/emptyResponseClassifier.d.ts +21 -0
- package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -1
- package/dist/orchestrator/emptyResponseClassifier.js +44 -0
- package/dist/orchestrator/emptyResponseClassifier.js.map +1 -1
- package/dist/orchestrator/imageRejectionHeal.d.ts +24 -0
- package/dist/orchestrator/imageRejectionHeal.d.ts.map +1 -0
- package/dist/orchestrator/imageRejectionHeal.js +65 -0
- package/dist/orchestrator/imageRejectionHeal.js.map +1 -0
- package/dist/orchestrator/timeBudget.d.ts +2 -0
- package/dist/orchestrator/timeBudget.d.ts.map +1 -1
- package/dist/orchestrator/timeBudget.js +10 -0
- package/dist/orchestrator/timeBudget.js.map +1 -1
- package/dist/orchestrator/turnEndGuards.d.ts +1 -1
- package/dist/orchestrator/turnEndGuards.d.ts.map +1 -1
- package/dist/orchestrator/turnEndGuards.js +21 -1
- package/dist/orchestrator/turnEndGuards.js.map +1 -1
- package/dist/training/DecisionStore.d.ts +1 -1
- package/dist/training/DecisionStore.d.ts.map +1 -1
- package/dist/training/DecisionStore.js.map +1 -1
- package/dist/utils/stateDir.d.ts +11 -0
- package/dist/utils/stateDir.d.ts.map +1 -1
- package/dist/utils/stateDir.js +53 -1
- package/dist/utils/stateDir.js.map +1 -1
- package/package.json +3 -3
|
@@ -48,7 +48,7 @@ import { resolveSubAgentTimeoutMs } from './subAgentTimeout.js';
|
|
|
48
48
|
import { resolveHerdrReporting, HerdrReporter, sanitizeHerdrAgentName } from './herdrReporter.js';
|
|
49
49
|
import { runDelegateInHerdr, readSubAgentRuntimeLever, resolveSubAgentRuntime } from './HerdrSubAgentRunner.js';
|
|
50
50
|
import { randomBytes } from 'crypto';
|
|
51
|
-
import { resolveTurnDeadlineMs, timeBudgetState, timeBudgetWarnNudge, resolveBudgetVisibility, resolveBudgetContinueMinRemaining, budgetBand, budgetVisibilityLine } from './timeBudget.js';
|
|
51
|
+
import { resolveTurnDeadlineMs, timeBudgetState, timeBudgetWarnNudge, resolveBudgetVisibility, resolveBudgetContinueMinRemaining, resolveBudgetContinueMaxNudges, budgetBand, budgetVisibilityLine } from './timeBudget.js';
|
|
52
52
|
import { readStagedDoctrine, applyCuratedDoctrine, runOrientForStaging, withTimeout } from './doctrineCuration.js';
|
|
53
53
|
import { ExactRepeatTracker, notePollResults } from '../training/loopLadder.js';
|
|
54
54
|
import { slashCommandRegistry } from '../commands/SlashCommandRegistry.js';
|
|
@@ -80,7 +80,8 @@ import { ListAvailableMcpServers, SearchMcpServers, GetMcpConfig, EnableMcpServe
|
|
|
80
80
|
import { InitCortexContext, MemoryWrite, MemoryRecall } from '../tools/context-management/index.js';
|
|
81
81
|
import { pruneAgedToolResults } from '../conversation/ToolResultPruner.js';
|
|
82
82
|
import { detectTailRepetition, tailLoopGuardEnabled } from './tailRepetitionDetector.js';
|
|
83
|
-
import { classifyEmptyResponse, emptyResponseNudge, nudgeForbidsTools } from './emptyResponseClassifier.js';
|
|
83
|
+
import { classifyEmptyResponse, emptyResponseNudge, nudgeForbidsTools, emptyResponseNudgeFor, isReasoningExhaustion, stepDownEffort, resolveReasoningExhaustBackoff } from './emptyResponseClassifier.js';
|
|
84
|
+
import { isImageRejectionError, stripRejectedImages } from './imageRejectionHeal.js'; // R154
|
|
84
85
|
// PTC: Progressive tool loading for non-PTC providers
|
|
85
86
|
import { ClientSideToolFilter } from '../tools/ClientSideToolFilter.js';
|
|
86
87
|
/** Grace the orchestrator's outer tool-abort waits past the tool's own deadline (ShellTool promote/kill fires first; 4.90.1). */
|
|
@@ -1591,6 +1592,9 @@ export class CortexOrchestrator {
|
|
|
1591
1592
|
// Keep executing tools until model returns a text response
|
|
1592
1593
|
let currentAssistantMessage = assistantMessage;
|
|
1593
1594
|
let currentAssistantCanonicalMessage = assistantCanonicalMessage;
|
|
1595
|
+
// R153: the stop reason of the LATEST response (continuation/retry/gate/synth refresh it) — the empty-response
|
|
1596
|
+
// classifier used to read the turn's FIRST response, so a `length` cutoff on a continuation was never seen.
|
|
1597
|
+
let lastStopReason = convertedResponse.stopReason;
|
|
1594
1598
|
this.recordDsmlRecovery(currentAssistantCanonicalMessage);
|
|
1595
1599
|
let toolCallIteration = 0;
|
|
1596
1600
|
// R137 HB-POLL-REPEAT-BREAKER: the exact-repeat breaker used to force-exit by overwriting
|
|
@@ -1651,7 +1655,9 @@ export class CortexOrchestrator {
|
|
|
1651
1655
|
this.visionHandoffsThisTurn = 0;
|
|
1652
1656
|
let endTurnNudges = 0;
|
|
1653
1657
|
let surrenderNudgeUsed = false; // item 13b: one execute-your-plan nudge per turn
|
|
1654
|
-
let
|
|
1658
|
+
let budgetContinueNudges = 0; // R151/R157: continue-with-budget nudges this turn (bounded by CORTEX_BUDGET_CONTINUE_MAX_NUDGES)
|
|
1659
|
+
const BUDGET_CONTINUE_MAX_NUDGES = resolveBudgetContinueMaxNudges();
|
|
1660
|
+
this.reasoningBackoffRemaining = 0; // R153: backoff is per turn
|
|
1655
1661
|
const END_TURN_MAX_NUDGES = 2;
|
|
1656
1662
|
// EndTurn gate / Stages 1-3 are OPT-IN (default OFF). The line-number
|
|
1657
1663
|
// fabrication they targeted is fully resolved at the root cause:
|
|
@@ -1759,7 +1765,7 @@ export class CortexOrchestrator {
|
|
|
1759
1765
|
// for observability + a tailored nudge. D-E: stopReason distinguishes a max_tokens truncation
|
|
1760
1766
|
// ('truncated' → continue) from a demand-final-answer.
|
|
1761
1767
|
const emptyClass = (!hasVisibleText && !structuredOutputState?.result)
|
|
1762
|
-
? classifyEmptyResponse(currentAssistantCanonicalMessage.content,
|
|
1768
|
+
? classifyEmptyResponse(currentAssistantCanonicalMessage.content, lastStopReason, loopHasBudget)
|
|
1763
1769
|
: null;
|
|
1764
1770
|
const isEmptyContinueKind = EMPTY_TURN_CONTINUE && !!emptyClass
|
|
1765
1771
|
&& (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active');
|
|
@@ -1775,6 +1781,7 @@ export class CortexOrchestrator {
|
|
|
1775
1781
|
}
|
|
1776
1782
|
console.warn(`[Orchestrator] Empty response detected (${emptyClass.kind}, hadReasoning=${emptyClass.hadReasoning}, iteration=${toolCallIteration}). ` +
|
|
1777
1783
|
`Retrying once with explicit completion prompt.`);
|
|
1784
|
+
const exhaustionLevel = this.armReasoningBackoffIfExhausted(emptyClass, options.parameters?.reasoningEffort ?? effectiveModel?.reasoning?.effort, toolCallIteration, currentAssistantMessage?.usage?.outputTokens);
|
|
1778
1785
|
// R26 (2026-05-15, surfaced by A/B benchmark): the empty assistant
|
|
1779
1786
|
// turn is already in messageHistory. The retry below rebuilds the
|
|
1780
1787
|
// full history and re-sends it. Anthropic's Messages API tolerates
|
|
@@ -1813,7 +1820,7 @@ export class CortexOrchestrator {
|
|
|
1813
1820
|
role: 'user',
|
|
1814
1821
|
content: [{
|
|
1815
1822
|
type: 'text',
|
|
1816
|
-
text: `<system-reminder>${
|
|
1823
|
+
text: `<system-reminder>${emptyResponseNudgeFor(emptyClass, exhaustionLevel)}${nudgeForbidsTools(emptyClass.kind) ? ' Do not call any more tools.' : ''}</system-reminder>`,
|
|
1817
1824
|
}],
|
|
1818
1825
|
},
|
|
1819
1826
|
timeline: {
|
|
@@ -1833,28 +1840,25 @@ export class CortexOrchestrator {
|
|
|
1833
1840
|
// tool-result continuation path (lines ~1395-1466) but without
|
|
1834
1841
|
// tool_result blocks.
|
|
1835
1842
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
else {
|
|
1856
|
-
retryApiResponse = await this.apiClient.sendRequest(retryRequest, effectiveModel);
|
|
1857
|
-
}
|
|
1843
|
+
const retryEffort = this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort; // R153 backoff > request param
|
|
1844
|
+
const buildRetryRequest = () => {
|
|
1845
|
+
const retryCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
1846
|
+
const req = this.gatewayTranslation.prepareRequest(retryCanonicalHistory, toolsToUse, effectiveModel, {
|
|
1847
|
+
temperature: options.parameters?.temperature,
|
|
1848
|
+
maxTokens: options.parameters?.maxTokens,
|
|
1849
|
+
topP: options.parameters?.topP,
|
|
1850
|
+
reasoningEffort: retryEffort,
|
|
1851
|
+
stream: options.streaming,
|
|
1852
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
1853
|
+
conversationId: this.currentConversationId, // R28b
|
|
1854
|
+
});
|
|
1855
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
1856
|
+
req.previousResponseId = this.lastResponseId;
|
|
1857
|
+
}
|
|
1858
|
+
req.conversationId = this.currentSessionId;
|
|
1859
|
+
return req;
|
|
1860
|
+
};
|
|
1861
|
+
const retryApiResponse = await this.sendWithImageHeal(buildRetryRequest, effectiveModel, 'empty_response_retry');
|
|
1858
1862
|
const retryConverted = this.gatewayTranslation.convertResponse(retryApiResponse.data, effectiveModel, {
|
|
1859
1863
|
sessionId: this.currentSessionId,
|
|
1860
1864
|
conversationId: this.currentConversationId,
|
|
@@ -1891,6 +1895,7 @@ export class CortexOrchestrator {
|
|
|
1891
1895
|
// Update current pointer so the loop re-evaluates with new content.
|
|
1892
1896
|
currentAssistantMessage = retryAssistantMessage;
|
|
1893
1897
|
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
1898
|
+
lastStopReason = retryConverted.stopReason; // R153
|
|
1894
1899
|
continue;
|
|
1895
1900
|
}
|
|
1896
1901
|
}
|
|
@@ -1962,7 +1967,7 @@ export class CortexOrchestrator {
|
|
|
1962
1967
|
temperature: options.parameters?.temperature,
|
|
1963
1968
|
maxTokens: options.parameters?.maxTokens,
|
|
1964
1969
|
topP: options.parameters?.topP,
|
|
1965
|
-
reasoningEffort: options.parameters?.reasoningEffort,
|
|
1970
|
+
reasoningEffort: this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort, // R153 backoff > request param
|
|
1966
1971
|
stream: options.streaming,
|
|
1967
1972
|
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
1968
1973
|
conversationId: this.currentConversationId, // R28b
|
|
@@ -2014,6 +2019,7 @@ export class CortexOrchestrator {
|
|
|
2014
2019
|
await this.historyStore.appendMessage(this.currentSessionId, inactionAssistantMessage);
|
|
2015
2020
|
currentAssistantMessage = inactionAssistantMessage;
|
|
2016
2021
|
currentAssistantCanonicalMessage = inactionAssistantCanonical;
|
|
2022
|
+
lastStopReason = inactionConverted.stopReason; // R153
|
|
2017
2023
|
continue;
|
|
2018
2024
|
}
|
|
2019
2025
|
}
|
|
@@ -2063,7 +2069,7 @@ export class CortexOrchestrator {
|
|
|
2063
2069
|
const budgetContinueMin = resolveBudgetContinueMinRemaining();
|
|
2064
2070
|
const budgetRemainingFrac = TURN_DEADLINE_MS > 0 ? Math.max(0, 1 - (Date.now() - loopStartMs) / TURN_DEADLINE_MS) : 0;
|
|
2065
2071
|
const budgetContinueUnsatisfied = !surrenderUnsatisfied && budgetContinueMin > 0 && TURN_DEADLINE_MS > 0 && resolveBudgetVisibility() &&
|
|
2066
|
-
ev.usedTools &&
|
|
2072
|
+
ev.usedTools && budgetContinueNudges < BUDGET_CONTINUE_MAX_NUDGES && budgetRemainingFrac >= budgetContinueMin &&
|
|
2067
2073
|
detectOpenItemsText(surrenderDraftText);
|
|
2068
2074
|
if (endTurnGateUnsatisfied && endTurnNudges >= END_TURN_MAX_NUDGES) {
|
|
2069
2075
|
const fallbackReason = !ev.endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
@@ -2093,16 +2099,16 @@ export class CortexOrchestrator {
|
|
|
2093
2099
|
console.warn('[Orchestrator] Surrender guard: remaining-steps finish detected — execute-your-plan nudge.');
|
|
2094
2100
|
}
|
|
2095
2101
|
else if (budgetContinueUnsatisfied) {
|
|
2096
|
-
|
|
2102
|
+
budgetContinueNudges++;
|
|
2097
2103
|
const store = this.getDecisionStore();
|
|
2098
2104
|
if (store) {
|
|
2099
2105
|
void store.recordEvent({
|
|
2100
2106
|
sessionId: this.currentSessionId ?? 'unknown',
|
|
2101
2107
|
kind: 'budget_continue_nudge',
|
|
2102
|
-
detail: { iteration: toolCallIteration, remainingFrac: Number(budgetRemainingFrac.toFixed(3)), deadlineMs: TURN_DEADLINE_MS, chars: surrenderDraftText.length },
|
|
2108
|
+
detail: { iteration: toolCallIteration, remainingFrac: Number(budgetRemainingFrac.toFixed(3)), deadlineMs: TURN_DEADLINE_MS, chars: surrenderDraftText.length, nudge: budgetContinueNudges, maxNudges: BUDGET_CONTINUE_MAX_NUDGES },
|
|
2103
2109
|
}).catch(() => { });
|
|
2104
2110
|
}
|
|
2105
|
-
console.warn(`[Orchestrator] Budget guard (R151): open-items finish with ${Math.round(budgetRemainingFrac * 100)}% of the wall budget left — continue-with-budget nudge.`);
|
|
2111
|
+
console.warn(`[Orchestrator] Budget guard (R151): open-items finish with ${Math.round(budgetRemainingFrac * 100)}% of the wall budget left — continue-with-budget nudge ${budgetContinueNudges}/${BUDGET_CONTINUE_MAX_NUDGES}.`);
|
|
2106
2112
|
}
|
|
2107
2113
|
else {
|
|
2108
2114
|
endTurnNudges++;
|
|
@@ -2131,7 +2137,7 @@ export class CortexOrchestrator {
|
|
|
2131
2137
|
const endTurnReminderText = surrenderUnsatisfied
|
|
2132
2138
|
? SURRENDER_REMINDER
|
|
2133
2139
|
: budgetContinueUnsatisfied
|
|
2134
|
-
? buildBudgetContinueReminder(TURN_DEADLINE_MS - (Date.now() - loopStartMs), TURN_DEADLINE_MS)
|
|
2140
|
+
? buildBudgetContinueReminder(TURN_DEADLINE_MS - (Date.now() - loopStartMs), TURN_DEADLINE_MS, budgetContinueNudges)
|
|
2135
2141
|
: !ev.endTurnCalled
|
|
2136
2142
|
? buildMissingEndTurnReminder(ev)
|
|
2137
2143
|
: ('<system-reminder>EndTurn REJECTED. Your drafted answer asserts line number(s) ' +
|
|
@@ -2174,7 +2180,7 @@ export class CortexOrchestrator {
|
|
|
2174
2180
|
temperature: options.parameters?.temperature,
|
|
2175
2181
|
maxTokens: options.parameters?.maxTokens,
|
|
2176
2182
|
topP: options.parameters?.topP,
|
|
2177
|
-
reasoningEffort: options.parameters?.reasoningEffort,
|
|
2183
|
+
reasoningEffort: this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort, // R153 backoff > request param
|
|
2178
2184
|
stream: options.streaming,
|
|
2179
2185
|
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
2180
2186
|
conversationId: this.currentConversationId,
|
|
@@ -2226,6 +2232,7 @@ export class CortexOrchestrator {
|
|
|
2226
2232
|
await this.historyStore.appendMessage(this.currentSessionId, gateAssistantMessage);
|
|
2227
2233
|
currentAssistantMessage = gateAssistantMessage;
|
|
2228
2234
|
currentAssistantCanonicalMessage = gateAssistantCanonical;
|
|
2235
|
+
lastStopReason = gateConverted.stopReason; // R153
|
|
2229
2236
|
continue;
|
|
2230
2237
|
}
|
|
2231
2238
|
}
|
|
@@ -2826,53 +2833,47 @@ export class CortexOrchestrator {
|
|
|
2826
2833
|
const continuationHistorySource = canSliceInput
|
|
2827
2834
|
? this.messageHistory.slice(this.messageCountAtLastResponse)
|
|
2828
2835
|
: this.messageHistory;
|
|
2829
|
-
const continuationCanonicalHistory = this.convertToCanonicalMessages([...continuationHistorySource]);
|
|
2830
2836
|
// §13-B2: re-evaluate the forced mentor choice on THIS continuation — thrash develops HERE
|
|
2831
2837
|
// (turnNumber ≥ 1 with accumulating failing outcomes), NOT on the initial request (turnNumber 0
|
|
2832
2838
|
// where the window is empty). Attaching it only to the initial assembly made the force unreachable.
|
|
2833
2839
|
const continuationForcedChoice = await this.resolveForcedMentorChoice(toolsToUse, toolCallIteration);
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2840
|
+
// R153: resolve the effort ONCE (the backoff/pulse counters are consumed per call, not per build).
|
|
2841
|
+
const continuationEffort = this.consumeReasoningBackoff() ?? this.consumeEffortPulse() ?? options.parameters?.reasoningEffort; // R153 backoff > effort pulse > request param > card
|
|
2842
|
+
// R154: the request is built by a closure so the image-rejection heal can rebuild it from the
|
|
2843
|
+
// (stubbed) history and retry once. Byte-identical request when no heal is needed.
|
|
2844
|
+
const buildContinuationRequest = () => {
|
|
2845
|
+
const continuationCanonicalHistory = this.convertToCanonicalMessages([...continuationHistorySource]);
|
|
2846
|
+
const req = this.gatewayTranslation.prepareRequest(continuationCanonicalHistory, toolsToUse, effectiveModel, {
|
|
2847
|
+
temperature: options.parameters?.temperature,
|
|
2848
|
+
maxTokens: options.parameters?.maxTokens,
|
|
2849
|
+
topP: options.parameters?.topP,
|
|
2850
|
+
reasoningEffort: continuationEffort,
|
|
2851
|
+
stream: options.streaming,
|
|
2852
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
2853
|
+
conversationId: this.currentConversationId, // R28b
|
|
2854
|
+
toolChoice: continuationForcedChoice // §13-B2 forced mentor tool_choice (continuation)
|
|
2855
|
+
});
|
|
2856
|
+
// Stateful Responses API: chain with previous_response_id for server-side reasoning preservation
|
|
2857
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
2858
|
+
req.previousResponseId = this.lastResponseId;
|
|
2859
|
+
}
|
|
2860
|
+
// Cache-routing conversation id
|
|
2861
|
+
req.conversationId = this.currentSessionId;
|
|
2862
|
+
// PTC: Override tools with defer_loading + PTC system tools on continuation
|
|
2863
|
+
if (isPTCEnabled && toolsToUse && toolsToUse.length > 0) {
|
|
2864
|
+
req.tools = this.gatewayTranslation.prepareToolsWithPTC(toolsToUse, effectiveModel);
|
|
2865
|
+
req.parameters.enablePTC = true;
|
|
2866
|
+
}
|
|
2867
|
+
return req;
|
|
2868
|
+
};
|
|
2848
2869
|
if (canSliceInput && this.config.debug) {
|
|
2849
2870
|
console.log(`[Orchestrator Phase 2.5] Input-sliced for previous_response_id: sent ${continuationHistorySource.length}/${this.messageHistory.length} messages`);
|
|
2850
2871
|
}
|
|
2851
|
-
// Cache-routing conversation id
|
|
2852
|
-
continuationRequest.conversationId = this.currentSessionId;
|
|
2853
|
-
// PTC: Override tools with defer_loading + PTC system tools on continuation
|
|
2854
|
-
if (isPTCEnabled && toolsToUse && toolsToUse.length > 0) {
|
|
2855
|
-
continuationRequest.tools = this.gatewayTranslation.prepareToolsWithPTC(toolsToUse, effectiveModel);
|
|
2856
|
-
continuationRequest.parameters.enablePTC = true;
|
|
2857
|
-
}
|
|
2858
2872
|
if (this.config.debug) {
|
|
2859
2873
|
console.log(`[Orchestrator Phase 2.5] Sending tool results back to model for continuation...`);
|
|
2860
2874
|
}
|
|
2861
|
-
// Send continuation request with retry logic
|
|
2862
|
-
|
|
2863
|
-
if (this.retryMiddleware) {
|
|
2864
|
-
const retryResult = await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(continuationRequest, effectiveModel), 'continuation_api_call');
|
|
2865
|
-
continuationApiResponse = retryResult.result;
|
|
2866
|
-
// Log retry information if retries occurred
|
|
2867
|
-
if (this.config.debug && retryResult.attemptCount > 1) {
|
|
2868
|
-
console.log(`[Orchestrator] Continuation API call succeeded after ${retryResult.attemptCount} attempts`);
|
|
2869
|
-
console.log(`[Orchestrator] Total retry time: ${retryResult.totalDelayMs}ms`);
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
else {
|
|
2873
|
-
// Fallback: direct call if middleware not available
|
|
2874
|
-
continuationApiResponse = await this.apiClient.sendRequest(continuationRequest, effectiveModel);
|
|
2875
|
-
}
|
|
2875
|
+
// Send continuation request with retry logic (network ladder inside; image-rejection heal around it — R154)
|
|
2876
|
+
const continuationApiResponse = await this.sendWithImageHeal(buildContinuationRequest, effectiveModel, 'continuation_api_call');
|
|
2876
2877
|
// Convert continuation response
|
|
2877
2878
|
const continuationConvertedResponse = this.gatewayTranslation.convertResponse(continuationApiResponse.data, effectiveModel, {
|
|
2878
2879
|
sessionId: this.currentSessionId,
|
|
@@ -2934,6 +2935,7 @@ export class CortexOrchestrator {
|
|
|
2934
2935
|
// Update current message for next iteration
|
|
2935
2936
|
currentAssistantMessage = continuationAssistantMessage;
|
|
2936
2937
|
currentAssistantCanonicalMessage = continuationAssistantCanonicalMessage;
|
|
2938
|
+
lastStopReason = continuationConvertedResponse.stopReason; // R153
|
|
2937
2939
|
// Phase 2.5 Day 3: Stop if too many errors accumulated
|
|
2938
2940
|
if (totalToolErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2939
2941
|
console.warn(`[Orchestrator Phase 2.5] Stopping loop due to accumulated errors`);
|
|
@@ -3112,36 +3114,33 @@ export class CortexOrchestrator {
|
|
|
3112
3114
|
this.messageHistory.push(synthUserMessage);
|
|
3113
3115
|
await this.historyStore.appendMessage(this.currentSessionId, synthUserMessage);
|
|
3114
3116
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3115
|
-
const
|
|
3116
|
-
const
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
else {
|
|
3143
|
-
synthApiResponse = await this.apiClient.sendRequest(synthRequest, effectiveModel);
|
|
3144
|
-
}
|
|
3117
|
+
const synthEffort = this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort; // R153 backoff > request param
|
|
3118
|
+
const buildSynthRequest = () => {
|
|
3119
|
+
const synthCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
3120
|
+
const req = this.gatewayTranslation.prepareRequest(synthCanonicalHistory,
|
|
3121
|
+
// Tools suppressed — the model MUST produce text, not call more tools.
|
|
3122
|
+
// NOTE (structured-output constraint, 2026-08-02): this is R29a's
|
|
3123
|
+
// plain-text escape hatch for tool-loop EXHAUSTION (30+ calls). A
|
|
3124
|
+
// jsonSchema turn that gets here degrades to text; StructuredOutput
|
|
3125
|
+
// is deliberately NOT re-forced (this terminal synth turn has no
|
|
3126
|
+
// tool-interception, so a StructuredOutput call would go uncaptured —
|
|
3127
|
+
// worse than the graceful text fallback + structuredOutput.valid=false).
|
|
3128
|
+
[], effectiveModel, {
|
|
3129
|
+
temperature: options.parameters?.temperature,
|
|
3130
|
+
maxTokens: options.parameters?.maxTokens,
|
|
3131
|
+
topP: options.parameters?.topP,
|
|
3132
|
+
reasoningEffort: synthEffort,
|
|
3133
|
+
stream: options.streaming,
|
|
3134
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
3135
|
+
conversationId: this.currentConversationId, // R28b
|
|
3136
|
+
});
|
|
3137
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
3138
|
+
req.previousResponseId = this.lastResponseId;
|
|
3139
|
+
}
|
|
3140
|
+
req.conversationId = this.currentSessionId;
|
|
3141
|
+
return req;
|
|
3142
|
+
};
|
|
3143
|
+
const synthApiResponse = await this.sendWithImageHeal(buildSynthRequest, effectiveModel, 'r29a_synthesis_retry');
|
|
3145
3144
|
const synthConverted = this.gatewayTranslation.convertResponse(synthApiResponse.data, effectiveModel, {
|
|
3146
3145
|
sessionId: this.currentSessionId,
|
|
3147
3146
|
conversationId: this.currentConversationId,
|
|
@@ -3182,6 +3181,7 @@ export class CortexOrchestrator {
|
|
|
3182
3181
|
}
|
|
3183
3182
|
currentAssistantMessage = synthAssistantMessage;
|
|
3184
3183
|
currentAssistantCanonicalMessage = synthAssistantCanonical;
|
|
3184
|
+
lastStopReason = synthConverted.stopReason; // R153
|
|
3185
3185
|
}
|
|
3186
3186
|
}
|
|
3187
3187
|
catch (synthErr) {
|
|
@@ -3777,6 +3777,7 @@ export class CortexOrchestrator {
|
|
|
3777
3777
|
throw new Error('No response message returned from provider');
|
|
3778
3778
|
}
|
|
3779
3779
|
let currentAssistantCanonicalMessage = convertedResponse.messages[0];
|
|
3780
|
+
let lastStopReason = convertedResponse.stopReason; // R153 (see non-streaming loop)
|
|
3780
3781
|
this.recordDsmlRecovery(currentAssistantCanonicalMessage);
|
|
3781
3782
|
const assistantMessageId = currentAssistantCanonicalMessage.uuid;
|
|
3782
3783
|
const assistantMessage = {
|
|
@@ -3926,7 +3927,7 @@ export class CortexOrchestrator {
|
|
|
3926
3927
|
&& (TURN_DEADLINE_MS <= 0 || (Date.now() - loopStartMs) < 0.6 * TURN_DEADLINE_MS)
|
|
3927
3928
|
&& toolCallIteration < 0.6 * MAX_TOOL_ITERATIONS;
|
|
3928
3929
|
const emptyClass = !hasVisibleText
|
|
3929
|
-
? classifyEmptyResponse(currentAssistantCanonicalMessage.content,
|
|
3930
|
+
? classifyEmptyResponse(currentAssistantCanonicalMessage.content, lastStopReason, loopHasBudget)
|
|
3930
3931
|
: null;
|
|
3931
3932
|
const isEmptyContinueKind = EMPTY_TURN_CONTINUE && !!emptyClass
|
|
3932
3933
|
&& (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active');
|
|
@@ -3942,6 +3943,7 @@ export class CortexOrchestrator {
|
|
|
3942
3943
|
}
|
|
3943
3944
|
console.warn(`[Orchestrator Streaming] R32/R18b: Empty response (${emptyClass.kind}, iteration=${toolCallIteration}). ` +
|
|
3944
3945
|
`Retrying with tools preserved.`);
|
|
3946
|
+
const exhaustionLevel = this.armReasoningBackoffIfExhausted(emptyClass, options.parameters?.reasoningEffort ?? effectiveModel?.reasoning?.effort, toolCallIteration, this.messageHistory[this.messageHistory.length - 1]?.usage?.outputTokens);
|
|
3945
3947
|
// R26 repair: ensure the empty assistant turn has content (xAI hard-400s on empty)
|
|
3946
3948
|
for (let i = this.messageHistory.length - 1; i >= 0; i--) {
|
|
3947
3949
|
const m = this.messageHistory[i];
|
|
@@ -3966,7 +3968,7 @@ export class CortexOrchestrator {
|
|
|
3966
3968
|
role: 'user',
|
|
3967
3969
|
content: [{
|
|
3968
3970
|
type: 'text',
|
|
3969
|
-
text: `<system-reminder>${
|
|
3971
|
+
text: `<system-reminder>${emptyResponseNudgeFor(emptyClass, exhaustionLevel)}${nudgeForbidsTools(emptyClass.kind) ? ' Do not call any more tools.' : ''}</system-reminder>`,
|
|
3970
3972
|
}],
|
|
3971
3973
|
},
|
|
3972
3974
|
timeline: {
|
|
@@ -3989,7 +3991,7 @@ export class CortexOrchestrator {
|
|
|
3989
3991
|
temperature: options.parameters?.temperature,
|
|
3990
3992
|
maxTokens: options.parameters?.maxTokens,
|
|
3991
3993
|
topP: options.parameters?.topP,
|
|
3992
|
-
reasoningEffort: options.parameters?.reasoningEffort,
|
|
3994
|
+
reasoningEffort: this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort, // R153 backoff > request param
|
|
3993
3995
|
stream: false,
|
|
3994
3996
|
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
3995
3997
|
conversationId: this.currentConversationId,
|
|
@@ -4040,6 +4042,7 @@ export class CortexOrchestrator {
|
|
|
4040
4042
|
this.messageHistory.push(retryAssistantMessage);
|
|
4041
4043
|
await this.historyStore.appendMessage(this.currentSessionId, retryAssistantMessage);
|
|
4042
4044
|
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
4045
|
+
lastStopReason = retryConverted.stopReason; // R153
|
|
4043
4046
|
// Yield any text from the retry as streaming chunks
|
|
4044
4047
|
const retryText = retryAssistantCanonical.content
|
|
4045
4048
|
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
@@ -4119,7 +4122,7 @@ export class CortexOrchestrator {
|
|
|
4119
4122
|
temperature: options.parameters?.temperature,
|
|
4120
4123
|
maxTokens: options.parameters?.maxTokens,
|
|
4121
4124
|
topP: options.parameters?.topP,
|
|
4122
|
-
reasoningEffort: options.parameters?.reasoningEffort,
|
|
4125
|
+
reasoningEffort: this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort, // R153 backoff > request param
|
|
4123
4126
|
stream: false,
|
|
4124
4127
|
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
4125
4128
|
conversationId: this.currentConversationId,
|
|
@@ -4170,6 +4173,7 @@ export class CortexOrchestrator {
|
|
|
4170
4173
|
this.messageHistory.push(retryAssistantMessage);
|
|
4171
4174
|
await this.historyStore.appendMessage(this.currentSessionId, retryAssistantMessage);
|
|
4172
4175
|
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
4176
|
+
lastStopReason = retryConverted.stopReason; // R153
|
|
4173
4177
|
const retryText = retryAssistantCanonical.content
|
|
4174
4178
|
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
4175
4179
|
.map((b) => b.text)
|
|
@@ -4603,7 +4607,7 @@ export class CortexOrchestrator {
|
|
|
4603
4607
|
temperature: options.parameters?.temperature,
|
|
4604
4608
|
maxTokens: options.parameters?.maxTokens,
|
|
4605
4609
|
topP: options.parameters?.topP,
|
|
4606
|
-
reasoningEffort: this.consumeEffortPulse() ?? options.parameters?.reasoningEffort, // effort pulse > request param > card
|
|
4610
|
+
reasoningEffort: this.consumeReasoningBackoff() ?? this.consumeEffortPulse() ?? options.parameters?.reasoningEffort, // R153 backoff > effort pulse > request param > card
|
|
4607
4611
|
stream: true, // STREAMING continuation
|
|
4608
4612
|
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
4609
4613
|
conversationId: this.currentConversationId, // R28b
|
|
@@ -4683,8 +4687,9 @@ export class CortexOrchestrator {
|
|
|
4683
4687
|
}
|
|
4684
4688
|
const finishReason = continuationProviderMessage.choices[0]?.finish_reason;
|
|
4685
4689
|
if (finishReason === 'length') {
|
|
4686
|
-
|
|
4687
|
-
|
|
4690
|
+
// R153: do NOT leave the loop — the empty-response classifier below sees `truncated` (via lastStopReason)
|
|
4691
|
+
// and continues with the effort backoff; a partial text answer simply ends the turn as before.
|
|
4692
|
+
console.warn(`[Orchestrator Streaming] Chat Completions API hit max tokens (finish_reason=length) — handing to the truncation path.`);
|
|
4688
4693
|
}
|
|
4689
4694
|
}
|
|
4690
4695
|
// Convert continuation response
|
|
@@ -4740,6 +4745,7 @@ export class CortexOrchestrator {
|
|
|
4740
4745
|
}
|
|
4741
4746
|
// Update for next iteration
|
|
4742
4747
|
currentAssistantCanonicalMessage = continuationAssistantCanonicalMessage;
|
|
4748
|
+
lastStopReason = continuationConvertedResponse.stopReason; // R153
|
|
4743
4749
|
// Check if continuation has more tool uses
|
|
4744
4750
|
const continuationToolUses = continuationAssistantCanonicalMessage.content
|
|
4745
4751
|
.filter((block) => block.type === 'tool_use')
|
|
@@ -4942,7 +4948,7 @@ export class CortexOrchestrator {
|
|
|
4942
4948
|
temperature: options.parameters?.temperature,
|
|
4943
4949
|
maxTokens: options.parameters?.maxTokens,
|
|
4944
4950
|
topP: options.parameters?.topP,
|
|
4945
|
-
reasoningEffort: options.parameters?.reasoningEffort,
|
|
4951
|
+
reasoningEffort: this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort, // R153 backoff > request param
|
|
4946
4952
|
stream: false,
|
|
4947
4953
|
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
4948
4954
|
conversationId: this.currentConversationId, // R28b
|
|
@@ -4998,6 +5004,7 @@ export class CortexOrchestrator {
|
|
|
4998
5004
|
await this.historyStore.appendMessage(this.currentSessionId, synthAssistantMessage);
|
|
4999
5005
|
this.sessionTimeline.recordMessage(synthAssistantMessage.uuid, 'assistant');
|
|
5000
5006
|
currentAssistantCanonicalMessage = synthCanonical;
|
|
5007
|
+
lastStopReason = synthConverted.stopReason; // R153
|
|
5001
5008
|
if (synthConverted.usage) {
|
|
5002
5009
|
this.cacheMetricsAccumulator.addUsage(synthConverted.usage, effectiveModel.provider);
|
|
5003
5010
|
this.noteRequestUsage(synthConverted.usage); // R132
|
|
@@ -8166,6 +8173,61 @@ export class CortexOrchestrator {
|
|
|
8166
8173
|
this.effortPulseRemaining -= 1;
|
|
8167
8174
|
return (process.env.CORTEX_EFFORT_PULSE_LEVEL || 'high');
|
|
8168
8175
|
}
|
|
8176
|
+
/** R153 HB-REASONING-EXHAUSTION: continuations left at a LOWERED reasoning effort after a truncated
|
|
8177
|
+
* reasoning-only turn (output cap hit, nothing delivered). Takes precedence over the effort pulse. */
|
|
8178
|
+
reasoningBackoffRemaining = 0;
|
|
8179
|
+
reasoningBackoffLevel;
|
|
8180
|
+
consumeReasoningBackoff() {
|
|
8181
|
+
if (this.reasoningBackoffRemaining <= 0)
|
|
8182
|
+
return undefined;
|
|
8183
|
+
this.reasoningBackoffRemaining -= 1;
|
|
8184
|
+
return this.reasoningBackoffLevel;
|
|
8185
|
+
}
|
|
8186
|
+
/** Arms the backoff when the empty turn is a reasoning exhaustion; returns the level the next calls will use. */
|
|
8187
|
+
armReasoningBackoffIfExhausted(cls, effectiveEffort, iteration, outputTokens) {
|
|
8188
|
+
if (!isReasoningExhaustion(cls))
|
|
8189
|
+
return undefined;
|
|
8190
|
+
const cfg = resolveReasoningExhaustBackoff();
|
|
8191
|
+
const store = this.getDecisionStore();
|
|
8192
|
+
if (!cfg.enabled) {
|
|
8193
|
+
console.warn(`[Orchestrator] R153: reasoning exhaustion at iteration ${iteration} (output cap hit, nothing delivered) — backoff disabled (CORTEX_REASONING_EXHAUST_BACKOFF=false).`);
|
|
8194
|
+
if (store)
|
|
8195
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'reasoning_exhaustion', detail: { iteration, outputTokens, backoff: false } }).catch(() => { });
|
|
8196
|
+
return undefined;
|
|
8197
|
+
}
|
|
8198
|
+
// A repeat exhaustion while a backoff is already armed steps down AGAIN (high → medium → low).
|
|
8199
|
+
const base = this.reasoningBackoffRemaining > 0 && this.reasoningBackoffLevel ? this.reasoningBackoffLevel : effectiveEffort;
|
|
8200
|
+
const level = stepDownEffort(base);
|
|
8201
|
+
this.reasoningBackoffLevel = level;
|
|
8202
|
+
this.reasoningBackoffRemaining = cfg.turns;
|
|
8203
|
+
console.warn(`[Orchestrator] R153: reasoning exhaustion at iteration ${iteration} (${outputTokens ?? '?'} output tokens, no text/tool) — next ${cfg.turns} continuation(s) at effort '${level}' (was '${base ?? 'card default'}').`);
|
|
8204
|
+
if (store)
|
|
8205
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'reasoning_exhaustion', detail: { iteration, outputTokens, from: base ?? null, level, turns: cfg.turns } }).catch(() => { });
|
|
8206
|
+
return level;
|
|
8207
|
+
}
|
|
8208
|
+
/** R154 HB-IMAGE-REJECTION-HEAL: send a request built from messageHistory; if the provider rejects an image
|
|
8209
|
+
* block (4xx "unsupported image"), stub every user-side image block and retry the SAME build once. */
|
|
8210
|
+
async sendWithImageHeal(build, model, label) {
|
|
8211
|
+
const send = async (req) => this.retryMiddleware
|
|
8212
|
+
? (await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(req, model), label)).result
|
|
8213
|
+
: this.apiClient.sendRequest(req, model);
|
|
8214
|
+
try {
|
|
8215
|
+
return await send(build());
|
|
8216
|
+
}
|
|
8217
|
+
catch (err) {
|
|
8218
|
+
if (!isImageRejectionError(err))
|
|
8219
|
+
throw err;
|
|
8220
|
+
const reason = String(err?.message ?? err).slice(0, 300);
|
|
8221
|
+
const stripped = stripRejectedImages(this.messageHistory, reason);
|
|
8222
|
+
if (stripped === 0)
|
|
8223
|
+
throw err;
|
|
8224
|
+
console.warn(`[Orchestrator] R154: provider rejected an image block on ${label} — stubbed ${stripped} image block(s) in history and retrying once.`);
|
|
8225
|
+
const store = this.getDecisionStore();
|
|
8226
|
+
if (store)
|
|
8227
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'image_rejection_heal', detail: { stripped, label, reason: reason.slice(0, 160) } }).catch(() => { });
|
|
8228
|
+
return await send(build());
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8169
8231
|
/** Honored AskForAdvice consults per session (rate-limit + rung state; persists across turns). */
|
|
8170
8232
|
mentorConsultCounts = new Map();
|
|
8171
8233
|
/**
|