Add scout constellation mode for APK venue persona packs.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Cluster 3+ scout_report hits on the same SSID within 10 minutes; server infers airport/campus/retail venue class and pushes persona spread_policy. Emberwake weather-map merges active scout biomes. Includes agent, server API, and Vitest coverage.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -46,7 +47,10 @@ type Scheduler struct {
|
||||
exec CommandExecutor
|
||||
store DecisionStore
|
||||
court CourtContext
|
||||
chamber CourtDeps
|
||||
elevator ClearanceElevator
|
||||
seer SeerBridge
|
||||
surgical SurgicalDeps
|
||||
stop chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
@@ -67,6 +71,21 @@ func NewScheduler(cfg ConfigProvider, snap SnapshotProvider, exec CommandExecuto
|
||||
}
|
||||
}
|
||||
|
||||
// SetSeerBridge attaches The Seer memory/stream hooks (optional).
|
||||
func (s *Scheduler) SetSeerBridge(b SeerBridge) {
|
||||
s.seer = b
|
||||
}
|
||||
|
||||
// SetSurgicalDeps wires Path Tracer replay, strain memory, and Seer emitters.
|
||||
func (s *Scheduler) SetSurgicalDeps(deps SurgicalDeps) {
|
||||
s.surgical = deps
|
||||
}
|
||||
|
||||
// SetCourtDeps wires adversarial chamber evidence, Seer feed, and Emberwake broadcast.
|
||||
func (s *Scheduler) SetCourtDeps(deps CourtDeps) {
|
||||
s.chamber = deps
|
||||
}
|
||||
|
||||
func (s *Scheduler) Start() {
|
||||
s.wg.Add(1)
|
||||
go s.loop()
|
||||
@@ -158,10 +177,32 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
|
||||
}
|
||||
var systemPrompt, userPrompt string
|
||||
var courtMeta *CourtDecisionMeta
|
||||
useCourt := ShouldUseCourt(snap)
|
||||
useSurgical := false
|
||||
if s.surgical.Trace != nil {
|
||||
if trace, ok := s.surgical.Trace.TraceForAgent(agentID); ok && ShouldUseSurgicalReplay(trace, snap) {
|
||||
useSurgical = true
|
||||
erasureOn := false
|
||||
if s.surgical.ErasureActive != nil {
|
||||
erasureOn = s.surgical.ErasureActive()
|
||||
}
|
||||
atlasSummary := ""
|
||||
if s.court != nil {
|
||||
goos := firstNonEmpty(snap.GOOS, snap.Platform)
|
||||
atlasSummary = s.court.FailureAtlasSummary(snap.FingerprintKey, goos)
|
||||
}
|
||||
bundle := BuildSurgicalDiagnosticBundle(snap, trace, atlasSummary, cfg.Persona, erasureOn)
|
||||
if s.surgical.StrainLookup != nil {
|
||||
bundle.Strain = s.surgical.StrainLookup(agentID)
|
||||
}
|
||||
systemPrompt, userPrompt = BuildSurgicalReplayPrompt(bundle)
|
||||
}
|
||||
}
|
||||
useCourt := !useSurgical && ShouldUseCourt(snap)
|
||||
var courtDebate CourtDebateTranscript
|
||||
if useCourt {
|
||||
atlasSummary := ""
|
||||
var phenotype *strategy.FleetPhenotype
|
||||
var evidence CourtChamberEvidence
|
||||
if s.court != nil {
|
||||
goos := firstNonEmpty(snap.GOOS, snap.Platform)
|
||||
atlasSummary = s.court.FailureAtlasSummary(snap.FingerprintKey, goos)
|
||||
@@ -169,8 +210,21 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
|
||||
phenotype = p
|
||||
}
|
||||
}
|
||||
if s.chamber.Chamber != nil {
|
||||
evidence = s.chamber.Chamber.ChamberEvidence(agentID, snap)
|
||||
} else {
|
||||
evidence = CourtChamberEvidence{
|
||||
AgentID: agentID, AgentName: snap.Name, Hashrate: snap.MiningHashrate,
|
||||
Stuck: snap.Stuck, ChainExhausted: snap.ChainExhausted,
|
||||
ClearanceLevel: snap.ClearanceLevel, AtlasSummary: atlasSummary,
|
||||
SubnetImmune: "chamber provider unavailable",
|
||||
ErasureRecovery: FormatErasureRecovery(false, false, false),
|
||||
GossipWhispers: "none",
|
||||
}
|
||||
}
|
||||
persona := NormalizePersona(cfg.Persona)
|
||||
bundle := BuildCourtPrompt(snap, atlasSummary, phenotype, persona)
|
||||
var bundle CourtPromptBundle
|
||||
courtDebate, bundle = BuildCourtDebate(snap, evidence, phenotype, persona)
|
||||
systemPrompt = bundle.SystemPrompt
|
||||
userPrompt = bundle.UserPrompt
|
||||
courtMeta = &CourtDecisionMeta{
|
||||
@@ -182,22 +236,41 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
|
||||
systemPrompt = PersonaSystemPrompt(cfg.Persona)
|
||||
userPrompt = BuildMissionPrompt(snap)
|
||||
}
|
||||
if s.seer != nil {
|
||||
systemPrompt = strings.TrimSpace(systemPrompt + "\n\n" + SeerNoteInstruction + "\n\n" + SeerToolCatalog)
|
||||
if notes := s.seer.NotesForPrompt(agentID); notes != "" {
|
||||
userPrompt = AugmentUserPromptWithNotes(userPrompt, notes)
|
||||
}
|
||||
}
|
||||
promptHash := hashPrompt(systemPrompt + "\n---\n" + userPrompt)
|
||||
|
||||
decide := Decide
|
||||
if DecideFunc != nil {
|
||||
decide = DecideFunc
|
||||
}
|
||||
if s.seer != nil {
|
||||
s.seer.EmitEvent(agentID, "request", BuildChatCompletionPayload(cfg.Model, systemPrompt, userPrompt))
|
||||
}
|
||||
response, err := decide(ctx, cfg.Endpoint, cfg.Model, systemPrompt, userPrompt)
|
||||
if err != nil {
|
||||
log.Printf("[fleet-ai] agent %s decide: %v", agentID, err)
|
||||
if s.store != nil {
|
||||
_ = s.store.InsertAIDecision(agentID, promptHash, "", "error:"+err.Error(), courtMeta)
|
||||
}
|
||||
if s.seer != nil {
|
||||
s.seer.EmitEvent(agentID, "response", map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
s.markRun(agentID)
|
||||
return
|
||||
}
|
||||
|
||||
if s.seer != nil {
|
||||
s.seer.EmitEvent(agentID, "response", map[string]interface{}{"content": response})
|
||||
if note, ok := ExtractSeerNote(response); ok {
|
||||
_ = s.seer.AppendNote(agentID, note, promptHash)
|
||||
}
|
||||
}
|
||||
|
||||
if courtMeta != nil {
|
||||
courtMeta.JudgeVerdict = ExtractJudgeVerdict(response)
|
||||
}
|
||||
@@ -205,6 +278,12 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
|
||||
if useCourt {
|
||||
cmds = ExpandCourtCommands(cmds)
|
||||
s.ensureCourtRetryClearance(agentID, cmds)
|
||||
courtDebate = FinalizeCourtDebate(courtDebate, response)
|
||||
}
|
||||
if useSurgical {
|
||||
s.runSurgicalReplay(agentID, snap, cmds, response, promptHash)
|
||||
s.markRun(agentID)
|
||||
return
|
||||
}
|
||||
results := make([]string, 0, len(cmds))
|
||||
for _, cmd := range cmds {
|
||||
@@ -224,12 +303,108 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
|
||||
}
|
||||
}
|
||||
executed := FormatExecuted(cmds, results)
|
||||
if useCourt {
|
||||
s.emitCourtDebate(agentID, courtDebate, executed)
|
||||
}
|
||||
if s.store != nil {
|
||||
_ = s.store.InsertAIDecision(agentID, promptHash, response, executed, courtMeta)
|
||||
}
|
||||
s.markRun(agentID)
|
||||
}
|
||||
|
||||
func (s *Scheduler) emitCourtDebate(agentID string, transcript CourtDebateTranscript, executed string) {
|
||||
seer := s.chamber.Seer
|
||||
if seer == nil {
|
||||
seer = s.surgical.Seer
|
||||
}
|
||||
if seer != nil {
|
||||
_ = seer.EmitSeerEvent("court_debate", agentID, map[string]interface{}{
|
||||
"type": "court_debate",
|
||||
"agent_id": transcript.AgentID,
|
||||
"agent_name": transcript.AgentName,
|
||||
"clearance": transcript.ClearanceLevel,
|
||||
"evidence": transcript.Evidence,
|
||||
"transcript": transcript.Transcript,
|
||||
"verdict": transcript.Verdict,
|
||||
"commands": transcript.CommandsJSON,
|
||||
"executed": executed,
|
||||
"ts": transcript.Timestamp,
|
||||
})
|
||||
}
|
||||
if s.chamber.Emberwake != nil {
|
||||
s.chamber.Emberwake(agentID, transcript)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) runSurgicalReplay(agentID string, snap AgentSnapshot, cmds []Command, response, promptHash string) {
|
||||
trace := SurgicalTraceContext{}
|
||||
if s.surgical.Trace != nil {
|
||||
trace, _ = s.surgical.Trace.TraceForAgent(agentID)
|
||||
}
|
||||
failedTier, _ := firstFailedSpreadTier(snap.LOTLAttempts)
|
||||
strain := ""
|
||||
if s.surgical.StrainLookup != nil {
|
||||
strain = s.surgical.StrainLookup(agentID)
|
||||
}
|
||||
|
||||
surgical, ok := SelectSurgicalCommand(cmds)
|
||||
if !ok {
|
||||
if s.store != nil {
|
||||
_ = s.store.InsertAIDecision(agentID, promptHash, response, "surgical:no_command", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
expanded := ExpandSurgicalCommand(surgical)
|
||||
var dispatched Command
|
||||
var outcome string
|
||||
for _, cmd := range expanded {
|
||||
if cmd.Type == CmdNoop {
|
||||
continue
|
||||
}
|
||||
dispatched = cmd
|
||||
if s.exec == nil {
|
||||
outcome = "no executor"
|
||||
break
|
||||
}
|
||||
sum, execErr := s.exec.Execute(agentID, cmd)
|
||||
if execErr != nil {
|
||||
outcome = "err:" + execErr.Error()
|
||||
} else {
|
||||
outcome = sum
|
||||
}
|
||||
break
|
||||
}
|
||||
if outcome == "" {
|
||||
outcome = "noop"
|
||||
}
|
||||
|
||||
fixType := surgical.Type
|
||||
if dispatched.Type != "" && dispatched.Type != surgical.Type {
|
||||
fixType = surgical.Type + "→" + dispatched.Type
|
||||
}
|
||||
fixArgs := FormatSurgicalFixArgs(surgical)
|
||||
if s.surgical.Strain != nil {
|
||||
_ = s.surgical.Strain.InsertStrainMemory(agentID, trace.SessionID, failedTier, strain, fixType, fixArgs, outcome)
|
||||
}
|
||||
if s.surgical.Seer != nil {
|
||||
_ = s.surgical.Seer.EmitSeerEvent("surgical_replay", agentID, map[string]interface{}{
|
||||
"session_id": trace.SessionID,
|
||||
"failed_tier": failedTier,
|
||||
"strain": strain,
|
||||
"fix_type": fixType,
|
||||
"fix_args": fixArgs,
|
||||
"outcome": outcome,
|
||||
"prompt_hash": promptHash,
|
||||
"response": response,
|
||||
})
|
||||
}
|
||||
executed := fixType + ":" + outcome
|
||||
if s.store != nil {
|
||||
_ = s.store.InsertAIDecision(agentID, promptHash, response, "surgical:"+executed, nil)
|
||||
}
|
||||
}
|
||||
|
||||
const stuckHostFailedTierThreshold = 14
|
||||
|
||||
func (s *Scheduler) ensureCourtRetryClearance(agentID string, cmds []Command) {
|
||||
|
||||
Reference in New Issue
Block a user