@nexus-cortex/core 4.94.0 → 4.95.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 +42 -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/middleware/HelperModelMiddleware.d.ts +16 -0
- package/dist/middleware/HelperModelMiddleware.d.ts.map +1 -1
- package/dist/middleware/HelperModelMiddleware.js +25 -0
- package/dist/middleware/HelperModelMiddleware.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +20 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +161 -0
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/loopToolBlock.d.ts +46 -0
- package/dist/orchestrator/loopToolBlock.d.ts.map +1 -0
- package/dist/orchestrator/loopToolBlock.js +66 -0
- package/dist/orchestrator/loopToolBlock.js.map +1 -0
- 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/loopExitPlanner.d.ts +30 -0
- package/dist/training/loopExitPlanner.d.ts.map +1 -0
- package/dist/training/loopExitPlanner.js +61 -0
- package/dist/training/loopExitPlanner.js.map +1 -0
- package/dist/training/toolOutcome.d.ts.map +1 -1
- package/dist/training/toolOutcome.js +33 -2
- package/dist/training/toolOutcome.js.map +1 -1
- package/package.json +3 -3
|
@@ -50,6 +50,8 @@ import { resolveThrashState, resolveThrashConfig } from '../training/thrashDetec
|
|
|
50
50
|
import { classifyErrorFamily } from '../training/errorFamily.js';
|
|
51
51
|
import { classifyToolOutcome } from '../training/toolOutcome.js';
|
|
52
52
|
import { LoopLadder, formatLadderSignal } from '../training/loopLadder.js';
|
|
53
|
+
import { decideLoopBlock, isLoopBlockTrigger } from './loopToolBlock.js';
|
|
54
|
+
import { resolveLoopExitConfig, parseLoopExitVerdict } from '../training/loopExitPlanner.js';
|
|
53
55
|
import { shouldNudgeInaction, formatInactionNudge } from './inactionGuard.js';
|
|
54
56
|
import { applyImageTtlForRequest } from './imageTtl.js';
|
|
55
57
|
import { detectSurrenderText, resolveSurrenderNudgeMode, SURRENDER_REMINDER } from './turnEndGuards.js';
|
|
@@ -171,6 +173,15 @@ export class CortexOrchestrator {
|
|
|
171
173
|
* exactly once, at the anchor-lift boundary. One-shot per orchestrator. */
|
|
172
174
|
deferredCorpusDelivered = false;
|
|
173
175
|
liftNudgeDelivered = false; // A′ proposal-1: lift-boundary SearchTools/AskForAdvice nudge, one-shot
|
|
176
|
+
// CORTEX_LOOP_TOOL_BLOCK (hard loop intervention): when the ladder flags a
|
|
177
|
+
// non-converging same-approach loop, disable the looping tool's EXECUTOR for
|
|
178
|
+
// ONE turn (tools list unchanged → cache-safe); a call to it returns an
|
|
179
|
+
// append-only redirect error. `loopBlockTool` is armed for the NEXT turn;
|
|
180
|
+
// `loopBlockCounts` tracks blocks per tool (escalate to a mentor consult after
|
|
181
|
+
// 2); `loopBlockEscalate` requests the direct-invoke consult.
|
|
182
|
+
loopBlockTool = null;
|
|
183
|
+
loopBlockCounts = new Map();
|
|
184
|
+
loopBlockEscalate = false;
|
|
174
185
|
liftPlanDelivered = false; // LIFT_MENTOR_PLANNER: bounded mentor-planner at the lift, one-shot
|
|
175
186
|
cachedEnvReport; // ENV_RECON_COMMAND output, gathered once, shared by lift-planner + endturn-resolver
|
|
176
187
|
endTurnResolverRejects = 0; // endTurnResolver: GAP vetoes so far this task (bounded by maxRejects → fallback-accept)
|
|
@@ -477,6 +488,48 @@ export class CortexOrchestrator {
|
|
|
477
488
|
}
|
|
478
489
|
return '';
|
|
479
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* The last `n` tool calls of `toolName` (each input paired with its result output), oldest→newest.
|
|
493
|
+
* Fed to the loop-exit mentor so it sees the ACTUAL looping calls + their failing outputs — the
|
|
494
|
+
* concrete thing to redirect — not just the model's reasoning prose. The tool being blocked right
|
|
495
|
+
* now is not yet in history, so this returns the PRIOR (looping) calls of that tool.
|
|
496
|
+
*/
|
|
497
|
+
recentToolCalls(toolName, n) {
|
|
498
|
+
const resultById = new Map();
|
|
499
|
+
for (const m of this.messageHistory) {
|
|
500
|
+
const c = m?.message?.content;
|
|
501
|
+
if (!Array.isArray(c))
|
|
502
|
+
continue;
|
|
503
|
+
for (const b of c) {
|
|
504
|
+
if (b?.type !== 'tool_result')
|
|
505
|
+
continue;
|
|
506
|
+
const rc = typeof b.content === 'string'
|
|
507
|
+
? b.content
|
|
508
|
+
: Array.isArray(b.content)
|
|
509
|
+
? b.content.filter((x) => x?.type === 'text').map((x) => x.text || '').join('\n')
|
|
510
|
+
: '';
|
|
511
|
+
resultById.set(b.tool_use_id, rc);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const calls = [];
|
|
515
|
+
for (let i = this.messageHistory.length - 1; i >= 0 && calls.length < n; i--) {
|
|
516
|
+
const m = this.messageHistory[i];
|
|
517
|
+
if (m?.message?.role !== 'assistant')
|
|
518
|
+
continue;
|
|
519
|
+
const c = m.message.content;
|
|
520
|
+
if (!Array.isArray(c))
|
|
521
|
+
continue;
|
|
522
|
+
for (let j = c.length - 1; j >= 0 && calls.length < n; j--) {
|
|
523
|
+
const b = c[j];
|
|
524
|
+
if (b?.type !== 'tool_use' || b.name !== toolName)
|
|
525
|
+
continue;
|
|
526
|
+
const input = JSON.stringify(b.input ?? {}).slice(0, 500);
|
|
527
|
+
const result = (resultById.get(b.id) ?? '(no result captured)').slice(0, 500);
|
|
528
|
+
calls.push(`call: ${input}\n → result: ${result}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return calls.reverse().join('\n');
|
|
532
|
+
}
|
|
480
533
|
/**
|
|
481
534
|
* endTurnResolver (operator design 2026-09-04) — the mentor-as-EndTurn-judge, the finish-side twin of
|
|
482
535
|
* deliverLiftPlanAtLift. Runs AFTER evaluateEndTurnGates: only when the mechanical gate ACCEPTED the
|
|
@@ -2217,6 +2270,7 @@ export class CortexOrchestrator {
|
|
|
2217
2270
|
continue;
|
|
2218
2271
|
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
2219
2272
|
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
2273
|
+
this.armLoopBlock(tr.tool_name, ladder.action);
|
|
2220
2274
|
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
2221
2275
|
if (sig)
|
|
2222
2276
|
ladderSignal = sig;
|
|
@@ -3995,6 +4049,7 @@ export class CortexOrchestrator {
|
|
|
3995
4049
|
continue;
|
|
3996
4050
|
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
3997
4051
|
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
4052
|
+
this.armLoopBlock(tr.tool_name, ladder.action);
|
|
3998
4053
|
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
3999
4054
|
if (sig)
|
|
4000
4055
|
ladderSignal = sig;
|
|
@@ -5939,6 +5994,13 @@ export class CortexOrchestrator {
|
|
|
5939
5994
|
if (this.config.debug) {
|
|
5940
5995
|
console.log(`[Orchestrator Phase 2.5] Executing tool: ${toolUse.name}`);
|
|
5941
5996
|
}
|
|
5997
|
+
// CORTEX_LOOP_TOOL_BLOCK: if this tool's executor is loop-disabled this
|
|
5998
|
+
// turn, return the append-only redirect error instead of running it.
|
|
5999
|
+
const loopBlocked = await this.maybeBlockLoopingTool(toolUse);
|
|
6000
|
+
if (loopBlocked) {
|
|
6001
|
+
results.push(await this.processToolTraining(toolUse, loopBlocked));
|
|
6002
|
+
continue;
|
|
6003
|
+
}
|
|
5942
6004
|
// Context management tools (CORTEX.md generation + two-tier memory)
|
|
5943
6005
|
const contextManagementToolNames = [
|
|
5944
6006
|
'InitCortexContext',
|
|
@@ -6365,6 +6427,10 @@ export class CortexOrchestrator {
|
|
|
6365
6427
|
if (this.config.debug) {
|
|
6366
6428
|
console.log(`[Orchestrator] Executing single tool: ${toolUse.name}`);
|
|
6367
6429
|
}
|
|
6430
|
+
// CORTEX_LOOP_TOOL_BLOCK: executor-gate for the parallel/single path.
|
|
6431
|
+
const loopBlocked = await this.maybeBlockLoopingTool(toolUse);
|
|
6432
|
+
if (loopBlocked)
|
|
6433
|
+
return loopBlocked;
|
|
6368
6434
|
// Context management tools
|
|
6369
6435
|
const contextManagementToolNames = ['InitCortexContext'];
|
|
6370
6436
|
const isContextManagementTool = contextManagementToolNames.includes(toolUse.name);
|
|
@@ -7453,6 +7519,101 @@ export class CortexOrchestrator {
|
|
|
7453
7519
|
}
|
|
7454
7520
|
return { success: true, llmContent: hint, metadata: { source: 'mentor-consult', rung, helperModel: this.config.reactiveMentorship?.helperModelId } };
|
|
7455
7521
|
}
|
|
7522
|
+
/** CORTEX_LOOP_TOOL_BLOCK — arm the hard intervention when the ladder flags a
|
|
7523
|
+
* non-converging same-approach loop (diversify/break). If this tool has
|
|
7524
|
+
* already been blocked twice, request a mentor escalation instead of a 3rd
|
|
7525
|
+
* block. Called from the turn loop where the ladder result is computed. */
|
|
7526
|
+
armLoopBlock(toolName, ladderAction) {
|
|
7527
|
+
if ((process.env.CORTEX_LOOP_TOOL_BLOCK ?? '').trim().toLowerCase() !== 'true')
|
|
7528
|
+
return;
|
|
7529
|
+
if (!isLoopBlockTrigger(ladderAction))
|
|
7530
|
+
return;
|
|
7531
|
+
if (this.loopBlockEscalate)
|
|
7532
|
+
return; // already pending
|
|
7533
|
+
const prior = this.loopBlockCounts.get(toolName) ?? 0;
|
|
7534
|
+
const decision = decideLoopBlock(toolName, prior);
|
|
7535
|
+
if (decision.action === 'escalate') {
|
|
7536
|
+
// Arm the tool AND the escalation flag: the next call to this tool
|
|
7537
|
+
// triggers a mentor consult (delivered as the redirect) instead of a 3rd
|
|
7538
|
+
// generic block.
|
|
7539
|
+
this.loopBlockTool = toolName;
|
|
7540
|
+
this.loopBlockEscalate = true;
|
|
7541
|
+
if (this.config.debug)
|
|
7542
|
+
console.log(`[LoopBlock] ${toolName} looped past ${prior} blocks → next call escalates to mentor consult`);
|
|
7543
|
+
}
|
|
7544
|
+
else if (decision.action === 'block') {
|
|
7545
|
+
this.loopBlockTool = toolName;
|
|
7546
|
+
this.loopBlockCounts.set(toolName, prior + 1);
|
|
7547
|
+
if (this.config.debug)
|
|
7548
|
+
console.log(`[LoopBlock] arming block #${prior + 1} on ${toolName} for next turn`);
|
|
7549
|
+
}
|
|
7550
|
+
}
|
|
7551
|
+
/** Executor-gate: if a tool is loop-blocked THIS turn, return an append-only
|
|
7552
|
+
* redirect error tool_result INSTEAD of executing it (the tools list is
|
|
7553
|
+
* unchanged → prompt cache is not busted). One-shot: the block clears on use
|
|
7554
|
+
* so the tool is available again next turn. Returns null when not blocked. */
|
|
7555
|
+
async maybeBlockLoopingTool(toolUse) {
|
|
7556
|
+
if (!this.loopBlockTool || this.loopBlockTool !== toolUse.name)
|
|
7557
|
+
return null;
|
|
7558
|
+
this.loopBlockTool = null; // one turn only
|
|
7559
|
+
const store = this.getDecisionStore();
|
|
7560
|
+
// Escalation: 2 blocks did not break the loop → deliver a mentor plan
|
|
7561
|
+
// directly as the redirect (orchestrator-direct-invoke, NOT forced
|
|
7562
|
+
// tool_choice which is unreliable on DeepSeek once history holds a tool_call).
|
|
7563
|
+
if (this.loopBlockEscalate) {
|
|
7564
|
+
this.loopBlockEscalate = false;
|
|
7565
|
+
// Escalation = the mentor-as-loop-exit-planner (bounded pro-max, sibling of the EndTurn
|
|
7566
|
+
// resolver). ORCHESTRATOR-DIRECT-INVOKE — the orchestrator calls evaluateLoopExit itself and
|
|
7567
|
+
// delivers the verdict as this redirect; it is NOT a tool the low-compliance model must reach
|
|
7568
|
+
// for (forced tool_choice is dead on DeepSeek mid-history — the MENTOR_AUTO pattern).
|
|
7569
|
+
let plan = '';
|
|
7570
|
+
let verdict = 'UNKNOWN';
|
|
7571
|
+
try {
|
|
7572
|
+
if (this.helperMiddleware?.evaluateLoopExit) {
|
|
7573
|
+
const cfg = resolveLoopExitConfig();
|
|
7574
|
+
const text = await withTimeout(this.helperMiddleware.evaluateLoopExit({
|
|
7575
|
+
task: this.lastRealUserText(),
|
|
7576
|
+
envReport: this.gatherEnvReport(),
|
|
7577
|
+
loopingTool: toolUse.name,
|
|
7578
|
+
recentAttempts: this.recentToolCalls(toolUse.name, 4) || this.lastAssistantText().slice(0, 2000),
|
|
7579
|
+
outputBudgetTokens: cfg.outputBudgetTokens,
|
|
7580
|
+
effort: cfg.effort,
|
|
7581
|
+
helperModelId: this.config.reactiveMentorship?.helperModelId,
|
|
7582
|
+
}), cfg.timeoutMs);
|
|
7583
|
+
plan = String(text ?? '');
|
|
7584
|
+
verdict = parseLoopExitVerdict(plan);
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7587
|
+
catch { /* fail-open */ }
|
|
7588
|
+
const content = plan
|
|
7589
|
+
? `Tool "${toolUse.name}" is DISABLED: repeated loop interventions have not broken the pattern. A senior engineer reviewed your situation and gave this exit plan:\n\n${plan}\n\nFollow this instead of retrying "${toolUse.name}".`
|
|
7590
|
+
: `Tool "${toolUse.name}" is DISABLED: repeated loop interventions have not broken this pattern. Stop retrying "${toolUse.name}", re-read the task requirements, and take a fundamentally different approach.`;
|
|
7591
|
+
if (store)
|
|
7592
|
+
void store.recordEvent({ sessionId: this.currentSessionId ?? 'unknown', kind: 'loop_tool_block', toolName: toolUse.name, detail: { escalated: true, consulted: !!plan, verdict } }).catch(() => { });
|
|
7593
|
+
if (this.config.debug)
|
|
7594
|
+
console.log(`[LoopBlock] ESCALATED ${toolUse.name} → exit-planner (${plan ? verdict + ' plan delivered' : 'fail-open'})`);
|
|
7595
|
+
return { tool_use_id: toolUse.id, tool_name: toolUse.name, content, is_error: true, metadata: { loopToolBlock: true, escalated: true, verdict } };
|
|
7596
|
+
}
|
|
7597
|
+
const prior = (this.loopBlockCounts.get(toolUse.name) ?? 1) - 1;
|
|
7598
|
+
const decision = decideLoopBlock(toolUse.name, prior);
|
|
7599
|
+
if (store) {
|
|
7600
|
+
void store.recordEvent({
|
|
7601
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
7602
|
+
kind: 'loop_tool_block',
|
|
7603
|
+
toolName: toolUse.name,
|
|
7604
|
+
detail: { blockNumber: prior + 1, redirectTools: decision.redirectTools },
|
|
7605
|
+
}).catch(() => { });
|
|
7606
|
+
}
|
|
7607
|
+
if (this.config.debug)
|
|
7608
|
+
console.log(`[LoopBlock] redirected ${toolUse.name} → ${decision.redirectTools.join(',')}`);
|
|
7609
|
+
return {
|
|
7610
|
+
tool_use_id: toolUse.id,
|
|
7611
|
+
tool_name: toolUse.name,
|
|
7612
|
+
content: decision.message,
|
|
7613
|
+
is_error: true,
|
|
7614
|
+
metadata: { loopToolBlock: true, redirectTools: decision.redirectTools },
|
|
7615
|
+
};
|
|
7616
|
+
}
|
|
7456
7617
|
async processToolTraining(toolUse, result) {
|
|
7457
7618
|
this.applySliceNudge(toolUse, result); // universal post-execution hook: every path funnels through processToolTraining
|
|
7458
7619
|
const store = this.getDecisionStore();
|