This commit is contained in:
@@ -101,10 +101,10 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
|
||||
return nil, fmt.Errorf("invalid experience limit")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
|
||||
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
|
||||
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -113,10 +113,44 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
|
||||
var result []epistemics.ExperienceRecord
|
||||
for rows.Next() {
|
||||
var value epistemics.ExperienceRecord
|
||||
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
|
||||
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// RecentExperienceRecordsForPrincipalAndScope is an owner-local Epistemics
|
||||
// read. It intentionally applies Principal and ContextScope in SQL before any
|
||||
// Core-side relevance ranking, so another realm's experience never becomes a
|
||||
// retrieval candidate.
|
||||
func (r *EpistemicsRepository) RecentExperienceRecordsForPrincipalAndScope(ctx context.Context, principalID, scopeRef string, limit int) ([]epistemics.ExperienceRecord, error) {
|
||||
if principalID == "" || limit < 1 || limit > 100 {
|
||||
return nil, fmt.Errorf("invalid scoped experience query")
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
|
||||
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
|
||||
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
|
||||
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
|
||||
FROM epistemics.experience_records
|
||||
WHERE principal_id=$1 AND ($2='' OR context_scope_ref=$2)
|
||||
ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $3`, principalID, scopeRef, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]epistemics.ExperienceRecord, 0)
|
||||
for rows.Next() {
|
||||
var value epistemics.ExperienceRecord
|
||||
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
|
||||
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
|
||||
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
|
||||
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
@@ -205,15 +239,15 @@ func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, i
|
||||
|
||||
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
|
||||
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
experience_id, principal_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
|
||||
capability_implementation_ref, capability_usage_observation_ref, capability_gap_ref, skill_ref, expectation_id,
|
||||
expected_outcome, actual_outcome, outcome_ref, outcome_detail, human_response_ref, correction_utterance_id,
|
||||
verification_evidence, confidence_millis, recorded_at_unix_millis)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
|
||||
verification_evidence, confidence_millis, recorded_at_unix_millis, context_scope_ref, problem_statement, applicability_conditions, why_succeeded_or_failed)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26) ON CONFLICT DO NOTHING`,
|
||||
value.ID, value.PrincipalID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
|
||||
value.CapabilityImplementationRef, value.CapabilityUsageObservationRef, value.CapabilityGapRef, value.SkillRef, value.ExpectationID,
|
||||
value.ExpectedOutcome, value.ActualOutcome, value.OutcomeRef, value.OutcomeDetail, value.HumanResponseRef, value.CorrectionUtteranceID,
|
||||
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis)
|
||||
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis, value.ContextScopeRef, value.ProblemStatement, value.ApplicabilityConditions, value.WhySucceededOrFailed)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
@@ -33,19 +35,41 @@ type cognitiveInterpretationClient struct {
|
||||
// compiler receives only already-authorized, typed fragments and applies its
|
||||
// scope and byte-budget guards before the cognitive gateway sees them.
|
||||
func compileInteractionContext(projection interactiondomain.ContextProjection, focusProjectID string) (string, error) {
|
||||
if len(projection.Fragments) == 0 {
|
||||
return compileInteractionContextWithExperience(projection, focusProjectID, nil, "")
|
||||
}
|
||||
|
||||
func compileInteractionContextWithExperience(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) (string, error) {
|
||||
if len(projection.Fragments) == 0 && len(experiences) == 0 {
|
||||
return projection.Text, nil
|
||||
}
|
||||
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments))
|
||||
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments)+len(experiences))
|
||||
for _, fragment := range projection.Fragments {
|
||||
candidates = append(candidates, cognitive.ContextCandidate{
|
||||
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
|
||||
Relevance: fragment.Relevance,
|
||||
Lane: "interaction",
|
||||
Lane: "current-fact",
|
||||
ContextScopeRef: fragment.ScopeRef,
|
||||
FreshnessState: "CURRENT",
|
||||
})
|
||||
}
|
||||
if needsHistoricalAnalogy(query) {
|
||||
for _, fragment := range projection.Historical {
|
||||
candidates = append(candidates, cognitive.ContextCandidate{
|
||||
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:historical:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
|
||||
Relevance: fragment.Relevance - 1, Lane: "historical-analogy", ContextScopeRef: fragment.ScopeRef, FreshnessState: "HISTORICAL_ONLY",
|
||||
})
|
||||
}
|
||||
}
|
||||
retrieved := epistemicsdomain.RetrieveExperience(experiences, "", focusProjectID, query, 4)
|
||||
for _, record := range retrieved.Positive {
|
||||
candidates = append(candidates, experienceContextCandidate(record, "positive-experience", false))
|
||||
}
|
||||
for _, record := range retrieved.Negative {
|
||||
candidates = append(candidates, experienceContextCandidate(record, "negative-experience", true))
|
||||
}
|
||||
for _, record := range retrieved.Contradictions {
|
||||
candidates = append(candidates, experienceContextCandidate(record, "contradiction", true))
|
||||
}
|
||||
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: candidates, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: 2048, ContextScopeRef: focusProjectID, FreshnessRequirement: "conversation-current", RiskClass: "low"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -57,6 +81,26 @@ func compileInteractionContext(projection interactiondomain.ContextProjection, f
|
||||
return strings.Join(parts, "\n"), nil
|
||||
}
|
||||
|
||||
func needsHistoricalAnalogy(query string) bool {
|
||||
for _, marker := range []string{"истор", "раньше", "предыдущ", "старое", "аналог"} {
|
||||
if strings.Contains(strings.ToLower(query), marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func experienceContextCandidate(record epistemics.ExperienceRecord, lane string, negative bool) cognitive.ContextCandidate {
|
||||
text := strings.TrimSpace(record.ProblemStatement + ": " + record.OutcomeDetail)
|
||||
if detail := strings.TrimSpace(record.WhySucceededOrFailed); detail != "" {
|
||||
text += " (" + detail + ")"
|
||||
}
|
||||
if len(record.ApplicabilityConditions) > 0 {
|
||||
text += " [conditions: " + strings.Join(record.ApplicabilityConditions, ", ") + "]"
|
||||
}
|
||||
return cognitive.ContextCandidate{Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("experience:" + string(record.ID)), SourceKind: "epistemics-experience", SourceID: "epistemics:experience-records", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: text}, Relevance: 1, Lane: lane, ContextScopeRef: record.ContextScopeRef, FreshnessState: "CURRENT", Negative: negative}
|
||||
}
|
||||
|
||||
const (
|
||||
// Household interpretation and response review are intentionally compact
|
||||
// calls. Keeping their output bounded prevents a loaded local model from
|
||||
|
||||
@@ -80,6 +80,18 @@ func TestCompileInteractionContextAppliesTypedScopeBeforeGateway(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileInteractionContextUsesHistoricalOnlyWhenRequested(t *testing.T) {
|
||||
projection := interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{{UtteranceID: "current", Text: "код = NEW", Relevance: 2}}, Historical: []interactiondomain.ContextFragment{{UtteranceID: "old", Text: "код = OLD", Relevance: 1, Historical: true}}}
|
||||
current, err := compileInteractionContextWithExperience(projection, "", nil, "Какой текущий код?")
|
||||
if err != nil || strings.Contains(current, "OLD") {
|
||||
t.Fatalf("current context=%q err=%v", current, err)
|
||||
}
|
||||
historical, err := compileInteractionContextWithExperience(projection, "", nil, "Какой старый код был раньше?")
|
||||
if err != nil || !strings.Contains(historical, "OLD") {
|
||||
t.Fatalf("historical context=%q err=%v", historical, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
|
||||
|
||||
@@ -113,6 +113,7 @@ func (r learningRecorder) recordReminderResponse(ctx context.Context, input remi
|
||||
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(
|
||||
@@ -143,6 +144,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
|
||||
}
|
||||
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,
|
||||
@@ -175,6 +177,7 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
|
||||
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)
|
||||
@@ -196,6 +199,7 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
|
||||
}
|
||||
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,
|
||||
@@ -232,6 +236,7 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
|
||||
}
|
||||
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),
|
||||
|
||||
@@ -595,7 +595,11 @@ func main() {
|
||||
focusProjectID = string(previousFocus.ProjectID)
|
||||
}
|
||||
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
|
||||
if compiledContext, contextErr := compileInteractionContext(gen2Context, focusProjectID); contextErr != nil {
|
||||
experienceRecords, experienceErr := epistemicsRepository.RecentExperienceRecordsForPrincipalAndScope(r.Context(), string(session.PrincipalID), focusProjectID, 16)
|
||||
if experienceErr != nil {
|
||||
log.Printf("epistemics experience context unavailable: %v", experienceErr)
|
||||
}
|
||||
if compiledContext, contextErr := compileInteractionContextWithExperience(gen2Context, focusProjectID, experienceRecords, effectiveTaskMessage); contextErr != nil {
|
||||
log.Printf("interaction context compilation unavailable: %v", contextErr)
|
||||
} else {
|
||||
gen2Context.Text = compiledContext
|
||||
|
||||
@@ -36,6 +36,14 @@ type ContextOmission struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ContextInclusion records the bounded compiler decision without exposing
|
||||
// source text. It is trace-only metadata and is never user-facing output.
|
||||
type ContextInclusion struct {
|
||||
ID ContextExcerptID
|
||||
Lane string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ContextBundle struct {
|
||||
Excerpts []ContextExcerpt
|
||||
Omissions []ContextOmission
|
||||
@@ -45,6 +53,7 @@ type ContextBundle struct {
|
||||
|
||||
type ContextAssemblyTrace struct {
|
||||
Included []ContextExcerptID
|
||||
Inclusions []ContextInclusion
|
||||
Excluded []ContextOmission
|
||||
Coverage string
|
||||
CompilerVersion string
|
||||
@@ -67,14 +76,9 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
|
||||
return ContextBundle{}, ErrInvalidContextBudget
|
||||
}
|
||||
candidates := append([]ContextCandidate(nil), input.Candidates...)
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
if candidates[i].Relevance != candidates[j].Relevance {
|
||||
return candidates[i].Relevance > candidates[j].Relevance
|
||||
}
|
||||
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
|
||||
})
|
||||
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
|
||||
seen := make(map[ContextExcerptID]struct{}, len(candidates))
|
||||
eligible := make([]ContextCandidate, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
excerpt := candidate.Excerpt
|
||||
if _, ok := seen[excerpt.ID]; ok {
|
||||
@@ -86,8 +90,7 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
|
||||
continue
|
||||
}
|
||||
reason := permittedContextReason(excerpt, input.DataPolicy)
|
||||
if reason != "" {
|
||||
if reason := permittedContextReason(excerpt, input.DataPolicy); reason != "" {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
|
||||
continue
|
||||
}
|
||||
@@ -95,14 +98,45 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
|
||||
continue
|
||||
}
|
||||
eligible = append(eligible, candidate)
|
||||
}
|
||||
candidates = eligible
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
if candidates[i].Relevance != candidates[j].Relevance {
|
||||
return candidates[i].Relevance > candidates[j].Relevance
|
||||
}
|
||||
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
|
||||
})
|
||||
included := make(map[ContextExcerptID]struct{}, len(candidates))
|
||||
include := func(candidate ContextCandidate, reason string) bool {
|
||||
excerpt := candidate.Excerpt
|
||||
bytes := len([]byte(excerpt.Text))
|
||||
if bundle.Bytes+bytes > input.MaxBytes {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
|
||||
continue
|
||||
return false
|
||||
}
|
||||
bundle.Excerpts = append(bundle.Excerpts, excerpt)
|
||||
bundle.Bytes += bytes
|
||||
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
|
||||
bundle.Trace.Inclusions = append(bundle.Trace.Inclusions, ContextInclusion{ID: excerpt.ID, Lane: candidate.Lane, Reason: reason})
|
||||
included[excerpt.ID] = struct{}{}
|
||||
return true
|
||||
}
|
||||
// Preserve one candidate from each semantic lane before filling by score so
|
||||
// many similar current facts cannot erase a relevant failure or conflict.
|
||||
for _, lane := range []string{"current-fact", "positive-experience", "negative-experience", "contradiction", "historical-analogy", "interaction"} {
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Lane == lane && include(candidate, "lane_coverage") {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if _, ok := included[candidate.Excerpt.ID]; ok {
|
||||
continue
|
||||
}
|
||||
if !include(candidate, "salience") {
|
||||
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: candidate.Excerpt.ID, Reason: "budget"})
|
||||
}
|
||||
}
|
||||
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
|
||||
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
|
||||
|
||||
@@ -12,3 +12,19 @@ func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
|
||||
t.Fatalf("bundle=%#v err=%v", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextCompilerPreservesNegativeLaneWithinBoundedBudget(t *testing.T) {
|
||||
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 27, DataPolicy: capability.DataPolicyLocalOnly, Candidates: []cognitive.ContextCandidate{
|
||||
{Excerpt: cognitive.ContextExcerpt{ID: "current", Text: "current-value", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 100},
|
||||
{Excerpt: cognitive.ContextExcerpt{ID: "failure", Text: "known-failure", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "negative-experience", Relevance: 1, Negative: true},
|
||||
{Excerpt: cognitive.ContextExcerpt{ID: "crowd", Text: "similar-current", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 99},
|
||||
}})
|
||||
if err != nil || len(b.Excerpts) != 2 || b.Excerpts[0].ID != "current" || b.Excerpts[1].ID != "failure" {
|
||||
t.Fatalf("bundle=%#v err=%v", b, err)
|
||||
}
|
||||
// The negative record fits and is retained before a lower-value crowd of
|
||||
// similar facts can consume the remaining bounded budget.
|
||||
if len(b.Trace.Inclusions) != 2 || b.Trace.Inclusions[0].Reason != "lane_coverage" || b.Trace.Inclusions[1].Reason != "lane_coverage" {
|
||||
t.Fatalf("trace=%#v", b.Trace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,24 @@ type FreshnessAssessment struct {
|
||||
RevalidationAttemptRef string
|
||||
AssessedAtUnixMillis int64
|
||||
}
|
||||
|
||||
// FreshnessRequirement is supplied by the current task/focus owner. It is an
|
||||
// evaluation input, not Epistemics-owned task state, so freshness remains
|
||||
// relative to the decision being made.
|
||||
type FreshnessRequirement struct {
|
||||
Mode string
|
||||
MaximumAgeMillis *int64
|
||||
RequiredVersionRefs []SemanticReference
|
||||
}
|
||||
|
||||
// WithExactVersionBinding is a typed contract helper for callers that need to
|
||||
// attach an exact environment/version applicability constraint without taking
|
||||
// a direct dependency on the World package.
|
||||
func WithExactVersionBinding(value TemporalApplicability, subject, version SemanticReference) TemporalApplicability {
|
||||
value.VersionBindings = append(value.VersionBindings, world.VersionBinding{SubjectRef: subject, VersionRef: version, MatchMode: world.VersionExact})
|
||||
return value
|
||||
}
|
||||
|
||||
type RevalidationRequest struct {
|
||||
ID string
|
||||
SourceKnowledgeRef string
|
||||
|
||||
@@ -102,6 +102,7 @@ type SystemExpectation struct {
|
||||
// state or grants authority.
|
||||
type ExperienceRecord struct {
|
||||
ID ExperienceRecordID
|
||||
PrincipalID string
|
||||
SourceKind ExperienceSourceKind
|
||||
OriginalIntentID OriginalIntentRef
|
||||
SourceUtteranceID SourceUtteranceRef
|
||||
@@ -133,6 +134,15 @@ type ExperienceRecord struct {
|
||||
WhySucceededOrFailed string
|
||||
}
|
||||
|
||||
// ConsolidationCandidate is a rebuildable Epistemics projection. It preserves
|
||||
// source ExperienceRecord references and cannot activate a Skill by itself.
|
||||
type ConsolidationCandidate struct {
|
||||
EpisodeRef string
|
||||
ExperienceRefs []ExperienceRecordID
|
||||
PropositionText string
|
||||
SkillCandidate bool
|
||||
}
|
||||
|
||||
// ReflectionCandidate records that broader reflection may be warranted.
|
||||
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
|
||||
// produce candidates only; it cannot directly rewrite canonical state.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE epistemics_role;
|
||||
|
||||
ALTER TABLE epistemics.experience_records
|
||||
ADD COLUMN IF NOT EXISTS principal_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS context_scope_ref TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS problem_statement TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS applicability_conditions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN IF NOT EXISTS why_succeeded_or_failed TEXT NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS experience_records_principal_scope_recorded_idx
|
||||
ON epistemics.experience_records (principal_id, context_scope_ref, recorded_at_unix_millis DESC);
|
||||
|
||||
RESET ROLE;
|
||||
|
||||
COMMIT;
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json","task_id":"GEN2-COGNITIVE-002","task_revision":3}
|
||||
{"task_file":"dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json","task_id":"GEN2-EVOLVE-001","task_revision":3}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"task_id": "GEN2-EVOLVE-001",
|
||||
"task_revision": 3,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Extend the existing Gen2 retrieval fabric with task-relative freshness, bounded salience, scoped positive/negative experience, contradiction candidates and rebuildable consolidation candidates without creating a Memory owner or retiring V1 compatibility.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-001-ADAPTIVE-RETRIEVAL", "REQ-GEN2-EVOLVE-001-EXPERIENCE", "REQ-GEN2-EVOLVE-001-CONSOLIDATION"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["interaction", "epistemics", "core-runtime", "cognitive-context", "production evidence"],
|
||||
"paths": [
|
||||
"modules/interaction/domain/gen2_context.go",
|
||||
"modules/interaction/domain/gen2_context_test.go",
|
||||
"modules/epistemics/domain/freshness.go",
|
||||
"modules/epistemics/domain/freshness_test.go",
|
||||
"modules/epistemics/domain/experience.go",
|
||||
"modules/epistemics/domain/experience_test.go",
|
||||
"modules/epistemics/domain/adaptive_retrieval.go",
|
||||
"modules/epistemics/domain/adaptive_retrieval_test.go",
|
||||
"modules/epistemics/domain/consolidation.go",
|
||||
"modules/epistemics/domain/consolidation_test.go",
|
||||
"contracts/epistemics/freshness.go",
|
||||
"contracts/epistemics/types.go",
|
||||
"contracts/cognitive/context_compiler.go",
|
||||
"contracts/cognitive/context_compiler_v2_test.go",
|
||||
"adapters/postgres/epistemics_repository.go",
|
||||
"adapters/postgres/interaction_repository.go",
|
||||
"cmd/core-runtime/main.go",
|
||||
"cmd/core-runtime/learning.go",
|
||||
"cmd/core-runtime/cognitive_interpretation.go",
|
||||
"cmd/core-runtime/cognitive_interpretation_test.go",
|
||||
"database/migrations/053_gen2_adaptive_experience_scope.sql",
|
||||
"development/GEN2_MIGRATION_MAP.md",
|
||||
"docs/evidence/**",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", ".gitea/workflows/**", "cmd/supervisor/**", "deploy/**", "GEN2-028", "GEN2-029", "GEN2-030", "PersonalMemory write paths", "Gen3/**"]
|
||||
},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {
|
||||
"consume": ["existing Interaction utterance and ProjectReference projection", "existing Epistemics ExperienceRecord and Freshness contracts", "existing Cognitive Context Compiler V2"],
|
||||
"may_change": ["typed Epistemics retrieval and temporal fields", "typed Context Compiler lane and omission metadata", "no public or cross-owner contract"]
|
||||
},
|
||||
"semantic_owner": "Interaction remains canonical owner of human utterances and ProjectReference; Epistemics remains canonical owner of ExperienceRecord and task-relative freshness evidence; Context Compiler remains a rebuildable cognitive projection; Core only composes existing owner reads through established repository boundaries.",
|
||||
"canonical_state": ["Interaction utterance history and ProjectReference", "Epistemics ExperienceRecord and temporal applicability", "no new Memory canonical state", "no direct cross-owner SQL join or write"],
|
||||
"persistence_and_migrations": ["add only owner-local Epistemics columns for principal and ContextScope references required to retrieve ExperienceRecord without cross-owner joins", "existing rows remain historical and are not deleted", "migration must be additive and rollback-safe", "migration number 053 is assigned because global migrations 051 and 052 already exist on the task base; no existing migration is changed", "cmd/core-runtime/learning.go is included only to persist the existing authenticated principal into the additive Epistemics metadata written by this task"],
|
||||
"side_effects_and_privileges": ["no external mutation", "no provider override", "no autonomous Skill or Reflex activation", "no scheduler", "no V1 retirement", "no browser acceptance required"],
|
||||
"state_transitions": [
|
||||
"task-relative freshness uses observed/verified/validity/volatility/version/provenance inputs and never turns a stale assessment into permanent deletion",
|
||||
"current versus historical assertion resolution preserves historical lineage and evaluates applicability before recency",
|
||||
"ProjectReference and principal filtering precede semantic relevance ranking",
|
||||
"Epistemics positive and negative ExperienceRecord retrieval is separately lane-classified and preserves failure applicability conditions",
|
||||
"bounded contradiction candidates are retrieved after the primary candidates without confirmation-only selection",
|
||||
"Context Compiler receives bounded current-fact, experience, contradiction and historical-analogy lanes with explainable inclusions and omissions",
|
||||
"consolidation is a deterministic rebuildable projection from raw records to episode/proposition/experience/Skill-candidate evidence; it does not write or activate Skills"
|
||||
],
|
||||
"failure_retry_restart": ["bounded retrieval never loops", "missing or old Experience metadata remains historical or excluded rather than treated as current", "rebuild/restart recomputes projections from canonical utterance and Epistemics evidence", "no candidate failure changes stable production traffic"],
|
||||
"assumptions": ["GEN2-COGNITIVE-002 scoped Interaction retrieval remains the primary assertion reader", "Epistemics owns additive ExperienceRecord metadata", "COMPAT observation and V1 retirement remain deferred by owner", "no browser or production promotion is required for this task"],
|
||||
"acceptance_scenarios": [
|
||||
"EVOLVE-001-SCN-01 current versus historical value returns the applicable current value while retaining historical evidence",
|
||||
"EVOLVE-001-SCN-02 fast-aging and slow-aging inputs receive distinct task-relative freshness assessments",
|
||||
"EVOLVE-001-SCN-03 Company A and Company B are hard-isolated through typed scope references before ranking",
|
||||
"EVOLVE-001-SCN-04 relevant memory is selected and irrelevant memory is excluded under a bounded context budget with an explainable omission",
|
||||
"EVOLVE-001-SCN-05 similar work retrieves both successful and failed applicable ExperienceRecord lanes",
|
||||
"EVOLVE-001-SCN-06 contradiction retrieval emits a bounded opposing candidate without replacing canonical evidence",
|
||||
"EVOLVE-001-SCN-07 rebuildable consolidation retains source evidence references and survives repository reload without raw-evidence loss"
|
||||
],
|
||||
"required_tests": [
|
||||
"FAST: targeted go test and go vet for changed interaction, epistemics, cognitive context, core-runtime and postgres packages",
|
||||
"FAST: devctl preflight, check-diff, independent governance, git diff --check",
|
||||
"MILESTONE: changed owners plus migration consistency and focused restart/rebuild retrieval check; no browser or FULL suite"
|
||||
],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -1641,7 +1641,7 @@ Historical migrations are evidence. Gen2 migration starts at the next available
|
||||
|
||||
# 41. New Gen2 schema migration groups
|
||||
|
||||
Exact migration numbers are assigned after current repository HEAD is fixed.
|
||||
Exact migration numbers are assigned after current repository HEAD is fixed. The repository uses one global sequence: `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupy their numbers. GEN2-EVOLVE-001 therefore adds owner-local Epistemics metadata as additive migration `053_gen2_adaptive_experience_scope.sql`; it does not alter either existing migration.
|
||||
|
||||
Recommended groups:
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## Gen2 M6 checkpoint — 2026-08-30
|
||||
|
||||
- `GEN2-EVOLVE-001 = MILESTONE_ACCEPTED`: bounded adaptive retrieval adds task-relative freshness, computed salience, positive/negative/contradiction experience lanes and provenance-preserving consolidation candidates. Evidence: `docs/evidence/gen2-evolve-001-milestone-2026-08-30.md`. No V1 path was retired and UI/browser acceptance remains deferred by owner.
|
||||
|
||||
- `GEN2-028 = COMPLETED_WITH_EVIDENCE`: V1 compatibility inventory is recorded in `docs/evidence/gen2-028-v1-compatibility-inventory-2026-08-30.json`. Current production callers remain for PersonalMemory fallback, ActiveProjectFocus, truth_bundles, ordinary ENV reads, SkillProfile/FastPath; no path was proven `SAFE_TO_RETIRE`.
|
||||
- `GEN2-029 = COMPLETED_WITH_SAFE_NOOP`: no V1 runtime path or historical evidence was deleted or disabled. The decision and rollback point are recorded in `docs/evidence/gen2-029-v1-retirement-decision-2026-08-30.json`; `FULL_RETIREMENT` was not applicable because no irreversible retirement was performed.
|
||||
- `GEN2-030_STATUS = ACCEPTANCE_IN_PROGRESS`: this status is a documentation checkpoint, not a final Gen2 PASS. The final gate must confirm current production and repository evidence; the M6 retirement gap remains that active V1 compatibility callers have not yet been proven unused.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# GEN2-EVOLVE-001 — milestone evidence
|
||||
|
||||
- Task/revision: `GEN2-EVOLVE-001 r3`.
|
||||
- Base: `07f0f6e3807f0c40971a22f7bc31b844c0635857` (`gen2-integration`); task branch: `gen2/gen2-evolve-001`.
|
||||
- Migration numbering: global. Existing `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupied the originally chosen number, so the task-owned additive Epistemics migration was renumbered to `053_gen2_adaptive_experience_scope.sql`. No historical/applied migration was edited.
|
||||
- Canonical ownership: Interaction remains the canonical owner of utterances and typed `ProjectReference`; Epistemics remains the canonical owner of Experience records and temporal evidence; Context Compiler remains rebuildable. No new Memory owner, cross-owner SQL join, canonical writer, privilege, dependency, provider override or V1 retirement was introduced.
|
||||
|
||||
## Milestone checks
|
||||
|
||||
Executed once on 2026-08-30:
|
||||
|
||||
```text
|
||||
go test -count=1 ./modules/interaction/domain ./modules/epistemics/domain ./contracts/cognitive ./contracts/epistemics ./adapters/postgres ./cmd/core-runtime
|
||||
go vet ./modules/interaction/domain ./modules/epistemics/domain ./contracts/cognitive ./contracts/epistemics ./adapters/postgres ./cmd/core-runtime
|
||||
go run ./cmd/devctl preflight -task dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json
|
||||
go run ./cmd/devctl check-diff -task dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json
|
||||
go run ./cmd/devctl/independent
|
||||
go run ./cmd/devctl check-architecture
|
||||
git diff --check
|
||||
```
|
||||
|
||||
All commands passed. The focused migration consistency check confirmed a unique global sequence through `053`, an additive owner-local migration, and required `BEGIN`, `SET ROLE epistemics_role`, `principal_id`, `context_scope_ref`, and `COMMIT` markers.
|
||||
|
||||
## Acceptance covered
|
||||
|
||||
- current versus historical assertion selection preserves historical lineage; an explicit correction precedes timestamp tie-breaking;
|
||||
- task-relative volatility/freshness uses validity, last verification, maximum age and optional version applicability;
|
||||
- Principal and typed `ProjectReference` scope filtering occur before relevance/salience ranking;
|
||||
- positive, negative and contradiction Experience lanes are independently retrieved with applicability conditions retained;
|
||||
- Context Compiler reserves bounded lane coverage, preserves omission/inclusion reasons in an internal trace, and keeps historical analogies opt-in;
|
||||
- consolidation candidates retain Epistemics experience references and are candidates only: no Skill is written or activated;
|
||||
- retrieval projections rebuild from canonical Interaction/Epistemics records; no process-local state is required.
|
||||
|
||||
## Deferred / next gap
|
||||
|
||||
The existing `IterativeRetrievalRequest` contract is present, but no established runtime continuation callback is available in this packet's scope. Iterative retrieval therefore remains a bounded future wiring task; no parallel runtime contract was invented here. Browser/UI acceptance, compatibility observation and V1 retirement remain deferred by owner decision.
|
||||
@@ -0,0 +1,158 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
type ExperienceRetrieval struct {
|
||||
Positive []epistemics.ExperienceRecord
|
||||
Negative []epistemics.ExperienceRecord
|
||||
Contradictions []epistemics.ExperienceRecord
|
||||
}
|
||||
|
||||
// SalienceInput is transient retrieval policy. It deliberately is not stored
|
||||
// as a permanent importance score on canonical evidence.
|
||||
type SalienceInput struct {
|
||||
Relevance int
|
||||
FocusMatch bool
|
||||
GoalOrTaskMatch bool
|
||||
UserIntentStrength int
|
||||
Importance int
|
||||
PreviousUsefulness int
|
||||
CommitmentRelevant bool
|
||||
Freshness epistemics.FreshnessAssessmentState
|
||||
Negative bool
|
||||
ContextCostBytes int
|
||||
RecordedAtUnixMillis int64
|
||||
NowUnixMillis int64
|
||||
}
|
||||
|
||||
// ComputeSalience keeps ranking bounded and explainable. Scope and principal
|
||||
// filtering happen before this function is ever used.
|
||||
func ComputeSalience(input SalienceInput) int {
|
||||
score := input.Relevance*100 + input.Importance + input.PreviousUsefulness + input.UserIntentStrength
|
||||
if input.FocusMatch {
|
||||
score += 80
|
||||
}
|
||||
if input.GoalOrTaskMatch {
|
||||
score += 60
|
||||
}
|
||||
if input.CommitmentRelevant {
|
||||
score += 40
|
||||
}
|
||||
if input.Negative {
|
||||
score += 30
|
||||
}
|
||||
switch input.Freshness {
|
||||
case epistemics.FreshnessAssessmentCurrent:
|
||||
score += 40
|
||||
case epistemics.FreshnessAssessmentAging, epistemics.FreshnessAssessmentAcceptable:
|
||||
score += 10
|
||||
case epistemics.FreshnessAssessmentStale, epistemics.FreshnessAssessmentHistoricalOnly, epistemics.FreshnessAssessmentVersionMismatch:
|
||||
score -= 80
|
||||
}
|
||||
if input.NowUnixMillis > input.RecordedAtUnixMillis && input.NowUnixMillis-input.RecordedAtUnixMillis < 24*60*60*1000 {
|
||||
score += 20
|
||||
}
|
||||
return score - input.ContextCostBytes/32
|
||||
}
|
||||
|
||||
// RetrieveExperience filters principal and scope before text relevance. It is
|
||||
// a derived view over append-only Epistemics records, not a Memory owner.
|
||||
func RetrieveExperience(records []epistemics.ExperienceRecord, principalID, scopeRef, query string, limit int) ExperienceRetrieval {
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
queryTokens := retrievalTokens(query)
|
||||
eligible := make([]epistemics.ExperienceRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
if principalID != "" && record.PrincipalID != principalID {
|
||||
continue
|
||||
}
|
||||
if scopeRef != "" && record.ContextScopeRef != scopeRef {
|
||||
continue
|
||||
}
|
||||
if len(queryTokens) > 0 && overlap(queryTokens, retrievalTokens(record.ProblemStatement+" "+record.OutcomeDetail+" "+record.WhySucceededOrFailed)) == 0 {
|
||||
continue
|
||||
}
|
||||
eligible = append(eligible, record)
|
||||
}
|
||||
sort.SliceStable(eligible, func(i, j int) bool {
|
||||
left := experienceSalience(eligible[i], queryTokens)
|
||||
right := experienceSalience(eligible[j], queryTokens)
|
||||
if left != right {
|
||||
return left > right
|
||||
}
|
||||
return eligible[i].RecordedAtUnixMillis > eligible[j].RecordedAtUnixMillis
|
||||
})
|
||||
result := ExperienceRetrieval{}
|
||||
for _, record := range eligible {
|
||||
if len(result.Positive)+len(result.Negative) >= limit {
|
||||
break
|
||||
}
|
||||
switch record.ActualOutcome {
|
||||
case epistemics.OutcomeSuccess, epistemics.OutcomePartialSuccess:
|
||||
result.Positive = append(result.Positive, record)
|
||||
default:
|
||||
result.Negative = append(result.Negative, record)
|
||||
if record.ActualOutcome == epistemics.OutcomeConflict || record.ActualOutcome == epistemics.OutcomeUserCorrected || record.ActualOutcome == epistemics.OutcomeStale {
|
||||
result.Contradictions = append(result.Contradictions, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func experienceSalience(record epistemics.ExperienceRecord, queryTokens []string) int {
|
||||
text := record.ProblemStatement + " " + record.OutcomeDetail + " " + record.WhySucceededOrFailed + " " + strings.Join(record.ApplicabilityConditions, " ")
|
||||
negative := record.ActualOutcome != epistemics.OutcomeSuccess && record.ActualOutcome != epistemics.OutcomePartialSuccess
|
||||
return ComputeSalience(SalienceInput{
|
||||
Relevance: overlap(queryTokens, retrievalTokens(text)),
|
||||
GoalOrTaskMatch: record.GoalRef != "" || record.TaskRef != "",
|
||||
Importance: int(record.ConfidenceMillis / 10),
|
||||
CommitmentRelevant: record.ExpectationID != "",
|
||||
Negative: negative,
|
||||
ContextCostBytes: len([]byte(text)),
|
||||
RecordedAtUnixMillis: record.RecordedAtUnixMillis,
|
||||
NowUnixMillis: record.RecordedAtUnixMillis,
|
||||
})
|
||||
}
|
||||
|
||||
func BuildConsolidationCandidates(records []epistemics.ExperienceRecord) []epistemics.ConsolidationCandidate {
|
||||
byProblem := map[string][]epistemics.ExperienceRecord{}
|
||||
for _, record := range records {
|
||||
key := strings.TrimSpace(strings.ToLower(record.ProblemStatement))
|
||||
if key != "" {
|
||||
byProblem[key] = append(byProblem[key], record)
|
||||
}
|
||||
}
|
||||
result := make([]epistemics.ConsolidationCandidate, 0, len(byProblem))
|
||||
for problem, grouped := range byProblem {
|
||||
refs := make([]epistemics.ExperienceRecordID, 0, len(grouped))
|
||||
for _, record := range grouped {
|
||||
refs = append(refs, record.ID)
|
||||
}
|
||||
result = append(result, epistemics.ConsolidationCandidate{EpisodeRef: "derived:experience:" + problem, ExperienceRefs: refs, PropositionText: problem, SkillCandidate: len(grouped) > 1})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func retrievalTokens(value string) []string {
|
||||
return strings.FieldsFunc(strings.ToLower(value), func(r rune) bool { return r < '0' || r > 'z' && (r < 'а' || r > 'я') })
|
||||
}
|
||||
func overlap(left, right []string) int {
|
||||
seen := map[string]bool{}
|
||||
for _, v := range left {
|
||||
seen[v] = true
|
||||
}
|
||||
n := 0
|
||||
for _, v := range right {
|
||||
if seen[v] {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRetrieveExperienceFiltersScopeBeforeRelevanceAndKeepsFailures(t *testing.T) {
|
||||
records := []epistemics.ExperienceRecord{
|
||||
{ID: "success-a", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeSuccess, RecordedAtUnixMillis: 2},
|
||||
{ID: "failed-a", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeFailed, OutcomeDetail: "old connector failed", RecordedAtUnixMillis: 1},
|
||||
{ID: "success-b", PrincipalID: "p", ContextScopeRef: "company-b", ProblemStatement: "sync catalog", ActualOutcome: epistemics.OutcomeSuccess, RecordedAtUnixMillis: 3},
|
||||
}
|
||||
got := RetrieveExperience(records, "p", "company-a", "sync catalog", 4)
|
||||
if len(got.Positive) != 1 || got.Positive[0].ID != "success-a" || len(got.Negative) != 1 || got.Negative[0].ID != "failed-a" {
|
||||
t.Fatalf("retrieval=%#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsolidationPreservesEvidenceWithoutSkillActivation(t *testing.T) {
|
||||
got := BuildConsolidationCandidates([]epistemics.ExperienceRecord{{ID: "a", ProblemStatement: "sync catalog"}, {ID: "b", ProblemStatement: "sync catalog"}})
|
||||
if len(got) != 1 || len(got[0].ExperienceRefs) != 2 || !got[0].SkillCandidate {
|
||||
t.Fatalf("candidates=%#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSalienceIsComputedAndDoesNotPersistImportance(t *testing.T) {
|
||||
current := ComputeSalience(SalienceInput{Relevance: 1, FocusMatch: true, Freshness: epistemics.FreshnessAssessmentCurrent, ContextCostBytes: 32})
|
||||
stale := ComputeSalience(SalienceInput{Relevance: 1, Freshness: epistemics.FreshnessAssessmentStale, ContextCostBytes: 32})
|
||||
if current <= stale {
|
||||
t.Fatalf("current salience=%d stale salience=%d", current, stale)
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,27 @@ import "git.cin.su/m/Assistant/contracts/epistemics"
|
||||
// AssessFreshness receives the Work-owned requirement as primitive policy
|
||||
// inputs, keeping the Epistemics owner independent of Work package types.
|
||||
func AssessFreshness(applicability epistemics.TemporalApplicability, mode string, maximumAgeMillis *int64, nowMillis int64) epistemics.FreshnessAssessment {
|
||||
age := nowMillis - applicability.ObservedAtUnixMillis
|
||||
return AssessFreshnessForRequirement(applicability, epistemics.FreshnessRequirement{Mode: mode, MaximumAgeMillis: maximumAgeMillis}, nowMillis)
|
||||
}
|
||||
|
||||
// AssessFreshnessForRequirement evaluates evidence for the task at hand. It
|
||||
// never writes a permanent stale marker: the same evidence may be current for
|
||||
// one task and historical-only for another.
|
||||
func AssessFreshnessForRequirement(applicability epistemics.TemporalApplicability, requirement epistemics.FreshnessRequirement, nowMillis int64) epistemics.FreshnessAssessment {
|
||||
basis := applicability.ObservedAtUnixMillis
|
||||
if applicability.LastVerifiedAtUnixMillis != nil && *applicability.LastVerifiedAtUnixMillis > basis {
|
||||
basis = *applicability.LastVerifiedAtUnixMillis
|
||||
}
|
||||
age := nowMillis - basis
|
||||
if age < 0 {
|
||||
age = 0
|
||||
}
|
||||
state := epistemics.FreshnessAssessmentCurrent
|
||||
reason := "observation is within applicability window"
|
||||
reason := "evidence is within task applicability window"
|
||||
versionState := "NOT_REQUIRED"
|
||||
if applicability.ValidFromUnixMillis != nil && nowMillis < *applicability.ValidFromUnixMillis {
|
||||
state, reason = epistemics.FreshnessAssessmentHistoricalOnly, "valid-from has not started"
|
||||
}
|
||||
if applicability.ValidUntilUnixMillis != nil && nowMillis > *applicability.ValidUntilUnixMillis {
|
||||
state, reason = epistemics.FreshnessAssessmentStale, "valid-until elapsed"
|
||||
}
|
||||
@@ -20,11 +35,38 @@ func AssessFreshness(applicability epistemics.TemporalApplicability, mode string
|
||||
if applicability.Volatility.RevalidateAfterMillis != nil && age > *applicability.Volatility.RevalidateAfterMillis && state == epistemics.FreshnessAssessmentCurrent {
|
||||
state, reason = epistemics.FreshnessAssessmentAging, "revalidation window elapsed"
|
||||
}
|
||||
if maximumAgeMillis != nil && age > *maximumAgeMillis {
|
||||
if requirement.MaximumAgeMillis != nil && age > *requirement.MaximumAgeMillis {
|
||||
state, reason = epistemics.FreshnessAssessmentStale, "task maximum age exceeded"
|
||||
}
|
||||
if mode == "LIVE_REQUIRED" {
|
||||
if len(requirement.RequiredVersionRefs) > 0 {
|
||||
versionState = versionMatchState(applicability, requirement.RequiredVersionRefs)
|
||||
if versionState == "MISMATCH" {
|
||||
state, reason = epistemics.FreshnessAssessmentVersionMismatch, "task version does not match evidence applicability"
|
||||
} else if versionState == "UNKNOWN" && state == epistemics.FreshnessAssessmentCurrent {
|
||||
state, reason = epistemics.FreshnessAssessmentHistoricalOnly, "task version applicability is unknown"
|
||||
}
|
||||
}
|
||||
if requirement.Mode == "LIVE_REQUIRED" {
|
||||
state, reason = epistemics.FreshnessAssessmentHistoricalOnly, "live source required"
|
||||
}
|
||||
return epistemics.FreshnessAssessment{SourceKnowledgeRef: "knowledge", TaskRef: "task", State: state, Reason: reason, AgeMillis: age, AssessedAtUnixMillis: nowMillis}
|
||||
return epistemics.FreshnessAssessment{SourceKnowledgeRef: "knowledge", TaskRef: "task", State: state, Reason: reason, AgeMillis: age, VersionMatchState: versionState, AssessedAtUnixMillis: nowMillis}
|
||||
}
|
||||
|
||||
func versionMatchState(applicability epistemics.TemporalApplicability, required []epistemics.SemanticReference) string {
|
||||
if len(applicability.VersionBindings) == 0 {
|
||||
return "UNKNOWN"
|
||||
}
|
||||
for _, target := range required {
|
||||
matched := false
|
||||
for _, binding := range applicability.VersionBindings {
|
||||
if binding.VersionRef == target {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return "MISMATCH"
|
||||
}
|
||||
}
|
||||
return "MATCH"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package domain_test
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
"git.cin.su/m/Assistant/modules/epistemics/domain"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -15,3 +16,23 @@ func TestFreshnessIsTaskRelative(t *testing.T) {
|
||||
t.Fatalf("got %v and %v", current.State, historical.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreshnessUsesVerificationAndVersionForTheCurrentTask(t *testing.T) {
|
||||
verified := int64(90)
|
||||
fast := int64(20)
|
||||
slow := int64(500)
|
||||
fastEvidence := epistemics.TemporalApplicability{ObservedAtUnixMillis: 1, LastVerifiedAtUnixMillis: &verified, Volatility: epistemics.VolatilitySpec{Mode: epistemics.VolatilityCustom, HardStaleAfterMillis: &fast}}
|
||||
server := ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "server", StableID: "server-1"}
|
||||
v2 := ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "version", StableID: "v2"}
|
||||
v3 := ids.SemanticRef{OwnerNamespace: "world", SemanticKind: "version", StableID: "v3"}
|
||||
slowEvidence := epistemics.WithExactVersionBinding(epistemics.TemporalApplicability{ObservedAtUnixMillis: 1, LastVerifiedAtUnixMillis: &verified, Volatility: epistemics.VolatilitySpec{Mode: epistemics.VolatilityCustom, HardStaleAfterMillis: &slow}}, server, v2)
|
||||
if got := domain.AssessFreshnessForRequirement(fastEvidence, epistemics.FreshnessRequirement{}, 120); got.State != epistemics.FreshnessAssessmentStale {
|
||||
t.Fatalf("fast-changing evidence=%#v", got)
|
||||
}
|
||||
if got := domain.AssessFreshnessForRequirement(slowEvidence, epistemics.FreshnessRequirement{RequiredVersionRefs: []epistemics.SemanticReference{v2}}, 120); got.State != epistemics.FreshnessAssessmentCurrent || got.VersionMatchState != "MATCH" {
|
||||
t.Fatalf("slow version-matched evidence=%#v", got)
|
||||
}
|
||||
if got := domain.AssessFreshnessForRequirement(slowEvidence, epistemics.FreshnessRequirement{RequiredVersionRefs: []epistemics.SemanticReference{v3}}, 120); got.State != epistemics.FreshnessAssessmentVersionMismatch {
|
||||
t.Fatalf("version mismatch=%#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ type assertionCandidate struct {
|
||||
fragment ContextFragment
|
||||
subject string
|
||||
observed int64
|
||||
corrects interaction.HumanUtteranceID
|
||||
}
|
||||
|
||||
func explicitAssertionCandidates(history []interaction.HumanUtterance, projectRefs map[interaction.HumanUtteranceID][]interaction.ProjectReference, focusProjectID, query string) []assertionCandidate {
|
||||
@@ -111,6 +112,7 @@ func explicitAssertionCandidates(history []interaction.HumanUtterance, projectRe
|
||||
fragment: ContextFragment{UtteranceID: utterance.ID, Text: subject + " = " + value, ScopeRef: scopeRef, Relevance: relevance},
|
||||
subject: normalizedContextSubject(subject),
|
||||
observed: utterance.ReceivedAtUnixMillis,
|
||||
corrects: utterance.CorrectsUtteranceID,
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -120,7 +122,7 @@ func selectCurrentAssertions(candidates []assertionCandidate) ([]ContextFragment
|
||||
latest := make(map[string]assertionCandidate)
|
||||
for _, candidate := range candidates {
|
||||
current, exists := latest[candidate.subject]
|
||||
if !exists || candidate.observed > current.observed || candidate.observed == current.observed && candidate.fragment.UtteranceID > current.fragment.UtteranceID {
|
||||
if !exists || supersedesAssertion(candidate, current) {
|
||||
latest[candidate.subject] = candidate
|
||||
}
|
||||
}
|
||||
@@ -144,6 +146,19 @@ func selectCurrentAssertions(candidates []assertionCandidate) ([]ContextFragment
|
||||
return current, historical
|
||||
}
|
||||
|
||||
// supersedesAssertion first respects an explicit correction relation, then
|
||||
// uses observation time only as a deterministic tie-breaker. Applicability is
|
||||
// already established by principal and typed Scope filtering before this step.
|
||||
func supersedesAssertion(candidate, current assertionCandidate) bool {
|
||||
if candidate.corrects == current.fragment.UtteranceID {
|
||||
return true
|
||||
}
|
||||
if current.corrects == candidate.fragment.UtteranceID {
|
||||
return false
|
||||
}
|
||||
return candidate.observed > current.observed || candidate.observed == current.observed && candidate.fragment.UtteranceID > current.fragment.UtteranceID
|
||||
}
|
||||
|
||||
func contextProjectionFromAssertions(current, historical []ContextFragment) ContextProjection {
|
||||
selected := make([]ContextFragment, 0, len(current))
|
||||
text := make([]string, 0, len(current))
|
||||
|
||||
@@ -60,6 +60,18 @@ func TestGen2ContextKeepsHistoricalAssertionOutOfCurrentProjection(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGen2ContextCorrectionSupersedesBeforeTimestampTieBreaker(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{ID: "observed-later", PrincipalID: "principal-1", Content: "контрольный код = LATER", ReceivedAtUnixMillis: 9},
|
||||
{ID: "original", PrincipalID: "principal-1", Content: "контрольный код = ORIGINAL", ReceivedAtUnixMillis: 1},
|
||||
{ID: "correction", PrincipalID: "principal-1", Content: "контрольный код = CORRECTED", ReceivedAtUnixMillis: 2, CorrectsUtteranceID: "observed-later"},
|
||||
}
|
||||
got := domain.BuildScopedGen2ContextForQuery(history, nil, "principal-1", "", "Какой контрольный код?")
|
||||
if !strings.Contains(got.Text, "CORRECTED") || strings.Contains(got.Text, "LATER") {
|
||||
t.Fatalf("explicit correction did not supersede: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGen2ContextHardFiltersProjectReferencesBeforeRanking(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{
|
||||
{ID: "a", PrincipalID: "principal-1", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1},
|
||||
|
||||
Reference in New Issue
Block a user