feat(workflow): ExecutionPlanCompiler + ExecutionPlanLockedEvent (Slice 3)

- ExecutionPlanLockedEvent: registered, replay-deterministic record of the
  locked plan (CAS ref + compiled stage/start ids).
- ExecutionPlanCompiler: validated execution_plan JSON -> WorkflowGraph,
  reusing ConditionSpec.toCondition(); inline prompts via metadata[promptInline];
  malformed plans throw WorkflowValidationException.
- Replay test: locked event + plan JSON recompiles to the identical graph,
  no architect re-run, no FS reads.
This commit is contained in:
2026-06-08 02:33:57 +04:00
parent be78eaa80f
commit 6c8c5e2ad9
8 changed files with 356 additions and 0 deletions
@@ -0,0 +1,22 @@
package com.correx.core.events.events
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.SessionId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* The architect's validated execution_plan was compiled into a runnable WorkflowGraph and
* locked as the baseline for phase 2. [planArtifactId] points at the validated plan JSON in
* CAS (referenced, not inlined); [stageIds]/[startStageId] pin the compiled shape so replay
* rebuilds the identical graph. Soft lock: later high-priority steering can still preempt.
*/
@Serializable
@SerialName("ExecutionPlanLocked")
data class ExecutionPlanLockedEvent(
val sessionId: SessionId,
val planArtifactId: ArtifactId,
val workflowId: String,
val stageIds: List<String>,
val startStageId: String,
) : EventPayload
@@ -13,6 +13,7 @@ import com.correx.core.events.events.RouterNarrationEvent
import com.correx.core.events.events.SessionWorkspaceBoundEvent
import com.correx.core.events.events.ContextTruncatedEvent
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.L3MemoryRetrievedEvent
import com.correx.core.events.events.InferenceCompletedEvent
@@ -91,6 +92,7 @@ val eventModule = SerializersModule {
subclass(SessionWorkspaceBoundEvent::class)
subclass(L3MemoryRetrievedEvent::class)
subclass(ContextTruncatedEvent::class)
subclass(ExecutionPlanLockedEvent::class)
}
}
@@ -0,0 +1,27 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.SessionId
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class ExecutionPlanLockedEventSerializationTest {
@Test
fun `ExecutionPlanLockedEvent round-trips as polymorphic EventPayload`() {
val sample: EventPayload = ExecutionPlanLockedEvent(
sessionId = SessionId("s"),
planArtifactId = ArtifactId("execution_plan"),
workflowId = "freestyle-s",
stageIds = listOf("impl", "review"),
startStageId = "impl",
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
assertTrue(encoded.contains("\"type\":\"ExecutionPlanLocked\""), "SerialName must be present: $encoded")
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
assertEquals(sample, decoded)
}
}