1use std::sync::Arc;
10
11use async_trait::async_trait;
12use chrono;
13use dashmap::DashMap;
14use serde::{Deserialize, Serialize};
15use tokio::sync::Mutex;
16use tokio::sync::watch;
17use tokio::task::JoinHandle;
18use tracing::{info, instrument, warn};
19
20use punch_memory::{BoutId, MemorySubstrate};
21use punch_runtime::{
22 FighterLoopParams, FighterLoopResult, LlmDriver, McpClient, run_fighter_loop,
23 tools_for_capabilities,
24};
25use punch_types::{
26 AgentCoordinator, AgentInfo, AgentMessageResult, CoordinationStrategy, FighterId,
27 FighterManifest, FighterStatus, GorillaId, GorillaManifest, GorillaMetrics, GorillaStatus,
28 PunchConfig, PunchError, PunchEvent, PunchResult, TenantId, TenantStatus, Troop, TroopId,
29};
30
31use punch_skills::{SkillMarketplace, builtin_skills};
32
33use crate::agent_messaging::MessageRouter;
34use crate::background::BackgroundExecutor;
35use crate::budget::BudgetEnforcer;
36use crate::event_bus::EventBus;
37use crate::metering::MeteringEngine;
38use crate::metrics::{self, MetricsRegistry};
39use crate::scheduler::{QuotaConfig, Scheduler};
40use crate::swarm::SwarmCoordinator;
41use crate::tenant_registry::TenantRegistry;
42use crate::triggers::{Trigger, TriggerEngine, TriggerId, TriggerSummary};
43use crate::troop::TroopManager;
44use crate::workflow::{Workflow, WorkflowEngine, WorkflowId, WorkflowRunId};
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct FighterEntry {
53 pub manifest: FighterManifest,
55 pub status: FighterStatus,
57 pub current_bout: Option<BoutId>,
59}
60
61pub struct GorillaEntry {
66 pub manifest: GorillaManifest,
68 pub status: GorillaStatus,
70 pub metrics: GorillaMetrics,
72 task_handle: Option<JoinHandle<()>>,
74}
75
76impl std::fmt::Debug for GorillaEntry {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("GorillaEntry")
80 .field("manifest", &self.manifest)
81 .field("status", &self.status)
82 .field("metrics", &self.metrics)
83 .field("has_task", &self.task_handle.is_some())
84 .finish()
85 }
86}
87
88pub struct Ring {
97 fighters: DashMap<FighterId, FighterEntry>,
99 gorillas: DashMap<GorillaId, Mutex<GorillaEntry>>,
101 memory: Arc<MemorySubstrate>,
103 driver: Arc<dyn LlmDriver>,
105 event_bus: EventBus,
107 scheduler: Scheduler,
109 config: PunchConfig,
111 background: BackgroundExecutor,
113 workflow_engine: WorkflowEngine,
115 metering: MeteringEngine,
117 budget_enforcer: Arc<BudgetEnforcer>,
119 trigger_engine: TriggerEngine,
121 troop_manager: TroopManager,
123 swarm_coordinator: SwarmCoordinator,
125 message_router: MessageRouter,
127 metrics: Arc<MetricsRegistry>,
129 tenant_registry: TenantRegistry,
131 marketplace: SkillMarketplace,
133 mcp_clients: Arc<DashMap<String, Arc<McpClient>>>,
135 shutdown_tx: watch::Sender<bool>,
137 _shutdown_rx: watch::Receiver<bool>,
139}
140
141impl Ring {
142 pub fn new(
148 config: PunchConfig,
149 memory: Arc<MemorySubstrate>,
150 driver: Arc<dyn LlmDriver>,
151 ) -> Self {
152 let (shutdown_tx, shutdown_rx) = watch::channel(false);
153 let background =
154 BackgroundExecutor::with_shutdown(shutdown_tx.clone(), shutdown_rx.clone());
155 let metering = MeteringEngine::new(Arc::clone(&memory));
156 let metering_arc = Arc::new(MeteringEngine::new(Arc::clone(&memory)));
157 let budget_enforcer = Arc::new(BudgetEnforcer::new(Arc::clone(&metering_arc)));
158 let metrics_registry = Arc::new(MetricsRegistry::new());
159 metrics::register_default_metrics(&metrics_registry);
160
161 let marketplace = SkillMarketplace::new();
162 for listing in builtin_skills() {
163 marketplace.publish(listing);
164 }
165
166 let mcp_clients = Arc::new(DashMap::new());
167
168 Self {
169 fighters: DashMap::new(),
170 gorillas: DashMap::new(),
171 memory,
172 driver,
173 event_bus: EventBus::new(),
174 scheduler: Scheduler::new(QuotaConfig::default()),
175 config,
176 background,
177 workflow_engine: WorkflowEngine::new(),
178 metering,
179 budget_enforcer,
180 trigger_engine: TriggerEngine::new(),
181 troop_manager: TroopManager::new(),
182 swarm_coordinator: SwarmCoordinator::new(),
183 message_router: MessageRouter::new(),
184 metrics: metrics_registry,
185 tenant_registry: TenantRegistry::new(),
186 marketplace,
187 mcp_clients,
188 shutdown_tx,
189 _shutdown_rx: shutdown_rx,
190 }
191 }
192
193 pub fn with_quota_config(
195 config: PunchConfig,
196 memory: Arc<MemorySubstrate>,
197 driver: Arc<dyn LlmDriver>,
198 quota_config: QuotaConfig,
199 ) -> Self {
200 let (shutdown_tx, shutdown_rx) = watch::channel(false);
201 let background =
202 BackgroundExecutor::with_shutdown(shutdown_tx.clone(), shutdown_rx.clone());
203 let metering = MeteringEngine::new(Arc::clone(&memory));
204 let metering_arc = Arc::new(MeteringEngine::new(Arc::clone(&memory)));
205 let budget_enforcer = Arc::new(BudgetEnforcer::new(Arc::clone(&metering_arc)));
206 let metrics_registry = Arc::new(MetricsRegistry::new());
207 metrics::register_default_metrics(&metrics_registry);
208
209 let marketplace = SkillMarketplace::new();
210 for listing in builtin_skills() {
211 marketplace.publish(listing);
212 }
213
214 let mcp_clients = Arc::new(DashMap::new());
215
216 Self {
217 fighters: DashMap::new(),
218 gorillas: DashMap::new(),
219 memory,
220 driver,
221 event_bus: EventBus::new(),
222 scheduler: Scheduler::new(quota_config),
223 config,
224 background,
225 workflow_engine: WorkflowEngine::new(),
226 metering,
227 budget_enforcer,
228 trigger_engine: TriggerEngine::new(),
229 troop_manager: TroopManager::new(),
230 swarm_coordinator: SwarmCoordinator::new(),
231 message_router: MessageRouter::new(),
232 metrics: metrics_registry,
233 tenant_registry: TenantRegistry::new(),
234 marketplace,
235 mcp_clients,
236 shutdown_tx,
237 _shutdown_rx: shutdown_rx,
238 }
239 }
240
241 pub fn event_bus(&self) -> &EventBus {
245 &self.event_bus
246 }
247
248 pub fn scheduler(&self) -> &Scheduler {
250 &self.scheduler
251 }
252
253 pub fn memory(&self) -> &Arc<MemorySubstrate> {
255 &self.memory
256 }
257
258 pub fn config(&self) -> &PunchConfig {
260 &self.config
261 }
262
263 pub fn background(&self) -> &BackgroundExecutor {
265 &self.background
266 }
267
268 pub fn workflow_engine(&self) -> &WorkflowEngine {
270 &self.workflow_engine
271 }
272
273 pub fn metering(&self) -> &MeteringEngine {
275 &self.metering
276 }
277
278 pub fn budget_enforcer(&self) -> &Arc<BudgetEnforcer> {
280 &self.budget_enforcer
281 }
282
283 pub fn trigger_engine(&self) -> &TriggerEngine {
285 &self.trigger_engine
286 }
287
288 pub fn metrics(&self) -> &Arc<MetricsRegistry> {
290 &self.metrics
291 }
292
293 pub fn tenant_registry(&self) -> &TenantRegistry {
295 &self.tenant_registry
296 }
297
298 pub fn marketplace(&self) -> &SkillMarketplace {
300 &self.marketplace
301 }
302
303 pub fn mcp_clients(&self) -> &Arc<DashMap<String, Arc<McpClient>>> {
305 &self.mcp_clients
306 }
307
308 pub async fn spawn_mcp_servers(&self) {
316 for (name, server_config) in &self.config.mcp_servers {
317 info!(server = %name, command = %server_config.command, "spawning MCP server");
318
319 match McpClient::spawn(
320 name.clone(),
321 &server_config.command,
322 &server_config.args,
323 &server_config.env,
324 )
325 .await
326 {
327 Ok(client) => {
328 if let Err(e) = client.initialize().await {
329 warn!(server = %name, error = %e, "MCP server initialization failed, skipping");
330 let _ = client.shutdown().await;
331 continue;
332 }
333
334 match client.list_tools().await {
335 Ok(tools) => {
336 info!(
337 server = %name,
338 tool_count = tools.len(),
339 "MCP server ready with {} tools",
340 tools.len()
341 );
342 }
343 Err(e) => {
344 warn!(server = %name, error = %e, "failed to list MCP tools");
345 }
346 }
347
348 self.mcp_clients.insert(name.clone(), Arc::new(client));
349
350 self.event_bus.publish(PunchEvent::McpServerStarted {
351 server_name: name.clone(),
352 });
353 }
354 Err(e) => {
355 warn!(server = %name, error = %e, "failed to spawn MCP server, skipping");
356 }
357 }
358 }
359 }
360
361 pub async fn shutdown_mcp_servers(&self) {
363 for entry in self.mcp_clients.iter() {
364 let name = entry.key().clone();
365 info!(server = %name, "shutting down MCP server");
366 if let Err(e) = entry.value().shutdown().await {
367 warn!(server = %name, error = %e, "MCP server shutdown error");
368 }
369 }
370 self.mcp_clients.clear();
371 }
372
373 pub async fn mcp_tools(&self) -> Vec<punch_types::ToolDefinition> {
375 let mut tools = Vec::new();
376 for entry in self.mcp_clients.iter() {
377 match entry.value().list_tools().await {
378 Ok(server_tools) => tools.extend(server_tools),
379 Err(e) => {
380 warn!(
381 server = %entry.key(),
382 error = %e,
383 "failed to list tools from MCP server"
384 );
385 }
386 }
387 }
388 tools
389 }
390
391 #[instrument(skip(self, manifest), fields(fighter_name = %manifest.name))]
398 pub async fn spawn_fighter_for_tenant(
399 &self,
400 tenant_id: &TenantId,
401 mut manifest: FighterManifest,
402 ) -> PunchResult<FighterId> {
403 let tenant = self
405 .tenant_registry
406 .get_tenant(tenant_id)
407 .ok_or_else(|| PunchError::Tenant(format!("tenant {} not found", tenant_id)))?;
408
409 if tenant.status == TenantStatus::Suspended {
410 return Err(PunchError::Tenant(format!(
411 "tenant {} is suspended",
412 tenant_id
413 )));
414 }
415
416 let current_count = self
418 .fighters
419 .iter()
420 .filter(|e| e.value().manifest.tenant_id.as_ref() == Some(tenant_id))
421 .count();
422
423 if current_count >= tenant.quota.max_fighters {
424 return Err(PunchError::QuotaExceeded(format!(
425 "tenant {} has reached max fighters limit ({})",
426 tenant_id, tenant.quota.max_fighters
427 )));
428 }
429
430 manifest.tenant_id = Some(*tenant_id);
432 Ok(self.spawn_fighter(manifest).await)
433 }
434
435 pub fn list_fighters_for_tenant(
437 &self,
438 tenant_id: &TenantId,
439 ) -> Vec<(FighterId, FighterManifest, FighterStatus)> {
440 self.fighters
441 .iter()
442 .filter(|entry| entry.value().manifest.tenant_id.as_ref() == Some(tenant_id))
443 .map(|entry| {
444 let id = *entry.key();
445 let e = entry.value();
446 (id, e.manifest.clone(), e.status)
447 })
448 .collect()
449 }
450
451 #[instrument(skip(self), fields(%fighter_id, %tenant_id))]
455 pub fn kill_fighter_for_tenant(
456 &self,
457 fighter_id: &FighterId,
458 tenant_id: &TenantId,
459 ) -> PunchResult<()> {
460 let entry = self
461 .fighters
462 .get(fighter_id)
463 .ok_or_else(|| PunchError::Fighter(format!("fighter {} not found", fighter_id)))?;
464
465 if entry.manifest.tenant_id.as_ref() != Some(tenant_id) {
466 return Err(PunchError::Auth(format!(
467 "fighter {} does not belong to tenant {}",
468 fighter_id, tenant_id
469 )));
470 }
471
472 drop(entry);
473 self.kill_fighter(fighter_id);
474 Ok(())
475 }
476
477 pub fn check_tenant_tool_access(&self, tenant_id: &TenantId, tool_name: &str) -> bool {
482 match self.tenant_registry.get_tenant(tenant_id) {
483 Some(tenant) => {
484 tenant.quota.max_tools.is_empty()
485 || tenant.quota.max_tools.iter().any(|t| t == tool_name)
486 }
487 None => false,
488 }
489 }
490
491 pub fn register_trigger(&self, trigger: Trigger) -> TriggerId {
495 self.trigger_engine.register_trigger(trigger)
496 }
497
498 pub fn remove_trigger(&self, id: &TriggerId) {
500 self.trigger_engine.remove_trigger(id);
501 }
502
503 pub fn list_triggers(&self) -> Vec<(TriggerId, TriggerSummary)> {
505 self.trigger_engine.list_triggers()
506 }
507
508 #[instrument(skip(self, manifest), fields(fighter_name = %manifest.name))]
515 pub async fn spawn_fighter(&self, manifest: FighterManifest) -> FighterId {
516 let id = FighterId::new();
517 let name = manifest.name.clone();
518
519 if let Err(e) = self
521 .memory
522 .save_fighter(&id, &manifest, FighterStatus::Idle)
523 .await
524 {
525 warn!(error = %e, "failed to persist fighter to database (continuing in-memory only)");
526 }
527
528 let entry = FighterEntry {
529 manifest,
530 status: FighterStatus::Idle,
531 current_bout: None,
532 };
533
534 self.fighters.insert(id, entry);
535
536 self.metrics.counter_inc(metrics::FIGHTER_SPAWNS_TOTAL);
538 self.metrics
539 .gauge_set(metrics::ACTIVE_FIGHTERS, self.fighters.len() as i64);
540
541 self.event_bus.publish(PunchEvent::FighterSpawned {
542 fighter_id: id,
543 name: name.clone(),
544 });
545
546 info!(%id, name, "fighter spawned");
547
548 {
552 let memory = Arc::clone(&self.memory);
553 let creed_name = name.clone();
554 let fid = id;
555 let manifest_for_creed = self.fighters.get(&id).map(|e| e.value().manifest.clone());
557 tokio::spawn(async move {
558 if let Ok(None) = memory.load_creed_by_name(&creed_name).await
560 && let Some(manifest) = &manifest_for_creed
561 {
562 let creed = punch_types::Creed::new(&creed_name).with_self_awareness(manifest);
563 if let Err(e) = memory.save_creed(&creed).await {
564 warn!(error = %e, fighter = %creed_name, "failed to create default creed");
565 } else {
566 info!(fighter = %creed_name, "default creed created on spawn");
567 }
568 }
569 if let Err(e) = memory.bind_creed_to_fighter(&creed_name, &fid).await {
571 warn!(error = %e, fighter = %creed_name, "failed to bind creed on spawn");
572 } else {
573 info!(fighter = %creed_name, id = %fid, "creed bound to fighter on spawn");
574 }
575 });
576 }
577
578 id
579 }
580
581 pub async fn ensure_creed(&self, fighter_name: &str, manifest: &FighterManifest) {
584 match self.memory.load_creed_by_name(fighter_name).await {
585 Ok(Some(_)) => {
586 }
588 Ok(None) => {
589 let creed = punch_types::Creed::new(fighter_name).with_self_awareness(manifest);
591 if let Err(e) = self.memory.save_creed(&creed).await {
592 warn!(error = %e, "failed to create default creed");
593 } else {
594 info!(fighter = %fighter_name, "default creed created with self-awareness");
595 }
596 }
597 Err(e) => {
598 warn!(error = %e, "failed to check for existing creed");
599 }
600 }
601 }
602
603 #[instrument(skip(self, message), fields(%fighter_id))]
608 pub async fn send_message(
609 &self,
610 fighter_id: &FighterId,
611 message: String,
612 ) -> PunchResult<FighterLoopResult> {
613 self.send_message_with_coordinator(fighter_id, message, None)
614 .await
615 }
616
617 #[instrument(skip(self, message, coordinator), fields(%fighter_id))]
626 pub async fn send_message_with_coordinator(
627 &self,
628 fighter_id: &FighterId,
629 message: String,
630 coordinator: Option<Arc<dyn AgentCoordinator>>,
631 ) -> PunchResult<FighterLoopResult> {
632 let mut entry = self
634 .fighters
635 .get_mut(fighter_id)
636 .ok_or_else(|| PunchError::Fighter(format!("fighter {} not found", fighter_id)))?;
637
638 if !self.scheduler.check_quota(fighter_id) {
640 entry.status = FighterStatus::Resting;
641 return Err(PunchError::RateLimited {
642 provider: "scheduler".to_string(),
643 retry_after_ms: 60_000,
644 });
645 }
646
647 match self.budget_enforcer.check_budget(fighter_id).await {
649 Ok(crate::budget::BudgetVerdict::Blocked {
650 reason,
651 retry_after_secs,
652 }) => {
653 entry.status = FighterStatus::Resting;
654 return Err(PunchError::RateLimited {
655 provider: format!("budget: {}", reason),
656 retry_after_ms: retry_after_secs * 1000,
657 });
658 }
659 Ok(crate::budget::BudgetVerdict::Warning { message, .. }) => {
660 info!(warning = %message, "budget warning for fighter");
661 }
662 Ok(crate::budget::BudgetVerdict::Allowed) => {}
663 Err(e) => {
664 warn!(error = %e, "budget check failed, allowing request");
665 }
666 }
667
668 let bout_id = match entry.current_bout {
670 Some(id) => id,
671 None => {
672 let id = self
674 .memory
675 .create_bout(fighter_id)
676 .await
677 .map_err(|e| PunchError::Bout(format!("failed to create bout: {e}")))?;
678 entry.current_bout = Some(id);
679
680 self.event_bus.publish(PunchEvent::BoutStarted {
681 bout_id: id.0,
682 fighter_id: *fighter_id,
683 });
684
685 id
686 }
687 };
688
689 entry.status = FighterStatus::Fighting;
691 let manifest = entry.manifest.clone();
692 let fighter_name = manifest.name.clone();
693 drop(entry); let mut available_tools = tools_for_capabilities(&manifest.capabilities);
696
697 let has_mcp_access = manifest.capabilities.iter().any(|c| {
699 matches!(c, punch_types::Capability::McpAccess(_))
700 });
701 if has_mcp_access && !self.mcp_clients.is_empty() {
702 for mcp_entry in self.mcp_clients.iter() {
703 let server_name = mcp_entry.key();
704 let can_access = manifest.capabilities.iter().any(|c| {
705 if let punch_types::Capability::McpAccess(pattern) = c {
706 pattern == "*" || pattern == server_name
707 } else {
708 false
709 }
710 });
711 if can_access {
712 match mcp_entry.value().list_tools().await {
713 Ok(tools) => available_tools.extend(tools),
714 Err(e) => {
715 warn!(
716 server = %server_name,
717 error = %e,
718 "failed to list MCP tools for fighter"
719 );
720 }
721 }
722 }
723 }
724 }
725
726 self.event_bus.publish(PunchEvent::FighterMessage {
728 fighter_id: *fighter_id,
729 bout_id: bout_id.0,
730 role: "user".to_string(),
731 content_preview: truncate_preview(&message, 120),
732 });
733
734 let params = FighterLoopParams {
736 manifest: manifest.clone(),
737 user_message: message,
738 bout_id,
739 fighter_id: *fighter_id,
740 memory: Arc::clone(&self.memory),
741 driver: Arc::clone(&self.driver),
742 available_tools,
743 max_iterations: None,
744 context_window: None,
745 tool_timeout_secs: None,
746 coordinator,
747 approval_engine: None,
748 sandbox: None,
749 mcp_clients: if self.mcp_clients.is_empty() {
750 None
751 } else {
752 Some(Arc::clone(&self.mcp_clients))
753 },
754 };
755
756 self.metrics.counter_inc(metrics::MESSAGES_TOTAL);
758
759 let result = run_fighter_loop(params).await;
760
761 if let Some(mut entry) = self.fighters.get_mut(fighter_id) {
763 match &result {
764 Ok(loop_result) => {
765 entry.status = FighterStatus::Idle;
766 self.scheduler
767 .record_usage(fighter_id, loop_result.usage.total());
768
769 self.metrics
771 .counter_add(metrics::TOKENS_INPUT_TOTAL, loop_result.usage.input_tokens);
772 self.metrics.counter_add(
773 metrics::TOKENS_OUTPUT_TOTAL,
774 loop_result.usage.output_tokens,
775 );
776
777 if let Err(e) = self
779 .metering
780 .record_usage(
781 fighter_id,
782 &manifest.model.model,
783 loop_result.usage.input_tokens,
784 loop_result.usage.output_tokens,
785 )
786 .await
787 {
788 warn!(error = %e, "failed to record metering usage");
789 }
790
791 let preview = truncate_preview(&loop_result.response, 120);
793 self.event_bus.publish(PunchEvent::FighterMessage {
794 fighter_id: *fighter_id,
795 bout_id: bout_id.0,
796 role: "assistant".to_string(),
797 content_preview: preview,
798 });
799
800 self.event_bus.publish(PunchEvent::BoutEnded {
802 bout_id: bout_id.0,
803 fighter_id: *fighter_id,
804 messages_exchanged: loop_result.usage.total(),
805 });
806 }
807 Err(e) => {
808 entry.status = FighterStatus::KnockedOut;
809 self.metrics.counter_inc(metrics::ERRORS_TOTAL);
810
811 self.event_bus.publish(PunchEvent::Error {
813 source: fighter_name.clone(),
814 message: format!("{e}"),
815 });
816 }
817 }
818 }
819
820 result
821 }
822
823 #[instrument(skip(self), fields(%fighter_id))]
825 pub fn kill_fighter(&self, fighter_id: &FighterId) {
826 if let Some((_, entry)) = self.fighters.remove(fighter_id) {
827 self.scheduler.remove_fighter(fighter_id);
828 self.metrics
829 .gauge_set(metrics::ACTIVE_FIGHTERS, self.fighters.len() as i64);
830
831 self.event_bus.publish(PunchEvent::Error {
832 source: "ring".to_string(),
833 message: format!("Fighter '{}' killed", entry.manifest.name),
834 });
835
836 info!(name = %entry.manifest.name, "fighter killed");
837 } else {
838 warn!("attempted to kill unknown fighter");
839 }
840 }
841
842 pub fn list_fighters(&self) -> Vec<(FighterId, FighterManifest, FighterStatus)> {
844 self.fighters
845 .iter()
846 .map(|entry| {
847 let id = *entry.key();
848 let e = entry.value();
849 (id, e.manifest.clone(), e.status)
850 })
851 .collect()
852 }
853
854 pub fn get_fighter(&self, fighter_id: &FighterId) -> Option<FighterEntry> {
856 self.fighters.get(fighter_id).map(|e| e.value().clone())
857 }
858
859 #[instrument(skip(self, manifest), fields(gorilla_name = %manifest.name))]
866 pub fn register_gorilla(&self, manifest: GorillaManifest) -> GorillaId {
867 let id = GorillaId::new();
868 let name = manifest.name.clone();
869
870 let entry = GorillaEntry {
871 manifest,
872 status: GorillaStatus::Caged,
873 metrics: GorillaMetrics::default(),
874 task_handle: None,
875 };
876
877 self.gorillas.insert(id, Mutex::new(entry));
878 info!(%id, name, "gorilla registered");
879 id
880 }
881
882 #[instrument(skip(self), fields(%gorilla_id))]
887 pub async fn unleash_gorilla(&self, gorilla_id: &GorillaId) -> PunchResult<()> {
888 let entry_ref = self
889 .gorillas
890 .get(gorilla_id)
891 .ok_or_else(|| PunchError::Gorilla(format!("gorilla {} not found", gorilla_id)))?;
892
893 let mut entry = entry_ref.value().lock().await;
894
895 if entry.status == GorillaStatus::Unleashed || entry.status == GorillaStatus::Rampaging {
896 return Err(PunchError::Gorilla(format!(
897 "gorilla {} is already active",
898 gorilla_id
899 )));
900 }
901
902 let gorilla_id_owned = *gorilla_id;
903 let name = entry.manifest.name.clone();
904 let manifest = entry.manifest.clone();
905
906 self.background.start_gorilla(
908 gorilla_id_owned,
909 manifest,
910 self.config.default_model.clone(),
911 Arc::clone(&self.memory),
912 Arc::clone(&self.driver),
913 )?;
914
915 entry.status = GorillaStatus::Unleashed;
916 drop(entry);
917 drop(entry_ref);
918
919 self.metrics.counter_inc(metrics::GORILLA_RUNS_TOTAL);
921 self.metrics.gauge_inc(metrics::ACTIVE_GORILLAS);
922
923 self.event_bus.publish(PunchEvent::GorillaUnleashed {
924 gorilla_id: gorilla_id_owned,
925 name,
926 });
927
928 Ok(())
929 }
930
931 #[instrument(skip(self), fields(%gorilla_id))]
933 pub async fn cage_gorilla(&self, gorilla_id: &GorillaId) -> PunchResult<()> {
934 let entry_ref = self
935 .gorillas
936 .get(gorilla_id)
937 .ok_or_else(|| PunchError::Gorilla(format!("gorilla {} not found", gorilla_id)))?;
938
939 let mut entry = entry_ref.value().lock().await;
940
941 self.background.stop_gorilla(gorilla_id);
943
944 if let Some(handle) = entry.task_handle.take() {
946 handle.abort();
947 }
948
949 let name = entry.manifest.name.clone();
950 entry.status = GorillaStatus::Caged;
951 drop(entry);
952 drop(entry_ref);
953
954 self.metrics.gauge_dec(metrics::ACTIVE_GORILLAS);
955
956 self.event_bus.publish(PunchEvent::GorillaPaused {
957 gorilla_id: *gorilla_id,
958 reason: "manually caged".to_string(),
959 });
960
961 info!(name, "gorilla caged");
962 Ok(())
963 }
964
965 pub async fn list_gorillas(
967 &self,
968 ) -> Vec<(GorillaId, GorillaManifest, GorillaStatus, GorillaMetrics)> {
969 let mut result = Vec::new();
970
971 for entry in self.gorillas.iter() {
972 let id = *entry.key();
973 let inner = entry.value().lock().await;
974 result.push((
975 id,
976 inner.manifest.clone(),
977 inner.status,
978 inner.metrics.clone(),
979 ));
980 }
981
982 result
983 }
984
985 pub async fn get_gorilla_manifest(&self, gorilla_id: &GorillaId) -> Option<GorillaManifest> {
987 let entry_ref = self.gorillas.get(gorilla_id)?;
988 let entry = entry_ref.value().lock().await;
989 Some(entry.manifest.clone())
990 }
991
992 pub async fn find_gorilla_by_name(&self, name: &str) -> Option<GorillaId> {
994 for entry in self.gorillas.iter() {
995 let inner = entry.value().lock().await;
996 if inner.manifest.name.eq_ignore_ascii_case(name) {
997 return Some(*entry.key());
998 }
999 }
1000 None
1001 }
1002
1003 #[instrument(skip(self), fields(%gorilla_id))]
1008 pub async fn run_gorilla_tick(
1009 &self,
1010 gorilla_id: &GorillaId,
1011 ) -> PunchResult<punch_runtime::FighterLoopResult> {
1012 let entry_ref = self
1013 .gorillas
1014 .get(gorilla_id)
1015 .ok_or_else(|| PunchError::Gorilla(format!("gorilla {} not found", gorilla_id)))?;
1016
1017 let entry = entry_ref.value().lock().await;
1018 let manifest = entry.manifest.clone();
1019 drop(entry);
1020 drop(entry_ref);
1021
1022 crate::background::run_gorilla_tick(
1023 *gorilla_id,
1024 &manifest,
1025 &self.config.default_model,
1026 &self.memory,
1027 &self.driver,
1028 )
1029 .await
1030 }
1031
1032 pub fn driver(&self) -> &Arc<dyn LlmDriver> {
1034 &self.driver
1035 }
1036
1037 #[instrument(skip(self, message), fields(%source_id, %target_id))]
1046 pub async fn fighter_to_fighter(
1047 &self,
1048 source_id: &FighterId,
1049 target_id: &FighterId,
1050 message: String,
1051 ) -> PunchResult<FighterLoopResult> {
1052 let source_name = self
1054 .fighters
1055 .get(source_id)
1056 .map(|entry| entry.value().manifest.name.clone())
1057 .ok_or_else(|| {
1058 PunchError::Fighter(format!("source fighter {} not found", source_id))
1059 })?;
1060
1061 if self.fighters.get(target_id).is_none() {
1063 return Err(PunchError::Fighter(format!(
1064 "target fighter {} not found",
1065 target_id
1066 )));
1067 }
1068
1069 let enriched_message = format!(
1071 "[Message from fighter '{}' (id: {})]\n\n{}",
1072 source_name, source_id, message
1073 );
1074
1075 self.send_message(target_id, enriched_message).await
1077 }
1078
1079 pub fn find_fighter_by_name_sync(&self, name: &str) -> Option<(FighterId, FighterManifest)> {
1083 self.fighters.iter().find_map(|entry| {
1084 if entry.value().manifest.name.eq_ignore_ascii_case(name) {
1085 Some((*entry.key(), entry.value().manifest.clone()))
1086 } else {
1087 None
1088 }
1089 })
1090 }
1091
1092 pub async fn update_fighter_relationships(&self, fighter_a_name: &str, fighter_b_name: &str) {
1098 let memory = Arc::clone(&self.memory);
1099 let a_name = fighter_a_name.to_string();
1100 let b_name = fighter_b_name.to_string();
1101
1102 tokio::spawn(async move {
1104 if let Ok(Some(mut creed_a)) = memory.load_creed_by_name(&a_name).await {
1106 update_relationship(&mut creed_a, &b_name, None);
1107 if let Err(e) = memory.save_creed(&creed_a).await {
1108 warn!(error = %e, fighter = %a_name, "failed to save creed relationship update");
1109 }
1110 }
1111
1112 if let Ok(Some(mut creed_b)) = memory.load_creed_by_name(&b_name).await {
1114 update_relationship(&mut creed_b, &a_name, None);
1115 if let Err(e) = memory.save_creed(&creed_b).await {
1116 warn!(error = %e, fighter = %b_name, "failed to save creed relationship update");
1117 }
1118 }
1119 });
1120 }
1121
1122 pub fn troop_manager(&self) -> &TroopManager {
1126 &self.troop_manager
1127 }
1128
1129 pub fn swarm_coordinator(&self) -> &SwarmCoordinator {
1131 &self.swarm_coordinator
1132 }
1133
1134 pub fn message_router(&self) -> &MessageRouter {
1136 &self.message_router
1137 }
1138
1139 #[instrument(skip(self, members), fields(troop_name = %name))]
1143 pub fn form_troop(
1144 &self,
1145 name: String,
1146 leader: FighterId,
1147 members: Vec<FighterId>,
1148 strategy: CoordinationStrategy,
1149 ) -> PunchResult<TroopId> {
1150 if self.fighters.get(&leader).is_none() {
1152 return Err(PunchError::Troop(format!(
1153 "leader fighter {} not found",
1154 leader
1155 )));
1156 }
1157
1158 for member in &members {
1160 if self.fighters.get(member).is_none() {
1161 return Err(PunchError::Troop(format!(
1162 "member fighter {} not found",
1163 member
1164 )));
1165 }
1166 }
1167
1168 let member_count = members.len() + 1; let troop_id = self
1170 .troop_manager
1171 .form_troop(name.clone(), leader, members, strategy);
1172
1173 self.event_bus.publish(PunchEvent::TroopFormed {
1174 troop_id,
1175 name,
1176 member_count,
1177 });
1178
1179 Ok(troop_id)
1180 }
1181
1182 #[instrument(skip(self), fields(%troop_id))]
1184 pub fn disband_troop(&self, troop_id: &TroopId) -> PunchResult<()> {
1185 let name = self.troop_manager.disband_troop(troop_id)?;
1186
1187 self.event_bus.publish(PunchEvent::TroopDisbanded {
1188 troop_id: *troop_id,
1189 name,
1190 });
1191
1192 Ok(())
1193 }
1194
1195 pub fn assign_troop_task(
1197 &self,
1198 troop_id: &TroopId,
1199 task_description: &str,
1200 ) -> PunchResult<Vec<FighterId>> {
1201 self.troop_manager.assign_task(troop_id, task_description)
1202 }
1203
1204 pub async fn assign_troop_task_async(
1207 &self,
1208 troop_id: &TroopId,
1209 task: &str,
1210 ) -> PunchResult<crate::troop::TaskAssignmentResult> {
1211 self.troop_manager.assign_task_async(troop_id, task).await
1212 }
1213
1214 pub fn get_troop_status(&self, troop_id: &TroopId) -> Option<Troop> {
1216 self.troop_manager.get_troop(troop_id)
1217 }
1218
1219 pub fn list_troops(&self) -> Vec<Troop> {
1221 self.troop_manager.list_troops()
1222 }
1223
1224 pub fn recruit_to_troop(&self, troop_id: &TroopId, fighter_id: FighterId) -> PunchResult<()> {
1226 if self.fighters.get(&fighter_id).is_none() {
1228 return Err(PunchError::Troop(format!(
1229 "fighter {} not found",
1230 fighter_id
1231 )));
1232 }
1233 self.troop_manager.recruit(troop_id, fighter_id)
1234 }
1235
1236 pub fn dismiss_from_troop(
1238 &self,
1239 troop_id: &TroopId,
1240 fighter_id: &FighterId,
1241 ) -> PunchResult<()> {
1242 self.troop_manager.dismiss(troop_id, fighter_id)
1243 }
1244
1245 #[instrument(skip(self), fields(%fighter_id))]
1252 pub fn kill_fighter_safe(&self, fighter_id: &FighterId) {
1253 let troop_ids = self.troop_manager.get_fighter_troops(fighter_id);
1255 for troop_id in troop_ids {
1256 if let Err(e) = self.troop_manager.dismiss(&troop_id, fighter_id) {
1257 warn!(
1258 %troop_id,
1259 %fighter_id,
1260 error = %e,
1261 "failed to dismiss fighter from troop before kill"
1262 );
1263 }
1264 }
1265 self.kill_fighter(fighter_id);
1266 }
1267
1268 pub fn register_workflow(&self, workflow: Workflow) -> WorkflowId {
1272 self.workflow_engine.register_workflow(workflow)
1273 }
1274
1275 pub async fn execute_workflow(
1277 &self,
1278 workflow_id: &WorkflowId,
1279 input: String,
1280 ) -> PunchResult<WorkflowRunId> {
1281 self.workflow_engine
1282 .execute_workflow(
1283 workflow_id,
1284 input,
1285 Arc::clone(&self.memory),
1286 Arc::clone(&self.driver),
1287 &self.config.default_model,
1288 )
1289 .await
1290 }
1291
1292 pub fn shutdown(&self) {
1296 info!("Ring shutdown initiated");
1297
1298 let _ = self.shutdown_tx.send(true);
1300
1301 self.background.shutdown_all();
1303
1304 info!("Ring shutdown complete");
1305 }
1306}
1307
1308fn truncate_preview(s: &str, max_len: usize) -> String {
1314 if s.len() <= max_len {
1315 return s.to_string();
1316 }
1317 let end = s
1319 .char_indices()
1320 .take_while(|(i, _)| *i <= max_len.saturating_sub(3))
1321 .last()
1322 .map(|(i, c)| i + c.len_utf8())
1323 .unwrap_or(0);
1324 format!("{}...", &s[..end])
1325}
1326
1327fn update_relationship(creed: &mut punch_types::Creed, peer_name: &str, trust_nudge: Option<f64>) {
1329 if let Some(rel) = creed
1330 .relationships
1331 .iter_mut()
1332 .find(|r| r.entity == peer_name && r.entity_type == "fighter")
1333 {
1334 rel.interaction_count += 1;
1335 if let Some(nudge) = trust_nudge {
1336 rel.trust = (rel.trust * 0.9 + nudge * 0.1).clamp(0.0, 1.0);
1337 }
1338 rel.notes = format!(
1339 "Last interaction: {}",
1340 chrono::Utc::now().format("%Y-%m-%d %H:%M UTC")
1341 );
1342 } else {
1343 creed.relationships.push(punch_types::Relationship {
1344 entity: peer_name.to_string(),
1345 entity_type: "fighter".to_string(),
1346 nature: "peer".to_string(),
1347 trust: trust_nudge.unwrap_or(0.5),
1348 interaction_count: 1,
1349 notes: format!(
1350 "First interaction: {}",
1351 chrono::Utc::now().format("%Y-%m-%d %H:%M UTC")
1352 ),
1353 });
1354 }
1355 creed.updated_at = chrono::Utc::now();
1356 creed.version += 1;
1357}
1358
1359#[async_trait]
1364impl AgentCoordinator for Ring {
1365 async fn spawn_fighter(&self, manifest: FighterManifest) -> PunchResult<FighterId> {
1366 Ok(Ring::spawn_fighter(self, manifest).await)
1367 }
1368
1369 async fn send_message_to_agent(
1370 &self,
1371 target: &FighterId,
1372 message: String,
1373 ) -> PunchResult<AgentMessageResult> {
1374 let result = self.send_message(target, message).await?;
1375 Ok(AgentMessageResult {
1376 response: result.response,
1377 tokens_used: result.usage.total(),
1378 })
1379 }
1380
1381 async fn find_fighter_by_name(&self, name: &str) -> PunchResult<Option<FighterId>> {
1382 let found = self.fighters.iter().find_map(|entry| {
1383 if entry.value().manifest.name.eq_ignore_ascii_case(name) {
1384 Some(*entry.key())
1385 } else {
1386 None
1387 }
1388 });
1389 Ok(found)
1390 }
1391
1392 async fn list_fighters(&self) -> PunchResult<Vec<AgentInfo>> {
1393 let fighters = self
1394 .fighters
1395 .iter()
1396 .map(|entry| AgentInfo {
1397 id: *entry.key(),
1398 name: entry.value().manifest.name.clone(),
1399 status: entry.value().status,
1400 })
1401 .collect();
1402 Ok(fighters)
1403 }
1404}
1405
1406const _: () = {
1412 fn _assert_send<T: Send>() {}
1413 fn _assert_sync<T: Sync>() {}
1414 fn _assert() {
1415 _assert_send::<Ring>();
1416 _assert_sync::<Ring>();
1417 }
1418};