feat(nouser): Phase D — proveedor Nous mock + cliente remoto
Cierra el patrón "Nous como módulo aparte intercambiable": el contrato
del proveedor de embeddings vive en su crate, el mock determinístico
implementa ese contrato sirviéndolo por Unix socket, y nouser-core
sabe consumirlo remotamente. El switch mock↔real (futuro) será vía
priority_contexts en el broker.
Crates nuevos:
- crates/modules/nouser/nous: contrato compartido.
- EmbedRequest { kind: { EmbedFile | EmbedText | Ping }, payload }.
- EmbedFilePayload (path, ext, size, mtime), EmbedTextPayload.
- EmbedResponse (embedding, model, elapsed_ms), PingResponse,
ErrorResponse.
- Wire: line-delimited JSON sobre Unix socket, single-shot.
- Constants FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NAME.
- transport::default_socket_path con env NOUSER_NOUS_SOCKET.
- crates/modules/nouser/nous-mock: bin nouser-nous-mock.
- Sidecarea a brahman-init con Card kind=Ente declarando los flows
embed-request/embed-result + priority_contexts.test = +1.
- Bind del socket Nous + accept loop tokio.
- EmbedFile delega a nouser_core::embed::embed (Phase C).
- Modelo: "mock-pseudo-32d".
Cambios:
- nouser-core: dep nueva nouser-nous. Subcomando attract --remote
abre un UnixStream blocking, envía EmbedRequest, lee response.
Imprime "embed: local|remote" para ver cuál ruta corrió.
Bug encontrado y corregido:
- ContextBias tenía #[serde(skip_serializing_if = ...)] en sus campos.
Postcard NO soporta skip-condicional en formatos no self-describing:
el serializer omitía bytes que el deserializer esperaba, rompiendo
la wire de cualquier Card con priority_contexts poblada.
Síntoma: "postcard decode: Hit the end of buffer" en el server,
"early eof" en el cliente.
- Fix: removidos los skip_serializing_if de ContextBias. JSON pretty
ahora emite {"pin_to": null, "priority_offset": 0} pero el wire
funciona. Trade-off aceptado.
- Test wirecard_postcard_with_priority_contexts en brahman-card que
ejercita el roundtrip postcard con biases poblados.
Validación end-to-end:
$ ente-zero & nouser-nous-mock & nouser daemon crates/core
$ brahman-status
Sessions (7):
[ente] nouser.nous_mock flows: embed-request, embed-result
[ente] brahman.nouser_engine
[data] src summary: 6 archivos en crates/core/brahman-handshake/src
[data] graph summary: 7 archivos en crates/core/ente-zero/src/graph
...
$ nouser attract --remote crates/core <archivo>.rs
embed: remote
🧲 0.9058 src ...
(mock log: embed_file path=...)
Tests: 75. cargo check --workspace: 0 errores, 0 warnings.
Próximo natural: Phase D-2 — real-nous con ONNX/Llama text-embedding.
Declara la misma Card con priority_contexts.prod = +1 y el swap es
transparente para el consumer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -472,20 +472,20 @@ pub enum Priority {
|
||||
pub struct ContextBias {
|
||||
/// Override del `pin_to` estático cuando el broker está en este
|
||||
/// contexto y la Card actúa como consumidor.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
///
|
||||
/// **No se usa `skip_serializing_if` aquí**: postcard requiere
|
||||
/// layout fijo. La verbosidad extra en JSON (campos null/cero
|
||||
/// emitidos) es el costo aceptado para compatibilidad de wire.
|
||||
#[serde(default)]
|
||||
pub pin_to: Option<String>,
|
||||
|
||||
/// Modifica la priority efectiva del Card como productor.
|
||||
/// `+1` lo eleva, `-1` lo baja. El resultado se clampa al rango de
|
||||
/// `Priority` ([Low, Critical]).
|
||||
#[serde(default, skip_serializing_if = "is_zero_i8")]
|
||||
#[serde(default)]
|
||||
pub priority_offset: i8,
|
||||
}
|
||||
|
||||
fn is_zero_i8(v: &i8) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Flujos tipados (del modelo brahman)
|
||||
// =====================================================================
|
||||
@@ -1093,6 +1093,59 @@ mod tests {
|
||||
assert!(c_back.extensions.is_empty(), "extensions sobreviven al wire");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wirecard_postcard_with_priority_contexts() {
|
||||
// Repro del bug que rompía nouser-nous-mock: ContextBias con
|
||||
// skip_serializing_if causaba que postcard leyera bytes
|
||||
// equivocados. Sin esos atributos, el roundtrip es estable.
|
||||
let src = r#"{
|
||||
"schema_version": 1,
|
||||
"id": "01HQAR53D4M2NBV8KZTYXFGS01",
|
||||
"label": "x",
|
||||
"soma": {
|
||||
"namespaces": {"mount":false,"pid":false,"net":false,"uts":false,"ipc":false,"user":false,"cgroup":false},
|
||||
"rlimits": {"mem_bytes":null,"nproc":null,"nofile":null},
|
||||
"cgroup": {"path":"x","cpu_weight":null,"io_weight":null},
|
||||
"cpu_affinity": null
|
||||
},
|
||||
"payload": "Virtual",
|
||||
"supervision": "OneShot"
|
||||
}"#;
|
||||
let mut c = Card::from_json(src).unwrap();
|
||||
c.priority_contexts.insert(
|
||||
"test".into(),
|
||||
ContextBias {
|
||||
pin_to: None,
|
||||
priority_offset: 1,
|
||||
},
|
||||
);
|
||||
c.priority_contexts.insert(
|
||||
"prod".into(),
|
||||
ContextBias {
|
||||
pin_to: Some("real-nous".into()),
|
||||
priority_offset: 2,
|
||||
},
|
||||
);
|
||||
|
||||
let wire: WireCard = c.into();
|
||||
let bytes = postcard::to_allocvec(&wire).expect("postcard encode");
|
||||
let decoded: WireCard = postcard::from_bytes(&bytes).expect("postcard decode");
|
||||
|
||||
assert_eq!(decoded.priority_contexts.len(), 2);
|
||||
let test_bias = decoded
|
||||
.priority_contexts
|
||||
.get("test")
|
||||
.expect("test context");
|
||||
assert_eq!(test_bias.priority_offset, 1);
|
||||
assert!(test_bias.pin_to.is_none());
|
||||
let prod_bias = decoded
|
||||
.priority_contexts
|
||||
.get("prod")
|
||||
.expect("prod context");
|
||||
assert_eq!(prod_bias.pin_to.as_deref(), Some("real-nous"));
|
||||
assert_eq!(prod_bias.priority_offset, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_card_postcard_friendly() {
|
||||
// Validación: WireCard puede ser postcard-encoded sin error.
|
||||
|
||||
Reference in New Issue
Block a user