feat(validation): capability-gap reflection rung (#30 part 2)
Bounded LLM "are you sure?" pass over the capability gaps part 1 detects, run once at plan-lock before the operator is asked to approve — honest mistakes self-correct without escalation; only a genuine tool need reaches the human. - CapabilityGapReflector: fun-interface seam in core:kernel (plain-data in/out, no infrastructure:workflow dep), mirroring SemanticReviewer/SalvageJudge. - CapabilityGapReflectorImpl (apps/server): one InferenceRouter call for the whole gap batch, no per-gap loop, no retry; runCatching degrades to a safe RESOLVED-advisory default on any failure (unroutable/timeout/malformed JSON) — a broken pass can never manufacture a NEEDS_TOOL escalation or a grant. - CapabilityGapReflectedEvent(verdict RESOLVED|NEEDS_TOOL) recorded per gap and registered in eventModule; replay reads the event, never re-invokes inference (invariants #7/#8/#9). - FreestyleDriver.lockAndRun: RESOLVED is advisory (recorded, plan untouched — invariant #3); NEEDS_TOOL is appended to the existing requestPlanApproval preview so the operator decides — no auto-grant anywhere (#4/#5). - reflector nullable -> degrades to part-1 behavior when unwired. Verified: ./gradlew :core:events:test :core:kernel:test :infrastructure:workflow:test :apps:server:test green.
This commit is contained in:
@@ -73,3 +73,28 @@ data class CapabilityGapDetectedEvent(
|
||||
val evidence: String,
|
||||
val timestampMs: Long,
|
||||
) : EventPayload
|
||||
|
||||
/** Outcome of the bounded LLM "are you sure?" reflection pass over a [CapabilityGapDetectedEvent]. */
|
||||
enum class CapabilityGapVerdict { RESOLVED, NEEDS_TOOL }
|
||||
|
||||
/**
|
||||
* Vikunja #30 part 2: records the single-pass LLM reflection consulted on a capability gap before
|
||||
* escalating to the operator. RESOLVED means the model self-corrected onto an already-granted
|
||||
* capability ([detail] carries its revised intent, advisory only — never mutates the locked plan).
|
||||
* NEEDS_TOOL means the model insists it genuinely needs [impliedCapability] ([detail] carries its
|
||||
* justification) — this is the only case that should reach the human approval seam; the tool is
|
||||
* never auto-granted (invariants #4/#5).
|
||||
*
|
||||
* The reflection itself is nondeterministic (LLM-backed, invariant #9): recorded here so replay
|
||||
* reads this event back and never re-invokes inference (invariant #8).
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("CapabilityGapReflected")
|
||||
data class CapabilityGapReflectedEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val impliedCapability: String,
|
||||
val verdict: CapabilityGapVerdict,
|
||||
val detail: String,
|
||||
val timestampMs: Long,
|
||||
) : EventPayload
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
||||
import com.correx.core.events.events.PlanCompileCheckedEvent
|
||||
import com.correx.core.events.events.CapabilityGapDetectedEvent
|
||||
import com.correx.core.events.events.CapabilityGapReflectedEvent
|
||||
import com.correx.core.events.events.ReviewFindingsRaisedEvent
|
||||
import com.correx.core.events.events.ExecutionPlanRejectedEvent
|
||||
import com.correx.core.events.events.HealthDegradedEvent
|
||||
@@ -173,6 +174,7 @@ val eventModule = SerializersModule {
|
||||
subclass(ExecutionPlanRejectedEvent::class)
|
||||
subclass(PlanCompileCheckedEvent::class)
|
||||
subclass(CapabilityGapDetectedEvent::class)
|
||||
subclass(CapabilityGapReflectedEvent::class)
|
||||
subclass(ReviewFindingsRaisedEvent::class)
|
||||
subclass(JournalCompactedEvent::class)
|
||||
subclass(HealthDegradedEvent::class)
|
||||
|
||||
Reference in New Issue
Block a user