Merge remote-tracking branch 'origin/feat/session-robustness-and-dox' into feat/session-robustness-and-dox

This commit is contained in:
2026-06-29 20:58:07 +04:00
29 changed files with 1047 additions and 272 deletions
@@ -106,6 +106,7 @@ import com.correx.core.inference.ResponseFormat
import com.correx.core.inference.Tokenizer
import com.correx.core.inference.ToolCallRequest
import com.correx.core.inference.ToolDefinition
import com.correx.core.inference.ModelCapability
import com.correx.core.sessions.ApprovalMode
import com.correx.core.inference.ToolFunction
import com.correx.core.kernel.execution.WorkflowResult
@@ -561,7 +562,8 @@ abstract class SessionOrchestrator(
continue
}
val toolEntries = dispatchToolCalls(sessionId, stageId,
inferenceResult.response.toolCalls, stageConfig, effectives)
inferenceResult.response.toolCalls, stageConfig, effectives,
approvalModeFor(session.state.boundProfile?.approvalMode))
val fatalEntry = toolEntries.firstOrNull { it.content.startsWith("FATAL:") }
if (fatalEntry != null) {
emitProcessResultEvents(sessionId, stageId, stageConfig)
@@ -654,12 +656,33 @@ abstract class SessionOrchestrator(
}
@Suppress("CyclomaticComplexMethod")
// Maps the operator profile's free-text approval_mode onto the engine's [ApprovalMode].
// Unset/unknown falls back to PROMPT so an absent profile keeps the human-in-the-loop
// default; only an explicit auto/yolo opts a session into unattended approval.
private fun approvalModeFor(profileMode: String?): ApprovalMode =
when (profileMode?.trim()?.lowercase()) {
"deny" -> ApprovalMode.DENY
"auto" -> ApprovalMode.AUTO
"yolo" -> ApprovalMode.YOLO
else -> ApprovalMode.PROMPT
}
// On a recoverable tool failure, append the tool's argument schema to the error fed back to the
// model so it can self-correct its next attempt (it already sees its own malformed call in the
// assistant entry above) rather than repeating the same mistake. Kept compact to bound context.
private fun toolArgsHint(tool: Tool?): String =
tool?.let {
"\nThe '${it.name}' tool requires arguments matching this JSON schema — re-issue the " +
"call with corrected arguments: ${it.parametersSchema}"
}.orEmpty()
private suspend fun dispatchToolCalls(
sessionId: SessionId,
stageId: StageId,
toolCalls: List<ToolCallRequest>,
stageConfig: StageConfig,
effectives: RunEffectives,
approvalMode: ApprovalMode,
): List<ContextEntry> {
val executor = effectives.executor ?: return emptyList()
val processResultSlots = stageConfig.produces.filter { it.kind.id == "process_result" }
@@ -793,7 +816,7 @@ abstract class SessionOrchestrator(
val projectId = effectives.policy?.workspaceRoot?.let { ProjectIdentity.of(it.toString()) }
val approvalCtx = ApprovalContext(
identity = ApprovalScopeIdentity(sessionId, stageId, projectId = projectId),
mode = ApprovalMode.PROMPT,
mode = approvalMode,
)
val requestId = ApprovalRequestId(UUID.randomUUID().toString())
val toolPreview = computeToolPreview(toolCall.function.name, parameters)
@@ -968,7 +991,7 @@ abstract class SessionOrchestrator(
?: result.output
is ToolResult.Failure -> {
if (!result.recoverable) "FATAL: ${result.reason}"
else "ERROR: ${result.reason}"
else "ERROR: ${result.reason}${toolArgsHint(tool)}"
}
}
val resultEntry = ContextEntry(
@@ -1772,7 +1795,12 @@ abstract class SessionOrchestrator(
responseFormat: ResponseFormat = ResponseFormat.Text,
effectives: RunEffectives = RunEffectives(toolRegistry, toolExecutor, workspacePolicy),
): InferenceResult {
val provider = inferenceRouter.route(stageId, stageConfig.requiredCapabilities, stageConfig.modelId)
// A stage that grants tools needs a tool-calling model, so request ToolCalling on top of any
// declared capabilities — the capability-aware strategy then ranks eligible providers by their
// ToolCalling score and routes the stage to the best tool-caller.
val requiredCapabilities = stageConfig.requiredCapabilities +
if (stageConfig.allowedTools.isNotEmpty()) setOf(ModelCapability.ToolCalling) else emptySet()
val provider = inferenceRouter.route(stageId, requiredCapabilities, stageConfig.modelId)
log.debug(
"[Orchestrator] inference session={} stage={} provider={} timeoutMs={}",
sessionId.value, stageId.value, provider.id.value, timeoutMs,