@nexus-cortex/core 4.95.0 → 4.97.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 +15 -0
- package/dist/config/effectiveConfig.d.ts.map +1 -1
- package/dist/config/effectiveConfig.js +1 -0
- package/dist/config/effectiveConfig.js.map +1 -1
- package/dist/models/cards/deepseek/deepseek-v4-1-flash.d.ts +19 -0
- package/dist/models/cards/deepseek/deepseek-v4-1-flash.d.ts.map +1 -0
- package/dist/models/cards/deepseek/deepseek-v4-1-flash.js +40 -0
- package/dist/models/cards/deepseek/deepseek-v4-1-flash.js.map +1 -0
- package/dist/models/cards/deepseek/index.d.ts +1 -0
- package/dist/models/cards/deepseek/index.d.ts.map +1 -1
- package/dist/models/cards/deepseek/index.js +1 -0
- package/dist/models/cards/deepseek/index.js.map +1 -1
- package/dist/models/cards/mercury/index.d.ts +1 -0
- package/dist/models/cards/mercury/index.d.ts.map +1 -1
- package/dist/models/cards/mercury/index.js +4 -0
- package/dist/models/cards/mercury/index.js.map +1 -1
- package/dist/models/cards/mercury/mercury-2-5.d.ts +28 -0
- package/dist/models/cards/mercury/mercury-2-5.d.ts.map +1 -0
- package/dist/models/cards/mercury/mercury-2-5.js +37 -0
- package/dist/models/cards/mercury/mercury-2-5.js.map +1 -0
- package/dist/models/registry/ModularModelRegistry.d.ts.map +1 -1
- package/dist/models/registry/ModularModelRegistry.js +3 -1
- package/dist/models/registry/ModularModelRegistry.js.map +1 -1
- package/dist/orchestrator/APIClient.d.ts.map +1 -1
- package/dist/orchestrator/APIClient.js +17 -0
- package/dist/orchestrator/APIClient.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +6 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +118 -15
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/dsmlRecovery.d.ts +40 -0
- package/dist/orchestrator/dsmlRecovery.d.ts.map +1 -0
- package/dist/orchestrator/dsmlRecovery.js +106 -0
- package/dist/orchestrator/dsmlRecovery.js.map +1 -0
- package/dist/orchestrator/emptyResponseClassifier.d.ts +17 -4
- package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -1
- package/dist/orchestrator/emptyResponseClassifier.js +26 -9
- package/dist/orchestrator/emptyResponseClassifier.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/package.json +3 -3
|
@@ -1518,6 +1518,7 @@ export class CortexOrchestrator {
|
|
|
1518
1518
|
// Keep executing tools until model returns a text response
|
|
1519
1519
|
let currentAssistantMessage = assistantMessage;
|
|
1520
1520
|
let currentAssistantCanonicalMessage = assistantCanonicalMessage;
|
|
1521
|
+
this.recordDsmlRecovery(currentAssistantCanonicalMessage);
|
|
1521
1522
|
let toolCallIteration = 0;
|
|
1522
1523
|
// Loop control settings — single source of truth via getLoopControlConfig()
|
|
1523
1524
|
const loopDefaults = this.getLoopControlConfig();
|
|
@@ -1548,6 +1549,12 @@ export class CortexOrchestrator {
|
|
|
1548
1549
|
// Round 18b: track whether we've already retried on empty response so
|
|
1549
1550
|
// we don't loop forever. See empty-detection block below.
|
|
1550
1551
|
let emptyResponseRetryUsed = false;
|
|
1552
|
+
// HB-ENDTURN-TERMINAL (2026-09-08): dark-lever gate + a SEPARATE bounded counter for the
|
|
1553
|
+
// continue-kinds (truncated / reasoning_only_active). Those re-enter the loop with tools rather
|
|
1554
|
+
// than burning the one-shot force-answer budget; capped so an empty-turn loop still terminates.
|
|
1555
|
+
const EMPTY_TURN_CONTINUE = loopDefaults.emptyTurnContinue;
|
|
1556
|
+
const EMPTY_CONTINUE_MAX = 3;
|
|
1557
|
+
let emptyContinueCount = 0;
|
|
1551
1558
|
// Inaction guard (backlog item 2 — the ladder's inverse): single-nudge
|
|
1552
1559
|
// bound for the actless-verbose retry. Default OFF via env.
|
|
1553
1560
|
let inactionNudgeUsed = false;
|
|
@@ -1653,16 +1660,32 @@ export class CortexOrchestrator {
|
|
|
1653
1660
|
// after its final tool call. Nudging it for "no text" would burn an
|
|
1654
1661
|
// extra round-trip (observed with haiku in the structured-output
|
|
1655
1662
|
// canary). The structured result IS the deliverable, so skip R18b.
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1663
|
+
// HB-ENDTURN-TERMINAL (2026-09-08): classify FIRST so retry policy keys on the kind.
|
|
1664
|
+
// loopHasBudget (dark-gated by CORTEX_EMPTY_TURN_CONTINUE) splits a reasoning-only empty turn
|
|
1665
|
+
// into reasoning_only_active (mid-recon → continue-with-tools, bounded) vs reasoning_only
|
|
1666
|
+
// (exhausted → force the answer, one-shot). Flag off → loopHasBudget false → byte-identical
|
|
1667
|
+
// to the prior one-shot guard.
|
|
1668
|
+
const loopHasBudget = EMPTY_TURN_CONTINUE
|
|
1669
|
+
&& (TURN_DEADLINE_MS <= 0 || (Date.now() - loopStartMs) < 0.6 * TURN_DEADLINE_MS)
|
|
1670
|
+
&& toolCallIteration < 0.6 * MAX_TOOL_ITERATIONS;
|
|
1671
|
+
// Typed empty-response classification (grok-build port): reasoning_only vs no_visible_content
|
|
1672
|
+
// for observability + a tailored nudge. D-E: stopReason distinguishes a max_tokens truncation
|
|
1673
|
+
// ('truncated' → continue) from a demand-final-answer.
|
|
1674
|
+
const emptyClass = (!hasVisibleText && !structuredOutputState?.result)
|
|
1675
|
+
? classifyEmptyResponse(currentAssistantCanonicalMessage.content, convertedResponse.stopReason, loopHasBudget)
|
|
1676
|
+
: null;
|
|
1677
|
+
const isEmptyContinueKind = EMPTY_TURN_CONTINUE && !!emptyClass
|
|
1678
|
+
&& (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active');
|
|
1679
|
+
const canRetryEmpty = isEmptyContinueKind
|
|
1680
|
+
? emptyContinueCount < EMPTY_CONTINUE_MAX
|
|
1681
|
+
: !emptyResponseRetryUsed;
|
|
1682
|
+
if (emptyClass && canRetryEmpty) {
|
|
1683
|
+
if (isEmptyContinueKind) {
|
|
1684
|
+
emptyContinueCount++;
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
emptyResponseRetryUsed = true;
|
|
1688
|
+
}
|
|
1666
1689
|
console.warn(`[Orchestrator] Empty response detected (${emptyClass.kind}, hadReasoning=${emptyClass.hadReasoning}, iteration=${toolCallIteration}). ` +
|
|
1667
1690
|
`Retrying once with explicit completion prompt.`);
|
|
1668
1691
|
// R26 (2026-05-15, surfaced by A/B benchmark): the empty assistant
|
|
@@ -3613,6 +3636,7 @@ export class CortexOrchestrator {
|
|
|
3613
3636
|
throw new Error('No response message returned from provider');
|
|
3614
3637
|
}
|
|
3615
3638
|
let currentAssistantCanonicalMessage = convertedResponse.messages[0];
|
|
3639
|
+
this.recordDsmlRecovery(currentAssistantCanonicalMessage);
|
|
3616
3640
|
const assistantMessageId = currentAssistantCanonicalMessage.uuid;
|
|
3617
3641
|
const assistantMessage = {
|
|
3618
3642
|
uuid: assistantMessageId,
|
|
@@ -3714,6 +3738,10 @@ export class CortexOrchestrator {
|
|
|
3714
3738
|
const TURN_DEADLINE_MS = loopDefaults.turnDeadlineMs;
|
|
3715
3739
|
const loopStartMs = Date.now();
|
|
3716
3740
|
let timeWarnFired = false;
|
|
3741
|
+
// HB-ENDTURN-TERMINAL (2026-09-08): dark gate + bounded continue-counter (streaming parity).
|
|
3742
|
+
const EMPTY_TURN_CONTINUE = loopDefaults.emptyTurnContinue;
|
|
3743
|
+
const EMPTY_CONTINUE_MAX = 3;
|
|
3744
|
+
let emptyContinueCount = 0;
|
|
3717
3745
|
while (hasToolUse && toolCallIteration < MAX_TOOL_ITERATIONS) {
|
|
3718
3746
|
const gateReRequest = pendingGateRequest;
|
|
3719
3747
|
pendingGateRequest = false;
|
|
@@ -3741,10 +3769,29 @@ export class CortexOrchestrator {
|
|
|
3741
3769
|
// R32 (streaming R18b parity): detect empty/thinking-only responses.
|
|
3742
3770
|
// sendMessage retries ONCE inside the loop with tools preserved.
|
|
3743
3771
|
const hasVisibleText = hasVisibleAssistantText(currentAssistantCanonicalMessage.content);
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3772
|
+
// HB-ENDTURN-TERMINAL (2026-09-08): classify-first + bounded continue (streaming parity with
|
|
3773
|
+
// sendMessage). Flag off → loopHasBudget false → reasoning_only_active never selected → the
|
|
3774
|
+
// guard reduces to the prior `!hasVisibleText && !emptyResponseRetryUsed` one-shot.
|
|
3775
|
+
const loopHasBudget = EMPTY_TURN_CONTINUE
|
|
3776
|
+
&& (TURN_DEADLINE_MS <= 0 || (Date.now() - loopStartMs) < 0.6 * TURN_DEADLINE_MS)
|
|
3777
|
+
&& toolCallIteration < 0.6 * MAX_TOOL_ITERATIONS;
|
|
3778
|
+
const emptyClass = !hasVisibleText
|
|
3779
|
+
? classifyEmptyResponse(currentAssistantCanonicalMessage.content, undefined, loopHasBudget)
|
|
3780
|
+
: null;
|
|
3781
|
+
const isEmptyContinueKind = EMPTY_TURN_CONTINUE && !!emptyClass
|
|
3782
|
+
&& (emptyClass.kind === 'truncated' || emptyClass.kind === 'reasoning_only_active');
|
|
3783
|
+
const canRetryEmpty = isEmptyContinueKind
|
|
3784
|
+
? emptyContinueCount < EMPTY_CONTINUE_MAX
|
|
3785
|
+
: !emptyResponseRetryUsed;
|
|
3786
|
+
if (emptyClass && canRetryEmpty) {
|
|
3787
|
+
if (isEmptyContinueKind) {
|
|
3788
|
+
emptyContinueCount++;
|
|
3789
|
+
}
|
|
3790
|
+
else {
|
|
3791
|
+
emptyResponseRetryUsed = true;
|
|
3792
|
+
}
|
|
3793
|
+
console.warn(`[Orchestrator Streaming] R32/R18b: Empty response (${emptyClass.kind}, iteration=${toolCallIteration}). ` +
|
|
3794
|
+
`Retrying with tools preserved.`);
|
|
3748
3795
|
// R26 repair: ensure the empty assistant turn has content (xAI hard-400s on empty)
|
|
3749
3796
|
for (let i = this.messageHistory.length - 1; i >= 0; i--) {
|
|
3750
3797
|
const m = this.messageHistory[i];
|
|
@@ -3769,7 +3816,7 @@ export class CortexOrchestrator {
|
|
|
3769
3816
|
role: 'user',
|
|
3770
3817
|
content: [{
|
|
3771
3818
|
type: 'text',
|
|
3772
|
-
text:
|
|
3819
|
+
text: `<system-reminder>${emptyResponseNudge(emptyClass.kind)}${nudgeForbidsTools(emptyClass.kind) ? ' Do not call any more tools.' : ''}</system-reminder>`,
|
|
3773
3820
|
}],
|
|
3774
3821
|
},
|
|
3775
3822
|
timeline: {
|
|
@@ -4528,6 +4575,33 @@ export class CortexOrchestrator {
|
|
|
4528
4575
|
input: block.toolUse.input
|
|
4529
4576
|
}));
|
|
4530
4577
|
hasToolUse = continuationToolUses.length > 0;
|
|
4578
|
+
// HB-ENDTURN-TERMINAL (2026-09-08): the streaming loop exits on a no-tool turn, so a reasoning-only
|
|
4579
|
+
// continuation WITH budget would otherwise fall to the terminal R29a synth. Instead keep the loop
|
|
4580
|
+
// alive with the CONTINUE nudge (mid-recon), reusing the EndTurn gate's pendingGateRequest re-request
|
|
4581
|
+
// (bypasses the empty-batch break + re-execution guard). Bounded by emptyContinueCount. Runs BEFORE
|
|
4582
|
+
// the EndTurn gate so a mid-recon reasoning turn continues instead of being treated as a finish.
|
|
4583
|
+
if (!hasToolUse && EMPTY_TURN_CONTINUE && emptyContinueCount < EMPTY_CONTINUE_MAX
|
|
4584
|
+
&& !hasVisibleAssistantText(continuationAssistantCanonicalMessage.content)) {
|
|
4585
|
+
const budget = (TURN_DEADLINE_MS <= 0 || (Date.now() - loopStartMs) < 0.6 * TURN_DEADLINE_MS)
|
|
4586
|
+
&& toolCallIteration < 0.6 * MAX_TOOL_ITERATIONS;
|
|
4587
|
+
const c = classifyEmptyResponse(continuationAssistantCanonicalMessage.content, undefined, budget);
|
|
4588
|
+
if (c.kind === 'reasoning_only_active' || c.kind === 'truncated') {
|
|
4589
|
+
emptyContinueCount++;
|
|
4590
|
+
const contMsg = {
|
|
4591
|
+
uuid: uuidv4(),
|
|
4592
|
+
timestamp: new Date().toISOString(),
|
|
4593
|
+
type: 'user',
|
|
4594
|
+
message: { role: 'user', content: [{ type: 'text', text: `<system-reminder>${emptyResponseNudge(c.kind)}</system-reminder>` }] },
|
|
4595
|
+
timeline: { sessionId: this.currentSessionId, conversationId: this.currentConversationId, turnNumber: this.turnNumber + 1 },
|
|
4596
|
+
model: { id: effectiveModel.id, provider: effectiveModel.provider, apiPattern: effectiveModel.api.pattern },
|
|
4597
|
+
};
|
|
4598
|
+
this.messageHistory.push(contMsg);
|
|
4599
|
+
await this.historyStore.appendMessage(this.currentSessionId, contMsg);
|
|
4600
|
+
console.warn(`[Orchestrator Streaming] HB-ENDTURN continue (${c.kind}, iteration=${toolCallIteration}) — re-requesting with tools.`);
|
|
4601
|
+
pendingGateRequest = true;
|
|
4602
|
+
hasToolUse = true;
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4531
4605
|
// 4.91.0 Stage-1 for STREAMING: tools were used but no valid EndTurn yet → append the reminder and
|
|
4532
4606
|
// run ONE more continuation with no tool batch (pendingGateRequest bypasses the empty-batch break
|
|
4533
4607
|
// and the re-execution guard). Two nudges, then fallback-accept (parity with sendMessage).
|
|
@@ -4993,6 +5067,10 @@ export class CortexOrchestrator {
|
|
|
4993
5067
|
maxLoopRepetitions: this.config.loopControl?.maxLoopRepetitions ?? 5,
|
|
4994
5068
|
// #2 wall-clock deadline: opt-in (0 = disabled), env CORTEX_TURN_DEADLINE_MS is the fallback.
|
|
4995
5069
|
turnDeadlineMs: resolveTurnDeadlineMs(this.config.loopControl?.turnDeadlineMs),
|
|
5070
|
+
// HB-ENDTURN-TERMINAL (2026-09-08, dark by default): when true, a reasoning-only empty turn WITH
|
|
5071
|
+
// budget remaining is treated as mid-recon (continue-with-tools) instead of forced terminal
|
|
5072
|
+
// surrender. Read as a dark lever (direct env), off = byte-identical to prior behavior.
|
|
5073
|
+
emptyTurnContinue: process.env.CORTEX_EMPTY_TURN_CONTINUE === 'true',
|
|
4996
5074
|
};
|
|
4997
5075
|
}
|
|
4998
5076
|
// Item 13c: fire on threshold CROSSINGS only (10, 20, 40… doubling) — the
|
|
@@ -7356,6 +7434,31 @@ export class CortexOrchestrator {
|
|
|
7356
7434
|
}
|
|
7357
7435
|
return this.decisionStore;
|
|
7358
7436
|
}
|
|
7437
|
+
/** HB-DSML-PARSE observability (2026-09-09): if the DeepSeek DSML tool-call recovery fired on this
|
|
7438
|
+
* response (recovered tool_use ids are prefixed `dsml_`), record ONE countable `dsml_recovered`
|
|
7439
|
+
* decision event so the bench data shows how often the leak occurred + that the fix is live.
|
|
7440
|
+
* Observability only — never throws into the turn. */
|
|
7441
|
+
recordDsmlRecovery(msg) {
|
|
7442
|
+
try {
|
|
7443
|
+
const content = Array.isArray(msg?.content) ? msg.content : [];
|
|
7444
|
+
const recovered = content.filter((b) => b?.type === 'tool_use' && typeof b?.toolUse?.id === 'string' && b.toolUse.id.startsWith('dsml_'));
|
|
7445
|
+
if (recovered.length === 0)
|
|
7446
|
+
return;
|
|
7447
|
+
const store = this.getDecisionStore();
|
|
7448
|
+
if (store) {
|
|
7449
|
+
void store
|
|
7450
|
+
.recordEvent({
|
|
7451
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
7452
|
+
kind: 'dsml_recovered',
|
|
7453
|
+
detail: { count: recovered.length, tools: recovered.map((b) => b.toolUse?.name) },
|
|
7454
|
+
})
|
|
7455
|
+
.catch(() => { });
|
|
7456
|
+
}
|
|
7457
|
+
if (this.config.debug)
|
|
7458
|
+
console.log(`[HB-DSML-PARSE] dsml_recovered event (count=${recovered.length})`);
|
|
7459
|
+
}
|
|
7460
|
+
catch { /* observability only */ }
|
|
7461
|
+
}
|
|
7359
7462
|
isRecordingEnabled() {
|
|
7360
7463
|
return process.env.CORTEX_RECORD_DECISIONS !== 'false';
|
|
7361
7464
|
}
|