Skip to main content

punch_kernel/
ring.rs

1//! **The Ring** — the central kernel and coordinator for the Punch system.
2//!
3//! The [`Ring`] owns every fighter and gorilla, wires them to the memory
4//! substrate, the LLM driver, the event bus, the scheduler, the background
5//! executor, the workflow engine, and the metering engine. All mutations
6//! go through the Ring so that invariants (quotas, capabilities, lifecycle
7//! events) are enforced in a single place.
8
9use 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// ---------------------------------------------------------------------------
47// Entry types
48// ---------------------------------------------------------------------------
49
50/// Everything the Ring tracks about a single fighter.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct FighterEntry {
53    /// The fighter's manifest (identity, model, capabilities, etc.).
54    pub manifest: FighterManifest,
55    /// Current operational status.
56    pub status: FighterStatus,
57    /// The active bout (conversation session) ID, if any.
58    pub current_bout: Option<BoutId>,
59}
60
61/// Everything the Ring tracks about a single gorilla.
62///
63/// The `task_handle` is behind a `Mutex` because [`JoinHandle`] is not `Clone`
64/// and we need interior mutability when starting / stopping background tasks.
65pub struct GorillaEntry {
66    /// The gorilla's manifest.
67    pub manifest: GorillaManifest,
68    /// Current operational status.
69    pub status: GorillaStatus,
70    /// Runtime metrics.
71    pub metrics: GorillaMetrics,
72    /// Handle to the background task (if running).
73    task_handle: Option<JoinHandle<()>>,
74}
75
76// Manual Debug impl because JoinHandle doesn't implement Debug in a useful way.
77impl 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
88// ---------------------------------------------------------------------------
89// The Ring
90// ---------------------------------------------------------------------------
91
92/// The Ring — the central coordinator for the Punch Agent Combat System.
93///
94/// Thread-safe by design: all collections use [`DashMap`] and all shared state
95/// is behind `Arc`. Wrap the `Ring` itself in an `Arc` to share across tasks.
96pub struct Ring {
97    /// All active fighters, keyed by their unique ID.
98    fighters: DashMap<FighterId, FighterEntry>,
99    /// All registered gorillas, keyed by their unique ID.
100    gorillas: DashMap<GorillaId, Mutex<GorillaEntry>>,
101    /// Shared memory substrate (SQLite persistence).
102    memory: Arc<MemorySubstrate>,
103    /// The LLM driver used for completions.
104    driver: Arc<dyn LlmDriver>,
105    /// System-wide event bus.
106    event_bus: EventBus,
107    /// Per-fighter quota scheduler.
108    scheduler: Scheduler,
109    /// Top-level Punch configuration.
110    config: PunchConfig,
111    /// Background executor for autonomous gorilla tasks.
112    background: BackgroundExecutor,
113    /// Multi-step workflow engine.
114    workflow_engine: WorkflowEngine,
115    /// Cost tracking and metering engine.
116    metering: MeteringEngine,
117    /// Budget enforcement layer (opt-in spending limits).
118    budget_enforcer: Arc<BudgetEnforcer>,
119    /// Event-driven trigger engine.
120    trigger_engine: TriggerEngine,
121    /// Troop manager for multi-agent coordination.
122    troop_manager: TroopManager,
123    /// Swarm coordinator for emergent behavior tasks.
124    swarm_coordinator: SwarmCoordinator,
125    /// Inter-agent message router.
126    message_router: MessageRouter,
127    /// Production observability metrics.
128    metrics: Arc<MetricsRegistry>,
129    /// Multi-tenant registry.
130    tenant_registry: TenantRegistry,
131    /// Skill marketplace for discovering and installing moves.
132    marketplace: SkillMarketplace,
133    /// Active MCP server clients, keyed by server name.
134    mcp_clients: Arc<DashMap<String, Arc<McpClient>>>,
135    /// Shutdown signal sender.
136    shutdown_tx: watch::Sender<bool>,
137    /// Shutdown signal receiver.
138    _shutdown_rx: watch::Receiver<bool>,
139}
140
141impl Ring {
142    /// Create a new Ring.
143    ///
144    /// The caller provides the already-initialised memory substrate, LLM
145    /// driver, and configuration. The Ring will create its own event bus and
146    /// scheduler internally.
147    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    /// Create a new Ring with a custom quota configuration.
194    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    // -- Accessors -----------------------------------------------------------
242
243    /// Get a reference to the event bus.
244    pub fn event_bus(&self) -> &EventBus {
245        &self.event_bus
246    }
247
248    /// Get a reference to the scheduler.
249    pub fn scheduler(&self) -> &Scheduler {
250        &self.scheduler
251    }
252
253    /// Get a reference to the memory substrate.
254    pub fn memory(&self) -> &Arc<MemorySubstrate> {
255        &self.memory
256    }
257
258    /// Get a reference to the configuration.
259    pub fn config(&self) -> &PunchConfig {
260        &self.config
261    }
262
263    /// Get a reference to the background executor.
264    pub fn background(&self) -> &BackgroundExecutor {
265        &self.background
266    }
267
268    /// Get a reference to the workflow engine.
269    pub fn workflow_engine(&self) -> &WorkflowEngine {
270        &self.workflow_engine
271    }
272
273    /// Get a reference to the metering engine.
274    pub fn metering(&self) -> &MeteringEngine {
275        &self.metering
276    }
277
278    /// Get a reference to the budget enforcer.
279    pub fn budget_enforcer(&self) -> &Arc<BudgetEnforcer> {
280        &self.budget_enforcer
281    }
282
283    /// Get a reference to the trigger engine.
284    pub fn trigger_engine(&self) -> &TriggerEngine {
285        &self.trigger_engine
286    }
287
288    /// Get a reference to the metrics registry.
289    pub fn metrics(&self) -> &Arc<MetricsRegistry> {
290        &self.metrics
291    }
292
293    /// Get a reference to the tenant registry.
294    pub fn tenant_registry(&self) -> &TenantRegistry {
295        &self.tenant_registry
296    }
297
298    /// Access the skill marketplace.
299    pub fn marketplace(&self) -> &SkillMarketplace {
300        &self.marketplace
301    }
302
303    /// Access the active MCP clients.
304    pub fn mcp_clients(&self) -> &Arc<DashMap<String, Arc<McpClient>>> {
305        &self.mcp_clients
306    }
307
308    // -- MCP server lifecycle ------------------------------------------------
309
310    /// Spawn and initialize all MCP servers defined in the configuration.
311    ///
312    /// Each server is started as a subprocess, initialized via JSON-RPC 2.0
313    /// handshake, and its tools are discovered. Servers that fail to start
314    /// are logged and skipped — they don't block the Ring from operating.
315    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    /// Shut down all active MCP servers gracefully.
362    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    /// Collect tool definitions from all active MCP servers.
374    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    // -- Tenant-scoped operations --------------------------------------------
392
393    /// Spawn a fighter scoped to a tenant, enforcing quota limits.
394    ///
395    /// Returns an error if the tenant is suspended or the fighter quota is
396    /// exceeded.
397    #[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        // Verify tenant exists and is active.
404        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        // Check fighter quota.
417        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        // Stamp the manifest with the tenant ID.
431        manifest.tenant_id = Some(*tenant_id);
432        Ok(self.spawn_fighter(manifest).await)
433    }
434
435    /// List fighters that belong to a specific tenant.
436    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    /// Kill a fighter, validating that the caller tenant owns it.
452    ///
453    /// Returns an error if the fighter doesn't belong to the given tenant.
454    #[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    /// Check whether a tenant's tool access is allowed for the given tool name.
478    ///
479    /// Returns `true` if the tenant has no tool restrictions (empty list) or
480    /// the tool is in the allowed list.
481    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    // -- Trigger operations --------------------------------------------------
492
493    /// Register a trigger with the engine.
494    pub fn register_trigger(&self, trigger: Trigger) -> TriggerId {
495        self.trigger_engine.register_trigger(trigger)
496    }
497
498    /// Remove a trigger by ID.
499    pub fn remove_trigger(&self, id: &TriggerId) {
500        self.trigger_engine.remove_trigger(id);
501    }
502
503    /// List all triggers with summary information.
504    pub fn list_triggers(&self) -> Vec<(TriggerId, TriggerSummary)> {
505        self.trigger_engine.list_triggers()
506    }
507
508    // -- Fighter operations --------------------------------------------------
509
510    /// Spawn a new fighter from a manifest.
511    ///
512    /// Returns the newly-assigned [`FighterId`]. The fighter starts in
513    /// [`FighterStatus::Idle`] and is persisted to the memory substrate.
514    #[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        // Persist to the database first so FK constraints work.
520        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        // Record metrics.
537        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        // --- Creed creation & binding ---
549        // Create a default creed if none exists, then bind it to this instance.
550        // This ensures every fighter has a consciousness layer from birth.
551        {
552            let memory = Arc::clone(&self.memory);
553            let creed_name = name.clone();
554            let fid = id;
555            // Clone the manifest for the creed before it's moved into the entry.
556            let manifest_for_creed = self.fighters.get(&id).map(|e| e.value().manifest.clone());
557            tokio::spawn(async move {
558                // Ensure a creed exists (create default if not).
559                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                // Bind the creed to this fighter instance.
570                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    /// Create a default creed for a fighter if none exists.
582    /// The default creed includes self-awareness from the manifest.
583    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                // Creed already exists.
587            }
588            Ok(None) => {
589                // Create a default creed with self-awareness.
590                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    /// Send a user message to a fighter and run the agent loop (without coordinator).
604    ///
605    /// Convenience wrapper around [`send_message_with_coordinator`] that passes
606    /// `None`, meaning the fighter will not have access to inter-agent tools.
607    #[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    /// Send a user message to a fighter and run the agent loop.
618    ///
619    /// This creates (or reuses) a bout for the fighter, checks quotas, then
620    /// delegates to [`run_fighter_loop`]. Usage is recorded through the
621    /// metering engine after a successful completion.
622    ///
623    /// If `coordinator` is provided, the fighter can use inter-agent tools
624    /// (`agent_spawn`, `agent_message`, `agent_list`).
625    #[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        // Look up the fighter.
633        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        // Check quota.
639        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        // Check budget enforcement (opt-in — only blocks if limits are configured).
648        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        // Ensure the fighter has an active bout.
669        let bout_id = match entry.current_bout {
670            Some(id) => id,
671            None => {
672                // Create the bout in the database.
673                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        // Mark as fighting and extract what we need before dropping the guard.
690        entry.status = FighterStatus::Fighting;
691        let manifest = entry.manifest.clone();
692        let fighter_name = manifest.name.clone();
693        drop(entry); // Release the DashMap guard before any async calls.
694
695        let mut available_tools = tools_for_capabilities(&manifest.capabilities);
696
697        // Merge MCP tools if the fighter has McpAccess capability.
698        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        // Publish the incoming user message event.
727        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        // Run the fighter loop.
735        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        // Record message metric.
757        self.metrics.counter_inc(metrics::MESSAGES_TOTAL);
758
759        let result = run_fighter_loop(params).await;
760
761        // Update state based on the outcome.
762        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                    // Record token usage metrics.
770                    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                    // Record usage through the metering engine.
778                    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                    // Publish response event.
792                    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                    // Publish bout ended.
801                    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                    // Publish error event.
812                    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    /// Kill (remove) a fighter.
824    #[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    /// List all fighters with their current status.
843    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    /// Get a snapshot of a single fighter's entry.
855    pub fn get_fighter(&self, fighter_id: &FighterId) -> Option<FighterEntry> {
856        self.fighters.get(fighter_id).map(|e| e.value().clone())
857    }
858
859    // -- Gorilla operations --------------------------------------------------
860
861    /// Register a gorilla with the Ring.
862    ///
863    /// Returns the newly-assigned [`GorillaId`]. The gorilla starts in
864    /// [`GorillaStatus::Caged`].
865    #[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    /// Unleash (start) a gorilla's background task.
883    ///
884    /// This uses the [`BackgroundExecutor`] to spawn the gorilla's autonomous
885    /// loop, which will run the fighter loop on the gorilla's schedule.
886    #[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        // Start the gorilla via the background executor.
907        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        // Record gorilla metrics.
920        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    /// Cage (stop) a gorilla's background task.
932    #[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        // Stop via background executor.
942        self.background.stop_gorilla(gorilla_id);
943
944        // Also abort any legacy task handle.
945        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    /// List all gorillas with their current status and metrics.
966    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    /// Get a gorilla's manifest by ID.
986    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    /// Find a gorilla ID by name (case-insensitive).
993    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    /// Run a single autonomous tick for a gorilla (for testing/debugging).
1004    ///
1005    /// This executes the gorilla's autonomous prompt once, without starting
1006    /// the background scheduler. Useful for verifying configuration.
1007    #[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    /// Get the LLM driver (useful for CLI commands that need to run ticks directly).
1033    pub fn driver(&self) -> &Arc<dyn LlmDriver> {
1034        &self.driver
1035    }
1036
1037    // -- Inter-agent communication -------------------------------------------
1038
1039    /// Send a message from one fighter to another.
1040    ///
1041    /// The source fighter's message becomes the target fighter's input,
1042    /// enriched with source context so the target knows who is speaking.
1043    /// The target processes it through its own fighter loop (with its own creed)
1044    /// and the response is returned.
1045    #[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        // Get source fighter name for context.
1053        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        // Verify target exists.
1062        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        // Wrap the message with source context so the target knows who is speaking.
1070        let enriched_message = format!(
1071            "[Message from fighter '{}' (id: {})]\n\n{}",
1072            source_name, source_id, message
1073        );
1074
1075        // Send to target through normal message flow (uses target's creed).
1076        self.send_message(target_id, enriched_message).await
1077    }
1078
1079    /// Find a fighter by name (case-insensitive).
1080    ///
1081    /// Returns the fighter ID and manifest if found.
1082    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    /// Update relationship tracking in both fighters' creeds after inter-agent
1093    /// communication.
1094    ///
1095    /// Loads both creeds, adds or updates the peer relationship entry
1096    /// (incrementing interaction_count), and saves them back.
1097    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        // Update in a spawned task to avoid blocking the caller.
1103        tokio::spawn(async move {
1104            // Update A's creed with relationship to B.
1105            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            // Update B's creed with relationship to A.
1113            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    // -- Troop / Swarm / Messaging accessors ---------------------------------
1123
1124    /// Get a reference to the troop manager.
1125    pub fn troop_manager(&self) -> &TroopManager {
1126        &self.troop_manager
1127    }
1128
1129    /// Get a reference to the swarm coordinator.
1130    pub fn swarm_coordinator(&self) -> &SwarmCoordinator {
1131        &self.swarm_coordinator
1132    }
1133
1134    /// Get a reference to the message router.
1135    pub fn message_router(&self) -> &MessageRouter {
1136        &self.message_router
1137    }
1138
1139    // -- Troop operations ----------------------------------------------------
1140
1141    /// Form a new troop with a leader and initial members.
1142    #[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        // Verify the leader exists.
1151        if self.fighters.get(&leader).is_none() {
1152            return Err(PunchError::Troop(format!(
1153                "leader fighter {} not found",
1154                leader
1155            )));
1156        }
1157
1158        // Verify all members exist.
1159        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; // +1 for leader if not in list
1169        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    /// Disband (dissolve) a troop.
1183    #[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    /// Assign a task to a troop, returning the fighters that should handle it.
1196    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    /// Assign a task to a troop asynchronously, returning full results
1205    /// including responses from fighters.
1206    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    /// Get the current status of a troop.
1215    pub fn get_troop_status(&self, troop_id: &TroopId) -> Option<Troop> {
1216        self.troop_manager.get_troop(troop_id)
1217    }
1218
1219    /// List all troops.
1220    pub fn list_troops(&self) -> Vec<Troop> {
1221        self.troop_manager.list_troops()
1222    }
1223
1224    /// Recruit a fighter into a troop.
1225    pub fn recruit_to_troop(&self, troop_id: &TroopId, fighter_id: FighterId) -> PunchResult<()> {
1226        // Verify the fighter exists.
1227        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    /// Dismiss a fighter from a troop.
1237    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    // -- Troop-aware fighter lifecycle ---------------------------------------
1246
1247    /// Kill a fighter, warning if they're in a troop.
1248    ///
1249    /// Unlike [`kill_fighter`], this checks troop membership and dismisses
1250    /// the fighter from all troops before killing them.
1251    #[instrument(skip(self), fields(%fighter_id))]
1252    pub fn kill_fighter_safe(&self, fighter_id: &FighterId) {
1253        // Dismiss from any troops first.
1254        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    // -- Workflow operations -------------------------------------------------
1269
1270    /// Register a workflow with the engine.
1271    pub fn register_workflow(&self, workflow: Workflow) -> WorkflowId {
1272        self.workflow_engine.register_workflow(workflow)
1273    }
1274
1275    /// Execute a workflow by ID with the given input.
1276    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    // -- Shutdown ------------------------------------------------------------
1293
1294    /// Gracefully shut down the Ring, stopping all gorillas and background tasks.
1295    pub fn shutdown(&self) {
1296        info!("Ring shutdown initiated");
1297
1298        // Signal shutdown to all background tasks.
1299        let _ = self.shutdown_tx.send(true);
1300
1301        // Stop all gorilla tasks via the background executor.
1302        self.background.shutdown_all();
1303
1304        info!("Ring shutdown complete");
1305    }
1306}
1307
1308// ---------------------------------------------------------------------------
1309// Relationship tracking helper
1310// ---------------------------------------------------------------------------
1311
1312/// Truncate a string to a maximum length, respecting UTF-8 char boundaries.
1313fn truncate_preview(s: &str, max_len: usize) -> String {
1314    if s.len() <= max_len {
1315        return s.to_string();
1316    }
1317    // Find the last char boundary at or before max_len - 3 (room for "...")
1318    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
1327/// Update or insert a peer relationship in a creed.
1328fn 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// ---------------------------------------------------------------------------
1360// AgentCoordinator implementation
1361// ---------------------------------------------------------------------------
1362
1363#[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
1406// ---------------------------------------------------------------------------
1407// Compile-time Send + Sync assertion
1408// ---------------------------------------------------------------------------
1409
1410/// Compile-time assertion that `Ring` is `Send + Sync`.
1411const _: () = {
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};