use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value, json};
use thiserror::Error;
use crate::envelope::{deserialize_unique_value, validate_read_version};
use crate::{
ApprovalId, BranchId, CURRENT_PROTOCOL_VERSION, CanonicalMessage, CommandId, CorrelationId,
MessageId, MessageRole, ProtocolError, ProtocolMetadata, ProtocolTimestamp, ProtocolVersion,
SessionId,
};
pub const MAX_COMMAND_TEXT_BYTES: usize = 256 * 1024;
pub const MAX_SELECTOR_BYTES: usize = 128;
macro_rules! selector {
($name:ident, $doc:literal, $validate:ident) => {
#[doc = $doc]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(String);
impl $name {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl FromStr for $name {
type Err = SelectorParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
$validate(value)?;
Ok(Self(value.to_owned()))
}
}
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
$validate(&self.0).map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
}
};
}
selector!(
ProfileId,
"A bounded product profile selector.",
validate_profile_selector
);
selector!(
ModelId,
"A bounded canonical model selector.",
validate_model_selector
);
selector!(
ProviderId,
"A bounded canonical model-provider selector.",
validate_profile_selector
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum SelectorParseError {
#[error(
"selector must start with lowercase ASCII and contain only supported canonical characters"
)]
Invalid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ApprovalDecision {
AllowOnce,
AllowSession,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentCommandType {
CreateSession,
Prompt,
Steer,
FollowUp,
Abort,
ResolveApproval,
SetModel,
SetReasoningEffort,
SetProfile,
CompactSession,
ForkSession,
}
impl AgentCommandType {
pub const ALL: [Self; 11] = [
Self::CreateSession,
Self::Prompt,
Self::Steer,
Self::FollowUp,
Self::Abort,
Self::ResolveApproval,
Self::SetModel,
Self::SetReasoningEffort,
Self::SetProfile,
Self::CompactSession,
Self::ForkSession,
];
}
#[derive(Debug, Clone, PartialEq)]
pub enum AgentCommand {
CreateSession {
profile_id: ProfileId,
metadata: ProtocolMetadata,
},
Prompt {
message: CanonicalMessage,
},
Steer {
text: CommandText,
},
FollowUp {
message: CanonicalMessage,
},
Abort {},
ResolveApproval {
approval_id: ApprovalId,
decision: ApprovalDecision,
},
SetModel {
model: crate::ModelRef,
},
SetReasoningEffort {
reasoning_effort: crate::ReasoningEffort,
},
SetProfile {
profile_id: ProfileId,
},
CompactSession {
instruction: Option<CommandText>,
},
ForkSession {
from_message_id: MessageId,
branch_id: BranchId,
},
}
impl AgentCommand {
#[must_use]
pub const fn command_type(&self) -> AgentCommandType {
match self {
Self::CreateSession { .. } => AgentCommandType::CreateSession,
Self::Prompt { .. } => AgentCommandType::Prompt,
Self::Steer { .. } => AgentCommandType::Steer,
Self::FollowUp { .. } => AgentCommandType::FollowUp,
Self::Abort {} => AgentCommandType::Abort,
Self::ResolveApproval { .. } => AgentCommandType::ResolveApproval,
Self::SetModel { .. } => AgentCommandType::SetModel,
Self::SetReasoningEffort { .. } => AgentCommandType::SetReasoningEffort,
Self::SetProfile { .. } => AgentCommandType::SetProfile,
Self::CompactSession { .. } => AgentCommandType::CompactSession,
Self::ForkSession { .. } => AgentCommandType::ForkSession,
}
}
fn validate(&self) -> Result<(), CommandValidationError> {
match self {
Self::Prompt { message } | Self::FollowUp { message }
if message.role() != MessageRole::User =>
{
Err(CommandValidationError::MessageMustBeUser)
}
_ => Ok(()),
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(
remote = "AgentCommand",
tag = "type",
content = "payload",
rename_all = "snake_case"
)]
enum AgentCommandDef {
CreateSession {
#[serde(rename = "profileId")]
profile_id: ProfileId,
#[serde(default, skip_serializing_if = "ProtocolMetadata::is_empty")]
metadata: ProtocolMetadata,
},
Prompt {
message: CanonicalMessage,
},
Steer {
text: CommandText,
},
FollowUp {
message: CanonicalMessage,
},
Abort {},
ResolveApproval {
#[serde(rename = "approvalId")]
approval_id: ApprovalId,
decision: ApprovalDecision,
},
SetModel {
model: crate::ModelRef,
},
SetReasoningEffort {
#[serde(rename = "reasoningEffort")]
reasoning_effort: crate::ReasoningEffort,
},
SetProfile {
#[serde(rename = "profileId")]
profile_id: ProfileId,
},
CompactSession {
#[serde(skip_serializing_if = "Option::is_none")]
instruction: Option<CommandText>,
},
ForkSession {
#[serde(rename = "fromMessageId")]
from_message_id: MessageId,
#[serde(rename = "branchId")]
branch_id: BranchId,
},
}
impl Serialize for AgentCommand {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.validate().map_err(serde::ser::Error::custom)?;
AgentCommandDef::serialize(self, serializer)
}
}
impl<'de> Deserialize<'de> for AgentCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let command = AgentCommandDef::deserialize(deserializer)?;
command.validate().map_err(serde::de::Error::custom)?;
Ok(command)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct CommandText(String);
impl CommandText {
pub fn new(value: impl Into<String>) -> Result<Self, CommandValidationError> {
let value = value.into();
if value.is_empty() || value.len() > MAX_COMMAND_TEXT_BYTES || value.contains('\0') {
return Err(CommandValidationError::InvalidText);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for CommandText {
type Error = CommandValidationError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<CommandText> for String {
fn from(value: CommandText) -> Self {
value.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CommandEnvelope {
protocol_version: ProtocolVersion,
command_id: CommandId,
session_id: Option<SessionId>,
timestamp: ProtocolTimestamp,
command: AgentCommand,
}
impl CommandEnvelope {
pub fn new(
command_id: CommandId,
session_id: Option<SessionId>,
timestamp: ProtocolTimestamp,
command: AgentCommand,
) -> Result<Self, CommandValidationError> {
let envelope = Self {
protocol_version: CURRENT_PROTOCOL_VERSION,
command_id,
session_id,
timestamp,
command,
};
envelope.validate()?;
Ok(envelope)
}
pub fn decode_value(value: Value) -> Result<Self, CommandDecodeError> {
let version = decode_version(&value).map_err(CommandDecodeError::Invalid)?;
if validate_read_version(version).is_err() {
return Err(CommandDecodeError::UnsupportedVersion { version });
}
let discriminator = value
.as_object()
.and_then(|object| object.get("type"))
.and_then(Value::as_str)
.ok_or_else(|| CommandDecodeError::Invalid("missing command type".to_owned()))?;
if discriminator.parse::<AgentCommandTypeText>().is_err() {
if valid_discriminator(discriminator) {
return Err(CommandDecodeError::UnsupportedType {
command_type: discriminator.to_owned(),
});
}
return Err(CommandDecodeError::Invalid(
"invalid command type".to_owned(),
));
}
serde_json::from_value(value)
.map_err(|error| CommandDecodeError::Invalid(error.to_string()))
}
#[must_use]
pub const fn protocol_version(&self) -> ProtocolVersion {
self.protocol_version
}
#[must_use]
pub const fn command_id(&self) -> CommandId {
self.command_id
}
#[must_use]
pub const fn session_id(&self) -> Option<SessionId> {
self.session_id
}
#[must_use]
pub const fn timestamp(&self) -> ProtocolTimestamp {
self.timestamp
}
#[must_use]
pub const fn command(&self) -> &AgentCommand {
&self.command
}
#[must_use]
pub const fn command_type(&self) -> AgentCommandType {
self.command.command_type()
}
fn validate(&self) -> Result<(), CommandValidationError> {
let is_create = matches!(self.command, AgentCommand::CreateSession { .. });
if is_create == self.session_id.is_some() {
return Err(CommandValidationError::InvalidSessionPresence);
}
self.command.validate()
}
}
impl Serialize for CommandEnvelope {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.validate().map_err(serde::ser::Error::custom)?;
let mut value = serde_json::to_value(&self.command).map_err(serde::ser::Error::custom)?;
let object = value
.as_object_mut()
.ok_or_else(|| serde::ser::Error::custom("command must encode as object"))?;
insert_envelope_fields(
object,
self.protocol_version,
self.command_id,
self.session_id,
self.timestamp,
);
value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for CommandEnvelope {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let mut value = deserialize_unique_value(deserializer)?;
let object = value
.as_object_mut()
.ok_or_else(|| serde::de::Error::custom("command envelope must be an object"))?;
let protocol_version = take(object, "protocolVersion").map_err(serde::de::Error::custom)?;
validate_read_version(protocol_version).map_err(serde::de::Error::custom)?;
let command_id = take(object, "commandId").map_err(serde::de::Error::custom)?;
let session_id = take_optional(object, "sessionId").map_err(serde::de::Error::custom)?;
let timestamp = take(object, "timestamp").map_err(serde::de::Error::custom)?;
let command = AgentCommand::deserialize(Value::Object(std::mem::take(object)))
.map_err(serde::de::Error::custom)?;
let envelope = Self {
protocol_version,
command_id,
session_id,
timestamp,
command,
};
envelope.validate().map_err(serde::de::Error::custom)?;
Ok(envelope)
}
}
#[derive(Debug, Error)]
pub enum CommandDecodeError {
#[error("unsupported protocol version: {version}")]
UnsupportedVersion {
version: ProtocolVersion,
},
#[error("unsupported command type: {command_type}")]
UnsupportedType {
command_type: String,
},
#[error("invalid command: {0}")]
Invalid(String),
}
impl CommandDecodeError {
#[must_use]
pub fn into_protocol_error(self, correlation_id: CorrelationId) -> ProtocolError {
match self {
Self::UnsupportedVersion { version } => {
ProtocolError::unsupported_protocol_version(correlation_id, version)
}
Self::UnsupportedType { command_type } => {
let details = ProtocolMetadata::protocol_compatibility_details(Some(&command_type));
ProtocolError::unsupported_command(correlation_id).with_details(details)
}
Self::Invalid(_) => ProtocolError::invalid_command(correlation_id),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum CommandValidationError {
#[error("sessionId must be absent for create_session and present for every other command")]
InvalidSessionPresence,
#[error("prompt and follow_up messages must have user role")]
MessageMustBeUser,
#[error("command text is invalid")]
InvalidText,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct AgentCommandTypeText;
impl FromStr for AgentCommandTypeText {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
if [
"create_session",
"prompt",
"steer",
"follow_up",
"abort",
"resolve_approval",
"set_model",
"set_reasoning_effort",
"set_profile",
"compact_session",
"fork_session",
]
.contains(&value)
{
Ok(Self)
} else {
Err(())
}
}
}
fn decode_version(value: &Value) -> Result<ProtocolVersion, String> {
let version = value
.as_object()
.and_then(|object| object.get("protocolVersion"))
.cloned()
.ok_or_else(|| "missing protocolVersion".to_owned())?;
serde_json::from_value(version).map_err(|error| error.to_string())
}
fn validate_profile_selector(value: &str) -> Result<(), SelectorParseError> {
validate_selector(value, false)
}
fn validate_model_selector(value: &str) -> Result<(), SelectorParseError> {
validate_selector(value, true)
}
fn validate_selector(value: &str, allow_colon: bool) -> Result<(), SelectorParseError> {
let mut bytes = value.bytes();
if value.len() > MAX_SELECTOR_BYTES
|| !bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
|| !bytes.all(|byte| {
byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| matches!(byte, b'_' | b'-' | b'.' | b'/')
|| (allow_colon && byte == b':')
})
{
Err(SelectorParseError::Invalid)
} else {
Ok(())
}
}
fn valid_discriminator(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_'))
}
fn insert_envelope_fields(
object: &mut Map<String, Value>,
version: ProtocolVersion,
command_id: CommandId,
session_id: Option<SessionId>,
timestamp: ProtocolTimestamp,
) {
object.insert("protocolVersion".to_owned(), json!(version));
object.insert("commandId".to_owned(), json!(command_id));
if let Some(session_id) = session_id {
object.insert("sessionId".to_owned(), json!(session_id));
}
object.insert("timestamp".to_owned(), json!(timestamp));
}
fn take<T>(object: &mut Map<String, Value>, key: &str) -> Result<T, serde_json::Error>
where
T: for<'de> Deserialize<'de>,
{
serde_json::from_value(object.remove(key).unwrap_or(Value::Null))
}
fn take_optional<T>(
object: &mut Map<String, Value>,
key: &str,
) -> Result<Option<T>, serde_json::Error>
where
T: for<'de> Deserialize<'de>,
{
object.remove(key).map_or(Ok(None), serde_json::from_value)
}