Skip to main content

monoloop_loop/transaction/
channel_registry.rs

1//! Immutable Channel registry and live bindings.
2
3use monoloop_connector::{ConnectorFactory, ConnectorInstance};
4use monoloop_contracts::{
5    ChannelCapabilities, ChannelDefaults, ChannelDescriptor, ChannelId, ChannelKind, ChannelLimits,
6    OutboundDialectEncoder, ToolExecutionMode,
7};
8use monoloop_interpreter::InterpreterFactory;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12/// One Channel's static binding (factories realized at runtime start).
13pub struct ChannelBinding {
14    /// Channel identity.
15    pub id: ChannelId,
16    /// External agent vs direct LLM.
17    pub kind: ChannelKind,
18    /// Tool execution mode.
19    pub tool_mode: ToolExecutionMode,
20    /// Matched Connector factory (one instance per Channel at start).
21    pub connector_factory: Arc<dyn ConnectorFactory>,
22    /// Outbound dialect encoder.
23    pub encoder: Arc<dyn OutboundDialectEncoder>,
24    /// Interpreter factory for this Channel's output dialect.
25    pub interpreter: Arc<dyn InterpreterFactory>,
26    /// Transport endpoint reference for `OpenConnection`.
27    pub endpoint_ref: String,
28    /// Optional credential reference for open.
29    pub credential_ref: Option<String>,
30    /// Channel defaults for effective config merge.
31    pub defaults: ChannelDefaults,
32    /// Declared capabilities.
33    pub capabilities: ChannelCapabilities,
34    /// Per-Channel limits.
35    pub limits: ChannelLimits,
36}
37
38impl ChannelBinding {
39    /// View as a data-only descriptor for capability validation.
40    pub fn descriptor(&self) -> ChannelDescriptor {
41        ChannelDescriptor {
42            kind: self.kind,
43            tool_mode: self.tool_mode,
44            capabilities: self.capabilities.clone(),
45            limits: self.limits.clone(),
46        }
47    }
48}
49
50/// Immutable registry of Channel bindings (built before start).
51pub struct ChannelRegistry {
52    channels: HashMap<ChannelId, ChannelBinding>,
53}
54
55impl ChannelRegistry {
56    /// Build from bindings; rejects duplicate IDs.
57    pub fn build(bindings: Vec<ChannelBinding>) -> Result<Self, super::StartupError> {
58        if bindings.is_empty() {
59            return Err(super::StartupError::ChannelRegistry(
60                "at least one Channel is required",
61            ));
62        }
63        let mut channels = HashMap::with_capacity(bindings.len());
64        for b in bindings {
65            b.descriptor().validate()?;
66            if channels.contains_key(&b.id) {
67                return Err(super::StartupError::ChannelRegistry("duplicate ChannelId"));
68            }
69            // Dialect on encoder path is checked when encoding (WP-08); startup
70            // requires input/output descriptors already match via ChannelDescriptor.
71            channels.insert(b.id.clone(), b);
72        }
73        Ok(Self { channels })
74    }
75
76    /// Iterate bindings.
77    pub fn iter(&self) -> impl Iterator<Item = (&ChannelId, &ChannelBinding)> {
78        self.channels.iter()
79    }
80
81    /// Lookup by id.
82    pub fn get(&self, id: &ChannelId) -> Option<&ChannelBinding> {
83        self.channels.get(id)
84    }
85
86    /// Number of Channels.
87    pub fn len(&self) -> usize {
88        self.channels.len()
89    }
90
91    /// Whether empty (should not occur after successful build).
92    pub fn is_empty(&self) -> bool {
93        self.channels.is_empty()
94    }
95}
96
97/// Live Channel after connector instance realization at runtime start.
98pub struct LiveChannel {
99    /// Static binding.
100    pub binding: ChannelBinding,
101    /// Matched connector instance.
102    pub instance: ConnectorInstance,
103}