diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index cce6d9e..eff7fc6 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -387,6 +387,11 @@ func main() { } needsCognition = false } + if isCognitiveChannelQuestion(utterance.Content) { + health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal")) + planningReply = humanCognitiveChannelReply(utterance.Content, health, healthErr) + needsCognition = false + } if helpPath.Reply != "" { planningReply = helpPath.Reply needsCognition = false @@ -1094,6 +1099,42 @@ func isCapabilityQuestion(message string) bool { return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности") } +func isCognitiveChannelQuestion(message string) bool { + lower := strings.ToLower(message) + markers := []string{"локальн", "модель", "когнитивн", "канал мышления", "проверь канал", "настройк"} + for _, marker := range markers { + if strings.Contains(lower, marker) { + return true + } + } + return false +} + +func humanCognitiveChannelReply(message string, health capability.Health, healthErr error) string { + 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 "Я не вижу подтверждённого состояния локального канала. Проверьте его в консоли или повторите вопрос позже." + } + var observation string + switch health.State { + case capability.HealthAvailable: + observation = "Основной локальный канал сейчас доступен." + case capability.HealthDegraded: + observation = "Основной локальный канал сейчас работает с ограничениями." + case capability.HealthUnavailable: + observation = "Основной локальный канал сейчас недоступен." + default: + observation = "Состояние основного локального канала пока не подтверждено." + } + if changeRequested { + return observation + " Я не меняю модель прямо из разговора: изменение выполняется в консоли с подтверждением. После изменения я увижу новую проверку канала автоматически." + } + return observation + " Это наблюдение о готовности capability, а не прямой доступ к модели." +} + func humanCapabilityList(kinds []capability.CapabilityKind) string { labels := make([]string, 0, len(kinds)) for _, kind := range kinds { diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index a244da4..e1a8a52 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" "time" + + "git.cin.su/m/Assistant/contracts/capability" ) func TestBearerTokenRequiresBearerScheme(t *testing.T) { @@ -23,6 +25,23 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) { } } +func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) { + if !isCognitiveChannelQuestion("Проверь локальную модель") { + t.Fatal("expected local model question to be recognized") + } + reply := humanCognitiveChannelReply("Проверь локальную модель", capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil) + if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") { + t.Fatalf("unexpected safe reply: %q", reply) + } +} + +func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) { + reply := humanCognitiveChannelReply("Переключи локальную модель", capability.Health{State: capability.HealthUnavailable}, nil) + if !strings.Contains(reply, "не меняю модель") || !strings.Contains(reply, "консоли") { + t.Fatalf("unexpected configuration 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"}`)) diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index 8d5b7d2..25655ca 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000077-creator-chat-cognitive-bridge.json", - "task_id": "DEV-000077", + "task_file": "dev/tasks/DEV-000078-core-conversational-cognitive-health.json", + "task_id": "DEV-000078", "task_revision": 1 } diff --git a/dev/tasks/DEV-000078-core-conversational-cognitive-health.json b/dev/tasks/DEV-000078-core-conversational-cognitive-health.json new file mode 100644 index 0000000..5f4b35e --- /dev/null +++ b/dev/tasks/DEV-000078-core-conversational-cognitive-health.json @@ -0,0 +1,45 @@ +{ + "task_id": "DEV-000078", + "task_revision": 1, + "architecture_revision": "DEV-000073-baseline", + "base_repo_revision": "0d10e6fd82a7c374bebbc8d3cee9c1ed0b420745", + "risk_tier": "E", + "intent": "Let Domovoy answer explicit questions about the local cognitive channel from the Capability-owned health observation and route configuration changes to the operator console without mutating runtime configuration.", + "requirement_ids": [ + "Explicit model/channel questions use observed capability health", + "Model configuration requests never call a provider directly from Core", + "Configuration changes are explained as a separate operator-console action", + "Provider and model identifiers remain hidden from human chat" + ], + "allowed_change_scope": { + "modules": ["cmd/core-runtime"], + "paths": [ + "cmd/core-runtime/main.go", + "cmd/core-runtime/main_test.go", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000078-core-conversational-cognitive-health.json" + ] + }, + "forbidden_change_scope": { + "paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "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": ["Capability repository LoadHealth", "existing cognitive-proposal health semantics", "existing /admin operator surface"], "may_change": []}, + "semantic_owner": "capability-observation-consumer", + "canonical_state": [], + "state_transitions": ["Human asks about local cognitive channel", "Core reads Capability-owned health observation", "Core returns observation or safe unavailable explanation"], + "side_effects_and_privileges": ["No provider call for explicit status/configuration questions", "No runtime configuration mutation", "No new privilege"], + "persistence_and_migrations": ["Existing utterance and semantic persistence only", "No migration"], + "failure_retry_restart": ["Missing health observation produces an honest unavailable reply", "Future status polling remains available in chat UI"], + "assumptions": ["The operator console is the authorized configuration surface", "Capability health is a sufficient provider-neutral observation for the first conversational response"], + "acceptance_scenarios": ["Проверь локальную модель returns a human-readable health observation", "Переключи модель does not mutate configuration and directs to console", "Replies contain no provider/model/binding identifiers", "Unit tests cover available, unavailable and repository failure states"], + "required_tests": ["go test ./cmd/core-runtime", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +}