@nexus-cortex/core 4.51.0 → 4.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/adapters/ChatCompletionsAPIAdapter.d.ts.map +1 -1
  2. package/dist/adapters/ChatCompletionsAPIAdapter.js +16 -0
  3. package/dist/adapters/ChatCompletionsAPIAdapter.js.map +1 -1
  4. package/dist/adapters/FormatAdapter.interface.d.ts +4 -4
  5. package/dist/adapters/FormatAdapter.interface.js +4 -4
  6. package/dist/adapters/GatewayTranslationLayer.d.ts +9 -0
  7. package/dist/adapters/GatewayTranslationLayer.d.ts.map +1 -1
  8. package/dist/adapters/GatewayTranslationLayer.js +24 -1
  9. package/dist/adapters/GatewayTranslationLayer.js.map +1 -1
  10. package/dist/adapters/ResponsesAPIAdapter.d.ts +66 -0
  11. package/dist/adapters/ResponsesAPIAdapter.d.ts.map +1 -1
  12. package/dist/adapters/ResponsesAPIAdapter.js +66 -0
  13. package/dist/adapters/ResponsesAPIAdapter.js.map +1 -1
  14. package/dist/adapters/ToolNamingHandler.d.ts +4 -4
  15. package/dist/adapters/ToolNamingHandler.js +4 -4
  16. package/dist/adapters/servedModelDrift.d.ts +44 -0
  17. package/dist/adapters/servedModelDrift.d.ts.map +1 -0
  18. package/dist/adapters/servedModelDrift.js +0 -0
  19. package/dist/adapters/servedModelDrift.js.map +1 -0
  20. package/dist/agents/AgentStore.d.ts.map +1 -1
  21. package/dist/agents/AgentStore.js +5 -2
  22. package/dist/agents/AgentStore.js.map +1 -1
  23. package/dist/config/RuntimeConfigRegistry.d.ts.map +1 -1
  24. package/dist/config/RuntimeConfigRegistry.js +1 -0
  25. package/dist/config/RuntimeConfigRegistry.js.map +1 -1
  26. package/dist/config/SettingsLoader.js +1 -1
  27. package/dist/config/SettingsLoader.js.map +1 -1
  28. package/dist/config/SettingsSchema.d.ts +2 -2
  29. package/dist/config/SettingsSchema.d.ts.map +1 -1
  30. package/dist/config/SettingsSchema.js +13 -8
  31. package/dist/config/SettingsSchema.js.map +1 -1
  32. package/dist/conversation/ContextBudgetManager.d.ts.map +1 -1
  33. package/dist/conversation/ContextBudgetManager.js +4 -2
  34. package/dist/conversation/ContextBudgetManager.js.map +1 -1
  35. package/dist/conversation/ToolResultPruner.d.ts +45 -0
  36. package/dist/conversation/ToolResultPruner.d.ts.map +1 -0
  37. package/dist/conversation/ToolResultPruner.js +94 -0
  38. package/dist/conversation/ToolResultPruner.js.map +1 -0
  39. package/dist/middleware/ErrorClassificationMiddleware.d.ts +19 -0
  40. package/dist/middleware/ErrorClassificationMiddleware.d.ts.map +1 -1
  41. package/dist/middleware/ErrorClassificationMiddleware.js +55 -1
  42. package/dist/middleware/ErrorClassificationMiddleware.js.map +1 -1
  43. package/dist/middleware/PermissionsMiddleware.d.ts +2 -2
  44. package/dist/middleware/PermissionsMiddleware.js +2 -2
  45. package/dist/middleware/RetryMiddleware.d.ts.map +1 -1
  46. package/dist/middleware/RetryMiddleware.js +8 -2
  47. package/dist/middleware/RetryMiddleware.js.map +1 -1
  48. package/dist/middleware/contracts/MiddlewareContracts.d.ts +4 -0
  49. package/dist/middleware/contracts/MiddlewareContracts.d.ts.map +1 -1
  50. package/dist/middleware/permissions/BashCommandPolicy.d.ts +2 -2
  51. package/dist/middleware/permissions/BashCommandPolicy.js +2 -2
  52. package/dist/middleware/permissions/BlacklistPolicy.d.ts +1 -1
  53. package/dist/middleware/permissions/BlacklistPolicy.js +1 -1
  54. package/dist/middleware/permissions/FileOperationPolicy.d.ts +2 -2
  55. package/dist/middleware/permissions/FileOperationPolicy.js +2 -2
  56. package/dist/middleware/permissions/PermissionAuditLogger.d.ts +1 -1
  57. package/dist/middleware/permissions/PermissionAuditLogger.js +1 -1
  58. package/dist/middleware/permissions/PermissionConfig.js +2 -2
  59. package/dist/middleware/permissions/PermissionConfig.js.map +1 -1
  60. package/dist/middleware/permissions/PermissionEvaluator.d.ts +2 -2
  61. package/dist/middleware/permissions/PermissionEvaluator.js +2 -2
  62. package/dist/middleware/permissions/WhitelistPolicy.d.ts +2 -2
  63. package/dist/middleware/permissions/WhitelistPolicy.js +2 -2
  64. package/dist/middleware/permissions/WorkspaceBoundaryPolicy.d.ts.map +1 -1
  65. package/dist/middleware/permissions/WorkspaceBoundaryPolicy.js +2 -3
  66. package/dist/middleware/permissions/WorkspaceBoundaryPolicy.js.map +1 -1
  67. package/dist/models/ModelConfig.interface.js +1 -1
  68. package/dist/orchestrator/APIClient.d.ts +10 -0
  69. package/dist/orchestrator/APIClient.d.ts.map +1 -1
  70. package/dist/orchestrator/APIClient.js +81 -6
  71. package/dist/orchestrator/APIClient.js.map +1 -1
  72. package/dist/orchestrator/CortexOrchestrator.d.ts +30 -0
  73. package/dist/orchestrator/CortexOrchestrator.d.ts.map +1 -1
  74. package/dist/orchestrator/CortexOrchestrator.js +256 -36
  75. package/dist/orchestrator/CortexOrchestrator.js.map +1 -1
  76. package/dist/orchestrator/OrchestratorFactory.js +1 -1
  77. package/dist/orchestrator/OrchestratorFactory.js.map +1 -1
  78. package/dist/orchestrator/SubAgentPermissionChecker.d.ts.map +1 -1
  79. package/dist/orchestrator/SubAgentPermissionChecker.js +13 -2
  80. package/dist/orchestrator/SubAgentPermissionChecker.js.map +1 -1
  81. package/dist/orchestrator/apiErrorClassifier.d.ts.map +1 -1
  82. package/dist/orchestrator/apiErrorClassifier.js +8 -1
  83. package/dist/orchestrator/apiErrorClassifier.js.map +1 -1
  84. package/dist/orchestrator/emptyResponseClassifier.d.ts +29 -0
  85. package/dist/orchestrator/emptyResponseClassifier.d.ts.map +1 -0
  86. package/dist/orchestrator/emptyResponseClassifier.js +42 -0
  87. package/dist/orchestrator/emptyResponseClassifier.js.map +1 -0
  88. package/dist/orchestrator/index.d.ts +1 -0
  89. package/dist/orchestrator/index.d.ts.map +1 -1
  90. package/dist/orchestrator/index.js +3 -0
  91. package/dist/orchestrator/index.js.map +1 -1
  92. package/dist/orchestrator/structuredOutput.d.ts +144 -0
  93. package/dist/orchestrator/structuredOutput.d.ts.map +1 -0
  94. package/dist/orchestrator/structuredOutput.js +230 -0
  95. package/dist/orchestrator/structuredOutput.js.map +1 -0
  96. package/dist/orchestrator/tailRepetitionDetector.d.ts +57 -0
  97. package/dist/orchestrator/tailRepetitionDetector.d.ts.map +1 -0
  98. package/dist/orchestrator/tailRepetitionDetector.js +98 -0
  99. package/dist/orchestrator/tailRepetitionDetector.js.map +1 -0
  100. package/dist/system-messages/SystemReminderInjector.d.ts +3 -3
  101. package/dist/system-messages/SystemReminderInjector.d.ts.map +1 -1
  102. package/dist/system-messages/SystemReminderInjector.js +4 -4
  103. package/dist/system-messages/SystemReminderInjector.js.map +1 -1
  104. package/dist/system-messages/messages/EXAMPLES.md +18 -18
  105. package/dist/system-messages/messages/SYSTEM_PROMPT.md +15 -13
  106. package/dist/system-messages/messages/TASK_AGENT_GUIDE.md +2 -2
  107. package/dist/system-messages/messages/TOOL_USAGE_GUIDE.md +43 -42
  108. package/dist/system-messages/messages/WORK_QUALITY.md +12 -10
  109. package/dist/tools/ToolFactory.d.ts.map +1 -1
  110. package/dist/tools/ToolFactory.js +3 -1
  111. package/dist/tools/ToolFactory.js.map +1 -1
  112. package/dist/tools/context-management/InitCortexContext.js +1 -1
  113. package/dist/tools/context-management/InitCortexContext.js.map +1 -1
  114. package/dist/tools/mcp-management/InitMcpConfig.js +1 -1
  115. package/dist/tools/registries/BaseToolRegistry.js +45 -45
  116. package/dist/tools/registries/BaseToolRegistry.js.map +1 -1
  117. package/package.json +4 -3
