diff --git a/modules/work/domain/planning.go b/modules/work/domain/planning.go index d4d248e..58719bd 100644 --- a/modules/work/domain/planning.go +++ b/modules/work/domain/planning.go @@ -82,6 +82,7 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath { hasQueryLogsHint := hasPlanningHint(input, capability.CapabilityKindQueryLogs) hasVerifyHint := hasPlanningHint(input, capability.CapabilityKindVerifyFact) rawTextFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition + unsupportedCapabilityFallbackAllowed := len(input.CapabilityHints) == 0 && !input.RequiresClarification && !input.NeedsCognition if input.SpeechAct != work.SpeechActRef("") && input.SpeechAct != work.SpeechActRef("UNKNOWN") { rawTextFallbackAllowed = false } @@ -100,7 +101,7 @@ func DetermineHelpPath(input work.PlanningInput) work.HelpPath { return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindSendEmail}, CapabilityGap: true, Reason: "email sending needs a communication capability implementation and explicit authority", Reply: "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксирую этот capability gap, чтобы позже к нему можно было вернуться."} case hasModifyHint || hasInspectHint || hasQueryLogsHint || hasReadEmailHint || hasRecallHint || hasVerifyHint || needsDurableWorkFromHints(input): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "semantic hints indicate project/code work even if cognition may still be useful upstream"} - case isUnsupportedCapabilityRequest(lower): + case unsupportedCapabilityFallbackAllowed && isUnsupportedCapabilityRequest(lower): return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."} case rawTextFallbackAllowed && requiresDurableWork(lower): return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, or outcome tracking"} @@ -144,7 +145,6 @@ func needsCognitiveAnswer(input work.PlanningInput) bool { return containsAny(lower, "модель", "gemma4", "qwen", "спроси", "обратись", "посчитай") } - func ValidatePlanCandidate(candidate work.PlanCandidate) error { if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 { return ErrInvalidPlanCandidate diff --git a/modules/work/domain/planning_test.go b/modules/work/domain/planning_test.go index 597f569..3370b18 100644 --- a/modules/work/domain/planning_test.go +++ b/modules/work/domain/planning_test.go @@ -109,20 +109,20 @@ func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) { func TestHelpPathStructuredIntentNotOverriddenByRawRussianText(t *testing.T) { path := DetermineHelpPath(work.PlanningInput{ - ConversationID: "conversation-1", - UtteranceID: "utterance-1", - IntentID: "intent-1", - ActorID: identity.ActorID("actor-1"), - PrincipalID: identity.PrincipalID("principal-1"), - ProjectID: work.ProjectRef("upo"), - ProjectDisplayName: "УПО", - ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"), - Message: "Объясни, как добавить поддержку отправки писем в этом проекте.", - SpeechAct: work.SpeechActRef("DIRECTIVE"), - NeedsCognition: false, - CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindModifyCode}, + ConversationID: "conversation-1", + UtteranceID: "utterance-1", + IntentID: "intent-1", + ActorID: identity.ActorID("actor-1"), + PrincipalID: identity.PrincipalID("principal-1"), + ProjectID: work.ProjectRef("upo"), + ProjectDisplayName: "УПО", + ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"), + Message: "Объясни, как добавить поддержку отправки писем в этом проекте.", + SpeechAct: work.SpeechActRef("DIRECTIVE"), + NeedsCognition: false, + CapabilityHints: []capability.CapabilityKind{capability.CapabilityKindModifyCode}, AssertionVerification: epistemics.VerificationNotApplicable, - ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(), + ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(), }) if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableWork || !path.NeedsDurablePlan { t.Fatalf("structured durable intent should be preserved over raw phrase heuristics: %#v", path) @@ -152,6 +152,15 @@ func TestHelpPathStructuredHintsOverrideRawCognitionPhrase(t *testing.T) { } } +func TestHelpPathDoesNotUseUnsupportedCapabilityHeuristicAfterCognitiveDeferral(t *testing.T) { + input := planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable) + input.NeedsCognition = true + path := DetermineHelpPath(input) + if path.Kind == work.HelpPathCapabilityGap { + t.Fatalf("raw unsupported-capability phrase must not override deferred cognitive interpretation: %#v", path) + } +} + func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult { t.Helper() return planForTestWithInput(t, planningInputForTest(message, act, verification))