use crate::envelope::{Envelope, ImagePart, Outcome};
use crate::event::GatewayHealth;
use crate::frame::ResBody;
use crate::ops::{Delivery, HealthArgs, RegisterChannelArgs, Report, Welcome};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const HELLO_TIMEOUT_MS: u64 = 5_000;
pub const HELLO_REQUIRED_MESSAGE: &str = "hello required first";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum MountKind {
#[default]
Agent,
Gateway,
Tools,
Admin,
Cluster,
Bridge,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
Register,
Report,
Inject,
Recycle,
Probe,
Typing,
Conversations,
Resume,
Open,
Suspend,
Close,
}
impl Capability {
pub fn as_str(self) -> &'static str {
match self {
Capability::Register => "register",
Capability::Report => "report",
Capability::Inject => "inject",
Capability::Recycle => "recycle",
Capability::Probe => "probe",
Capability::Typing => "typing",
Capability::Conversations => "conversations",
Capability::Resume => "resume",
Capability::Open => "open",
Capability::Suspend => "suspend",
Capability::Close => "close",
}
}
pub const ALL: [Capability; 11] = [
Capability::Register,
Capability::Report,
Capability::Inject,
Capability::Recycle,
Capability::Probe,
Capability::Typing,
Capability::Conversations,
Capability::Resume,
Capability::Open,
Capability::Suspend,
Capability::Close,
];
pub const HOSTING: [Capability; 3] = [Capability::Open, Capability::Suspend, Capability::Close];
pub fn is_hosting(capabilities: &[Capability]) -> bool {
Self::HOSTING
.iter()
.any(|wanted| capabilities.contains(wanted))
}
}
impl std::fmt::Display for Capability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct AgentMount {
pub role: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct GatewayMount {
pub gateway: String,
pub platform: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct ClusterMount {
pub cluster: String,
pub role: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct ToolsMount {
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum Mount {
Agent(AgentMount),
Gateway(GatewayMount),
Cluster(ClusterMount),
Tools(ToolsMount),
Admin,
}
impl Mount {
pub fn decode(kind: MountKind, value: serde_json::Value) -> Result<Mount, serde_json::Error> {
use serde::de::Error as _;
use serde_json::Value;
match (kind, value) {
(MountKind::Admin, Value::Null) => Ok(Mount::Admin),
(MountKind::Admin, other) => Err(serde_json::Error::custom(format!(
"an admin mount carries no payload, and this one carries {other}"
))),
(MountKind::Agent, value) => Ok(Mount::Agent(serde_json::from_value(value)?)),
(MountKind::Tools, value) => Ok(Mount::Tools(serde_json::from_value(value)?)),
(MountKind::Cluster, value) => Ok(Mount::Cluster(serde_json::from_value(value)?)),
(MountKind::Gateway | MountKind::Bridge, value) => {
Ok(Mount::Gateway(serde_json::from_value(value)?))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct HelloArgs {
pub protocol: u16,
pub plugin: String,
pub version: String,
pub kind: MountKind,
pub capabilities: Vec<Capability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mount: Option<Mount>,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "snake_case", default)]
struct HelloArgsWire {
protocol: u16,
plugin: String,
version: String,
kind: MountKind,
capabilities: Vec<Capability>,
mount: Option<serde_json::Value>,
}
impl<'de> Deserialize<'de> for HelloArgs {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let wire = HelloArgsWire::deserialize(deserializer)?;
let mount = match wire.mount {
None | Some(serde_json::Value::Null) => None,
Some(value) => Some(Mount::decode(wire.kind, value).map_err(D::Error::custom)?),
};
Ok(HelloArgs {
protocol: wire.protocol,
plugin: wire.plugin,
version: wire.version,
kind: wire.kind,
capabilities: wire.capabilities,
mount,
})
}
}
impl HelloArgs {
pub fn has(&self, capability: Capability) -> bool {
self.capabilities.contains(&capability)
}
pub fn missing(&self, expected: &[Capability]) -> Vec<Capability> {
expected.iter().copied().filter(|c| !self.has(*c)).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct HelloAck {
pub protocol: u16,
pub role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub generation: u64,
pub prose: String,
pub server: ServerInfo,
pub host_capabilities: Vec<Capability>,
pub delivered_tasks: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct ServerInfo {
pub connected: bool,
pub cluster: String,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", tag = "op", content = "args")]
pub enum PluginOp {
Hello(HelloArgs),
Report(Report),
SessionRegister(SessionRegisterArgs),
AssignAck(AssignAckArgs),
Send(Box<Envelope>),
Handoff(HandoffArgs),
Deliver(Delivery),
RegisterChannel(RegisterChannelArgs),
Health(HealthArgs),
Typing(TypingArgs),
Detach(DetachArgs),
}
impl PluginOp {
pub fn name(&self) -> &'static str {
match self {
PluginOp::Hello(_) => "hello",
PluginOp::Report(_) => "report",
PluginOp::SessionRegister(_) => "session_register",
PluginOp::AssignAck(_) => "assign_ack",
PluginOp::Send(_) => "send",
PluginOp::Handoff(_) => "handoff",
PluginOp::Deliver(_) => "deliver",
PluginOp::RegisterChannel(_) => "register_channel",
PluginOp::Health(_) => "health",
PluginOp::Typing(_) => "typing",
PluginOp::Detach(_) => "detach",
}
}
pub fn is_pre_auth(self) -> bool {
matches!(self, PluginOp::Hello(_))
}
pub fn is_gateway(self) -> bool {
matches!(
self,
PluginOp::Deliver(_)
| PluginOp::RegisterChannel(_)
| PluginOp::Health(_)
| PluginOp::Typing(_)
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", tag = "op", content = "args")]
pub enum HostOp {
Welcome(HelloAck),
Assign(AssignArgs),
RenderSend(RenderSendArgs),
Probe(Value),
Recycle(RecycleArgs),
ConfigGet(ConfigGetArgs),
Nudge { task_id: String, text: String },
Open(OpenArgs),
Bye(ByeNotice),
}
impl HostOp {
pub fn name(&self) -> &'static str {
match self {
HostOp::Welcome(_) => "welcome",
HostOp::Assign(_) => "assign",
HostOp::RenderSend(_) => "render_send",
HostOp::Probe(_) => "probe",
HostOp::Recycle(_) => "recycle",
HostOp::ConfigGet(_) => "config_get",
HostOp::Nudge { .. } => "nudge",
HostOp::Open(_) => "open",
HostOp::Bye(_) => "bye",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct OpenArgs {
pub session_id: String,
pub task_id: String,
pub scope: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
#[serde(default)]
pub prose: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume_handle: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub struct OpenedArgs {
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume_handle: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct HandoffArgs {
pub task_id: String,
pub to: String,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image: Option<ImagePart>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct AssignArgs {
pub envelope: Box<Envelope>,
pub prose: String,
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<String>,
pub task_id: String,
pub generation: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<Box<Envelope>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct AssignAckArgs {
pub task_id: String,
pub accepted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct SessionRegisterArgs {
pub session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
pub generation: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct RenderSendArgs {
pub envelope: Box<Envelope>,
pub conversation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub gateway_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_to: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct RecycleArgs {
pub task_id: String,
pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome: Option<Outcome>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct ConfigGetArgs {
pub key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct TypingArgs {
pub conversation: String,
pub on: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct DetachArgs {
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct ByeNotice {
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum AdapterMsg {
Plugin(PluginOp),
Host(HostOp),
Res(ResBody),
}
impl AdapterMsg {
pub fn direction(&self) -> MsgDirection {
match self {
AdapterMsg::Plugin(_) => MsgDirection::ToHost,
AdapterMsg::Host(_) => MsgDirection::ToPlugin,
AdapterMsg::Res(body) => {
if body.ok {
MsgDirection::Response
} else {
MsgDirection::ErrorResponse
}
}
}
}
pub fn op_name(&self) -> Option<&str> {
match self {
AdapterMsg::Plugin(op) => Some(op.name()),
AdapterMsg::Host(op) => Some(op.name()),
AdapterMsg::Res(_) => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MsgDirection {
ToHost,
ToPlugin,
Response,
ErrorResponse,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case", default)]
pub struct GatewayBinding {
pub gateway: String,
pub channel: String,
pub conversation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
impl From<GatewayHealth> for HealthArgs {
fn from(state: GatewayHealth) -> Self {
HealthArgs {
state: state.as_str().to_string(),
detail: None,
uptime_s: 0,
}
}
}
pub type WelcomeSlice = Welcome;