1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use std::future::Future;
4use std::panic::AssertUnwindSafe;
5
6strkind::strkind! {
9 pub ThreadId;
17}
18
19#[derive(Debug, Serialize, Deserialize)]
22pub struct RapInvocation {
23 pub operation: String,
24 #[serde(default)]
25 pub arguments: serde_json::Value,
26 pub id: String,
27 pub call_id: Option<String>,
28 pub callback_url: String,
29 pub group_id: ThreadId,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub user_id: Option<String>,
32 #[serde(default)]
34 pub thread_ancestors: Option<Vec<ThreadId>>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(tag = "type", content = "content")]
40pub enum DisplaySegment {
41 #[serde(rename = "text")]
43 Text(String),
44 #[serde(rename = "diff")]
46 Diff(DiffContent),
47 #[serde(rename = "image")]
51 Image(ImageContent),
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DiffContent {
57 pub path: String,
59 pub patch: String,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ImageContent {
66 pub data: String,
68 #[serde(rename = "mediaType")]
70 pub media_type: String,
71}
72
73pub fn build_display_segments(
76 display_as: Option<&[DisplaySegment]>,
77 text: &str,
78) -> Vec<DisplaySegment> {
79 let mut segments: Vec<DisplaySegment> = display_as.map(|s| s.to_vec()).unwrap_or_default();
80 segments.push(DisplaySegment::Text(text.to_owned()));
81 segments
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(tag = "type")]
91pub enum RapToolResultContent {
92 #[serde(rename = "text")]
94 Text { text: String },
95 #[serde(rename = "image")]
97 Image {
98 data: String,
100 #[serde(rename = "mediaType")]
102 media_type: String,
103 },
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct RapToolResult {
108 pub group_id: ThreadId,
109 pub id: String,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub call_id: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub text: Option<String>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub content: Option<Vec<RapToolResultContent>>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub display_as: Option<Vec<DisplaySegment>>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub subscription: Option<bool>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct RapUserChoice {
131 pub group_id: ThreadId,
132 pub id: String,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub call_id: Option<String>,
135 pub prompt: String,
136 pub choices: Vec<String>,
137 #[serde(default)]
138 pub default: usize,
139 pub response_url: String,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct RapSubscriptionEvent {
144 pub group_id: ThreadId,
145 pub tool_call_id: String,
146 pub text: String,
147 #[serde(default)]
148 pub associative: bool,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub r#final: Option<bool>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct RapViewUpdate {
157 pub group_id: ThreadId,
158 pub view_type: String,
160 pub content: serde_json::Value,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct RapOAuth {
166 pub group_id: ThreadId,
167 pub id: String,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub call_id: Option<String>,
170 pub auth_url: String,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(tag = "type")]
176pub enum RapCallback {
177 #[serde(rename = "tool_result")]
178 ToolResult(RapToolResult),
179 #[serde(rename = "subscription_event")]
180 SubscriptionEvent(RapSubscriptionEvent),
181 #[serde(rename = "oauth")]
182 OAuth(RapOAuth),
183 #[serde(rename = "user_choice")]
184 UserChoice(RapUserChoice),
185 #[serde(rename = "view_update")]
186 ViewUpdate(RapViewUpdate),
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ToolsetManifest {
191 pub name: String,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub description: Option<String>,
194 pub endpoint: String,
195 pub tools: Vec<ToolDef>,
196 #[serde(default, rename = "needsMigration")]
197 pub needs_migration: bool,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ToolDef {
202 pub name: String,
203 pub description: String,
204 #[serde(rename = "inputSchema")]
205 pub input_schema: serde_json::Value,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub annotations: Option<serde_json::Value>,
208 #[serde(
209 default,
210 rename = "displayScript",
211 skip_serializing_if = "Option::is_none"
212 )]
213 pub display_script: Option<String>,
214}
215
216#[async_trait]
220pub trait CallbackClient: Send + Sync + 'static {
221 async fn post_json(
222 &self,
223 url: &str,
224 body: &str,
225 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
226}
227
228pub struct PlainCallbackClient {
230 client: reqwest::Client,
231}
232
233impl Default for PlainCallbackClient {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239impl PlainCallbackClient {
240 pub fn new() -> Self {
241 Self {
242 client: reqwest::Client::new(),
243 }
244 }
245}
246
247#[async_trait]
248impl CallbackClient for PlainCallbackClient {
249 async fn post_json(
250 &self,
251 url: &str,
252 body: &str,
253 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
254 self.client
255 .post(url)
256 .header("content-type", "application/json")
257 .body(body.to_owned())
258 .send()
259 .await?;
260 Ok(())
261 }
262}
263
264pub async fn send_tool_result<C: CallbackClient>(
268 client: &C,
269 invocation: &RapInvocation,
270 text: &str,
271 display_as: Option<Vec<DisplaySegment>>,
272 subscription: bool,
273) {
274 let result = RapCallback::ToolResult(RapToolResult {
275 group_id: invocation.group_id.clone(),
276 id: invocation.id.clone(),
277 call_id: invocation.call_id.clone(),
278 text: Some(text.to_owned()),
279 content: None,
280 display_as,
281 subscription: if subscription { Some(true) } else { None },
282 });
283 let body = serde_json::to_string(&result).expect("bug: failed to serialize tool result");
284 if let Err(e) = client.post_json(&invocation.callback_url, &body).await {
285 tracing::error!("failed to send tool result: {e}");
286 }
287}
288
289pub async fn send_user_choice<C: CallbackClient>(
291 client: &C,
292 invocation: &RapInvocation,
293 prompt: &str,
294 choices: Vec<String>,
295 default: usize,
296 response_url: &str,
297) {
298 let msg = RapCallback::UserChoice(RapUserChoice {
299 group_id: invocation.group_id.clone(),
300 id: invocation.id.clone(),
301 call_id: invocation.call_id.clone(),
302 prompt: prompt.to_owned(),
303 choices,
304 default,
305 response_url: response_url.to_owned(),
306 });
307 let body = serde_json::to_string(&msg).expect("bug: failed to serialize user_choice");
308 if let Err(e) = client.post_json(&invocation.callback_url, &body).await {
309 tracing::error!("failed to send user_choice: {e}");
310 }
311}
312
313pub async fn send_subscription_event<C: CallbackClient>(
315 client: &C,
316 callback_url: &str,
317 group_id: ThreadId,
318 tool_call_id: String,
319 text: &str,
320 associative: bool,
321 r#final: bool,
322) {
323 let event = RapCallback::SubscriptionEvent(RapSubscriptionEvent {
324 group_id,
325 tool_call_id,
326 text: text.to_owned(),
327 associative,
328 r#final: if r#final { Some(true) } else { None },
329 });
330 let body = serde_json::to_string(&event).expect("bug: failed to serialize subscription event");
331 if let Err(e) = client.post_json(callback_url, &body).await {
332 tracing::error!("failed to send subscription event: {e}");
333 }
334}
335
336pub async fn send_view_update<C: CallbackClient>(
338 client: &C,
339 callback_url: &str,
340 group_id: ThreadId,
341 view_type: &str,
342 content: serde_json::Value,
343) {
344 let msg = RapCallback::ViewUpdate(RapViewUpdate {
345 group_id,
346 view_type: view_type.to_owned(),
347 content,
348 });
349 let body = serde_json::to_string(&msg).expect("bug: failed to serialize view_update");
350 if let Err(e) = client.post_json(callback_url, &body).await {
351 tracing::error!("failed to send view_update: {e}");
352 }
353}
354
355pub async fn log_panic<F: Future<Output = ()>>(name: &'static str, f: F) {
358 use futures_util::FutureExt;
359 if let Err(e) = AssertUnwindSafe(f).catch_unwind().await {
360 let msg = if let Some(s) = e.downcast_ref::<&str>() {
361 *s
362 } else if let Some(s) = e.downcast_ref::<String>() {
363 &**s
364 } else {
365 "unknown panic payload"
366 };
367 tracing::error!("task '{name}' panicked: {msg}");
368 }
369}