feat: expose cognitive fleet readiness #114

Merged
m merged 2 commits from codex/wp-601-fleet-status into main 2026-08-22 12:46:46 +00:00
6 changed files with 140 additions and 12 deletions
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
fleetdomain "git.cin.su/m/Assistant/modules/capability/domain"
)
type fleetEndpoint struct {
ProviderID string
ModelID string
Tier capability.FleetTier
URL string
}
type fleetStatusResponse struct {
Readiness capability.FleetReadiness `json:"readiness"`
Preferred capability.FleetTier `json:"preferred_tier"`
Selected capability.FleetTier `json:"selected_tier,omitempty"`
Error string `json:"error,omitempty"`
}
func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier, endpoints []fleetEndpoint) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
members := make([]capability.FleetMember, 0, len(endpoints))
for _, endpoint := range endpoints {
member := capability.FleetMember{ProviderInstanceID: capability.ProviderInstanceID(endpoint.ProviderID), ModelDescriptorID: capability.ModelDescriptorID(endpoint.ModelID), Tier: endpoint.Tier, Reason: "NOT_CONFIGURED"}
if strings.TrimSpace(endpoint.URL) == "" {
members = append(members, member)
continue
}
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
if err == nil {
response, requestErr := client.Do(request)
if requestErr == nil {
response.Body.Close()
if response.StatusCode >= 200 && response.StatusCode < 300 {
member.Ready, member.Reason = true, "AVAILABLE"
} else {
member.Reason = fmt.Sprintf("status=%s", response.Status)
}
} else {
member.Reason = requestErr.Error()
}
}
members = append(members, member)
}
readiness, err := fleetdomain.BuildFleetReadiness(time.Now().UnixMilli(), members)
response := fleetStatusResponse{Readiness: readiness, Preferred: preferred}
if err == nil {
response.Selected, err = fleetdomain.SelectFleetTier(readiness, preferred)
}
if err != nil {
response.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
if response.Selected == "" {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(response)
})
}
func fleetContext(ctx context.Context) context.Context { return ctx }
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestFleetReadinessSelectsLifeboatWhenPrimaryTiersUnavailable(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
defer server.Close()
handler := fleetReadinessHandler(server.Client(), capability.FleetTierHeavy, []fleetEndpoint{
{ProviderID: "cpu", ModelID: "cpu-model", Tier: capability.FleetTierFast, URL: server.URL},
{ProviderID: "medium", ModelID: "medium-model", Tier: capability.FleetTierMedium},
{ProviderID: "heavy", ModelID: "heavy-model", Tier: capability.FleetTierHeavy},
{ProviderID: "lifeboat", ModelID: "lifeboat-model", Tier: capability.FleetTierLifeboat, URL: server.URL},
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"LIFEBOAT"`) || !strings.Contains(recorder.Body.String(), `"tier":"MEDIUM"`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
+8
View File
@@ -80,6 +80,14 @@ func main() {
health.HTTPProbe(readinessClient, lifeboatURL),
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
}})
fleetClient := &http.Client{Timeout: 2 * time.Second}
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierHeavy)))
mux.Handle("/v1/fleet/readiness", fleetReadinessHandler(fleetClient, preferredFleet, []fleetEndpoint{
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"},
{ProviderID: "local-medium-unconfigured", ModelID: "medium-unconfigured", Tier: capability.FleetTierMedium},
{ProviderID: "local-gpu-opportunistic", ModelID: "gpu-opportunistic", Tier: capability.FleetTierHeavy, URL: strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/") + "/api/tags"},
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
}))
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
+9 -9
View File
@@ -10,16 +10,16 @@ const (
)
type FleetMember struct {
ProviderInstanceID ProviderInstanceID
ModelDescriptorID ModelDescriptorID
Tier FleetTier
Ready bool
Reason string
ProviderInstanceID ProviderInstanceID `json:"provider_instance_id"`
ModelDescriptorID ModelDescriptorID `json:"model_descriptor_id"`
Tier FleetTier `json:"tier"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
}
type FleetReadiness struct {
ObservedAtUnixMillis int64
Members []FleetMember
ReadyTiers []FleetTier
FallbackTier FleetTier
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Members []FleetMember `json:"members"`
ReadyTiers []FleetTier `json:"ready_tiers"`
FallbackTier FleetTier `json:"fallback_tier"`
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000143-research-runtime-orchestration.json",
"task_id": "DEV-000143",
"task_revision": 3
"task_file": "dev/tasks/DEV-000144-fleet-readiness-runtime.json",
"task_id": "DEV-000144",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000144",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "23b04aa",
"risk_tier": "E",
"intent": "Expose explainable runtime fleet readiness and explicit fallback tier from live provider probes.",
"requirement_ids": ["WP-203", "WP-601", "Model fleet/failover"],
"allowed_change_scope": {"modules": ["capability", "cognitive-worker"], "paths": ["contracts/capability/fleet.go", "cmd/cognitive-worker/**", "tests/cognitive_registry/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000144-fleet-readiness-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", ".gitea/**"]},
"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": ["FleetReadiness and SelectFleetTier"], "may_change": ["JSON projection and live probe endpoint"]},
"semantic_owner": "capability and cognitive-worker",
"canonical_state": [],
"state_transitions": ["live provider probes -> typed member states -> preferred tier or explicit Lifeboat fallback"],
"side_effects_and_privileges": ["Read-only HTTP probes only"],
"persistence_and_migrations": ["None"],
"failure_retry_restart": ["Each request re-probes providers; no in-memory health authority"],
"assumptions": ["Lifeboat is the explicit fallback tier"],
"acceptance_scenarios": ["unavailable CPU/GPU selects Lifeboat", "unconfigured Medium is visible", "no fallback returns typed 503"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}