@nexus-cortex/core 4.69.1 → 4.71.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/adapters/ChatCompletionsAPIAdapter.d.ts +17 -2
- package/dist/adapters/ChatCompletionsAPIAdapter.d.ts.map +1 -1
- package/dist/adapters/ChatCompletionsAPIAdapter.js +26 -3
- package/dist/adapters/ChatCompletionsAPIAdapter.js.map +1 -1
- package/dist/adapters/MessagesAPIAdapter.d.ts.map +1 -1
- package/dist/adapters/MessagesAPIAdapter.js +15 -0
- package/dist/adapters/MessagesAPIAdapter.js.map +1 -1
- package/dist/models/cards/deepseek/deepseek-v4-flash-vision-exp.d.ts +28 -0
- package/dist/models/cards/deepseek/deepseek-v4-flash-vision-exp.d.ts.map +1 -0
- package/dist/models/cards/deepseek/deepseek-v4-flash-vision-exp.js +47 -0
- package/dist/models/cards/deepseek/deepseek-v4-flash-vision-exp.js.map +1 -0
- package/dist/models/cards/deepseek/index.d.ts +5 -2
- package/dist/models/cards/deepseek/index.d.ts.map +1 -1
- package/dist/models/cards/deepseek/index.js +5 -2
- package/dist/models/cards/deepseek/index.js.map +1 -1
- package/dist/models/configurators/DeepSeekConfigurator.d.ts +5 -0
- package/dist/models/configurators/DeepSeekConfigurator.d.ts.map +1 -1
- package/dist/models/configurators/DeepSeekConfigurator.js +2 -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/models/registry/ModularModelRegistry.d.ts.map +1 -1
- package/dist/models/registry/ModularModelRegistry.js +1 -0
- package/dist/models/registry/ModularModelRegistry.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +23 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +628 -22
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/imageTtl.d.ts +37 -0
- package/dist/orchestrator/imageTtl.d.ts.map +1 -0
- package/dist/orchestrator/imageTtl.js +72 -0
- package/dist/orchestrator/imageTtl.js.map +1 -0
- 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 +26 -3
- package/dist/tools/ToolProfile.js.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.d.ts.map +1 -1
- package/dist/tools/registries/BaseToolRegistry.js +42 -1
- 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,17 @@ 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 { applyImageTtlForRequest } from './imageTtl.js';
|
|
43
|
+
import { verifyRequirements, resolveEndTurnRequirementsMode } from './requirementsVerification.js';
|
|
38
44
|
import { ModelRouterMatrix } from '../training/ModelRouterMatrix.js';
|
|
39
45
|
import { classifyTask } from '../training/TaskClassifier.js';
|
|
40
46
|
import { closestToolMatches } from './toolNameMatcher.js';
|
|
@@ -143,6 +149,10 @@ export class CortexOrchestrator {
|
|
|
143
149
|
/** The active model card's anchorProfile (captured at request assembly) —
|
|
144
150
|
* env CORTEX_TOOL_ANCHOR still overrides inside resolveToolAnchor. */
|
|
145
151
|
cardAnchorProfile = null;
|
|
152
|
+
/** Card frameProfile ('lifted'|'persist') captured with the anchor — under
|
|
153
|
+
* 'persist' the anchor never lifts (backlog item 5; env
|
|
154
|
+
* CORTEX_TOOL_ANCHOR_PERSIST overrides in resolveFrameProfile). */
|
|
155
|
+
cardFrameProfile = null;
|
|
146
156
|
/** P6 deferral (CORTEX_PROMPT_MASS=defer): the static corpus is delivered
|
|
147
157
|
* exactly once, at the anchor-lift boundary. One-shot per orchestrator. */
|
|
148
158
|
deferredCorpusDelivered = false;
|
|
@@ -316,8 +326,10 @@ export class CortexOrchestrator {
|
|
|
316
326
|
// deferred setup tools are actually findable.
|
|
317
327
|
const extras = [...this.getMcpManagementTools(), ...this.getContextManagementTools()];
|
|
318
328
|
const seen = new Set();
|
|
329
|
+
const catalogVision = this.getModel(this.currentModelId ?? '')?.vision === true;
|
|
319
330
|
return [...toolFactory.getAllTools(), ...extras]
|
|
320
331
|
.filter((t) => endTurnGateOn || t.name !== 'EndTurn')
|
|
332
|
+
.filter((t) => catalogVision || t.name !== 'ReadImage')
|
|
321
333
|
.filter((t) => (seen.has(t.name) ? false : (seen.add(t.name), true)))
|
|
322
334
|
.map((t) => ({
|
|
323
335
|
name: namingHandler.convertName(t.name, convention),
|
|
@@ -446,9 +458,12 @@ export class CortexOrchestrator {
|
|
|
446
458
|
const model = this.getModel(modelId);
|
|
447
459
|
// Phase 1 & Phase 2: Get tools (used for both injection context and API request)
|
|
448
460
|
const endTurnGateOn = (process.env.CORTEX_ENDTURN_GATE) === 'true';
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
461
|
+
// Vision gate (item 7): ReadImage is offered ONLY to probe-verified vision
|
|
462
|
+
// cards — a text-only model must never see or reach the image path.
|
|
463
|
+
const visionOn = model?.vision === true;
|
|
464
|
+
const factoryTools = this.applyBaseToolAllowlist(toolFactory.getAllTools()
|
|
465
|
+
.filter(t => endTurnGateOn || t.name !== 'EndTurn')
|
|
466
|
+
.filter(t => visionOn || t.name !== 'ReadImage'));
|
|
452
467
|
// Tool-profile: MCP/management/context tools bypass ToolFactory, so the
|
|
453
468
|
// bash-only arm must suppress them here or the surface leaks (lean keeps
|
|
454
469
|
// them — they are part of the "guided" surface under test).
|
|
@@ -699,6 +714,7 @@ export class CortexOrchestrator {
|
|
|
699
714
|
// Card-level home door: capture the active card's anchorProfile (env
|
|
700
715
|
// CORTEX_TOOL_ANCHOR overrides inside the resolver; env 'none' disables).
|
|
701
716
|
this.cardAnchorProfile = effectiveModel.anchorProfile ?? null;
|
|
717
|
+
this.cardFrameProfile = effectiveModel.frameProfile ?? null;
|
|
702
718
|
// Thread the CARD's anchor to the executors (per-orchestrator config, by
|
|
703
719
|
// reference) so framing-aware executor behavior (ShellTool steering
|
|
704
720
|
// default) is model-card/registry-scoped, not process-global env.
|
|
@@ -978,6 +994,9 @@ export class CortexOrchestrator {
|
|
|
978
994
|
// Round 18b: track whether we've already retried on empty response so
|
|
979
995
|
// we don't loop forever. See empty-detection block below.
|
|
980
996
|
let emptyResponseRetryUsed = false;
|
|
997
|
+
// Inaction guard (backlog item 2 — the ladder's inverse): single-nudge
|
|
998
|
+
// bound for the actless-verbose retry. Default OFF via env.
|
|
999
|
+
let inactionNudgeUsed = false;
|
|
981
1000
|
// EndTurn gate (Stage 1): the turn must not complete until the model
|
|
982
1001
|
// calls the EndTurn attestation tool. Steering is ignored by some
|
|
983
1002
|
// models (grok-4.3); a required tool call is not. Bounded so a model
|
|
@@ -1021,6 +1040,13 @@ export class CortexOrchestrator {
|
|
|
1021
1040
|
const thisTurnToolOutputs = [];
|
|
1022
1041
|
// Loop detection: Track recent tool calls to detect repetitive loops
|
|
1023
1042
|
const recentToolCalls = [];
|
|
1043
|
+
// Unified Outcome Ladder (docs/UNIFIED_OUTCOME_LADDER.md): per-approach
|
|
1044
|
+
// FAILURE escalation — remind(2)→diversify(4)→break(6) over normalized
|
|
1045
|
+
// near-duplicate approaches. Complements isToolProgressStalled (which is
|
|
1046
|
+
// outcome-blind exact-hash cycling) by firing earlier on failing thrash.
|
|
1047
|
+
const loopLadder = new LoopLadder();
|
|
1048
|
+
let ladderBreak = null;
|
|
1049
|
+
let ladderSignal = null;
|
|
1024
1050
|
// Track all tool uses across iterations for response
|
|
1025
1051
|
const allExecutedToolUses = [];
|
|
1026
1052
|
// Track tool call counts for diversity warning
|
|
@@ -1198,6 +1224,130 @@ export class CortexOrchestrator {
|
|
|
1198
1224
|
continue;
|
|
1199
1225
|
}
|
|
1200
1226
|
}
|
|
1227
|
+
// Inaction guard (backlog item 2 — the loop ladder's inverse).
|
|
1228
|
+
// TB2 2×2: persist-frame arms produced never-acted failures (5
|
|
1229
|
+
// pro-persist) — a huge reason-to-the-wall text dump with ZERO tool
|
|
1230
|
+
// calls, invisible to every call-counting guard. One bounded
|
|
1231
|
+
// steering retry, R18b-style; default OFF (CORTEX_INACTION_NUDGE).
|
|
1232
|
+
{
|
|
1233
|
+
const responseChars = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
1234
|
+
? currentAssistantCanonicalMessage.content
|
|
1235
|
+
.map((b) => b?.type === 'text' ? (b.text || '') : b?.type === 'thinking' ? (b.thinking || '') : '')
|
|
1236
|
+
.join('').length
|
|
1237
|
+
: String(currentAssistantCanonicalMessage.content ?? '').length;
|
|
1238
|
+
if (hasVisibleText &&
|
|
1239
|
+
!structuredOutputState?.result &&
|
|
1240
|
+
shouldNudgeInaction({
|
|
1241
|
+
responseChars,
|
|
1242
|
+
toolUseBlocksThisResponse: 0,
|
|
1243
|
+
executedToolCallsThisTurn: allExecutedToolUses.length,
|
|
1244
|
+
toolsOffered: toolsToUse.length,
|
|
1245
|
+
turnNumber: this.turnNumber,
|
|
1246
|
+
alreadyNudged: inactionNudgeUsed,
|
|
1247
|
+
})) {
|
|
1248
|
+
inactionNudgeUsed = true;
|
|
1249
|
+
console.warn(`[Orchestrator] Inaction guard: actless-verbose response (${responseChars} chars, 0 tool calls). ` +
|
|
1250
|
+
`Injecting act-first nudge (single retry).`);
|
|
1251
|
+
// Observability (backlog item 3): bank the event either way it
|
|
1252
|
+
// resolves — the distiller counts nudge→acted conversions.
|
|
1253
|
+
{
|
|
1254
|
+
const store = this.getDecisionStore();
|
|
1255
|
+
if (store) {
|
|
1256
|
+
void store
|
|
1257
|
+
.recordEvent({
|
|
1258
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1259
|
+
kind: 'inaction_nudge',
|
|
1260
|
+
detail: { responseChars, turnNumber: this.turnNumber },
|
|
1261
|
+
})
|
|
1262
|
+
.catch(() => { });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const inactionUserMessage = {
|
|
1266
|
+
uuid: uuidv4(),
|
|
1267
|
+
timestamp: new Date().toISOString(),
|
|
1268
|
+
type: 'user',
|
|
1269
|
+
message: {
|
|
1270
|
+
role: 'user',
|
|
1271
|
+
content: [{
|
|
1272
|
+
type: 'text',
|
|
1273
|
+
text: `<system-reminder>${formatInactionNudge()}</system-reminder>`,
|
|
1274
|
+
}],
|
|
1275
|
+
},
|
|
1276
|
+
timeline: {
|
|
1277
|
+
sessionId: this.currentSessionId,
|
|
1278
|
+
conversationId: this.currentConversationId,
|
|
1279
|
+
turnNumber: this.turnNumber + 1,
|
|
1280
|
+
},
|
|
1281
|
+
model: {
|
|
1282
|
+
id: effectiveModel.id,
|
|
1283
|
+
provider: effectiveModel.provider,
|
|
1284
|
+
apiPattern: effectiveModel.api.pattern,
|
|
1285
|
+
},
|
|
1286
|
+
};
|
|
1287
|
+
this.messageHistory.push(inactionUserMessage);
|
|
1288
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionUserMessage);
|
|
1289
|
+
await this.ensureHistoryFitsModel(effectiveModel);
|
|
1290
|
+
const inactionCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
1291
|
+
const inactionRequest = this.gatewayTranslation.prepareRequest(inactionCanonicalHistory, toolsToUse, effectiveModel, {
|
|
1292
|
+
temperature: options.parameters?.temperature,
|
|
1293
|
+
maxTokens: options.parameters?.maxTokens,
|
|
1294
|
+
topP: options.parameters?.topP,
|
|
1295
|
+
reasoningEffort: options.parameters?.reasoningEffort,
|
|
1296
|
+
stream: options.streaming,
|
|
1297
|
+
staticSystemPrompt: this.currentStaticSystemPrompt, // R28
|
|
1298
|
+
conversationId: this.currentConversationId, // R28b
|
|
1299
|
+
});
|
|
1300
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
1301
|
+
inactionRequest.previousResponseId = this.lastResponseId;
|
|
1302
|
+
}
|
|
1303
|
+
inactionRequest.conversationId = this.currentSessionId;
|
|
1304
|
+
let inactionApiResponse;
|
|
1305
|
+
if (this.retryMiddleware) {
|
|
1306
|
+
const r = await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(inactionRequest, effectiveModel), 'inaction_nudge_retry');
|
|
1307
|
+
inactionApiResponse = r.result;
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
inactionApiResponse = await this.apiClient.sendRequest(inactionRequest, effectiveModel);
|
|
1311
|
+
}
|
|
1312
|
+
const inactionConverted = this.gatewayTranslation.convertResponse(inactionApiResponse.data, effectiveModel, {
|
|
1313
|
+
sessionId: this.currentSessionId,
|
|
1314
|
+
conversationId: this.currentConversationId,
|
|
1315
|
+
turnNumber: this.turnNumber + 1,
|
|
1316
|
+
});
|
|
1317
|
+
if (effectiveModel.api.pattern === 'responses' && inactionApiResponse.data?.id) {
|
|
1318
|
+
this.lastResponseId = inactionApiResponse.data.id;
|
|
1319
|
+
this.lastResponseIdProvider = effectiveModel.provider; // R20a
|
|
1320
|
+
}
|
|
1321
|
+
if (inactionConverted.messages && inactionConverted.messages.length > 0) {
|
|
1322
|
+
const inactionAssistantCanonical = inactionConverted.messages[0];
|
|
1323
|
+
const inactionAssistantMessage = {
|
|
1324
|
+
uuid: inactionAssistantCanonical.uuid,
|
|
1325
|
+
timestamp: inactionAssistantCanonical.timestamp,
|
|
1326
|
+
type: 'assistant',
|
|
1327
|
+
message: {
|
|
1328
|
+
role: 'assistant',
|
|
1329
|
+
content: inactionAssistantCanonical.content,
|
|
1330
|
+
},
|
|
1331
|
+
timeline: {
|
|
1332
|
+
sessionId: this.currentSessionId,
|
|
1333
|
+
conversationId: this.currentConversationId,
|
|
1334
|
+
turnNumber: this.turnNumber + 1,
|
|
1335
|
+
},
|
|
1336
|
+
model: {
|
|
1337
|
+
id: effectiveModel.id,
|
|
1338
|
+
provider: effectiveModel.provider,
|
|
1339
|
+
apiPattern: effectiveModel.api.pattern,
|
|
1340
|
+
},
|
|
1341
|
+
...(inactionConverted.usage && { usage: inactionConverted.usage }),
|
|
1342
|
+
};
|
|
1343
|
+
this.messageHistory.push(inactionAssistantMessage);
|
|
1344
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionAssistantMessage);
|
|
1345
|
+
currentAssistantMessage = inactionAssistantMessage;
|
|
1346
|
+
currentAssistantCanonicalMessage = inactionAssistantCanonical;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1201
1351
|
// EndTurn gate (Stage 1): refuse to finalize until the model has
|
|
1202
1352
|
// called the EndTurn attestation tool. Only arms when the turn
|
|
1203
1353
|
// actually used a (non-EndTurn) tool — pure-language turns bypass.
|
|
@@ -1221,8 +1371,27 @@ export class CortexOrchestrator {
|
|
|
1221
1371
|
if (!cv.ok)
|
|
1222
1372
|
stage3Violations = cv.violations;
|
|
1223
1373
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1374
|
+
// Fallback observability (backlog item 1.4): when the nudge bound
|
|
1375
|
+
// is exhausted and the gate is STILL unsatisfied, the turn ships
|
|
1376
|
+
// anyway (liveness beats purity) — but that fallback-accept was
|
|
1377
|
+
// previously silent. Bank an event so the distiller can see
|
|
1378
|
+
// un-attested passes.
|
|
1379
|
+
const endTurnGateUnsatisfied = endTurnGateEnabled && turnUsedTools && (!endTurnCalled || stage3Violations.length > 0);
|
|
1380
|
+
if (endTurnGateUnsatisfied && endTurnNudges >= END_TURN_MAX_NUDGES) {
|
|
1381
|
+
const fallbackReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1382
|
+
console.warn(`[Orchestrator] EndTurn gate FALLBACK-ACCEPT: ${fallbackReason} after ${endTurnNudges} nudges — turn ships un-attested.`);
|
|
1383
|
+
const store = this.getDecisionStore();
|
|
1384
|
+
if (store) {
|
|
1385
|
+
void store
|
|
1386
|
+
.recordEvent({
|
|
1387
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1388
|
+
kind: 'endturn_gate_fallback',
|
|
1389
|
+
detail: { reason: fallbackReason, nudges: endTurnNudges, iteration: toolCallIteration },
|
|
1390
|
+
})
|
|
1391
|
+
.catch(() => { });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (endTurnGateUnsatisfied && endTurnNudges < END_TURN_MAX_NUDGES) {
|
|
1226
1395
|
endTurnNudges++;
|
|
1227
1396
|
const gateReason = !endTurnCalled ? 'missing-EndTurn' : 'coordinate-violation';
|
|
1228
1397
|
console.warn(`[Orchestrator] EndTurn gate: ${gateReason} ` +
|
|
@@ -1498,6 +1667,49 @@ export class CortexOrchestrator {
|
|
|
1498
1667
|
try {
|
|
1499
1668
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
1500
1669
|
clearTimeout(timeoutId);
|
|
1670
|
+
// Unified Outcome Ladder: observe each result's TRUE outcome (exit
|
|
1671
|
+
// codes, not wire is_error) and escalate on repeated failing
|
|
1672
|
+
// approaches. The strongest signal this batch is injected at the
|
|
1673
|
+
// signals block below; a 'break' rung ends the loop R29b-style.
|
|
1674
|
+
ladderSignal = null;
|
|
1675
|
+
{
|
|
1676
|
+
const inputById = new Map(toolUseBlocks.map((b) => [b.id, b.input]));
|
|
1677
|
+
for (const tr of toolResults) {
|
|
1678
|
+
if (tr.tool_name === 'EndTurn')
|
|
1679
|
+
continue;
|
|
1680
|
+
const input = inputById.get(tr.tool_use_id);
|
|
1681
|
+
if (input === undefined)
|
|
1682
|
+
continue;
|
|
1683
|
+
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
1684
|
+
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
1685
|
+
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
1686
|
+
if (sig)
|
|
1687
|
+
ladderSignal = sig;
|
|
1688
|
+
if (ladder.action === 'break')
|
|
1689
|
+
ladderBreak = sig;
|
|
1690
|
+
// Observability (backlog item 3): escalations are invisible in
|
|
1691
|
+
// the session record (signals inject post-persist) — bank each
|
|
1692
|
+
// rung as a decision-store event for the distiller.
|
|
1693
|
+
if (ladder.action !== 'none') {
|
|
1694
|
+
const store = this.getDecisionStore();
|
|
1695
|
+
if (store) {
|
|
1696
|
+
void store
|
|
1697
|
+
.recordEvent({
|
|
1698
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1699
|
+
kind: 'loop_escalation',
|
|
1700
|
+
toolName: tr.tool_name,
|
|
1701
|
+
detail: {
|
|
1702
|
+
rung: ladder.action,
|
|
1703
|
+
count: ladder.count,
|
|
1704
|
+
approachHash: outcome.approachHash,
|
|
1705
|
+
...(outcome.family ? { family: outcome.family } : {}),
|
|
1706
|
+
},
|
|
1707
|
+
})
|
|
1708
|
+
.catch(() => { });
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1501
1713
|
// File-history snapshots (Claude Code parity — previously built but
|
|
1502
1714
|
// UNWIRED: FileCheckpointManager existed with zero markModified
|
|
1503
1715
|
// callers and its snapshot message was discarded). Mark files
|
|
@@ -1578,7 +1790,31 @@ export class CortexOrchestrator {
|
|
|
1578
1790
|
lastEndTurnCitations = Array.isArray(cits) ? cits : undefined; // Stage 3 baseline
|
|
1579
1791
|
const verdict = verifyCitationsGrounded(cits, thisTurnToolOutputs.join('\n'));
|
|
1580
1792
|
if (verdict.grounded) {
|
|
1581
|
-
|
|
1793
|
+
// Stage 4 (backlog item 1, CORTEX_ENDTURN_REQUIREMENTS): verify
|
|
1794
|
+
// the requirements attestation — the wrong-artifact weapon.
|
|
1795
|
+
// Stage 2 verifies citations; nothing before this forced
|
|
1796
|
+
// re-reading the TASK. Rides the same nudge budget: a Stage-4
|
|
1797
|
+
// reject leaves the gate unsatisfied so the bounded
|
|
1798
|
+
// fallback-accept still guarantees liveness.
|
|
1799
|
+
if (resolveEndTurnRequirementsMode()) {
|
|
1800
|
+
const s4 = verifyRequirements({
|
|
1801
|
+
requirements: etUse?.input?.requirements,
|
|
1802
|
+
verification: etUse?.input?.verification,
|
|
1803
|
+
userTaskText: this.lastRealUserText(),
|
|
1804
|
+
turnUsedMutatingTool,
|
|
1805
|
+
});
|
|
1806
|
+
if (s4.ok) {
|
|
1807
|
+
endTurnCalled = true;
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
tr.is_error = true;
|
|
1811
|
+
tr.content = s4.nudge;
|
|
1812
|
+
console.warn('[Orchestrator] Stage4: EndTurn rejected — requirements attestation unsatisfied.');
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
else {
|
|
1816
|
+
endTurnCalled = true;
|
|
1817
|
+
}
|
|
1582
1818
|
}
|
|
1583
1819
|
else {
|
|
1584
1820
|
const bad = verdict.ungrounded
|
|
@@ -1592,6 +1828,9 @@ export class CortexOrchestrator {
|
|
|
1592
1828
|
console.warn(`[Orchestrator] Stage2: EndTurn rejected — ${verdict.ungrounded.length} ungrounded citation(s).`);
|
|
1593
1829
|
}
|
|
1594
1830
|
}
|
|
1831
|
+
// Item 7: scrub ReadImage payloads from metadata BEFORE persist;
|
|
1832
|
+
// the images inject as a user message after the results land.
|
|
1833
|
+
const pendingImages = this.collectPendingImages(toolResults, effectiveModel.vision === true);
|
|
1595
1834
|
// Create tool_result messages and add to history
|
|
1596
1835
|
for (const toolResult of toolResults) {
|
|
1597
1836
|
const toolResultMessageId = uuidv4();
|
|
@@ -1726,14 +1965,43 @@ export class CortexOrchestrator {
|
|
|
1726
1965
|
const progressStalled = isToolProgressStalled(recentToolCalls);
|
|
1727
1966
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
1728
1967
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
1729
|
-
if (budgetSignal || diversityWarning) {
|
|
1730
|
-
const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
|
|
1968
|
+
if (budgetSignal || diversityWarning || ladderSignal) {
|
|
1969
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
|
|
1731
1970
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
1732
1971
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
1733
1972
|
const block = lastMsg.message.content[0];
|
|
1734
1973
|
const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
1735
1974
|
block.content = existing + '\n\n' + signals;
|
|
1975
|
+
// Observability (backlog item 3, micro-suite defect #4): this
|
|
1976
|
+
// mutation happens AFTER the tool_result was persisted — the
|
|
1977
|
+
// durable session lacks the steering the model saw. Bank a
|
|
1978
|
+
// steering_injected event so harvest/distill sees it. Session rows
|
|
1979
|
+
// stay append-only (never rewritten).
|
|
1980
|
+
const store = this.getDecisionStore();
|
|
1981
|
+
if (store) {
|
|
1982
|
+
const kinds = [
|
|
1983
|
+
budgetSignal ? 'budget' : null,
|
|
1984
|
+
diversityWarning ? 'diversity' : null,
|
|
1985
|
+
ladderSignal ? 'ladder' : null,
|
|
1986
|
+
].filter(Boolean);
|
|
1987
|
+
void store
|
|
1988
|
+
.recordEvent({
|
|
1989
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
1990
|
+
kind: 'steering_injected',
|
|
1991
|
+
detail: { kinds, iteration: toolCallIteration, chars: signals.length },
|
|
1992
|
+
})
|
|
1993
|
+
.catch(() => { });
|
|
1994
|
+
}
|
|
1736
1995
|
}
|
|
1996
|
+
ladderSignal = null; // consumed — never re-inject on later iterations
|
|
1997
|
+
}
|
|
1998
|
+
// Ladder break (teach-then-break): the break instruction is already in
|
|
1999
|
+
// the tool result above; exit the loop R29b-style so the post-loop
|
|
2000
|
+
// synthesis net produces an honest final answer — never the blunt
|
|
2001
|
+
// toolCallIteration=MAX turn-kill of the exact-match detector.
|
|
2002
|
+
if (ladderBreak) {
|
|
2003
|
+
console.warn('[Orchestrator] LoopLadder break: repeated failing approach — forcing synthesis.');
|
|
2004
|
+
break;
|
|
1737
2005
|
}
|
|
1738
2006
|
// R29b hard cap: a weaker model may ignore the firm budget reminders
|
|
1739
2007
|
// and keep spinning (benchmark: deepseek 46 successful local calls,
|
|
@@ -1774,6 +2042,10 @@ export class CortexOrchestrator {
|
|
|
1774
2042
|
console.error(`[Orchestrator Mentorship] Interleaved continuation thinking failed:`, error.message);
|
|
1775
2043
|
}
|
|
1776
2044
|
}
|
|
2045
|
+
// Item 7: attach any ReadImage images as a user message so the
|
|
2046
|
+
// continuation request carries them (after the signals block — it
|
|
2047
|
+
// targets the LAST message being a tool_result).
|
|
2048
|
+
await this.injectImageUserMessage(pendingImages, effectiveModel);
|
|
1777
2049
|
// Send tool results back to model and get continuation response
|
|
1778
2050
|
// Proactive context management - ensure history fits before continuation
|
|
1779
2051
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
@@ -1782,7 +2054,11 @@ export class CortexOrchestrator {
|
|
|
1782
2054
|
// full profile applies from here. Rebuild from allTools because the
|
|
1783
2055
|
// armed anchor narrowed toolsToUse at assembly. (When deferred loading
|
|
1784
2056
|
// is on, the re-filter block below performs the rebuild.)
|
|
1785
|
-
if (!this.anchorLifted &&
|
|
2057
|
+
if (!this.anchorLifted &&
|
|
2058
|
+
resolveToolAnchor(process.env, this.cardAnchorProfile) &&
|
|
2059
|
+
// Persist frame (backlog item 5): the anchored surface IS the session
|
|
2060
|
+
// surface — never lift, never deliver the deferred corpus.
|
|
2061
|
+
resolveFrameProfile(process.env, this.cardFrameProfile) !== 'persist') {
|
|
1786
2062
|
this.anchorLifted = true;
|
|
1787
2063
|
if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
|
|
1788
2064
|
toolsToUse = allTools;
|
|
@@ -2372,9 +2648,11 @@ export class CortexOrchestrator {
|
|
|
2372
2648
|
// === END MENTORSHIP HOOKS ===
|
|
2373
2649
|
// Get tools (needed for middleware injection decision)
|
|
2374
2650
|
const endTurnGateOn2 = (process.env.CORTEX_ENDTURN_GATE) === 'true';
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2651
|
+
// Vision gate (item 7): streaming mirror of the sendMessage filter.
|
|
2652
|
+
const visionOn2 = model?.vision === true;
|
|
2653
|
+
const factoryTools = this.applyBaseToolAllowlist(toolFactory.getAllTools()
|
|
2654
|
+
.filter(t => endTurnGateOn2 || t.name !== 'EndTurn')
|
|
2655
|
+
.filter(t => visionOn2 || t.name !== 'ReadImage'));
|
|
2378
2656
|
// Tool-profile: streaming mirror of the non-streaming suppression above.
|
|
2379
2657
|
const bashOnlyProfile2 = isNarrowProfile();
|
|
2380
2658
|
const mcpTools = this.mcpAutoInject && !bashOnlyProfile2 ? this.getMcpToolsAsCanonical() : [];
|
|
@@ -2564,6 +2842,7 @@ export class CortexOrchestrator {
|
|
|
2564
2842
|
// Card-level home door: capture the active card's anchorProfile (env
|
|
2565
2843
|
// CORTEX_TOOL_ANCHOR overrides inside the resolver; env 'none' disables).
|
|
2566
2844
|
this.cardAnchorProfile = effectiveModel.anchorProfile ?? null;
|
|
2845
|
+
this.cardFrameProfile = effectiveModel.frameProfile ?? null;
|
|
2567
2846
|
// Thread the CARD's anchor to the executors (per-orchestrator config, by
|
|
2568
2847
|
// reference) so framing-aware executor behavior (ShellTool steering
|
|
2569
2848
|
// default) is model-card/registry-scoped, not process-global env.
|
|
@@ -2766,7 +3045,12 @@ export class CortexOrchestrator {
|
|
|
2766
3045
|
let toolCallIteration = 0;
|
|
2767
3046
|
let totalToolErrors = 0;
|
|
2768
3047
|
let emptyResponseRetryUsed = false; // R32: parity with sendMessage R18b guard
|
|
3048
|
+
let inactionNudgeUsed = false; // Inaction guard (backlog item 2) — streaming parity
|
|
2769
3049
|
const allToolCalls = [];
|
|
3050
|
+
// Unified Outcome Ladder (streaming parity — see sendMessage).
|
|
3051
|
+
const loopLadder = new LoopLadder();
|
|
3052
|
+
let ladderBreak = null;
|
|
3053
|
+
let ladderSignal = null;
|
|
2770
3054
|
const allExecutedToolUses = [];
|
|
2771
3055
|
const toolCallCounts = new Map();
|
|
2772
3056
|
// DEBUG: Log canonical message content structure for tool extraction (streaming path)
|
|
@@ -2933,6 +3217,136 @@ export class CortexOrchestrator {
|
|
|
2933
3217
|
console.warn(`[Orchestrator Streaming] R32 retry failed:`, retryErr);
|
|
2934
3218
|
}
|
|
2935
3219
|
}
|
|
3220
|
+
// Inaction guard (backlog item 2 — streaming parity with sendMessage).
|
|
3221
|
+
// Actless-verbose first-turn response in a tool-capable request: one
|
|
3222
|
+
// bounded act-first steering retry. Default OFF (CORTEX_INACTION_NUDGE).
|
|
3223
|
+
{
|
|
3224
|
+
const responseChars = Array.isArray(currentAssistantCanonicalMessage.content)
|
|
3225
|
+
? currentAssistantCanonicalMessage.content
|
|
3226
|
+
.map((b) => b?.type === 'text' ? (b.text || '') : b?.type === 'thinking' ? (b.thinking || '') : '')
|
|
3227
|
+
.join('').length
|
|
3228
|
+
: String(currentAssistantCanonicalMessage.content ?? '').length;
|
|
3229
|
+
if (hasVisibleText &&
|
|
3230
|
+
shouldNudgeInaction({
|
|
3231
|
+
responseChars,
|
|
3232
|
+
toolUseBlocksThisResponse: 0,
|
|
3233
|
+
executedToolCallsThisTurn: allExecutedToolUses.length,
|
|
3234
|
+
toolsOffered: toolsToUse.length,
|
|
3235
|
+
turnNumber: this.turnNumber,
|
|
3236
|
+
alreadyNudged: inactionNudgeUsed,
|
|
3237
|
+
})) {
|
|
3238
|
+
inactionNudgeUsed = true;
|
|
3239
|
+
console.warn(`[Orchestrator Streaming] Inaction guard: actless-verbose response (${responseChars} chars, 0 tool calls). ` +
|
|
3240
|
+
`Injecting act-first nudge (single retry).`);
|
|
3241
|
+
{
|
|
3242
|
+
const store = this.getDecisionStore();
|
|
3243
|
+
if (store) {
|
|
3244
|
+
void store
|
|
3245
|
+
.recordEvent({
|
|
3246
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3247
|
+
kind: 'inaction_nudge',
|
|
3248
|
+
detail: { responseChars, turnNumber: this.turnNumber, streaming: true },
|
|
3249
|
+
})
|
|
3250
|
+
.catch(() => { });
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
const inactionUserMessage = {
|
|
3254
|
+
uuid: uuidv4(),
|
|
3255
|
+
timestamp: new Date().toISOString(),
|
|
3256
|
+
type: 'user',
|
|
3257
|
+
message: {
|
|
3258
|
+
role: 'user',
|
|
3259
|
+
content: [{
|
|
3260
|
+
type: 'text',
|
|
3261
|
+
text: `<system-reminder>${formatInactionNudge()}</system-reminder>`,
|
|
3262
|
+
}],
|
|
3263
|
+
},
|
|
3264
|
+
timeline: {
|
|
3265
|
+
sessionId: this.currentSessionId,
|
|
3266
|
+
conversationId: this.currentConversationId,
|
|
3267
|
+
turnNumber: this.turnNumber + 1,
|
|
3268
|
+
},
|
|
3269
|
+
model: {
|
|
3270
|
+
id: effectiveModel.id,
|
|
3271
|
+
provider: effectiveModel.provider,
|
|
3272
|
+
apiPattern: effectiveModel.api.pattern,
|
|
3273
|
+
},
|
|
3274
|
+
};
|
|
3275
|
+
this.messageHistory.push(inactionUserMessage);
|
|
3276
|
+
await this.historyStore.appendMessage(this.currentSessionId, inactionUserMessage);
|
|
3277
|
+
try {
|
|
3278
|
+
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3279
|
+
const retryCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
3280
|
+
const retryRequest = this.gatewayTranslation.prepareRequest(retryCanonicalHistory, toolsToUse, effectiveModel, {
|
|
3281
|
+
temperature: options.parameters?.temperature,
|
|
3282
|
+
maxTokens: options.parameters?.maxTokens,
|
|
3283
|
+
topP: options.parameters?.topP,
|
|
3284
|
+
reasoningEffort: options.parameters?.reasoningEffort,
|
|
3285
|
+
stream: false,
|
|
3286
|
+
staticSystemPrompt: this.currentStaticSystemPrompt,
|
|
3287
|
+
conversationId: this.currentConversationId,
|
|
3288
|
+
});
|
|
3289
|
+
if (this.lastResponseId && effectiveModel.api.pattern === 'responses') {
|
|
3290
|
+
retryRequest.previousResponseId = this.lastResponseId;
|
|
3291
|
+
}
|
|
3292
|
+
retryRequest.conversationId = this.currentSessionId;
|
|
3293
|
+
let retryApiResponse;
|
|
3294
|
+
if (this.retryMiddleware) {
|
|
3295
|
+
const retryResult = await this.retryMiddleware.executeWithRetry(() => this.apiClient.sendRequest(retryRequest, effectiveModel), 'inaction_nudge_retry');
|
|
3296
|
+
retryApiResponse = retryResult.result;
|
|
3297
|
+
}
|
|
3298
|
+
else {
|
|
3299
|
+
retryApiResponse = await this.apiClient.sendRequest(retryRequest, effectiveModel);
|
|
3300
|
+
}
|
|
3301
|
+
const retryConverted = this.gatewayTranslation.convertResponse(retryApiResponse.data, effectiveModel, {
|
|
3302
|
+
sessionId: this.currentSessionId,
|
|
3303
|
+
conversationId: this.currentConversationId,
|
|
3304
|
+
turnNumber: this.turnNumber + 1,
|
|
3305
|
+
});
|
|
3306
|
+
if (effectiveModel.api.pattern === 'responses' && retryApiResponse.data?.id) {
|
|
3307
|
+
this.lastResponseId = retryApiResponse.data.id;
|
|
3308
|
+
this.lastResponseIdProvider = effectiveModel.provider;
|
|
3309
|
+
}
|
|
3310
|
+
if (retryConverted.messages && retryConverted.messages.length > 0) {
|
|
3311
|
+
const retryAssistantCanonical = retryConverted.messages[0];
|
|
3312
|
+
const retryAssistantMessage = {
|
|
3313
|
+
uuid: retryAssistantCanonical.uuid,
|
|
3314
|
+
timestamp: retryAssistantCanonical.timestamp,
|
|
3315
|
+
type: 'assistant',
|
|
3316
|
+
message: {
|
|
3317
|
+
role: 'assistant',
|
|
3318
|
+
content: retryAssistantCanonical.content,
|
|
3319
|
+
},
|
|
3320
|
+
timeline: {
|
|
3321
|
+
sessionId: this.currentSessionId,
|
|
3322
|
+
conversationId: this.currentConversationId,
|
|
3323
|
+
turnNumber: this.turnNumber + 1,
|
|
3324
|
+
},
|
|
3325
|
+
model: {
|
|
3326
|
+
id: effectiveModel.id,
|
|
3327
|
+
provider: effectiveModel.provider,
|
|
3328
|
+
apiPattern: effectiveModel.api.pattern,
|
|
3329
|
+
},
|
|
3330
|
+
...(retryConverted.usage && { usage: retryConverted.usage }),
|
|
3331
|
+
};
|
|
3332
|
+
this.messageHistory.push(retryAssistantMessage);
|
|
3333
|
+
await this.historyStore.appendMessage(this.currentSessionId, retryAssistantMessage);
|
|
3334
|
+
currentAssistantCanonicalMessage = retryAssistantCanonical;
|
|
3335
|
+
const retryText = retryAssistantCanonical.content
|
|
3336
|
+
.filter((b) => b && b.type === 'text' && typeof b.text === 'string')
|
|
3337
|
+
.map((b) => b.text)
|
|
3338
|
+
.join('');
|
|
3339
|
+
if (retryText.trim().length > 0) {
|
|
3340
|
+
yield { type: 'text_delta', delta: retryText };
|
|
3341
|
+
}
|
|
3342
|
+
continue; // re-enter loop — retry may have produced tool_use
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
catch (retryErr) {
|
|
3346
|
+
console.warn(`[Orchestrator Streaming] Inaction-nudge retry failed:`, retryErr);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
2936
3350
|
hasToolUse = false;
|
|
2937
3351
|
break;
|
|
2938
3352
|
}
|
|
@@ -2972,6 +3386,45 @@ export class CortexOrchestrator {
|
|
|
2972
3386
|
// Execute tools (reuse existing method)
|
|
2973
3387
|
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
2974
3388
|
clearTimeout(timeoutId);
|
|
3389
|
+
// Unified Outcome Ladder (streaming parity with sendMessage).
|
|
3390
|
+
ladderSignal = null;
|
|
3391
|
+
{
|
|
3392
|
+
const inputById = new Map(toolUseBlocks.map((b) => [b.id, b.input]));
|
|
3393
|
+
for (const tr of toolResults) {
|
|
3394
|
+
if (tr.tool_name === 'EndTurn')
|
|
3395
|
+
continue;
|
|
3396
|
+
const input = inputById.get(tr.tool_use_id);
|
|
3397
|
+
if (input === undefined)
|
|
3398
|
+
continue;
|
|
3399
|
+
const outcome = classifyToolOutcome(tr.tool_name, input, tr);
|
|
3400
|
+
const ladder = loopLadder.observe(tr.tool_name, outcome);
|
|
3401
|
+
const sig = formatLadderSignal(tr.tool_name, ladder);
|
|
3402
|
+
if (sig)
|
|
3403
|
+
ladderSignal = sig;
|
|
3404
|
+
if (ladder.action === 'break')
|
|
3405
|
+
ladderBreak = sig;
|
|
3406
|
+
// Observability (backlog item 3): streaming parity — bank each
|
|
3407
|
+
// escalation rung as a decision-store event.
|
|
3408
|
+
if (ladder.action !== 'none') {
|
|
3409
|
+
const store = this.getDecisionStore();
|
|
3410
|
+
if (store) {
|
|
3411
|
+
void store
|
|
3412
|
+
.recordEvent({
|
|
3413
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3414
|
+
kind: 'loop_escalation',
|
|
3415
|
+
toolName: tr.tool_name,
|
|
3416
|
+
detail: {
|
|
3417
|
+
rung: ladder.action,
|
|
3418
|
+
count: ladder.count,
|
|
3419
|
+
approachHash: outcome.approachHash,
|
|
3420
|
+
...(outcome.family ? { family: outcome.family } : {}),
|
|
3421
|
+
},
|
|
3422
|
+
})
|
|
3423
|
+
.catch(() => { });
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
2975
3428
|
// R21 (2026-05-15): same fix as non-streaming path — count consecutive
|
|
2976
3429
|
// ITERATIONS WITH ZERO SUCCESSFUL TOOLS, not cumulative individual
|
|
2977
3430
|
// failures. Reset whenever at least one tool succeeded.
|
|
@@ -2992,6 +3445,8 @@ export class CortexOrchestrator {
|
|
|
2992
3445
|
else if (successCount > 0) {
|
|
2993
3446
|
totalToolErrors = 0; // Reset — model is making progress
|
|
2994
3447
|
}
|
|
3448
|
+
// Item 7: scrub ReadImage payloads before persist (streaming parity).
|
|
3449
|
+
const pendingImages = this.collectPendingImages(toolResults, effectiveModel.vision === true);
|
|
2995
3450
|
// Create tool_result messages and save to history
|
|
2996
3451
|
for (const toolResult of toolResults) {
|
|
2997
3452
|
// Emit tool result as streaming chunk for CLI display
|
|
@@ -3123,15 +3578,43 @@ export class CortexOrchestrator {
|
|
|
3123
3578
|
const progressStalled = isToolProgressStalled(allToolCalls);
|
|
3124
3579
|
const budgetSignal = computeToolBudgetSignal(effectiveToolBudgetCount, TOOL_BUDGET_SOFT, progressStalled);
|
|
3125
3580
|
const diversityWarning = this.getDiversityWarning(toolCallCounts);
|
|
3126
|
-
if (budgetSignal || diversityWarning) {
|
|
3127
|
-
const signals = [budgetSignal, diversityWarning].filter(Boolean).join('\n');
|
|
3581
|
+
if (budgetSignal || diversityWarning || ladderSignal) {
|
|
3582
|
+
const signals = [budgetSignal, diversityWarning, ladderSignal].filter(Boolean).join('\n');
|
|
3128
3583
|
const lastMsg = this.messageHistory[this.messageHistory.length - 1];
|
|
3129
3584
|
if (lastMsg?.message?.content?.[0]?.type === 'tool_result') {
|
|
3130
3585
|
const block = lastMsg.message.content[0];
|
|
3131
3586
|
const existing = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
3132
3587
|
block.content = existing + '\n\n' + signals;
|
|
3588
|
+
// Observability (backlog item 3): post-persist mutation — bank a
|
|
3589
|
+
// steering_injected event (streaming parity).
|
|
3590
|
+
const store = this.getDecisionStore();
|
|
3591
|
+
if (store) {
|
|
3592
|
+
const kinds = [
|
|
3593
|
+
budgetSignal ? 'budget' : null,
|
|
3594
|
+
diversityWarning ? 'diversity' : null,
|
|
3595
|
+
ladderSignal ? 'ladder' : null,
|
|
3596
|
+
].filter(Boolean);
|
|
3597
|
+
void store
|
|
3598
|
+
.recordEvent({
|
|
3599
|
+
sessionId: this.currentSessionId ?? 'unknown',
|
|
3600
|
+
kind: 'steering_injected',
|
|
3601
|
+
detail: { kinds, iteration: toolCallIteration, chars: signals.length, streaming: true },
|
|
3602
|
+
})
|
|
3603
|
+
.catch(() => { });
|
|
3604
|
+
}
|
|
3133
3605
|
}
|
|
3606
|
+
ladderSignal = null; // consumed
|
|
3134
3607
|
}
|
|
3608
|
+
// Ladder break (streaming parity): instruction already injected above;
|
|
3609
|
+
// exit R29b-style so the streaming synthesis net closes the turn.
|
|
3610
|
+
if (ladderBreak) {
|
|
3611
|
+
console.warn('[Orchestrator Streaming] LoopLadder break: repeated failing approach — forcing synthesis.');
|
|
3612
|
+
break;
|
|
3613
|
+
}
|
|
3614
|
+
// Item 7: attach ReadImage images as a user message (streaming parity;
|
|
3615
|
+
// after the signals block — it targets the LAST message being a
|
|
3616
|
+
// tool_result).
|
|
3617
|
+
await this.injectImageUserMessage(pendingImages, effectiveModel);
|
|
3135
3618
|
// R29b hard cap (streaming parity): force-stop at 2x soft budget;
|
|
3136
3619
|
// the post-loop R29a streaming net then synthesizes a deliverable.
|
|
3137
3620
|
// Progress-gated hard cap (parity with sendMessage): force-stop only when
|
|
@@ -3170,7 +3653,11 @@ export class CortexOrchestrator {
|
|
|
3170
3653
|
// Proactive context management - ensure history fits before streaming continuation
|
|
3171
3654
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
3172
3655
|
// Anchor lift — streaming mirror of the sendMessage continuation path.
|
|
3173
|
-
if (!this.anchorLifted &&
|
|
3656
|
+
if (!this.anchorLifted &&
|
|
3657
|
+
resolveToolAnchor(process.env, this.cardAnchorProfile) &&
|
|
3658
|
+
// Persist frame (backlog item 5): the anchored surface IS the session
|
|
3659
|
+
// surface — never lift, never deliver the deferred corpus.
|
|
3660
|
+
resolveFrameProfile(process.env, this.cardFrameProfile) !== 'persist') {
|
|
3174
3661
|
this.anchorLifted = true;
|
|
3175
3662
|
if (!(this.config.enableDeferredToolLoading && !isPTCEnabled)) {
|
|
3176
3663
|
toolsToUse = allTools;
|
|
@@ -5886,6 +6373,89 @@ export class CortexOrchestrator {
|
|
|
5886
6373
|
}
|
|
5887
6374
|
return this.routerMatrix;
|
|
5888
6375
|
}
|
|
6376
|
+
/** Item 7 (image-path bridge): consume ReadImage payloads from this batch.
|
|
6377
|
+
* SCRUBS the base64 payload out of tool_result metadata BEFORE the result
|
|
6378
|
+
* is persisted (no double-stored image; the image rides the injected user
|
|
6379
|
+
* message instead). When the active model is not vision-capable (a
|
|
6380
|
+
* hallucinated/PTC call that slipped the surface gate), the result is
|
|
6381
|
+
* rewritten into an actionable error — a text-only model must never have
|
|
6382
|
+
* an image dangled at it. */
|
|
6383
|
+
collectPendingImages(toolResults, modelVision) {
|
|
6384
|
+
const out = [];
|
|
6385
|
+
for (const tr of toolResults) {
|
|
6386
|
+
const payload = tr.metadata?.imagePayload;
|
|
6387
|
+
if (!payload)
|
|
6388
|
+
continue;
|
|
6389
|
+
delete tr.metadata.imagePayload;
|
|
6390
|
+
if (tr.is_error)
|
|
6391
|
+
continue;
|
|
6392
|
+
if (!modelVision) {
|
|
6393
|
+
tr.is_error = true;
|
|
6394
|
+
tr.content =
|
|
6395
|
+
'ReadImage requires a vision-capable model — the active model cannot see images. ' +
|
|
6396
|
+
'Extract what you need through bash instead (e.g. tesseract OCR, ffprobe, PIL).';
|
|
6397
|
+
continue;
|
|
6398
|
+
}
|
|
6399
|
+
out.push({ mediaType: payload.mediaType, data: payload.base64 });
|
|
6400
|
+
}
|
|
6401
|
+
return out;
|
|
6402
|
+
}
|
|
6403
|
+
/** Item 7: attach collected images as a synthetic USER message (R18b shape).
|
|
6404
|
+
* Providers reject image parts on tool messages — DeepSeek accepts them on
|
|
6405
|
+
* user messages only — so the image hops from the tool_result to the next
|
|
6406
|
+
* user turn. Persisted BEFORE the continuation request is assembled, so the
|
|
6407
|
+
* session record shows exactly what the model saw (item-3 lesson). */
|
|
6408
|
+
async injectImageUserMessage(images, effectiveModel) {
|
|
6409
|
+
if (images.length === 0)
|
|
6410
|
+
return;
|
|
6411
|
+
const content = [{
|
|
6412
|
+
type: 'text',
|
|
6413
|
+
text: `<system-reminder>Visual input from ReadImage (${images.length} image${images.length > 1 ? 's' : ''}) attached below — read the image content directly.</system-reminder>`,
|
|
6414
|
+
}];
|
|
6415
|
+
for (const img of images) {
|
|
6416
|
+
content.push({ type: 'image', image: { mediaType: img.mediaType, data: img.data } });
|
|
6417
|
+
}
|
|
6418
|
+
const imageUserMessage = {
|
|
6419
|
+
uuid: uuidv4(),
|
|
6420
|
+
timestamp: new Date().toISOString(),
|
|
6421
|
+
type: 'user',
|
|
6422
|
+
message: { role: 'user', content },
|
|
6423
|
+
timeline: {
|
|
6424
|
+
sessionId: this.currentSessionId,
|
|
6425
|
+
conversationId: this.currentConversationId,
|
|
6426
|
+
turnNumber: this.turnNumber + 1,
|
|
6427
|
+
},
|
|
6428
|
+
model: {
|
|
6429
|
+
id: effectiveModel.id,
|
|
6430
|
+
provider: effectiveModel.provider,
|
|
6431
|
+
apiPattern: effectiveModel.api.pattern,
|
|
6432
|
+
},
|
|
6433
|
+
};
|
|
6434
|
+
this.messageHistory.push(imageUserMessage);
|
|
6435
|
+
await this.historyStore.appendMessage(this.currentSessionId, imageUserMessage);
|
|
6436
|
+
}
|
|
6437
|
+
/** The turn's real user prompt text — the most recent user message that is
|
|
6438
|
+
* neither a tool_result carrier nor an injected <system-reminder> steering
|
|
6439
|
+
* message. Used by EndTurn Stage 4 to judge task shape. Empty string when
|
|
6440
|
+
* none found (Stage 4 then never fires its task-shape branch). */
|
|
6441
|
+
lastRealUserText() {
|
|
6442
|
+
for (let i = this.messageHistory.length - 1; i >= 0; i--) {
|
|
6443
|
+
const m = this.messageHistory[i];
|
|
6444
|
+
const isUser = m?.type === 'user' || m?.message?.role === 'user';
|
|
6445
|
+
if (!isUser)
|
|
6446
|
+
continue;
|
|
6447
|
+
const content = m?.message?.content;
|
|
6448
|
+
if (Array.isArray(content) && content.some((b) => b?.type === 'tool_result'))
|
|
6449
|
+
continue;
|
|
6450
|
+
const text = Array.isArray(content)
|
|
6451
|
+
? content.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n')
|
|
6452
|
+
: typeof content === 'string' ? content : '';
|
|
6453
|
+
if (!text.trim() || text.trimStart().startsWith('<system-reminder>'))
|
|
6454
|
+
continue;
|
|
6455
|
+
return text;
|
|
6456
|
+
}
|
|
6457
|
+
return '';
|
|
6458
|
+
}
|
|
5889
6459
|
getDecisionStore() {
|
|
5890
6460
|
if (this.isRecordingEnabled() === false && this.isLookupEnabled() === false) {
|
|
5891
6461
|
return undefined;
|
|
@@ -5915,7 +6485,14 @@ export class CortexOrchestrator {
|
|
|
5915
6485
|
const store = this.getDecisionStore();
|
|
5916
6486
|
if (!store)
|
|
5917
6487
|
return result;
|
|
5918
|
-
|
|
6488
|
+
// Unified outcome layer (docs/UNIFIED_OUTCOME_LADDER.md): ONE failure
|
|
6489
|
+
// semantics for recording + reminders. Critically, a bash command that
|
|
6490
|
+
// ran and exited nonzero is a FAILURE here even though its tool result
|
|
6491
|
+
// is not is_error on the wire (ShellTool returns success results for
|
|
6492
|
+
// exit!=0) — previously those loops were recorded as successes, so the
|
|
6493
|
+
// prior/family reminders never fired on the dominant thrash mode.
|
|
6494
|
+
const outcome = classifyToolOutcome(toolUse.name, toolUse.input, result);
|
|
6495
|
+
const inputHash = outcome.exactHash;
|
|
5919
6496
|
let augmented = result;
|
|
5920
6497
|
// 1. Lookup priors (read side) — only emits reminder when failures exist.
|
|
5921
6498
|
// Fetch the 3 most recent matching decisions so the formatter can
|
|
@@ -5930,6 +6507,21 @@ export class CortexOrchestrator {
|
|
|
5930
6507
|
if (reminder && result.content) {
|
|
5931
6508
|
augmented = { ...result, content: reminder + result.content };
|
|
5932
6509
|
}
|
|
6510
|
+
else if (outcome.status !== 'ok' && result.content) {
|
|
6511
|
+
// Family lens (AHE borrow): no exact-input prior fired, but the
|
|
6512
|
+
// CURRENT failure may be the latest of a repeated-approach family
|
|
6513
|
+
// across different inputs — hint once the family has >=2 prior
|
|
6514
|
+
// failures spanning >=2 distinct inputs. Gated on the UNIFIED
|
|
6515
|
+
// outcome (not is_error) so failing-command loops are covered.
|
|
6516
|
+
const family = outcome.family ?? classifyErrorFamily(String(result.content).slice(0, 200));
|
|
6517
|
+
if (family) {
|
|
6518
|
+
const ff = await store.familyFailures(toolUse.name, family);
|
|
6519
|
+
const famReminder = formatFamilyReminder(toolUse.name, family, ff.count, ff.distinctInputs, ff.recent);
|
|
6520
|
+
if (famReminder) {
|
|
6521
|
+
augmented = { ...result, content: famReminder + result.content };
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
5933
6525
|
}
|
|
5934
6526
|
catch (err) {
|
|
5935
6527
|
if (this.config.debug) {
|
|
@@ -5945,8 +6537,10 @@ export class CortexOrchestrator {
|
|
|
5945
6537
|
sessionId: this.currentSessionId ?? 'unknown',
|
|
5946
6538
|
toolName: toolUse.name,
|
|
5947
6539
|
input: toolUse.input,
|
|
5948
|
-
|
|
5949
|
-
|
|
6540
|
+
// Unified outcome: exit!=0 commands are FAILURES for the store even
|
|
6541
|
+
// though their wire result is not is_error (see outcome above).
|
|
6542
|
+
success: outcome.status === 'ok',
|
|
6543
|
+
...(outcome.status !== 'ok'
|
|
5950
6544
|
? { errorSnippet: String(result.content).slice(0, 200) }
|
|
5951
6545
|
: {}),
|
|
5952
6546
|
});
|
|
@@ -6503,10 +7097,15 @@ export class CortexOrchestrator {
|
|
|
6503
7097
|
});
|
|
6504
7098
|
// Validate and repair any orphaned tool_use blocks (crash recovery)
|
|
6505
7099
|
const repaired = this.validateAndRepairMessages(converted);
|
|
7100
|
+
// Image TTL eviction (item 7 addendum): stale images forfeit the whole
|
|
7101
|
+
// request's cache reads on vision-exp — stub them out of the REQUEST copy
|
|
7102
|
+
// after CORTEX_IMAGE_TTL_TURNS. Always-on (not utilization-gated); pure;
|
|
7103
|
+
// swaps content arrays only (cache contract).
|
|
7104
|
+
const imageAged = applyImageTtlForRequest(repaired);
|
|
6506
7105
|
// Age-tiered tool-result pruning on the outgoing request copy (all
|
|
6507
7106
|
// request-build call sites flow through here). No-op below the
|
|
6508
7107
|
// utilization gate; never touches the cache (pruner clones).
|
|
6509
|
-
return this.pruneAgedForRequest(
|
|
7108
|
+
return this.pruneAgedForRequest(imageAged);
|
|
6510
7109
|
}
|
|
6511
7110
|
/**
|
|
6512
7111
|
* Apply age-tiered tool-result pruning to an outgoing canonical request
|
|
@@ -6664,6 +7263,13 @@ export class CortexOrchestrator {
|
|
|
6664
7263
|
}
|
|
6665
7264
|
return thinkingBlock;
|
|
6666
7265
|
}
|
|
7266
|
+
else if (block.type === 'image') {
|
|
7267
|
+
// Item 7 (image-path bridge): pass image blocks through
|
|
7268
|
+
// untouched — the unknown-type fallback below stringifies,
|
|
7269
|
+
// which delivered the base64 as TEXT to the model (live-probe
|
|
7270
|
+
// finding 2026-08-25: "attached base64 blob").
|
|
7271
|
+
return { type: 'image', image: block.image };
|
|
7272
|
+
}
|
|
6667
7273
|
else if (block.type === 'redacted_thinking') {
|
|
6668
7274
|
// XAI grok-4/4.1 encrypted reasoning — preserve opaquely
|
|
6669
7275
|
return {
|