fix(validation): resolve F-007 artifact validator CAS-hash conflation
ArtifactPayloadValidator passed the logical slot name into the content-hash-keyed CAS store, crashing every workflow with a typed produces slot. Validator now reads payloads from a content map threaded through ValidationContext (populated from the orchestrator's per-session artifact cache across all stages) and no longer depends on ArtifactStore. Also records the slot->CAS-hash mapping as a new ArtifactContentStoredEvent (registered for serialization), emitted at both CAS write sites, so artifact content is recoverable from CAS by hash after a cold start.
This commit is contained in:
@@ -30,3 +30,12 @@ data class ArtifactCreatedEvent(
|
||||
val stageId: StageId,
|
||||
val schemaVersion: Int,
|
||||
) : EventPayload
|
||||
|
||||
@Serializable
|
||||
@SerialName("ArtifactContentStored")
|
||||
data class ArtifactContentStoredEvent(
|
||||
val artifactId: ArtifactId,
|
||||
val contentHash: ArtifactId, // CAS content hash returned by ArtifactStore.put(...)
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
) : EventPayload
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
||||
import com.correx.core.events.events.ApprovalGrantCreatedEvent
|
||||
import com.correx.core.events.events.ApprovalGrantExpiredEvent
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||
@@ -70,6 +71,7 @@ val eventModule = SerializersModule {
|
||||
subclass(ApprovalDecisionResolvedEvent::class)
|
||||
subclass(ApprovalGrantCreatedEvent::class)
|
||||
subclass(ApprovalGrantExpiredEvent::class)
|
||||
subclass(ArtifactContentStoredEvent::class)
|
||||
subclass(ArtifactCreatedEvent::class)
|
||||
subclass(ArtifactValidatingEvent::class)
|
||||
subclass(ArtifactValidatedEvent::class)
|
||||
|
||||
+28
-4
@@ -22,6 +22,7 @@ import com.correx.core.context.model.TokenBudget
|
||||
import com.correx.core.context.model.EntryRole
|
||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||
import com.correx.core.events.events.ContextTruncatedEvent
|
||||
import com.correx.core.events.events.ToolCallAssessedEvent
|
||||
@@ -418,11 +419,25 @@ abstract class SessionOrchestrator(
|
||||
llmEmittedSlots.firstOrNull()?.let { slot ->
|
||||
artifactContentCache["${sessionId.value}:${slot.name.value}"] =
|
||||
inferenceResult.response.text
|
||||
inferenceResult.responseArtifactId?.let { hash ->
|
||||
emit(
|
||||
sessionId,
|
||||
ArtifactContentStoredEvent(
|
||||
artifactId = slot.name,
|
||||
contentHash = hash,
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val validationCtx = ValidationContext(
|
||||
graph = graph,
|
||||
sessionState = session.state,
|
||||
availableTools = effectives.registry?.all()?.map { it.name }?.toSet()
|
||||
availableTools = effectives.registry?.all()?.map { it.name }?.toSet(),
|
||||
producedArtifactContent = graph.stages.values.flatMap { it.produces }.mapNotNull { slot ->
|
||||
artifactContentCache["${sessionId.value}:${slot.name.value}"]?.let { slot.name.value to it }
|
||||
}.toMap(),
|
||||
)
|
||||
when (val outcome = mapValidationOutcome(sessionId, stageId, validationCtx)) {
|
||||
is StageExecutionResult.Success -> {
|
||||
@@ -702,8 +717,17 @@ abstract class SessionOrchestrator(
|
||||
)
|
||||
}
|
||||
val jsonContent = Json.encodeToString(ProcessResultArtifact.serializer(), processResult)
|
||||
artifactStore.put(jsonContent.toByteArray())
|
||||
val storedHash = artifactStore.put(jsonContent.toByteArray())
|
||||
artifactContentCache["${sessionId.value}:${slot.name.value}"] = jsonContent
|
||||
emit(
|
||||
sessionId,
|
||||
ArtifactContentStoredEvent(
|
||||
artifactId = slot.name,
|
||||
contentHash = storedHash,
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val sourceId = toolCall.id ?: invocationId.value
|
||||
@@ -1073,7 +1097,7 @@ abstract class SessionOrchestrator(
|
||||
),
|
||||
)
|
||||
|
||||
InferenceResult.Success(response)
|
||||
InferenceResult.Success(response, responseArtifactId)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
emit(
|
||||
sessionId,
|
||||
@@ -1456,7 +1480,7 @@ private fun buildDiffString(path: String, existingContent: String?, proposedCont
|
||||
}
|
||||
|
||||
internal sealed interface InferenceResult {
|
||||
data class Success(val response: InferenceResponse) : InferenceResult
|
||||
data class Success(val response: InferenceResponse, val responseArtifactId: ArtifactId? = null) : InferenceResult
|
||||
data class Failed(val reason: String) : InferenceResult
|
||||
data object Cancelled : InferenceResult
|
||||
}
|
||||
|
||||
+4
-8
@@ -3,7 +3,6 @@ package com.correx.core.validation.artifact
|
||||
import com.correx.core.artifacts.kind.FileWrittenArtifact
|
||||
import com.correx.core.artifacts.kind.ProcessResultArtifact
|
||||
import com.correx.core.artifacts.kind.TypedArtifactSlot
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.events.types.ArtifactId
|
||||
import com.correx.core.validation.model.ValidationContext
|
||||
import com.correx.core.validation.model.ValidationIssue
|
||||
@@ -13,16 +12,14 @@ import com.correx.core.validation.pipeline.Validator
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Path
|
||||
|
||||
class ArtifactPayloadValidator(
|
||||
private val artifactStore: ArtifactStore,
|
||||
) : Validator {
|
||||
class ArtifactPayloadValidator : Validator {
|
||||
|
||||
override suspend fun validate(context: ValidationContext): ValidationSection {
|
||||
val issues = mutableListOf<ValidationIssue>()
|
||||
|
||||
for ((_, stageConfig) in context.graph.stages) {
|
||||
for (slot in stageConfig.produces) {
|
||||
validateSlot(slot, issues)
|
||||
validateSlot(slot, context.producedArtifactContent, issues)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +28,10 @@ class ArtifactPayloadValidator(
|
||||
|
||||
private suspend fun validateSlot(
|
||||
slot: TypedArtifactSlot,
|
||||
artifactContent: Map<String, String>,
|
||||
issues: MutableList<ValidationIssue>,
|
||||
) {
|
||||
val bytes = artifactStore.get(slot.name) ?: return
|
||||
|
||||
val payloadStr = bytes.toString(Charsets.UTF_8)
|
||||
val payloadStr = artifactContent[slot.name.value] ?: return
|
||||
|
||||
when (slot.kind.id) {
|
||||
"file_written" ->
|
||||
|
||||
@@ -11,4 +11,5 @@ data class ValidationContext(
|
||||
val cyclePolicies: Set<CyclePolicyBinding> = emptySet(),
|
||||
val sessionState: SessionState? = null,
|
||||
val availableTools: Set<String>? = null,
|
||||
val producedArtifactContent: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user