Files
Assistant/cmd/core-runtime/learning.go

485 lines
22 KiB
Go

package main
import (
"context"
"log"
"strconv"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
selfcontract "git.cin.su/m/Assistant/contracts/self"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
selfdomain "git.cin.su/m/Assistant/modules/self/domain"
"git.cin.su/m/Assistant/platform/ids"
"git.cin.su/m/Assistant/platform/telemetry"
)
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
type learningEpistemicsStore interface {
SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error
SaveExperienceRecord(context.Context, epistemics.ExperienceRecord) error
SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error
}
type learningCapabilityStore interface {
LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error)
SaveSkillProfile(context.Context, capability.SkillProfile) error
}
type learningSelfStore interface {
SaveLearningSignal(context.Context, selfcontract.LearningSignal) error
SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error
SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error
}
type learningRecorder struct {
epistemics learningEpistemicsStore
capability capabilityLearningStore
self learningSelfStore
}
type capabilityLearningStore interface {
learningCapabilityStore
}
func (r learningRecorder) recordSelfSignal(ctx context.Context, signal selfcontract.LearningSignal) {
if r.self == nil {
return
}
if err := signal.Validate(); err != nil {
log.Printf("learning SELF signal invalid: %v", err)
return
}
if err := r.self.SaveLearningSignal(ctx, signal); err != nil {
log.Printf("learning SELF signal unavailable: %v", err)
return
}
diagnosis := selfdomain.AttributeFailure(signal)
need, err := selfdomain.BuildImprovementNeed(signal, diagnosis, string(signal.Kind))
if err != nil {
log.Printf("learning SELF improvement need invalid: %v", err)
return
}
if err := r.self.SaveImprovementNeed(ctx, need); err != nil {
log.Printf("learning SELF improvement need unavailable: %v", err)
return
}
needRef := selfcontract.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(need.ID)}
ledger := selfcontract.LearningLedgerEntry{ID: selfcontract.LearningLedgerEntryID("ledger:observed:" + signal.ID), PrincipalID: signal.PrincipalID, ImprovementNeedRef: &needRef, ObservedProblem: need.ProblemSummary, EvidenceRefs: signal.EvidenceRefs, Diagnosis: string(diagnosis.Class), Hypothesis: diagnosis.Rationale, ProposedChangeRef: signal.SubjectRef, ExpectedImprovement: need.ExpectedBenefit, Decision: "OBSERVED_REQUIRES_GOVERNED_REVIEW", RecordedAt: signal.ObservedAt}
if err := r.self.SaveLearningLedger(ctx, ledger); err != nil {
log.Printf("learning SELF ledger unavailable: %v", err)
}
}
func selfEvidence(owner, kind, stable string) selfcontract.SemanticReference {
return selfcontract.SemanticReference{OwnerNamespace: owner, SemanticKind: kind, StableID: ids.StableID(stable)}
}
type reminderResponseLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
Result continuity.HumanResponseResult
At time.Time
}
type correctionLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
At time.Time
}
type fastPathDeclineLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
Result continuity.HumanResponseResult
Reason capability.FastPathDeclineReason
At time.Time
}
type capabilityLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
ImplementationID capability.CapabilityImplementationID
UsageObservation capability.CapabilityUsageObservationID
GapID capability.CapabilityGapID
Outcome capability.UsageOutcomeClass
OutcomeRef string
At time.Time
}
type delegationLearningInput struct {
Intent interaction.OriginalHumanIntent
Utterance interaction.HumanUtterance
DelegationID string
RunID string
ImplementationID capability.CapabilityImplementationID
OutcomeRef string
VerificationRefs []epistemics.EvidenceRef
Verified bool
ExecutionWindows int
HumanBlocked bool
At time.Time
}
func (r learningRecorder) recordReminderResponse(ctx context.Context, input reminderResponseLearningInput) {
if r.epistemics == nil || r.capability == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.Result.Response.ID == "" {
return
}
expectedEffect := "resolve obvious reminder continuation using deterministic target resolver"
if input.Result.Response.TargetProspectiveID != "" {
expectedEffect += " for prospective item " + string(input.Result.Response.TargetProspectiveID)
}
expectation := epistemics.SystemExpectation{
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ExpectedEffect: expectedEffect,
ConfidenceMillis: 850,
EvidenceBasis: []epistemics.EvidenceRef{"continuity:response-target-resolution"},
RecordedAtUnixMillis: input.At.UnixMilli(),
}
expectation, err := epistemicsdomain.NewSystemExpectation(expectation)
if err != nil {
log.Printf("learning reminder expectation invalid: %v", err)
return
}
if err := r.epistemics.SaveSystemExpectation(ctx, expectation); err != nil {
log.Printf("learning reminder expectation unavailable: %v", err)
return
}
actual, confidence := outcomeForHumanResponse(input.Result.Response)
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceContinuity,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs(
"continuity:human-response:"+string(input.Result.Response.ID),
"continuity:prospective:"+string(input.Result.Response.TargetProspectiveID),
"continuity:attention:"+string(input.Result.Response.TargetAttentionID),
),
HelpMethod: epistemics.HelpMethodDeterministicFastPath,
HelpMethodRef: "continuity:human-response-target-resolver-v1",
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
ExpectationID: expectation.ID,
ExpectedOutcome: expectation.ExpectedOutcome,
ActualOutcome: actual,
OutcomeRef: "continuity:human-response:" + string(input.Result.Response.ID),
OutcomeDetail: string(input.Result.Response.Operation) + " -> " + string(input.Result.Response.Outcome),
HumanResponseRef: epistemics.HumanResponseRef(input.Result.Response.ID),
VerificationEvidence: []epistemics.EvidenceRef{epistemics.EvidenceRef("human-response:" + string(input.Result.Response.Outcome))},
ConfidenceMillis: confidence,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
r.updateReminderSkill(ctx, experience, skillOutcomeForExperience(experience), false)
}
func (r learningRecorder) recordCorrection(ctx context.Context, input correctionLearningInput) {
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
return
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceInteraction,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodClarification,
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: epistemics.OutcomeUserCorrected,
OutcomeDetail: "human correction indicates prior interpretation or target resolution may be wrong",
CorrectionUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
VerificationEvidence: []epistemics.EvidenceRef{"interaction:human-correction"},
ConfidenceMillis: 1000,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:correction:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalUserCorrection,
TaskPattern: "interaction-correction", ScopeKey: "principal:" + string(input.Utterance.PrincipalID),
SubjectRef: selfEvidence("interaction", "intent", string(input.Intent.ID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ObservedAt: input.At.UnixMilli(), Severity: 2, UserImpact: 2,
})
}
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
}
telemetry.RecordCompatibility("fast-path-v1", "core-runtime", "decision", "decline-or-escalate", true, true, false, "OBSERVED", string(input.Utterance.CorrelationID), string(input.Utterance.PrincipalID), input.At)
experience, err := epistemicsdomain.FastPathDeclineExperience(
epistemics.OriginalIntentRef(input.Intent.ID),
epistemics.SourceUtteranceRef(input.Utterance.ID),
epistemics.SkillRef(reminderResponseSkillID),
string(outcomeForDecline(input.Reason)),
input.At.UnixMilli(),
)
if err != nil {
return
}
if input.Result.Response.ID != "" {
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
}
experience.PrincipalID = string(input.Utterance.PrincipalID)
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
}
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:fast-path:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalSkillFallback,
TaskPattern: "fast-path", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "skill", string(reminderResponseSkillID)),
EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ActualOutcome: string(input.Reason), ObservedAt: input.At.UnixMilli(), Severity: 1,
})
}
func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabilityLearningInput) {
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
return
}
actual := epistemics.OutcomeSuccess
switch input.Outcome {
case capability.UsageFailed:
actual = epistemics.OutcomeFailed
case capability.UsageSkipped:
actual = epistemics.OutcomeNotApplicable
case capability.UsageUnknown:
actual = epistemics.OutcomeUnknown
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodCapabilityExecution,
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
CapabilityUsageObservationRef: epistemics.CapabilityUsageObservationRef(input.UsageObservation),
CapabilityGapRef: epistemics.CapabilityGapRef(input.GapID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: actual,
OutcomeRef: input.OutcomeRef,
OutcomeDetail: "capability outcome " + string(input.Outcome),
VerificationEvidence: []epistemics.EvidenceRef{"capability:usage-observation"},
ConfidenceMillis: 900,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
if input.Outcome == capability.UsageFailed || input.Outcome == capability.UsageUnknown {
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:capability:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
TaskPattern: "capability-use", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))},
ActualOutcome: string(input.Outcome), ObservedAt: input.At.UnixMilli(), Severity: 2,
})
}
}
func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) {
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.ImplementationID == "" {
return
}
actual := epistemics.OutcomeSuccess
confidence := int64(950)
if !input.Verified {
actual = epistemics.OutcomeFailed
confidence = 850
}
detailParts := []string{"delegation outcome recorded"}
if input.ExecutionWindows > 1 {
detailParts = append(detailParts, "execution-window-continuations="+strconv.Itoa(input.ExecutionWindows-1))
}
if input.HumanBlocked {
detailParts = append(detailParts, "human-blocker-occurred")
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
HelpMethod: epistemics.HelpMethodCapabilityExecution,
HelpMethodRef: "work:agent-delegation-v1",
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: actual,
OutcomeRef: input.OutcomeRef,
OutcomeDetail: strings.Join(detailParts, "; "),
VerificationEvidence: input.VerificationRefs,
ConfidenceMillis: confidence,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok && input.Verified {
r.createDelegationCandidateSkill(ctx, input, saved)
}
if !input.Verified {
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:delegation:" + input.DelegationID, PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
TaskPattern: "delegation", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", input.OutcomeRef)},
ActualOutcome: string(actual), ObservedAt: input.At.UnixMilli(), Severity: 2,
})
}
}
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
value, err := epistemicsdomain.NewExperienceRecord(value)
if err != nil {
log.Printf("learning experience invalid: %v", err)
return epistemics.ExperienceRecord{}, false
}
if err := r.epistemics.SaveExperienceRecord(ctx, value); err != nil {
log.Printf("learning experience unavailable: %v", err)
return epistemics.ExperienceRecord{}, false
}
if candidate, ok, err := epistemicsdomain.ReflectionForExperience(value); err == nil && ok {
if err := r.epistemics.SaveReflectionCandidate(ctx, candidate); err != nil {
log.Printf("learning reflection unavailable: %v", err)
}
} else if err != nil {
log.Printf("learning reflection invalid: %v", err)
}
return value, true
}
func (r learningRecorder) updateReminderSkill(ctx context.Context, experience epistemics.ExperienceRecord, outcome capability.UsageOutcomeClass, corrected bool) {
if r.capability == nil || experience.ID == "" {
return
}
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
if err != nil {
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
log.Printf("learning skill profile unavailable: %v", err)
return
}
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "read", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
ExperienceRef: capability.SkillEvidenceRef("epistemics:experience:" + string(experience.ID)),
Outcome: outcome,
UserCorrected: corrected,
ObservedAtUnixMillis: experience.RecordedAtUnixMillis,
})
if err != nil {
log.Printf("learning skill observation invalid: %v", err)
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "FAILURE", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
log.Printf("learning skill profile save unavailable: %v", err)
return
}
telemetry.RecordCompatibility("skill-profile-v1", "core-runtime", "write", "fast-path-learning", true, false, true, "SUCCESS", string(experience.OriginalIntentID), string(experience.SourceUtteranceID), time.UnixMilli(experience.RecordedAtUnixMillis))
}
func outcomeForHumanResponse(response continuity.HumanResponse) (epistemics.OutcomeClass, int64) {
if response.ResolutionState != continuity.ResponseResolved {
switch response.ResolutionState {
case continuity.ResponseAmbiguous:
return epistemics.OutcomeConflict, 850
case continuity.ResponseNoTarget:
return epistemics.OutcomeOutOfDistribution, 850
default:
return epistemics.OutcomeUnexpected, 700
}
}
switch response.Outcome {
case continuity.ResponseOutcomeUserReportedComplete, continuity.ResponseOutcomeSnoozed, continuity.ResponseOutcomeCancelled, continuity.ResponseOutcomeDismissed:
return epistemics.OutcomeSuccess, 950
case continuity.ResponseOutcomeClarificationNeeded:
return epistemics.OutcomeConflict, 850
default:
return epistemics.OutcomeUnknown, 500
}
}
func skillOutcomeForExperience(experience epistemics.ExperienceRecord) capability.UsageOutcomeClass {
switch experience.ActualOutcome {
case epistemics.OutcomeSuccess, epistemics.OutcomePartialSuccess:
return capability.UsageSucceeded
case epistemics.OutcomeFailed, epistemics.OutcomeUnexpected, epistemics.OutcomeUserCorrected:
return capability.UsageFailed
case epistemics.OutcomeUnknown:
return capability.UsageUnknown
default:
return capability.UsageSkipped
}
}
func outcomeForDecline(reason capability.FastPathDeclineReason) epistemics.OutcomeClass {
switch reason {
case capability.FastPathLowConfidence:
return epistemics.OutcomeLowConfidence
case capability.FastPathOutOfDistribution:
return epistemics.OutcomeOutOfDistribution
case capability.FastPathConflict:
return epistemics.OutcomeConflict
case capability.FastPathStale:
return epistemics.OutcomeStale
default:
return epistemics.OutcomeNotApplicable
}
}
func contextRefs(values ...string) []epistemics.ContextRef {
refs := make([]epistemics.ContextRef, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || strings.HasSuffix(value, ":") {
continue
}
refs = append(refs, epistemics.ContextRef(value))
}
return refs
}
func looksLikeUnsupportedContinuation(message string, result continuity.HumanResponseResult) bool {
lower := strings.ToLower(strings.TrimSpace(message))
if result.Handled {
return false
}
if result.Response.Operation == continuity.ResponseContinue || result.Response.Operation == continuity.ResponseReject {
return true
}
return strings.Contains(lower, "это") && (strings.Contains(lower, "напомин") || strings.Contains(lower, "перенеси") || strings.Contains(lower, "закрой") || strings.Contains(lower, "передвинь"))
}
func declineReasonForResponse(result continuity.HumanResponseResult) capability.FastPathDeclineReason {
switch result.Response.Operation {
case continuity.ResponseReject:
return capability.FastPathConflict
case continuity.ResponseContinue:
return capability.FastPathNotApplicable
default:
return capability.FastPathOutOfDistribution
}
}