1use crate::config::{ContinuationPolicy, OptionPolicy};
4use crate::dialect::DialectDescriptor;
5use crate::limits::ChannelLimits;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8use thiserror::Error;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum ChannelKind {
13 ExternalAgent,
15 DirectLlm,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum ToolExecutionMode {
22 McpGateway,
24 ModelToolCalls,
26 None,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub enum McpConfigurationCapability {
33 None,
35 CreationOnly,
37 Refreshable,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum SessionMode {
44 Stateless,
46 External,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub enum McpReachability {
53 None,
55 SameLoopbackNamespace,
57 QualifiedRemoteTransport,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
63pub enum ExchangeMode {
64 RequestResponse,
66 Bidirectional,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ChannelCapabilities {
73 pub session_mode: SessionMode,
75 pub mcp_configuration: McpConfigurationCapability,
77 pub mcp_reachability: McpReachability,
79 pub exchange_mode: ExchangeMode,
81 pub continuation_policies: BTreeSet<ContinuationPolicy>,
83 pub supports_distinct_session_concurrency: bool,
85 pub input_dialect: DialectDescriptor,
87 pub output_dialect: DialectDescriptor,
89 pub option_policy: OptionPolicy,
91}
92
93#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct ChannelDescriptor {
96 pub kind: ChannelKind,
98 pub tool_mode: ToolExecutionMode,
100 pub capabilities: ChannelCapabilities,
102 pub limits: ChannelLimits,
104}
105
106impl ChannelDescriptor {
107 pub fn validate(&self) -> Result<(), ChannelCapabilityError> {
109 if self.limits.max_active_transactions == 0
110 || self.limits.max_distinct_sessions == 0
111 || self.limits.max_encoded_exchange_bytes == 0
112 {
113 return Err(ChannelCapabilityError::ZeroLimit);
114 }
115
116 match self.kind {
117 ChannelKind::DirectLlm => {
118 if self.capabilities.session_mode != SessionMode::Stateless {
119 return Err(ChannelCapabilityError::DirectLlmRequiresStateless);
120 }
121 if self.capabilities.exchange_mode != ExchangeMode::RequestResponse {
122 return Err(ChannelCapabilityError::DirectLlmRequiresRequestResponse);
123 }
124 }
125 ChannelKind::ExternalAgent => {
126 if self.capabilities.session_mode != SessionMode::External {
127 return Err(ChannelCapabilityError::ExternalAgentRequiresExternalSession);
128 }
129 }
130 }
131
132 match self.tool_mode {
133 ToolExecutionMode::McpGateway => {
134 if self.capabilities.mcp_configuration == McpConfigurationCapability::None {
135 return Err(ChannelCapabilityError::McpGatewayRequiresConfiguration);
136 }
137 if self.capabilities.mcp_reachability == McpReachability::None {
138 return Err(ChannelCapabilityError::McpGatewayRequiresReachability);
139 }
140 }
141 ToolExecutionMode::ModelToolCalls | ToolExecutionMode::None => {
142 if self.capabilities.mcp_configuration != McpConfigurationCapability::None {
143 return Err(ChannelCapabilityError::NonMcpMustDisableConfiguration);
144 }
145 if self.capabilities.mcp_reachability != McpReachability::None {
146 return Err(ChannelCapabilityError::NonMcpMustDisableReachability);
147 }
148 }
149 }
150
151 if self
152 .capabilities
153 .continuation_policies
154 .contains(&ContinuationPolicy::InlineToolContinuation)
155 && self.tool_mode != ToolExecutionMode::ModelToolCalls
156 {
157 return Err(ChannelCapabilityError::InlineContinuationRequiresModelTools);
158 }
159
160 if !self.capabilities.supports_distinct_session_concurrency {
161 return Err(ChannelCapabilityError::DistinctSessionConcurrencyRequired);
162 }
163
164 if self.capabilities.input_dialect != self.capabilities.output_dialect {
165 return Err(ChannelCapabilityError::DialectMismatch);
169 }
170
171 Ok(())
172 }
173}
174
175pub fn send_and_retain_allowed(caps: &ChannelCapabilities) -> bool {
177 caps.exchange_mode == ExchangeMode::Bidirectional
178}
179
180#[derive(Clone, Debug, Error, PartialEq, Eq)]
182pub enum ChannelCapabilityError {
183 #[error("channel limits must be non-zero")]
185 ZeroLimit,
186 #[error("DirectLlm requires Stateless session mode")]
188 DirectLlmRequiresStateless,
189 #[error("DirectLlm requires RequestResponse exchange mode")]
191 DirectLlmRequiresRequestResponse,
192 #[error("ExternalAgent requires External session mode")]
194 ExternalAgentRequiresExternalSession,
195 #[error("McpGateway requires non-None mcp_configuration")]
197 McpGatewayRequiresConfiguration,
198 #[error("McpGateway requires declared mcp_reachability")]
200 McpGatewayRequiresReachability,
201 #[error("non-MCP tool mode requires mcp_configuration == None")]
203 NonMcpMustDisableConfiguration,
204 #[error("non-MCP tool mode requires mcp_reachability == None")]
206 NonMcpMustDisableReachability,
207 #[error("InlineToolContinuation requires ModelToolCalls")]
209 InlineContinuationRequiresModelTools,
210 #[error("supports_distinct_session_concurrency must be true")]
212 DistinctSessionConcurrencyRequired,
213 #[error("input and output dialect descriptors must match")]
215 DialectMismatch,
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::dialect::DialectDescriptor;
222
223 fn base_caps(mode: SessionMode, exchange: ExchangeMode) -> ChannelCapabilities {
224 let d = DialectDescriptor::openai_chat_completions("v1");
225 ChannelCapabilities {
226 session_mode: mode,
227 mcp_configuration: McpConfigurationCapability::None,
228 mcp_reachability: McpReachability::None,
229 exchange_mode: exchange,
230 continuation_policies: BTreeSet::from([ContinuationPolicy::CallerControlled]),
231 supports_distinct_session_concurrency: true,
232 input_dialect: d.clone(),
233 output_dialect: d,
234 option_policy: crate::config::OptionPolicy::direct_llm(),
235 }
236 }
237
238 #[test]
239 fn direct_llm_matrix() {
240 let d = ChannelDescriptor {
241 kind: ChannelKind::DirectLlm,
242 tool_mode: ToolExecutionMode::ModelToolCalls,
243 capabilities: base_caps(SessionMode::Stateless, ExchangeMode::RequestResponse),
244 limits: ChannelLimits::default(),
245 };
246 assert!(d.validate().is_ok());
247 }
248
249 #[test]
250 fn mcp_gateway_requires_config() {
251 let mut caps = base_caps(SessionMode::External, ExchangeMode::Bidirectional);
252 let d = ChannelDescriptor {
253 kind: ChannelKind::ExternalAgent,
254 tool_mode: ToolExecutionMode::McpGateway,
255 capabilities: caps.clone(),
256 limits: ChannelLimits::default(),
257 };
258 assert_eq!(
259 d.validate(),
260 Err(ChannelCapabilityError::McpGatewayRequiresConfiguration)
261 );
262 caps.mcp_configuration = McpConfigurationCapability::CreationOnly;
263 caps.mcp_reachability = McpReachability::SameLoopbackNamespace;
264 let d = ChannelDescriptor {
265 kind: ChannelKind::ExternalAgent,
266 tool_mode: ToolExecutionMode::McpGateway,
267 capabilities: caps,
268 limits: ChannelLimits::default(),
269 };
270 assert!(d.validate().is_ok());
271 }
272}