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
@@ -537,6 +537,8 @@ object ConfigLoader {
modelPath = modelPath,
url = url,
capabilities = capabilities,
apiKey = asString(providerMap["api_key"], ""),
apiKeyEnv = asString(providerMap["api_key_env"], ""),
)
}
@@ -173,6 +173,11 @@ data class ProviderConfig(
val modelPath: String = "",
val url: String = "http://127.0.0.1:10000",
val capabilities: Map<String, Double> = emptyMap(),
// Bearer credentials for remote providers (e.g. NVIDIA NIM). `apiKey` is the literal token;
// `apiKeyEnv` names an env var to read it from at startup (preferred — keeps the secret out
// of config files). Both are blank/unused for local providers like llamacpp.
val apiKey: String = "",
val apiKeyEnv: String = "",
)
@Serializable
@@ -113,6 +113,8 @@ object CorrexConfigWriter {
b.kv("model_path", str(p.modelPath))
b.kv("url", str(p.url))
if (p.capabilities.isNotEmpty()) b.kv("capabilities", caps(p.capabilities))
if (p.apiKey.isNotEmpty()) b.kv("api_key", str(p.apiKey))
if (p.apiKeyEnv.isNotEmpty()) b.kv("api_key_env", str(p.apiKeyEnv))
}
cfg.models.forEach { m ->
+3 -1
View File
@@ -13,7 +13,9 @@ CORREX kernel team. This is the integration point for all other `core/` modules.
- `SessionOrchestrator` / `DefaultSessionOrchestrator` — primary entry point for launching and advancing sessions through workflow stages.
- `OrchestrationState` / `OrchestrationReducer` (`DefaultOrchestrationReducer`) / `OrchestrationProjector` / `OrchestrationRepository` — standard event-sourcing stack for orchestration state.
- `RetryCoordinator` / `DefaultRetryCoordinator` — manages retry logic per `RetryPolicy`.
- `ApprovalGateway` — kernel-side approval bridge; calls `core:approvals` engine before executing gated operations.
- On a recoverable tool failure `dispatchToolCalls` feeds the failing tool's argument schema back into context alongside the error (`toolArgsHint`), so the model self-corrects a malformed call instead of repeating it — the contract stays strict; the feedback is what loosens.
- Stages that grant tools (`allowedTools` non-empty) request `ModelCapability.ToolCalling` on top of their declared capabilities when routing, so the capability-aware strategy steers them to the best tool-calling model.
- `ApprovalGateway` — kernel-side approval bridge; calls `core:approvals` engine before executing gated operations. Per-tool gating in `dispatchToolCalls` builds the `ApprovalContext` mode from the session's bound operator profile (`boundProfile.approvalMode`, mapped by `approvalModeFor`): unset/`prompt` keeps a human in the loop (default), `auto` auto-approves up to T2, `yolo` all tiers, `deny` blocks above T0. The engine is always consulted (Invariant #4 holds); policy/plane-2 BLOCK stays terminal regardless of mode.
- `ReplayOrchestrator` / `ReplayInferenceProvider` / `ReplayStrategy` — deterministic replay of a session from its event log. `ReplayInferenceProvider` returns recorded responses — no live LLM (Hard Invariant #8).
- `SubagentRunner` / `InSessionSubagentRunner` — runs sub-agent invocations within an active session.
- `StaticAnalysisRunner` / `ProcessStaticAnalysisRunner` — runs static analysis tools and records results as events.
@@ -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,