feat(core:kernel): tool call dispatch loop, tool definitions wiring, sandboxRoot and defaultSystemPromptPath in config
This commit is contained in:
+10
-8
@@ -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)) {
|
||||||
|
|||||||
+3
@@ -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
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-2
@@ -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(
|
||||||
|
|||||||
+195
-25
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user