feat(gen2): add experience skill reflex lifecycle
This commit is contained in:
@@ -13,3 +13,13 @@ func TestStrategyUsesRiskAndCostSignals(t *testing.T) {
|
||||
t.Fatalf("%#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnavailableReflexFallsBackThroughSkillToReasoning(t *testing.T) {
|
||||
request := capability.StrategyRequest{PreferLowLatency: true, RequiredQuality: 700}
|
||||
if got := request.SelectEligible(capability.StrategyEligibility{SkillEligible: true}); got.Path != capability.PathSkill {
|
||||
t.Fatalf("expected Skill fallback, got %#v", got)
|
||||
}
|
||||
if got := request.SelectEligible(capability.StrategyEligibility{}); got.Path != capability.PathSmallLocal || !got.Escalate {
|
||||
t.Fatalf("expected reasoning fallback, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,91 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
var ErrInsufficientReflexEvidence = errors.New("insufficient reflex evidence")
|
||||
|
||||
func ValidateReflexRevision(v capability.ReflexRevision) error {
|
||||
if v.ReflexID == "" || v.SourceSkillRevision < 1 || v.Kind == "" || v.ImplementationRef == "" || v.Lifecycle == "" || v.RecordedAt < 1 {
|
||||
return fmt.Errorf("invalid reflex revision")
|
||||
}
|
||||
if v.Lifecycle == capability.ReflexActive && len(v.ValidationEvidenceRefs) == 0 {
|
||||
if (v.Lifecycle == capability.ReflexActive || v.Lifecycle == capability.ReflexValidating) && len(v.ValidationEvidenceRefs) == 0 {
|
||||
return fmt.Errorf("active reflex requires evidence")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CandidateReflex admits only a bounded, deterministic implementation from an
|
||||
// already active Skill with multiple verified results. It never activates it.
|
||||
func CandidateReflex(skill capability.SkillProfile, value capability.ReflexRevision, verifierEvidence capability.SkillEvidenceRef, recordedAt int64) (capability.ReflexRevision, error) {
|
||||
if err := ValidateSkillProfile(skill); err != nil || skill.Status != capability.SkillActive || skill.VerifiedOutcomeCount < 3 || verifierEvidence == "" || recordedAt < 1 {
|
||||
return capability.ReflexRevision{}, ErrInsufficientReflexEvidence
|
||||
}
|
||||
if value.Kind != capability.ReflexDeterministicCode && value.Kind != capability.ReflexRule && value.Kind != capability.ReflexWorkflow && value.Kind != capability.ReflexBoundedAdapter {
|
||||
return capability.ReflexRevision{}, ErrInsufficientReflexEvidence
|
||||
}
|
||||
value.SourceSkillRevision = skill.Revision
|
||||
value.Lifecycle = capability.ReflexCandidate
|
||||
value.ValidationEvidenceRefs = appendEvidence(value.ValidationEvidenceRefs, verifierEvidence)
|
||||
value.LastValidatedAt = 0
|
||||
value.RecordedAt = recordedAt
|
||||
if err := ValidateReflexRevision(value); err != nil {
|
||||
return capability.ReflexRevision{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func ValidateAndActivateReflex(value capability.ReflexRevision, independentEvidence capability.SkillEvidenceRef, mustNotWorsen bool, recordedAt int64) (capability.ReflexRevision, error) {
|
||||
if err := ValidateReflexRevision(value); err != nil || value.Lifecycle != capability.ReflexCandidate || independentEvidence == "" || !mustNotWorsen || recordedAt < 1 {
|
||||
return capability.ReflexRevision{}, ErrInsufficientReflexEvidence
|
||||
}
|
||||
next := value
|
||||
next.Revision++
|
||||
next.Lifecycle = capability.ReflexActive
|
||||
next.ValidationEvidenceRefs = appendEvidence(next.ValidationEvidenceRefs, independentEvidence)
|
||||
next.LastValidatedAt = recordedAt
|
||||
next.RecordedAt = recordedAt
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// ObserveShadow keeps normal execution non-blocking. Divergence marks the
|
||||
// revision stale for governed revalidation; it cannot replace implementation.
|
||||
func ObserveShadow(value capability.ReflexRevision, comparison capability.ShadowComparison) (capability.ReflexRevision, bool, error) {
|
||||
if err := ValidateReflexRevision(value); err != nil || comparison.ReflexID != value.ReflexID || comparison.ReflexRevision != value.Revision || comparison.ObservedAt < 1 {
|
||||
return capability.ReflexRevision{}, false, ErrInsufficientReflexEvidence
|
||||
}
|
||||
next := value
|
||||
next.ExecutionCount++
|
||||
if comparison.Diverged || !comparison.VerifierPassed {
|
||||
next.DivergenceCount++
|
||||
next.Lifecycle = capability.ReflexStale
|
||||
next.RecordedAt = comparison.ObservedAt
|
||||
return next, true, nil
|
||||
}
|
||||
return next, false, nil
|
||||
}
|
||||
|
||||
func RevalidationRequired(value capability.ReflexRevision, dependencyChanged bool, correctionCount int64, now int64) bool {
|
||||
if value.Lifecycle == capability.ReflexRetired || value.Lifecycle == capability.ReflexStale || value.Lifecycle == capability.ReflexDegraded {
|
||||
return true
|
||||
}
|
||||
if dependencyChanged || (value.Revalidation.RevalidateAfterCorrectionCount != nil && correctionCount >= *value.Revalidation.RevalidateAfterCorrectionCount) {
|
||||
return true
|
||||
}
|
||||
return value.Revalidation.MaxAgeMillis != nil && value.LastValidatedAt > 0 && now-value.LastValidatedAt >= *value.Revalidation.MaxAgeMillis
|
||||
}
|
||||
|
||||
func appendEvidence(values []capability.SkillEvidenceRef, value capability.SkillEvidenceRef) []capability.SkillEvidenceRef {
|
||||
for _, existing := range values {
|
||||
if existing == value {
|
||||
return values
|
||||
}
|
||||
}
|
||||
return append(values, value)
|
||||
}
|
||||
func ReflexFallback(d capability.FastPathDecision) string {
|
||||
if d.Decision == capability.FastPathApplied {
|
||||
return "reflex"
|
||||
|
||||
@@ -19,3 +19,19 @@ func TestReflexActiveRequiresEvidenceAndFallbacks(t *testing.T) {
|
||||
t.Fatal("missing escalation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReflexRequiresStableSkillAndShadowDivergenceOnlyRequestsReview(t *testing.T) {
|
||||
skill := capability.SkillProfile{ID: "s", Revision: 4, Name: "bounded", Description: "bounded", Status: capability.SkillActive, ConfidenceMillis: 900, RecordedAtUnixMillis: 1, VerifiedOutcomeCount: 3}
|
||||
candidate, err := domain.CandidateReflex(skill, capability.ReflexRevision{ReflexID: "r", Revision: 1, Kind: capability.ReflexRule, ImplementationRef: "rule", RecordedAt: 1}, "verifier:success", 2)
|
||||
if err != nil || candidate.Lifecycle != capability.ReflexCandidate {
|
||||
t.Fatalf("candidate=%#v err=%v", candidate, err)
|
||||
}
|
||||
active, err := domain.ValidateAndActivateReflex(candidate, "evaluation:independent", true, 3)
|
||||
if err != nil || active.Lifecycle != capability.ReflexActive {
|
||||
t.Fatalf("active=%#v err=%v", active, err)
|
||||
}
|
||||
next, review, err := domain.ObserveShadow(active, capability.ShadowComparison{ReflexID: "r", ReflexRevision: active.Revision, EvidenceRefs: []capability.SkillEvidenceRef{"shadow:1"}, VerifierPassed: false, Diverged: true, ObservedAt: 4})
|
||||
if err != nil || !review || next.Lifecycle != capability.ReflexStale {
|
||||
t.Fatalf("next=%#v review=%t err=%v", next, review, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
var ErrInvalidSkillProfile = errors.New("invalid skill profile")
|
||||
var ErrInvalidFastPathInput = errors.New("invalid fast path input")
|
||||
var ErrInsufficientSkillEvidence = errors.New("insufficient independent skill evidence")
|
||||
|
||||
func ValidateSkillProfile(value capability.SkillProfile) error {
|
||||
if value.ID == "" || value.Revision < 1 || strings.TrimSpace(value.Name) == "" ||
|
||||
@@ -17,13 +18,59 @@ func ValidateSkillProfile(value capability.SkillProfile) error {
|
||||
return ErrInvalidSkillProfile
|
||||
}
|
||||
switch value.Status {
|
||||
case capability.SkillCandidate, capability.SkillEvaluating, capability.SkillValidated, capability.SkillDegraded, capability.SkillSuspended, capability.SkillRetired:
|
||||
case capability.SkillCandidate, capability.SkillEvaluating, capability.SkillValidated, capability.SkillActive, capability.SkillDegraded, capability.SkillSuspended, capability.SkillRetired:
|
||||
default:
|
||||
return ErrInvalidSkillProfile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CandidateFromVerifiedExperience starts a reusable approach but deliberately
|
||||
// does not validate or activate it. Evidence stays in Epistemics by reference.
|
||||
func CandidateFromVerifiedExperience(profile capability.SkillProfile, experience capability.SkillEvidenceRef, verified bool, recordedAt int64) (capability.SkillProfile, error) {
|
||||
if err := ValidateSkillProfile(profile); err != nil || experience == "" || !verified || recordedAt < 1 {
|
||||
return capability.SkillProfile{}, ErrInvalidSkillProfile
|
||||
}
|
||||
next := profile
|
||||
next.Revision++
|
||||
next.Status = capability.SkillCandidate
|
||||
next.SupportingExperienceRefs = appendEvidenceRef(next.SupportingExperienceRefs, experience)
|
||||
next.VerifiedOutcomeCount++
|
||||
next.RecordedAtUnixMillis = recordedAt
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// ValidateSkill requires repeatable verified outcomes plus evidence produced by
|
||||
// an evaluator other than the candidate creator. It never activates a Skill.
|
||||
func ValidateSkill(profile capability.SkillProfile, evaluation capability.SkillEvidenceRef, independent bool, mustNotWorsen bool, recordedAt int64) (capability.SkillProfile, error) {
|
||||
if err := ValidateSkillProfile(profile); err != nil || evaluation == "" || !independent || !mustNotWorsen || recordedAt < 1 {
|
||||
return capability.SkillProfile{}, ErrInsufficientSkillEvidence
|
||||
}
|
||||
if profile.VerifiedOutcomeCount < 2 || len(profile.SupportingExperienceRefs) < 2 {
|
||||
return capability.SkillProfile{}, ErrInsufficientSkillEvidence
|
||||
}
|
||||
next := profile
|
||||
next.Revision++
|
||||
next.Status = capability.SkillValidated
|
||||
next.ValidationEvidenceRefs = appendEvidenceRef(next.ValidationEvidenceRefs, evaluation)
|
||||
next.IndependentEvaluationRefs = appendEvidenceRef(next.IndependentEvaluationRefs, evaluation)
|
||||
next.MustNotWorsenSatisfied = true
|
||||
next.LastValidatedUnixMillis = recordedAt
|
||||
next.RecordedAtUnixMillis = recordedAt
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func ActivateSkill(profile capability.SkillProfile, recordedAt int64) (capability.SkillProfile, error) {
|
||||
if err := ValidateSkillProfile(profile); err != nil || profile.Status != capability.SkillValidated || len(profile.IndependentEvaluationRefs) == 0 || !profile.MustNotWorsenSatisfied || recordedAt < 1 {
|
||||
return capability.SkillProfile{}, ErrInsufficientSkillEvidence
|
||||
}
|
||||
next := profile
|
||||
next.Revision++
|
||||
next.Status = capability.SkillActive
|
||||
next.RecordedAtUnixMillis = recordedAt
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func ObserveSkillOutcome(profile capability.SkillProfile, observation capability.SkillOutcomeObservation) (capability.SkillProfile, error) {
|
||||
if err := ValidateSkillProfile(profile); err != nil {
|
||||
return capability.SkillProfile{}, err
|
||||
@@ -37,10 +84,6 @@ func ObserveSkillOutcome(profile capability.SkillProfile, observation capability
|
||||
switch observation.Outcome {
|
||||
case capability.UsageSucceeded:
|
||||
next.SupportingExperienceRefs = appendEvidenceRef(next.SupportingExperienceRefs, observation.ExperienceRef)
|
||||
if next.Status == capability.SkillCandidate || next.Status == capability.SkillEvaluating {
|
||||
next.Status = capability.SkillValidated
|
||||
}
|
||||
next.LastValidatedUnixMillis = observation.ObservedAtUnixMillis
|
||||
next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis + 25)
|
||||
case capability.UsageFailed:
|
||||
next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef)
|
||||
@@ -57,7 +100,7 @@ func ObserveSkillOutcome(profile capability.SkillProfile, observation capability
|
||||
next.KnownFailureExperienceRefs = appendEvidenceRef(next.KnownFailureExperienceRefs, observation.ExperienceRef)
|
||||
next.ConfidenceMillis = clampConfidence(next.ConfidenceMillis - 200)
|
||||
}
|
||||
if next.Status == capability.SkillValidated && (next.ObservedFailureCount >= 3 || next.ObservedCorrectionCount >= 2 || next.ConfidenceMillis < 500) {
|
||||
if (next.Status == capability.SkillValidated || next.Status == capability.SkillActive) && (next.ObservedFailureCount >= 3 || next.ObservedCorrectionCount >= 2 || next.ConfidenceMillis < 500) {
|
||||
next.Status = capability.SkillDegraded
|
||||
}
|
||||
if next.Status == capability.SkillDegraded && next.ConfidenceMillis < 250 {
|
||||
@@ -80,7 +123,7 @@ func DecideFastPath(profile capability.SkillProfile, supported bool, confidenceM
|
||||
ObservedAtUnixMillis: observedAtUnixMillis,
|
||||
}
|
||||
switch {
|
||||
case profile.Status == capability.SkillSuspended || profile.Status == capability.SkillRetired:
|
||||
case profile.Status == capability.SkillSuspended || profile.Status == capability.SkillRetired || profile.Status == capability.SkillDegraded:
|
||||
return decline(decision, capability.FastPathStale, "skill profile is not eligible for fast-path use"), nil
|
||||
case environmentVersionChanged:
|
||||
return decline(decision, capability.FastPathStale, "environment or implementation version changed since validation"), nil
|
||||
@@ -90,7 +133,7 @@ func DecideFastPath(profile capability.SkillProfile, supported bool, confidenceM
|
||||
return decline(decision, capability.FastPathOutOfDistribution, "input is outside supported semantics"), nil
|
||||
case confidenceMillis < 700 || profile.ConfidenceMillis < 700:
|
||||
return decline(decision, capability.FastPathLowConfidence, "confidence is too low for optimized path"), nil
|
||||
case profile.Status != capability.SkillValidated:
|
||||
case profile.Status != capability.SkillActive:
|
||||
return decline(decision, capability.FastPathNotApplicable, "skill is not validated"), nil
|
||||
default:
|
||||
return decision, nil
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestFastPathDeclinesUnsupportedInputAndEscalatesToThinking(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatedSkillCanApplyFastPathWhenFreshAndConfident(t *testing.T) {
|
||||
func TestActiveSkillCanApplyFastPathWhenFreshAndConfident(t *testing.T) {
|
||||
decision, err := DecideFastPath(skillProfile(), true, 950, false, false, 2000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -58,18 +58,43 @@ func TestUnknownAndSkippedOutcomesDoNotBecomeSkillFailures(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateNeedsIndependentRepeatableValidationBeforeActive(t *testing.T) {
|
||||
profile := skillProfile()
|
||||
profile.Status = capability.SkillCandidate
|
||||
profile.VerifiedOutcomeCount = 1
|
||||
profile.SupportingExperienceRefs = []capability.SkillEvidenceRef{"experience:1"}
|
||||
if _, err := ValidateSkill(profile, "evaluation:independent", true, true, 2000); err == nil {
|
||||
t.Fatal("one success promoted a skill")
|
||||
}
|
||||
profile.VerifiedOutcomeCount = 2
|
||||
profile.SupportingExperienceRefs = append(profile.SupportingExperienceRefs, "experience:2")
|
||||
validated, err := ValidateSkill(profile, "evaluation:independent", true, true, 2000)
|
||||
if err != nil || validated.Status != capability.SkillValidated {
|
||||
t.Fatalf("validation=%#v err=%v", validated, err)
|
||||
}
|
||||
active, err := ActivateSkill(validated, 2100)
|
||||
if err != nil || active.Status != capability.SkillActive {
|
||||
t.Fatalf("activation=%#v err=%v", active, err)
|
||||
}
|
||||
}
|
||||
|
||||
func skillProfile() capability.SkillProfile {
|
||||
return capability.SkillProfile{
|
||||
ID: "skill-obvious-reminder-response-v1",
|
||||
Revision: 1,
|
||||
Name: "Resolve obvious reminder continuation",
|
||||
Description: "Use a recent unambiguous reminder/attention target for complete/snooze/cancel without cognition.",
|
||||
ApplicableContext: []string{"continuity:reminder-response"},
|
||||
ImplementationRef: "continuity:human-response-target-resolver-v1",
|
||||
ImplementationVersion: "1",
|
||||
Status: capability.SkillValidated,
|
||||
ConfidenceMillis: 850,
|
||||
LastValidatedUnixMillis: 1000,
|
||||
RecordedAtUnixMillis: 1000,
|
||||
ID: "skill-obvious-reminder-response-v1",
|
||||
Revision: 1,
|
||||
Name: "Resolve obvious reminder continuation",
|
||||
Description: "Use a recent unambiguous reminder/attention target for complete/snooze/cancel without cognition.",
|
||||
ApplicableContext: []string{"continuity:reminder-response"},
|
||||
ImplementationRef: "continuity:human-response-target-resolver-v1",
|
||||
ImplementationVersion: "1",
|
||||
Status: capability.SkillActive,
|
||||
ConfidenceMillis: 850,
|
||||
LastValidatedUnixMillis: 1000,
|
||||
RecordedAtUnixMillis: 1000,
|
||||
SupportingExperienceRefs: []capability.SkillEvidenceRef{"experience:1", "experience:2"},
|
||||
IndependentEvaluationRefs: []capability.SkillEvidenceRef{"evaluation:independent"},
|
||||
ValidationEvidenceRefs: []capability.SkillEvidenceRef{"evaluation:independent"},
|
||||
VerifiedOutcomeCount: 2,
|
||||
MustNotWorsenSatisfied: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/contracts/self"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
func ValidateCompetence(v self.CompetenceAssessment) error { return v.Validate() }
|
||||
func ValidateLearning(v self.LearningLedgerEntry) error { return v.Validate() }
|
||||
|
||||
// ImprovementForReflexDivergence records a proposed review only. SELF never
|
||||
// promotes, rewrites or trusts a Reflex from this observation.
|
||||
func ImprovementForReflexDivergence(principal string, reflexRef self.SemanticReference, evidence []self.SemanticReference, at int64) (self.ImprovementNeed, self.LearningLedgerEntry, error) {
|
||||
if principal == "" || !reflexRef.Valid() || len(evidence) == 0 || at < 1 {
|
||||
return self.ImprovementNeed{}, self.LearningLedgerEntry{}, fmt.Errorf("reflex improvement lacks evidence")
|
||||
}
|
||||
need := self.ImprovementNeed{ID: self.AssessmentID("improvement:" + string(reflexRef.StableID) + ":" + fmt.Sprint(at)), PrincipalID: principal, SubjectRef: reflexRef, ProblemSummary: "shadow divergence requires governed reflex revalidation", EvidenceRefs: evidence, ExpectedBenefit: "restore verified bounded behavior or retire the reflex", Priority: 1, State: "OPEN", CreatedAt: at}
|
||||
ref := self.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(need.ID)}
|
||||
ledger := self.LearningLedgerEntry{ID: self.LearningLedgerEntryID("ledger:" + string(reflexRef.StableID) + ":" + fmt.Sprint(at)), PrincipalID: principal, ImprovementNeedRef: &ref, ObservedProblem: need.ProblemSummary, EvidenceRefs: evidence, Diagnosis: "bounded shadow comparison diverged or verifier rejected result", Hypothesis: "implementation or applicability no longer matches evidence", ProposedChangeRef: reflexRef, ExpectedImprovement: need.ExpectedBenefit, Decision: "PROPOSE_REVALIDATION", RecordedAt: at}
|
||||
if err := ledger.Validate(); err != nil {
|
||||
return self.ImprovementNeed{}, self.LearningLedgerEntry{}, err
|
||||
}
|
||||
return need, ledger, nil
|
||||
}
|
||||
|
||||
@@ -16,3 +16,12 @@ func TestCompetenceCannotBeSelfDeclaredWithoutEvidence(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShadowDivergenceCreatesProposalNotPromotion(t *testing.T) {
|
||||
reflex := selfcontract.SemanticReference{OwnerNamespace: "capability", SemanticKind: "reflex", StableID: "r"}
|
||||
evidence := []selfcontract.SemanticReference{{OwnerNamespace: "epistemics", SemanticKind: "experience", StableID: "e"}}
|
||||
need, ledger, err := domain.ImprovementForReflexDivergence("p", reflex, evidence, 1)
|
||||
if err != nil || need.State != "OPEN" || ledger.Decision != "PROPOSE_REVALIDATION" {
|
||||
t.Fatalf("need=%#v ledger=%#v err=%v", need, ledger, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user