diff --git a/cmd/core-runtime/learning.go b/cmd/core-runtime/learning.go index 0acb551..b268f68 100644 --- a/cmd/core-runtime/learning.go +++ b/cmd/core-runtime/learning.go @@ -159,6 +159,33 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction r.saveExperienceAndReflection(ctx, experience) } +func (r learningRecorder) createDelegationCandidateSkill(ctx context.Context, input delegationLearningInput, experience epistemics.ExperienceRecord) { + if r.capability == nil || experience.ID == "" || len(experience.VerificationEvidence) == 0 { + return + } + // One verified result creates only a candidate. Repeated outcome evidence and + // independent validation are required by Capability before activation. + profile := capability.SkillProfile{ + ID: capability.SkillProfileID("skill:delegation:" + string(input.ImplementationID)), + Revision: 1, + Name: "Verified bounded delegation strategy", + Description: "Observed verified delegation outcome; requires repeatability and independent validation.", + ApplicableContext: []string{"work:delegation"}, + ImplementationRef: "work:agent-delegation-v1", + ImplementationVersion: "1", + Status: capability.SkillCandidate, + ConfidenceMillis: 500, + RecordedAtUnixMillis: experience.RecordedAtUnixMillis, + } + next, err := capabilitydomain.CandidateFromVerifiedExperience(profile, capability.SkillEvidenceRef("epistemics:experience:"+string(experience.ID)), true, experience.RecordedAtUnixMillis) + if err != nil { + return + } + if err := r.capability.SaveSkillProfile(ctx, next); err != nil { + log.Printf("learning candidate skill unavailable: %v", err) + } +} + func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) { if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" { return @@ -251,7 +278,10 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del ConfidenceMillis: confidence, RecordedAtUnixMillis: input.At.UnixMilli(), } - r.saveExperienceAndReflection(ctx, experience) + saved, ok := r.saveExperienceAndReflection(ctx, experience) + if ok && input.Verified { + r.createDelegationCandidateSkill(ctx, input, saved) + } } func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) { diff --git a/cmd/core-runtime/learning_test.go b/cmd/core-runtime/learning_test.go index f82418d..aa563a4 100644 --- a/cmd/core-runtime/learning_test.go +++ b/cmd/core-runtime/learning_test.go @@ -28,17 +28,29 @@ func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, episte type learningCapabilityStub struct { loadCalls int saveCalls int + saved []capability.SkillProfile } func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) { s.loadCalls++ return capability.SkillProfile{}, nil } -func (s *learningCapabilityStub) SaveSkillProfile(context.Context, capability.SkillProfile) error { +func (s *learningCapabilityStub) SaveSkillProfile(_ context.Context, value capability.SkillProfile) error { s.saveCalls++ + s.saved = append(s.saved, value) return nil } +func TestVerifiedDelegationCreatesCandidateNotActiveSkill(t *testing.T) { + e := &learningEpistemicsStub{} + c := &learningCapabilityStub{} + r := learningRecorder{epistemics: e, capability: c} + r.recordDelegationOutcome(context.Background(), delegationLearningInput{Intent: interaction.OriginalHumanIntent{ID: "i"}, Utterance: interaction.HumanUtterance{ID: "u"}, DelegationID: "d", RunID: "r", ImplementationID: "impl", OutcomeRef: "outcome", VerificationRefs: []epistemics.EvidenceRef{"verified"}, Verified: true, At: time.UnixMilli(1000)}) + if len(c.saved) != 1 || c.saved[0].Status != capability.SkillCandidate || c.saved[0].VerifiedOutcomeCount != 1 { + t.Fatalf("candidate=%#v", c.saved) + } +} + func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) { epistemicsStore := &learningEpistemicsStub{} capabilityStore := &learningCapabilityStub{}