Skip to main content

dodecet_encoder/
lighthouse.rs

1//! PLATO Agent Runtime — Agents live in rooms, Forgemaster is the lighthouse.
2//!
3//! Architecture:
4//! - AgentRoom: a PLATO room that hosts an agent
5//! - Lighthouse: Forgemaster's relay, orientation, and gate
6//! - TileRegistry: shared fleet intelligence
7//!
8//! The lighthouse doesn't sail the ships. It shows them where the rocks are.
9
10use std::collections::HashMap;
11
12/// Agent status in a PLATO room
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum AgentStatus {
15    /// Agent is being configured
16    Orienting,
17    /// Seeds are running discovery
18    Seeding,
19    /// Agent is working
20    Running,
21    /// Agent paused (waiting for gate/approval)
22    Paused,
23    /// Agent finished successfully
24    Complete,
25    /// Agent failed
26    Failed,
27}
28
29/// Model tier — matches resource allocation to task complexity
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum ModelTier {
32    /// Claude Code — synthesis, big ideas, stepping-back
33    /// Daily limit — use WISELY
34    Claude,
35    /// GLM-5.1 — architecture, complex code, orchestration
36    /// Monthly, short rate limit
37    GLM,
38    /// Seed-2.0-mini — discovery, exploration, variation
39    /// Per-token, cheap
40    Seed,
41    /// DeepSeek Flash — token-heavy work, documentation
42    /// Per-token, cheap
43    DeepSeek,
44    /// Hermes-70B — second opinions, adversarial testing
45    /// Per-token, cheap
46    Hermes,
47}
48
49impl ModelTier {
50    /// Cost estimate per 1K queries (relative)
51    pub fn relative_cost(&self) -> f64 {
52        match self {
53            ModelTier::Claude => 50.0,  // Daily limit — expensive per slot
54            ModelTier::GLM => 5.0,      // Monthly but rate-limited
55            ModelTier::Seed => 0.1,     // Cheap
56            ModelTier::DeepSeek => 0.2, // Cheap
57            ModelTier::Hermes => 0.15,  // Cheap
58        }
59    }
60
61    /// Should this model be used for this task type?
62    pub fn appropriate_for(&self, task: TaskType) -> bool {
63        match (self, task) {
64            (ModelTier::Claude, TaskType::Synthesis) => true,
65            (ModelTier::Claude, TaskType::Critique) => true,
66            (ModelTier::Claude, TaskType::BigIdea) => true,
67            (ModelTier::Claude, _) => false, // Don't waste on drafting
68
69            (ModelTier::GLM, TaskType::Architecture) => true,
70            (ModelTier::GLM, TaskType::ComplexCode) => true,
71            (ModelTier::GLM, TaskType::Orchestration) => true,
72            (ModelTier::GLM, _) => false,
73
74            (ModelTier::Seed, TaskType::Discovery) => true,
75            (ModelTier::Seed, TaskType::Exploration) => true,
76            (ModelTier::Seed, TaskType::Drafting) => true,
77            (ModelTier::Seed, TaskType::Variation) => true,
78            (ModelTier::Seed, _) => false,
79
80            (ModelTier::DeepSeek, TaskType::Documentation) => true,
81            (ModelTier::DeepSeek, TaskType::Research) => true,
82            (ModelTier::DeepSeek, TaskType::Drafting) => true,
83            (ModelTier::DeepSeek, _) => false,
84
85            (ModelTier::Hermes, TaskType::Adversarial) => true,
86            (ModelTier::Hermes, TaskType::SecondOpinion) => true,
87            (ModelTier::Hermes, _) => false,
88        }
89    }
90}
91
92/// Task type classification
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum TaskType {
95    /// Synthesis of multiple sources — USE CLAUDE
96    Synthesis,
97    /// Critical review, finding weak points — USE CLAUDE
98    Critique,
99    /// Big idea, stepping-back analysis — USE CLAUDE
100    BigIdea,
101    /// System design, interfaces — USE GLM
102    Architecture,
103    /// Multi-file, algorithmic — USE GLM
104    ComplexCode,
105    /// Coordinating multiple agents — USE GLM
106    Orchestration,
107    /// Parameter exploration — USE SEED
108    Discovery,
109    /// Survey the landscape — USE SEED
110    Exploration,
111    /// Generate text, docs — USE SEED or DEEPSEEK
112    Drafting,
113    /// Run many variations — USE SEED
114    Variation,
115    /// Write docs, READMEs — USE DEEPSEEK
116    Documentation,
117    /// Literature review — USE DEEPSEEK
118    Research,
119    /// Try to break something — USE HERMES
120    Adversarial,
121    /// Independent verification — USE HERMES
122    SecondOpinion,
123}
124
125/// An agent living in a PLATO room
126#[derive(Debug, Clone)]
127pub struct AgentRoom {
128    /// Room ID in PLATO
129    pub room_id: String,
130    /// Agent role
131    pub role: String,
132    /// Current status
133    pub status: AgentStatus,
134    /// Model tier assigned
135    pub model: ModelTier,
136    /// Task type
137    pub task_type: TaskType,
138    /// Generation (for seed refinement)
139    pub generation: u32,
140    /// Number of seed iterations run
141    pub seed_iterations: usize,
142    /// Crystallization score (from seeds, if applicable)
143    pub crystallization_score: f64,
144    /// Whether this agent has been gated (safety check)
145    pub gated: bool,
146    /// Whether gate passed
147    pub gate_passed: Option<bool>,
148    /// Timestamps
149    pub created_at: u64,
150    pub updated_at: u64,
151}
152
153/// Gate result — safety and alignment check
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum GateResult {
156    Approved,
157    Rejected(String),
158    NeedsApproval(String),
159}
160
161/// The Lighthouse — Forgemaster's relay, orientation, and gate
162pub struct Lighthouse {
163    /// Active agent rooms
164    agents: HashMap<String, AgentRoom>,
165    /// Available models and their remaining capacity
166    capacity: HashMap<ModelTier, f64>,
167}
168
169impl Default for Lighthouse {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl Lighthouse {
176    pub fn new() -> Self {
177        let mut capacity = HashMap::new();
178        capacity.insert(ModelTier::Claude, 1.0);    // 100% daily budget
179        capacity.insert(ModelTier::GLM, 1.0);       // 100% monthly budget
180        capacity.insert(ModelTier::Seed, 1.0);       // Effectively unlimited
181        capacity.insert(ModelTier::DeepSeek, 1.0);   // Effectively unlimited
182        capacity.insert(ModelTier::Hermes, 1.0);     // Effectively unlimited
183
184        Lighthouse {
185            agents: HashMap::new(),
186            capacity,
187        }
188    }
189
190    /// ORIENT: Classify a task and choose the right model.
191    ///
192    /// The lighthouse's first job: what needs doing, who should do it.
193    pub fn orient(&mut self, task: &str, task_type: TaskType) -> AgentRoom {
194        // Find the cheapest appropriate model
195        let model = self.cheapest_appropriate(task_type);
196
197        let room_id = format!("agent-{}", simple_hash(task));
198
199        let agent = AgentRoom {
200            room_id: room_id.clone(),
201            role: task.to_string(),
202            status: AgentStatus::Orienting,
203            model,
204            task_type,
205            generation: 0,
206            seed_iterations: 0,
207            crystallization_score: 0.0,
208            gated: false,
209            gate_passed: None,
210            created_at: current_timestamp(),
211            updated_at: current_timestamp(),
212        };
213
214        self.agents.insert(room_id, agent.clone());
215        agent
216    }
217
218    /// RELAY: Configure agent with API access and conditioning.
219    ///
220    /// The lighthouse relays keys without exposing them.
221    /// The lighthouse provides tiles as conditioning context.
222    pub fn relay(&mut self, room_id: &str, seed_iterations: usize) -> Option<&AgentRoom> {
223        let agent = self.agents.get_mut(room_id)?;
224
225        // If seed work needed, set to Seeding first
226        if seed_iterations > 0 && agent.model != ModelTier::Seed {
227            // Run seeds first, then upgrade
228            agent.status = AgentStatus::Seeding;
229            agent.seed_iterations = seed_iterations;
230        } else {
231            agent.status = AgentStatus::Running;
232        }
233
234        agent.updated_at = current_timestamp();
235        Some(self.agents.get(room_id)?)
236    }
237
238    /// GATE: Safety and alignment check on agent output.
239    ///
240    /// The lighthouse checks:
241    /// 1. No credential leaks
242    /// 2. No external actions without approval
243    /// 3. No overclaims
244    /// 4. Constraint satisfaction verified
245    pub fn gate(&mut self, room_id: &str, output: &str) -> GateResult {
246        let agent = match self.agents.get_mut(room_id) {
247            Some(a) => a,
248            None => return GateResult::Rejected("Unknown room".to_string()),
249        };
250
251        agent.gated = true;
252
253        // Check 1: Credential leaks
254        if contains_credentials(output) {
255            agent.gate_passed = Some(false);
256            agent.status = AgentStatus::Failed;
257            return GateResult::Rejected("Credential leak detected".to_string());
258        }
259
260        // Check 2: External action markers
261        if contains_external_action(output) {
262            agent.gate_passed = Some(false);
263            return GateResult::NeedsApproval(
264                "External action requires Casey approval".to_string(),
265            );
266        }
267
268        // Check 3: Overclaim markers
269        if contains_overclaims(output) {
270            agent.gate_passed = Some(false);
271            return GateResult::Rejected(
272                "Overclaim detected — falsify before asserting".to_string(),
273            );
274        }
275
276        // All checks passed
277        agent.gate_passed = Some(true);
278        agent.status = AgentStatus::Complete;
279        agent.updated_at = current_timestamp();
280
281        // Deduct capacity
282        let cost = agent.model.relative_cost() * 0.01;
283        if let Some(remaining) = self.capacity.get_mut(&agent.model) {
284            *remaining = (*remaining - cost).max(0.0);
285        }
286
287        GateResult::Approved
288    }
289
290    /// Find cheapest appropriate model for a task type.
291    fn cheapest_appropriate(&self, task_type: TaskType) -> ModelTier {
292        // Try from cheapest to most expensive
293        let tiers = [ModelTier::Seed, ModelTier::Hermes, ModelTier::DeepSeek, ModelTier::GLM, ModelTier::Claude];
294
295        for &tier in &tiers {
296            if tier.appropriate_for(task_type) {
297                if let Some(cap) = self.capacity.get(&tier) {
298                    if *cap > 0.1 {
299                        return tier;
300                    }
301                }
302            }
303        }
304
305        // Fallback: Seed is always available
306        ModelTier::Seed
307    }
308
309    /// List active agents
310    pub fn active_agents(&self) -> Vec<&AgentRoom> {
311        self.agents.values()
312            .filter(|a| a.status == AgentStatus::Running || a.status == AgentStatus::Seeding)
313            .collect()
314    }
315
316    /// Get agent status
317    pub fn get_agent(&self, room_id: &str) -> Option<&AgentRoom> {
318        self.agents.get(room_id)
319    }
320
321    /// Resource summary for fleet reporting
322    pub fn resource_summary(&self) -> String {
323        let mut lines = vec!["LIGHTHOUSE RESOURCE STATUS".to_string()];
324        for (tier, remaining) in &self.capacity {
325            let bar_len = (*remaining * 20.0) as usize;
326            let bar: String = "█".repeat(bar_len) + &"░".repeat(20 - bar_len);
327            lines.push(format!(
328                "  {:?}: [{}] {:.0}% remaining",
329                tier, bar, remaining * 100.0
330            ));
331        }
332        lines.push(format!("  Active agents: {}", self.active_agents().len()));
333        lines.join("\n")
334    }
335}
336
337// ─── Helper functions ─────────────────────────────────────────
338
339fn simple_hash(s: &str) -> String {
340    use std::fmt::Write;
341    let mut hash: u64 = 5381;
342    for b in s.bytes() {
343        hash = hash.wrapping_mul(33).wrapping_add(b as u64);
344    }
345    format!("{:08x}", hash)
346}
347
348fn current_timestamp() -> u64 {
349    std::time::SystemTime::now()
350        .duration_since(std::time::UNIX_EPOCH)
351        .unwrap_or_default()
352        .as_secs()
353}
354
355fn contains_credentials(s: &str) -> bool {
356    let lower = s.to_lowercase();
357    lower.contains("api_key=") || lower.contains("password=") || lower.contains("secret=") ||
358    lower.contains("token=") || lower.contains("bearer ")
359}
360
361fn contains_external_action(s: &str) -> bool {
362    let markers = ["send_email", "post_tweet", "git push", "npm publish", "deploy"];
363    markers.iter().any(|m| s.contains(m))
364}
365
366fn contains_overclaims(s: &str) -> bool {
367    let markers = ["proven that", "theorem:", "this proves", "we have proven"];
368    markers.iter().any(|m| s.to_lowercase().contains(m))
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_orient_synthesis_uses_claude() {
377        let mut lh = Lighthouse::new();
378        let agent = lh.orient("synthesis paper", TaskType::Synthesis);
379        assert_eq!(agent.model, ModelTier::Claude);
380    }
381
382    #[test]
383    fn test_orient_drafting_uses_seed() {
384        let mut lh = Lighthouse::new();
385        let agent = lh.orient("draft a readme", TaskType::Drafting);
386        assert_eq!(agent.model, ModelTier::Seed);
387    }
388
389    #[test]
390    fn test_orient_adversarial_uses_hermes() {
391        let mut lh = Lighthouse::new();
392        let agent = lh.orient("find weak points", TaskType::Adversarial);
393        assert_eq!(agent.model, ModelTier::Hermes);
394    }
395
396    #[test]
397    fn test_orient_architecture_uses_glm() {
398        let mut lh = Lighthouse::new();
399        let agent = lh.orient("design the system", TaskType::Architecture);
400        assert_eq!(agent.model, ModelTier::GLM);
401    }
402
403    #[test]
404    fn test_gate_approves_clean_output() {
405        let mut lh = Lighthouse::new();
406        let agent = lh.orient("task", TaskType::Drafting);
407        let result = lh.gate(&agent.room_id, "Here is a clean result with no issues.");
408        assert_eq!(result, GateResult::Approved);
409    }
410
411    #[test]
412    fn test_gate_rejects_credentials() {
413        let mut lh = Lighthouse::new();
414        let agent = lh.orient("task", TaskType::Drafting);
415        let result = lh.gate(&agent.room_id, "The api_key=abc123 is here");
416        assert!(matches!(result, GateResult::Rejected(_)));
417    }
418
419    #[test]
420    fn test_gate_needs_approval_for_external() {
421        let mut lh = Lighthouse::new();
422        let agent = lh.orient("task", TaskType::Drafting);
423        let result = lh.gate(&agent.room_id, "Running git push to main");
424        assert!(matches!(result, GateResult::NeedsApproval(_)));
425    }
426
427    #[test]
428    fn test_gate_rejects_overclaims() {
429        let mut lh = Lighthouse::new();
430        let agent = lh.orient("task", TaskType::Drafting);
431        let result = lh.gate(&agent.room_id, "We have proven that all lattices are perfect");
432        assert!(matches!(result, GateResult::Rejected(_)));
433    }
434
435    #[test]
436    fn test_relay_sets_seeding() {
437        let mut lh = Lighthouse::new();
438        let agent = lh.orient("explore", TaskType::Architecture);
439        // GLM agent with seed iterations -> Seeding first
440        let result = lh.relay(&agent.room_id, 50);
441        assert!(result.is_some());
442        assert_eq!(result.unwrap().status, AgentStatus::Seeding);
443    }
444
445    #[test]
446    fn test_resource_summary() {
447        let mut lh = Lighthouse::new();
448        let summary = lh.resource_summary();
449        assert!(summary.contains("Claude"));
450        assert!(summary.contains("Seed"));
451        assert!(summary.contains("remaining"));
452    }
453
454    #[test]
455    fn test_capacity_decreases_after_gate() {
456        let mut lh = Lighthouse::new();
457        let initial = *lh.capacity.get(&ModelTier::Seed).unwrap();
458        let agent = lh.orient("task", TaskType::Drafting);
459        lh.gate(&agent.room_id, "clean output");
460        let after = *lh.capacity.get(&ModelTier::Seed).unwrap();
461        assert!(after < initial);
462    }
463}