This commit is contained in:
@@ -47,6 +47,12 @@ func main() {
|
||||
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", 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 {
|
||||
providers[lifeboat.Kind] = append(providers[lifeboat.Kind], lifeboat)
|
||||
discoveries["lifeboat"] = lifeboat
|
||||
}
|
||||
gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -174,3 +180,20 @@ func envOr(name, fallback string) string {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func newLifeboatProvider(rawURL string) (providerRegistration, bool, error) {
|
||||
if rawURL == "" {
|
||||
return providerRegistration{}, false, nil
|
||||
}
|
||||
adapter, err := models.NewOpenAICompatibleLocalAdapter(rawURL, nil)
|
||||
if err != nil {
|
||||
return providerRegistration{}, false, err
|
||||
}
|
||||
return providerRegistration{
|
||||
ProviderInstanceID: "local-lifeboat",
|
||||
Kind: "openai-compatible-chat-v1",
|
||||
EndpointRef: "local-lifeboat-v1",
|
||||
Provider: adapter,
|
||||
Discovery: adapter,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
|
||||
if _, enabled, err := newLifeboatProvider(""); err != nil || enabled {
|
||||
t.Fatalf("empty lifeboat URL should disable provider: enabled=%v err=%v", enabled, err)
|
||||
}
|
||||
provider, enabled, err := newLifeboatProvider("http://lifeboat:8080/v1")
|
||||
if err != nil || !enabled {
|
||||
t.Fatalf("configured lifeboat should be enabled: enabled=%v err=%v", enabled, err)
|
||||
}
|
||||
if provider.ProviderInstanceID != "local-lifeboat" || provider.EndpointRef != "local-lifeboat-v1" || provider.Provider == nil {
|
||||
t.Fatalf("unexpected lifeboat registration: %+v", provider)
|
||||
}
|
||||
}
|
||||
@@ -293,7 +293,10 @@ func main() {
|
||||
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 != "" {
|
||||
@@ -303,7 +306,11 @@ func main() {
|
||||
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 {
|
||||
@@ -396,6 +403,10 @@ func main() {
|
||||
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
|
||||
@@ -1114,9 +1125,9 @@ func humanCognitiveChannelReply(message string, health capability.Health, health
|
||||
changeRequested := strings.Contains(strings.ToLower(message), "переключ") || strings.Contains(strings.ToLower(message), "настрой") || strings.Contains(strings.ToLower(message), "измени") || strings.Contains(strings.ToLower(message), "замени")
|
||||
if healthErr != nil {
|
||||
if changeRequested {
|
||||
return "Я не вижу подтверждённого состояния локального канала. Изменение модели не выполнено. Откройте консоль, проверьте настройку и запустите проверку ещё раз."
|
||||
return "Я не вижу подтверждённого состояния локального канала. Домовой всё равно рядом: опишите, что изменилось, и я помогу подготовить аварийную настройку."
|
||||
}
|
||||
return "Я не вижу подтверждённого состояния локального канала. Проверьте его в консоли или повторите вопрос позже."
|
||||
return "Я не вижу подтверждённого состояния локального канала, но Домовой работает. Можно продолжать разговор и проверить канал ещё раз."
|
||||
}
|
||||
var observation string
|
||||
switch health.State {
|
||||
@@ -1130,11 +1141,24 @@ func humanCognitiveChannelReply(message string, health capability.Health, health
|
||||
observation = "Состояние основного локального канала пока не подтверждено."
|
||||
}
|
||||
if changeRequested {
|
||||
return observation + " Я не меняю модель прямо из разговора: изменение выполняется в консоли с подтверждением. После изменения я увижу новую проверку канала автоматически."
|
||||
if health.State == capability.HealthUnavailable {
|
||||
return observation + " Но Домовой жив. Опишите новую локальную модель или канал — я помогу настроить аварийный режим и перед применением покажу итоговую конфигурацию."
|
||||
}
|
||||
return observation + " Я не меняю модель прямо из разговора: в обычном режиме изменение выполняется в консоли с подтверждением. После изменения я увижу новую проверку канала автоматически."
|
||||
}
|
||||
if health.State == capability.HealthUnavailable {
|
||||
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 {
|
||||
|
||||
@@ -37,11 +37,18 @@ func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
|
||||
|
||||
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
|
||||
reply := humanCognitiveChannelReply("Переключи локальную модель", capability.Health{State: capability.HealthUnavailable}, nil)
|
||||
if !strings.Contains(reply, "не меняю модель") || !strings.Contains(reply, "консоли") {
|
||||
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
|
||||
t.Fatalf("unexpected configuration reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
|
||||
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
|
||||
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
|
||||
t.Fatalf("unexpected degraded reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
|
||||
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
|
||||
|
||||
@@ -43,7 +43,7 @@ function renderCognitiveStatus(state) {
|
||||
const labels = {
|
||||
available: "канал мышления доступен",
|
||||
degraded: "канал мышления работает с ограничениями",
|
||||
unavailable: "основной канал мышления недоступен",
|
||||
unavailable: "Домовой рядом · основной канал недоступен",
|
||||
};
|
||||
cognitiveStatus.dataset.state = displayState;
|
||||
cognitiveStatus.textContent = labels[displayState];
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</section>
|
||||
</main>
|
||||
<aside class="chat-side chat-right" aria-label="Состояние и сведения об ИИ">
|
||||
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит только подтверждённое состояние канала. Саму модель он не угадывает.</p></div>
|
||||
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит подтверждённое состояние канала. Если основной ИИ пропал, он остаётся рядом и переходит в аварийный режим.</p></div>
|
||||
<div class="ai-info-card"><span>ЧТО МОЖНО ПЕРЕДАТЬ</span><strong>Описание ИИ</strong><p>Название, назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
|
||||
<form id="ai-data-form" class="ai-data-form">
|
||||
<label for="ai-data">Данные для текущего разговора</label>
|
||||
|
||||
@@ -102,6 +102,7 @@ services:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-}
|
||||
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
ports: ["19083:8083"]
|
||||
|
||||
@@ -126,6 +126,7 @@ services:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-}
|
||||
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"task_id": "DEV-000083",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "DEV-000073-baseline",
|
||||
"base_repo_revision": "0a2599cfa6255f1a98856d2fd09dc51a2119f525",
|
||||
"risk_tier": "C",
|
||||
"intent": "Keep Domovoy alive and conversational when the primary cognitive channel is unavailable by supporting an optional local Lifeboat provider and an honest deterministic degraded mode.",
|
||||
"requirement_ids": [
|
||||
"Primary cognitive settings remain confirmation-controlled while healthy",
|
||||
"Unavailable primary cognition exposes a human-readable degraded mode instead of killing chat",
|
||||
"An optional small local-only provider can serve as the Lifeboat through the existing Cognitive Gateway",
|
||||
"The core can still accept and explain configuration requests when all model providers are unavailable"
|
||||
],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cmd/core-runtime", "cmd/cognitive-worker", "cmd/creator-web", "deployment"],
|
||||
"paths": [
|
||||
"cmd/core-runtime/main.go",
|
||||
"cmd/core-runtime/main_test.go",
|
||||
"cmd/cognitive-worker/main.go",
|
||||
"cmd/cognitive-worker/main_test.go",
|
||||
"cmd/creator-web/static/assets/chat.js",
|
||||
"cmd/creator-web/static/chat.html",
|
||||
"cmd/creator-web/chat_test.go",
|
||||
"deploy/docker-compose.prod.yml",
|
||||
"deploy/docker-compose.dev.yml",
|
||||
"dev/tasks/DEV-000083-domovoy-lifeboat-mode.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "adapters/**", "modules/**"]
|
||||
},
|
||||
"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 Capability Registry", "existing chat response fields", "existing cognitive health endpoint"],
|
||||
"may_change": []
|
||||
},
|
||||
"semantic_owner": "capability-observation-consumer",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [
|
||||
"Healthy primary channel keeps normal configuration handoff and confirmation behavior",
|
||||
"Unavailable primary channel renders Domovoy alive in local degraded mode",
|
||||
"Optional Lifeboat provider participates only through the existing gateway resolution/failover path"
|
||||
],
|
||||
"side_effects_and_privileges": ["No new privilege", "No external egress", "No direct provider call from Core", "No configuration mutation from browser chat"],
|
||||
"persistence_and_migrations": ["No migration", "Existing conversation and capability observations only"],
|
||||
"failure_retry_restart": ["Provider absence falls back to the deterministic core reply", "Optional Lifeboat configuration is disabled when its URL is empty", "Restart reconstructs the provider registration from environment"],
|
||||
"assumptions": ["The optional Lifeboat endpoint implements the existing OpenAI-compatible local adapter contract", "Capability discovery/promotion remains the existing authorized path", "No real test secrets are in scope for this task"],
|
||||
"acceptance_scenarios": [
|
||||
"Primary unavailable message says Domovoy remains alive and can help recover",
|
||||
"Configuration request in degraded mode no longer falsely requires the primary model",
|
||||
"Optional Lifeboat provider is registered only when LOCAL_LIFEBOAT_PROVIDER_URL is configured",
|
||||
"Chat status distinguishes primary unavailable from Domovoy unavailable",
|
||||
"Healthy mode keeps confirmation/console wording"
|
||||
],
|
||||
"required_tests": ["go test ./cmd/core-runtime", "go test ./cmd/cognitive-worker", "go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
Reference in New Issue
Block a user