diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 0fddc6c9..755a3c48 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -239,11 +239,11 @@ abstract class SessionOrchestrator( inferenceResult.response.finishReason is FinishReason.ToolCall && toolRounds < MAX_TOOL_ROUNDS ) { - val toolEntries = dispatchToolCalls(sessionId, stageId, inferenceResult.response.toolCalls) + val toolEntries = dispatchToolCalls(sessionId, stageId, + inferenceResult.response.toolCalls, stageConfig) val fatalEntry = toolEntries.firstOrNull { it.content.startsWith("FATAL:") } if (fatalEntry != null) { - storeFailedProcessResult(sessionId, stageId, stageConfig, - inferenceResult.response.toolCalls, fatalEntry) + emitProcessResultEvents(sessionId, stageId, stageConfig) return StageExecutionResult.Failure( fatalEntry.content.removePrefix("FATAL: "), retryable = false, @@ -251,6 +251,7 @@ abstract class SessionOrchestrator( } val errorEntry = toolEntries.firstOrNull { it.content.startsWith("ERROR:") } if (errorEntry != null) { + emitProcessResultEvents(sessionId, stageId, stageConfig) return StageExecutionResult.Failure( errorEntry.content.removePrefix("ERROR: "), retryable = true, @@ -299,8 +300,10 @@ abstract class SessionOrchestrator( sessionId: SessionId, stageId: StageId, toolCalls: List, + stageConfig: StageConfig, ): List { val executor = toolExecutor ?: return emptyList() + val processResultSlots = stageConfig.produces.filter { it.kind.id == "process_result" } return toolCalls.flatMap { toolCall -> val invocationId = ToolInvocationId(UUID.randomUUID().toString()) val parameters = runCatching { @@ -391,6 +394,32 @@ abstract class SessionOrchestrator( emit(sessionId, OrchestrationResumedEvent(sessionId, stageId)) } val result = executor.execute(request) + + // Store ProcessResult artifact for every shell execution outcome + for (slot in processResultSlots) { + val processResult = when (result) { + is ToolResult.Success -> ProcessResultArtifact( + status = "success", + command = toolCall.function.arguments, + exitCode = result.exitCode, + stdout = result.output, + stderr = result.metadata["stderr"] ?: "", + durationMs = 0, + ) + is ToolResult.Failure -> ProcessResultArtifact( + status = "failed", + command = toolCall.function.arguments, + exitCode = -1, + stdout = "", + stderr = result.reason, + durationMs = 0, + ) + } + val jsonContent = Json.encodeToString(ProcessResultArtifact.serializer(), processResult) + artifactStore.put(jsonContent.toByteArray()) + artifactContentCache["${sessionId.value}:${slot.name.value}"] = jsonContent + } + val sourceId = toolCall.id ?: invocationId.value val assistantEntry = ContextEntry( id = ContextEntryId(UUID.randomUUID().toString()), @@ -477,28 +506,14 @@ abstract class SessionOrchestrator( ) } - private suspend fun storeFailedProcessResult( + private suspend fun emitProcessResultEvents( sessionId: SessionId, stageId: StageId, stageConfig: StageConfig, - toolCalls: List, - fatalEntry: ContextEntry, ) { + // Emit artifact lifecycle events for process_result slots on failure branches. + // Content was already stored in CAS + cache by dispatchToolCalls for every outcome. stageConfig.produces.filter { it.kind.id == "process_result" }.forEach { slot -> - val cmd = toolCalls.firstOrNull()?.function?.arguments ?: "" - val reason = fatalEntry.content.removePrefix("FATAL: ") - val processResult = ProcessResultArtifact( - status = "failed", - command = cmd, - exitCode = -1, - stdout = "", - stderr = reason, - durationMs = 0, - ) - val jsonContent = Json.encodeToString(ProcessResultArtifact.serializer(), processResult) - artifactStore.put(jsonContent.toByteArray()) - val cacheKey = "${sessionId.value}:${slot.name.value}" - artifactContentCache[cacheKey] = jsonContent emit(sessionId, ArtifactCreatedEvent(slot.name, sessionId, stageId, schemaVersion = 1)) emit(sessionId, ArtifactValidatingEvent(slot.name, sessionId, stageId)) emit(sessionId, ArtifactValidatedEvent(slot.name, sessionId, stageId))