@nexus-cortex/core 4.69.0 → 4.70.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/dist/models/configurators/DeepSeekConfigurator.d.ts +3 -0
- package/dist/models/configurators/DeepSeekConfigurator.d.ts.map +1 -1
- package/dist/models/configurators/DeepSeekConfigurator.js +1 -0
- package/dist/models/configurators/DeepSeekConfigurator.js.map +1 -1
- package/dist/models/configurators/XAIConfigurator.d.ts +3 -0
- package/dist/models/configurators/XAIConfigurator.d.ts.map +1 -1
- package/dist/models/configurators/XAIConfigurator.js +1 -0
- package/dist/models/configurators/XAIConfigurator.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +9 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +535 -15
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/inactionGuard.d.ts +42 -0
- package/dist/orchestrator/inactionGuard.d.ts.map +1 -0
- package/dist/orchestrator/inactionGuard.js +47 -0
- package/dist/orchestrator/inactionGuard.js.map +1 -0
- package/dist/orchestrator/requirementsVerification.d.ts +43 -0
- package/dist/orchestrator/requirementsVerification.d.ts.map +1 -0
- package/dist/orchestrator/requirementsVerification.js +87 -0
- package/dist/orchestrator/requirementsVerification.js.map +1 -0
- package/dist/tools/ToolProfile.d.ts +9 -0
- package/dist/tools/ToolProfile.d.ts.map +1 -1
- package/dist/tools/ToolProfile.js +19 -0
- package/dist/tools/ToolProfile.js.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.d.ts.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.js +14 -0
- package/dist/tools/registries/BaseToolRegistry.js.map +1 -1
- package/dist/training/DecisionPriorInjector.d.ts +7 -0
- package/dist/training/DecisionPriorInjector.d.ts.map +1 -1
- package/dist/training/DecisionPriorInjector.js +22 -0
- package/dist/training/DecisionPriorInjector.js.map +1 -1
- package/dist/training/DecisionStore.d.ts +39 -0
- package/dist/training/DecisionStore.d.ts.map +1 -1
- package/dist/training/DecisionStore.js +94 -0
- package/dist/training/DecisionStore.js.map +1 -1
- package/dist/training/errorFamily.d.ts +15 -0
- package/dist/training/errorFamily.d.ts.map +1 -0
- package/dist/training/errorFamily.js +40 -0
- package/dist/training/errorFamily.js.map +1 -0
- package/dist/training/loopLadder.d.ts +35 -0
- package/dist/training/loopLadder.d.ts.map +1 -0
- package/dist/training/loopLadder.js +58 -0
- package/dist/training/loopLadder.js.map +1 -0
- package/dist/training/toolOutcome.d.ts +20 -0
- package/dist/training/toolOutcome.d.ts.map +1 -0
- package/dist/training/toolOutcome.js +100 -0
- package/dist/training/toolOutcome.js.map +1 -0
- package/package.json +3 -3
|
@@ -30,11 +30,16 @@ import { CacheMetricsAccumulator } from '../session/CacheMetricsAccumulator.js';
|
|
|
30
30
|
import { SubAgentProcessManager, createSubAgentProcessManager } from './SubAgentProcessManager.js';
|
|
31
31
|
// Phase 1: Tool Architecture Refactor - Unified Tool Registry
|
|
32
32
|
import { toolFactory } from '../tools/ToolFactory.js';
|
|
33
|
-
import { resolveToolProfile, resolveToolAnchor, isNarrowProfile, isToolAllowedByProfile, applyToolProfile } from '../tools/ToolProfile.js';
|
|
33
|
+
import { resolveToolProfile, resolveToolAnchor, resolveFrameProfile, isNarrowProfile, isToolAllowedByProfile, applyToolProfile } from '../tools/ToolProfile.js';
|
|
34
34
|
import { slashCommandRegistry } from '../commands/SlashCommandRegistry.js';
|
|
35
35
|
import { prefixMcpToolName, parseMcpToolName } from '../mcp/mcpToolNamespacing.js';
|
|
36
|
-
import { DecisionStore
|
|
37
|
-
import { formatPriorReminder } from '../training/DecisionPriorInjector.js';
|
|
36
|
+
import { DecisionStore } from '../training/DecisionStore.js';
|
|
37
|
+
import { formatPriorReminder, formatFamilyReminder } from '../training/DecisionPriorInjector.js';
|
|
38
|
+
import { classifyErrorFamily } from '../training/errorFamily.js';
|
|
39
|
+
import { classifyToolOutcome } from '../training/toolOutcome.js';
|
|
40
|
+
import { LoopLadder, formatLadderSignal } from '../training/loopLadder.js';
|
|
41
|
+
import { shouldNudgeInaction, formatInactionNudge } from './inactionGuard.js';
|
|
42
|
+
import { verifyRequirements, resolveEndTurnRequirementsMode } from './requirementsVerification.js';
|
|
38
43
|
import { ModelRouterMatrix } from '../training/ModelRouterMatrix.js';
|
|
39
44
|
import { classifyTask } from '../training/TaskClassifier.js';
|
|
40
45
|
import { closestToolMatches } from './toolNameMatcher.js';
|
|
@@ -143,6 +148,10 @@ export class CortexOrchestrator {
|
|
|
143
148
|
/** The active model card's anchorProfile (captured at request assembly) —
|
|
144
149
|
* env CORTEX_TOOL_ANCHOR still overrides inside resolveToolAnchor. */
|
|
145
150
|
cardAnchorProfile = null;
|
|
151
|
+
/** Card frameProfile ('lifted'|'persist') captured with the anchor — under
|
|
152
|
+
* 'persist' the anchor never lifts (backlog item 5; env
|
|
153
|
+
* CORTEX_TOOL_ANCHOR_PERSIST overrides in resolveFrameProfile). */
|
|
154
|
+
cardFrameProfile = null;
|
|
146
155
|
/** P6 deferral (CORTEX_PROMPT_MASS=defer): the static corpus is delivered
|
|
147
156
|
* exactly once, at the anchor-lift boundary. One-shot per orchestrator. */
|
|
148
157
|
deferredCorpusDelivered = false;
|
|
@@ -699,6 +708,11 @@ export class CortexOrchestrator {
|
|
|
699
708
|
// Card-level home door: capture the active card's anchorProfile (env
|
|
700
709
|
// CORTEX_TOOL_ANCHOR overrides inside the resolver; env 'none' disables).
|
|
701
710
|
this.cardAnchorProfile = effectiveModel.anchorProfile ?? null;
|
|
711
|
+
this.cardFrameProfile = effectiveModel.frameProfile ?? null;
|
|
712
|
+
// Thread the CARD's anchor to the executors (per-orchestrator config, by
|
|
713
|
+
// reference) so framing-aware executor behavior (ShellTool steering
|
|
714
|
+
// default) is model-card/registry-scoped, not process-global env.
|
|
715
|
+
this.executorRegistry.updateConfig({ activeAnchorProfile: this.cardAnchorProfile });
|
|
702
716
|
if (toolsToUse && toolsToUse.length > 0) {
|
|
703
717
|
toolsToUse = this.applyAnchorIfArmed(toolsToUse);
|
|
704
718
|
}
|
|
@@ -974,6 +988,9 @@ export class CortexOrchestrator {
|
|
|
974
988
|
// Round 18b: track whether we've already retried on empty response so
|
|
975
989
|
// we don't loop forever. See empty-detection block below.
|
|
976
990
|
let emptyResponseRetryUsed = false;
|
|
991
|
+
// Inaction guard (backlog item 2 — the ladder's inverse): single-nudge
|
|
992
|
+
// bound for the actless-verbose retry. Default OFF via env.
|
|
993
|
+
let inactionNudgeUsed = false;
|
|
977
994
|
// EndTurn gate (Stage 1): the turn must not complete until the model
|
|
978
995
|
// calls the EndTurn attestation tool. Steering is ignored by some
|
|
979
996
|
// models (grok-4.3); a required tool call is not. Bounded so a model
|
|
@@ -1017,6 +1034,13 @@ export class CortexOrchestrator {
|
|
|
1017
1034
|
const thisTurnToolOutputs = [];
|
|
1018
1035
|
// Loop detection: Track recent tool calls to detect repetitive loops
|
|
1019
1036
|
const recentToolCalls = [];
|
|
1037
|
+
// Unified Outcome Ladder (docs/UNIFIED_OUTCOME_LADDER.md): per-approach
|
|
1038
|
+
// FAILURE escalation — remind(2)→diversify(4)→break(6) over normalized
|
|
1039
|
+
// near-duplicate approaches. Complements isToolProgressStalled (which is
|
|
1040
|
+
// outcome-blind exact-hash cycling) by firing earlier on failing thrash.
|
|
1041
|
+
const loopLadder = new LoopLadder();
|
|
1042
|
+
let ladderBreak = null;
|
|
1043
|
+
let ladderSignal = null;
|
|
1020
1044
|
// Track all tool uses across iterations for response
|
|
1021
1045
|
const allExecutedToolUses = [];
|
|
1022
1046
|
// Track tool call counts for diversity warning
|
|
@@ -1194,6 +1218,130 @@ export class CortexOrchestrator {
|
|
|
1194
1218
|
continue;
|
|
1195
1219
|
}
|
|
1196
1220
|
}
|
|
1221
|
+
// Inaction guard (backlog item 2 — the loop ladder's inverse).
|
|
1222
|
+
// TB2 2×2: persist-frame arms produced never-acted failures (5
|
|
1223
|
+
// pro-persist) — a huge reason-to-the-wall text dump with ZERO tool
|
|
1224
|
+
// calls, invisible to every call-counting guard. One bounded
|
|
1225
|
+
// steering retry, R18b-style; default OFF (CORTEX_INACTION_NUDGE).
|
|
1226
|
+
{
|
|
1227
|
+
const responseChars = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
1228
|
+
? currentAssistantCanonicalMessage.content
|
|
1229
|
+
.map((b) => b?.type === 'text' ? (b.text || '') : b?.type === 'thinking' ? (b.thinking || '') : '')
|
|
1230
|
+
.join('').length
|
|
1231
|
+
: String(currentAssistantCanonicalMessage.content ?? '').length;
|
|
1232
|
+
if (hasVisibleText &&
|
|
1233
|
+
!structuredOutputState?.result &&
|
|
1234
|
+
shouldNudgeInaction({
|
|
1235
|
+
responseChars,
|
|
1236
|
+
toolUseBlocksThisResponse: 0,
|
|
1237
|
+
executedToolCallsThisTurn: allExecutedToolUses.length,
|
|
1238
|
+
toolsOffered: toolsToUse.length,
|
|
1239
|
+
turnNumber: this.turnNumber,
|
|
1240
|
+
alreadyNudged: inactionNudgeUsed,
|
|
1241
|
+
})) {
|
|
1242
|
+
inactionNudgeUsed = true;
|
|
1243
|
+
console.warn(`[Orchestrator] Inaction guard: actless-verbose response (${responseChars} chars, 0 tool calls). ` +
|
|
1244
|
+
`Injecting act-first nudge (single retry).`);
|
|
1245
|
+
// Observability (backlog item 3): bank the event either way it
|
|
1246
|
+
// resolves — the distiller counts nudge→acted conversions.
|
|
1247
|
+
{
|
|
1248
|
+
const store = this.getDecisionStore();
|
|
1249
|
+
if (store) {
|
|
1250
|
+
void store
|
|
1251
|
+
.recordEvent({
|
|
1252
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1253
|
+
kind: 'inaction_nudge',
|
|
1254
|
+
detail: { responseChars, turnNumber: this.turnNumber },
|
|
1255
|
+
})
|
|
1256
|
+
.catch(() => { });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const inactionUserMessage = {
|
|
1260
|
+
uuid: uuidv4(),
|
|
1261
|
+
timestamp: new Date().toISOString(),
|
|
1262
|
+
type: 'user',
|
|
1263
|
+
message: {
|
|
1264
|
+
role: 'user',
|
|
1265
|
+
content: [{
|
|
1266
|
+
type: 'text',
|
|
1267
|
+
text: `<system-reminder>${formatInactionNudge()}</system-reminder>`,
|
|
1268
|
+
}],
|
|
1269
|
+
},
|
|
1270
|
+
timeline: {
|
|
1271
|
+
sessionId: this.currentSessionId,
|
|
1272
|
+
conversationId: this.currentConversationId,
|
|
1273
|
+
turnNumber: this.turnNumber + 1,
|
|
1274
|
+
},
|
|
1275
|
+
model: {
|
|
1276
|
+
id: effectiveModel.id,
|
|
1277
|
+
provider: effectiveModel.provider,
|
|
1278
|
+
apiPattern: effectiveModel.api.pattern,
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
this.messageHistory.push(inactionUserMessage);
|
|
1282
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionUserMessage);
|
|
1283
|
+
await this.ensureHistoryFitsModel(effectiveModel);
|
|
1284
|
+
const inactionCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
1285
|
+
const inactionRequest = this.gatewayTranslation.prepareRequest(inactionCanonicalHistory, toolsToUse, effectiveModel, {
|
|
1286
|
+
temperature: options.parameters?.temperature,
|
|
1287
|
+
maxTokens: options.parameters?.maxTokens,
|
|
1288
|
+
topP: options.parameters?.topP,
|
|
1289
|
+
reasoningEffort: options.parameters?.reasoningEffort,
|
|
1290
|
+
stream: options.streaming,
|
|
1291
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
1292
|
+
conversationId: this.currentConversationId, // R28b
|
|
1293
|
+
});
|
|
1294
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
1295
|
+
inactionRequest.previousResponseId = this.lastResponseId;
|
|
1296
|
+
}
|
|
1297
|
+
inactionRequest.conversationId = this.currentSessionId;
|
|
1298
|
+
let inactionApiResponse;
|
|
1299
|
+
if (this.retryMiddleware) {
|
|
1300
|
+
const r = await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(inactionRequest, effectiveModel), 'inaction_nudge_retry');
|
|
1301
|
+
inactionApiResponse = r.result;
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
inactionApiResponse = await this.apiClient.sendRequest(inactionRequest, effectiveModel);
|
|
1305
|
+
}
|
|
1306
|
+
const inactionConverted = this.gatewayTranslation.convertResponse(inactionApiResponse.data, effectiveModel, {
|
|
1307
|
+
sessionId: this.currentSessionId,
|
|
1308
|
+
conversationId: this.currentConversationId,
|
|
1309
|
+
turnNumber: this.turnNumber + 1,
|
|
1310
|
+
});
|
|
1311
|
+
if (effectiveModel.api.pattern === 'responses' && inactionApiResponse.data?.id) {
|
|
1312
|
+
this.lastResponseId = inactionApiResponse.data.id;
|
|
1313
|
+
this.lastResponseIdProvider = effectiveModel.provider; // R20a
|
|
1314
|
+
}
|
|
1315
|
+
if (inactionConverted.messages && inactionConverted.messages.length > 0) {
|
|
1316
|
+
const inactionAssistantCanonical = inactionConverted.messages[0];
|
|
1317
|
+
const inactionAssistantMessage = {
|
|
1318
|
+
uuid: inactionAssistantCanonical.uuid,
|
|
1319
|
+
timestamp: inactionAssistantCanonical.timestamp,
|
|
1320
|
+
type: 'assistant',
|
|
1321
|
+
message: {
|
|
1322
|
+
role: 'assistant',
|
|
1323
|
+
content: inactionAssistantCanonical.content,
|
|
1324
|
+
},
|
|
1325
|
+
timeline: {
|
|
1326
|
+
sessionId: this.currentSessionId,
|
|
1327
|
+
conversationId: this.currentConversationId,
|
|
1328
|
+
turnNumber: this.turnNumber + 1,
|
|
1329
|
+
},
|
|
1330
|
+
model: {
|
|
1331
|
+
id: effectiveModel.id,
|
|
1332
|
+
provider: effectiveModel.provider,
|
|
1333
|
+
apiPattern: effectiveModel.api.pattern,
|
|
1334
|
+
},
|
|
1335
|
+
...(inactionConverted.usage && { usage: inactionConverted.usage }),
|
|
1336
|
+
};
|
|
1337
|
+
this.messageHistory.push(inactionAssistantMessage);
|
|
1338
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionAssistantMessage);
|
|
1339
|
+
currentAssistantMessage = inactionAssistantMessage;
|
|
1340
|
+
currentAssistantCanonicalMessage = inactionAssistantCanonical;
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1197
1345
|
// EndTurn gate (Stage 1): refuse to finalize until the model has
|
|
1198
1346
|
// called the EndTurn attestation tool. Only arms when the turn
|
|
1199
1347
|
// actually used a (non-EndTurn) tool — pure-language turns bypass.
|
|
@@ -1217,8 +1365,27 @@ export class CortexOrchestrator {
|
|
|
1217
1365
|
if (!cv.ok)
|
|
1218
1366
|
stage3Violations = cv.violations;
|
|
1219
1367
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1368
|
+
// Fallback observability (backlog item 1.4): when the nudge bound
|
|
1369
|
+
// is exhausted and the gate is STILL unsatisfied, the turn ships
|
|
1370
|
+
// anyway (liveness beats purity) — but that fallback-accept was
|
|
1371
|
+
// previously silent. Bank an event so the distiller can see
|
|
1372
|
+
// un-attested passes.
|
|
1373
|
+
const endTurnGateUnsatisfied = endTurnGateEnabled && turnUsedTools && (!endTurnCalled || stage3Violations.length > 0);
|
|
1374
|
+
if (endTurnGateUnsatisfied && endTurnNudges >= END_TURN_MAX_NUDGES) {
|
|
1375
|
+
const fallbackReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1376
|
+
console.warn(`[Orchestrator] EndTurn gate FALLBACK-ACCEPT: ${fallbackReason} after ${endTurnNudges} nudges — turn ships un-attested.`);
|
|
1377
|
+
const store = this.getDecisionStore();
|
|
1378
|
+
if (store) {
|
|
1379
|
+
void store
|
|
1380
|
+
.recordEvent({
|
|
1381
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1382
|
+
kind: 'endturn_gate_fallback',
|
|
1383
|
+
detail: { reason: fallbackReason, nudges: endTurnNudges, iteration: toolCallIteration },
|
|
1384
|
+
})
|
|
1385
|
+
.catch(() => { });
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (endTurnGateUnsatisfied && endTurnNudges < END_TURN_MAX_NUDGES) {
|
|
1222
1389
|
endTurnNudges++;
|
|
1223
1390
|
const gateReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1224
1391
|
console.warn(`[Orchestrator] EndTurn gate: ${gateReason} ` +
|
|
@@ -1494,6 +1661,49 @@ export class CortexOrchestrator {
|
|
|
1494
1661
|
try {
|
|
1495
1662
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
1496
1663
|
clearTimeout(timeoutId);
|
|
1664
|
+
// Unified Outcome Ladder: observe each result's TRUE outcome (exit
|
|
1665
|
+
// codes, not wire is_error) and escalate on repeated failing
|
|
1666
|
+
// approaches. The strongest signal this batch is injected at the
|
|
1667
|
+
// signals block below; a 'break' rung ends the loop R29b-style.
|
|
1668
|
+
ladderSignal = null;
|
|
1669
|
+
{
|
|
1670
|
+
const inputById = new Map(toolUseBlocks.map((b) => [b.id, b.input]));
|
|
1671
|
+
for (const tr of toolResults) {
|
|
1672
|
+
if (tr.tool_name === 'EndTurn')
|
|
1673
|
+
continue;
|
|
1674
|
+
const input = inputById.get(tr.tool_use_id);
|
|
1675
|
+
if (input === undefined)
|
|
1676
|
+
continue;
|
|
1677
|
+
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
1678
|
+
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
1679
|
+
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
1680
|
+
if (sig)
|
|
1681
|
+
ladderSignal = sig;
|
|
1682
|
+
if (ladder.action === 'break')
|
|
1683
|
+
ladderBreak = sig;
|
|
1684
|
+
// Observability (backlog item 3): escalations are invisible in
|
|
1685
|
+
// the session record (signals inject post-persist) — bank each
|
|
1686
|
+
// rung as a decision-store event for the distiller.
|
|
1687
|
+
if (ladder.action !== 'none') {
|
|
1688
|
+
const store = this.getDecisionStore();
|
|
1689
|
+
if (store) {
|
|
1690
|
+
void store
|
|
1691
|
+
.recordEvent({
|
|
1692
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1693
|
+
kind: 'loop_escalation',
|
|
1694
|
+
toolName: tr.tool_name,
|
|
1695
|
+
detail: {
|
|
1696
|
+
rung: ladder.action,
|
|
1697
|
+
count: ladder.count,
|
|
1698
|
+
approachHash: outcome.approachHash,
|
|
1699
|
+
...(outcome.family ? { family: outcome.family } : {}),
|
|
1700
|
+
},
|
|
1701
|
+
})
|
|
1702
|
+
.catch(() => { });
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1497
1707
|
// File-history snapshots (Claude Code parity — previously built but
|
|
1498
1708
|
// UNWIRED: FileCheckpointManager existed with zero markModified
|
|
1499
1709
|
// callers and its snapshot message was discarded). Mark files
|
|
@@ -1574,7 +1784,31 @@ export class CortexOrchestrator {
|
|
|
1574
1784
|
lastEndTurnCitations = Array.isArray(cits) ? cits : undefined; // Stage 3 baseline
|
|
1575
1785
|
const verdict = verifyCitationsGrounded(cits, thisTurnToolOutputs.join('\n'));
|
|
1576
1786
|
if (verdict.grounded) {
|
|
1577
|
-
|
|
1787
|
+
// Stage 4 (backlog item 1, CORTEX_ENDTURN_REQUIREMENTS): verify
|
|
1788
|
+
// the requirements attestation — the wrong-artifact weapon.
|
|
1789
|
+
// Stage 2 verifies citations; nothing before this forced
|
|
1790
|
+
// re-reading the TASK. Rides the same nudge budget: a Stage-4
|
|
1791
|
+
// reject leaves the gate unsatisfied so the bounded
|
|
1792
|
+
// fallback-accept still guarantees liveness.
|
|
1793
|
+
if (resolveEndTurnRequirementsMode()) {
|
|
1794
|
+
const s4 = verifyRequirements({
|
|
1795
|
+
requirements: etUse?.input?.requirements,
|
|
1796
|
+
verification: etUse?.input?.verification,
|
|
1797
|
+
userTaskText: this.lastRealUserText(),
|
|
1798
|
+
turnUsedMutatingTool,
|
|
1799
|
+
});
|
|
1800
|
+
if (s4.ok) {
|
|
1801
|
+
endTurnCalled = true;
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
tr.is_error = true;
|
|
1805
|
+
tr.content = s4.nudge;
|
|
1806
|
+
console.warn('[Orchestrator] Stage4: EndTurn rejected — requirements attestation unsatisfied.');
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
endTurnCalled = true;
|
|
1811
|
+
}
|
|
1578
1812
|
}
|
|
1579
1813
|
else {
|
|
1580
1814
|
const bad = verdict.ungrounded
|
|
@@ -1722,14 +1956,43 @@ export class CortexOrchestrator {
|
|
|
1722
1956
|
const progressStalled = isToolProgressStalled(recentToolCalls);
|
|
1723
1957
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
1724
1958
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
1725
|
-
if (budgetSignal || diversityWarning) {
|
|
1726
|
-
const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
|
|
1959
|
+
if (budgetSignal || diversityWarning || ladderSignal) {
|
|
1960
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
|
|
1727
1961
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
1728
1962
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
1729
1963
|
const block = lastMsg.message.content[0];
|
|
1730
1964
|
const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
1731
1965
|
block.content = existing + '\n\n' + signals;
|
|
1966
|
+
// Observability (backlog item 3, micro-suite defect #4): this
|
|
1967
|
+
// mutation happens AFTER the tool_result was persisted — the
|
|
1968
|
+
// durable session lacks the steering the model saw. Bank a
|
|
1969
|
+
// steering_injected event so harvest/distill sees it. Session rows
|
|
1970
|
+
// stay append-only (never rewritten).
|
|
1971
|
+
const store = this.getDecisionStore();
|
|
1972
|
+
if (store) {
|
|
1973
|
+
const kinds = [
|
|
1974
|
+
budgetSignal ? 'budget' : null,
|
|
1975
|
+
diversityWarning ? 'diversity' : null,
|
|
1976
|
+
ladderSignal ? 'ladder' : null,
|
|
1977
|
+
].filter(Boolean);
|
|
1978
|
+
void store
|
|
1979
|
+
.recordEvent({
|
|
1980
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1981
|
+
kind: 'steering_injected',
|
|
1982
|
+
detail: { kinds, iteration: toolCallIteration, chars: signals.length },
|
|
1983
|
+
})
|
|
1984
|
+
.catch(() => { });
|
|
1985
|
+
}
|
|
1732
1986
|
}
|
|
1987
|
+
ladderSignal = null; // consumed — never re-inject on later iterations
|
|
1988
|
+
}
|
|
1989
|
+
// Ladder break (teach-then-break): the break instruction is already in
|
|
1990
|
+
// the tool result above; exit the loop R29b-style so the post-loop
|
|
1991
|
+
// synthesis net produces an honest final answer — never the blunt
|
|
1992
|
+
// toolCallIteration=MAX turn-kill of the exact-match detector.
|
|
1993
|
+
if (ladderBreak) {
|
|
1994
|
+
console.warn('[Orchestrator] LoopLadder break: repeated failing approach — forcing synthesis.');
|
|
1995
|
+
break;
|
|
1733
1996
|
}
|
|
1734
1997
|
// R29b hard cap: a weaker model may ignore the firm budget reminders
|
|
1735
1998
|
// and keep spinning (benchmark: deepseek 46 successful local calls,
|
|
@@ -1778,7 +2041,11 @@ export class CortexOrchestrator {
|
|
|
1778
2041
|
// full profile applies from here. Rebuild from allTools because the
|
|
1779
2042
|
// armed anchor narrowed toolsToUse at assembly. (When deferred loading
|
|
1780
2043
|
// is on, the re-filter block below performs the rebuild.)
|
|
1781
|
-
if (!this.anchorLifted &&
|
|
2044
|
+
if (!this.anchorLifted &&
|
|
2045
|
+
resolveToolAnchor(process.env, this.cardAnchorProfile) &&
|
|
2046
|
+
// Persist frame (backlog item 5): the anchored surface IS the session
|
|
2047
|
+
// surface — never lift, never deliver the deferred corpus.
|
|
2048
|
+
resolveFrameProfile(process.env, this.cardFrameProfile) !== 'persist') {
|
|
1782
2049
|
this.anchorLifted = true;
|
|
1783
2050
|
if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
|
|
1784
2051
|
toolsToUse = allTools;
|
|
@@ -2560,6 +2827,11 @@ export class CortexOrchestrator {
|
|
|
2560
2827
|
// Card-level home door: capture the active card's anchorProfile (env
|
|
2561
2828
|
// CORTEX_TOOL_ANCHOR overrides inside the resolver; env 'none' disables).
|
|
2562
2829
|
this.cardAnchorProfile = effectiveModel.anchorProfile ?? null;
|
|
2830
|
+
this.cardFrameProfile = effectiveModel.frameProfile ?? null;
|
|
2831
|
+
// Thread the CARD's anchor to the executors (per-orchestrator config, by
|
|
2832
|
+
// reference) so framing-aware executor behavior (ShellTool steering
|
|
2833
|
+
// default) is model-card/registry-scoped, not process-global env.
|
|
2834
|
+
this.executorRegistry.updateConfig({ activeAnchorProfile: this.cardAnchorProfile });
|
|
2563
2835
|
if (toolsToUse && toolsToUse.length > 0) {
|
|
2564
2836
|
toolsToUse = this.applyAnchorIfArmed(toolsToUse);
|
|
2565
2837
|
}
|
|
@@ -2758,7 +3030,12 @@ export class CortexOrchestrator {
|
|
|
2758
3030
|
let toolCallIteration = 0;
|
|
2759
3031
|
let totalToolErrors = 0;
|
|
2760
3032
|
let emptyResponseRetryUsed = false; // R32: parity with sendMessage R18b guard
|
|
3033
|
+
let inactionNudgeUsed = false; // Inaction guard (backlog item 2) — streaming parity
|
|
2761
3034
|
const allToolCalls = [];
|
|
3035
|
+
// Unified Outcome Ladder (streaming parity — see sendMessage).
|
|
3036
|
+
const loopLadder = new LoopLadder();
|
|
3037
|
+
let ladderBreak = null;
|
|
3038
|
+
let ladderSignal = null;
|
|
2762
3039
|
const allExecutedToolUses = [];
|
|
2763
3040
|
const toolCallCounts = new Map();
|
|
2764
3041
|
// DEBUG: Log canonical message content structure for tool extraction (streaming path)
|
|
@@ -2925,6 +3202,136 @@ export class CortexOrchestrator {
|
|
|
2925
3202
|
console.warn(`[Orchestrator Streaming] R32 retry failed:`, retryErr);
|
|
2926
3203
|
}
|
|
2927
3204
|
}
|
|
3205
|
+
// Inaction guard (backlog item 2 — streaming parity with sendMessage).
|
|
3206
|
+
// Actless-verbose first-turn response in a tool-capable request: one
|
|
3207
|
+
// bounded act-first steering retry. Default OFF (CORTEX_INACTION_NUDGE).
|
|
3208
|
+
{
|
|
3209
|
+
const responseChars = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
3210
|
+
? currentAssistantCanonicalMessage.content
|
|
3211
|
+
.map((b) => b?.type === 'text' ? (b.text || '') : b?.type === 'thinking' ? (b.thinking || '') : '')
|
|
3212
|
+
.join('').length
|
|
3213
|
+
: String(currentAssistantCanonicalMessage.content ?? '').length;
|
|
3214
|
+
if (hasVisibleText &&
|
|
3215
|
+
shouldNudgeInaction({
|
|
3216
|
+
responseChars,
|
|
3217
|
+
toolUseBlocksThisResponse: 0,
|
|
3218
|
+
executedToolCallsThisTurn: allExecutedToolUses.length,
|
|
3219
|
+
toolsOffered: toolsToUse.length,
|
|
3220
|
+
turnNumber: this.turnNumber,
|
|
3221
|
+
alreadyNudged: inactionNudgeUsed,
|
|
3222
|
+
})) {
|
|
3223
|
+
inactionNudgeUsed = true;
|
|
3224
|
+
console.warn(`[Orchestrator Streaming] Inaction guard: actless-verbose response (${responseChars} chars, 0 tool calls). ` +
|
|
3225
|
+
`Injecting act-first nudge (single retry).`);
|
|
3226
|
+
{
|
|
3227
|
+
const store = this.getDecisionStore();
|
|
3228
|
+
if (store) {
|
|
3229
|
+
void store
|
|
3230
|
+
.recordEvent({
|
|
3231
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3232
|
+
kind: 'inaction_nudge',
|
|
3233
|
+
detail: { responseChars, turnNumber: this.turnNumber, streaming: true },
|
|
3234
|
+
})
|
|
3235
|
+
.catch(() => { });
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
const inactionUserMessage = {
|
|
3239
|
+
uuid: uuidv4(),
|
|
3240
|
+
timestamp: new Date().toISOString(),
|
|
3241
|
+
type: 'user',
|
|
3242
|
+
message: {
|
|
3243
|
+
role: 'user',
|
|
3244
|
+
content: [{
|
|
3245
|
+
type: 'text',
|
|
3246
|
+
text: `<system-reminder>${formatInactionNudge()}</system-reminder>`,
|
|
3247
|
+
}],
|
|
3248
|
+
},
|
|
3249
|
+
timeline: {
|
|
3250
|
+
sessionId: this.currentSessionId,
|
|
3251
|
+
conversationId: this.currentConversationId,
|
|
3252
|
+
turnNumber: this.turnNumber + 1,
|
|
3253
|
+
},
|
|
3254
|
+
model: {
|
|
3255
|
+
id: effectiveModel.id,
|
|
3256
|
+
provider: effectiveModel.provider,
|
|
3257
|
+
apiPattern: effectiveModel.api.pattern,
|
|
3258
|
+
},
|
|
3259
|
+
};
|
|
3260
|
+
this.messageHistory.push(inactionUserMessage);
|
|
3261
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionUserMessage);
|
|
3262
|
+
try {
|
|
3263
|
+
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3264
|
+
const retryCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
3265
|
+
const retryRequest = this.gatewayTranslation.prepareRequest(retryCanonicalHistory, toolsToUse, effectiveModel, {
|
|
3266
|
+
temperature: options.parameters?.temperature,
|
|
3267
|
+
maxTokens: options.parameters?.maxTokens,
|
|
3268
|
+
topP: options.parameters?.topP,
|
|
3269
|
+
reasoningEffort: options.parameters?.reasoningEffort,
|
|
3270
|
+
stream: false,
|
|
3271
|
+
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
3272
|
+
conversationId: this.currentConversationId,
|
|
3273
|
+
});
|
|
3274
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
3275
|
+
retryRequest.previousResponseId = this.lastResponseId;
|
|
3276
|
+
}
|
|
3277
|
+
retryRequest.conversationId = this.currentSessionId;
|
|
3278
|
+
let retryApiResponse;
|
|
3279
|
+
if (this.retryMiddleware) {
|
|
3280
|
+
const retryResult = await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(retryRequest, effectiveModel), 'inaction_nudge_retry');
|
|
3281
|
+
retryApiResponse = retryResult.result;
|
|
3282
|
+
}
|
|
3283
|
+
else {
|
|
3284
|
+
retryApiResponse = await this.apiClient.sendRequest(retryRequest, effectiveModel);
|
|
3285
|
+
}
|
|
3286
|
+
const retryConverted = this.gatewayTranslation.convertResponse(retryApiResponse.data, effectiveModel, {
|
|
3287
|
+
sessionId: this.currentSessionId,
|
|
3288
|
+
conversationId: this.currentConversationId,
|
|
3289
|
+
turnNumber: this.turnNumber + 1,
|
|
3290
|
+
});
|
|
3291
|
+
if (effectiveModel.api.pattern === 'responses' && retryApiResponse.data?.id) {
|
|
3292
|
+
this.lastResponseId = retryApiResponse.data.id;
|
|
3293
|
+
this.lastResponseIdProvider = effectiveModel.provider;
|
|
3294
|
+
}
|
|
3295
|
+
if (retryConverted.messages && retryConverted.messages.length > 0) {
|
|
3296
|
+
const retryAssistantCanonical = retryConverted.messages[0];
|
|
3297
|
+
const retryAssistantMessage = {
|
|
3298
|
+
uuid: retryAssistantCanonical.uuid,
|
|
3299
|
+
timestamp: retryAssistantCanonical.timestamp,
|
|
3300
|
+
type: 'assistant',
|
|
3301
|
+
message: {
|
|
3302
|
+
role: 'assistant',
|
|
3303
|
+
content: retryAssistantCanonical.content,
|
|
3304
|
+
},
|
|
3305
|
+
timeline: {
|
|
3306
|
+
sessionId: this.currentSessionId,
|
|
3307
|
+
conversationId: this.currentConversationId,
|
|
3308
|
+
turnNumber: this.turnNumber + 1,
|
|
3309
|
+
},
|
|
3310
|
+
model: {
|
|
3311
|
+
id: effectiveModel.id,
|
|
3312
|
+
provider: effectiveModel.provider,
|
|
3313
|
+
apiPattern: effectiveModel.api.pattern,
|
|
3314
|
+
},
|
|
3315
|
+
...(retryConverted.usage && { usage: retryConverted.usage }),
|
|
3316
|
+
};
|
|
3317
|
+
this.messageHistory.push(retryAssistantMessage);
|
|
3318
|
+
await this.historyStore.appendMessage(this.currentSessionId, retryAssistantMessage);
|
|
3319
|
+
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
3320
|
+
const retryText = retryAssistantCanonical.content
|
|
3321
|
+
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
3322
|
+
.map((b) => b.text)
|
|
3323
|
+
.join('');
|
|
3324
|
+
if (retryText.trim().length > 0) {
|
|
3325
|
+
yield { type: 'text_delta', delta: retryText };
|
|
3326
|
+
}
|
|
3327
|
+
continue; // re-enter loop — retry may have produced tool_use
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
catch (retryErr) {
|
|
3331
|
+
console.warn(`[Orchestrator Streaming] Inaction-nudge retry failed:`, retryErr);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
2928
3335
|
hasToolUse = false;
|
|
2929
3336
|
break;
|
|
2930
3337
|
}
|
|
@@ -2964,6 +3371,45 @@ export class CortexOrchestrator {
|
|
|
2964
3371
|
// Execute tools (reuse existing method)
|
|
2965
3372
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
2966
3373
|
clearTimeout(timeoutId);
|
|
3374
|
+
// Unified Outcome Ladder (streaming parity with sendMessage).
|
|
3375
|
+
ladderSignal = null;
|
|
3376
|
+
{
|
|
3377
|
+
const inputById = new Map(toolUseBlocks.map((b) => [b.id, b.input]));
|
|
3378
|
+
for (const tr of toolResults) {
|
|
3379
|
+
if (tr.tool_name === 'EndTurn')
|
|
3380
|
+
continue;
|
|
3381
|
+
const input = inputById.get(tr.tool_use_id);
|
|
3382
|
+
if (input === undefined)
|
|
3383
|
+
continue;
|
|
3384
|
+
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
3385
|
+
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
3386
|
+
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
3387
|
+
if (sig)
|
|
3388
|
+
ladderSignal = sig;
|
|
3389
|
+
if (ladder.action === 'break')
|
|
3390
|
+
ladderBreak = sig;
|
|
3391
|
+
// Observability (backlog item 3): streaming parity — bank each
|
|
3392
|
+
// escalation rung as a decision-store event.
|
|
3393
|
+
if (ladder.action !== 'none') {
|
|
3394
|
+
const store = this.getDecisionStore();
|
|
3395
|
+
if (store) {
|
|
3396
|
+
void store
|
|
3397
|
+
.recordEvent({
|
|
3398
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3399
|
+
kind: 'loop_escalation',
|
|
3400
|
+
toolName: tr.tool_name,
|
|
3401
|
+
detail: {
|
|
3402
|
+
rung: ladder.action,
|
|
3403
|
+
count: ladder.count,
|
|
3404
|
+
approachHash: outcome.approachHash,
|
|
3405
|
+
...(outcome.family ? { family: outcome.family } : {}),
|
|
3406
|
+
},
|
|
3407
|
+
})
|
|
3408
|
+
.catch(() => { });
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
2967
3413
|
// R21 (2026-05-15): same fix as non-streaming path — count consecutive
|
|
2968
3414
|
// ITERATIONS WITH ZERO SUCCESSFUL TOOLS, not cumulative individual
|
|
2969
3415
|
// failures. Reset whenever at least one tool succeeded.
|
|
@@ -3115,14 +3561,38 @@ export class CortexOrchestrator {
|
|
|
3115
3561
|
const progressStalled = isToolProgressStalled(allToolCalls);
|
|
3116
3562
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
3117
3563
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
3118
|
-
if (budgetSignal || diversityWarning) {
|
|
3119
|
-
const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
|
|
3564
|
+
if (budgetSignal || diversityWarning || ladderSignal) {
|
|
3565
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
|
|
3120
3566
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
3121
3567
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
3122
3568
|
const block = lastMsg.message.content[0];
|
|
3123
3569
|
const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
3124
3570
|
block.content = existing + '\n\n' + signals;
|
|
3571
|
+
// Observability (backlog item 3): post-persist mutation — bank a
|
|
3572
|
+
// steering_injected event (streaming parity).
|
|
3573
|
+
const store = this.getDecisionStore();
|
|
3574
|
+
if (store) {
|
|
3575
|
+
const kinds = [
|
|
3576
|
+
budgetSignal ? 'budget' : null,
|
|
3577
|
+
diversityWarning ? 'diversity' : null,
|
|
3578
|
+
ladderSignal ? 'ladder' : null,
|
|
3579
|
+
].filter(Boolean);
|
|
3580
|
+
void store
|
|
3581
|
+
.recordEvent({
|
|
3582
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3583
|
+
kind: 'steering_injected',
|
|
3584
|
+
detail: { kinds, iteration: toolCallIteration, chars: signals.length, streaming: true },
|
|
3585
|
+
})
|
|
3586
|
+
.catch(() => { });
|
|
3587
|
+
}
|
|
3125
3588
|
}
|
|
3589
|
+
ladderSignal = null; // consumed
|
|
3590
|
+
}
|
|
3591
|
+
// Ladder break (streaming parity): instruction already injected above;
|
|
3592
|
+
// exit R29b-style so the streaming synthesis net closes the turn.
|
|
3593
|
+
if (ladderBreak) {
|
|
3594
|
+
console.warn('[Orchestrator Streaming] LoopLadder break: repeated failing approach — forcing synthesis.');
|
|
3595
|
+
break;
|
|
3126
3596
|
}
|
|
3127
3597
|
// R29b hard cap (streaming parity): force-stop at 2x soft budget;
|
|
3128
3598
|
// the post-loop R29a streaming net then synthesizes a deliverable.
|
|
@@ -3162,7 +3632,11 @@ export class CortexOrchestrator {
|
|
|
3162
3632
|
// Proactive context management - ensure history fits before streaming continuation
|
|
3163
3633
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3164
3634
|
// Anchor lift — streaming mirror of the sendMessage continuation path.
|
|
3165
|
-
if (!this.anchorLifted &&
|
|
3635
|
+
if (!this.anchorLifted &&
|
|
3636
|
+
resolveToolAnchor(process.env, this.cardAnchorProfile) &&
|
|
3637
|
+
// Persist frame (backlog item 5): the anchored surface IS the session
|
|
3638
|
+
// surface — never lift, never deliver the deferred corpus.
|
|
3639
|
+
resolveFrameProfile(process.env, this.cardFrameProfile) !== 'persist') {
|
|
3166
3640
|
this.anchorLifted = true;
|
|
3167
3641
|
if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
|
|
3168
3642
|
toolsToUse = allTools;
|
|
@@ -5878,6 +6352,28 @@ export class CortexOrchestrator {
|
|
|
5878
6352
|
}
|
|
5879
6353
|
return this.routerMatrix;
|
|
5880
6354
|
}
|
|
6355
|
+
/** The turn's real user prompt text — the most recent user message that is
|
|
6356
|
+
* neither a tool_result carrier nor an injected <system-reminder> steering
|
|
6357
|
+
* message. Used by EndTurn Stage 4 to judge task shape. Empty string when
|
|
6358
|
+
* none found (Stage 4 then never fires its task-shape branch). */
|
|
6359
|
+
lastRealUserText() {
|
|
6360
|
+
for (let i = this.messageHistory.length - 1; i >= 0; i--) {
|
|
6361
|
+
const m = this.messageHistory[i];
|
|
6362
|
+
const isUser = m?.type === 'user' || m?.message?.role === 'user';
|
|
6363
|
+
if (!isUser)
|
|
6364
|
+
continue;
|
|
6365
|
+
const content = m?.message?.content;
|
|
6366
|
+
if (Array.isArray(content) && content.some((b) => b?.type === 'tool_result'))
|
|
6367
|
+
continue;
|
|
6368
|
+
const text = Array.isArray(content)
|
|
6369
|
+
? content.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n')
|
|
6370
|
+
: typeof content === 'string' ? content : '';
|
|
6371
|
+
if (!text.trim() || text.trimStart().startsWith('<system-reminder>'))
|
|
6372
|
+
continue;
|
|
6373
|
+
return text;
|
|
6374
|
+
}
|
|
6375
|
+
return '';
|
|
6376
|
+
}
|
|
5881
6377
|
getDecisionStore() {
|
|
5882
6378
|
if (this.isRecordingEnabled() === false && this.isLookupEnabled() === false) {
|
|
5883
6379
|
return undefined;
|
|
@@ -5907,7 +6403,14 @@ export class CortexOrchestrator {
|
|
|
5907
6403
|
const store = this.getDecisionStore();
|
|
5908
6404
|
if (!store)
|
|
5909
6405
|
return result;
|
|
5910
|
-
|
|
6406
|
+
// Unified outcome layer (docs/UNIFIED_OUTCOME_LADDER.md): ONE failure
|
|
6407
|
+
// semantics for recording + reminders. Critically, a bash command that
|
|
6408
|
+
// ran and exited nonzero is a FAILURE here even though its tool result
|
|
6409
|
+
// is not is_error on the wire (ShellTool returns success results for
|
|
6410
|
+
// exit!=0) — previously those loops were recorded as successes, so the
|
|
6411
|
+
// prior/family reminders never fired on the dominant thrash mode.
|
|
6412
|
+
const outcome = classifyToolOutcome(toolUse.name, toolUse.input, result);
|
|
6413
|
+
const inputHash = outcome.exactHash;
|
|
5911
6414
|
let augmented = result;
|
|
5912
6415
|
// 1. Lookup priors (read side) — only emits reminder when failures exist.
|
|
5913
6416
|
// Fetch the 3 most recent matching decisions so the formatter can
|
|
@@ -5922,6 +6425,21 @@ export class CortexOrchestrator {
|
|
|
5922
6425
|
if (reminder && result.content) {
|
|
5923
6426
|
augmented = { ...result, content: reminder + result.content };
|
|
5924
6427
|
}
|
|
6428
|
+
else if (outcome.status !== 'ok' && result.content) {
|
|
6429
|
+
// Family lens (AHE borrow): no exact-input prior fired, but the
|
|
6430
|
+
// CURRENT failure may be the latest of a repeated-approach family
|
|
6431
|
+
// across different inputs — hint once the family has >=2 prior
|
|
6432
|
+
// failures spanning >=2 distinct inputs. Gated on the UNIFIED
|
|
6433
|
+
// outcome (not is_error) so failing-command loops are covered.
|
|
6434
|
+
const family = outcome.family ?? classifyErrorFamily(String(result.content).slice(0, 200));
|
|
6435
|
+
if (family) {
|
|
6436
|
+
const ff = await store.familyFailures(toolUse.name, family);
|
|
6437
|
+
const famReminder = formatFamilyReminder(toolUse.name, family, ff.count, ff.distinctInputs, ff.recent);
|
|
6438
|
+
if (famReminder) {
|
|
6439
|
+
augmented = { ...result, content: famReminder + result.content };
|
|
6440
|
+
}
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
5925
6443
|
}
|
|
5926
6444
|
catch (err) {
|
|
5927
6445
|
if (this.config.debug) {
|
|
@@ -5937,8 +6455,10 @@ export class CortexOrchestrator {
|
|
|
5937
6455
|
sessionId: this.currentSessionId ?? 'unknown',
|
|
5938
6456
|
toolName: toolUse.name,
|
|
5939
6457
|
input: toolUse.input,
|
|
5940
|
-
|
|
5941
|
-
|
|
6458
|
+
// Unified outcome: exit!=0 commands are FAILURES for the store even
|
|
6459
|
+
// though their wire result is not is_error (see outcome above).
|
|
6460
|
+
success: outcome.status === 'ok',
|
|
6461
|
+
...(outcome.status !== 'ok'
|
|
5942
6462
|
? { errorSnippet: String(result.content).slice(0, 200) }
|
|
5943
6463
|
: {}),
|
|
5944
6464
|
});
|