2634 lines
120 KiB
Go
2634 lines
120 KiB
Go
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
|
||
}
|