@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.
Files changed (42) hide show
  1. package/.env.defaults +15 -0
  2. package/dist/config/effectiveConfig.d.ts.map +1 -1
  3. package/dist/config/effectiveConfig.js +1 -0
  4. package/dist/config/effectiveConfig.js.map +1 -1
  5. package/dist/models/cards/deepseek/deepseek-v4-1-flash.d.ts +19 -0
  6. package/dist/models/cards/deepseek/deepseek-v4-1-flash.d.ts.map +1 -0
  7. package/dist/models/cards/deepseek/deepseek-v4-1-flash.js +40 -0
  8. package/dist/models/cards/deepseek/deepseek-v4-1-flash.js.map +1 -0
  9. package/dist/models/cards/deepseek/index.d.ts +1 -0
  10. package/dist/models/cards/deepseek/index.d.ts.map +1 -1
  11. package/dist/models/cards/deepseek/index.js +1 -0
  12. package/dist/models/cards/deepseek/index.js.map +1 -1
  13. package/dist/models/cards/mercury/index.d.ts +1 -0
  14. package/dist/models/cards/mercury/index.d.ts.map +1 -1
  15. package/dist/models/cards/mercury/index.js +4 -0
  16. package/dist/models/cards/mercury/index.js.map +1 -1
  17. package/dist/models/cards/mercury/mercury-2-5.d.ts +28 -0
  18. package/dist/models/cards/mercury/mercury-2-5.d.ts.map +1 -0
  19. package/dist/models/cards/mercury/mercury-2-5.js +37 -0
  20. package/dist/models/cards/mercury/mercury-2-5.js.map +1 -0
  21. package/dist/models/registry/ModularModelRegistry.d.ts.map +1 -1
  22. package/dist/models/registry/ModularModelRegistry.js +3 -1
  23. package/dist/models/registry/ModularModelRegistry.js.map +1 -1
  24. package/dist/orchestrator/APIClient.d.ts.map +1 -1
  25. package/dist/orchestrator/APIClient.js +17 -0
  26. package/dist/orchestrator/APIClient.js.map +1 -1
  27. package/dist/orchestrator/CortexOrchestrator.d.ts +6 -0
  28. package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
  29. package/dist/orchestrator/CortexOrchestrator.js +118 -15
  30. package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
  31. package/dist/orchestrator/dsmlRecovery.d.ts +40 -0
  32. package/dist/orchestrator/dsmlRecovery.d.ts.map +1 -0
  33. package/dist/orchestrator/dsmlRecovery.js +106 -0
  34. package/dist/orchestrator/dsmlRecovery.js.map +1 -0
  35. package/dist/orchestrator/emptyResponseClassifier.d.ts +17 -4
  36. package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -1
  37. package/dist/orchestrator/emptyResponseClassifier.js +26 -9
  38. package/dist/orchestrator/emptyResponseClassifier.js.map +1 -1
  39. package/dist/training/DecisionStore.d.ts +1 -1
  40. package/dist/training/DecisionStore.d.ts.map +1 -1
  41. package/dist/training/DecisionStore.js.map +1 -1
  42. 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
- if (!hasVisibleText && !emptyResponseRetryUsed && !structuredOutputState?.result) {
1657
- emptyResponseRetryUsed = true;
1658
- // Typed empty-response classification (grok-build port): distinguish
1659
- // reasoning_only (model reasoned but never answered) from
1660
- // no_visible_content (nothing at all) for observability + a nudge
1661
- // tailored to the failure shape. Does NOT change the one-bounded-retry
1662
- // decision — only the log + nudge text.
1663
- // D-E (2026-09-04): pass stopReason so a max_tokens truncation is classified 'truncated'
1664
- // (continue), not 'reasoning_only' (which demands a final answer and just re-truncates).
1665
- const emptyClass = classifyEmptyResponse(currentAssistantCanonicalMessage.content, convertedResponse.stopReason);
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
- if (!hasVisibleText && !emptyResponseRetryUsed) {
3745
- emptyResponseRetryUsed = true;
3746
- console.warn(`[Orchestrator Streaming] R32/R18b: Empty response (no tool_use, no text, iteration=${toolCallIteration}). ` +
3747
- `Retrying once with tools preserved.`);
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: '<system-reminder>Your previous response had no visible text. Please provide your final answer in plain text now — summarize your findings or complete the requested task. Do not call any more tools.</system-reminder>',
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
  }