Files
Assistant/cmd/core-runtime/main.go
T

2634 lines
120 KiB
Go
Raw 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 main
import (
"context"
"encoding/json"
"errors"
"fmt"
"git.cin.su/m/Assistant/adapters/developmentexecutor"
"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/cognitive"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/contracts/situation"
"git.cin.su/m/Assistant/contracts/work"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
identitydomain "git.cin.su/m/Assistant/modules/identity/domain"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
situationdomain "git.cin.su/m/Assistant/modules/situation/domain"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/platform/ids"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/platform/telemetry"
"git.cin.su/m/Assistant/runtime/health"
"github.com/jackc/pgx/v5"
"io"
"log"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
const (
creatorPasswordPath = "/run/secrets/creator_password"
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
selfDBPasswordPath = "/run/secrets/self_db_password"
)
const maxCreatorCredentialsBytes = 4 << 10
func main() {
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8080"
}
bootstrapToken, err := readRequiredSecretFile("CREATOR_BOOTSTRAP_TOKEN", creatorBootstrapTokenPath)
if err != nil {
log.Fatal(err)
}
sessionKeyValue, err := readRequiredSecretFile("CREATOR_SESSION_SIGNING_KEY", creatorSessionKeyPath)
if err != nil {
log.Fatal(err)
}
sessionKey := []byte(sessionKeyValue)
creatorUsername := os.Getenv("CREATOR_USERNAME")
if creatorUsername == "" {
log.Fatal("CREATOR_USERNAME is required")
}
telemetry.RecordCompatibility("non-bootstrap-env-consumers", "core-runtime", "read", "runtime-startup-configuration", true, true, true, "OBSERVED", "startup", "core-runtime", time.Now())
telemetry.RecordCompatibility("migration-compatibility-switches", "core-runtime", "observe", "runtime-startup-switch-scan", true, false, false, "NOT_OBSERVED", "startup", "core-runtime", time.Now())
repository, err := postgres.OpenSituation(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD")})
if err != nil {
log.Fatal(err)
}
defer repository.Close()
workRepository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")})
if err != nil {
log.Fatal(err)
}
defer workRepository.Close()
capabilityRepository, err := postgres.OpenCapability(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("CAPABILITY_POSTGRES_USER"), Password: os.Getenv("CAPABILITY_POSTGRES_PASSWORD")})
if err != nil {
log.Fatal(err)
}
defer capabilityRepository.Close()
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF", epistemicsDBPasswordPath)
if err != nil {
log.Fatal(err)
}
epistemicsUser := os.Getenv("EPISTEMICS_POSTGRES_USER")
if epistemicsUser == "" {
epistemicsUser = "epistemics_role"
}
epistemicsRepository, err := postgres.OpenEpistemics(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: epistemicsUser, Password: epistemicsPassword})
if err != nil {
log.Fatal(err)
}
defer epistemicsRepository.Close()
identityRepository, err := postgres.OpenIdentity(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("IDENTITY_POSTGRES_USER"), Password: os.Getenv("IDENTITY_POSTGRES_PASSWORD")})
if err != nil {
log.Fatal(err)
}
defer identityRepository.Close()
if err := bootstrapCreatorAccount(context.Background(), identityRepository, creatorUsername, creatorPasswordPath); err != nil {
log.Fatal(err)
}
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF", interactionDBPasswordPath)
if err != nil {
log.Fatal(err)
}
interactionUser := os.Getenv("INTERACTION_POSTGRES_USER")
if interactionUser == "" {
interactionUser = "interaction_role"
}
interactionRepository, err := postgres.OpenInteraction(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: interactionUser, Password: interactionPassword})
if err != nil {
log.Fatal(err)
}
defer interactionRepository.Close()
var selfRepository *postgres.SelfRepository
selfUser := os.Getenv("SELF_POSTGRES_USER")
selfPassword := ""
if selfUser != "" && os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") != "" {
selfPassword, err = readCredentialRefSecret("SELF_POSTGRES_PASSWORD", "SELF_POSTGRES_CREDENTIAL_REF", selfDBPasswordPath)
if err != nil {
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
}
}
if selfUser != "" && selfPassword != "" {
selfRepository, err = postgres.OpenSelf(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: selfUser, Password: selfPassword})
if err != nil {
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
} else {
defer selfRepository.Close()
}
} else if selfUser == "" || os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") == "" {
log.Printf("SELF learning repository is not configured; user responses remain unaffected")
}
reminderClient, err := newContinuityReminderClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
}
responseClient, err := newContinuityResponseClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
}
attentionClient, err := newContinuityAttentionClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
}
cognitiveClient, err := newCognitiveInterpretationClient(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), &http.Client{Timeout: 90 * time.Second})
if err != nil {
log.Fatal(err)
}
researchClient, err := newResearchRuntimeClient(envOr("RESEARCH_RUNTIME_URL", "http://research-runtime:8087"), &http.Client{Timeout: 20 * time.Second})
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository, self: selfRepository}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
if err != nil {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return identity.CreatorSession{}, false
}
return session, true
}
resolveFocus := func(r *http.Request) (situation.FocusBinding, error) {
runtime := situation.RuntimeBinding{SessionID: situation.SessionID(r.URL.Query().Get("session_id")), Workspace: situation.WorkspaceID(r.URL.Query().Get("workspace_id"))}
bindings, err := repository.ByRuntime(r.Context(), runtime)
if err != nil {
return situation.FocusBinding{}, err
}
return situationdomain.NewResolver(bindings).Resolve(runtime)
}
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
cognitiveReadinessClient := &http.Client{Timeout: 135 * time.Second}
cognitiveReadinessProbe := health.HTTPProbe(cognitiveReadinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/readyz")
cognitiveReadinessProbe.Timeout = 135 * time.Second
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Timeout: 140 * time.Second, Probes: []health.Probe{
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
// A running worker is not necessarily ready to serve cognitive traffic.
// Keep deterministic paths alive, but do not advertise overall readiness
// while the cognitive dependency is still warming.
cognitiveReadinessProbe,
}})
admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)}
mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) {
var request struct {
LeaseID string `json:"lease_id"`
Request capability.AdmissionRequest `json:"request"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&request); err != nil || request.LeaseID == "" {
http.Error(w, "invalid admission request", http.StatusBadRequest)
return
}
decision, lease, err := capabilityRepository.AdmitCognitive(r.Context(), request.LeaseID, admissionPolicy, request.Request, time.Now().UnixMilli())
if err != nil {
http.Error(w, "admission unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Decision capability.AdmissionDecision `json:"decision"`
Lease capability.AdmissionLease `json:"lease"`
}{decision, lease})
})
mux.HandleFunc("POST /v1/internal/cognitive/admission/{lease}/release", func(w http.ResponseWriter, r *http.Request) {
if err := capabilityRepository.ReleaseCognitiveAdmission(r.Context(), capability.AdmissionLease{ID: r.PathValue("lease")}, time.Now().UnixMilli()); err != nil {
http.Error(w, "admission release unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/creator/sessions", creatorDatabaseSessionHandler(bootstrapToken, creatorUsername, identityRepository, sessionKey, time.Now))
mux.HandleFunc("POST /v1/creator/account/password", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.CurrentPassword == "" || request.NewPassword == "" {
http.Error(w, "invalid password change request", http.StatusBadRequest)
return
}
account, err := identityRepository.LoadCreatorAccountByBinding(r.Context(), session.ActorID, session.PrincipalID)
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, request.CurrentPassword) {
http.Error(w, "current password denied", http.StatusUnauthorized)
return
}
newHash, err := identitydomain.HashCreatorPassword(request.NewPassword)
if err != nil {
http.Error(w, "invalid new password", http.StatusBadRequest)
return
}
if err := identityRepository.UpdateCreatorPassword(r.Context(), session.ActorID, session.PrincipalID, account.Revision, newHash); err != nil {
http.Error(w, "password change conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "changed"})
})
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(session)
})
mux.HandleFunc("POST /v1/creator/research/web", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request epistemics.WebResearchRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid web research request", http.StatusBadRequest)
return
}
result, err := researchClient.EvaluateWeb(r.Context(), request)
if err != nil {
http.Error(w, "web research unavailable", http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
Evidence []work.ExtensionEvidence `json:"evidence"`
Proposal work.ExtensionProposalRequest `json:"proposal"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid development proposal", http.StatusBadRequest)
return
}
proposal, err := workdomain.BuildExtensionProposal(request.Evidence, request.Proposal)
if err != nil {
http.Error(w, "development proposal rejected", http.StatusUnprocessableEntity)
return
}
proposal.RequestedByPrincipalID = string(session.PrincipalID)
if err := workRepository.SaveExtensionProposal(r.Context(), proposal); err != nil {
http.Error(w, "development proposal conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(proposal)
})
mux.HandleFunc("POST /v1/creator/development/scaffolds", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request work.DevelopmentScaffoldRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid development scaffold", http.StatusBadRequest)
return
}
scaffold, err := workdomain.BuildDevelopmentScaffold(request)
if err != nil {
http.Error(w, "development scaffold rejected", http.StatusUnprocessableEntity)
return
}
scaffold.RequestedByPrincipalID = string(session.PrincipalID)
if err := workRepository.SaveDevelopmentScaffold(r.Context(), scaffold); err != nil {
http.Error(w, "development scaffold conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(scaffold)
})
mux.HandleFunc("POST /v1/creator/development/gates", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var gate work.DevelopmentGate
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&gate); err != nil {
http.Error(w, "invalid development gate", http.StatusBadRequest)
return
}
gate, err := prepareDevelopmentGate(gate, time.Now())
if err != nil {
http.Error(w, "development gate rejected", http.StatusUnprocessableEntity)
return
}
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, 0); err != nil {
http.Error(w, "development gate conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(gate)
})
mux.HandleFunc("POST /v1/creator/development/gates/{id}/advance", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
ExpectedRevision int64 `json:"expected_revision"`
NextStage work.DevelopmentStage `json:"next_stage"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.NextStage == "" {
http.Error(w, "invalid development gate transition", http.StatusBadRequest)
return
}
gate, err := workRepository.LoadDevelopmentGate(r.Context(), r.PathValue("id"))
if err != nil || gate.Revision != request.ExpectedRevision {
http.Error(w, "development gate revision conflict", http.StatusConflict)
return
}
gate, err = advanceDevelopmentGate(gate, request.NextStage, time.Now())
if err != nil {
http.Error(w, "development gate transition rejected", http.StatusUnprocessableEntity)
return
}
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, request.ExpectedRevision); err != nil {
http.Error(w, "development gate conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(gate)
})
mux.HandleFunc("POST /v1/creator/development/gates/{id}/verify", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
ExpectedRevision int64 `json:"expected_revision"`
VerifierRunID work.AgentRunID `json:"verifier_run_id"`
Plan developmentexecutor.Plan `json:"plan"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.VerifierRunID == "" {
http.Error(w, "invalid development verification request", http.StatusBadRequest)
return
}
gate, result, err := verifyDevelopmentGate(r.Context(), workRepository, developmentGateVerificationRequest{GateID: r.PathValue("id"), ExpectedRevision: request.ExpectedRevision, VerifierRunID: request.VerifierRunID, Plan: request.Plan}, time.Now(), developmentexecutor.OSCommandRunner{})
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"gate": gate, "verification": result})
})
registerDevelopmentGateEvidenceRoute := func(path string, next work.DevelopmentStage, fieldName string) {
mux.HandleFunc("POST /v1/creator/development/gates/{id}/"+path, func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
ExpectedRevision int64 `json:"expected_revision"`
EvidenceRef string `json:"evidence_ref"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.EvidenceRef == "" {
http.Error(w, "invalid development transition evidence", http.StatusBadRequest)
return
}
gate, err := recordDevelopmentGateEvidence(r.Context(), workRepository, developmentGateEvidenceRequest{GateID: r.PathValue("id"), ExpectedRevision: request.ExpectedRevision, NextStage: next, EvidenceRef: request.EvidenceRef}, time.Now())
if err != nil {
http.Error(w, "development "+fieldName+" transition rejected", http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(gate)
})
}
registerDevelopmentGateEvidenceRoute("canary", work.DevelopmentCanary, "canary")
registerDevelopmentGateEvidenceRoute("promote", work.DevelopmentPromoted, "promotion")
registerDevelopmentGateEvidenceRoute("rollback", work.DevelopmentRolledBack, "rollback")
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
focus, err := resolveFocus(r)
if err != nil {
http.Error(w, "creator focus unavailable", http.StatusConflict)
return
}
if focus.ActorID != session.ActorID {
http.Error(w, "creator focus denied", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(focus)
})
mux.HandleFunc("GET /v1/creator/tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
focus, err := resolveFocus(r)
if err != nil || focus.ActorID != session.ActorID {
http.Error(w, "creator focus denied", http.StatusForbidden)
return
}
target, err := workRepository.Load(r.Context(), work.TaskID(r.PathValue("id")))
if err != nil {
http.Error(w, "task not found", http.StatusNotFound)
return
}
if err := situationdomain.ValidateTaskTarget(focus, target); err != nil {
http.Error(w, "task outside creator focus", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(target)
})
mux.HandleFunc("GET /v1/creator/capabilities/{id}/health", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
status, err := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID(r.PathValue("id")))
if err != nil {
http.Error(w, "capability health not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(status)
})
mux.HandleFunc("GET /v1/creator/learning/recent", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
limit := 10
if raw := r.URL.Query().Get("limit"); raw != "" {
parsed, err := strconv.Atoi(raw)
if err != nil || parsed < 1 || parsed > 50 {
http.Error(w, "invalid learning limit", http.StatusBadRequest)
return
}
limit = parsed
}
experiences, err := epistemicsRepository.RecentExperienceRecords(r.Context(), limit)
if err != nil {
log.Printf("learning recent experiences unavailable: %v", err)
http.Error(w, "learning experience unavailable", http.StatusServiceUnavailable)
return
}
reflections, err := epistemicsRepository.RecentReflectionCandidates(r.Context(), limit)
if err != nil {
log.Printf("learning recent reflections unavailable: %v", err)
http.Error(w, "learning reflection unavailable", http.StatusServiceUnavailable)
return
}
skills, err := capabilityRepository.RecentSkillProfiles(r.Context(), limit)
if err != nil {
log.Printf("learning recent skills unavailable: %v", err)
http.Error(w, "learning skills unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(struct {
Experiences []epistemics.ExperienceRecord
Reflections []epistemics.ReflectionCandidate
Skills []capability.SkillProfile
}{Experiences: experiences, Reflections: reflections, Skills: skills})
})
mux.HandleFunc("POST /v1/creator/chat/messages", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request interaction.ChatMessageRequest
decoder := json.NewDecoder(io.LimitReader(r.Body, interactiondomain.MaxHumanUtteranceBytes+1024))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
http.Error(w, "invalid chat message", http.StatusBadRequest)
return
}
now := time.Now()
var conversation interaction.Conversation
if request.ConversationID == "" {
conversation, err = interactiondomain.NewConversation(session.ActorID, session.PrincipalID, now)
if err != nil {
http.Error(w, "conversation unavailable", http.StatusServiceUnavailable)
return
}
} else {
conversation, err = interactionRepository.Conversation(r.Context(), request.ConversationID)
if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "conversation not found", http.StatusNotFound)
return
}
if err != nil {
http.Error(w, "conversation unavailable", http.StatusServiceUnavailable)
return
}
if conversation.ActorID != session.ActorID || conversation.PrincipalID != session.PrincipalID {
http.Error(w, "conversation denied", http.StatusForbidden)
return
}
conversation.UpdatedAtUnixMillis = now.UnixMilli()
}
utterance, intent, relations, err := interactiondomain.NewHumanUtterance(conversation, request, now)
if err != nil {
http.Error(w, "invalid chat message", http.StatusBadRequest)
return
}
projects, err := repository.ListProjects(r.Context())
if err != nil {
http.Error(w, "project registry unavailable", http.StatusServiceUnavailable)
return
}
var previousFocus *situation.ActiveProjectFocus
storedFocus, err := repository.ActiveProjectFocus(r.Context(), session.ActorID, session.PrincipalID)
if err == nil {
previousFocus = &storedFocus
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
} else if !errors.Is(err, pgx.ErrNoRows) {
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
return
} else {
telemetry.RecordCompatibility("active-project-focus-read", "core-runtime", "read", "chat-focus-load", true, false, true, "EMPTY", utterance.CorrelationID, string(session.PrincipalID), now)
}
var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content)
creativeUserRequest := interactiondomain.IsCreativeRequest(utterance.Content)
locationReply := interactiondomain.LocationContextReply(utterance.Content)
continuationMessage := ""
travelContinuation := false
var previousTask *interaction.HumanUtterance
var conversationHistory []interaction.HumanUtterance
var contextProjectReferences []interaction.ProjectReference
if request.ConversationID != "" {
var historyErr error
conversationHistory, contextProjectReferences, historyErr = interactionRepository.UtterancesWithProjectReferences(r.Context(), conversation.ID)
if historyErr != nil {
log.Printf("interaction conversation history unavailable: %v", historyErr)
}
}
// Keep durable user context available even when the web client carries a
// conversation ID; a new or rotated conversation must not erase learned
// preferences.
recentUserHistory, recentProjectReferences, historyErr := interactionRepository.RecentUtterancesForPrincipalWithProjectReferences(r.Context(), session.PrincipalID, 64)
if historyErr != nil {
log.Printf("interaction principal history unavailable: %v", historyErr)
} else {
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
}
conversationHistory = append(conversationHistory, recentUserHistory...)
contextProjectReferences = append(contextProjectReferences, recentProjectReferences...)
focusProjectID := ""
if previousFocus != nil {
focusProjectID = string(previousFocus.ProjectID)
}
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
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
}
iterativeRetriever := newIterativeContextRetriever(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, experienceRecords)
if gen2Context.UsedPersonalMemoryFallback {
telemetry.RecordCompatibility("personal-memory-fallback", "core-runtime", "read", "gen2-context-empty", true, true, false, "USED", utterance.CorrelationID, string(session.PrincipalID), now)
}
retrievedMemoryReply := interactiondomain.RecallPersonalPreference(conversationHistory, effectiveTaskMessage)
stableConversationalReply, stableConversational := interactiondomain.StableConversationalReply(
effectiveTaskMessage,
interactiondomain.ClassifySpeechAct(effectiveTaskMessage, utterance.CorrectsUtteranceID),
)
if !creativeUserRequest && locationReply == "" && !stableConversational && retrievedMemoryReply == "" {
if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" {
continuationMessage = continuation
effectiveTaskMessage = continuation
} else if continuation := interactiondomain.ContinueTravelOrigin(effectiveTaskMessage, conversationHistory); continuation != "" {
continuationMessage = continuation
travelContinuation = true
effectiveTaskMessage = continuation
} else if continuation := interactiondomain.ContinueTravelDate(effectiveTaskMessage, conversationHistory); continuation != "" {
continuationMessage = continuation
travelContinuation = true
effectiveTaskMessage = continuation
}
}
if request.ConversationID != "" && interactiondomain.IsRetryRequest(utterance.Content) {
if len(conversationHistory) > 0 {
for index := len(conversationHistory) - 1; index >= 0; index-- {
if interactiondomain.IsRetryRequest(conversationHistory[index].Content) {
continue
}
candidate := conversationHistory[index]
previousTask = &candidate
effectiveTaskMessage = interactiondomain.StripAssistantVocative(candidate.Content)
break
}
}
}
semantic, err := interactiondomain.InterpretUtterance(utterance, projects, previousFocus, now)
if err != nil {
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
return
}
if continuationMessage != "" {
continuedUtterance := utterance
continuedUtterance.Content = continuationMessage
continuedSemantic, continuedErr := interactiondomain.InterpretUtterance(continuedUtterance, projects, previousFocus, now)
if continuedErr == nil {
continuedSemantic.Interpretation.Source = "CONVERSATION_CONTINUATION"
if travelContinuation {
continuedSemantic.Interpretation.SelectionReason = "travel follow-up completed the previous travel task"
} else {
continuedSemantic.Interpretation.SelectionReason = "location follow-up completed the previous local-search task"
}
semantic = continuedSemantic
}
}
if !creativeUserRequest && travelContinuation && interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory) == "" {
semantic.CapabilityHints = append(semantic.CapabilityHints, string(capability.CapabilityKindWebResearch))
semantic.NeedsCognition = false
semantic.Interpretation.NeedsCognition = false
}
creativeRequest := creativeUserRequest || interactiondomain.IsCreativeRequest(effectiveTaskMessage)
creativeReply := interactiondomain.CreativeDeterministicReply(utterance.Content)
if creativeReply == "" {
creativeReply = interactiondomain.CreativeDeterministicReply(effectiveTaskMessage)
}
creativeFallbackReply := interactiondomain.CreativeFallbackReply(utterance.Content)
if creativeFallbackReply == "" {
creativeFallbackReply = interactiondomain.CreativeFallbackReply(effectiveTaskMessage)
}
if creativeRequest {
semantic.CapabilityHints = interactiondomain.RemoveCapabilityHint(semantic.CapabilityHints, string(capability.CapabilityKindWebResearch))
if creativeReply != "" {
semantic.Reply = creativeReply
semantic.NeedsCognition = false
semantic.Interpretation.NeedsCognition = false
} else {
semantic.NeedsCognition = true
semantic.Interpretation.NeedsCognition = true
semantic.CapabilityHints = append(semantic.CapabilityHints, string(capability.CapabilityKindCognitiveReasoning))
if creativeFallbackReply != "" {
semantic.Reply = creativeFallbackReply
}
}
}
if locationReply != "" {
continuationMessage = ""
travelContinuation = false
effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content)
previousTask = nil
semantic.Reply = locationReply
semantic.NeedsCognition = false
semantic.Interpretation.NeedsCognition = false
}
if previousTask != nil {
previousSemantic, previousErr := interactiondomain.InterpretUtterance(*previousTask, projects, previousFocus, now)
if previousErr == nil {
semantic = interactiondomain.CarryForwardPreviousTask(semantic, previousSemantic)
}
}
assistantName := interactiondomain.PreferredAssistantName(utterance.Content)
if assistantName == "" {
assistantName = latestAssistantName(conversationHistory)
}
identityReply := ""
memoryInstructionReply := ""
log.Printf("interaction identity context: name=%q history=%d", assistantName, len(conversationHistory))
if interactiondomain.IsIdentityInquiry(utterance.Content) && assistantName != "" {
identityReply = "Меня зовут " + assistantName + ". Я Домовой, ваш помощник."
semantic.Reply = identityReply
semantic.NeedsCognition = false
semantic.CapabilityHints = nil
semantic.Interpretation.Applicability = interaction.SemanticApplicable
semantic.Interpretation.Clarification = interaction.ClarificationNotRequired
}
if identityReply == "" && interactiondomain.IsExplicitMemoryInstruction(utterance.Content) {
memoryInstructionReply = "Запомнил."
semantic.Reply = memoryInstructionReply
semantic.NeedsCognition = false
semantic.CapabilityHints = nil
semantic.Interpretation.Applicability = interaction.SemanticApplicable
semantic.Interpretation.Clarification = interaction.ClarificationNotRequired
}
deterministicSpeechAct := semantic.Interpretation.SpeechAct
deterministicReply := semantic.Reply
routePlan := buildChatRoutePlan(semantic)
cognitiveAttempted := false
cognitiveAvailable := true
semanticInterpretationRequested := shouldRequestSemanticInterpretation(semantic)
semanticReflexAudit := shouldAuditDeterministicReflex(semantic, string(utterance.ID), now)
// A selected specialized route already has an executable semantic outcome.
// Do not block it on the general cognitive channel: research and image
// generation must reach their own adapters even when cognition is degraded.
if specializedRouteBypassesGeneralInterpretation(routePlan) {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if stableConversational {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if retrievedMemoryReply != "" {
semanticInterpretationRequested = false
semanticReflexAudit = false
}
if identityReply != "" || memoryInstructionReply != "" {
semanticReflexAudit = false
}
if semanticInterpretationRequested || semanticReflexAudit {
cognitiveAttempted = true
projectNames := make([]string, 0, len(semantic.Projects))
for _, ref := range semantic.Projects {
if ref.ProjectDisplayName != "" {
projectNames = append(projectNames, ref.ProjectDisplayName)
}
}
cognitiveUtterance := utterance
cognitiveUtterance.Content = effectiveTaskMessage
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, cognitiveUtterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
if candidate, candidateErr := interactionCandidateFromGateway(utterance.ID, response.Candidate, now); candidateErr == nil {
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, effectiveTaskMessage, candidate, previousFocus)
} else {
cognitiveAvailable = false
}
} else {
cognitiveAvailable = false
}
}
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
semantic.Interpretation.Clarification = interaction.ClarificationRequired
semantic.Interpretation.Applicability = interaction.SemanticCandidateClarify
semantic.Reply = travelReply
deterministicReply = travelReply
semantic.NeedsCognition = false
semantic.Interpretation.NeedsCognition = false
}
}
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
http.Error(w, "save chat message", http.StatusInternalServerError)
return
}
if err := interactionRepository.SaveSemanticResult(r.Context(), semantic.Interpretation, semantic.Candidates, semantic.Projects, semantic.Relations); err != nil {
http.Error(w, "save chat semantics", http.StatusInternalServerError)
return
}
var responseResult continuity.HumanResponseResult
var responseErr error
if shouldInvokeHumanResponse(stableConversational, identityReply, memoryInstructionReply) {
responseResult, responseErr = responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
SourceUtteranceID: string(utterance.ID),
SourceIntentID: string(intent.ID),
Message: effectiveTaskMessage,
RequestedAtUnixMillis: now.UnixMilli(),
})
}
if semantic.Interpretation.SpeechAct == interaction.SpeechActCorrection {
learning.recordCorrection(r.Context(), correctionLearningInput{Utterance: utterance, Intent: intent, At: now})
}
activeFocusName := ""
activeFocusProvenance := situation.ProjectResolutionProvenance("")
if semantic.ActiveFocus != nil {
if err := repository.SaveActiveProjectFocus(r.Context(), *semantic.ActiveFocus); err != nil {
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "FAILURE", utterance.CorrelationID, string(session.PrincipalID), now)
http.Error(w, "save project focus", http.StatusInternalServerError)
return
}
telemetry.RecordCompatibility("active-project-focus-write", "core-runtime", "write", "semantic-focus-update", true, false, true, "SUCCESS", utterance.CorrelationID, string(session.PrincipalID), now)
activeFocusName = semantic.ActiveFocus.ProjectDisplayName
activeFocusProvenance = semantic.ActiveFocus.Provenance
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus && previousFocus != nil {
activeFocusName = previousFocus.ProjectDisplayName
activeFocusProvenance = previousFocus.Provenance
}
planningReply := deterministicReply
workPlanned := false
needsCognition := semantic.NeedsCognition
projectID := ""
if semantic.ActiveFocus != nil {
projectID = string(semantic.ActiveFocus.ProjectID)
} else if len(semantic.Projects) > 0 && semantic.Projects[0].Provenance == situation.ProjectResolutionInheritedFocus {
projectID = string(previousFocus.ProjectID)
}
if responseErr == nil && responseResult.Handled && !stableConversational {
planningReply = responseResult.Reply
needsCognition = false
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
if resumed, err := continueDelegationFromHumanResponse(r.Context(), workRepository, responseResult.Response, now); err == nil && resumed {
planningReply = "Принял. Продолжаю ту же задачу coding-agent без потери контекста."
}
} else if responseErr == nil && looksLikeUnsupportedContinuation(utterance.Content, responseResult) {
learning.recordFastPathDecline(r.Context(), fastPathDeclineLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, Reason: declineReasonForResponse(responseResult), At: now})
}
planningInput := work.PlanningInput{
ConversationID: work.ConversationRef(conversation.ID),
UtteranceID: work.SourceUtteranceRef(utterance.ID),
IntentID: work.SourceIntentRef(intent.ID),
ActorID: session.ActorID,
PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
ProjectID: work.ProjectRef(projectID),
ProjectDisplayName: activeFocusName,
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
Message: effectiveTaskMessage,
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
SecondarySpeechAct: work.SpeechActRef(semantic.Interpretation.SecondarySpeechAct),
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
NeedsCognition: semantic.NeedsCognition,
RequiresClarification: semantic.Interpretation.Clarification == interaction.ClarificationRequired,
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
// Introspection requests are read-only household UX. A cognitive
// candidate must not reinterpret them as an executable delegation.
switch semantic.InformationRequestKind {
case interaction.InformationRequestCapabilityInventory, interaction.InformationRequestLocalCapabilityInventory, interaction.InformationRequestCognitiveModelInventory, interaction.InformationRequestCognitiveCurrentModel, interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery, interaction.InformationRequestMemoryInventory:
helpPath = work.HelpPath{}
}
if responseErr == nil && responseResult.Handled {
helpPath = work.HelpPath{}
}
switch semantic.InformationRequestKind {
case interaction.InformationRequestMemoryInventory:
planningReply = humanMemoryInventory(conversationHistory, assistantName)
needsCognition = false
case interaction.InformationRequestResearchFailureExplanation:
planningReply = "Я не выдаю предположение как факт, если не получил согласованных проверяемых данных. Могу повторить поиск, уточнить запрос или проверить другой источник."
needsCognition = false
case interaction.InformationRequestCapabilityInventory:
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
if err == nil && len(kinds) > 0 {
planningReply = "Я умею общаться, искать проверяемую информацию, работать с подключёнными моделями и " + humanCapabilityList(kinds) + "."
} else {
planningReply = "Я умею общаться, сохранять контекст, искать проверяемую информацию и работать с подключёнными моделями. Дополнительные действия подключаются отдельно."
}
needsCognition = false
case interaction.InformationRequestLocalCapabilityInventory:
planningReply = "Без внешних моделей я могу поддерживать разговор, хранить контекст, вести цели и напоминания, а также честно сообщать состояние подключённых возможностей."
needsCognition = false
case interaction.InformationRequestCognitiveModelInventory:
candidates, inventoryErr := capabilityRepository.LoadCognitiveCandidates(r.Context(), capability.CapabilityID("creator-advisory"), session.PrincipalID)
planningReply = humanCognitiveModelInventory(candidates, inventoryErr)
needsCognition = false
case interaction.InformationRequestCognitiveCurrentModel:
health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal"))
planningReply = humanCurrentCognitiveModelReply(health, healthErr)
needsCognition = false
case interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery:
health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal"))
planningReply = humanCognitiveChannelReply(semantic.InformationRequestKind, semantic.Interpretation.SpeechAct, health, healthErr)
needsCognition = false
}
researchBacked := false
researchAttempted := false
stableKnowledgeReply := false
if retrievedMemoryReply != "" {
planningReply = retrievedMemoryReply
needsCognition = false
}
if hasChatRoute(routePlan, chatRouteResearch) && len(gen2Context.Fragments) == 0 && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
researchAttempted = true
if planID, idErr := ids.New(); idErr == nil {
parts := interactiondomain.IndependentRequestParts(effectiveTaskMessage)
replies := make([]string, 0, len(parts))
for index, part := range parts {
if clarification := interactiondomain.LocalSearchClarification(part); clarification != "" {
replies = append(replies, "По запросу «"+part+"»: "+clarification)
continue
}
learnedLexicon := interactiondomain.TaskLexiconFromHistory(conversationHistory)
// Preserve the original utterance for semantic route compilation
// before removing a search imperative. Distance and route skills
// need the user's relation words (for example "до") to bind a
// remembered current location to the requested destination.
searchQuery := interactiondomain.TravelResearchQuery(part, conversationHistory, now)
searchQuery = interactiondomain.SearchQueryWithLexicon(searchQuery, learnedLexicon)
searchQuery = interactiondomain.TimeDifferenceResearchQuery(searchQuery)
// The research capability may try its configured source and a
// source-recovery fallback. Its caller must leave enough budget for
// both attempts and evidence evaluation.
researchCtx, cancelResearch := context.WithTimeout(r.Context(), 18*time.Second)
result, researchErr := researchClient.SearchWeb(researchCtx, epistemics.WebSearchRequest{
Plan: epistemics.ResearchPlan{ID: epistemics.ResearchPlanID(fmt.Sprintf("plan:%s:%d", planID, index)), Question: searchQuery, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"},
PropositionKey: fmt.Sprintf("question:%s:%d", utterance.ID, index), Query: searchQuery,
})
reply, backed := renderResearchReply(searchQuery, result, researchErr)
if !backed && cognitiveAvailable {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
}, iterativeRetriever)
cancelAnswer()
if answerErr == nil && stableKnowledgeAnswer(answer) {
reply, backed = answer, true
stableKnowledgeReply = true
}
}
cancelResearch()
if backed {
researchBacked = true
}
if len(parts) > 1 {
reply = "По запросу «" + part + "»: " + reply
}
replies = append(replies, reply)
}
planningReply = strings.Join(replies, "\n\n")
if researchBacked {
semantic.Reply = planningReply
}
// A failed source must not turn the complete conversation into a
// runtime failure or invite an unsupported local guess for a fact.
needsCognition = false
}
}
if helpPath.Reply != "" {
planningReply = helpPath.Reply
needsCognition = false
}
if !creativeRequest && !stableConversational && retrievedMemoryReply == "" {
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
planningReply = travelReply
needsCognition = false
}
}
canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && (!hasChatRoute(routePlan, chatRouteResearch) || len(gen2Context.Fragments) > 0) && !creativeRequest {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
}, iterativeRetriever)
cancelAnswer()
if answerErr == nil && stableKnowledgeAnswer(answer) {
planningReply = answer
stableKnowledgeReply = true
needsCognition = false
}
}
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
planningReply = humanCognitiveDegradedReply(utterance.Content)
needsCognition = false
}
if creativeFallbackReply != "" && cognitiveAttempted && !cognitiveAvailable {
planningReply = creativeFallbackReply
needsCognition = false
}
var capabilityStatus capability.CapabilityResolutionStatus
var capabilityKind capability.CapabilityKind
var capabilityGapID capability.CapabilityGapID
var delegationID work.DelegationID
var delegationState work.DelegationState
var reminder *continuity.ProspectiveItem
var generatedImage capability.MultimodalResponse
mediaHandled := false
mediaAttempted := false
if hasChatRoute(routePlan, chatRouteMedia) && semantic.Interpretation.Clarification != interaction.ClarificationRequired {
mediaAttempted = true
imageCtx, cancelImage := context.WithTimeout(r.Context(), 15*time.Minute)
generatedImage, err = cognitiveClient.GenerateImage(imageCtx, effectiveTaskMessage, "")
cancelImage()
if err == nil && strings.TrimSpace(generatedImage.DataBase64) != "" {
planningReply = "Готово."
semantic.Reply = planningReply
needsCognition = false
mediaHandled = true
} else {
planningReply = "Сейчас не удалось создать изображение."
semantic.Reply = planningReply
needsCognition = false
}
}
if helpPath.Kind == work.HelpPathDirectCapability && len(helpPath.SuggestedCapabilityKinds) > 0 {
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
started := time.Now()
resolution, resolveErr := resolveChatCapability(r.Context(), capabilityRepository, capabilityKind, session, intent, utterance, now)
capabilityStatus = resolution.Status
if resolveErr == nil && len(resolution.Eligible) > 0 {
reminderRequest, reminderErr := buildReminderRequest(utterance.Content, session, intent, utterance, now)
if reminderErr == nil {
result, scheduleErr := reminderClient.Schedule(r.Context(), r.Header.Get("Authorization"), reminderRequest)
finished := time.Now()
if scheduleErr == nil {
reminder = &result.Item
planningReply = fmt.Sprintf("Готово. Я создал напоминание: %s.", result.Item.Summary)
usageID, _ := observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageSucceeded, string(result.Item.ID), started, finished)
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, ImplementationID: resolution.Eligible[0].Implementation.ID, UsageObservation: usageID, Outcome: capability.UsageSucceeded, OutcomeRef: "continuity:prospective:" + string(result.Item.ID), At: finished})
} else {
planningReply = "Scheduling capability сейчас недоступна: напоминание не создано. Я не буду притворяться, что действие выполнено."
usageID, _ := observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageFailed, "continuity-reminder-unavailable", started, finished)
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, ImplementationID: resolution.Eligible[0].Implementation.ID, UsageObservation: usageID, Outcome: capability.UsageFailed, OutcomeRef: "continuity-reminder-unavailable", At: finished})
}
} else {
planningReply = "Мне нужно уточнить время напоминания. Например: «Напомни завтра в 10 позвонить врачу»."
capabilityStatus = capability.ResolutionPartiallyAvailable
}
} else if resolution.Gap != nil {
capabilityGapID = resolution.Gap.ID
planningReply = "Сейчас scheduling capability недоступна, поэтому напоминание не создано. Я зафиксировал capability gap."
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, GapID: resolution.Gap.ID, Outcome: capability.UsageSkipped, OutcomeRef: "capability:gap:" + string(resolution.Gap.ID), At: now})
}
needsCognition = false
}
if helpPath.Kind == work.HelpPathCapabilityGap && len(helpPath.SuggestedCapabilityKinds) > 0 {
capabilityKind = helpPath.SuggestedCapabilityKinds[0]
resolution, resolveErr := resolveChatCapability(r.Context(), capabilityRepository, capabilityKind, session, intent, utterance, now)
capabilityStatus = resolution.Status
if resolveErr == nil {
planningReply = "Capability найдена, но в этом шаге она не исполняется без отдельного разрешённого action path."
} else if resolution.Gap != nil {
capabilityGapID = resolution.Gap.ID
planningReply = "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксировал capability gap, без ложного выполнения."
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, GapID: resolution.Gap.ID, Outcome: capability.UsageSkipped, OutcomeRef: "capability:gap:" + string(resolution.Gap.ID), At: now})
}
needsCognition = false
}
if helpPath.NeedsDurableWork {
planning, err := workdomain.BuildPlanningResult(planningInput, now)
if err != nil {
http.Error(w, "plan chat work", http.StatusServiceUnavailable)
return
}
if err := workRepository.SavePlanningResult(r.Context(), planning); err != nil {
http.Error(w, "save work plan", http.StatusInternalServerError)
return
}
planningReply = planning.Reply
workPlanned = true
needsCognition = false
if created, resolution, createErr := maybeCreateAgentDelegation(r.Context(), workRepository, capabilityRepository, session, conversation, intent, utterance, planning, now); createErr == nil && created != nil {
delegationID = created.Delegation.ID
delegationState = created.Delegation.State
capabilityKind = created.Delegation.CapabilityKind
capabilityStatus = resolution.Status
planningReply = "Понял задачу. Я собрал work item, выбрал coding-agent в CANARY-режиме и создал delegation. Буду вести её как отдельный run с checkpoint/resume, без потери исходного human intent."
} else if resolution.Gap != nil {
capabilityKind = resolution.Requirement.CapabilityKind
capabilityStatus = resolution.Status
capabilityGapID = resolution.Gap.ID
planningReply = planningReply + " Для coding delegation подходящий agent implementation сейчас недоступен, поэтому я сохранил план без ложного запуска."
}
}
if identityReply != "" {
planningReply = identityReply
needsCognition = false
}
if memoryInstructionReply != "" {
planningReply = memoryInstructionReply
needsCognition = false
}
// Stable-knowledge answers have already passed the local cognitive
// route with compact, source-aware context. Re-reviewing the same answer
// serially can exhaust the household request budget on a loaded local
// provider and turn a valid answer into a degraded response.
if shouldReviewResponse(planningReply, stableKnowledgeReply, stableConversational, identityReply, memoryInstructionReply, retrievedMemoryReply != "") {
reviewContext, cancelReview := context.WithTimeout(r.Context(), 12*time.Second)
reviewedReply, reviewErr := cognitiveClient.ReviewResponse(reviewContext, session, utterance.Content, planningReply, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct),
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
InformationRequestKind: semantic.InformationRequestKind,
ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
cancelReview()
// A source-backed research result is owned by the research capability,
// but its wording is not canonical state. Let the cognitive editor turn
// it into a useful human answer while reviewPreservesResearchEvidence
// still requires the source evidence to remain present. Information
// requests without research (identity, channel state and so on) remain
// stable semantic replies and cannot be rewritten here.
stableSemanticReply := mediaAttempted || identityReply != "" || memoryInstructionReply != "" || creativeReply != "" || (creativeFallbackReply != "" && !cognitiveAvailable) || (semantic.InformationRequestKind != "" && !(researchAttempted && researchBacked)) || locationReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection
if reviewErr != nil {
log.Printf("cognitive response review unavailable: %v", reviewErr)
} else if reviewMayReplacePlanningReply(stableSemanticReply, stableKnowledgeReply, researchAttempted, researchBacked, reviewedReply, planningReply) {
planningReply = reviewedReply
} else if reviewedReply != planningReply {
log.Printf("cognitive response review proposed stable semantic rewrite; preserving semantic reply")
}
}
if !mediaAttempted && !mediaHandled && identityReply == "" && creativeReply == "" && creativeFallbackReply == "" {
if stableConversational {
planningReply = stableConversationalReply
needsCognition = false
}
}
if identityReply != "" {
semantic.Reply = identityReply
needsCognition = false
planningReply = identityReply
}
if memoryInstructionReply != "" {
semantic.Reply = memoryInstructionReply
needsCognition = false
planningReply = memoryInstructionReply
}
if creativeReply != "" {
semantic.Reply = creativeReply
needsCognition = false
planningReply = creativeReply
}
if creativeFallbackReply != "" && !cognitiveAvailable {
semantic.Reply = creativeFallbackReply
needsCognition = false
planningReply = creativeFallbackReply
}
if strings.TrimSpace(planningReply) != "" {
semantic.Reply = planningReply
}
log.Printf("chat response final message=%q act=%s deterministic=%s handled=%t fallback=%q planning=%q", utterance.Content, semantic.Interpretation.SpeechAct, deterministicSpeechAct, responseResult.Handled, semantic.Reply, planningReply)
response := interaction.ChatMessageResponse{
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
OriginalIntentID: intent.ID,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: needsCognition,
FallbackText: semantic.Reply,
AssistantName: assistantName,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
ResearchBacked: researchBacked,
CapabilityStatus: capabilityStatus,
CapabilityKind: capabilityKind,
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
InformationRequestKind: semantic.InformationRequestKind,
InterpretationConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
CapabilityGapID: capabilityGapID,
DelegationID: string(delegationID),
DelegationState: string(delegationState),
Reminder: reminder,
GeneratedImageContentType: generatedImage.ContentType,
GeneratedImageBase64: generatedImage.DataBase64,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(response)
})
mux.HandleFunc("GET /v1/creator/delegations/{id}", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
value, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "delegation not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
})
mux.HandleFunc("GET /v1/creator/delegations/{id}/latest-checkpoint", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
value, err := workRepository.LatestAgentCheckpoint(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "checkpoint not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/checkpoints", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
BlockerClass work.AgentBlockerClass
CompletedSummary string
RemainingSummary string
TestSummary string
BranchName string
CommitHash string
NextStep string
ArtifactRefs []work.EvidenceRef
RecordedAtUnixMillis int64
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid checkpoint", http.StatusBadRequest)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run")))
if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) {
http.Error(w, "run not found", http.StatusNotFound)
return
}
updatedRun, checkpoint, err := workdomain.CheckpointRun(run, request.BlockerClass, request.CompletedSummary, request.RemainingSummary, request.TestSummary, request.BranchName, request.CommitHash, request.NextStep, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis))
if err != nil {
http.Error(w, "invalid checkpoint", http.StatusBadRequest)
return
}
if err := workRepository.SaveAgentCheckpoint(r.Context(), updatedRun, checkpoint); err != nil {
http.Error(w, "save checkpoint", http.StatusInternalServerError)
return
}
if request.BlockerClass == work.AgentBlockerHumanDecisionRequired {
delegation, err := workRepository.LoadDelegation(r.Context(), run.DelegationID)
if err == nil {
_, _ = attentionClient.Create(r.Context(), r.Header.Get("Authorization"), continuity.CreateAttentionRequest{
OwnerPrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
SourceKind: continuity.AttentionSourceAgentDelegation,
SourceExternalRef: delegationAttentionRef(delegation.ID),
Level: continuity.AttentionActionRequired,
Summary: "Нужен твой выбор по задаче coding-agent: " + firstNonEmpty(request.RemainingSummary, delegation.DesiredOutcome),
DueAtUnixMillis: checkpoint.RecordedAtUnixMillis,
CreatedAtUnixMillis: checkpoint.RecordedAtUnixMillis,
})
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(checkpoint)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/runs/{run}/results", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
Summary string
OutcomeRef string
VerificationSummary string
ArtifactRefs []work.EvidenceRef
RecordedAtUnixMillis int64
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid result", http.StatusBadRequest)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), work.AgentRunID(r.PathValue("run")))
if err != nil || run.DelegationID != work.DelegationID(r.PathValue("id")) {
http.Error(w, "run not found", http.StatusNotFound)
return
}
updatedRun, result, err := workdomain.CompleteRun(run, request.Summary, request.OutcomeRef, request.VerificationSummary, request.ArtifactRefs, time.UnixMilli(request.RecordedAtUnixMillis))
if err != nil {
http.Error(w, "invalid result", http.StatusBadRequest)
return
}
if err := workRepository.SaveAgentResult(r.Context(), updatedRun, result); err != nil {
http.Error(w, "save result", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(result)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/resume", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "delegation not found", http.StatusNotFound)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID)
if err != nil {
http.Error(w, "run not found", http.StatusNotFound)
return
}
checkpoint, err := workRepository.LatestAgentCheckpoint(r.Context(), delegation.ID)
if err != nil {
http.Error(w, "checkpoint not found", http.StatusNotFound)
return
}
updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, time.Now())
if err != nil {
http.Error(w, "resume not allowed", http.StatusConflict)
return
}
if err := workRepository.SaveResumedRun(r.Context(), updatedDelegation, resumedRun); err != nil {
http.Error(w, "save resumed run", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(resumedRun)
})
mux.HandleFunc("POST /v1/creator/delegations/{id}/verify", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
Verified bool
Summary string
EvidenceRefs []work.EvidenceRef
RecordedAtUnixMillis int64
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.RecordedAtUnixMillis < 1 {
http.Error(w, "invalid verification", http.StatusBadRequest)
return
}
delegation, err := workRepository.LoadDelegation(r.Context(), work.DelegationID(r.PathValue("id")))
if err != nil {
http.Error(w, "delegation not found", http.StatusNotFound)
return
}
run, err := workRepository.LoadAgentRun(r.Context(), delegation.LatestRunID)
if err != nil {
http.Error(w, "run not found", http.StatusNotFound)
return
}
result, err := workRepository.LatestAgentResult(r.Context(), delegation.ID)
if err != nil {
http.Error(w, "result not found", http.StatusNotFound)
return
}
updatedDelegation, verification, err := workdomain.VerifyDelegationResult(delegation, run, result, request.Verified, request.Summary, request.EvidenceRefs, time.UnixMilli(request.RecordedAtUnixMillis))
if err != nil {
http.Error(w, "invalid verification", http.StatusBadRequest)
return
}
if err := workRepository.SaveDelegationVerification(r.Context(), updatedDelegation, verification); err != nil {
http.Error(w, "save verification", http.StatusInternalServerError)
return
}
runCount, _ := workRepository.CountAgentRuns(r.Context(), delegation.ID)
learning.recordDelegationOutcome(r.Context(), delegationLearningInput{
Intent: interaction.OriginalHumanIntent{ID: interaction.OriginalHumanIntentID(delegation.SourceIntentID), Text: delegation.DesiredOutcome},
Utterance: interaction.HumanUtterance{ID: interaction.HumanUtteranceID(delegation.SourceUtteranceID), Content: delegation.DesiredOutcome},
DelegationID: string(delegation.ID),
RunID: string(run.ID),
ImplementationID: run.ImplementationID,
OutcomeRef: result.OutcomeRef,
VerificationRefs: evidenceRefsToEpistemics(request.EvidenceRefs),
Verified: request.Verified,
ExecutionWindows: runCount,
HumanBlocked: false,
At: time.UnixMilli(request.RecordedAtUnixMillis),
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(verification)
})
mux.HandleFunc("GET /v1/principal-bindings/{actor}/{principal}", func(w http.ResponseWriter, r *http.Request) {
binding, err := identityRepository.LoadBinding(r.Context(), identity.ActorID(r.PathValue("actor")), identity.PrincipalID(r.PathValue("principal")))
if err != nil {
http.Error(w, "principal binding not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(binding)
})
mux.HandleFunc("GET /v1/tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
target, err := workRepository.Load(r.Context(), work.TaskID(r.PathValue("id")))
if err != nil {
http.Error(w, "task not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(target)
})
mux.HandleFunc("POST /v1/capabilities/health", func(w http.ResponseWriter, r *http.Request) {
var status capability.Health
if err := json.NewDecoder(r.Body).Decode(&status); err != nil {
http.Error(w, "invalid capability health", http.StatusBadRequest)
return
}
if err := capabilitydomain.ValidateHealth(status); err != nil {
http.Error(w, "invalid capability health", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveHealth(r.Context(), status); err != nil {
http.Error(w, "save capability health", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("GET /v1/capabilities/{id}/health", func(w http.ResponseWriter, r *http.Request) {
status, err := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID(r.PathValue("id")))
if err != nil {
http.Error(w, "capability health not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(status)
})
mux.HandleFunc("GET /v1/capability-bindings/{id}/revisions/{revision}", func(w http.ResponseWriter, r *http.Request) {
revision, err := strconv.ParseInt(r.PathValue("revision"), 10, 64)
if err != nil || revision < 1 {
http.Error(w, "invalid binding revision", http.StatusBadRequest)
return
}
binding, err := capabilityRepository.LoadBinding(r.Context(), capability.CapabilityBindingID(r.PathValue("id")), revision)
if err != nil {
http.Error(w, "capability binding not found", http.StatusNotFound)
return
}
if err = capabilitydomain.ValidateBinding(binding); err != nil {
http.Error(w, "capability binding invalid", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(binding)
})
mux.HandleFunc("GET /v1/capability-bindings/active", func(w http.ResponseWriter, r *http.Request) {
capabilityID := capability.CapabilityID(r.URL.Query().Get("capability_id"))
ownerID := identity.PrincipalID(r.URL.Query().Get("owner_principal_id"))
if capabilityID == "" || ownerID == "" {
http.Error(w, "invalid binding query", http.StatusBadRequest)
return
}
binding, err := capabilityRepository.LoadActiveBinding(r.Context(), capabilityID, ownerID)
if err != nil {
http.Error(w, "active capability binding not found", http.StatusNotFound)
return
}
if err = capabilitydomain.UsableBinding(binding); err != nil {
http.Error(w, "active capability binding unavailable", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(binding)
})
mux.HandleFunc("GET /v1/cognitive-bindings/{id}/revisions/{revision}/snapshot", func(w http.ResponseWriter, r *http.Request) {
revision, err := strconv.ParseInt(r.PathValue("revision"), 10, 64)
if err != nil || revision < 1 {
http.Error(w, "invalid cognitive binding revision", http.StatusBadRequest)
return
}
snapshot, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), capability.CapabilityBindingID(r.PathValue("id")), revision)
if err != nil {
http.Error(w, "cognitive binding snapshot not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(snapshot)
})
mux.HandleFunc("POST /v1/cognitive-bindings/resolve", func(w http.ResponseWriter, r *http.Request) {
var requirement capability.CognitiveRequirement
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&requirement); err != nil || requirement.CapabilityID == "" || requirement.OwnerPrincipalID == "" || requirement.InputBytes < 0 || requirement.MaxOutputBytes < 1 {
http.Error(w, "invalid cognitive requirement", http.StatusBadRequest)
return
}
var candidates []capability.ResolvedCognitiveBinding
var err error
if requirement.TaskProfileID != "" {
candidates, err = capabilityRepository.LoadCognitiveCandidatesForProfile(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID, requirement.TaskProfileID)
} else {
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
}
if err != nil {
// The resolver is an internal capability boundary. Keep request identity
// and content out of logs, but preserve the repository error so a 503
// can be distinguished from a true empty/unsuitable binding set.
log.Printf("cognitive registry resolve unavailable: %v", err)
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
selected, err := capabilitydomain.SelectCognitiveBinding(requirement, candidates)
if err != nil {
http.Error(w, "no invocable cognitive binding", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(selected)
})
mux.HandleFunc("POST /v1/cognitive-registry/discoveries", func(w http.ResponseWriter, r *http.Request) {
var snapshot capability.CognitiveDiscoverySnapshot
decoder := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&snapshot); err != nil {
http.Error(w, "invalid cognitive discovery", http.StatusBadRequest)
return
}
registration, err := capabilitydomain.BuildDiscoveryRegistration(snapshot)
if err != nil {
http.Error(w, "invalid cognitive discovery", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveCognitiveDiscovery(r.Context(), registration); err != nil {
log.Printf("cognitive discovery persistence failed: %v", err)
http.Error(w, "save cognitive discovery", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(registration)
})
mux.HandleFunc("POST /v1/cognitive-registry/evaluated-capabilities", func(w http.ResponseWriter, r *http.Request) {
var value capability.EvaluatedCapability
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.ID == "" || value.BindingID == "" || value.BindingRevision < 1 || value.CapabilityID == "" || value.BenchmarkVersion == "" || value.ArtifactID == "" || value.ObservedAtUnixMillis < 1 {
http.Error(w, "invalid evaluated capability", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveEvaluatedCapability(r.Context(), value); err != nil {
http.Error(w, "save evaluated capability", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/cognitive-registry/task-evaluations", func(w http.ResponseWriter, r *http.Request) {
var value capability.TaskEvaluation
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || capabilitydomain.ValidateTaskEvaluation(value) != nil {
http.Error(w, "invalid task evaluation", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveTaskEvaluation(r.Context(), value); err != nil {
http.Error(w, "save task evaluation", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/cognitive-registry/model-qualification-observations", func(w http.ResponseWriter, r *http.Request) {
var value capability.ModelQualificationObservation
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.Validate() != nil {
http.Error(w, "invalid model qualification observation", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveModelQualificationObservation(r.Context(), value); err != nil {
http.Error(w, "save model qualification observation", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request capability.CognitiveCanaryPromotionRequest
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid canary promotion", http.StatusBadRequest)
return
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
candidates, err := capabilityRepository.LoadCognitiveCandidatesForEvaluation(r.Context(), request.CapabilityID, session.PrincipalID, request.EvaluationID)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
var current capability.ResolvedCognitiveBinding
for _, candidate := range candidates {
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
current = candidate
break
}
}
promotion, err := capabilitydomain.BuildCanaryPromotion(request, current)
if err != nil {
http.Error(w, "canary promotion precondition failed", http.StatusConflict)
return
}
if err = capabilityRepository.SaveCognitiveCanaryPromotion(r.Context(), promotion); err != nil {
http.Error(w, "canary promotion conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/creator/cognitive-activate", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request capability.CognitiveActivationPromotionRequest
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid cognitive activation", http.StatusBadRequest)
return
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), request.BindingID, request.ExpectedBindingRevision)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
promotion, err := capabilitydomain.BuildActivationPromotion(request, current)
if err != nil {
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
return
}
if err = capabilityRepository.SaveCognitiveActivationPromotion(r.Context(), promotion); err != nil {
http.Error(w, "cognitive activation conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) {
var value capability.PerformanceEvidence
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.ID == "" || value.BindingID == "" || value.BindingRevision < 1 || value.ModelDescriptorID == "" || value.ModelRevision < 1 || value.HardwareProfile == "" || value.Runtime == "" || value.BenchmarkVersion == "" || value.ArtifactID == "" || value.ObservedAtUnixMillis < 1 {
http.Error(w, "invalid performance evidence", http.StatusBadRequest)
return
}
if err := capabilityRepository.SavePerformanceEvidence(r.Context(), value); err != nil {
http.Error(w, "save performance evidence", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/cognitive-usage-observations", func(w http.ResponseWriter, r *http.Request) {
var observation capability.CognitiveUsageObservation
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&observation); err != nil || observation.ObservationID == "" || observation.RequestID == "" || observation.BindingID == "" || observation.BindingRevision < 1 || observation.ProviderInstanceID == "" || observation.ModelDescriptorID == "" || observation.ObservedAtUnixMillis < 1 || observation.LatencyMillis < 0 || observation.InputUnits < 0 || observation.OutputUnits < 0 || (observation.EstimatedCostMicros != nil && *observation.EstimatedCostMicros < 0) || (observation.ActualCostMicros != nil && *observation.ActualCostMicros < 0) || observation.ResultClass == "" {
http.Error(w, "invalid cognitive usage observation", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveCognitiveUsageObservation(r.Context(), observation); err != nil {
http.Error(w, "save cognitive usage observation", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/capabilities/health-projection/rebuild", func(w http.ResponseWriter, r *http.Request) {
rebuilt, err := capabilityRepository.RebuildHealthProjection(r.Context())
if err != nil {
http.Error(w, "rebuild capability health projection", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(struct{ Rebuilt int64 }{rebuilt})
})
mux.HandleFunc("POST /v1/focus-bindings", func(w http.ResponseWriter, r *http.Request) {
var binding situation.FocusBinding
if err := json.NewDecoder(r.Body).Decode(&binding); err != nil {
http.Error(w, "invalid focus binding", http.StatusBadRequest)
return
}
if err := repository.Save(r.Context(), binding); err != nil {
http.Error(w, "save focus binding", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("GET /v1/focus", func(w http.ResponseWriter, r *http.Request) {
focus, err := resolveFocus(r)
if err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(focus)
})
mux.HandleFunc("POST /v1/tasks", func(w http.ResponseWriter, r *http.Request) {
var target work.TaskTarget
if err := json.NewDecoder(r.Body).Decode(&target); err != nil {
http.Error(w, "invalid task", 400)
return
}
if err := workRepository.Create(r.Context(), target); err != nil {
http.Error(w, "create task", 500)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/tasks/{id}/commit", func(w http.ResponseWriter, r *http.Request) {
var target work.TaskTarget
if err := json.NewDecoder(r.Body).Decode(&target); err != nil {
http.Error(w, "invalid task commit", 400)
return
}
target.TaskID = work.TaskID(r.PathValue("id"))
revision, err := workRepository.Commit(r.Context(), target)
if err != nil {
http.Error(w, "task revision conflict", http.StatusConflict)
return
}
_ = json.NewEncoder(w).Encode(struct{ Revision work.TaskRevision }{revision})
})
mux.HandleFunc("POST /v1/focus-task-commits", func(w http.ResponseWriter, r *http.Request) {
var request struct {
SessionID situation.SessionID
Workspace situation.WorkspaceID
Target work.TaskTarget
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "invalid guarded commit", http.StatusBadRequest)
return
}
bindings, err := repository.ByRuntime(r.Context(), situation.RuntimeBinding{SessionID: request.SessionID, Workspace: request.Workspace})
if err != nil {
http.Error(w, "load focus", http.StatusInternalServerError)
return
}
focus, err := situationdomain.NewResolver(bindings).Resolve(situation.RuntimeBinding{SessionID: request.SessionID, Workspace: request.Workspace})
if err != nil {
http.Error(w, "focus unavailable", http.StatusConflict)
return
}
if err := situationdomain.ValidateTaskTarget(focus, request.Target); err != nil {
http.Error(w, "target outside resolved focus", http.StatusConflict)
return
}
revision, err := workRepository.Commit(r.Context(), request.Target)
if err != nil {
http.Error(w, "task revision conflict", http.StatusConflict)
return
}
_ = json.NewEncoder(w).Encode(struct{ Revision work.TaskRevision }{revision})
})
log.Printf("core-runtime listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func shouldInvokeHumanResponse(stableConversational bool, identityReply, memoryInstructionReply string) bool {
return !stableConversational && identityReply == "" && memoryInstructionReply == ""
}
func shouldReviewResponse(planningReply string, stableKnowledgeReply, stableConversational bool, identityReply, memoryInstructionReply string, deterministicMemoryReply bool) bool {
return strings.TrimSpace(planningReply) != "" && !stableKnowledgeReply && !stableConversational && identityReply == "" && memoryInstructionReply == "" && !deterministicMemoryReply
}
func shouldRunGeneralResearch(kind interaction.InformationRequestKind) bool {
return kind == "" || kind == interaction.InformationRequestTimeDifference
}
func creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword string, sessionKey []byte, now func() time.Time) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authenticated := identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r))
if !authenticated {
var credentials identity.CreatorCredentials
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&credentials); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
authenticated = identitydomain.CreatorCredentialsMatch(creatorUsername, creatorPassword, credentials)
}
if !authenticated {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
token, session, err := identitydomain.IssueCreatorSession(sessionKey, now(), 8*time.Hour)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Token string
Session identity.CreatorSession
}{token, session})
}
}
func creatorDatabaseSessionHandler(bootstrapToken, creatorUsername string, repository *postgres.IdentityRepository, sessionKey []byte, now func() time.Time) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r)) {
token, session, err := identitydomain.IssueCreatorSession(sessionKey, now(), 8*time.Hour)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
return
}
writeCreatorSession(w, token, session)
return
}
var credentials identity.CreatorCredentials
decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&credentials); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
account, err := repository.LoadCreatorAccount(r.Context(), credentials.Username)
if err != nil || !identitydomain.VerifyCreatorPassword(account.PasswordHash, credentials.Password) {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
binding, err := repository.LoadBinding(r.Context(), account.ActorID, account.PrincipalID)
if err != nil || !binding.Active {
http.Error(w, "creator authentication denied", http.StatusUnauthorized)
return
}
token, session, err := identitydomain.IssueCreatorSessionForBinding(sessionKey, account.ActorID, account.PrincipalID, now(), 8*time.Hour)
if err != nil {
http.Error(w, "creator session unavailable", http.StatusServiceUnavailable)
return
}
writeCreatorSession(w, token, session)
}
}
func writeCreatorSession(w http.ResponseWriter, token string, session identity.CreatorSession) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(struct {
Token string
Session identity.CreatorSession
}{token, session})
}
func bootstrapCreatorAccount(ctx context.Context, repository *postgres.IdentityRepository, username, passwordPath string) error {
if account, err := repository.LoadCreatorAccount(ctx, username); err == nil {
if account.ActorID == "" || account.PrincipalID == "" {
return errors.New("creator account binding is incomplete")
}
return nil
} else if !errors.Is(unwrapIdentityRepositoryError(err), pgx.ErrNoRows) {
return err
}
password, err := readRequiredSecretFile("CREATOR_PASSWORD", passwordPath)
if err != nil {
return fmt.Errorf("creator account is missing and bootstrap password is unavailable: %w", err)
}
hash, err := identitydomain.HashCreatorPassword(password)
if err != nil {
return fmt.Errorf("hash creator bootstrap password: %w", err)
}
if err := repository.CreateCreatorAccount(ctx, identity.CreatorAccount{Username: username, ActorID: "domovoy-creator-actor", PrincipalID: "domovoy-creator-principal", PasswordHash: hash}); err != nil {
return fmt.Errorf("create creator account: %w", err)
}
return nil
}
func unwrapIdentityRepositoryError(err error) error {
for err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return pgx.ErrNoRows
}
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
break
}
err = unwrapped
}
return err
}
func bearerToken(r *http.Request) string {
value := r.Header.Get("Authorization")
if !strings.HasPrefix(value, "Bearer ") {
return ""
}
return strings.TrimPrefix(value, "Bearer ")
}
func readRequiredSecret(name string) (string, error) {
direct, directSet := os.LookupEnv(name)
fileName, fileSet := os.LookupEnv(name + "_FILE")
if directSet && fileSet {
return "", fmt.Errorf("%s and %s_FILE cannot both be configured", name, name)
}
if fileSet {
if fileName == "" {
return "", fmt.Errorf("%s_FILE is empty", name)
}
contents, err := os.ReadFile(fileName)
if err != nil {
return "", fmt.Errorf("read %s_FILE: %w", name, err)
}
return normalizeSecretFile(name, contents)
}
if direct == "" {
return "", fmt.Errorf("%s is required", name)
}
return direct, nil
}
func readRequiredSecretFile(name, path string) (string, error) {
contents, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read %s secret file: %w", name, err)
}
return normalizeSecretFile(name, contents)
}
func normalizeSecretFile(name string, contents []byte) (string, error) {
value := strings.TrimRight(string(contents), "\r\n")
if strings.ContainsAny(value, "\r\n") {
return "", fmt.Errorf("%s secret file must contain one line", name)
}
if value == "" {
return "", fmt.Errorf("%s is required", name)
}
return value, nil
}
func readCredentialRefSecret(name, refName, fileName string) (string, error) {
if _, directSet := os.LookupEnv(name); directSet {
return "", fmt.Errorf("%s direct environment secret is forbidden; use the standard secret file", name)
}
ref := os.Getenv(refName)
if ref == "" {
return "", fmt.Errorf("%s is required", refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{
CredentialRef: secretcontracts.CredentialRef(ref),
Purpose: secretcontracts.SecretPurposeDatabasePassword,
Consumer: secretcontracts.RuntimeConsumerCore,
Path: fileName,
})
}
func envOr(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func envInt(name string, fallback int) int {
value, err := strconv.Atoi(os.Getenv(name))
if err != nil || value < 1 {
return fallback
}
return value
}
func hasCapabilityHint(hints []string, wanted string) bool {
for _, hint := range hints {
if hint == wanted {
return true
}
}
return false
}
func renderResearchReply(query string, result epistemics.ResearchResult, researchErr error) (string, bool) {
if researchErr != nil {
return "Сейчас не удалось получить данные из подключённого источника. Попробую снова через доступный маршрут.", false
}
lines := make([]string, 0, 2)
for _, finding := range result.Findings {
if finding.State != epistemics.FindingSupported || strings.TrimSpace(finding.Excerpt.Text) == "" || strings.TrimSpace(finding.Source.Locator) == "" {
continue
}
if !researchFindingMatchesIntent(query, finding) {
continue
}
excerpt := humanResearchExcerpt(query, finding.Excerpt.Text)
limit := 180
if len(lines) > 0 {
limit = 100
}
if len([]rune(excerpt)) > limit {
excerpt = string([]rune(excerpt)[:limit]) + "…"
}
lines = append(lines, excerpt+"\nИсточник: "+researchSourceLabel(finding))
// A nearest-flight request needs one route-relevant schedule result.
// Appending weaker matches makes unrelated category pages look like
// alternatives even after the relevance check has accepted the route.
if nearestFlightQuery(query) {
break
}
if len(lines) == 2 {
break
}
}
if len(lines) == 0 {
if identifiers := requestedResearchIdentifiers(query); len(identifiers) > 0 {
return "Не нашёл проверяемых данных о «" + strings.Join(identifiers, "», «") + "». Возможно, название написано иначе; пришлите ссылку или уточните написание, и я проверю.", false
}
return "Проверяемых данных пока не найдено. Могу повторить поиск или уточнить, что именно проверить.", false
}
if isInternetStatusQuery(query) {
return "Интернет работает: Домовой получил ответ от внешнего источника.", true
}
if nearestFlightQuery(query) && !containsFlightTimeEvidence(strings.Join(lines, " ")) {
return "Нашёл расписание по маршруту, но точное время ближайшего вылета в доступных фрагментах не указано. Откройте источник и выберите ближайший рейс:\n\n" + strings.Join(lines, "\n\n"), true
}
if trainScheduleQuery(query) && !containsScheduleTimeEvidence(strings.Join(lines, " ")) {
return "Нашёл страницу расписания по маршруту, но в доступных фрагментах нет точного времени отправления:\n\n" + strings.Join(lines, "\n\n"), true
}
return "Нашёл проверяемые данные:\n\n" + strings.Join(lines, "\n\n"), true
}
func isInternetStatusQuery(query string) bool {
lower := strings.ToLower(query)
return (strings.Contains(lower, "интернет") || strings.Contains(lower, "сет")) &&
(strings.Contains(lower, "работает") || strings.Contains(lower, "доступ") || strings.Contains(lower, "проверь") || strings.Contains(lower, "связ"))
}
func nearestFlightQuery(query string) bool {
lower := strings.ToLower(query)
return strings.Contains(lower, "ближ") && (strings.Contains(lower, "рейс") || strings.Contains(lower, "самолет") || strings.Contains(lower, "самолёт") || strings.Contains(lower, "вылет"))
}
// researchFindingMatchesIntent is a final relevance check over evidence
// already obtained by the read-only research route. It prevents unrelated
// search-engine snippets from becoming a human-facing answer when a search
// term has a common non-travel meaning.
func researchFindingMatchesIntent(query string, finding epistemics.ResearchFinding) bool {
if !findingContainsRequestedIdentifier(query, finding) {
return false
}
if ticketSearchQuery(query) && !ticketFindingMatchesDestination(query, finding) {
return false
}
if distanceResearchQuery(query) && !containsDistanceEvidence(finding.Excerpt.Text) {
return false
}
if localPlaceSearchQuery(query) && !containsLocalPlaceEvidence(finding.Excerpt.Text) {
return false
}
if !nearestFlightQuery(query) {
return true
}
displayedExcerpt := strings.ToLower(humanResearchExcerpt(query, finding.Excerpt.Text))
travelEvidence := strings.Contains(displayedExcerpt, "рейс") ||
strings.Contains(displayedExcerpt, "вылет") ||
strings.Contains(displayedExcerpt, "авиабил") ||
strings.Contains(displayedExcerpt, "аэропорт") ||
strings.Contains(displayedExcerpt, "самолет") ||
strings.Contains(displayedExcerpt, "самолёт") ||
strings.Contains(displayedExcerpt, "расписан")
if !travelEvidence {
return false
}
routeTerms := nearestFlightRouteTerms(query)
if len(routeTerms) < 2 {
return true
}
// A route-looking URL or page title alone is not enough for a human-facing
// answer: the displayed excerpt itself must support both endpoints.
excerptEvidence := strings.ToLower(finding.Excerpt.Text)
for _, term := range routeTerms {
if !strings.Contains(excerptEvidence, term) {
return false
}
}
return true
}
func localPlaceSearchQuery(query string) bool {
lower := strings.ToLower(query)
return (strings.Contains(lower, "ресторан") || strings.Contains(lower, "кафе")) &&
(strings.Contains(lower, "адрес") || strings.Contains(lower, "отзыв") || strings.Contains(lower, "меню"))
}
func containsLocalPlaceEvidence(value string) bool {
lower := strings.ToLower(value)
for _, term := range []string{"ресторан", "кафе", "адрес", "меню", "отзыв", "кухн"} {
if strings.Contains(lower, term) {
return true
}
}
return false
}
func distanceResearchQuery(query string) bool {
lower := strings.ToLower(query)
return strings.Contains(lower, "расстоян") || strings.Contains(lower, "километр")
}
func containsDistanceEvidence(value string) bool {
return regexp.MustCompile(`(?i)\b\d[\d\s]*\s*(?:км|километр)`).MatchString(value)
}
func ticketSearchQuery(query string) bool {
return interactiondomain.TicketSearchDestination(query) != ""
}
func ticketFindingMatchesDestination(query string, finding epistemics.ResearchFinding) bool {
destination := strings.ToLower(interactiondomain.TicketSearchDestination(query))
if destination == "" {
return true
}
// The source title or URL may describe a category; human-facing proof must
// mention the requested destination in the actual displayed excerpt.
return strings.Contains(strings.ToLower(finding.Excerpt.Text), destination)
}
// findingContainsRequestedIdentifier prevents a generic landing page from
// being presented as a result for a concrete model, article or brand request.
// Identifiers are detected by their syntax, not by a catalogue of providers or
// entities, so the rule stays valid as user vocabulary changes.
func findingContainsRequestedIdentifier(query string, finding epistemics.ResearchFinding) bool {
identifiers := requestedResearchIdentifiers(query)
if len(identifiers) == 0 {
return true
}
evidence := strings.ToLower(strings.Join([]string{finding.Source.Title, finding.Source.Locator, finding.Excerpt.Text}, " "))
for _, identifier := range identifiers {
if !strings.Contains(evidence, strings.ToLower(identifier)) {
return false
}
}
return true
}
func requestedResearchIdentifiers(query string) []string {
rawIdentifiers := regexp.MustCompile(`(?i)[a-z0-9][a-z0-9._-]{3,}`).FindAllString(query, -1)
identifiers := make([]string, 0, len(rawIdentifiers))
exact := make([]string, 0, len(rawIdentifiers))
for _, identifier := range rawIdentifiers {
if researchDateIdentifier(identifier) {
continue
}
identifiers = append(identifiers, identifier)
if strings.ContainsAny(identifier, "._-") || strings.IndexFunc(identifier, func(r rune) bool { return r >= '0' && r <= '9' }) >= 0 {
exact = append(exact, identifier)
}
}
if len(exact) > 0 {
return exact
}
return identifiers
}
func researchDateIdentifier(value string) bool {
return regexp.MustCompile(`^\d{1,2}[.-]\d{1,2}[.-]\d{2,4}$`).MatchString(value) ||
regexp.MustCompile(`^\d{4}[.-]\d{1,2}[.-]\d{1,2}$`).MatchString(value)
}
// nearestFlightRouteTerms retains only the two endpoint-shaped terms from a
// structured flight search. It has no city or provider vocabulary: endpoint
// relevance is determined from the request itself.
func nearestFlightRouteTerms(query string) []string {
fields := regexp.MustCompile(`[\p{L}]{4,}`).FindAllString(strings.ToLower(query), -1)
ignored := []string{"ближай", "авиарейс", "сегодня", "самолет", "самолёт", "вылет", "рейс", "пункт", "отправлен"}
terms := make([]string, 0, 2)
for _, field := range fields {
skip := false
for _, prefix := range ignored {
if strings.HasPrefix(field, prefix) {
skip = true
break
}
}
if skip {
continue
}
prefixLength := 4
if len([]rune(field)) < prefixLength {
prefixLength = len([]rune(field))
}
term := string([]rune(field)[:prefixLength])
already := false
for _, current := range terms {
if current == term {
already = true
break
}
}
if !already {
terms = append(terms, term)
}
}
return terms
}
func containsFlightTimeEvidence(value string) bool {
return regexp.MustCompile(`(?m)\b(?:[01]?\d|2[0-3]):[0-5]\d\b`).MatchString(value)
}
func humanResearchExcerpt(query, raw string) string {
text := strings.Join(strings.Fields(strings.TrimSpace(raw)), " ")
if text == "" {
return text
}
parts := strings.Split(text, " — ")
if len(parts) < 2 {
return text
}
lowerQuery := strings.ToLower(query)
for index := len(parts) - 1; index >= 0; index-- {
candidate := strings.TrimSpace(parts[index])
lower := strings.ToLower(candidate)
if len([]rune(candidate)) >= 24 && (strings.Contains(lower, "сейчас") || strings.Contains(lower, "температур") || strings.Contains(lower, "цена") || strings.Contains(lower, "стоим") || strings.Contains(lower, "рейс") || strings.Contains(lower, "вылет") || strings.Contains(lowerQuery, "найди")) {
return candidate
}
}
return text
}
func trainScheduleQuery(query string) bool {
lower := strings.ToLower(query)
return strings.Contains(lower, "поезд") && strings.Contains(lower, "время отправления")
}
func containsScheduleTimeEvidence(value string) bool {
return regexp.MustCompile(`\b(?:[01]?\d|2[0-3]):[0-5]\d\b`).MatchString(value)
}
func stableKnowledgeAnswer(answer string) bool {
return strings.TrimSpace(answer) != "" && strings.TrimSpace(answer) != "NEEDS_VERIFIED_RESEARCH"
}
// shouldGenerateNonMutatingTextDirective identifies a request that can be
// completed entirely in the reply. It excludes specialized routes and durable
// work, so cognition can produce content but cannot stand in for an executor.
func shouldGenerateNonMutatingTextDirective(semantic interactiondomain.SemanticResult, routes []chatRoute, helpPath work.HelpPath, creativeRequest bool) bool {
if semantic.Interpretation.SpeechAct != interaction.SpeechActDirective || creativeRequest || helpPath.NeedsDurableWork {
return false
}
// ANSWER and COGNITION are reply-only help paths. They do not execute an
// external action, unlike direct capabilities, gaps or durable work.
if helpPath.Kind != work.HelpPathAnswer && helpPath.Kind != work.HelpPathCognition && helpPath.Kind != "" {
return false
}
return !hasChatRoute(routes, chatRouteResearch) && !hasChatRoute(routes, chatRouteMedia) && !hasChatRoute(routes, chatRouteAgent)
}
func reviewPreservesResearchEvidence(reviewed, draft string) bool {
if strings.Contains(draft, "http://") || strings.Contains(draft, "https://") {
return strings.Contains(reviewed, "http://") || strings.Contains(reviewed, "https://")
}
if strings.Contains(draft, "Источник:") {
return strings.Contains(reviewed, "Источник:")
}
return true
}
// reviewMayReplacePlanningReply keeps ownership of a concrete capability
// result with that capability. A review may improve source-backed research,
// but must not turn an honest no-result into an invented clarification.
func reviewMayReplacePlanningReply(stableSemanticReply, stableKnowledgeReply, researchAttempted, researchBacked bool, reviewed, draft string) bool {
// A stable-knowledge answer is a model-produced draft rather than an owned
// capability result. It therefore goes through a second cognitive pass for
// conversational quality, while identity, memory, media and action outcomes
// remain immutable at this boundary.
if stableKnowledgeReply {
return true
}
if stableSemanticReply {
return false
}
if !researchAttempted {
return true
}
return researchBacked && reviewPreservesResearchEvidence(reviewed, draft)
}
func humanMemoryInventory(history []interaction.HumanUtterance, assistantName string) string {
rows := make([]string, 0, 4)
if assistantName != "" {
rows = append(rows, "| Имя помощника | "+assistantName+" |")
}
latestPreferenceAt := int64(-1)
latestPreference := ""
for _, utterance := range history {
message := strings.TrimSpace(utterance.Content)
lower := strings.ToLower(message)
if (strings.Contains(lower, "нравится") || strings.Contains(lower, "люблю")) && utterance.ReceivedAtUnixMillis >= latestPreferenceAt {
latestPreferenceAt = utterance.ReceivedAtUnixMillis
latestPreference = strings.TrimSpace(message)
for _, prefix := range []string{"запомни:", "запомни", "помни:"} {
if strings.HasPrefix(strings.ToLower(latestPreference), prefix) {
latestPreference = strings.TrimSpace(latestPreference[len(prefix):])
break
}
}
}
}
if latestPreference != "" {
rows = append(rows, "| Предпочтение пользователя | "+latestPreference+" |")
}
if len(rows) == 0 {
return "| Ячейка памяти | Значение |\n|---|---|\n| — | Сохранённых пользовательских предпочтений пока не найдено. |"
}
return "| Ячейка памяти | Значение |\n|---|---|\n" + strings.Join(rows, "\n")
}
func latestAssistantName(history []interaction.HumanUtterance) string {
latestAt := int64(-1)
latestName := ""
for _, utterance := range history {
if name := interactiondomain.PreferredAssistantName(utterance.Content); name != "" && utterance.ReceivedAtUnixMillis >= latestAt {
latestAt = utterance.ReceivedAtUnixMillis
latestName = name
}
}
return latestName
}
func researchSourceLabel(finding epistemics.ResearchFinding) string {
title := strings.TrimSpace(finding.Source.Title)
locator := strings.TrimSpace(finding.Source.Locator)
publisher := strings.TrimSpace(finding.Source.Publisher)
if strings.Contains(locator, "duckduckgo.com/html") || strings.Contains(locator, "bing.com/search") {
locator = ""
}
if title != "" && publisher != "" {
if locator != "" {
return publisher + " (" + locator + ")"
}
return publisher
}
if title != "" {
return title
}
if publisher != "" {
return publisher
}
if locator != "" {
return locator
}
return "подключённый источник"
}
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
if inventoryErr != nil {
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
}
if len(candidates) == 0 {
return "В реестре пока нет подтверждённых подключённых моделей. Встроенный локальный канал Домового доступен всегда; внешние модели добавляются через настройки подключения. Ключи и пароли в чат не выводятся."
}
type inventoryEntry struct {
name string
status string
rank int
seenAt int
}
entries := make([]inventoryEntry, 0, len(candidates))
byName := make(map[string]int, len(candidates))
for _, candidate := range candidates {
modelName := string(candidate.Model.ProviderModelID)
if modelName == "" {
continue
}
status := conciseCognitiveInventoryStatus(candidate)
rank := cognitiveInventoryRank(candidate)
if index, exists := byName[modelName]; exists {
if rank < entries[index].rank {
entries[index].status = status
entries[index].rank = rank
}
continue
}
byName[modelName] = len(entries)
entries = append(entries, inventoryEntry{name: modelName, status: status, rank: rank, seenAt: len(entries)})
}
sort.SliceStable(entries, func(left, right int) bool {
if entries[left].rank != entries[right].rank {
return entries[left].rank < entries[right].rank
}
return entries[left].seenAt < entries[right].seenAt
})
lines := make([]string, 0, len(entries)+2)
lines = append(lines, "Доступные модели:")
unconfirmed := 0
for _, entry := range entries {
if entry.rank >= 3 {
unconfirmed++
continue
}
lines = append(lines, "- "+entry.name+" — "+entry.status)
}
if len(lines) == 1 {
return "Подтверждённых доступных моделей сейчас нет."
}
if unconfirmed > 0 {
lines = append(lines, fmt.Sprintf("Ещё %d моделей требуют проверки.", unconfirmed))
}
return strings.Join(lines, "\n")
}
func conciseCognitiveInventoryStatus(candidate capability.ResolvedCognitiveBinding) string {
if candidate.Provider.Health == capability.HealthAvailable && candidate.Model.Health == capability.HealthAvailable {
if candidate.Binding.Activation == capability.ActivationActive {
return "активна"
}
return "доступна"
}
if candidate.Provider.Health == capability.HealthDegraded || candidate.Model.Health == capability.HealthDegraded {
return "работает с ограничениями"
}
return "не подтверждена"
}
func cognitiveInventoryRank(candidate capability.ResolvedCognitiveBinding) int {
if candidate.Provider.Health == capability.HealthAvailable && candidate.Model.Health == capability.HealthAvailable {
if candidate.Binding.Activation == capability.ActivationActive {
return 0
}
return 1
}
if candidate.Provider.Health == capability.HealthDegraded || candidate.Model.Health == capability.HealthDegraded {
return 2
}
return 3
}
func cognitiveInventoryStatus(candidate capability.ResolvedCognitiveBinding) string {
if candidate.Provider.Health == capability.HealthAvailable && candidate.Model.Health == capability.HealthAvailable {
if candidate.Binding.Activation == capability.ActivationActive {
return "активна"
}
return "доступна, не активирована для автоматического выбора"
}
if candidate.Provider.Health == capability.HealthDegraded || candidate.Model.Health == capability.HealthDegraded {
return "работает с ограничениями"
}
return "недоступна или не подтверждена"
}
func cognitiveInventoryPricing(provider capability.ProviderInstance) string {
switch provider.BillingMode {
case capability.BillingFree:
return "бесплатная"
case capability.BillingPerInput:
return "платная: стоимость входа указана в реестре"
case capability.BillingPerOutput:
return "платная: стоимость выхода указана в реестре"
case capability.BillingPerRequest:
return "платная: стоимость запроса указана в реестре"
default:
return "стоимость не подтверждена"
}
}
func humanAuthMode(mode capability.AuthMode) string {
switch mode {
case capability.AuthKey:
return "API-ключ"
case capability.AuthBearer:
return "Bearer-токен"
case capability.AuthUsernamePassword:
return "логин и пароль"
case capability.AuthOAuth:
return "OAuth"
case capability.AuthSecretFile:
return "защищённый файл секрета"
default:
return "защищённый способ"
}
}
func humanCognitiveChannelReply(kind interaction.InformationRequestKind, act interaction.SpeechAct, health capability.Health, healthErr error) string {
if healthErr != nil {
return "Не удалось проверить канал. Домовой остаётся на связи."
}
switch health.State {
case capability.HealthAvailable:
return "Канал мышления доступен."
case capability.HealthDegraded:
return "Канал мышления работает с ограничениями."
case capability.HealthUnavailable:
return "Канал мышления недоступен. Домовой остаётся на связи."
default:
return "Состояние канала пока не подтверждено. Домовой остаётся на связи."
}
}
// humanCurrentCognitiveModelReply answers about the route that produced this
// reply. It intentionally does not infer a provider/model name from health:
// that would claim a model was used when this direct status path did not call it.
func humanCurrentCognitiveModelReply(health capability.Health, healthErr error) string {
base := "Этот ответ сформирован встроенным режимом Домового; внешняя модель для него не использовалась."
if healthErr != nil {
return base + " Состояние канала мышления сейчас не удалось проверить."
}
switch health.State {
case capability.HealthAvailable:
return base + " Канал мышления доступен для следующих задач."
case capability.HealthDegraded:
return base + " Канал мышления работает с ограничениями."
case capability.HealthUnavailable:
return base + " Канал мышления сейчас недоступен."
default:
return base + " Состояние канала мышления пока не подтверждено."
}
}
func humanCognitiveDegradedReply(message string) string {
if strings.TrimSpace(message) == "" {
return "Сейчас не удалось обработать запрос."
}
return "Не удалось получить проверяемые данные. Ответ не выдумываю."
}
func humanCapabilityList(kinds []capability.CapabilityKind) string {
labels := make([]string, 0, len(kinds))
for _, kind := range kinds {
switch kind {
case capability.CapabilityKindScheduleReminder:
labels = append(labels, "создавать напоминания")
case capability.CapabilityKindCognitiveReasoning:
labels = append(labels, "отвечать через когнитивный gateway")
default:
labels = append(labels, strings.ToLower(strings.ReplaceAll(string(kind), "_", " ")))
}
}
return strings.Join(labels, ", ")
}
func toCapabilityKinds(values []string) []capability.CapabilityKind {
result := make([]capability.CapabilityKind, 0, len(values))
for _, value := range values {
kind := capability.CapabilityKind(strings.TrimSpace(value))
if kind == "" {
continue
}
result = append(result, kind)
}
return result
}
func delegationAttentionRef(id work.DelegationID) string {
return "work:delegation:" + string(id)
}
func continueDelegationFromHumanResponse(ctx context.Context, repository *postgres.WorkRepository, response continuity.HumanResponse, now time.Time) (bool, error) {
if response.Operation != continuity.ResponseContinue || !strings.HasPrefix(response.TargetExternalRef, "work:delegation:") {
return false, nil
}
delegationID := work.DelegationID(strings.TrimPrefix(response.TargetExternalRef, "work:delegation:"))
delegation, err := repository.LoadDelegation(ctx, delegationID)
if err != nil {
return false, err
}
run, err := repository.LoadAgentRun(ctx, delegation.LatestRunID)
if err != nil {
return false, err
}
checkpoint, err := repository.LatestAgentCheckpoint(ctx, delegationID)
if err != nil {
return false, err
}
updatedDelegation, resumedRun, err := workdomain.ResumeDelegationRun(delegation, run, checkpoint, now)
if err != nil {
return false, err
}
if err := repository.SaveResumedRun(ctx, updatedDelegation, resumedRun); err != nil {
return false, err
}
return true, nil
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func evidenceRefsToEpistemics(values []work.EvidenceRef) []epistemics.EvidenceRef {
result := make([]epistemics.EvidenceRef, 0, len(values))
for _, value := range values {
if strings.TrimSpace(string(value)) == "" {
continue
}
result = append(result, epistemics.EvidenceRef(value))
}
return result
}