package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "net/url" "regexp" "strconv" "strings" "time" "git.cin.su/m/Assistant/adapters/postgres" "git.cin.su/m/Assistant/contracts/authority" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/contracts/work" capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain" workdomain "git.cin.su/m/Assistant/modules/work/domain" "git.cin.su/m/Assistant/platform/ids" ) type continuityReminderClient struct { baseURL *url.URL client *http.Client } type continuityResponseClient struct { baseURL *url.URL client *http.Client } type continuityAttentionClient struct { baseURL *url.URL client *http.Client } func newContinuityReminderClient(rawURL string, client *http.Client) (continuityReminderClient, error) { base, err := url.Parse(rawURL) if err != nil || base.Scheme == "" || base.Host == "" { return continuityReminderClient{}, fmt.Errorf("invalid Continuity Runtime URL") } if client == nil { client = http.DefaultClient } return continuityReminderClient{baseURL: base, client: client}, nil } func newContinuityResponseClient(rawURL string, client *http.Client) (continuityResponseClient, error) { base, err := url.Parse(rawURL) if err != nil || base.Scheme == "" || base.Host == "" { return continuityResponseClient{}, fmt.Errorf("invalid Continuity Runtime URL") } if client == nil { client = http.DefaultClient } return continuityResponseClient{baseURL: base, client: client}, nil } func newContinuityAttentionClient(rawURL string, client *http.Client) (continuityAttentionClient, error) { base, err := url.Parse(rawURL) if err != nil || base.Scheme == "" || base.Host == "" { return continuityAttentionClient{}, fmt.Errorf("invalid Continuity Runtime URL") } if client == nil { client = http.DefaultClient } return continuityAttentionClient{baseURL: base, client: client}, nil } func (c continuityReminderClient) Schedule(ctx context.Context, authorization string, request continuity.ScheduleReminderRequest) (continuity.ScheduleReminderResult, error) { target := *c.baseURL target.Path = "/v1/creator/reminders" payload, err := json.Marshal(request) if err != nil { return continuity.ScheduleReminderResult{}, err } upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload)) if err != nil { return continuity.ScheduleReminderResult{}, err } upstream.Header.Set("Content-Type", "application/json") upstream.Header.Set("Authorization", authorization) response, err := c.client.Do(upstream) if err != nil { return continuity.ScheduleReminderResult{}, err } defer response.Body.Close() if response.StatusCode != http.StatusCreated { return continuity.ScheduleReminderResult{}, fmt.Errorf("continuity reminder status %d", response.StatusCode) } var result continuity.ScheduleReminderResult if err := json.NewDecoder(response.Body).Decode(&result); err != nil { return continuity.ScheduleReminderResult{}, err } if result.Item.ID == "" || result.Item.State != continuity.ProspectiveArmed { return continuity.ScheduleReminderResult{}, fmt.Errorf("invalid reminder result") } return result, nil } func (c continuityResponseClient) Respond(ctx context.Context, authorization string, request continuity.HumanResponseRequest) (continuity.HumanResponseResult, error) { target := *c.baseURL target.Path = "/v1/creator/responses" payload, err := json.Marshal(request) if err != nil { return continuity.HumanResponseResult{}, err } upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload)) if err != nil { return continuity.HumanResponseResult{}, err } upstream.Header.Set("Content-Type", "application/json") upstream.Header.Set("Authorization", authorization) response, err := c.client.Do(upstream) if err != nil { return continuity.HumanResponseResult{}, err } defer response.Body.Close() if response.StatusCode != http.StatusCreated { return continuity.HumanResponseResult{}, fmt.Errorf("continuity response status %d", response.StatusCode) } var result continuity.HumanResponseResult if err := json.NewDecoder(response.Body).Decode(&result); err != nil { return continuity.HumanResponseResult{}, err } return result, nil } func (c continuityAttentionClient) Create(ctx context.Context, authorization string, request continuity.CreateAttentionRequest) (continuity.AttentionItem, error) { target := *c.baseURL target.Path = "/v1/creator/attention" payload, err := json.Marshal(request) if err != nil { return continuity.AttentionItem{}, err } upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload)) if err != nil { return continuity.AttentionItem{}, err } upstream.Header.Set("Content-Type", "application/json") upstream.Header.Set("Authorization", authorization) response, err := c.client.Do(upstream) if err != nil { return continuity.AttentionItem{}, err } defer response.Body.Close() if response.StatusCode != http.StatusCreated { return continuity.AttentionItem{}, fmt.Errorf("continuity attention status %d", response.StatusCode) } var result continuity.AttentionItem if err := json.NewDecoder(response.Body).Decode(&result); err != nil { return continuity.AttentionItem{}, err } if result.ID == "" || result.SourceKind == "" { return continuity.AttentionItem{}, fmt.Errorf("invalid attention result") } return result, nil } func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityRepository, kind capability.CapabilityKind, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (capability.CapabilityResolution, error) { requirementID, err := ids.New() if err != nil { return capability.CapabilityResolution{}, err } requirement := capability.CapabilityResolutionRequirement{ RequirementID: capability.RequirementRef(requirementID), CapabilityKind: kind, OwnerPrincipalID: session.PrincipalID, ActorID: session.ActorID, OriginalIntentID: capability.OriginalIntentRef(intent.ID), SourceUtteranceID: capability.SourceUtteranceRef(utterance.ID), RequiredConstraints: []capability.DataConstraint{capability.DataConstraintLocalOnly}, AllowCanary: allowCanaryForChatCapability(kind), AuthorityContextRef: capability.AuthorityPolicyCreatorDirectiveV1, ObservedAtUnixMillis: now.UnixMilli(), } candidates, err := repository.LoadCapabilityImplementations(ctx, kind, session.PrincipalID) if err != nil { return capability.CapabilityResolution{}, err } resolution, err := capabilitydomain.ResolveCapability(requirement, candidates) if errors.Is(err, capabilitydomain.ErrNoCapabilityImplementation) && resolution.Gap != nil { if saveErr := repository.SaveCapabilityGap(ctx, *resolution.Gap); saveErr != nil { return capability.CapabilityResolution{}, saveErr } return resolution, err } return resolution, err } func allowCanaryForChatCapability(kind capability.CapabilityKind) bool { switch kind { case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject: return true default: return false } } type createdDelegation struct { Delegation work.Delegation Run work.AgentRun } func maybeCreateAgentDelegation(ctx context.Context, workRepository *postgres.WorkRepository, capabilityRepository *postgres.CapabilityRepository, session identity.CreatorSession, conversation interaction.Conversation, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, planning work.PlanningResult, now time.Time) (*createdDelegation, capability.CapabilityResolution, error) { requirement, ok := primarySoftwareRequirement(planning.Candidate.Requirements) if !ok { return nil, capability.CapabilityResolution{}, nil } resolution, err := resolveChatCapability(ctx, capabilityRepository, requirement.CapabilityKind, session, intent, utterance, now) if err != nil { return nil, resolution, err } if len(resolution.Eligible) == 0 || resolution.Eligible[0].Implementation.Kind != capability.ImplementationAgent { return nil, resolution, capabilitydomain.ErrNoCapabilityImplementation } started, err := workdomain.StartDelegation(workdomain.DelegationRequest{ Goal: planning.Goal, Plan: planning.Plan, Candidate: planning.Candidate, Requirement: requirement, Implementation: resolution.Eligible[0].Implementation, ConversationID: work.ConversationRef(conversation.ID), OriginalIntentText: intent.Text, ContextSummary: planning.Candidate.Summary, ContextRefs: []string{"interaction:intent:" + string(intent.ID), "interaction:utterance:" + string(utterance.ID), "work:plan:" + string(planning.Plan.ID)}, VerificationSummary: "verify through changed files, relevant tests, and explicit run evidence before work completion", DerivedInstruction: buildSoftwareAgentInstruction(planning, intent, utterance), StartedAt: now, }) if err != nil { return nil, capability.CapabilityResolution{}, err } if err := workRepository.SaveDelegationBundle(ctx, started.Delegation, started.Run); err != nil { return nil, capability.CapabilityResolution{}, err } return &createdDelegation{Delegation: started.Delegation, Run: started.Run}, resolution, nil } func primarySoftwareRequirement(values []work.CapabilityRequirement) (work.CapabilityRequirement, bool) { for _, requirement := range values { switch requirement.CapabilityKind { case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindInspectProject: return requirement, true } } return work.CapabilityRequirement{}, false } func buildSoftwareAgentInstruction(planning work.PlanningResult, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance) string { steps := make([]string, 0, len(planning.Candidate.Steps)) for _, step := range planning.Candidate.Steps { if step.Description != "" { steps = append(steps, step.Description) } } return strings.TrimSpace(strings.Join([]string{ "Human asked: " + strings.TrimSpace(intent.Text), "Original utterance: " + strings.TrimSpace(utterance.Content), "Desired outcome: " + planning.Goal.DesiredOutcome, "Plan summary: " + planning.Candidate.Summary, "Verification: run relevant tests and keep explicit evidence before claiming completion.", "Steps: " + strings.Join(steps, " | "), }, "\n")) } func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) (capability.CapabilityUsageObservationID, error) { id, err := ids.New() if err != nil { return "", err } latency := now.Sub(started).Milliseconds() observation := capability.CapabilityUsageObservation{ ID: capability.CapabilityUsageObservationID(id), ImplementationID: selected.Implementation.ID, ImplementationRevision: selected.Implementation.Revision, CapabilityID: selected.Implementation.CapabilityID, CapabilityKind: selected.Implementation.CapabilityKind, OwnerPrincipalID: selected.Implementation.OwnerPrincipalID, OriginalIntentID: requirement.OriginalIntentID, SourceUtteranceID: requirement.SourceUtteranceID, Outcome: outcome, OutcomeRef: outcomeRef, LatencyMillis: &latency, ObservedAtUnixMillis: now.UnixMilli(), } if err := repository.SaveCapabilityUsageObservation(ctx, observation); err != nil { return "", err } return observation.ID, nil } func buildReminderRequest(message string, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (continuity.ScheduleReminderRequest, error) { trigger, err := parseReminderTime(message, now) if err != nil { return continuity.ScheduleReminderRequest{}, err } summary := reminderSummary(message) if summary == "" { summary = strings.TrimSpace(message) } return continuity.ScheduleReminderRequest{ OwnerPrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"), SourceIntentID: string(intent.ID), SourceUtteranceID: string(utterance.ID), Summary: summary, TriggerAtUnixMillis: trigger.UnixMilli(), RequestedAtUnixMillis: now.UnixMilli(), }, nil } func parseReminderTime(message string, now time.Time) (time.Time, error) { lower := strings.ToLower(message) relative := regexp.MustCompile(`через\s+(\d{1,3})\s*(минуту|минуты|минут|час|часа|часов)`) if match := relative.FindStringSubmatch(lower); len(match) > 0 { value, err := strconv.Atoi(match[1]) if err != nil || value < 1 { return time.Time{}, fmt.Errorf("invalid relative reminder time") } unit := match[2] if strings.HasPrefix(unit, "час") { if value > 72 { return time.Time{}, fmt.Errorf("relative reminder time is too far") } return now.Add(time.Duration(value) * time.Hour), nil } if value > 24*60 { return time.Time{}, fmt.Errorf("relative reminder time is too far") } return now.Add(time.Duration(value) * time.Minute), nil } day := now if strings.Contains(lower, "послезавтра") { day = now.AddDate(0, 0, 2) } else if strings.Contains(lower, "завтра") { day = now.AddDate(0, 0, 1) } hour, minute := 9, 0 re := regexp.MustCompile(`(?:в|на)\s+([0-2]?\d)(?::([0-5]\d))?`) if match := re.FindStringSubmatch(lower); len(match) > 0 { parsedHour, err := strconv.Atoi(match[1]) if err != nil || parsedHour > 23 { return time.Time{}, fmt.Errorf("invalid reminder hour") } hour = parsedHour if match[2] != "" { parsedMinute, err := strconv.Atoi(match[2]) if err != nil { return time.Time{}, fmt.Errorf("invalid reminder minute") } minute = parsedMinute } } trigger := time.Date(day.Year(), day.Month(), day.Day(), hour, minute, 0, 0, now.Location()) if !trigger.After(now) { trigger = trigger.AddDate(0, 0, 1) } return trigger, nil } func reminderSummary(message string) string { clean := strings.TrimSpace(message) lower := strings.ToLower(clean) for _, prefix := range []string{"напомни мне", "напомни"} { if strings.HasPrefix(lower, prefix) { clean = strings.TrimSpace(clean[len(prefix):]) lower = strings.ToLower(clean) break } } for _, word := range []string{"послезавтра", "завтра", "сегодня"} { clean = strings.TrimSpace(strings.ReplaceAll(strings.ToLower(clean), word, "")) } relative := regexp.MustCompile(`(?i)через\s+\d{1,3}\s*(минуту|минуты|минут|час|часа|часов)`) clean = strings.TrimSpace(relative.ReplaceAllString(clean, "")) re := regexp.MustCompile(`(?i)(в|на)\s+[0-2]?\d(?::[0-5]\d)?`) clean = strings.TrimSpace(re.ReplaceAllString(clean, "")) clean = strings.Trim(clean, " .,!?:;") return clean }