@nexus-cortex/core 4.108.22 → 4.110.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 +56 -0
- package/dist/config/RuntimeConfigRegistry.d.ts.map +1 -1
- package/dist/config/RuntimeConfigRegistry.js +5 -0
- package/dist/config/RuntimeConfigRegistry.js.map +1 -1
- package/dist/config/SettingsLoader.d.ts.map +1 -1
- package/dist/config/SettingsLoader.js +5 -0
- package/dist/config/SettingsLoader.js.map +1 -1
- package/dist/config/SettingsSchema.d.ts +5 -0
- package/dist/config/SettingsSchema.d.ts.map +1 -1
- package/dist/config/SettingsSchema.js +45 -0
- package/dist/config/SettingsSchema.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts.map +1 -1
- package/dist/config/effectiveConfig.js +5 -0
- package/dist/config/effectiveConfig.js.map +1 -1
- package/dist/middleware/helpers/adapters/ChatCompletionsAPIHelperAdapter.d.ts.map +1 -1
- package/dist/middleware/helpers/adapters/ChatCompletionsAPIHelperAdapter.js +3 -1
- package/dist/middleware/helpers/adapters/ChatCompletionsAPIHelperAdapter.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 +270 -175
- 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/tools/registries/BaseToolRegistry.d.ts.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.js +1 -0
- package/dist/tools/registries/BaseToolRegistry.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/training/mentorRole.d.ts +2 -0
- package/dist/training/mentorRole.d.ts.map +1 -1
- package/dist/training/mentorRole.js +7 -2
- package/dist/training/mentorRole.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: {
|
|
@@ -3984,28 +3986,25 @@ export class CortexOrchestrator {
|
|
|
3984
3986
|
await this.historyStore.appendMessage(this.currentSessionId, followupUserMessage);
|
|
3985
3987
|
try {
|
|
3986
3988
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3987
|
-
const
|
|
3988
|
-
const
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
else {
|
|
4007
|
-
retryApiResponse = await this.apiClient.sendRequest(retryRequest, effectiveModel);
|
|
4008
|
-
}
|
|
3989
|
+
const retryEffort = this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort; // R153 backoff > request param
|
|
3990
|
+
const buildRetryRequest = () => {
|
|
3991
|
+
const retryCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
3992
|
+
const req = this.gatewayTranslation.prepareRequest(retryCanonicalHistory, toolsToUse, effectiveModel, {
|
|
3993
|
+
temperature: options.parameters?.temperature,
|
|
3994
|
+
maxTokens: options.parameters?.maxTokens,
|
|
3995
|
+
topP: options.parameters?.topP,
|
|
3996
|
+
reasoningEffort: retryEffort,
|
|
3997
|
+
stream: false,
|
|
3998
|
+
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
3999
|
+
conversationId: this.currentConversationId,
|
|
4000
|
+
});
|
|
4001
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
4002
|
+
req.previousResponseId = this.lastResponseId;
|
|
4003
|
+
}
|
|
4004
|
+
req.conversationId = this.currentSessionId;
|
|
4005
|
+
return req;
|
|
4006
|
+
};
|
|
4007
|
+
const retryApiResponse = await this.sendWithImageHeal(buildRetryRequest, effectiveModel, 'r32_stream_empty_retry');
|
|
4009
4008
|
const retryConverted = this.gatewayTranslation.convertResponse(retryApiResponse.data, effectiveModel, {
|
|
4010
4009
|
sessionId: this.currentSessionId,
|
|
4011
4010
|
conversationId: this.currentConversationId,
|
|
@@ -4040,6 +4039,7 @@ export class CortexOrchestrator {
|
|
|
4040
4039
|
this.messageHistory.push(retryAssistantMessage);
|
|
4041
4040
|
await this.historyStore.appendMessage(this.currentSessionId, retryAssistantMessage);
|
|
4042
4041
|
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
4042
|
+
lastStopReason = retryConverted.stopReason; // R153
|
|
4043
4043
|
// Yield any text from the retry as streaming chunks
|
|
4044
4044
|
const retryText = retryAssistantCanonical.content
|
|
4045
4045
|
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
@@ -4119,7 +4119,7 @@ export class CortexOrchestrator {
|
|
|
4119
4119
|
temperature: options.parameters?.temperature,
|
|
4120
4120
|
maxTokens: options.parameters?.maxTokens,
|
|
4121
4121
|
topP: options.parameters?.topP,
|
|
4122
|
-
reasoningEffort: options.parameters?.reasoningEffort,
|
|
4122
|
+
reasoningEffort: this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort, // R153 backoff > request param
|
|
4123
4123
|
stream: false,
|
|
4124
4124
|
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
4125
4125
|
conversationId: this.currentConversationId,
|
|
@@ -4170,6 +4170,7 @@ export class CortexOrchestrator {
|
|
|
4170
4170
|
this.messageHistory.push(retryAssistantMessage);
|
|
4171
4171
|
await this.historyStore.appendMessage(this.currentSessionId, retryAssistantMessage);
|
|
4172
4172
|
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
4173
|
+
lastStopReason = retryConverted.stopReason; // R153
|
|
4173
4174
|
const retryText = retryAssistantCanonical.content
|
|
4174
4175
|
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
4175
4176
|
.map((b) => b.text)
|
|
@@ -4592,25 +4593,30 @@ export class CortexOrchestrator {
|
|
|
4592
4593
|
const streamContinuationHistorySource = streamCanSliceInput
|
|
4593
4594
|
? this.messageHistory.slice(this.messageCountAtLastResponse)
|
|
4594
4595
|
: this.messageHistory;
|
|
4595
|
-
const continuationCanonicalHistory = this.convertToCanonicalMessages([...streamContinuationHistorySource]);
|
|
4596
4596
|
// §13-B2: re-evaluate the forced mentor choice on THIS streaming continuation — thrash
|
|
4597
4597
|
// develops here, not on the initial request (mirror of the non-stream continuation fix).
|
|
4598
4598
|
const continuationForcedChoice = await this.resolveForcedMentorChoice(toolsToUse, toolCallIteration);
|
|
4599
|
-
|
|
4599
|
+
const streamContinuationEffort = this.consumeReasoningBackoff() ?? this.consumeEffortPulse() ?? options.parameters?.reasoningEffort; // R153 backoff > effort pulse > request param > card
|
|
4600
|
+
// Phase 2.8: Prepare continuation request (R154: built by a closure so the image-rejection heal can
|
|
4601
|
+
// rebuild it from the stubbed history and re-stream once; byte-identical request otherwise).
|
|
4600
4602
|
// All providers maintain thinking throughout multi-turn execution
|
|
4601
|
-
const
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4603
|
+
const buildStreamContinuationRequest = () => {
|
|
4604
|
+
const continuationCanonicalHistory = this.convertToCanonicalMessages([...streamContinuationHistorySource]);
|
|
4605
|
+
return this.gatewayTranslation.prepareRequest(continuationCanonicalHistory, toolsToUse, // Updated tools including SearchTools discoveries
|
|
4606
|
+
effectiveModel, {
|
|
4607
|
+
temperature: options.parameters?.temperature,
|
|
4608
|
+
maxTokens: options.parameters?.maxTokens,
|
|
4609
|
+
topP: options.parameters?.topP,
|
|
4610
|
+
reasoningEffort: streamContinuationEffort,
|
|
4611
|
+
stream: true, // STREAMING continuation
|
|
4612
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
4613
|
+
conversationId: this.currentConversationId, // R28b
|
|
4614
|
+
toolChoice: continuationForcedChoice // §13-B2 forced mentor tool_choice (continuation)
|
|
4615
|
+
// Thinking enabled for all providers in continuations
|
|
4616
|
+
// This allows interleaved thinking during tool execution
|
|
4617
|
+
});
|
|
4618
|
+
};
|
|
4619
|
+
const continuationRequest = buildStreamContinuationRequest();
|
|
4614
4620
|
// Stateful Responses API: chain with previous_response_id for server-side reasoning preservation
|
|
4615
4621
|
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
4616
4622
|
continuationRequest.previousResponseId = this.lastResponseId;
|
|
@@ -4628,18 +4634,41 @@ export class CortexOrchestrator {
|
|
|
4628
4634
|
if (this.config.debug) {
|
|
4629
4635
|
console.log(`[Orchestrator Streaming] Sending tool results back to model for continuation (iteration ${toolCallIteration})...`);
|
|
4630
4636
|
}
|
|
4631
|
-
// NEW: Stream continuation response
|
|
4632
|
-
const continuationStreamingResponse = this.apiClient.streamRequest(continuationRequest, effectiveModel);
|
|
4633
4637
|
// Yield a marker so CLI knows this is a continuation
|
|
4634
4638
|
if (process.env.DEBUG_THINKING === 'true') {
|
|
4635
4639
|
console.log(`\n[DEBUG Orchestrator] === CONTINUATION ${toolCallIteration} STARTING ===`);
|
|
4636
4640
|
}
|
|
4637
|
-
//
|
|
4638
|
-
|
|
4639
|
-
|
|
4641
|
+
// NEW: Stream continuation response. R154: one heal attempt — if the provider rejects an image block
|
|
4642
|
+
// (a request-shape 4xx raised before any chunk arrives), stub the image blocks and re-stream the
|
|
4643
|
+
// rebuilt request once; every other error propagates exactly as before.
|
|
4644
|
+
let continuationProviderMessage;
|
|
4645
|
+
for (let healAttempt = 0;; healAttempt++) {
|
|
4646
|
+
const streamReq = healAttempt === 0 ? continuationRequest : buildStreamContinuationRequest();
|
|
4647
|
+
const continuationStreamingResponse = this.apiClient.streamRequest(streamReq, effectiveModel);
|
|
4648
|
+
try {
|
|
4649
|
+
// Yield continuation chunks in real-time
|
|
4650
|
+
for await (const chunk of continuationStreamingResponse.chunks) {
|
|
4651
|
+
yield chunk;
|
|
4652
|
+
}
|
|
4653
|
+
// Get final continuation message
|
|
4654
|
+
continuationProviderMessage = await continuationStreamingResponse.finalMessage;
|
|
4655
|
+
break;
|
|
4656
|
+
}
|
|
4657
|
+
catch (streamErr) {
|
|
4658
|
+
if (healAttempt === 0 && isImageRejectionError(streamErr)) {
|
|
4659
|
+
const reason = String(streamErr?.message ?? streamErr).slice(0, 300);
|
|
4660
|
+
const stripped = stripRejectedImages(this.messageHistory, reason);
|
|
4661
|
+
if (stripped > 0) {
|
|
4662
|
+
console.warn(`[Orchestrator Streaming] R154: provider rejected an image block on the continuation — stubbed ${stripped} image block(s) in history and re-streaming once.`);
|
|
4663
|
+
const healStore = this.getDecisionStore();
|
|
4664
|
+
if (healStore)
|
|
4665
|
+
void healStore.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'image_rejection_heal', detail: { stripped, label: 'stream_continuation', reason: reason.slice(0, 160) } }).catch(() => { });
|
|
4666
|
+
continue;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
throw streamErr;
|
|
4670
|
+
}
|
|
4640
4671
|
}
|
|
4641
|
-
// Get final continuation message
|
|
4642
|
-
const continuationProviderMessage = await continuationStreamingResponse.finalMessage;
|
|
4643
4672
|
// Validate continuation response (API pattern-aware)
|
|
4644
4673
|
if (!continuationProviderMessage) {
|
|
4645
4674
|
console.warn(`[Orchestrator Streaming] Continuation response is null/undefined. Stopping loop.`);
|
|
@@ -4683,8 +4712,9 @@ export class CortexOrchestrator {
|
|
|
4683
4712
|
}
|
|
4684
4713
|
const finishReason = continuationProviderMessage.choices[0]?.finish_reason;
|
|
4685
4714
|
if (finishReason === 'length') {
|
|
4686
|
-
|
|
4687
|
-
|
|
4715
|
+
// R153: do NOT leave the loop — the empty-response classifier below sees `truncated` (via lastStopReason)
|
|
4716
|
+
// and continues with the effort backoff; a partial text answer simply ends the turn as before.
|
|
4717
|
+
console.warn(`[Orchestrator Streaming] Chat Completions API hit max tokens (finish_reason=length) — handing to the truncation path.`);
|
|
4688
4718
|
}
|
|
4689
4719
|
}
|
|
4690
4720
|
// Convert continuation response
|
|
@@ -4740,6 +4770,7 @@ export class CortexOrchestrator {
|
|
|
4740
4770
|
}
|
|
4741
4771
|
// Update for next iteration
|
|
4742
4772
|
currentAssistantCanonicalMessage = continuationAssistantCanonicalMessage;
|
|
4773
|
+
lastStopReason = continuationConvertedResponse.stopReason; // R153
|
|
4743
4774
|
// Check if continuation has more tool uses
|
|
4744
4775
|
const continuationToolUses = continuationAssistantCanonicalMessage.content
|
|
4745
4776
|
.filter((block) => block.type === 'tool_use')
|
|
@@ -4936,29 +4967,26 @@ export class CortexOrchestrator {
|
|
|
4936
4967
|
this.messageHistory.push(synthUserMessage);
|
|
4937
4968
|
await this.historyStore.appendMessage(this.currentSessionId, synthUserMessage);
|
|
4938
4969
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
4939
|
-
const
|
|
4940
|
-
const
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
else {
|
|
4960
|
-
synthApiResponse = await this.apiClient.sendRequest(synthRequest, effectiveModel);
|
|
4961
|
-
}
|
|
4970
|
+
const synthEffort = this.consumeReasoningBackoff() ?? options.parameters?.reasoningEffort; // R153 backoff > request param
|
|
4971
|
+
const buildSynthRequest = () => {
|
|
4972
|
+
const synthCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
4973
|
+
const req = this.gatewayTranslation.prepareRequest(synthCanonicalHistory, [], // tools suppressed — must produce text
|
|
4974
|
+
effectiveModel, {
|
|
4975
|
+
temperature: options.parameters?.temperature,
|
|
4976
|
+
maxTokens: options.parameters?.maxTokens,
|
|
4977
|
+
topP: options.parameters?.topP,
|
|
4978
|
+
reasoningEffort: synthEffort,
|
|
4979
|
+
stream: false,
|
|
4980
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
4981
|
+
conversationId: this.currentConversationId, // R28b
|
|
4982
|
+
});
|
|
4983
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
4984
|
+
req.previousResponseId = this.lastResponseId;
|
|
4985
|
+
}
|
|
4986
|
+
req.conversationId = this.currentSessionId;
|
|
4987
|
+
return req;
|
|
4988
|
+
};
|
|
4989
|
+
const synthApiResponse = await this.sendWithImageHeal(buildSynthRequest, effectiveModel, 'r29a_stream_synthesis_retry');
|
|
4962
4990
|
const synthConverted = this.gatewayTranslation.convertResponse(synthApiResponse.data, effectiveModel, {
|
|
4963
4991
|
sessionId: this.currentSessionId,
|
|
4964
4992
|
conversationId: this.currentConversationId,
|
|
@@ -4998,6 +5026,7 @@ export class CortexOrchestrator {
|
|
|
4998
5026
|
await this.historyStore.appendMessage(this.currentSessionId, synthAssistantMessage);
|
|
4999
5027
|
this.sessionTimeline.recordMessage(synthAssistantMessage.uuid, 'assistant');
|
|
5000
5028
|
currentAssistantCanonicalMessage = synthCanonical;
|
|
5029
|
+
lastStopReason = synthConverted.stopReason; // R153
|
|
5001
5030
|
if (synthConverted.usage) {
|
|
5002
5031
|
this.cacheMetricsAccumulator.addUsage(synthConverted.usage, effectiveModel.provider);
|
|
5003
5032
|
this.noteRequestUsage(synthConverted.usage); // R132
|
|
@@ -7393,6 +7422,10 @@ export class CortexOrchestrator {
|
|
|
7393
7422
|
// callbacks set after listener setup still work
|
|
7394
7423
|
const getCallback = () => this.config.onSubAgentEvent;
|
|
7395
7424
|
// Emit event via callback or console.log
|
|
7425
|
+
// 2026-09-16 (tb4-flash-v3 vllm-deepseek-streaming, 500+ ticker lines): the child heartbeats `progress` once a
|
|
7426
|
+
// SECOND; the console fallback used to print every beat. Print a beat only when the turn number changes or
|
|
7427
|
+
// 30 s have passed since the last printed one (the callback path is untouched — UIs want every beat).
|
|
7428
|
+
const progressLog = { turn: -1, at: 0 };
|
|
7396
7429
|
const emitEvent = (event) => {
|
|
7397
7430
|
const callback = getCallback();
|
|
7398
7431
|
if (callback) {
|
|
@@ -7404,9 +7437,16 @@ export class CortexOrchestrator {
|
|
|
7404
7437
|
case 'started':
|
|
7405
7438
|
console.log(`\n${prefix} Started (model: ${event.data.model})`);
|
|
7406
7439
|
break;
|
|
7407
|
-
case 'progress':
|
|
7408
|
-
|
|
7440
|
+
case 'progress': {
|
|
7441
|
+
const turn = event.data.turnNumber ?? -1;
|
|
7442
|
+
const now = Date.now();
|
|
7443
|
+
if (turn !== progressLog.turn || now - progressLog.at >= 30_000) {
|
|
7444
|
+
progressLog.turn = turn;
|
|
7445
|
+
progressLog.at = now;
|
|
7446
|
+
console.log(`${prefix} Turn ${event.data.turnNumber} (${(event.data.elapsedMs / 1000).toFixed(1)}s, ${event.data.totalTokens} tokens)`);
|
|
7447
|
+
}
|
|
7409
7448
|
break;
|
|
7449
|
+
}
|
|
7410
7450
|
case 'tool_call':
|
|
7411
7451
|
console.log(`${prefix} Using: ${event.data.toolName}${event.data.toolSummary || ''}`);
|
|
7412
7452
|
break;
|
|
@@ -8166,6 +8206,61 @@ export class CortexOrchestrator {
|
|
|
8166
8206
|
this.effortPulseRemaining -= 1;
|
|
8167
8207
|
return (process.env.CORTEX_EFFORT_PULSE_LEVEL || 'high');
|
|
8168
8208
|
}
|
|
8209
|
+
/** R153 HB-REASONING-EXHAUSTION: continuations left at a LOWERED reasoning effort after a truncated
|
|
8210
|
+
* reasoning-only turn (output cap hit, nothing delivered). Takes precedence over the effort pulse. */
|
|
8211
|
+
reasoningBackoffRemaining = 0;
|
|
8212
|
+
reasoningBackoffLevel;
|
|
8213
|
+
consumeReasoningBackoff() {
|
|
8214
|
+
if (this.reasoningBackoffRemaining <= 0)
|
|
8215
|
+
return undefined;
|
|
8216
|
+
this.reasoningBackoffRemaining -= 1;
|
|
8217
|
+
return this.reasoningBackoffLevel;
|
|
8218
|
+
}
|
|
8219
|
+
/** Arms the backoff when the empty turn is a reasoning exhaustion; returns the level the next calls will use. */
|
|
8220
|
+
armReasoningBackoffIfExhausted(cls, effectiveEffort, iteration, outputTokens) {
|
|
8221
|
+
if (!isReasoningExhaustion(cls))
|
|
8222
|
+
return undefined;
|
|
8223
|
+
const cfg = resolveReasoningExhaustBackoff();
|
|
8224
|
+
const store = this.getDecisionStore();
|
|
8225
|
+
if (!cfg.enabled) {
|
|
8226
|
+
console.warn(`[Orchestrator] R153: reasoning exhaustion at iteration ${iteration} (output cap hit, nothing delivered) — backoff disabled (CORTEX_REASONING_EXHAUST_BACKOFF=false).`);
|
|
8227
|
+
if (store)
|
|
8228
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'reasoning_exhaustion', detail: { iteration, outputTokens, backoff: false } }).catch(() => { });
|
|
8229
|
+
return undefined;
|
|
8230
|
+
}
|
|
8231
|
+
// A repeat exhaustion while a backoff is already armed steps down AGAIN (high → medium → low).
|
|
8232
|
+
const base = this.reasoningBackoffRemaining > 0 && this.reasoningBackoffLevel ? this.reasoningBackoffLevel : effectiveEffort;
|
|
8233
|
+
const level = stepDownEffort(base);
|
|
8234
|
+
this.reasoningBackoffLevel = level;
|
|
8235
|
+
this.reasoningBackoffRemaining = cfg.turns;
|
|
8236
|
+
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'}').`);
|
|
8237
|
+
if (store)
|
|
8238
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'reasoning_exhaustion', detail: { iteration, outputTokens, from: base ?? null, level, turns: cfg.turns } }).catch(() => { });
|
|
8239
|
+
return level;
|
|
8240
|
+
}
|
|
8241
|
+
/** R154 HB-IMAGE-REJECTION-HEAL: send a request built from messageHistory; if the provider rejects an image
|
|
8242
|
+
* block (4xx "unsupported image"), stub every user-side image block and retry the SAME build once. */
|
|
8243
|
+
async sendWithImageHeal(build, model, label) {
|
|
8244
|
+
const send = async (req) => this.retryMiddleware
|
|
8245
|
+
? (await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(req, model), label)).result
|
|
8246
|
+
: this.apiClient.sendRequest(req, model);
|
|
8247
|
+
try {
|
|
8248
|
+
return await send(build());
|
|
8249
|
+
}
|
|
8250
|
+
catch (err) {
|
|
8251
|
+
if (!isImageRejectionError(err))
|
|
8252
|
+
throw err;
|
|
8253
|
+
const reason = String(err?.message ?? err).slice(0, 300);
|
|
8254
|
+
const stripped = stripRejectedImages(this.messageHistory, reason);
|
|
8255
|
+
if (stripped === 0)
|
|
8256
|
+
throw err;
|
|
8257
|
+
console.warn(`[Orchestrator] R154: provider rejected an image block on ${label} — stubbed ${stripped} image block(s) in history and retrying once.`);
|
|
8258
|
+
const store = this.getDecisionStore();
|
|
8259
|
+
if (store)
|
|
8260
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'image_rejection_heal', detail: { stripped, label, reason: reason.slice(0, 160) } }).catch(() => { });
|
|
8261
|
+
return await send(build());
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8169
8264
|
/** Honored AskForAdvice consults per session (rate-limit + rung state; persists across turns). */
|
|
8170
8265
|
mentorConsultCounts = new Map();
|
|
8171
8266
|
/**
|