feat(sidecar): API reusable de discovery via broker

Promueve el patron ad-hoc discover_producer_socket que vivia inline en
'nouser attract --remote' a un modulo publico brahman_sidecar::discovery.
Cualquier consumer ahora puede preguntar al broker "quien provee este
TypeRef?" sin reimplementar el patron a mano.

API:
- build_consumer_card(label, flow_name, type_name) construye una Card
  minima (Ente, Oneshot, Virtual) con un input flow. Asigna Ulid::new()
  real (no nil), evitando colisiones en el broker.
- await_provider(card, timeout) async: conecta al init, espera
  MatchEvent::Available, devuelve producer_service_socket, manda
  Farewell. Ignora eventos Lost durante el await.
- await_provider_blocking(card, timeout) wrapper para mundos no-async
  (CLIs, std-thread loops). Crea su propio runtime current_thread.
- ConsumerError tipado: Connect{socket,source}, NoProvider{flow,type_ref,
  timeout}, Client(ClientError), Runtime(String). Adios al Box<dyn Error>.

Refactor en nouser daemon: discover_producer_socket inline (60 LOC) ->
5 LOC delegando en el helper. remote_embed ya no construye su propio
runtime.

Tests: 4 unitarios (id no-nil, id unico por llamada, formateo de Wit
TypeRef, fallback sin input). Build verde para sidecar y nouser-core.
This commit is contained in:
Sergio
2026-05-09 02:30:48 +00:00
parent 2725d6a297
commit 006640057a
6 changed files with 294 additions and 75 deletions
+15 -75
View File
@@ -651,12 +651,12 @@ fn cmd_attract(args: &[String]) -> Cmd {
/// Pipeline completo del modo `--remote`:
/// 1. Si `NOUSER_NOUS_SOCKET` está set, lo usa directo (override
/// explícito, atajo para tests).
/// 2. Si no, abre Client al brahman-init, anuncia un consumer Card
/// con `flow.input = embed-result:json`, espera el primer
/// `MatchEvent::Available`, y usa el `producer_service_socket`
/// del evento. Esto activa la lógica de `priority_contexts`: si
/// el broker corre bajo `BRAHMAN_BROKER_CONTEXT=test/prod`, el
/// proveedor electo cambia sin que este consumer toque su código.
/// 2. Si no, delega en `brahman_sidecar::await_provider_blocking` —
/// el sidecar se conecta al broker, registra un consumer Card con
/// `flow.input = embed-result:json`, espera el primer
/// `MatchEvent::Available` y devuelve el socket. Esto activa la
/// lógica de `priority_contexts`: bajo `BRAHMAN_BROKER_CONTEXT=test/prod`,
/// el proveedor electo cambia sin que este código toque nada.
/// 3. Con el socket resuelto, dispara la RPC `EmbedFile`.
///
/// Devuelve `(embedding, model_id)` — el caller necesita ambos para
@@ -669,11 +669,15 @@ fn remote_embed(
return embed_via(&sock, file);
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
let producer_sock = rt.block_on(discover_producer_socket())?;
let consumer = brahman_sidecar::build_consumer_card(
"nouser.attract-cli",
nouser_nous::FLOW_EMBED_RESULT,
nouser_nous::FLOW_TYPE_NAME,
);
let producer_sock = brahman_sidecar::await_provider_blocking(
consumer,
std::time::Duration::from_secs(3),
)?;
embed_via(&producer_sock, file)
}
@@ -720,70 +724,6 @@ fn embed_via(
Err(format!("nouser-nous: {}", err.error).into())
}
/// Conecta al brahman-init, anuncia un consumer Card y espera el
/// primer `MatchEvent::Available`. Devuelve el `producer_service_socket`
/// que el broker emite. Timeout 3s.
async fn discover_producer_socket() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
use brahman_card::{
ulid::Ulid, Card, CardKind, Flow, Flows, Lifecycle, Payload, Priority, Supervision,
TypeRef,
};
use brahman_handshake::client::Client;
use brahman_handshake::messages::MatchEventKind;
let consumer_card = Card {
schema_version: brahman_card::CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: "nouser.attract-cli".into(),
payload: Payload::Virtual,
supervision: Supervision::OneShot,
lifecycle: Lifecycle::Oneshot,
priority: Priority::Normal,
kind: CardKind::Ente,
flow: Flows {
input: vec![Flow {
name: nouser_nous::FLOW_EMBED_RESULT.into(),
ty: TypeRef::Primitive {
name: nouser_nous::FLOW_TYPE_NAME.into(),
},
pin_to: None,
}],
output: vec![],
},
..Default::default()
};
let init_path = brahman_handshake::transport::default_socket_path();
let mut client = Client::connect(&init_path, consumer_card)
.await
.map_err(|e| format!("conectar a brahman-init en {}: {e}", init_path.display()))?;
// El broker empuja MatchEvents tras registrar la sesión. Iteramos
// hasta encontrar Available; ignoramos Lost (no aplica al arranque).
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
let socket = loop {
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {
break None;
}
match client.await_event(remaining).await? {
Some(ev) if ev.kind == MatchEventKind::Available => {
break ev.producer_service_socket;
}
Some(_) => continue, // Lost u otros — seguir esperando
None => break None,
}
};
let _ = client.farewell().await; // best-effort cleanup
socket.ok_or_else(|| {
"ningún proveedor con service_socket matcheó el input embed-result \
(¿está corriendo nouser-nous-mock o nouser-nous-real?)"
.into()
})
}
/// Card del propio engine (kind=Ente). Es el "ser" que produce y
/// administra Mónadas; aparece en brahman-status junto a sus Mónadas.
fn build_engine_card() -> brahman_card::Card {