Files
Assistant/modules/work/domain/planning.go
m 9a84ac942c
CI / test (push) Successful in 15s
CI / test (pull_request) Successful in 14s
DEV-000064 capability resolution foundation
2026-08-17 15:51:25 +03:00

260 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package domain
import (
"errors"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/platform/ids"
)
var ErrInvalidPlanningInput = errors.New("invalid planning input")
var ErrInvalidPlanCandidate = errors.New("invalid plan candidate")
func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.PlanningResult, error) {
if input.UtteranceID == "" || input.IntentID == "" || input.ActorID == "" || input.PrincipalID == "" || strings.TrimSpace(input.Message) == "" {
return work.PlanningResult{}, ErrInvalidPlanningInput
}
millis := now.UnixMilli()
goalID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
candidateID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
planID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
goal := work.Goal{
ID: work.GoalID(goalID),
SourceIntentID: input.IntentID,
SourceUtteranceID: input.UtteranceID,
ActorID: input.ActorID,
PrincipalID: input.PrincipalID,
ScopeID: input.ScopeID,
ProjectID: input.ProjectID,
ProjectDisplayName: input.ProjectDisplayName,
DesiredOutcome: desiredOutcome(input),
Constraints: "planning-only; no execution authority; no external mutation",
State: work.GoalReady,
Provenance: input.ProjectProvenance,
Revision: 1,
CreatedAtUnixMillis: millis,
UpdatedAtUnixMillis: millis,
}
candidate := work.PlanCandidate{
ID: work.PlanCandidateID(candidateID),
GoalID: goal.ID,
Source: "deterministic-planner-v1",
State: work.PlanCandidateAccepted,
Summary: planSummary(input),
ValidationSummary: "validated: structured deterministic candidate; executor-neutral; authority not granted",
RequiresClarification: false,
AcceptedAtUnixMillis: millis,
RecordedAtUnixMillis: millis,
}
addScenarioPlan(input, millis, &candidate)
if hasRiskyUnverifiedAssertion(input) {
addRequirement(&candidate, capability.CapabilityKindVerifyFact, "verify the user-provided resource assertion before risky work", epistemics.VerificationUnverified, millis)
addStep(&candidate, work.PlanStepVerify, "Verify the reported server free-space/capacity using trusted observations before any large model installation.", len(candidate.Steps)+1, millis)
}
if err := ValidatePlanCandidate(candidate); err != nil {
return work.PlanningResult{}, err
}
plan := work.WorkPlan{ID: work.WorkPlanID(planID), GoalID: goal.ID, AcceptedCandidateID: candidate.ID, Revision: 1, State: candidate.State, Summary: candidate.Summary, RecordedAtUnixMillis: millis}
return work.PlanningResult{Goal: goal, Candidate: candidate, Plan: plan, Reply: planningReply(goal, candidate)}, nil
}
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "объясни", "расскажи", "что делает"):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
case containsAny(lower, "напомни", "напомин"):
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
case containsAny(lower, "отправь", "письм", "почт"):
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 containsAny(lower, "не умеешь", "подключи интеграцию", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер"):
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 requiresDurableWork(lower):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
default:
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
}
}
func ValidatePlanCandidate(candidate work.PlanCandidate) error {
if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 {
return ErrInvalidPlanCandidate
}
switch candidate.State {
case work.PlanCandidateProposed, work.PlanCandidateValidated, work.PlanCandidateAccepted, work.PlanCandidateRejected:
default:
return ErrInvalidPlanCandidate
}
ordinals := map[int]bool{}
for _, step := range candidate.Steps {
if step.ID == "" || step.PlanCandidateID != candidate.ID || step.Ordinal < 1 || step.Kind == "" || step.Description == "" || ordinals[step.Ordinal] {
return ErrInvalidPlanCandidate
}
ordinals[step.Ordinal] = true
}
seenRequirements := map[work.CapabilityRequirementID]bool{}
for _, requirement := range candidate.Requirements {
if requirement.ID == "" || requirement.PlanCandidateID != candidate.ID || requirement.CapabilityKind == "" || requirement.Purpose == "" || requirement.State == "" || requirement.EpistemicPrecondition == "" {
return ErrInvalidPlanCandidate
}
seenRequirements[requirement.ID] = true
}
for _, delegation := range candidate.Delegations {
if delegation.ID == "" || delegation.PlanCandidateID != candidate.ID || delegation.CapabilityRequirementID == "" || delegation.Kind == "" || delegation.State == "" || !seenRequirements[delegation.CapabilityRequirementID] {
return ErrInvalidPlanCandidate
}
}
return nil
}
func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"):
addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis)
addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis)
addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis)
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"):
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis)
addStep(candidate, work.PlanStepModify, "Prepare a code-change work item for a capable coding executor.", 2, millis)
addStep(candidate, work.PlanStepRunChecks, "Run relevant tests and architecture checks before outcome claim.", 3, millis)
case containsAny(lower, "письм", "почт"):
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
if containsAny(lower, "ответ", "ответить", "скажи") {
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
}
if containsAny(lower, "отправь", "напиши") {
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
}
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
case containsAny(lower, "напомни", "завтра", "напомин"):
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
case containsAny(lower, "отправь", "ивану", "сообщи"):
addRequirement(candidate, capability.CapabilityKindSendEmail, "communicate with the named recipient after channel/authority resolution", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
default:
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "clarify/decompose ambiguous human intent", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepClarify, "Clarify desired outcome and required capabilities.", 1, millis)
candidate.RequiresClarification = true
}
for _, requirement := range candidate.Requirements {
addDelegation(candidate, requirement, millis)
}
}
func requiresDurableWork(lower string) bool {
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "письм", "почт", "отправь", "ивану", "сообщи", "установи", "большую модель")
}
func addRequirement(candidate *work.PlanCandidate, kind capability.CapabilityKind, purpose string, precondition epistemics.VerificationState, millis int64) {
id, err := ids.New()
if err != nil {
return
}
candidate.Requirements = append(candidate.Requirements, work.CapabilityRequirement{ID: work.CapabilityRequirementID(id), PlanCandidateID: candidate.ID, CapabilityKind: kind, Purpose: purpose, State: work.CapabilityRequirementMissing, EpistemicPrecondition: precondition, RecordedAtUnixMillis: millis})
}
func addStep(candidate *work.PlanCandidate, kind work.PlanStepKind, description string, ordinal int, millis int64) {
id, err := ids.New()
if err != nil {
return
}
var requirementID work.CapabilityRequirementID
if len(candidate.Requirements) > 0 {
requirementID = candidate.Requirements[len(candidate.Requirements)-1].ID
}
candidate.Steps = append(candidate.Steps, work.PlanStep{ID: work.PlanStepID(id), PlanCandidateID: candidate.ID, Ordinal: ordinal, Kind: kind, Description: description, RequiresCapabilityID: requirementID, RecordedAtUnixMillis: millis})
}
func addDelegation(candidate *work.PlanCandidate, requirement work.CapabilityRequirement, millis int64) {
id, err := ids.New()
if err != nil {
return
}
candidate.Delegations = append(candidate.Delegations, work.DelegationCandidate{ID: work.DelegationCandidateID(id), PlanCandidateID: candidate.ID, CapabilityRequirementID: requirement.ID, Kind: delegationKind(requirement.CapabilityKind), ExecutorHint: "", State: work.DelegationCandidateBlocked, Reason: "no concrete executor selected in DEV-000063 planning-only mode", RecordedAtUnixMillis: millis})
}
func delegationKind(kind capability.CapabilityKind) work.DelegationKind {
switch kind {
case capability.CapabilityKindModifyCode, capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindWebResearch:
return work.DelegationKindAgent
case capability.CapabilityKindReadEmail, capability.CapabilityKindSendEmail, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindScheduleReminder, capability.CapabilityKindInspectProject, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact:
return work.DelegationKindAdapter
default:
return work.DelegationKindUnavailable
}
}
func desiredOutcome(input work.PlanningInput) string {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "тормозит", "производительн"):
return "identify likely performance cause with sufficient evidence"
case containsAny(lower, "письм", "почт"):
return "review communications and identify what needs response"
case containsAny(lower, "напомни", "напомин"):
return "create a future reminder when scheduling capability is available"
case containsAny(lower, "отправь", "сообщи"):
return "communicate requested information to the recipient after authority"
case containsAny(lower, "добавь", "поддержк", "возможность"):
return "software project gains the requested capability after validated implementation"
default:
return strings.TrimSpace(input.Message)
}
}
func planSummary(input work.PlanningInput) string {
return "Planning foundation for: " + desiredOutcome(input)
}
func planningReply(goal work.Goal, candidate work.PlanCandidate) string {
requirements := make([]string, 0, len(candidate.Requirements))
for _, requirement := range candidate.Requirements {
requirements = append(requirements, string(requirement.CapabilityKind))
}
project := goal.ProjectDisplayName
if project == "" {
project = "текущий контекст"
}
return "Понял цель: " + goal.DesiredOutcome + ". Контекст: " + project + ". План сохранён в безопасном режиме: нужны capability " + strings.Join(requirements, ", ") + ". Исполнение этих инструментов пока не выполнено и требует отдельной capability/authority."
}
func hasRiskyUnverifiedAssertion(input work.PlanningInput) bool {
lower := strings.ToLower(input.Message)
return input.AssertionVerification == epistemics.VerificationUnverified && containsAny(lower, "300 гб", "свободно", "большую модель", "установи", "модель")
}
func containsAny(value string, needles ...string) bool {
for _, needle := range needles {
if strings.Contains(value, needle) {
return true
}
}
return false
}