use crate::config::{ContinuationPolicy, OptionPolicy};
use crate::dialect::DialectDescriptor;
use crate::limits::ChannelLimits;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use thiserror::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChannelKind {
ExternalAgent,
DirectLlm,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ToolExecutionMode {
McpGateway,
ModelToolCalls,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum McpConfigurationCapability {
None,
CreationOnly,
Refreshable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SessionMode {
Stateless,
External,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum McpReachability {
None,
SameLoopbackNamespace,
QualifiedRemoteTransport,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExchangeMode {
RequestResponse,
Bidirectional,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelCapabilities {
pub session_mode: SessionMode,
pub mcp_configuration: McpConfigurationCapability,
pub mcp_reachability: McpReachability,
pub exchange_mode: ExchangeMode,
pub continuation_policies: BTreeSet<ContinuationPolicy>,
pub supports_distinct_session_concurrency: bool,
pub input_dialect: DialectDescriptor,
pub output_dialect: DialectDescriptor,
pub option_policy: OptionPolicy,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChannelDescriptor {
pub kind: ChannelKind,
pub tool_mode: ToolExecutionMode,
pub capabilities: ChannelCapabilities,
pub limits: ChannelLimits,
}
impl ChannelDescriptor {
pub fn validate(&self) -> Result<(), ChannelCapabilityError> {
if self.limits.max_active_transactions == 0
|| self.limits.max_distinct_sessions == 0
|| self.limits.max_encoded_exchange_bytes == 0
{
return Err(ChannelCapabilityError::ZeroLimit);
}
match self.kind {
ChannelKind::DirectLlm => {
if self.capabilities.session_mode != SessionMode::Stateless {
return Err(ChannelCapabilityError::DirectLlmRequiresStateless);
}
if self.capabilities.exchange_mode != ExchangeMode::RequestResponse {
return Err(ChannelCapabilityError::DirectLlmRequiresRequestResponse);
}
}
ChannelKind::ExternalAgent => {
if self.capabilities.session_mode != SessionMode::External {
return Err(ChannelCapabilityError::ExternalAgentRequiresExternalSession);
}
}
}
match self.tool_mode {
ToolExecutionMode::McpGateway => {
if self.capabilities.mcp_configuration == McpConfigurationCapability::None {
return Err(ChannelCapabilityError::McpGatewayRequiresConfiguration);
}
if self.capabilities.mcp_reachability == McpReachability::None {
return Err(ChannelCapabilityError::McpGatewayRequiresReachability);
}
}
ToolExecutionMode::ModelToolCalls | ToolExecutionMode::None => {
if self.capabilities.mcp_configuration != McpConfigurationCapability::None {
return Err(ChannelCapabilityError::NonMcpMustDisableConfiguration);
}
if self.capabilities.mcp_reachability != McpReachability::None {
return Err(ChannelCapabilityError::NonMcpMustDisableReachability);
}
}
}
if self
.capabilities
.continuation_policies
.contains(&ContinuationPolicy::InlineToolContinuation)
&& self.tool_mode != ToolExecutionMode::ModelToolCalls
{
return Err(ChannelCapabilityError::InlineContinuationRequiresModelTools);
}
if !self.capabilities.supports_distinct_session_concurrency {
return Err(ChannelCapabilityError::DistinctSessionConcurrencyRequired);
}
if self.capabilities.input_dialect != self.capabilities.output_dialect {
return Err(ChannelCapabilityError::DialectMismatch);
}
Ok(())
}
}
pub fn send_and_retain_allowed(caps: &ChannelCapabilities) -> bool {
caps.exchange_mode == ExchangeMode::Bidirectional
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ChannelCapabilityError {
#[error("channel limits must be non-zero")]
ZeroLimit,
#[error("DirectLlm requires Stateless session mode")]
DirectLlmRequiresStateless,
#[error("DirectLlm requires RequestResponse exchange mode")]
DirectLlmRequiresRequestResponse,
#[error("ExternalAgent requires External session mode")]
ExternalAgentRequiresExternalSession,
#[error("McpGateway requires non-None mcp_configuration")]
McpGatewayRequiresConfiguration,
#[error("McpGateway requires declared mcp_reachability")]
McpGatewayRequiresReachability,
#[error("non-MCP tool mode requires mcp_configuration == None")]
NonMcpMustDisableConfiguration,
#[error("non-MCP tool mode requires mcp_reachability == None")]
NonMcpMustDisableReachability,
#[error("InlineToolContinuation requires ModelToolCalls")]
InlineContinuationRequiresModelTools,
#[error("supports_distinct_session_concurrency must be true")]
DistinctSessionConcurrencyRequired,
#[error("input and output dialect descriptors must match")]
DialectMismatch,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dialect::DialectDescriptor;
fn base_caps(mode: SessionMode, exchange: ExchangeMode) -> ChannelCapabilities {
let d = DialectDescriptor::openai_chat_completions("v1");
ChannelCapabilities {
session_mode: mode,
mcp_configuration: McpConfigurationCapability::None,
mcp_reachability: McpReachability::None,
exchange_mode: exchange,
continuation_policies: BTreeSet::from([ContinuationPolicy::CallerControlled]),
supports_distinct_session_concurrency: true,
input_dialect: d.clone(),
output_dialect: d,
option_policy: crate::config::OptionPolicy::direct_llm(),
}
}
#[test]
fn direct_llm_matrix() {
let d = ChannelDescriptor {
kind: ChannelKind::DirectLlm,
tool_mode: ToolExecutionMode::ModelToolCalls,
capabilities: base_caps(SessionMode::Stateless, ExchangeMode::RequestResponse),
limits: ChannelLimits::default(),
};
assert!(d.validate().is_ok());
}
#[test]
fn mcp_gateway_requires_config() {
let mut caps = base_caps(SessionMode::External, ExchangeMode::Bidirectional);
let d = ChannelDescriptor {
kind: ChannelKind::ExternalAgent,
tool_mode: ToolExecutionMode::McpGateway,
capabilities: caps.clone(),
limits: ChannelLimits::default(),
};
assert_eq!(
d.validate(),
Err(ChannelCapabilityError::McpGatewayRequiresConfiguration)
);
caps.mcp_configuration = McpConfigurationCapability::CreationOnly;
caps.mcp_reachability = McpReachability::SameLoopbackNamespace;
let d = ChannelDescriptor {
kind: ChannelKind::ExternalAgent,
tool_mode: ToolExecutionMode::McpGateway,
capabilities: caps,
limits: ChannelLimits::default(),
};
assert!(d.validate().is_ok());
}
}