@@ -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
- const convertedResponse = this.gatewayTranslation.convertResponse(apiResponse.data, effectiveModel, // Use effectiveModel (may have switched to Responses API)
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
- if (!hasVisibleText && !emptyResponseRetryUsed) {
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
- console.warn(`[Orchestrator] Empty response detected (no tool_use, no text, iteration=${toolCallIteration}). ` +
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: '<system-reminder>Your previous response had no visible text. Please provide your final answer in plain text now — summarize your findings or complete the requested task. Do not call any more tools.</system-reminder>',
1026
+ text: `<system-reminder>${emptyResponseNudge(emptyClass.kind)} Do not call any more tools.</system-reminder>`,
973
1027
  }],
974
1028
  },
975
1029
  timeline: {
@@ -1104,16 +1158,16 @@ export class CortexOrchestrator {
1104
1158
  ? ' You ran edit/write/bash this turn: in `verification` list every build/test/lint command you ACTUALLY ran with the real result line you saw — do not claim a check you did not run.'
1105
1159
  : '';
1106
1160
  const endTurnReminderText = !endTurnCalled
1107
- ? ('<system-reminder>You used tools this turn but have not called end_turn. ' +
1108
- 'You MUST call end_turn before any final answer. It is generative, not a checkbox: ' +
1161
+ ? ('<system-reminder>You used tools this turn but have not called EndTurn. ' +
1162
+ 'You MUST call EndTurn before any final answer. It is generative, not a checkbox: ' +
1109
1163
  'reconstruct `citations` (array of {reference, verbatim_source}), `verification` ' +
1110
1164
  '(array of {command, observed_result}), `summary`, `open_items`, and a skeptical ' +
1111
1165
  '`self_review` (what you did NOT check, what is assumed/possibly wrong, what one ' +
1112
1166
  'more tool call would verify).' +
1113
1167
  citEmphasis +
1114
1168
  verEmphasis +
1115
- ' Call end_turn now — do not produce a final answer until you have.</system-reminder>')
1116
- : ('<system-reminder>end_turn REJECTED. Your drafted answer asserts line number(s) ' +
1169
+ ' Call EndTurn now — do not produce a final answer until you have.</system-reminder>')
1170
+ : ('<system-reminder>EndTurn REJECTED. Your drafted answer asserts line number(s) ' +
1117
1171
  stage3Violations.map((v) => v.line).join(', ') +
1118
1172
  ' that are NOT backed by any citation whose verbatim_source actually sits at that ' +
1119
1173
  "line in what you read this turn — a regurgitated coordinate, not an observation, " +
@@ -1122,7 +1176,7 @@ export class CortexOrchestrator {
1122
1176
  "is the EXACT code copied from that line of this turn's read output; " +
1123
1177
  '(2) for any you cannot ground, DELETE the number and quote the verbatim code ' +
1124
1178
  'instead — a quote with no number is correct; a wrong number is a failed answer. ' +
1125
- 'Then call end_turn again with corrected citations and produce the answer.</system-reminder>');
1179
+ 'Then call EndTurn again with corrected citations and produce the answer.</system-reminder>');
1126
1180
  const endTurnReminder = {
1127
1181
  uuid: uuidv4(),
1128
1182
  timestamp: new Date().toISOString(),
@@ -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
@@ -1407,9 +1461,9 @@ export class CortexOrchestrator {
1407
1461
  .join('\n');
1408
1462
  tr.is_error = true;
1409
1463
  tr.content =
1410
- `end_turn REJECTED — these citations are not grounded in anything you read this turn:\n${bad}\n\n` +
1464
+ `EndTurn REJECTED — these citations are not grounded in anything you read this turn:\n${bad}\n\n` +
1411
1465
  `A quote or coordinate you did not transcribe from this turn's tool output is a fabrication (a regurgitated guess), exactly like a non-matching edit old_string. ` +
1412
- `Either RE-READ the exact region and copy the real text, or DELETE that reference from your answer (quote only code you can ground), then call end_turn again.`;
1466
+ `Either RE-READ the exact region and copy the real text, or DELETE that reference from your answer (quote only code you can ground), then call EndTurn again.`;
1413
1467
  console.warn(`[Orchestrator] Stage2: EndTurn rejected — ${verdict.ungrounded.length} ungrounded citation(s).`);
1414
1468
  }
1415
1469
  }
@@ -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, [], // tools suppressed — the model MUST produce text, not call more tools
1883
- effectiveModel, {
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
- const streamingResponse = this.apiClient.streamRequest(preparedRequest, effectiveModel);
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
- try {
2383
- for await (const chunk of streamingResponse.chunks) {
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] Yielding chunk type: ${chunk.type}, delta length: ${chunk.delta?.length || 0}`);
2508
+ console.log(`[Orchestrator] Stream interrupted:`, streamError);
2386
2509
  }
2387
- yield chunk;
2388
2510
  }
2389
- streamCompleted = true;
2390
- }
2391
- catch (streamError) {
2392
- // Stream was interrupted or errored - log but don't throw yet
2393
- if (this.config.debug) {
2394
- console.log(`[Orchestrator] Stream interrupted:`, streamError);
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
@@ -3469,7 +3611,7 @@ export class CortexOrchestrator {
3469
3611
  isMutationTool(toolName) {
3470
3612
  const mutationTools = new Set([
3471
3613
  'Write', 'Edit', 'NotebookEdit',
3472
- 'TodoCreate', 'TodoUpdate', 'TodoWrite',
3614
+ 'TodoCreate', 'TodoUpdate',
3473
3615
  'Bash',
3474
3616
  ]);
3475
3617
  return mutationTools.has(toolName);
@@ -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;
@@ -4085,18 +4231,16 @@ export class CortexOrchestrator {
4085
4231
  const truncated = this.truncateToolOutput(toolOutput, MAX_TOOL_OUTPUT_TOKENS);
4086
4232
  const toolLower = toolName.toLowerCase();
4087
4233
  let toolSpecificHint = '';
4088
- if (toolLower === 'read' || toolLower === 'readfile') {
4234
+ if (toolLower === 'read') {
4089
4235
  toolSpecificHint = `This file is too large to read ${MAX_TOOL_OUTPUT_TOKENS.toLocaleString()} tokens at once. ` +
4090
4236
  `Re-read with limit: 500 (or smaller) and use offset to navigate to the section you need.\n\n`;
4091
4237
  }
4092
4238
  const guidanceMessage = `Tool result too large (~${estimatedTokens.toLocaleString()} tokens, limit ${MAX_TOOL_OUTPUT_TOKENS.toLocaleString()}).\n\n` +
4093
4239
  toolSpecificHint +
4094
4240
  `Please try a more targeted approach:\n` +
4095
- `• For read: Use limit: 500 (or smaller) with offset to read manageable chunks\n` +
4096
- `• For grep/rg: Add --max-count=100 or search specific paths\n` +
4097
- `• For find: Use -maxdepth N to limit recursion\n` +
4098
- `• For ls: Target specific directories instead of -R\n` +
4099
- `• For bash: Pipe to 'head -n 100' or use more specific filters\n\n` +
4241
+ `• For Read: Use limit: 500 (or smaller) with offset to read manageable chunks\n` +
4242
+ `• For Grep: Use head_limit (e.g. 100) or search specific paths\n` +
4243
+ `• For Bash: pipe to 'head -n 100', use find -maxdepth N, and target specific directories instead of ls -R\n\n` +
4100
4244
  `Preview of truncated output (first/last portions shown):\n` +
4101
4245
  `${'='.repeat(70)}\n${truncated}\n${'='.repeat(70)}\n\n` +
4102
4246
  `If you cannot be more specific, acknowledge this and request the truncated output, ` +
@@ -4324,7 +4468,42 @@ export class CortexOrchestrator {
4324
4468
  * @param signal AbortSignal for cancellation
4325
4469
  * @returns Array of tool results
4326
4470
  */
4327
- 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
+ }
4328
4507
  const results = [];
4329
4508
  if (this.config.debug) {
4330
4509
  console.log(`[Orchestrator Phase 2.5] Executing ${toolUseBlocks.length} tool call(s)`);
@@ -6106,7 +6285,48 @@ export class CortexOrchestrator {
6106
6285
  return { ...fresh };
6107
6286
  });
6108
6287
  // Validate and repair any orphaned tool_use blocks (crash recovery)
6109
- return this.validateAndRepairMessages(converted);
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;
6110
6330
  }
6111
6331
  /**
6112
6332
  * Pure conversion of a single Message to its CanonicalMessage form.
@@ -6557,7 +6777,7 @@ export class CortexOrchestrator {
6557
6777
  lines.push(`- ${convert(d.name)} — ${firstSentence}`);
6558
6778
  }
6559
6779
  }
6560
- return `<harness-note source="automated-harness" from-user="false">\nThe following is automated context injected by the harness — NOT a message from the user. The tools below are available but their schemas are NOT loaded yet. To use any of them, call search_tools first to load the schema, then call the tool.\n${lines.join('\n')}\n</harness-note>`;
6780
+ return `<harness-note source="automated-harness" from-user="false">\nThe following is automated context injected by the harness — NOT a message from the user. The tools below are available but their schemas are NOT loaded yet. To use any of them, call ${convert('SearchTools')} first to load the schema, then call the tool.\n${lines.join('\n')}\n</harness-note>`;
6561
6781
  }
6562
6782
  }
6563
6783
  //# sourceMappingURL=CortexOrchestrator.js.map