feat(core:kernel): tool call dispatch loop, tool definitions wiring, sandboxRoot and defaultSystemPromptPath in config

This commit is contained in:
2026-05-18 21:39:50 +04:00
parent 15d1e09c44
commit 5c3a8fda63
5 changed files with 218 additions and 35 deletions
@@ -36,7 +36,7 @@ class DefaultSessionOrchestrator(
graph: WorkflowGraph, graph: WorkflowGraph,
config: OrchestrationConfig, config: OrchestrationConfig,
): WorkflowResult { ): WorkflowResult {
System.err.println("[Orchestrator] session=${sessionId.value} workflow=${graph.id} start=${graph.start.value}") log.debug("[Orchestrator] session={} workflow={} start={}", sessionId.value, graph.id, graph.start.value)
emitWorkflowStarted(sessionId, graph, config) emitWorkflowStarted(sessionId, graph, config)
val base = ExecutionContext(graph, sessionId, 0, graph.start, config, null, null) val base = ExecutionContext(graph, sessionId, 0, graph.start, config, null, null)
@@ -50,9 +50,9 @@ class DefaultSessionOrchestrator(
} }
private tailrec suspend fun step(ctx: EnrichedExecutionContext): WorkflowResult { private tailrec suspend fun step(ctx: EnrichedExecutionContext): WorkflowResult {
System.err.println( log.debug(
"[Orchestrator] step session=${ctx.sessionId.value} stage=${ctx.currentStageId.value} " + "[Orchestrator] step session={} stage={} stageCount={}",
"stageCount=${ctx.stageCount}" ctx.sessionId.value, ctx.currentStageId.value, ctx.stageCount,
) )
if (isCancelled(ctx.sessionId)) return handleCancellation(ctx.sessionId, ctx.currentStageId) if (isCancelled(ctx.sessionId)) return handleCancellation(ctx.sessionId, ctx.currentStageId)
@@ -68,13 +68,15 @@ class DefaultSessionOrchestrator(
val stageConfig = enriched.graph.stages[enriched.currentStageId] val stageConfig = enriched.graph.stages[enriched.currentStageId]
val stageArtifacts = stageConfig?.produces val stageArtifacts = stageConfig?.produces
?.let { repositories.artifactRepository.getByIds(enriched.sessionId, it) } ?.let { slots ->
repositories.artifactRepository.getByIds(enriched.sessionId, slots.map { it.name }.toSet())
}
?: emptyMap() ?: emptyMap()
val decision = resolveTransition(enriched.graph, enriched.sessionId, enriched.currentStageId, stageArtifacts) val decision = resolveTransition(enriched.graph, enriched.sessionId, enriched.currentStageId, stageArtifacts)
System.err.println( log.debug(
"[Orchestrator] transition session=${enriched.sessionId.value} " + "[Orchestrator] transition session={} stage={} decision={}",
"stage=${enriched.currentStageId.value} decision=${decision::class.simpleName}" enriched.sessionId.value, enriched.currentStageId.value, decision::class.simpleName,
) )
return when (decision) { return when (decision) {
is TransitionDecision.Move -> when (val r = executeMove(enriched, decision)) { is TransitionDecision.Move -> when (val r = executeMove(enriched, decision)) {
@@ -2,9 +2,12 @@ package com.correx.core.kernel.orchestration
import com.correx.core.events.execution.RetryPolicy import com.correx.core.events.execution.RetryPolicy
import com.correx.core.kernel.execution.ReplayStrategy import com.correx.core.kernel.execution.ReplayStrategy
import java.nio.file.Path
data class OrchestrationConfig( data class OrchestrationConfig(
val retryPolicy: RetryPolicy = RetryPolicy(maxAttempts = 3), val retryPolicy: RetryPolicy = RetryPolicy(maxAttempts = 3),
val replayStrategy: ReplayStrategy = ReplayStrategy.Full, val replayStrategy: ReplayStrategy = ReplayStrategy.Full,
val stageTimeoutMs: Long = 60_000L, val stageTimeoutMs: Long = 60_000L,
val sandboxRoot: Path = Path.of(System.getProperty("java.io.tmpdir"), "correx-sandbox"),
val defaultSystemPromptPath: String? = null,
) )
@@ -4,6 +4,8 @@ import com.correx.core.approvals.domain.ApprovalEngine
import com.correx.core.context.builder.ContextPackBuilder import com.correx.core.context.builder.ContextPackBuilder
import com.correx.core.inference.InferenceRouter import com.correx.core.inference.InferenceRouter
import com.correx.core.risk.RiskAssessor import com.correx.core.risk.RiskAssessor
import com.correx.core.tools.contract.ToolExecutor
import com.correx.core.tools.registry.ToolRegistry
import com.correx.core.transitions.evaluation.PromptResolver import com.correx.core.transitions.evaluation.PromptResolver
import com.correx.core.transitions.resolution.TransitionResolver import com.correx.core.transitions.resolution.TransitionResolver
import com.correx.core.validation.pipeline.ValidationPipeline import com.correx.core.validation.pipeline.ValidationPipeline
@@ -16,4 +18,6 @@ data class OrchestratorEngines(
val approvalEngine: ApprovalEngine, val approvalEngine: ApprovalEngine,
val riskAssessor: RiskAssessor, val riskAssessor: RiskAssessor,
val promptResolver: PromptResolver = PromptResolver { "" }, val promptResolver: PromptResolver = PromptResolver { "" },
val toolExecutor: ToolExecutor? = null,
val toolRegistry: ToolRegistry? = null,
) )
@@ -10,8 +10,9 @@ import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import com.correx.core.inference.GenerationConfig import com.correx.core.inference.GenerationConfig
import com.correx.core.sessions.Session
import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.ResponseFormat
import com.correx.core.sessions.Session
import com.correx.core.kernel.execution.ReplayStrategy import com.correx.core.kernel.execution.ReplayStrategy
import com.correx.core.kernel.execution.WorkflowResult import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.kernel.replay.ReplayInferenceProvider import com.correx.core.kernel.replay.ReplayInferenceProvider
@@ -142,12 +143,14 @@ class ReplayOrchestrator(
cancellations.getOrPut(sessionId) { AtomicBoolean(false) }.set(true) cancellations.getOrPut(sessionId) { AtomicBoolean(false) }.set(true)
} }
@Suppress("LongParameterList")
override suspend fun runInference( override suspend fun runInference(
sessionId: SessionId, sessionId: SessionId,
stageId: StageId, stageId: StageId,
contextPack: ContextPack, contextPack: ContextPack,
stageConfig: StageConfig, stageConfig: StageConfig,
timeoutMs: Long, timeoutMs: Long,
responseFormat: ResponseFormat,
): InferenceResult = when (strategy) { ): InferenceResult = when (strategy) {
is ReplayStrategy.SkipInference -> { is ReplayStrategy.SkipInference -> {
// bypass router entirely — use recorded artifact // bypass router entirely — use recorded artifact
@@ -162,6 +165,7 @@ class ReplayOrchestrator(
maxTokens = 0, maxTokens = 0,
seed = 0L, seed = 0L,
), ),
responseFormat = responseFormat,
) )
runCatching { replayProvider.infer(request) } runCatching { replayProvider.infer(request) }
.fold( .fold(
@@ -175,7 +179,7 @@ class ReplayOrchestrator(
}, },
) )
} }
else -> super.runInference(sessionId, stageId, contextPack, stageConfig, timeoutMs) else -> super.runInference(sessionId, stageId, contextPack, stageConfig, timeoutMs, responseFormat)
} }
override suspend fun mapValidationOutcome( override suspend fun mapValidationOutcome(
@@ -28,6 +28,7 @@ import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.artifacts.ArtifactState import com.correx.core.artifacts.ArtifactState
import com.correx.core.artifacts.kind.JsonSchema
import com.correx.core.events.stores.EventStore import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ApprovalDecisionId import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId import com.correx.core.events.types.ApprovalRequestId
@@ -39,10 +40,22 @@ import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId import com.correx.core.events.types.StageId
import com.correx.core.events.types.ValidationReportId import com.correx.core.events.types.ValidationReportId
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.inference.FinishReason
import com.correx.core.inference.InferenceRepository import com.correx.core.inference.InferenceRepository
import com.correx.core.inference.InferenceRequest import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.InferenceResponse import com.correx.core.inference.InferenceResponse
import com.correx.core.inference.InferenceRouter import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.ResponseFormat
import com.correx.core.inference.ToolCallRequest
import com.correx.core.inference.ToolDefinition
import com.correx.core.inference.ToolFunction
import com.correx.core.tools.contract.ToolExecutor
import com.correx.core.tools.contract.ToolResult
import com.correx.core.tools.registry.ToolRegistry
import com.correx.core.kernel.execution.WorkflowResult import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.risk.RiskAssessor import com.correx.core.risk.RiskAssessor
import com.correx.core.risk.RiskContext import com.correx.core.risk.RiskContext
@@ -58,16 +71,19 @@ import com.correx.core.transitions.resolution.TransitionResolver
import com.correx.core.validation.model.ValidationContext import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.pipeline.ValidationOutcome import com.correx.core.validation.pipeline.ValidationOutcome
import com.correx.core.validation.pipeline.ValidationPipeline import com.correx.core.validation.pipeline.ValidationPipeline
import org.slf4j.LoggerFactory
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
import java.util.* import java.util.*
import java.util.concurrent.* import java.util.concurrent.*
import java.util.concurrent.atomic.* import java.util.concurrent.atomic.*
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
private const val MAX_TOOL_ROUNDS = 10
@SuppressWarnings( @SuppressWarnings(
"ForbiddenComment", "ForbiddenComment",
"UnusedParameter", "UnusedParameter",
@@ -90,6 +106,8 @@ abstract class SessionOrchestrator(
private val validationPipeline: ValidationPipeline = engines.validationPipeline private val validationPipeline: ValidationPipeline = engines.validationPipeline
private val riskAssessor: RiskAssessor = engines.riskAssessor private val riskAssessor: RiskAssessor = engines.riskAssessor
private val promptResolver: PromptResolver = engines.promptResolver private val promptResolver: PromptResolver = engines.promptResolver
private val toolExecutor: ToolExecutor? = engines.toolExecutor
private val toolRegistry: ToolRegistry? = engines.toolRegistry
private val inferenceRepository: InferenceRepository = repositories.inferenceRepository private val inferenceRepository: InferenceRepository = repositories.inferenceRepository
private val artifactRepository = repositories.artifactRepository private val artifactRepository = repositories.artifactRepository
internal val orchestrationRepository: OrchestrationRepository = repositories.orchestrationRepository internal val orchestrationRepository: OrchestrationRepository = repositories.orchestrationRepository
@@ -107,6 +125,7 @@ abstract class SessionOrchestrator(
// --- stage execution --- // --- stage execution ---
@Suppress("CyclomaticComplexMethod")
internal suspend fun executeStage( internal suspend fun executeStage(
sessionId: SessionId, sessionId: SessionId,
stageId: StageId, stageId: StageId,
@@ -129,6 +148,29 @@ abstract class SessionOrchestrator(
.getOrNull() .getOrNull()
} }
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
?.let { text ->
listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
content = text,
sourceType = "systemPrompt",
sourceId = stageId.value,
tokenEstimate = text.length / 4,
),
)
} ?: config.defaultSystemPromptPath
?.let { path ->
runCatching { promptResolver.resolve(path) }
.onFailure {
log.error(
"[SessionOrchestrator] stage=${stageId.value}: " +
"failed to load default system prompt '$path': ${it.message}",
)
}
.getOrNull()
}
?.takeIf { it.isNotBlank() }
?.let { text -> ?.let { text ->
listOf( listOf(
ContextEntry( ContextEntry(
@@ -166,15 +208,49 @@ abstract class SessionOrchestrator(
) )
} ?: emptyList() } ?: emptyList()
val llmEmittedSlots = stageConfig.produces.filter { it.kind.llmEmitted }
require(llmEmittedSlots.size <= 1) {
"Stage '${stageId.value}' declares ${llmEmittedSlots.size} LLM-emitted artifact kinds; " +
"only 0 or 1 is supported"
}
val responseFormat = llmEmittedSlots.firstOrNull()
?.let { ResponseFormat.Json(it.kind.deriveJsonSchema()) }
?: ResponseFormat.Text
val schemaEntries = buildSchemaEntries(responseFormat, stageId)
val contextPack = contextPackBuilder.build( val contextPack = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()), id = ContextPackId(UUID.randomUUID().toString()),
sessionId = sessionId, sessionId = sessionId,
stageId = stageId, stageId = stageId,
entries = systemPrompt + promptEntries, entries = systemPrompt + schemaEntries + promptEntries,
budget = TokenBudget(limit = stageConfig.tokenBudget), budget = TokenBudget(limit = stageConfig.tokenBudget),
) )
val inferenceResult = runInference(sessionId, stageId, contextPack, stageConfig, config.stageTimeoutMs) var currentContext = contextPack
var inferenceResult = runInference(
sessionId, stageId, currentContext, stageConfig, config.stageTimeoutMs, responseFormat,
)
var toolRounds = 0
while (
inferenceResult is InferenceResult.Success &&
inferenceResult.response.finishReason is FinishReason.ToolCall &&
toolRounds < MAX_TOOL_ROUNDS
) {
val toolEntries = dispatchToolCalls(sessionId, stageId, inferenceResult.response.toolCalls)
val allEntries = currentContext.layers.values.flatten() + toolEntries
currentContext = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()),
sessionId = sessionId,
stageId = stageId,
entries = allEntries,
budget = TokenBudget(limit = stageConfig.tokenBudget),
)
inferenceResult = runInference(
sessionId, stageId, currentContext, stageConfig, config.stageTimeoutMs, responseFormat,
)
toolRounds++
}
return when (inferenceResult) { return when (inferenceResult) {
is InferenceResult.Cancelled -> StageExecutionResult.Failure("CANCELLED", retryable = false) is InferenceResult.Cancelled -> StageExecutionResult.Failure("CANCELLED", retryable = false)
is InferenceResult.Failed -> StageExecutionResult.Failure(inferenceResult.reason, retryable = true) is InferenceResult.Failed -> StageExecutionResult.Failure(inferenceResult.reason, retryable = true)
@@ -189,14 +265,94 @@ abstract class SessionOrchestrator(
} }
} }
private suspend fun dispatchToolCalls(
sessionId: SessionId,
stageId: StageId,
toolCalls: List<ToolCallRequest>,
): List<ContextEntry> {
val executor = toolExecutor ?: return emptyList()
return toolCalls.flatMap { toolCall ->
val invocationId = ToolInvocationId(UUID.randomUUID().toString())
val parameters = runCatching {
val element = Json.parseToJsonElement(toolCall.function.arguments)
val jsonObject = element as? kotlinx.serialization.json.JsonObject ?: return@runCatching emptyMap()
jsonObject.entries.associate { (k, v) -> k to v.toString().removeSurrounding("\"") as Any }
}.getOrElse { emptyMap() }
val request = ToolRequest(
invocationId = invocationId,
sessionId = sessionId,
stageId = stageId,
toolName = toolCall.function.name,
parameters = parameters,
)
val tier = toolRegistry?.resolve(toolCall.function.name)?.tier ?: Tier.T2
emit(
sessionId,
ToolInvocationRequestedEvent(
invocationId = invocationId,
sessionId = sessionId,
stageId = stageId,
toolName = toolCall.function.name,
tier = tier,
request = request,
),
)
val result = executor.execute(request)
val sourceId = toolCall.id ?: invocationId.value
val assistantEntry = ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L2,
sourceType = "assistantToolCall",
sourceId = sourceId,
content = Json.encodeToString(ToolCallRequest.serializer(), toolCall),
tokenEstimate = toolCall.function.arguments.length / 4,
)
val resultContent = when (result) {
is ToolResult.Success -> result.output
is ToolResult.Failure -> "ERROR: ${result.reason}"
}
val resultEntry = ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L2,
sourceType = "toolResult",
sourceId = sourceId,
content = resultContent,
tokenEstimate = resultContent.length / 4,
)
listOf(assistantEntry, resultEntry)
}
}
private fun buildSchemaEntries(
responseFormat: ResponseFormat,
stageId: StageId,
): List<ContextEntry> {
if (responseFormat !is ResponseFormat.Json) return emptyList()
val compactSchema = Json.encodeToString(JsonSchema.serializer(), responseFormat.schema)
val instruction = "Respond with a single JSON object matching this schema. " +
"Do not include markdown, code fences, or commentary outside the JSON. " +
"Schema: $compactSchema"
return listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
content = instruction,
sourceType = "schemaInstruction",
sourceId = stageId.value,
tokenEstimate = instruction.length / 4,
),
)
}
private fun verifyProduces( private fun verifyProduces(
sessionId: SessionId, sessionId: SessionId,
stageId: StageId, stageId: StageId,
stageConfig: StageConfig, stageConfig: StageConfig,
): StageExecutionResult { ): StageExecutionResult {
if (stageConfig.produces.isEmpty()) return StageExecutionResult.Success(emptyList()) if (stageConfig.produces.isEmpty()) return StageExecutionResult.Success(emptyList())
val present = artifactRepository.getByIds(sessionId, stageConfig.produces).keys val producedIds = stageConfig.produces.map { it.name }.toSet()
val missing = stageConfig.produces - present val present = artifactRepository.getByIds(sessionId, producedIds).keys
val missing = producedIds - present
if (missing.isEmpty()) return StageExecutionResult.Success(emptyList()) if (missing.isEmpty()) return StageExecutionResult.Success(emptyList())
val missingIds = missing.joinToString(", ") { it.value } val missingIds = missing.joinToString(", ") { it.value }
log.error( log.error(
@@ -221,17 +377,19 @@ abstract class SessionOrchestrator(
is ValidationOutcome.NeedsApproval -> handleApproval(sessionId, stageId, outcome) is ValidationOutcome.NeedsApproval -> handleApproval(sessionId, stageId, outcome)
} }
@Suppress("LongParameterList")
internal open suspend fun runInference( internal open suspend fun runInference(
sessionId: SessionId, sessionId: SessionId,
stageId: StageId, stageId: StageId,
contextPack: ContextPack, contextPack: ContextPack,
stageConfig: StageConfig, stageConfig: StageConfig,
timeoutMs: Long, timeoutMs: Long,
responseFormat: ResponseFormat = ResponseFormat.Text,
): InferenceResult { ): InferenceResult {
val provider = inferenceRouter.route(stageId, stageConfig.requiredCapabilities) val provider = inferenceRouter.route(stageId, stageConfig.requiredCapabilities)
System.err.println( log.debug(
"[Orchestrator] inference session=${sessionId.value} stage=${stageId.value} " + "[Orchestrator] inference session={} stage={} provider={} timeoutMs={}",
"provider=${provider.id.value} timeoutMs=$timeoutMs", sessionId.value, stageId.value, provider.id.value, timeoutMs,
) )
val requestId = InferenceRequestId(UUID.randomUUID().toString()) val requestId = InferenceRequestId(UUID.randomUUID().toString())
val request = InferenceRequest( val request = InferenceRequest(
@@ -240,6 +398,18 @@ abstract class SessionOrchestrator(
stageId = stageId, stageId = stageId,
contextPack = contextPack, contextPack = contextPack,
generationConfig = stageConfig.generationConfig, generationConfig = stageConfig.generationConfig,
responseFormat = responseFormat,
tools = stageConfig.allowedTools
.mapNotNull { toolRegistry?.resolve(it) }
.map { tool ->
ToolDefinition(
function = ToolFunction(
name = tool.name,
description = tool.description,
parameters = tool.parametersSchema,
),
)
},
) )
// TODO: capture rendered prompt; currently contextpack.toString() // TODO: capture rendered prompt; currently contextpack.toString()
@@ -250,9 +420,9 @@ abstract class SessionOrchestrator(
return try { return try {
val response = withTimeout(timeoutMs) { provider.infer(request) } val response = withTimeout(timeoutMs) { provider.infer(request) }
System.err.println( log.debug(
"[Orchestrator] inference done session=${sessionId.value} stage=${stageId.value} " + "[Orchestrator] inference done session={} stage={} tokens={} latencyMs={}",
"tokens=${response.tokensUsed} latencyMs=${response.latencyMs}", sessionId.value, stageId.value, response.tokensUsed, response.latencyMs,
) )
if (isCancelled(sessionId)) return InferenceResult.Cancelled if (isCancelled(sessionId)) return InferenceResult.Cancelled
@@ -343,9 +513,9 @@ abstract class SessionOrchestrator(
terminalStageId: StageId, terminalStageId: StageId,
stageCount: Int, stageCount: Int,
): WorkflowResult.Completed { ): WorkflowResult.Completed {
System.err.println( log.debug(
"[Orchestrator] COMPLETED session=${sessionId.value} " + "[Orchestrator] COMPLETED session={} terminalStage={} stages={}",
"terminalStage=${terminalStageId.value} stages=$stageCount", sessionId.value, terminalStageId.value, stageCount,
) )
emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount)) emit(sessionId, WorkflowCompletedEvent(sessionId, terminalStageId, stageCount))
cancellations.remove(sessionId) cancellations.remove(sessionId)
@@ -358,9 +528,9 @@ abstract class SessionOrchestrator(
reason: String, reason: String,
retryExhausted: Boolean, retryExhausted: Boolean,
): WorkflowResult.Failed { ): WorkflowResult.Failed {
System.err.println( log.warn(
"[Orchestrator] FAILED session=${sessionId.value} stage=${stageId.value} " + "[Orchestrator] FAILED session={} stage={} reason={} retryExhausted={}",
"reason=$reason retryExhausted=$retryExhausted", sessionId.value, stageId.value, reason, retryExhausted,
) )
emit(sessionId, WorkflowFailedEvent(sessionId, stageId, reason, retryExhausted)) emit(sessionId, WorkflowFailedEvent(sessionId, stageId, reason, retryExhausted))
cancellations.remove(sessionId) cancellations.remove(sessionId)
@@ -371,7 +541,7 @@ abstract class SessionOrchestrator(
sessionId: SessionId, sessionId: SessionId,
stageId: StageId, stageId: StageId,
): WorkflowResult.Cancelled { ): WorkflowResult.Cancelled {
System.err.println("[Orchestrator] CANCELLED session=${sessionId.value} stage=${stageId.value}") log.warn("[Orchestrator] CANCELLED session={} stage={}", sessionId.value, stageId.value)
emit(sessionId, WorkflowFailedEvent(sessionId, stageId, "CANCELLED", retryExhausted = false)) emit(sessionId, WorkflowFailedEvent(sessionId, stageId, "CANCELLED", retryExhausted = false))
cancellations.remove(sessionId) cancellations.remove(sessionId)
return WorkflowResult.Cancelled(sessionId) return WorkflowResult.Cancelled(sessionId)
@@ -407,7 +577,7 @@ abstract class SessionOrchestrator(
stageId: StageId, stageId: StageId,
outcome: ValidationOutcome.NeedsApproval, outcome: ValidationOutcome.NeedsApproval,
): StageExecutionResult { ): StageExecutionResult {
System.err.println("[Orchestrator] approval required session=${sessionId.value} stage=${stageId.value}") log.debug("[Orchestrator] approval required session={} stage={}", sessionId.value, stageId.value)
emit(sessionId, OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING")) emit(sessionId, OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING"))
val domainRequest = buildApprovalRequest(sessionId, stageId, outcome) val domainRequest = buildApprovalRequest(sessionId, stageId, outcome)
@@ -428,14 +598,14 @@ abstract class SessionOrchestrator(
pendingApprovals[domainRequest.id] = deferred pendingApprovals[domainRequest.id] = deferred
return try { return try {
System.err.println( log.debug(
"[Orchestrator] awaiting approval session=${sessionId.value} " + "[Orchestrator] awaiting approval session={} requestId={}",
"requestId=${domainRequest.id.value}", sessionId.value, domainRequest.id.value,
) )
val decision = deferred.await() val decision = deferred.await()
System.err.println( log.debug(
"[Orchestrator] approval resolved session=${sessionId.value} " + "[Orchestrator] approval resolved session={} approved={}",
"approved=${decision.isApproved}", sessionId.value, decision.isApproved,
) )
emitDecisionResolved(sessionId, domainRequest, decision) emitDecisionResolved(sessionId, domainRequest, decision)
if (decision.isApproved) { if (decision.isApproved) {