gioser
⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229 crates/ente-zero/src/brain_glue.rs Rust · 129 líneas · 4.8 KB Historia Quién tocó qué crudo

Pausa: 11 crates del fractal Ente #0 con cerebro completo d6b8f18b4 · Sergio ·

1//! Glue entre el bucle primordial y `ente-brain`.
2//!
3//! Tres responsabilidades:
4//!   1. Traducir eventos del grafo (`GraphEvent`) a `ente_brain::EventKind`
5//!      + `SubjectInfo` para el observador y el motor.
6//!   2. Implementar `ActionSink` para que las Acciones del cerebro tengan
7//!      un canal de salida hacia el grafo (Spawn → SpawnRequest, etc.).
8//!   3. Encapsular el snapshot de SubjectInfo desde el grafo sin filtrar
9//!      detalles internos al cerebro.
10
11use crate::events::GraphEvent;
12use crate::graph::EnteGraph;
13use ente_brain::{ActionSink, EventKind as BrainEventKind, SubjectInfo};
14use ente_card::Capability;
15use serde::Deserialize;
16use tokio::sync::mpsc;
17use tracing::warn;
18use ulid::Ulid;
19
20/// Traduce un GraphEvent a (EventKind, SubjectInfo) para alimentar el cerebro.
21///
22/// Devuelve `None` para eventos puramente internos del bus (Response, Close)
23/// que no son interesantes para reglas o estadística.
24pub fn graph_event_to_brain<'a>(
25    evt: &'a GraphEvent,
26    graph: &EnteGraph,
27) -> Option<(BrainEventKind, SubjectInfo)> {
28    match evt {
29        GraphEvent::EnteDied { id, .. } => {
30            Some((BrainEventKind::EnteDied, subject_info_for(graph, *id)))
31        }
32        GraphEvent::SpawnRequest { card, .. } => {
33            // El "sujeto" del spawn es el child que va a nacer.
34            let info = SubjectInfo {
35                id: Some(card.id),
36                label: Some(card.label.clone()),
37                capabilities: card.provides.iter().cloned().collect(),
38            };
39            Some((BrainEventKind::EnteSpawned, info))
40        }
41        GraphEvent::BusRequest { from, request, .. } => {
42            let kind = match request {
43                ente_bus::BusRequest::Announce { .. } => BrainEventKind::BusAnnounce,
44                ente_bus::BusRequest::Invoke { cap, .. } => {
45                    BrainEventKind::BusInvokeOf(cap.clone())
46                }
47                _ => BrainEventKind::BusInvoke,
48            };
49            let info = match from {
50                Some(id) => subject_info_for(graph, *id),
51                None => SubjectInfo::default(),
52            };
53            Some((kind, info))
54        }
55        GraphEvent::CapabilityRequested { from, .. } => {
56            Some((BrainEventKind::BusInvoke, subject_info_for(graph, *from)))
57        }
58        // Responses, ConnClosed, Shutdown — irrelevantes para reglas
59        _ => None,
60    }
61}
62
63fn subject_info_for(graph: &EnteGraph, id: Ulid) -> SubjectInfo {
64    // Acceso de sólo lectura — usamos el método público lookup_pid + cards
65    // virtuales en el grafo. Si el Ente no existe (ya disuelto), info vacía.
66    if let Some(card) = graph.card_for(&id) {
67        SubjectInfo {
68            id: Some(id),
69            label: Some(card.label.clone()),
70            capabilities: card.provides.iter().cloned().collect(),
71        }
72    } else {
73        SubjectInfo { id: Some(id), label: None, capabilities: Vec::new() }
74    }
75}
76
77/// `ActionSink` que enruta acciones del cerebro al bucle primordial.
78pub struct GraphSink {
79    pub graph_tx: mpsc::Sender<GraphEvent>,
80    pub requester: Ulid,
81}
82
83impl ActionSink for GraphSink {
84    fn spawn(&self, card_blob: &str) {
85        // El blob es JSON de EntityCard.
86        match serde_json::from_str::<ente_card::EntityCard>(card_blob) {
87            Ok(card) => {
88                let evt = GraphEvent::SpawnRequest { card, requester: self.requester };
89                if self.graph_tx.try_send(evt).is_err() {
90                    warn!("brain spawn: graph_tx lleno o cerrado");
91                }
92            }
93            Err(e) => warn!(?e, "brain spawn: blob no parseable como EntityCard JSON"),
94        }
95    }
96
97    fn invoke(&self, target_cap: Capability, blob: Vec<u8>) {
98        // Sin BusClient en proceso — el sink registra la intención. Una mejora
99        // futura: spawn un BusClient::connect + call. Por ahora log estructurado.
100        warn!(?target_cap, blob_len = blob.len(), "brain invoke: no bus client en glue (TODO)");
101    }
102
103    fn notify(&self, target_id: Ulid, message: &str) {
104        warn!(%target_id, %message, "brain notify: no implementado en glue");
105    }
106
107    fn inhibit(&self, reason: &str) {
108        warn!(%reason, "brain inhibit: no implementado en glue");
109    }
110}
111
112/// Helper para que el grafo exponga la Card de un Ente vivo. Lo añadimos como
113/// trait extension porque graph::EnteGraph mantiene `incarnated` privado.
114pub trait GraphCardLookup {
115    fn card_for(&self, id: &Ulid) -> Option<&ente_card::EntityCard>;
116}
117
118impl GraphCardLookup for EnteGraph {
119    fn card_for(&self, id: &Ulid) -> Option<&ente_card::EntityCard> {
120        // Acceso vía método público que añadiremos en graph/mod.rs.
121        self.peek_card(id)
122    }
123}
124
125// Eliminar el campo `_unused` que rustc puede quejarse — placeholder para
126// evitar warning si algún field queda sin uso.
127#[allow(dead_code)]
128#[derive(Deserialize)]
129struct _Touch {}