use std::fmt;
use std::str::FromStr;
use std::time::Duration;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use crate::id::{ConversationKey, UserRef};
use crate::{BoxError, async_trait};
pub const PEER_PLATFORM: &str = "peer";
pub const PEER_CANCEL_PREFIX: &str = "peer-cancel:";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PeerError {
#[error("no member of the team can take this: {0}")]
NoRoute(String),
#[error("refused: {0}")]
Refused(String),
#[error("unreachable: {0}")]
Unreachable(String),
#[error("no such task")]
UnknownTask,
#[error("peer error: {0}")]
Other(#[source] BoxError),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegationContext {
pub origin: Origin,
pub chain: Vec<String>,
pub deadline: Timestamp,
pub budget: Budget,
pub trace: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Origin {
pub bot: String,
pub conversation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub person: Option<String>,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Budget {
pub turns: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tokens: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PeerTarget {
Member(String),
Role(String),
Skill(String),
}
impl FromStr for PeerTarget {
type Err = PeerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let target = if let Some(role) = s.strip_prefix("role:") {
Self::Role(role.to_string())
} else if let Some(skill) = s.strip_prefix("skill:") {
Self::Skill(skill.to_string())
} else {
Self::Member(s.to_string())
};
match &target {
Self::Member(n) | Self::Role(n) | Self::Skill(n) if n.is_empty() => {
Err(PeerError::NoRoute("an empty target".into()))
}
_ => Ok(target),
}
}
}
impl fmt::Display for PeerTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Member(n) => f.write_str(n),
Self::Role(n) => write!(f, "role:{n}"),
Self::Skill(n) => write!(f, "skill:{n}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Delegation {
pub team: Option<String>,
pub to: PeerTarget,
pub task: String,
pub context: DelegationContext,
pub wait: Duration,
pub message_id: String,
pub requester: Option<UserRef>,
pub direct: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PeerTaskState {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
Rejected,
}
impl PeerTaskState {
pub fn is_final(self) -> bool {
matches!(
self,
Self::Completed | Self::Failed | Self::Canceled | Self::Rejected
)
}
pub fn as_str(self) -> &'static str {
match self {
Self::Submitted => "submitted",
Self::Working => "working",
Self::InputRequired => "input-required",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Canceled => "canceled",
Self::Rejected => "rejected",
}
}
}
impl FromStr for PeerTaskState {
type Err = PeerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"submitted" => Self::Submitted,
"working" => Self::Working,
"input-required" => Self::InputRequired,
"completed" => Self::Completed,
"failed" => Self::Failed,
"canceled" => Self::Canceled,
"rejected" => Self::Rejected,
other => return Err(PeerError::Refused(format!("unknown task state {other}"))),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerTask {
pub team: String,
pub peer: String,
pub id: String,
pub state: PeerTaskState,
pub text: Option<String>,
pub prompt: Option<PeerPrompt>,
pub tokens: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerPrompt {
pub id: String,
pub buttons: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PeerInfo {
pub team: String,
pub name: String,
pub roles: Vec<String>,
pub skills: Vec<String>,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerCaller {
pub team: String,
pub member: String,
pub roles: Vec<String>,
pub context: DelegationContext,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerTaskRecord {
pub id: String,
pub inbound: bool,
pub team: String,
pub peer: String,
pub context_id: String,
pub message_id: String,
pub state: PeerTaskState,
pub conversation: Option<ConversationKey>,
pub trace: String,
pub text: Option<String>,
pub requester: Option<UserRef>,
pub direct: bool,
pub tokens: Option<u64>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
#[async_trait]
pub trait Peers: Send + Sync {
async fn directory(&self) -> Result<Vec<PeerInfo>, PeerError>;
async fn delegate(&self, delegation: Delegation) -> Result<PeerTask, PeerError>;
async fn status(&self, team: &str, peer: &str, task: &str) -> Result<PeerTask, PeerError>;
async fn cancel(&self, team: &str, peer: &str, task: &str) -> Result<PeerTask, PeerError>;
async fn report_usage(&self, conversation: &ConversationKey, task: Option<&str>, tokens: u64);
fn progress(&self, conversation: &ConversationKey, task: Option<&str>, text: &str);
async fn fail(&self, conversation: &ConversationKey, task: Option<&str>, reason: &str);
async fn answer(
&self,
task: &PeerTask,
prompt: &str,
button: &str,
) -> Result<PeerTask, PeerError>;
async fn caller(&self, conversation: &ConversationKey) -> Option<PeerCaller>;
fn team_person(&self, account: &str) -> Option<String>;
fn member_name(&self, team: &str) -> Option<String>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn targets_parse() {
assert_eq!(
"ops".parse::<PeerTarget>().unwrap(),
PeerTarget::Member("ops".into())
);
assert_eq!(
"role:leader".parse::<PeerTarget>().unwrap(),
PeerTarget::Role("leader".into())
);
assert_eq!(
"skill:web".parse::<PeerTarget>().unwrap().to_string(),
"skill:web"
);
assert!("role:".parse::<PeerTarget>().is_err());
assert!("".parse::<PeerTarget>().is_err());
}
#[test]
fn states_round_trip() {
for state in [
PeerTaskState::Submitted,
PeerTaskState::Working,
PeerTaskState::InputRequired,
PeerTaskState::Completed,
PeerTaskState::Failed,
PeerTaskState::Canceled,
PeerTaskState::Rejected,
] {
assert_eq!(state.as_str().parse::<PeerTaskState>().unwrap(), state);
}
assert!(PeerTaskState::Completed.is_final());
assert!(!PeerTaskState::InputRequired.is_final());
}
}