1390 lines
64 KiB
Go
1390 lines
64 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"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"
|
||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||
"git.cin.su/m/Assistant/runtime/health"
|
||
"github.com/jackc/pgx/v5"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
const maxCreatorCredentialsBytes = 4 << 10
|
||
|
||
func main() {
|
||
addr := os.Getenv("LISTEN_ADDR")
|
||
if addr == "" {
|
||
addr = ":8080"
|
||
}
|
||
bootstrapToken, err := readRequiredSecret("CREATOR_BOOTSTRAP_TOKEN")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
sessionKeyValue, err := readRequiredSecret("CREATOR_SESSION_SIGNING_KEY")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
creatorPassword, err := readRequiredSecret("CREATOR_PASSWORD")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
sessionKey := []byte(sessionKeyValue)
|
||
creatorUsername := os.Getenv("CREATOR_USERNAME")
|
||
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")
|
||
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()
|
||
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF")
|
||
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()
|
||
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: 20 * time.Second})
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
|
||
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}
|
||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", 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"),
|
||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
|
||
}})
|
||
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", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
|
||
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("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
|
||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
|
||
return
|
||
}
|
||
semantic, err := interactiondomain.InterpretUtterance(utterance, projects, previousFocus, now)
|
||
if err != nil {
|
||
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
|
||
return
|
||
}
|
||
cognitiveAttempted := false
|
||
cognitiveAvailable := true
|
||
if shouldRequestBroaderInterpretation(semantic) {
|
||
cognitiveAttempted = true
|
||
projectNames := make([]string, 0, len(semantic.Projects))
|
||
for _, ref := range semantic.Projects {
|
||
if ref.ProjectDisplayName != "" {
|
||
projectNames = append(projectNames, ref.ProjectDisplayName)
|
||
}
|
||
}
|
||
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, utterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
|
||
if candidate, candidateErr := interactionCandidateFromGateway(utterance.ID, response.Candidate, now); candidateErr == nil {
|
||
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, candidate, previousFocus)
|
||
} else {
|
||
cognitiveAvailable = false
|
||
}
|
||
} else {
|
||
cognitiveAvailable = 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
|
||
}
|
||
responseResult, responseErr := responseClient.Respond(r.Context(), r.Header.Get("Authorization"), continuity.HumanResponseRequest{
|
||
SourceUtteranceID: string(utterance.ID),
|
||
SourceIntentID: string(intent.ID),
|
||
Message: utterance.Content,
|
||
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 {
|
||
http.Error(w, "save project focus", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
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
|
||
}
|
||
var planningReply string
|
||
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 {
|
||
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: utterance.Content,
|
||
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)
|
||
if responseErr == nil && responseResult.Handled {
|
||
helpPath = work.HelpPath{}
|
||
}
|
||
switch semantic.InformationRequestKind {
|
||
case interaction.InformationRequestCapabilityInventory:
|
||
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
|
||
if err == nil && len(kinds) > 0 {
|
||
planningReply = "Сейчас у меня подключены реальные capabilities: " + humanCapabilityList(kinds) + ". Для рассуждений я использую отдельный Cognitive Gateway/Router, если локальный CANARY доступен. Возможности вроде отправки почты пока будут фиксироваться как capability gap, а не как выполненное действие."
|
||
} else {
|
||
planningReply = "Я умею записывать диалог и намерения, работать с текущим контекстом, строить планы и обращаться к Cognitive Gateway при доступности локального CANARY. Реальные action-capabilities подключаются через registry; если capability нет, я фиксирую gap."
|
||
}
|
||
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.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery:
|
||
health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal"))
|
||
planningReply = humanCognitiveChannelReply(semantic.InformationRequestKind, semantic.Interpretation.SpeechAct, health, healthErr)
|
||
needsCognition = false
|
||
}
|
||
if helpPath.Reply != "" {
|
||
planningReply = helpPath.Reply
|
||
needsCognition = false
|
||
}
|
||
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
|
||
planningReply = humanCognitiveDegradedReply(utterance.Content)
|
||
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
|
||
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 сейчас недоступен, поэтому я сохранил план без ложного запуска."
|
||
}
|
||
}
|
||
response := interaction.ChatMessageResponse{
|
||
ConversationID: conversation.ID,
|
||
UtteranceID: utterance.ID,
|
||
OriginalIntentID: intent.ID,
|
||
SpeechAct: semantic.Interpretation.SpeechAct,
|
||
InterpretationState: utterance.InterpretationState,
|
||
NeedsCognition: needsCognition,
|
||
FallbackText: semantic.Reply,
|
||
ActiveProjectName: activeFocusName,
|
||
ProjectProvenance: activeFocusProvenance,
|
||
Clarification: semantic.Interpretation.Clarification,
|
||
WorkPlanned: workPlanned,
|
||
PlanningReply: planningReply,
|
||
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,
|
||
}
|
||
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("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
|
||
}
|
||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
||
if err != nil {
|
||
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/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.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
|
||
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"
|
||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
|
||
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.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 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 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)
|
||
}
|
||
direct = strings.TrimRight(string(contents), "\r\n")
|
||
if strings.ContainsAny(direct, "\r\n") {
|
||
return "", fmt.Errorf("%s_FILE must contain one line", name)
|
||
}
|
||
}
|
||
if direct == "" {
|
||
return "", fmt.Errorf("%s is required", name)
|
||
}
|
||
return direct, nil
|
||
}
|
||
|
||
func readCredentialRefSecret(name, refName string) (string, error) {
|
||
if _, directSet := os.LookupEnv(name); directSet {
|
||
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s_FILE", name, name)
|
||
}
|
||
fileName := os.Getenv(name + "_FILE")
|
||
if fileName == "" {
|
||
return "", fmt.Errorf("%s_FILE is required", 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 humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
|
||
if inventoryErr != nil {
|
||
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
|
||
}
|
||
if len(candidates) == 0 {
|
||
return "В реестре пока нет подтверждённых подключённых моделей. Встроенный локальный канал Домового доступен всегда; внешние модели добавляются через настройки подключения. Ключи и пароли в чат не выводятся."
|
||
}
|
||
lines := make([]string, 0, len(candidates)+1)
|
||
lines = append(lines, "Подключённые модели из реестра:")
|
||
for _, candidate := range candidates {
|
||
modelName := string(candidate.Model.ProviderModelID)
|
||
if modelName == "" {
|
||
modelName = "имя модели не подтверждено"
|
||
}
|
||
status := cognitiveInventoryStatus(candidate)
|
||
pricing := cognitiveInventoryPricing(candidate.Provider)
|
||
details := []string{status, "тип " + string(candidate.Provider.Kind), "политика " + string(candidate.Binding.DataPolicy)}
|
||
if pricing != "" {
|
||
details = append(details, pricing)
|
||
}
|
||
if candidate.Provider.AuthMode != "" && candidate.Provider.AuthMode != capability.AuthNone {
|
||
details = append(details, "доступ "+humanAuthMode(candidate.Provider.AuthMode))
|
||
}
|
||
lines = append(lines, "- "+modelName+" ("+strings.Join(details, "; ")+")")
|
||
}
|
||
lines = append(lines, "Секреты, ключи и пароли намеренно не показываются.")
|
||
return strings.Join(lines, "\n")
|
||
}
|
||
|
||
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 {
|
||
recoveryRequested := kind == interaction.InformationRequestCognitiveRecovery || act == interaction.SpeechActRequestToInvestigate || act == interaction.SpeechActDirective
|
||
if healthErr != nil {
|
||
if recoveryRequested {
|
||
return "Я не вижу подтверждённого состояния локального канала. Домовой всё равно рядом: опишите, что изменилось, и я помогу подготовить аварийную настройку."
|
||
}
|
||
return "Я не вижу подтверждённого состояния локального канала, но Домовой работает. Можно продолжать разговор и проверить канал ещё раз."
|
||
}
|
||
var observation string
|
||
switch health.State {
|
||
case capability.HealthAvailable:
|
||
observation = "Основной локальный канал сейчас доступен."
|
||
case capability.HealthDegraded:
|
||
observation = "Основной локальный канал сейчас работает с ограничениями."
|
||
case capability.HealthUnavailable:
|
||
observation = "Основной локальный канал сейчас недоступен."
|
||
default:
|
||
observation = "Состояние основного локального канала пока не подтверждено."
|
||
}
|
||
if recoveryRequested {
|
||
if health.State == capability.HealthUnavailable {
|
||
return observation + " Домовой жив: начну с проверки встроенного маршрута, затем зафиксирую варианты аварийного восстановления без передачи секретов."
|
||
}
|
||
return observation + " Я не меняю модель прямо из разговора: сначала покажу наблюдаемое состояние и подготовлю следующий проверяемый шаг восстановления."
|
||
}
|
||
if health.State == capability.HealthUnavailable {
|
||
if recoveryRequested {
|
||
return observation + " Домовой не остановился: начну с проверки встроенного маршрута, затем зафиксирую доступные варианты восстановления без передачи секретов."
|
||
}
|
||
return observation + " Но Домовой не остановился: продолжаю принимать сообщения и помогу восстановить локальный канал."
|
||
}
|
||
return observation + " Это наблюдение о готовности capability, а не прямой доступ к модели."
|
||
}
|
||
|
||
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
|
||
}
|