feat: upgrade Domovoy local cognition and recovery #87
@@ -23,7 +23,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
discovered, probe := registration.Discovery.Discover(r.Context())
|
||||
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: capability.DataPolicyLocalOnly, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
|
||||
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
|
||||
for _, model := range discovered {
|
||||
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type providerRegistration struct {
|
||||
CredentialRef capability.CredentialRef
|
||||
AuthMode capability.AuthMode
|
||||
BillingMode capability.BillingMode
|
||||
DataPolicy capability.DataPolicy
|
||||
Provider advisoryProvider
|
||||
Discovery models.LocalProvider
|
||||
}
|
||||
@@ -39,6 +40,8 @@ type providerRegistration struct {
|
||||
type cognitiveGateway struct {
|
||||
resolver bindingResolver
|
||||
providers map[capability.ProviderKind][]providerRegistration
|
||||
builtinLocal *providerRegistration
|
||||
externalRecovery []providerRegistration
|
||||
now func() time.Time
|
||||
allowCanary bool
|
||||
canaryMaxOutputBytes int
|
||||
@@ -77,7 +80,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)}
|
||||
selected, err := g.resolver.Resolve(r.Context(), requirement)
|
||||
if err != nil {
|
||||
writeGatewayResponse(w, builtinAdvisory(request))
|
||||
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
|
||||
@@ -86,7 +89,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
provider := g.provider(selected)
|
||||
if provider == nil {
|
||||
writeGatewayResponse(w, builtinAdvisory(request))
|
||||
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
||||
@@ -114,7 +117,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
if response.FinishReason != cognitive.FinishCompleted && fallbackEligible(response.FinishReason) {
|
||||
writeGatewayResponse(w, builtinAdvisory(request))
|
||||
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
|
||||
@@ -164,6 +167,43 @@ func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.Gateway
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
|
||||
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
||||
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
|
||||
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
|
||||
}
|
||||
}
|
||||
if request.EgressAcknowledged {
|
||||
if response, ok := g.recoverExternalAdvisory(ctx, request, maxOutputBytes); ok {
|
||||
return response
|
||||
}
|
||||
}
|
||||
return builtinAdvisory(request)
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) recoverExternalAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) (cognitive.GatewayAdvisoryResponse, bool) {
|
||||
for _, candidate := range g.externalRecovery {
|
||||
if candidate.Provider == nil || candidate.Discovery == nil {
|
||||
continue
|
||||
}
|
||||
probeCtx, cancel := context.WithTimeout(ctx, 8*time.Second)
|
||||
modelsFound, probe := candidate.Discovery.Discover(probeCtx)
|
||||
cancel()
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
continue
|
||||
}
|
||||
model := modelsFound[0]
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "recovery-external-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: capability.ProviderInstanceID("recovery-" + string(candidate.ProviderInstanceID)), ModelDescriptorID: capability.ModelDescriptorID("recovery-model-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
|
||||
response, err := candidate.Provider.Advise(ctx, invocation)
|
||||
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
|
||||
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}, true
|
||||
}
|
||||
}
|
||||
return cognitive.GatewayAdvisoryResponse{}, false
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) observe(ctx context.Context, selected capability.ResolvedCognitiveBinding, response cognitive.AdvisoryResponse, started time.Time) {
|
||||
usage := response.Usage
|
||||
observationID, idErr := ids.New()
|
||||
|
||||
@@ -168,6 +168,19 @@ func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("no active binding")}
|
||||
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
|
||||
builtin := providerRegistration{ProviderInstanceID: "local-lifeboat", Kind: "openai-compatible-chat-v1", EndpointRef: "local-lifeboat-v1", Provider: provider}
|
||||
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
||||
response := httptest.NewRecorder()
|
||||
gateway.advisoryHandler(response, request)
|
||||
if response.Code != http.StatusOK || !provider.called || !strings.Contains(response.Body.String(), "локальный ответ") {
|
||||
t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderLossBetweenRoutingAndInvocationFallsBackOnce(t *testing.T) {
|
||||
primary := gatewaySelection()
|
||||
primary.Binding.ID = "gpu-binding"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -36,12 +37,12 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
selected, err := g.resolver.Resolve(r.Context(), requirement)
|
||||
if err != nil {
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
provider := g.provider(selected)
|
||||
if provider == nil {
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{
|
||||
@@ -77,6 +78,22 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cognitive.GatewayInterpretationRequest, maxOutputBytes int) cognitive.GatewayInterpretationResponse {
|
||||
if g.builtinLocal == nil || g.builtinLocal.Provider == nil {
|
||||
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable}
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: interpretationPrompt(request), Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
||||
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
|
||||
if err != nil || response.FinishReason != cognitive.FinishCompleted {
|
||||
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable}
|
||||
}
|
||||
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
|
||||
if !ok {
|
||||
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage}
|
||||
}
|
||||
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage}
|
||||
}
|
||||
|
||||
func selectionObjectiveForInterpretation(request cognitive.GatewayInterpretationRequest) capability.SelectionObjective {
|
||||
if request.RequestedOutcome != "" || request.DeterministicApplicability == cognitive.InterpretationNeedsBroader || request.DeterministicApplicability == cognitive.InterpretationMixedIntent || request.DeterministicApplicability == cognitive.InterpretationLowConfidence {
|
||||
return capability.SelectionQuality
|
||||
|
||||
@@ -40,31 +40,36 @@ func main() {
|
||||
}
|
||||
providers := map[capability.ProviderKind][]providerRegistration{}
|
||||
discoveries := map[string]providerRegistration{}
|
||||
var builtinLocal *providerRegistration
|
||||
externalRecovery := []providerRegistration{}
|
||||
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, Provider: cpuAdapter, Discovery: cpuAdapter}
|
||||
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter}
|
||||
providers[cpu.Kind] = append(providers[cpu.Kind], cpu)
|
||||
discoveries["cpu"] = cpu
|
||||
if lifeboat, enabled, lifeboatErr := newLifeboatProvider(os.Getenv("LOCAL_LIFEBOAT_PROVIDER_URL")); lifeboatErr != nil {
|
||||
log.Fatal(lifeboatErr)
|
||||
} else if enabled {
|
||||
lifeboat.DataPolicy = capability.DataPolicyLocalOnly
|
||||
providers[lifeboat.Kind] = append(providers[lifeboat.Kind], lifeboat)
|
||||
discoveries["lifeboat"] = lifeboat
|
||||
builtinLocal = &lifeboat
|
||||
}
|
||||
gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, Provider: gpuAdapter, Discovery: gpuAdapter}
|
||||
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
|
||||
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
|
||||
discoveries["gpu"] = gpu
|
||||
externalRecovery = append(externalRecovery, gpu)
|
||||
if openAIProvider != nil {
|
||||
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
|
||||
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, Provider: openAIProvider})
|
||||
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
|
||||
}
|
||||
gateway := cognitiveGateway{resolver: resolver, providers: providers, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
|
||||
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
|
||||
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
|
||||
@@ -99,7 +104,7 @@ func refreshProviderDiscoveries(resolver bindingResolver, service discoveryServi
|
||||
Enabled: true,
|
||||
OwnerPrincipalID: service.owner,
|
||||
CapabilityID: creatorAdvisoryCapabilityID,
|
||||
DataPolicy: capability.DataPolicyLocalOnly,
|
||||
DataPolicy: registration.DataPolicy,
|
||||
LimitPolicyRef: "creator-advisory-local-v1",
|
||||
Health: probe.Health,
|
||||
FailureClass: capability.CognitiveFailureType(probe.Failure),
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
|
||||
t.Fatalf("expected /chat status 200, got %d", recorder.Code)
|
||||
}
|
||||
body := recorder.Body.String()
|
||||
for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "версия 1.0.0", "обновлено 21.08.2026", "datetime=\"2026-08-21\""} {
|
||||
for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "build-meta", "версия ", "обновлено ", "datetime=\""} {
|
||||
if !strings.Contains(body, required) {
|
||||
t.Fatalf("/chat is missing human status marker %q", required)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -8,7 +9,9 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -36,6 +39,12 @@ type gateway struct {
|
||||
cognitive *url.URL
|
||||
client *http.Client
|
||||
static http.Handler
|
||||
build buildMetadata
|
||||
}
|
||||
|
||||
type buildMetadata struct {
|
||||
revision string
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
type routeTarget uint8
|
||||
@@ -74,7 +83,20 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets))}), nil
|
||||
updated := time.Now().UTC()
|
||||
if raw := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_TIME")); raw != "" {
|
||||
if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil {
|
||||
updated = parsed.UTC()
|
||||
}
|
||||
}
|
||||
revision := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_REVISION"))
|
||||
if revision == "" {
|
||||
revision = "development"
|
||||
}
|
||||
if len(revision) > 12 {
|
||||
revision = revision[:12]
|
||||
}
|
||||
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil
|
||||
}
|
||||
|
||||
func parseRuntimeURL(rawURL, name string) (*url.URL, error) {
|
||||
@@ -112,11 +134,11 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" {
|
||||
serveEmbeddedHTML(w, "chat.html")
|
||||
g.serveEmbeddedHTML(w, "chat.html")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/admin" {
|
||||
serveEmbeddedHTML(w, "index.html")
|
||||
g.serveEmbeddedHTML(w, "index.html")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/chat" {
|
||||
@@ -127,7 +149,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
g.static.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func serveEmbeddedHTML(w http.ResponseWriter, name string) {
|
||||
func (g *gateway) serveEmbeddedHTML(w http.ResponseWriter, name string) {
|
||||
body, err := staticFiles.ReadFile("static/" + name)
|
||||
if err != nil {
|
||||
http.Error(w, "page unavailable", http.StatusNotFound)
|
||||
@@ -135,6 +157,9 @@ func serveEmbeddedHTML(w http.ResponseWriter, name string) {
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_REVISION__"), []byte(g.build.revision))
|
||||
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_TIME__"), []byte(g.build.updated.Format("02.01.2006 15:04 UTC")))
|
||||
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_DATETIME__"), []byte(g.build.updated.Format(time.RFC3339)))
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
|
||||
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
|
||||
<div class="build-meta" aria-label="Версия и время обновления">
|
||||
<span>версия 1.0.0</span>
|
||||
<time datetime="2026-08-21">обновлено 21.08.2026</time>
|
||||
<span>версия __ASSISTANT_BUILD_REVISION__</span>
|
||||
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
|
||||
</div>
|
||||
<a href="/admin" class="operator-link">Консоль</a>
|
||||
<button id="logout" type="button" aria-label="Выйти">↗</button>
|
||||
|
||||
@@ -141,7 +141,7 @@ services:
|
||||
- --port
|
||||
- "8086"
|
||||
- --model
|
||||
- /models/qwen2.5-0.5b-instruct-q4_k_m.gguf
|
||||
- /models/Qwen3-8B-Q4_K_M.gguf
|
||||
- --alias
|
||||
- domovoy-lifeboat
|
||||
- --jinja
|
||||
@@ -150,9 +150,11 @@ services:
|
||||
- --n-gpu-layers
|
||||
- "0"
|
||||
- --threads
|
||||
- "4"
|
||||
- "32"
|
||||
- --parallel
|
||||
- "1"
|
||||
- --reasoning
|
||||
- "off"
|
||||
volumes:
|
||||
- llm_local_models:/models:ro
|
||||
healthcheck:
|
||||
@@ -170,6 +172,7 @@ services:
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime:8082
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
|
||||
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
|
||||
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
|
||||
healthcheck:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000082-chat-ai-data-safety-guard.json",
|
||||
"task_id": "DEV-000082",
|
||||
"task_file": "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json",
|
||||
"task_id": "DEV-000089",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000085",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
|
||||
"risk_tier": "C",
|
||||
"intent": "Keep Domovoy useful when external or active cognitive bindings are unavailable by invoking the built-in local lifeboat through the Cognitive Gateway and presenting an actionable recovery focus when all cognition fails.",
|
||||
"requirement_ids": ["Built-in local cognition remains available", "Provider loss does not collapse conversation", "Recovery state is explained and actionable"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation_test.go", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing Cognitive Gateway and local provider adapter contracts"], "may_change": []},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["No active binding -> built-in local invocation", "Built-in invocation failure -> recovery-focused degraded response"],
|
||||
"side_effects_and_privileges": ["No new privilege", "Built-in path is local-only and cannot egress externally"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Provider failure remains observable; next request retries resolution and built-in local path"],
|
||||
"assumptions": ["The configured local lifeboat endpoint is part of Domovoy and may be used without external egress acknowledgement"],
|
||||
"acceptance_scenarios": ["Inactive registry with healthy local lifeboat invokes the local adapter", "Interpretation also uses the built-in local path", "All cognition failure returns an actionable recovery response"],
|
||||
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000086",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
|
||||
"risk_tier": "C",
|
||||
"intent": "Replace the unusable tiny lifeboat model with a capable, locally hosted Qwen3 8B quantized baseline while keeping Domovoy runtime on docker.cin.su.",
|
||||
"requirement_ids": ["Built-in local cognition is useful for ordinary dialogue", "Local model remains available without external egress", "Model choice is explicit and replaceable"],
|
||||
"allowed_change_scope": {"modules": ["deployment", "cognitive-worker"], "paths": ["deploy/docker-compose.prod.yml", "dev/tasks/DEV-000086-local-model-baseline.json", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing llama.cpp OpenAI-compatible local endpoint", "existing Cognitive Gateway"], "may_change": []},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Configured model file -> loaded local baseline", "Local model replacement -> restart and health verification"],
|
||||
"side_effects_and_privileges": ["No new privilege", "Model is local-only and mounted read-only into the runtime"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Missing or invalid model keeps the container unhealthy and does not silently fall back to the tiny model"],
|
||||
"assumptions": ["Qwen3-8B-Q4_K_M.gguf is present in the protected Docker model volume on docker.cin.su before rollout"],
|
||||
"acceptance_scenarios": ["The configured lifeboat starts with the named Qwen3 8B model", "The OpenAI-compatible health endpoint is healthy", "No other host is used for Domovoy runtime"],
|
||||
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check", "production health and model endpoint verification"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MODEL_ARTIFACT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000087",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
|
||||
"risk_tier": "D",
|
||||
"intent": "When the selected cognitive route is unavailable, keep Domovoy conversational, diagnose the failure, use the built-in local model, and probe explicitly configured external GPU/HTTP providers through the Cognitive Gateway without exposing credentials or bypassing egress policy.",
|
||||
"requirement_ids": ["Provider loss becomes a recovery objective", "Meaning and task objective guide provider choice", "External egress requires explicit acknowledgement", "Credentials remain opaque secret references"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/recovery.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation_test.go", "cmd/cognitive-worker/recovery_test.go", "dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing typed Cognitive Gateway", "existing local and Ollama adapters", "existing discovery and binding resolver"], "may_change": []},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Selected provider failure -> local built-in attempt", "Local failure -> recovery diagnosis and configured-provider probe", "External probe -> registry discovery only; invocation requires policy acknowledgement"],
|
||||
"side_effects_and_privileges": ["No new privilege", "No arbitrary network scanning", "No credential material in logs or messages"],
|
||||
"persistence_and_migrations": ["Existing discovery registration only; no new persistence"],
|
||||
"failure_retry_restart": ["Bounded sequential probes per request", "No unmanaged background retry loop", "Each new request re-resolves and re-probes"],
|
||||
"assumptions": ["External GPU/HTTP endpoints are explicitly configured by deployment operators", "Core Runtime remains the owner of activation and selection"],
|
||||
"acceptance_scenarios": ["Inactive registry with healthy local model still produces an intelligent local response", "Interpretation uses the same local recovery path", "External endpoints are discovered but never invoked without egress acknowledgement", "All failures yield actionable recovery text instead of a generic unavailable error"],
|
||||
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check", "production recovery request and provider health verification"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000088",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
|
||||
"risk_tier": "B",
|
||||
"intent": "Expose self-describing build revision and runtime update time in the Domovoy chat without requiring a human status query.",
|
||||
"requirement_ids": ["Human can identify the running build", "Displayed update time reflects the running container", "No internal identifiers are exposed in the chat UI"],
|
||||
"allowed_change_scope": {"modules": ["creator-web", "deployment"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/tasks/DEV-000088-runtime-build-status.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_provider_adapters": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing source revision build argument"], "may_change": []},
|
||||
"semantic_owner": "creator-web",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Container startup -> build metadata visible in chat"],
|
||||
"side_effects_and_privileges": ["No new privilege", "No user or provider secrets displayed"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Missing revision is displayed as development/unknown; startup time remains observable"],
|
||||
"assumptions": ["The production compose runner supplies ASSISTANT_SOURCE_REVISION for the running revision"],
|
||||
"acceptance_scenarios": ["Chat shows revision and update time without a manual question", "HTML contains no internal provider or session identifiers", "Tests remain deterministic outside production"],
|
||||
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "git diff --check", "production page verification"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000089",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "9f717d720e217afd4cc87a4fbf851050d466d8cd",
|
||||
"risk_tier": "E",
|
||||
"intent": "Integrate the Domovoy local-model baseline, cognitive recovery orchestrator, and self-describing runtime status into one reviewed production change.",
|
||||
"requirement_ids": ["Built-in cognition remains useful", "Provider loss triggers recovery", "Production runtime identifies its revision and update time"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker", "creator-web", "deployment", "development-governance"], "paths": ["cmd/cognitive-worker/discovery.go", "cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/server.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json", "dev/tasks/DEV-000086-local-model-baseline.json", "dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json", "dev/tasks/DEV-000088-runtime-build-status.json", "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing cognitive gateway", "existing creator-web runtime", "existing production compose runner"], "may_change": []},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Old active task -> integrated reviewed task", "Old local baseline -> Qwen3-8B baseline", "Unavailable active binding -> built-in and configured external recovery"],
|
||||
"side_effects_and_privileges": ["No new privilege", "External invocation remains acknowledgement-gated", "No secrets in source or UI"],
|
||||
"persistence_and_migrations": ["No persistence or migration"],
|
||||
"failure_retry_restart": ["Existing auto-update runner deploys the merged main revision", "Missing model artifact leaves the lifeboat unhealthy rather than silently selecting the tiny model"],
|
||||
"assumptions": ["Qwen3-8B-Q4_K_M.gguf is present in the protected Docker model volume on docker.cin.su"],
|
||||
"acceptance_scenarios": ["CI accepts exact task scope", "Production chat shows running revision and update time", "Domovoy can answer through built-in local cognition when registry activation is unavailable"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check", "production health and revision verification"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MODEL_ARTIFACT"]
|
||||
}
|
||||
Reference in New Issue
Block a user