1use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum AgentStatus {
15 Orienting,
17 Seeding,
19 Running,
21 Paused,
23 Complete,
25 Failed,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum ModelTier {
32 Claude,
35 GLM,
38 Seed,
41 DeepSeek,
44 Hermes,
47}
48
49impl ModelTier {
50 pub fn relative_cost(&self) -> f64 {
52 match self {
53 ModelTier::Claude => 50.0, ModelTier::GLM => 5.0, ModelTier::Seed => 0.1, ModelTier::DeepSeek => 0.2, ModelTier::Hermes => 0.15, }
59 }
60
61 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, (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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum TaskType {
95 Synthesis,
97 Critique,
99 BigIdea,
101 Architecture,
103 ComplexCode,
105 Orchestration,
107 Discovery,
109 Exploration,
111 Drafting,
113 Variation,
115 Documentation,
117 Research,
119 Adversarial,
121 SecondOpinion,
123}
124
125#[derive(Debug, Clone)]
127pub struct AgentRoom {
128 pub room_id: String,
130 pub role: String,
132 pub status: AgentStatus,
134 pub model: ModelTier,
136 pub task_type: TaskType,
138 pub generation: u32,
140 pub seed_iterations: usize,
142 pub crystallization_score: f64,
144 pub gated: bool,
146 pub gate_passed: Option<bool>,
148 pub created_at: u64,
150 pub updated_at: u64,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum GateResult {
156 Approved,
157 Rejected(String),
158 NeedsApproval(String),
159}
160
161pub struct Lighthouse {
163 agents: HashMap<String, AgentRoom>,
165 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); capacity.insert(ModelTier::GLM, 1.0); capacity.insert(ModelTier::Seed, 1.0); capacity.insert(ModelTier::DeepSeek, 1.0); capacity.insert(ModelTier::Hermes, 1.0); Lighthouse {
185 agents: HashMap::new(),
186 capacity,
187 }
188 }
189
190 pub fn orient(&mut self, task: &str, task_type: TaskType) -> AgentRoom {
194 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 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_iterations > 0 && agent.model != ModelTier::Seed {
227 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 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 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 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 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 agent.gate_passed = Some(true);
278 agent.status = AgentStatus::Complete;
279 agent.updated_at = current_timestamp();
280
281 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 fn cheapest_appropriate(&self, task_type: TaskType) -> ModelTier {
292 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 ModelTier::Seed
307 }
308
309 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 pub fn get_agent(&self, room_id: &str) -> Option<&AgentRoom> {
318 self.agents.get(room_id)
319 }
320
321 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
337fn 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 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}