1use std::any::Any;
22use std::collections::{HashMap, HashSet};
23
24use bb_ir::ids::PeerId;
25use bb_ir::keys::{
26 parse_binding_key, parse_binding_value, read_model_metadata, BINDING_KEY_PREFIX,
27 COMPILED_CURRENT_VERSION, COMPILED_KEY,
28};
29use bb_ir::proto::onnx::{FunctionProto, ModelProto};
30use bb_ir::registry::find_concrete_component;
31use bb_runtime::concrete::ComponentHandle;
32use bb_runtime::engine::dispatch_entry::FunctionKey;
33use bb_runtime::framework::Address;
34use bb_runtime::ids::ComponentRef;
35use bb_runtime::node::Node;
36use bb_runtime::registry::ComponentRole as R;
37use bb_runtime::registry::{dispatcher_for, roles_for_component};
38
39pub struct Config {
44 configs: HashMap<String, Box<dyn Any>>,
45}
46
47impl Config {
48 pub fn new() -> Self {
50 Self {
51 configs: HashMap::new(),
52 }
53 }
54
55 pub fn with<C: Any + 'static>(mut self, slot: impl Into<String>, config: C) -> Self {
58 self.configs.insert(slot.into(), Box::new(config));
59 self
60 }
61}
62
63impl Default for Config {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69#[derive(Debug)]
71pub enum InstallError {
72 NotCompiled,
75
76 IncompatibleCompiledVersion {
78 got: String,
80 expected: &'static str,
82 },
83
84 UnknownTarget {
86 target: String,
88 available: Vec<String>,
90 },
91
92 InvalidBindingTable {
95 key: String,
97 detail: String,
99 },
100
101 UnregisteredConcrete {
105 type_name: String,
107 },
108
109 MissingConfig {
112 slot: String,
114 type_name: String,
116 },
117
118 ConfigTypeMismatch {
121 slot: String,
123 type_name: String,
125 detail: String,
127 },
128
129 ConstructionFailed {
131 slot: String,
133 type_name: String,
135 detail: String,
137 },
138
139 SlotBindingConflict {
143 slot: String,
145 conflicts: Vec<(String, String, String)>,
147 },
148
149 EmptyTargets,
151}
152
153impl std::fmt::Display for InstallError {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 Self::NotCompiled => write!(
157 f,
158 "install: ModelProto carries no `{COMPILED_KEY}` metadata stamp; \
159 only `bb_compiler::Compiler::compile()` output may be installed",
160 ),
161 Self::IncompatibleCompiledVersion { got, expected } => write!(
162 f,
163 "install: ModelProto was compiled against `{got}` but this framework \
164 requires `{expected}`; recompile",
165 ),
166 Self::UnknownTarget { target, available } => write!(
167 f,
168 "install: target function `{target}` not found in model.functions[]; \
169 available targets: {available:?}",
170 ),
171 Self::InvalidBindingTable { key, detail } => write!(
172 f,
173 "install: binding metadata entry `{key}` is malformed: {detail}",
174 ),
175 Self::UnregisteredConcrete { type_name } => write!(
176 f,
177 "install: artifact references `{type_name}` but no \
178 `inventory::submit!` carrier registers it in this binary",
179 ),
180 Self::MissingConfig { slot, type_name } => write!(
181 f,
182 "install: slot `{slot}` expected a config for `{type_name}` \
183 (its `Config` associated type is not `()`); add \
184 `Config::new().with(\"{slot}\", <config>)`",
185 ),
186 Self::ConfigTypeMismatch {
187 slot,
188 type_name,
189 detail,
190 } => write!(
191 f,
192 "install: slot `{slot}` received a config whose type does not \
193 match `{type_name}`'s `Config` associated type ({detail})",
194 ),
195 Self::ConstructionFailed {
196 slot,
197 type_name,
198 detail,
199 } => write!(
200 f,
201 "install: `{type_name}::new` for slot `{slot}` returned an error: {detail}",
202 ),
203 Self::SlotBindingConflict { slot, conflicts } => {
204 write!(
205 f,
206 "install: slot `{slot}` has conflicting bindings across targets:",
207 )?;
208 for (target, type_name, role) in conflicts {
209 write!(f, "\n target `{target}` → `{type_name}` (role `{role}`)")?;
210 }
211 Ok(())
212 }
213 Self::EmptyTargets => write!(
214 f,
215 "install: targets slice is empty; supply at least one target name",
216 ),
217 }
218 }
219}
220
221impl std::error::Error for InstallError {}
222
223pub fn install(
238 peer_id: PeerId,
239 addresses: Vec<Address>,
240 model: ModelProto,
241 targets: &[&str],
242 config: Config,
243) -> Result<Node, InstallError> {
244 bb_ops::link_force();
246
247 if targets.is_empty() {
248 return Err(InstallError::EmptyTargets);
249 }
250
251 verify_compilation_stamp(&model)?;
252
253 let mut resolved_target_names: Vec<String> = Vec::with_capacity(targets.len());
254 let mut per_target_bindings: Vec<Vec<ResolvedBinding>> = Vec::with_capacity(targets.len());
255 for raw in targets {
256 let target_function = find_target(&model, raw)?;
257 let resolved_name = target_function.name.clone();
258 let bindings = parse_target_bindings(&model, &resolved_name)?;
259 resolved_target_names.push(resolved_name);
260 per_target_bindings.push(bindings);
261 }
262
263 let unified = dedupe_bindings_across_targets(&resolved_target_names, &per_target_bindings)?;
264
265 let mut node = Node::new(peer_id, addresses);
266 let mut registered_dispatchers: HashSet<(&'static str, &'static str)> = HashSet::new();
267 let unit_default: &dyn Any = &();
268
269 for (idx, binding) in unified.iter().enumerate() {
270 let next_cref = idx as u32;
271 let entry = find_concrete_component(&binding.type_name).ok_or_else(|| {
272 InstallError::UnregisteredConcrete {
273 type_name: binding.type_name.clone(),
274 }
275 })?;
276
277 let supplied: &dyn Any = config
279 .configs
280 .get(&binding.slot)
281 .map(|b| b.as_ref())
282 .unwrap_or(unit_default);
283
284 let instance = (entry.construct_fn)(supplied).map_err(|e| {
285 if e.detail.starts_with("config type mismatch:") {
286 InstallError::ConfigTypeMismatch {
287 slot: binding.slot.clone(),
288 type_name: binding.type_name.clone(),
289 detail: e.detail,
290 }
291 } else {
292 InstallError::ConstructionFailed {
293 slot: binding.slot.clone(),
294 type_name: binding.type_name.clone(),
295 detail: e.detail,
296 }
297 }
298 })?;
299
300 register_dispatchers_for(
301 node.engine_install_handle(),
302 entry.type_name,
303 &mut registered_dispatchers,
304 );
305 let cref = ComponentRef::from(next_cref);
306 let instance_id = next_cref;
307 let engine = node.engine_install_handle();
308 engine.register_component(cref, instance);
309 engine.bind_slot(binding.slot.clone(), cref);
310 if let Some(slot_id) = binding.slot_id {
311 engine.bind_slot_id(slot_id, cref);
312 if let Some(role) = parse_role(&binding.role) {
313 engine.bind_slot_id_with_role(slot_id, role, cref);
314 }
315 }
316 stamp_component_roles(engine, entry.type_name, cref);
317
318 node.push_linked_component(ComponentHandle {
319 type_name: entry.type_name,
320 package: entry.package,
321 instance_id,
322 serialize_fn: entry.serialize_fn,
323 restore_fn: entry.restore_fn,
324 state_bytes: Vec::new(),
325 });
326 }
327
328 install_targets(node.engine_install_handle(), &model, &resolved_target_names);
331 node.engine_install_handle().resolve_dispatch();
332
333 node.set_model(model);
335 for resolved in &resolved_target_names {
336 node.register_module(resolved.clone());
337 }
338
339 Ok(node)
340}
341
342fn verify_compilation_stamp(model: &ModelProto) -> Result<(), InstallError> {
344 let Some(got) = read_model_metadata(model, COMPILED_KEY) else {
345 return Err(InstallError::NotCompiled);
346 };
347 if got != COMPILED_CURRENT_VERSION {
348 return Err(InstallError::IncompatibleCompiledVersion {
349 got: got.to_string(),
350 expected: COMPILED_CURRENT_VERSION,
351 });
352 }
353 Ok(())
354}
355
356fn find_target<'a>(model: &'a ModelProto, target: &str) -> Result<&'a FunctionProto, InstallError> {
359 if let Some(exact) = model.functions.iter().find(|f| f.name == target) {
360 return Ok(exact);
361 }
362 let prefix = format!("{target}#");
363 if let Some(suffixed) = model.functions.iter().find(|f| f.name.starts_with(&prefix)) {
364 return Ok(suffixed);
365 }
366 let available = model
367 .functions
368 .iter()
369 .map(|f| f.name.clone())
370 .collect::<Vec<_>>();
371 Err(InstallError::UnknownTarget {
372 target: target.to_string(),
373 available,
374 })
375}
376
377#[derive(Debug, Clone)]
379struct ResolvedBinding {
380 slot: String,
381 type_name: String,
382 slot_id: Option<u32>,
386 role: String,
391}
392
393fn parse_target_bindings(
396 model: &ModelProto,
397 target_name: &str,
398) -> Result<Vec<ResolvedBinding>, InstallError> {
399 let mut out = Vec::new();
400 for entry in &model.metadata_props {
401 if !entry.key.starts_with(BINDING_KEY_PREFIX) {
402 continue;
403 }
404 let Some((target, slot)) = parse_binding_key(&entry.key) else {
405 return Err(InstallError::InvalidBindingTable {
406 key: entry.key.clone(),
407 detail: "key not in `ai.bytesandbrains.binding.<target>.<slot>` form".into(),
408 });
409 };
410 if target != target_name {
411 continue;
412 }
413 let Some((role, type_name, slot_id)) = parse_binding_value(&entry.value) else {
414 return Err(InstallError::InvalidBindingTable {
415 key: entry.key.clone(),
416 detail: format!(
417 "value `{}` not in `<role>|<TYPE_NAME>|<slot_id|-1>` form",
418 entry.value
419 ),
420 });
421 };
422 let slot_id = if slot_id < 0 {
423 None
424 } else {
425 Some(slot_id as u32)
426 };
427 out.push(ResolvedBinding {
428 slot: slot.to_string(),
429 type_name: type_name.to_string(),
430 slot_id,
431 role: role.to_string(),
432 });
433 }
434 Ok(out)
435}
436
437fn register_dispatchers_for(
438 engine: &mut bb_runtime::engine::Engine,
439 type_name: &'static str,
440 registered: &mut HashSet<(&'static str, &'static str)>,
441) {
442 for role in roles_for_component(type_name) {
443 let key = (type_name, role_as_str(role));
444 if !registered.insert(key) {
445 continue;
446 }
447 if let Some(register_fn) = dispatcher_for(type_name, role) {
448 register_fn(engine);
449 }
450 }
451}
452
453fn stamp_component_roles(
454 engine: &mut bb_runtime::engine::Engine,
455 type_name: &str,
456 cref: ComponentRef,
457) {
458 let roles: std::collections::HashSet<bb_runtime::registry::ComponentRole> =
459 roles_for_component(type_name).collect();
460 if !roles.is_empty() {
461 engine.set_component_roles(cref, roles);
462 }
463}
464
465fn install_targets(
473 engine: &mut bb_runtime::engine::Engine,
474 model: &ModelProto,
475 resolved_target_names: &[String],
476) {
477 let target_set: HashSet<&str> = resolved_target_names.iter().map(|s| s.as_str()).collect();
478 let mut entry_point_keys: Vec<FunctionKey> = Vec::with_capacity(resolved_target_names.len());
479 for resolved in resolved_target_names {
480 let Some(entry) = model
481 .functions
482 .iter()
483 .find(|f| f.name == *resolved)
484 .cloned()
485 else {
486 continue;
487 };
488 entry_point_keys.push((
489 entry.domain.clone(),
490 entry.name.clone(),
491 entry.overload.clone(),
492 ));
493 engine.install_graph(entry.name.clone(), entry);
494 }
495
496 let sub_functions: Vec<bb_ir::proto::onnx::FunctionProto> = model
497 .functions
498 .iter()
499 .filter(|f| !target_set.contains(f.name.as_str()))
500 .cloned()
501 .collect();
502 engine.install_function_library(&sub_functions, &entry_point_keys);
503}
504
505#[derive(Debug, Clone)]
509struct UnifiedBinding {
510 slot: String,
513 type_name: String,
516 slot_id: Option<u32>,
520 role: String,
522}
523
524fn dedupe_bindings_across_targets(
528 target_names: &[String],
529 per_target_bindings: &[Vec<ResolvedBinding>],
530) -> Result<Vec<UnifiedBinding>, InstallError> {
531 let mut order: Vec<String> = Vec::new();
534 let mut by_slot: HashMap<String, UnifiedBinding> = HashMap::new();
535 let mut contributors: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
536
537 for (target_idx, bindings) in per_target_bindings.iter().enumerate() {
538 let target_name = &target_names[target_idx];
539 for binding in bindings {
540 contributors.entry(binding.slot.clone()).or_default().push((
541 target_name.clone(),
542 binding.type_name.clone(),
543 binding.role.clone(),
544 ));
545 match by_slot.get(&binding.slot) {
546 None => {
547 order.push(binding.slot.clone());
548 by_slot.insert(
549 binding.slot.clone(),
550 UnifiedBinding {
551 slot: binding.slot.clone(),
552 type_name: binding.type_name.clone(),
553 slot_id: binding.slot_id,
554 role: binding.role.clone(),
555 },
556 );
557 }
558 Some(existing) => {
559 if existing.type_name != binding.type_name || existing.role != binding.role {
560 return Err(InstallError::SlotBindingConflict {
561 slot: binding.slot.clone(),
562 conflicts: contributors.remove(&binding.slot).unwrap_or_default(),
563 });
564 }
565 }
566 }
567 }
568 }
569
570 Ok(order
571 .into_iter()
572 .map(|slot| by_slot.remove(&slot).expect("slot inserted above"))
573 .collect())
574}
575
576fn role_as_str(role: bb_runtime::registry::ComponentRole) -> &'static str {
577 match role {
578 R::Index => "Index",
579 R::Aggregator => "Aggregator",
580 R::Model => "Model",
581 R::Codec => "Codec",
582 R::DataSource => "DataSource",
583 R::PeerSelector => "PeerSelector",
584 R::Backend => "Backend",
585 R::Protocol => "Protocol",
586 }
587}
588
589fn parse_role(role: &str) -> Option<bb_runtime::registry::ComponentRole> {
593 match role {
594 "Index" => Some(R::Index),
595 "Aggregator" => Some(R::Aggregator),
596 "Model" => Some(R::Model),
597 "Codec" => Some(R::Codec),
598 "DataSource" => Some(R::DataSource),
599 "PeerSelector" => Some(R::PeerSelector),
600 "Backend" => Some(R::Backend),
601 "Protocol" => Some(R::Protocol),
602 _ => None,
603 }
604}
605