feat(kernel,events): freestyle graph re-routing — deterministic override core
Implements the replay-safe half of preemptive redirect (graph re-routing). An operator-confirmed PreemptRedirectEvent overrides the resolver's edge at the next transition boundary; the pure resolver stays untouched. - PreemptRedirectEvent / PreemptRedirectBlockedEvent (+ serialization registration) - TransitionExecutedEvent + TransitionDecision.Move gain optional redirectId — the durable 'consumed once' marker - PreemptRedirect.decide: pure decision over the event log (override / block / none), so replay reaches the identical outcome without re-classifying (invariant #8) - override wired in DefaultSessionOrchestrator.step above resolveTransition; back-edge jumps count against the existing maxRetries cap via executeMove - blocks jumps to unknown stages or targets with unsatisfied needs - DomainEventMapper: redirect events mapped to null (operator surface ships with the LLM-proposal + approval-confirm front-half) - tests: override+consume-once, block-on-needs, block-on-unknown, ignore-consumed Front-half (LLM proposal -> approval-gate confirm -> emit PreemptRedirectEvent) is deferred; needs live LLM verification. Until it ships no redirect event is emitted in production, so the override is inert. Spec: docs/plans/2026-06-10-freestyle-graph-rerouting.md
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package com.correx.core.events.events
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A confirmed preemptive redirect (freestyle graph re-routing): the run should jump from
|
||||
* [fromStageId] to [toStageId] at the next transition boundary, overriding the resolver's edge.
|
||||
*
|
||||
* This is the **recorded, deterministic** half of the hybrid design — an LLM proposes a target
|
||||
* from a free-text steering note and the operator confirms it (via the approval gate), but only
|
||||
* the confirmed [toStageId] is recorded here. Replay reads this target and never re-classifies
|
||||
* (invariants #8/#9). [sourceNoteId] links back to the originating steering note when present.
|
||||
*
|
||||
* Consumed exactly once: the override applies it and records [redirectId] on the resulting
|
||||
* [TransitionExecutedEvent], so a replay sees it already applied.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PreemptRedirect")
|
||||
data class PreemptRedirectEvent(
|
||||
val redirectId: String,
|
||||
val sessionId: SessionId,
|
||||
val fromStageId: StageId,
|
||||
val toStageId: StageId,
|
||||
val sourceNoteId: String? = null,
|
||||
val timestampMs: Long,
|
||||
) : EventPayload
|
||||
|
||||
/**
|
||||
* A preemptive redirect that failed the safety guard — the target stage is unknown/terminal or
|
||||
* its `needs` artifacts aren't satisfied (decision: invalid jumps are blocked). The run continues
|
||||
* on its normally-resolved edge. Recording this also marks [redirectId] consumed, so the blocked
|
||||
* redirect is not retried on every subsequent boundary.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PreemptRedirectBlocked")
|
||||
data class PreemptRedirectBlockedEvent(
|
||||
val redirectId: String,
|
||||
val sessionId: SessionId,
|
||||
val toStageId: StageId,
|
||||
val reason: String,
|
||||
val timestampMs: Long,
|
||||
) : EventPayload
|
||||
@@ -29,5 +29,9 @@ data class TransitionExecutedEvent(
|
||||
val sessionId: SessionId,
|
||||
val from: StageId,
|
||||
val to: StageId,
|
||||
val transitionId: TransitionId
|
||||
val transitionId: TransitionId,
|
||||
// Set when this transition was produced by a preemptive redirect override (freestyle
|
||||
// graph re-routing). Carries the originating PreemptRedirectEvent.redirectId — this is the
|
||||
// durable "redirect consumed" marker the override reads to apply each redirect exactly once.
|
||||
val redirectId: String? = null,
|
||||
) : EventPayload
|
||||
|
||||
@@ -33,6 +33,8 @@ import com.correx.core.events.events.RepoMapComputedEvent
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.events.RiskAssessedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.PreemptRedirectBlockedEvent
|
||||
import com.correx.core.events.events.PreemptRedirectEvent
|
||||
import com.correx.core.events.events.StageFailedEvent
|
||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||
import com.correx.core.events.events.ToolCallAssessedEvent
|
||||
@@ -63,6 +65,8 @@ val eventModule = SerializersModule {
|
||||
subclass(ToolCallAssessedEvent::class)
|
||||
subclass(FileWrittenEvent::class)
|
||||
subclass(SteeringNoteAddedEvent::class)
|
||||
subclass(PreemptRedirectEvent::class)
|
||||
subclass(PreemptRedirectBlockedEvent::class)
|
||||
subclass(InitialIntentEvent::class)
|
||||
subclass(StageFailedEvent::class)
|
||||
subclass(StageCompletedEvent::class)
|
||||
|
||||
Reference in New Issue
Block a user