use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::panic::AssertUnwindSafe;
strkind::strkind! {
pub ThreadId;
pub ToolCallId;
pub ProviderCallId;
}
pub type ChoiceId<T = String> = ToolCallId<T>;
#[derive(Debug, Serialize, Deserialize)]
pub struct RapInvocation {
pub operation: String,
#[serde(default)]
pub arguments: serde_json::Value,
pub id: ToolCallId,
pub call_id: Option<ProviderCallId>,
pub callback_url: String,
pub group_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(default)]
pub thread_ancestors: Option<Vec<ThreadId>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
pub enum DisplaySegment {
#[serde(rename = "text")]
Text(String),
#[serde(rename = "diff")]
Diff(DiffContent),
#[serde(rename = "image")]
Image(ImageContent),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffContent {
pub path: String,
pub patch: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageContent {
pub data: String,
#[serde(rename = "mediaType")]
pub media_type: String,
}
pub fn build_display_segments(
display_as: Option<&[DisplaySegment]>,
text: &str,
) -> Vec<DisplaySegment> {
let mut segments: Vec<DisplaySegment> = display_as.map(|s| s.to_vec()).unwrap_or_default();
segments.push(DisplaySegment::Text(text.to_owned()));
segments
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RapToolResultContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image {
data: String,
#[serde(rename = "mediaType")]
media_type: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RapToolResult {
pub group_id: ThreadId,
pub id: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call_id: Option<ProviderCallId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<RapToolResultContent>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_as: Option<Vec<DisplaySegment>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subscription: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RapUserChoice {
pub group_id: ThreadId,
pub id: ChoiceId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call_id: Option<ProviderCallId>,
pub prompt: String,
pub choices: Vec<String>,
#[serde(default)]
pub default: usize,
pub response_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RapSubscriptionEvent {
pub group_id: ThreadId,
pub tool_call_id: ToolCallId,
pub text: String,
#[serde(default)]
pub associative: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#final: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RapViewUpdate {
pub group_id: ThreadId,
pub view_type: String,
pub content: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RapOAuth {
pub group_id: ThreadId,
pub id: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub call_id: Option<ProviderCallId>,
pub auth_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RapCallback {
#[serde(rename = "tool_result")]
ToolResult(RapToolResult),
#[serde(rename = "subscription_event")]
SubscriptionEvent(RapSubscriptionEvent),
#[serde(rename = "oauth")]
OAuth(RapOAuth),
#[serde(rename = "user_choice")]
UserChoice(RapUserChoice),
#[serde(rename = "view_update")]
ViewUpdate(RapViewUpdate),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsetManifest {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub endpoint: String,
pub tools: Vec<ToolDef>,
#[serde(default, rename = "needsMigration")]
pub needs_migration: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
pub name: String,
pub description: String,
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<serde_json::Value>,
#[serde(
default,
rename = "displayScript",
skip_serializing_if = "Option::is_none"
)]
pub display_script: Option<String>,
}
#[async_trait]
pub trait CallbackClient: Send + Sync + 'static {
async fn post_json(
&self,
url: &str,
body: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
pub struct PlainCallbackClient {
client: reqwest::Client,
}
impl Default for PlainCallbackClient {
fn default() -> Self {
Self::new()
}
}
impl PlainCallbackClient {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl CallbackClient for PlainCallbackClient {
async fn post_json(
&self,
url: &str,
body: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.client
.post(url)
.header("content-type", "application/json")
.body(body.to_owned())
.send()
.await?;
Ok(())
}
}
pub async fn send_tool_result<C: CallbackClient>(
client: &C,
invocation: &RapInvocation,
text: &str,
display_as: Option<Vec<DisplaySegment>>,
subscription: bool,
) {
let result = RapCallback::ToolResult(RapToolResult {
group_id: invocation.group_id.clone(),
id: invocation.id.clone(),
call_id: invocation.call_id.clone(),
text: Some(text.to_owned()),
content: None,
display_as,
subscription: if subscription { Some(true) } else { None },
});
let body = serde_json::to_string(&result).expect("bug: failed to serialize tool result");
if let Err(e) = client.post_json(&invocation.callback_url, &body).await {
tracing::error!("failed to send tool result: {e}");
}
}
pub async fn send_user_choice<C: CallbackClient>(
client: &C,
invocation: &RapInvocation,
prompt: &str,
choices: Vec<String>,
default: usize,
response_url: &str,
) {
let msg = RapCallback::UserChoice(RapUserChoice {
group_id: invocation.group_id.clone(),
id: invocation.id.clone(),
call_id: invocation.call_id.clone(),
prompt: prompt.to_owned(),
choices,
default,
response_url: response_url.to_owned(),
});
let body = serde_json::to_string(&msg).expect("bug: failed to serialize user_choice");
if let Err(e) = client.post_json(&invocation.callback_url, &body).await {
tracing::error!("failed to send user_choice: {e}");
}
}
pub async fn send_subscription_event<C: CallbackClient>(
client: &C,
callback_url: &str,
group_id: ThreadId,
tool_call_id: ToolCallId,
text: &str,
associative: bool,
r#final: bool,
) {
let event = RapCallback::SubscriptionEvent(RapSubscriptionEvent {
group_id,
tool_call_id,
text: text.to_owned(),
associative,
r#final: if r#final { Some(true) } else { None },
});
let body = serde_json::to_string(&event).expect("bug: failed to serialize subscription event");
if let Err(e) = client.post_json(callback_url, &body).await {
tracing::error!("failed to send subscription event: {e}");
}
}
pub async fn send_view_update<C: CallbackClient>(
client: &C,
callback_url: &str,
group_id: ThreadId,
view_type: &str,
content: serde_json::Value,
) {
let msg = RapCallback::ViewUpdate(RapViewUpdate {
group_id,
view_type: view_type.to_owned(),
content,
});
let body = serde_json::to_string(&msg).expect("bug: failed to serialize view_update");
if let Err(e) = client.post_json(callback_url, &body).await {
tracing::error!("failed to send view_update: {e}");
}
}
pub async fn log_panic<F: Future<Output = ()>>(name: &'static str, f: F) {
use futures_util::FutureExt;
if let Err(e) = AssertUnwindSafe(f).catch_unwind().await {
let msg = if let Some(s) = e.downcast_ref::<&str>() {
*s
} else if let Some(s) = e.downcast_ref::<String>() {
&**s
} else {
"unknown panic payload"
};
tracing::error!("task '{name}' panicked: {msg}");
}
}