@nexus-cortex/core 4.51.1 → 4.53.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/GatewayTranslationLayer.d.ts +9 -0
- package/dist/adapters/GatewayTranslationLayer.d.ts.map +1 -1
- package/dist/adapters/GatewayTranslationLayer.js +24 -1
- package/dist/adapters/GatewayTranslationLayer.js.map +1 -1
- package/dist/adapters/ResponsesAPIAdapter.d.ts +66 -0
- package/dist/adapters/ResponsesAPIAdapter.d.ts.map +1 -1
- package/dist/adapters/ResponsesAPIAdapter.js +66 -0
- package/dist/adapters/ResponsesAPIAdapter.js.map +1 -1
- package/dist/adapters/node/GitHistoryStore.d.ts +80 -0
- package/dist/adapters/node/GitHistoryStore.d.ts.map +1 -0
- package/dist/adapters/node/GitHistoryStore.js +179 -0
- package/dist/adapters/node/GitHistoryStore.js.map +1 -0
- package/dist/adapters/node/index.d.ts +2 -0
- package/dist/adapters/node/index.d.ts.map +1 -1
- package/dist/adapters/node/index.js +1 -0
- package/dist/adapters/node/index.js.map +1 -1
- package/dist/adapters/servedModelDrift.d.ts +44 -0
- package/dist/adapters/servedModelDrift.d.ts.map +1 -0
- package/dist/adapters/servedModelDrift.js +0 -0
- package/dist/adapters/servedModelDrift.js.map +1 -0
- package/dist/conversation/ToolResultPruner.d.ts +45 -0
- package/dist/conversation/ToolResultPruner.d.ts.map +1 -0
- package/dist/conversation/ToolResultPruner.js +94 -0
- package/dist/conversation/ToolResultPruner.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/ErrorClassificationMiddleware.d.ts +19 -0
- package/dist/middleware/ErrorClassificationMiddleware.d.ts.map +1 -1
- package/dist/middleware/ErrorClassificationMiddleware.js +55 -1
- package/dist/middleware/ErrorClassificationMiddleware.js.map +1 -1
- package/dist/middleware/RetryMiddleware.d.ts.map +1 -1
- package/dist/middleware/RetryMiddleware.js +8 -2
- package/dist/middleware/RetryMiddleware.js.map +1 -1
- package/dist/middleware/contracts/MiddlewareContracts.d.ts +4 -0
- package/dist/middleware/contracts/MiddlewareContracts.d.ts.map +1 -1
- package/dist/orchestrator/APIClient.d.ts +10 -0
- package/dist/orchestrator/APIClient.d.ts.map +1 -1
- package/dist/orchestrator/APIClient.js +81 -6
- package/dist/orchestrator/APIClient.js.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.d.ts +30 -0
- package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
- package/dist/orchestrator/CortexOrchestrator.js +243 -21
- package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
- package/dist/orchestrator/apiErrorClassifier.d.ts.map +1 -1
- package/dist/orchestrator/apiErrorClassifier.js +8 -1
- package/dist/orchestrator/apiErrorClassifier.js.map +1 -1
- package/dist/orchestrator/emptyResponseClassifier.d.ts +29 -0
- package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -0
- package/dist/orchestrator/emptyResponseClassifier.js +42 -0
- package/dist/orchestrator/emptyResponseClassifier.js.map +1 -0
- package/dist/orchestrator/index.d.ts +1 -0
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +3 -0
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/structuredOutput.d.ts +144 -0
- package/dist/orchestrator/structuredOutput.d.ts.map +1 -0
- package/dist/orchestrator/structuredOutput.js +230 -0
- package/dist/orchestrator/structuredOutput.js.map +1 -0
- package/dist/orchestrator/tailRepetitionDetector.d.ts +57 -0
- package/dist/orchestrator/tailRepetitionDetector.d.ts.map +1 -0
- package/dist/orchestrator/tailRepetitionDetector.js +98 -0
- package/dist/orchestrator/tailRepetitionDetector.js.map +1 -0
- package/dist/system-messages/messages/WORK_QUALITY.md +2 -0
- package/dist/tools/registries/BaseToolRegistry.js +1 -1
- package/dist/tools/registries/BaseToolRegistry.js.map +1 -1
- package/package.json +18 -4
|
@@ -41,10 +41,14 @@ import { classifyApiError } from './apiErrorClassifier.js';
|
|
|
41
41
|
import { pinStaticSystemPrompt } from './staticSystemPromptPin.js';
|
|
42
42
|
import { hasVisibleAssistantText, shouldForceSynthesis } from './assistantTextPresence.js';
|
|
43
43
|
import { computeToolBudgetSignal, isToolProgressStalled } from './toolBudgetSignal.js';
|
|
44
|
+
import { createStructuredOutputTurnState, ensureStructuredOutputTool, evaluateStructuredOutputCall, finalizeStructuredOutput, isStructuredOutputToolName, } from './structuredOutput.js';
|
|
44
45
|
// Phase 2.6: MCP Model Management Tools
|
|
45
46
|
import { ListAvailableMcpServers, SearchMcpServers, GetMcpConfig, EnableMcpServer, DisableMcpServer, ConfigureMcpServer, InitMcpConfig } from '../tools/mcp-management/index.js';
|
|
46
47
|
// Context Management Tools
|
|
47
48
|
import { InitCortexContext, MemoryWrite, MemoryRecall } from '../tools/context-management/index.js';
|
|
49
|
+
import { pruneAgedToolResults } from '../conversation/ToolResultPruner.js';
|
|
50
|
+
import { detectTailRepetition, tailLoopGuardEnabled } from './tailRepetitionDetector.js';
|
|
51
|
+
import { classifyEmptyResponse, emptyResponseNudge } from './emptyResponseClassifier.js';
|
|
48
52
|
// PTC: Progressive tool loading for non-PTC providers
|
|
49
53
|
import { ClientSideToolFilter } from '../tools/ClientSideToolFilter.js';
|
|
50
54
|
/**
|
|
@@ -93,6 +97,9 @@ export class CortexOrchestrator {
|
|
|
93
97
|
// in messageHistory — they're sent separately to the API — so the budget must
|
|
94
98
|
// reserve space for them. The old hardcoded estimate (2000) was ~4-5x too low.
|
|
95
99
|
currentToolTokens = 0;
|
|
100
|
+
/** Active model's context window, captured by ensureHistoryFitsModel for
|
|
101
|
+
* the request-build pruning gate (pruneAgedForRequest). */
|
|
102
|
+
lastKnownContextWindow;
|
|
96
103
|
// Responses API stateful chaining: track last response ID for XAI/OpenAI
|
|
97
104
|
// When set, continuation requests send previous_response_id instead of full history,
|
|
98
105
|
// letting the server preserve reasoning state and improve coherence.
|
|
@@ -608,6 +615,19 @@ export class CortexOrchestrator {
|
|
|
608
615
|
if (!isPTCEnabled && this.config.enableDeferredToolLoading && toolsToUse && toolsToUse.length > 0) {
|
|
609
616
|
toolsToUse = this.toolFilter.getFilteredTools(toolsToUse);
|
|
610
617
|
}
|
|
618
|
+
// StructuredOutput (grok-build port): when the caller requested
|
|
619
|
+
// schema-constrained output, inject the request-scoped synthetic tool.
|
|
620
|
+
// Appended AFTER server-side detection and the deferred filter so it can
|
|
621
|
+
// neither trigger endpoint switching nor be stripped; `toolsToUse` is the
|
|
622
|
+
// variable every request of this turn (initial, retries, EndTurn-gate,
|
|
623
|
+
// continuations) is prepared from, so one append covers the whole loop —
|
|
624
|
+
// except the deferred re-filter inside the loop, which re-appends.
|
|
625
|
+
const structuredOutputState = options.jsonSchema
|
|
626
|
+
? createStructuredOutputTurnState(options.jsonSchema)
|
|
627
|
+
: undefined;
|
|
628
|
+
if (structuredOutputState) {
|
|
629
|
+
toolsToUse = ensureStructuredOutputTool(toolsToUse, structuredOutputState);
|
|
630
|
+
}
|
|
611
631
|
// Reset sequential call counter at start of each user turn
|
|
612
632
|
this.mentorshipMiddleware?.resetSequentialCalls(this.currentSessionId);
|
|
613
633
|
const preparedRequest = this.gatewayTranslation.prepareRequest(canonicalHistory, toolsToUse, effectiveModel, {
|
|
@@ -737,12 +757,34 @@ export class CortexOrchestrator {
|
|
|
737
757
|
}
|
|
738
758
|
}
|
|
739
759
|
// 9. Convert response back to canonical format
|
|
740
|
-
|
|
760
|
+
let convertedResponse = this.gatewayTranslation.convertResponse(apiResponse.data, effectiveModel, // Use effectiveModel (may have switched to Responses API)
|
|
741
761
|
{
|
|
742
762
|
sessionId: this.currentSessionId,
|
|
743
763
|
conversationId: this.currentConversationId,
|
|
744
764
|
turnNumber: this.turnNumber + 1
|
|
745
765
|
});
|
|
766
|
+
// Non-streaming tail-repetition doom-loop guard (grok-build port) — OPT-IN,
|
|
767
|
+
// default OFF. The streaming path aborts mid-stream; on a single response
|
|
768
|
+
// we detect a repeating thinking tail POST-HOC and resample ONCE (a fresh
|
|
769
|
+
// sample at temp>0 escapes the attractor). Same flag + detector as the
|
|
770
|
+
// streaming guard, bounded to one extra call. OFF = byte-identical.
|
|
771
|
+
if (tailLoopGuardEnabled(effectiveModel.provider)) {
|
|
772
|
+
const thinkingText = (convertedResponse.messages || [])
|
|
773
|
+
.flatMap((m) => (Array.isArray(m.content) ? m.content : []))
|
|
774
|
+
.filter((b) => b?.type === 'thinking' && typeof b.thinking === 'string')
|
|
775
|
+
.map((b) => b.thinking)
|
|
776
|
+
.join('\n');
|
|
777
|
+
const loop = detectTailRepetition(thinkingText);
|
|
778
|
+
if (loop.looping) {
|
|
779
|
+
console.warn(`[TailLoopGuard] xAI thinking loop detected (non-streaming, ${loop.trigger}) — resampling once`);
|
|
780
|
+
apiResponse = await this.apiClient.sendRequest(preparedRequest, effectiveModel);
|
|
781
|
+
convertedResponse = this.gatewayTranslation.convertResponse(apiResponse.data, effectiveModel, {
|
|
782
|
+
sessionId: this.currentSessionId,
|
|
783
|
+
conversationId: this.currentConversationId,
|
|
784
|
+
turnNumber: this.turnNumber + 1
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
746
788
|
// Track Responses API response ID for stateful chaining (XAI, OpenAI)
|
|
747
789
|
// R20a: also track which provider produced it — prevents cross-provider
|
|
748
790
|
// leak (XAI UUIDs vs OpenAI resp_* are mutually incompatible).
|
|
@@ -927,9 +969,21 @@ export class CortexOrchestrator {
|
|
|
927
969
|
// emit thinking blocks then stop without producing the final
|
|
928
970
|
// answer. Retry ONCE with an explicit completion prompt.
|
|
929
971
|
const hasVisibleText = hasVisibleAssistantText(currentAssistantCanonicalMessage.content);
|
|
930
|
-
|
|
972
|
+
// R18b skip-when-captured (2026-08-01): when the caller requested
|
|
973
|
+
// structured output and a StructuredOutput call was already captured,
|
|
974
|
+
// an empty visible-text turn is EXPECTED — the model correctly ended
|
|
975
|
+
// after its final tool call. Nudging it for "no text" would burn an
|
|
976
|
+
// extra round-trip (observed with haiku in the structured-output
|
|
977
|
+
// canary). The structured result IS the deliverable, so skip R18b.
|
|
978
|
+
if (!hasVisibleText && !emptyResponseRetryUsed && !structuredOutputState?.result) {
|
|
931
979
|
emptyResponseRetryUsed = true;
|
|
932
|
-
|
|
980
|
+
// Typed empty-response classification (grok-build port): distinguish
|
|
981
|
+
// reasoning_only (model reasoned but never answered) from
|
|
982
|
+
// no_visible_content (nothing at all) for observability + a nudge
|
|
983
|
+
// tailored to the failure shape. Does NOT change the one-bounded-retry
|
|
984
|
+
// decision — only the log + nudge text.
|
|
985
|
+
const emptyClass = classifyEmptyResponse(currentAssistantCanonicalMessage.content);
|
|
986
|
+
console.warn(`[Orchestrator] Empty response detected (${emptyClass.kind}, hadReasoning=${emptyClass.hadReasoning}, iteration=${toolCallIteration}). ` +
|
|
933
987
|
`Retrying once with explicit completion prompt.`);
|
|
934
988
|
// R26 (2026-05-15, surfaced by A/B benchmark): the empty assistant
|
|
935
989
|
// turn is already in messageHistory. The retry below rebuilds the
|
|
@@ -969,7 +1023,7 @@ export class CortexOrchestrator {
|
|
|
969
1023
|
role: 'user',
|
|
970
1024
|
content: [{
|
|
971
1025
|
type: 'text',
|
|
972
|
-
text:
|
|
1026
|
+
text: `<system-reminder>${emptyResponseNudge(emptyClass.kind)} Do not call any more tools.</system-reminder>`,
|
|
973
1027
|
}],
|
|
974
1028
|
},
|
|
975
1029
|
timeline: {
|
|
@@ -1348,7 +1402,7 @@ export class CortexOrchestrator {
|
|
|
1348
1402
|
// Track which tool_use_ids have been processed to avoid duplicates in error handler
|
|
1349
1403
|
const processedToolUseIds = new Set();
|
|
1350
1404
|
try {
|
|
1351
|
-
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal);
|
|
1405
|
+
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
1352
1406
|
clearTimeout(timeoutId);
|
|
1353
1407
|
// R21 (2026-05-15): MAX_CONSECUTIVE_ERRORS now counts CONSECUTIVE
|
|
1354
1408
|
// ITERATIONS WITH ZERO SUCCESSFUL TOOLS, not cumulative individual
|
|
@@ -1603,6 +1657,12 @@ export class CortexOrchestrator {
|
|
|
1603
1657
|
if (this.config.enableDeferredToolLoading && !isPTCEnabled) {
|
|
1604
1658
|
const beforeCount = toolsToUse.length;
|
|
1605
1659
|
toolsToUse = this.toolFilter.getFilteredTools(allTools);
|
|
1660
|
+
// StructuredOutput: the re-filter rebuilds from allTools (which never
|
|
1661
|
+
// contained the request-scoped tool) — re-append so it stays present
|
|
1662
|
+
// on every request of the turn.
|
|
1663
|
+
if (structuredOutputState) {
|
|
1664
|
+
toolsToUse = ensureStructuredOutputTool(toolsToUse, structuredOutputState);
|
|
1665
|
+
}
|
|
1606
1666
|
if (this.config.debug && toolsToUse.length !== beforeCount) {
|
|
1607
1667
|
console.log(`[Deferred] Tools re-filtered: ${beforeCount} → ${toolsToUse.length} (${toolsToUse.map(t => t.name).join(', ')})`);
|
|
1608
1668
|
}
|
|
@@ -1879,8 +1939,15 @@ export class CortexOrchestrator {
|
|
|
1879
1939
|
await this.historyStore.appendMessage(this.currentSessionId, synthUserMessage);
|
|
1880
1940
|
await this.ensureHistoryFitsModel(effectiveModel);
|
|
1881
1941
|
const synthCanonicalHistory = this.convertToCanonicalMessages([...this.messageHistory]);
|
|
1882
|
-
const synthRequest = this.gatewayTranslation.prepareRequest(synthCanonicalHistory,
|
|
1883
|
-
|
|
1942
|
+
const synthRequest = this.gatewayTranslation.prepareRequest(synthCanonicalHistory,
|
|
1943
|
+
// Tools suppressed — the model MUST produce text, not call more tools.
|
|
1944
|
+
// NOTE (structured-output constraint, 2026-08-02): this is R29a's
|
|
1945
|
+
// plain-text escape hatch for tool-loop EXHAUSTION (30+ calls). A
|
|
1946
|
+
// jsonSchema turn that gets here degrades to text; StructuredOutput
|
|
1947
|
+
// is deliberately NOT re-forced (this terminal synth turn has no
|
|
1948
|
+
// tool-interception, so a StructuredOutput call would go uncaptured —
|
|
1949
|
+
// worse than the graceful text fallback + structuredOutput.valid=false).
|
|
1950
|
+
[], effectiveModel, {
|
|
1884
1951
|
temperature: options.parameters?.temperature,
|
|
1885
1952
|
maxTokens: options.parameters?.maxTokens,
|
|
1886
1953
|
topP: options.parameters?.topP,
|
|
@@ -2046,6 +2113,16 @@ export class CortexOrchestrator {
|
|
|
2046
2113
|
// Phase 2.5 Day 2: Multi-turn tool execution metadata
|
|
2047
2114
|
toolCallIterations: toolCallIteration,
|
|
2048
2115
|
multiTurnToolExecution: toolCallIteration > 0,
|
|
2116
|
+
// StructuredOutput (grok-build port): surfaced whenever the caller
|
|
2117
|
+
// set options.jsonSchema — deterministic even if the tool was never called.
|
|
2118
|
+
...(structuredOutputState
|
|
2119
|
+
? { structuredOutput: finalizeStructuredOutput(structuredOutputState) }
|
|
2120
|
+
: {}),
|
|
2121
|
+
// Served-model drift (2026-08-01): provider served a different backend
|
|
2122
|
+
// than the requested slug (see GatewayTranslationLayer.convertResponse).
|
|
2123
|
+
...(convertedResponse?.servedModelDrift
|
|
2124
|
+
? { servedModel: convertedResponse.servedModel, servedModelDrift: true }
|
|
2125
|
+
: {}),
|
|
2049
2126
|
...(usedHelperModel ? helperModelMetadata : {}),
|
|
2050
2127
|
...(turnSummaryData ? {
|
|
2051
2128
|
turnSummary: turnSummaryData.summary,
|
|
@@ -2330,6 +2407,16 @@ export class CortexOrchestrator {
|
|
|
2330
2407
|
if (!isPTCEnabled && this.config.enableDeferredToolLoading && toolsToUse && toolsToUse.length > 0) {
|
|
2331
2408
|
toolsToUse = this.toolFilter.getFilteredTools(toolsToUse);
|
|
2332
2409
|
}
|
|
2410
|
+
// StructuredOutput (grok-build port) — streaming mirror of the sendMessage
|
|
2411
|
+
// injection: appended AFTER server-side detection and the deferred filter;
|
|
2412
|
+
// covers every request of the turn via the shared `toolsToUse` variable
|
|
2413
|
+
// (the in-loop deferred re-filter re-appends).
|
|
2414
|
+
const structuredOutputState = options.jsonSchema
|
|
2415
|
+
? createStructuredOutputTurnState(options.jsonSchema)
|
|
2416
|
+
: undefined;
|
|
2417
|
+
if (structuredOutputState) {
|
|
2418
|
+
toolsToUse = ensureStructuredOutputTool(toolsToUse, structuredOutputState);
|
|
2419
|
+
}
|
|
2333
2420
|
// Reset sequential call counter at start of each user turn
|
|
2334
2421
|
this.mentorshipMiddleware?.resetSequentialCalls(this.currentSessionId);
|
|
2335
2422
|
// Input-slicing at initial (streaming) request: send only items since last checkpoint
|
|
@@ -2367,7 +2454,7 @@ export class CortexOrchestrator {
|
|
|
2367
2454
|
preparedRequest.parameters.enablePTC = true;
|
|
2368
2455
|
}
|
|
2369
2456
|
// 7. Stream request (NEW: Use streaming API)
|
|
2370
|
-
|
|
2457
|
+
let streamingResponse = this.apiClient.streamRequest(preparedRequest, effectiveModel);
|
|
2371
2458
|
// CRITICAL: Attach a catch handler to finalMessage immediately to prevent unhandled rejection
|
|
2372
2459
|
// If stream is interrupted (ESC), the SDK may reject finalMessage after we've exited
|
|
2373
2460
|
// This prevents the "unhandledRejection" crash while still allowing errors when awaited
|
|
@@ -2378,21 +2465,61 @@ export class CortexOrchestrator {
|
|
|
2378
2465
|
});
|
|
2379
2466
|
// Track whether stream completed normally (vs interrupted by ESC/abort)
|
|
2380
2467
|
let streamCompleted = false;
|
|
2468
|
+
// Tail-repetition doom-loop guard (grok-build port, 2026-08-01) — OPT-IN,
|
|
2469
|
+
// default OFF. When XAI_TAIL_LOOP_GUARD=true and the provider is xAI, watch
|
|
2470
|
+
// the thinking channel (chunks already flowing past us — NO edit to the
|
|
2471
|
+
// sacred APIClient reader) for a repeating tail; on a confident loop,
|
|
2472
|
+
// abandon the doomed stream and resample ONCE (a fresh sample at temp>0
|
|
2473
|
+
// usually escapes the attractor — grok-build's own remedy). Bounded to one
|
|
2474
|
+
// resample; when off, the loop body is byte-identical to before.
|
|
2475
|
+
const tailGuardOn = tailLoopGuardEnabled(effectiveModel.provider);
|
|
2476
|
+
let tailResampleUsed = false;
|
|
2381
2477
|
// 8. Yield chunks in real-time
|
|
2382
|
-
|
|
2383
|
-
|
|
2478
|
+
while (true) {
|
|
2479
|
+
let tailLoopTrigger = null;
|
|
2480
|
+
let thinkingBuf = '';
|
|
2481
|
+
let lastTailCheck = 0;
|
|
2482
|
+
try {
|
|
2483
|
+
for await (const chunk of streamingResponse.chunks) {
|
|
2484
|
+
if (this.config.debug) {
|
|
2485
|
+
console.log(`[Orchestrator] Yielding chunk type: ${chunk.type}, delta length: ${chunk.delta?.length || 0}`);
|
|
2486
|
+
}
|
|
2487
|
+
if (tailGuardOn && !tailResampleUsed
|
|
2488
|
+
&& chunk.data?.reasoning === true && typeof chunk.delta === 'string') {
|
|
2489
|
+
thinkingBuf += chunk.delta;
|
|
2490
|
+
if (thinkingBuf.length - lastTailCheck >= 400) {
|
|
2491
|
+
lastTailCheck = thinkingBuf.length;
|
|
2492
|
+
const d = detectTailRepetition(thinkingBuf);
|
|
2493
|
+
if (d.looping) {
|
|
2494
|
+
tailLoopTrigger = d.trigger || 'tail_repetition@thinking';
|
|
2495
|
+
break;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
yield chunk;
|
|
2500
|
+
}
|
|
2501
|
+
if (tailLoopTrigger === null) {
|
|
2502
|
+
streamCompleted = true;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
catch (streamError) {
|
|
2506
|
+
// Stream was interrupted or errored - log but don't throw yet
|
|
2384
2507
|
if (this.config.debug) {
|
|
2385
|
-
console.log(`[Orchestrator]
|
|
2508
|
+
console.log(`[Orchestrator] Stream interrupted:`, streamError);
|
|
2386
2509
|
}
|
|
2387
|
-
yield chunk;
|
|
2388
2510
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
console.
|
|
2511
|
+
if (tailLoopTrigger !== null && !tailResampleUsed) {
|
|
2512
|
+
// One bounded resample: re-issue the identical prepared request (fresh
|
|
2513
|
+
// sample). The abandoned stream's finalMessage rejection is already
|
|
2514
|
+
// caught above; re-point to the new stream.
|
|
2515
|
+
tailResampleUsed = true;
|
|
2516
|
+
console.warn(`[TailLoopGuard] xAI thinking loop detected (${tailLoopTrigger}) — resampling once`);
|
|
2517
|
+
streamingResponse = this.apiClient.streamRequest(preparedRequest, effectiveModel);
|
|
2518
|
+
finalMessageError = null;
|
|
2519
|
+
streamingResponse.finalMessage.catch((err) => { finalMessageError = err; });
|
|
2520
|
+
continue;
|
|
2395
2521
|
}
|
|
2522
|
+
break;
|
|
2396
2523
|
}
|
|
2397
2524
|
// 9. Get final accumulated message (SDK accumulates internally)
|
|
2398
2525
|
// Only await finalMessage if stream completed normally
|
|
@@ -2676,7 +2803,7 @@ export class CortexOrchestrator {
|
|
|
2676
2803
|
}, TOOL_TIMEOUT_MS);
|
|
2677
2804
|
try {
|
|
2678
2805
|
// Execute tools (reuse existing method)
|
|
2679
|
-
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal);
|
|
2806
|
+
const toolResults = await this.handleToolCalls(toolUseBlocks, abortController.signal, structuredOutputState);
|
|
2680
2807
|
clearTimeout(timeoutId);
|
|
2681
2808
|
// R21 (2026-05-15): same fix as non-streaming path — count consecutive
|
|
2682
2809
|
// ITERATIONS WITH ZERO SUCCESSFUL TOOLS, not cumulative individual
|
|
@@ -2878,6 +3005,11 @@ export class CortexOrchestrator {
|
|
|
2878
3005
|
// Re-filter tools after SearchTools discovery (same as sendMessage path)
|
|
2879
3006
|
if (this.config.enableDeferredToolLoading && !isPTCEnabled) {
|
|
2880
3007
|
toolsToUse = this.toolFilter.getFilteredTools(allTools);
|
|
3008
|
+
// StructuredOutput: re-filter rebuilds from allTools — re-append the
|
|
3009
|
+
// request-scoped tool (same as the sendMessage continuation path).
|
|
3010
|
+
if (structuredOutputState) {
|
|
3011
|
+
toolsToUse = ensureStructuredOutputTool(toolsToUse, structuredOutputState);
|
|
3012
|
+
}
|
|
2881
3013
|
}
|
|
2882
3014
|
// Input-slicing for stateful Responses API (same logic as non-streaming path)
|
|
2883
3015
|
const streamCanSliceInput = this.lastResponseId !== null &&
|
|
@@ -3293,6 +3425,16 @@ export class CortexOrchestrator {
|
|
|
3293
3425
|
},
|
|
3294
3426
|
durationMs: Date.now() - turnStartMs,
|
|
3295
3427
|
toolCallIterations: toolCallIteration,
|
|
3428
|
+
// StructuredOutput (grok-build port): streaming surface — present
|
|
3429
|
+
// whenever the caller set options.jsonSchema.
|
|
3430
|
+
...(structuredOutputState
|
|
3431
|
+
? { structuredOutput: finalizeStructuredOutput(structuredOutputState) }
|
|
3432
|
+
: {}),
|
|
3433
|
+
// Served-model drift (2026-08-01): best-effort on streaming (the
|
|
3434
|
+
// provider model field is present when the SDK final message carries it).
|
|
3435
|
+
...(convertedResponse?.servedModelDrift
|
|
3436
|
+
? { servedModel: convertedResponse.servedModel, servedModelDrift: true }
|
|
3437
|
+
: {}),
|
|
3296
3438
|
},
|
|
3297
3439
|
};
|
|
3298
3440
|
// Clean up ephemeral mentorship messages — they served their purpose during this turn
|
|
@@ -3999,6 +4141,10 @@ export class CortexOrchestrator {
|
|
|
3999
4141
|
* - Preserves critical messages (tool calls, recent context)
|
|
4000
4142
|
*/
|
|
4001
4143
|
async ensureHistoryFitsModel(model) {
|
|
4144
|
+
// Capture the active model's context window for request-build-time
|
|
4145
|
+
// pruning (pruneAgedForRequest) — set before any early return so the
|
|
4146
|
+
// gate works even when compaction itself is disabled.
|
|
4147
|
+
this.lastKnownContextWindow = model.limits?.contextWindow;
|
|
4002
4148
|
// Skip if compaction disabled in config
|
|
4003
4149
|
if (!this.config.autoCompact) {
|
|
4004
4150
|
return;
|
|
@@ -4322,7 +4468,42 @@ export class CortexOrchestrator {
|
|
|
4322
4468
|
* @param signal AbortSignal for cancellation
|
|
4323
4469
|
* @returns Array of tool results
|
|
4324
4470
|
*/
|
|
4325
|
-
async handleToolCalls(toolUseBlocks, signal) {
|
|
4471
|
+
async handleToolCalls(toolUseBlocks, signal, structuredOutputState) {
|
|
4472
|
+
// StructuredOutput (grok-build port): intercept BEFORE any dispatch — the
|
|
4473
|
+
// synthetic tool is request-scoped (never in a registry) and must NEVER
|
|
4474
|
+
// reach a real executor. Each call gets a synthesized tool_result:
|
|
4475
|
+
// co-emission steering (other tools in the same round), corrective
|
|
4476
|
+
// validation errors (bounded retries), or acceptance (valid or fail-open).
|
|
4477
|
+
// Remaining tools are dispatched normally via recursion.
|
|
4478
|
+
if (structuredOutputState && toolUseBlocks.some((t) => isStructuredOutputToolName(t.name))) {
|
|
4479
|
+
const structuredCalls = toolUseBlocks.filter((t) => isStructuredOutputToolName(t.name));
|
|
4480
|
+
const otherCalls = toolUseBlocks.filter((t) => !isStructuredOutputToolName(t.name));
|
|
4481
|
+
// Co-emission = any other tool in the round, or multiple simultaneous
|
|
4482
|
+
// StructuredOutput calls (it must be the single final action).
|
|
4483
|
+
const hasOthersInRound = toolUseBlocks.length > 1;
|
|
4484
|
+
const syntheticResults = [];
|
|
4485
|
+
for (const call of structuredCalls) {
|
|
4486
|
+
const decision = evaluateStructuredOutputCall(structuredOutputState.schema, call.input, structuredOutputState.attempts, hasOthersInRound);
|
|
4487
|
+
structuredOutputState.attempts = decision.attemptsAfter;
|
|
4488
|
+
if (decision.result) {
|
|
4489
|
+
structuredOutputState.result = decision.result;
|
|
4490
|
+
}
|
|
4491
|
+
if (this.config.debug) {
|
|
4492
|
+
console.log(`[Orchestrator] StructuredOutput call: accepted=${decision.accepted} ` +
|
|
4493
|
+
`attempts=${decision.attemptsAfter} coEmission=${hasOthersInRound}`);
|
|
4494
|
+
}
|
|
4495
|
+
syntheticResults.push({
|
|
4496
|
+
tool_use_id: call.id,
|
|
4497
|
+
tool_name: call.name,
|
|
4498
|
+
content: decision.toolResultText,
|
|
4499
|
+
is_error: decision.isError,
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
4502
|
+
const otherResults = otherCalls.length > 0
|
|
4503
|
+
? await this.handleToolCalls(otherCalls, signal)
|
|
4504
|
+
: [];
|
|
4505
|
+
return [...otherResults, ...syntheticResults];
|
|
4506
|
+
}
|
|
4326
4507
|
const results = [];
|
|
4327
4508
|
if (this.config.debug) {
|
|
4328
4509
|
console.log(`[Orchestrator Phase 2.5] Executing ${toolUseBlocks.length} tool call(s)`);
|
|
@@ -6104,7 +6285,48 @@ export class CortexOrchestrator {
|
|
|
6104
6285
|
return { ...fresh };
|
|
6105
6286
|
});
|
|
6106
6287
|
// Validate and repair any orphaned tool_use blocks (crash recovery)
|
|
6107
|
-
|
|
6288
|
+
const repaired = this.validateAndRepairMessages(converted);
|
|
6289
|
+
// Age-tiered tool-result pruning on the outgoing request copy (all
|
|
6290
|
+
// request-build call sites flow through here). No-op below the
|
|
6291
|
+
// utilization gate; never touches the cache (pruner clones).
|
|
6292
|
+
return this.pruneAgedForRequest(repaired);
|
|
6293
|
+
}
|
|
6294
|
+
/**
|
|
6295
|
+
* Apply age-tiered tool-result pruning to an outgoing canonical request
|
|
6296
|
+
* copy (grok-build port, 2026-08-01). Gated on estimated context
|
|
6297
|
+
* utilization > 50% — below that, the identity function, so ordinary
|
|
6298
|
+
* conversations keep byte-stable request prefixes for prompt caching. At
|
|
6299
|
+
* high utilization the trade inverts: pruning old tool results is strictly
|
|
6300
|
+
* gentler than the compaction it defers (which rewrites the whole history).
|
|
6301
|
+
*
|
|
6302
|
+
* The context window is captured by ensureHistoryFitsModel (which every
|
|
6303
|
+
* request path calls before converting); when it has not run yet, this is
|
|
6304
|
+
* a no-op.
|
|
6305
|
+
*/
|
|
6306
|
+
pruneAgedForRequest(messages) {
|
|
6307
|
+
const window = this.lastKnownContextWindow;
|
|
6308
|
+
if (!window || window <= 0)
|
|
6309
|
+
return messages;
|
|
6310
|
+
let approxChars = 0;
|
|
6311
|
+
for (const m of messages) {
|
|
6312
|
+
for (const b of m.content) {
|
|
6313
|
+
if (b.type === 'text' && b.text)
|
|
6314
|
+
approxChars += b.text.length;
|
|
6315
|
+
else if (b.type === 'thinking' && b.thinking)
|
|
6316
|
+
approxChars += b.thinking.length;
|
|
6317
|
+
else
|
|
6318
|
+
approxChars += JSON.stringify(b)?.length ?? 0;
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
const approxTokens = Math.ceil(approxChars / 4) + (this.currentToolTokens || 0);
|
|
6322
|
+
if (approxTokens / window <= 0.5)
|
|
6323
|
+
return messages;
|
|
6324
|
+
const pruned = pruneAgedToolResults(messages);
|
|
6325
|
+
if (pruned.prunedCount > 0 && this.config.debug) {
|
|
6326
|
+
console.log(`[Orchestrator Context] Aged tool-result pruning: ${pruned.prunedCount} results, ` +
|
|
6327
|
+
`~${pruned.savedChars} chars removed from request copy (history unchanged)`);
|
|
6328
|
+
}
|
|
6329
|
+
return pruned.messages;
|
|
6108
6330
|
}
|
|
6109
6331
|
/**
|
|
6110
6332
|
* Pure conversion of a single Message to its CanonicalMessage form.
|