1use aster::conversation::message::{
2 ActionRequiredData, Message, MessageContent, ToolRequest, ToolResponse,
3};
4use aster::utils::safe_truncate;
5use rmcp::model::{RawContent, ResourceContents, Role};
6use serde_json::Value;
7
8const MAX_STRING_LENGTH_MD_EXPORT: usize = 4096; const REDACTED_PREFIX_LENGTH: usize = 100; fn value_to_simple_markdown_string(value: &Value, export_full_strings: bool) -> String {
12 match value {
13 Value::String(s) => {
14 if !export_full_strings && s.chars().count() > MAX_STRING_LENGTH_MD_EXPORT {
15 let prefix = safe_truncate(s, REDACTED_PREFIX_LENGTH);
16 let trimmed_chars = s.chars().count() - prefix.chars().count();
17 format!("`{}[ ... trimmed : {} chars ... ]`", prefix, trimmed_chars)
18 } else {
19 let escaped = s.replace('`', "\\`").replace("\n", "\\\\n");
21 format!("`{}`", escaped)
22 }
23 }
24 Value::Number(n) => n.to_string(),
25 Value::Bool(b) => format!("*{}*", b),
26 Value::Null => "_null_".to_string(),
27 _ => "`[Complex Value]`".to_string(),
28 }
29}
30
31fn value_to_markdown(value: &Value, depth: usize, export_full_strings: bool) -> String {
32 let mut md_string = String::new();
33 let base_indent_str = " ".repeat(depth); match value {
36 Value::Object(map) => {
37 if map.is_empty() {
38 md_string.push_str(&format!("{}*empty object*\n", base_indent_str));
39 } else {
40 for (key, val) in map {
41 md_string.push_str(&format!("{}* **{}**: ", base_indent_str, key));
42 match val {
43 Value::String(s) => {
44 if s.contains('\n') || s.chars().count() > 80 {
45 md_string.push_str(&format!(
47 "\n{} ```\n{}{}\n{} ```\n",
48 base_indent_str,
49 base_indent_str,
50 s.trim(),
51 base_indent_str
52 ));
53 } else {
54 md_string.push_str(&format!("`{}`\n", s.replace('`', "\\`")));
55 }
56 }
57 _ => {
58 md_string.push('\n');
60 md_string.push_str(&value_to_markdown(
61 val,
62 depth + 2,
63 export_full_strings,
64 ));
65 }
66 }
67 }
68 }
69 }
70 Value::Array(arr) => {
71 if arr.is_empty() {
72 md_string.push_str(&format!("{}* *empty list*\n", base_indent_str));
73 } else {
74 for item in arr {
75 md_string.push_str(&format!("{}* - ", base_indent_str));
76 match item {
77 Value::String(s) => {
78 if s.contains('\n') || s.chars().count() > 80 {
79 md_string.push_str(&format!(
81 "\n{} ```\n{}{}\n{} ```\n",
82 base_indent_str,
83 base_indent_str,
84 s.trim(),
85 base_indent_str
86 ));
87 } else {
88 md_string.push_str(&format!("`{}`\n", s.replace('`', "\\`")));
89 }
90 }
91 _ => {
92 md_string.push('\n');
94 md_string.push_str(&value_to_markdown(
95 item,
96 depth + 2,
97 export_full_strings,
98 ));
99 }
100 }
101 }
102 }
103 }
104 _ => {
105 md_string.push_str(&format!(
106 "{}{}\n",
107 base_indent_str,
108 value_to_simple_markdown_string(value, export_full_strings)
109 ));
110 }
111 }
112 md_string
113}
114
115pub fn tool_request_to_markdown(req: &ToolRequest, export_all_content: bool) -> String {
116 let mut md = String::new();
117 match &req.tool_call {
118 Ok(call) => {
119 let parts: Vec<_> = call.name.rsplitn(2, "__").collect();
120 let (namespace, tool_name_only) = if parts.len() == 2 {
121 (parts[1], parts[0])
122 } else {
123 ("Tool", parts[0])
124 };
125
126 md.push_str(&format!(
127 "#### Tool Call: `{}` (namespace: `{}`)\n",
128 tool_name_only, namespace
129 ));
130 md.push_str("**Arguments:**\n");
131
132 match call.name.as_ref() {
133 "developer__shell" => {
134 if let Some(Value::String(command)) =
135 call.arguments.as_ref().and_then(|args| args.get("command"))
136 {
137 md.push_str(&format!(
138 "* **command**:\n ```sh\n {}\n ```\n",
139 command.trim()
140 ));
141 }
142 let other_args: serde_json::Map<String, Value> = call
143 .arguments
144 .as_ref()
145 .map(|obj| {
146 obj.iter()
147 .filter(|(k, _)| k.as_str() != "command")
148 .map(|(k, v)| (k.clone(), v.clone()))
149 .collect()
150 })
151 .unwrap_or_default();
152 if !other_args.is_empty() {
153 md.push_str(&value_to_markdown(
154 &Value::Object(other_args),
155 0,
156 export_all_content,
157 ));
158 }
159 }
160 "developer__text_editor" => {
161 if let Some(Value::String(path)) =
162 call.arguments.as_ref().and_then(|args| args.get("path"))
163 {
164 md.push_str(&format!("* **path**: `{}`\n", path));
165 }
166 if let Some(Value::String(code_edit)) = call
167 .arguments
168 .as_ref()
169 .and_then(|args| args.get("code_edit"))
170 {
171 md.push_str(&format!(
172 "* **code_edit**:\n ```\n{}\n ```\n",
173 code_edit
174 ));
175 }
176
177 let other_args: serde_json::Map<String, Value> = call
178 .arguments
179 .as_ref()
180 .map(|obj| {
181 obj.iter()
182 .filter(|(k, _)| k.as_str() != "path" && k.as_str() != "code_edit")
183 .map(|(k, v)| (k.clone(), v.clone()))
184 .collect()
185 })
186 .unwrap_or_default();
187 if !other_args.is_empty() {
188 md.push_str(&value_to_markdown(
189 &Value::Object(other_args),
190 0,
191 export_all_content,
192 ));
193 }
194 }
195 _ => {
196 if let Some(args) = &call.arguments {
197 md.push_str(&value_to_markdown(
198 &Value::Object(args.clone()),
199 0,
200 export_all_content,
201 ));
202 } else {
203 md.push_str("*No arguments*\n");
204 }
205 }
206 }
207 }
208 Err(e) => {
209 md.push_str(&format!(
210 "**Error in Tool Call:**\n```\n{}
211```\n",
212 e
213 ));
214 }
215 }
216 md
217}
218
219pub fn tool_response_to_markdown(resp: &ToolResponse, export_all_content: bool) -> String {
220 let mut md = String::new();
221 md.push_str("#### Tool Response:\n");
222
223 match &resp.tool_result {
224 Ok(result) => {
225 if result.content.is_empty() {
226 md.push_str("*No textual output from tool.*\n");
227 }
228
229 for content in &result.content {
230 if !export_all_content {
231 if let Some(audience) = content.audience() {
232 if !audience.contains(&Role::Assistant) {
233 continue;
234 }
235 }
236 }
237
238 match &content.raw {
239 RawContent::Text(text_content) => {
240 let trimmed_text = text_content.text.trim();
241 if (trimmed_text.starts_with('{') && trimmed_text.ends_with('}'))
242 || (trimmed_text.starts_with('[') && trimmed_text.ends_with(']'))
243 {
244 md.push_str(&format!("```json\n{}\n```\n", trimmed_text));
245 } else if trimmed_text.starts_with('<')
246 && trimmed_text.ends_with('>')
247 && trimmed_text.contains("</")
248 {
249 md.push_str(&format!("```xml\n{}\n```\n", trimmed_text));
250 } else {
251 md.push_str(&text_content.text);
252 md.push_str("\n\n");
253 }
254 }
255 RawContent::Image(image_content) => {
256 if image_content.mime_type.starts_with("image/") {
257 md.push_str(&format!(
259 "**Image:** `(type: {}, data: first 30 chars of base64...)`\n\n",
260 image_content.mime_type
261 ));
262 } else {
263 md.push_str(&format!(
265 "**Binary Content:** `(type: {}, length: {} bytes)`\n\n",
266 image_content.mime_type,
267 image_content.data.len()
268 ));
269 }
270 }
271 RawContent::Resource(resource) => {
272 match &resource.resource {
273 ResourceContents::TextResourceContents {
274 uri,
275 mime_type,
276 text,
277 meta: _,
278 } => {
279 let file_extension = uri.split('.').next_back().unwrap_or("");
281 let syntax_type = match file_extension {
282 "rs" => "rust",
283 "js" => "javascript",
284 "ts" => "typescript",
285 "py" => "python",
286 "json" => "json",
287 "yaml" | "yml" => "yaml",
288 "md" => "markdown",
289 "html" => "html",
290 "css" => "css",
291 "sh" => "bash",
292 _ => mime_type
293 .as_ref()
294 .map(|mime| if mime == "text" { "" } else { mime })
295 .unwrap_or(""),
296 };
297
298 md.push_str(&format!("**File:** `{}`\n", uri));
299 md.push_str(&format!(
300 "```{}\n{}\n```\n\n",
301 syntax_type,
302 text.trim()
303 ));
304 }
305 ResourceContents::BlobResourceContents {
306 uri,
307 mime_type,
308 blob,
309 ..
310 } => {
311 md.push_str(&format!(
312 "**Binary File:** `{}` (type: {}, {} bytes)\n\n",
313 uri,
314 mime_type.as_ref().map(|s| s.as_str()).unwrap_or("unknown"),
315 blob.len()
316 ));
317 }
318 }
319 }
320 RawContent::ResourceLink(_link) => {
321 md.push_str("[resource link]\n\n");
323 }
324 RawContent::Audio(_) => {
325 md.push_str("[audio content not displayed in Markdown export]\n\n")
326 }
327 }
328 }
329 }
330 Err(e) => {
331 md.push_str(&format!(
332 "**Error in Tool Response:**\n```\n{}
333```\n",
334 e
335 ));
336 }
337 }
338 md
339}
340
341pub fn message_to_markdown(message: &Message, export_all_content: bool) -> String {
342 let mut md = String::new();
343 for content in &message.content {
344 match content {
345 MessageContent::ActionRequired(action) => match &action.data {
346 ActionRequiredData::ToolConfirmation { tool_name, .. } => {
347 md.push_str(&format!(
348 "**Action Required** (tool_confirmation): {}\n\n",
349 tool_name
350 ));
351 }
352 ActionRequiredData::Elicitation { message, .. } => {
353 md.push_str(&format!(
354 "**Action Required** (elicitation): {}\n\n",
355 message
356 ));
357 }
358 ActionRequiredData::ElicitationResponse { id, user_data } => {
359 md.push_str(&format!(
360 "**Action Required** (elicitation_response): {}\n```json\n{}\n```\n\n",
361 id,
362 serde_json::to_string_pretty(user_data)
363 .unwrap_or_else(|_| "{}".to_string())
364 ));
365 }
366 },
367 MessageContent::Text(text) => {
368 md.push_str(&text.text);
369 md.push_str("\n\n");
370 }
371 MessageContent::ToolRequest(req) => {
372 md.push_str(&tool_request_to_markdown(req, export_all_content));
373 md.push('\n');
374 }
375 MessageContent::ToolResponse(resp) => {
376 md.push_str(&tool_response_to_markdown(resp, export_all_content));
377 md.push('\n');
378 }
379 MessageContent::Image(image) => {
380 md.push_str(&format!(
381 "**Image:** `(type: {}, data placeholder: {}...)`\n\n",
382 image.mime_type,
383 image.data.chars().take(30).collect::<String>()
384 ));
385 }
386 MessageContent::Thinking(thinking) => {
387 md.push_str("**Thinking:**\n");
388 md.push_str("> ");
389 md.push_str(&thinking.thinking.replace("\n", "\n> "));
390 md.push_str("\n\n");
391 }
392 MessageContent::RedactedThinking(_) => {
393 md.push_str("**Thinking:**\n");
394 md.push_str("> *Thinking was redacted*\n\n");
395 }
396 MessageContent::SystemNotification(notification) => {
397 md.push_str(&format!("*{}*\n\n", notification.msg));
398 }
399 _ => {
400 md.push_str(
401 "`WARNING: Message content type could not be rendered to Markdown`\n\n",
402 );
403 }
404 }
405 }
406 md.trim_end_matches("\n").to_string()
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use aster::conversation::message::{Message, ToolRequest, ToolResponse};
413 use rmcp::model::{CallToolRequestParam, Content, RawTextContent, TextContent};
414 use rmcp::object;
415 use serde_json::json;
416
417 #[test]
418 fn test_value_to_simple_markdown_string_normal() {
419 let value = json!("hello world");
420 let result = value_to_simple_markdown_string(&value, true);
421 assert_eq!(result, "`hello world`");
422 }
423
424 #[test]
425 fn test_value_to_simple_markdown_string_with_backticks() {
426 let value = json!("hello `world`");
427 let result = value_to_simple_markdown_string(&value, true);
428 assert_eq!(result, "`hello \\`world\\``");
429 }
430
431 #[test]
432 fn test_value_to_simple_markdown_string_long_string_full_export() {
433 let long_string = "a".repeat(5000);
434 let value = json!(long_string);
435 let result = value_to_simple_markdown_string(&value, true);
436 assert!(result.starts_with("`"));
438 assert!(result.ends_with("`"));
439 assert!(result.contains(&"a".repeat(5000)));
440 }
441
442 #[test]
443 fn test_value_to_simple_markdown_string_long_string_trimmed() {
444 let long_string = "a".repeat(5000);
445 let value = json!(long_string);
446 let result = value_to_simple_markdown_string(&value, false);
447 assert!(result.starts_with("`"));
449 assert!(result.contains("[ ... trimmed : "));
450 assert!(result.contains("4900 chars ... ]`"));
451 assert!(result.contains(&"a".repeat(97))); }
453
454 #[test]
455 fn test_value_to_simple_markdown_string_numbers_and_bools() {
456 assert_eq!(value_to_simple_markdown_string(&json!(42), true), "42");
457 assert_eq!(
458 value_to_simple_markdown_string(&json!(true), true),
459 "*true*"
460 );
461 assert_eq!(
462 value_to_simple_markdown_string(&json!(false), true),
463 "*false*"
464 );
465 assert_eq!(
466 value_to_simple_markdown_string(&json!(null), true),
467 "_null_"
468 );
469 }
470
471 #[test]
472 fn test_value_to_markdown_empty_object() {
473 let value = json!({});
474 let result = value_to_markdown(&value, 0, true);
475 assert!(result.contains("*empty object*"));
476 }
477
478 #[test]
479 fn test_value_to_markdown_empty_array() {
480 let value = json!([]);
481 let result = value_to_markdown(&value, 0, true);
482 assert!(result.contains("*empty list*"));
483 }
484
485 #[test]
486 fn test_value_to_markdown_simple_object() {
487 let value = json!({
488 "name": "test",
489 "count": 42,
490 "active": true
491 });
492 let result = value_to_markdown(&value, 0, true);
493 assert!(result.contains("**name**"));
494 assert!(result.contains("`test`"));
495 assert!(result.contains("**count**"));
496 assert!(result.contains("42"));
497 assert!(result.contains("**active**"));
498 assert!(result.contains("*true*"));
499 }
500
501 #[test]
502 fn test_value_to_markdown_nested_object() {
503 let value = json!({
504 "user": {
505 "name": "Alice",
506 "age": 30
507 }
508 });
509 let result = value_to_markdown(&value, 0, true);
510 assert!(result.contains("**user**"));
511 assert!(result.contains("**name**"));
512 assert!(result.contains("`Alice`"));
513 assert!(result.contains("**age**"));
514 assert!(result.contains("30"));
515 }
516
517 #[test]
518 fn test_value_to_markdown_array_with_items() {
519 let value = json!(["item1", "item2", 42]);
520 let result = value_to_markdown(&value, 0, true);
521 assert!(result.contains("- `item1`"));
522 assert!(result.contains("- `item2`"));
523 assert!(result.contains("42"));
525 }
526
527 #[test]
528 fn test_tool_request_to_markdown_shell() {
529 let tool_call = CallToolRequestParam {
530 name: "developer__shell".into(),
531 arguments: Some(object!({
532 "command": "ls -la",
533 "working_dir": "/home/user"
534 })),
535 };
536 let tool_request = ToolRequest {
537 id: "test-id".to_string(),
538 tool_call: Ok(tool_call),
539 metadata: None,
540 tool_meta: None,
541 };
542
543 let result = tool_request_to_markdown(&tool_request, true);
544 assert!(result.contains("#### Tool Call: `shell`"));
545 assert!(result.contains("namespace: `developer`"));
546 assert!(result.contains("**command**:"));
547 assert!(result.contains("```sh"));
548 assert!(result.contains("ls -la"));
549 assert!(result.contains("**working_dir**"));
550 }
551
552 #[test]
553 fn test_tool_request_to_markdown_text_editor() {
554 let tool_call = CallToolRequestParam {
555 name: "developer__text_editor".into(),
556 arguments: Some(object!({
557 "path": "/path/to/file.txt",
558 "code_edit": "print('Hello World')"
559 })),
560 };
561 let tool_request = ToolRequest {
562 id: "test-id".to_string(),
563 tool_call: Ok(tool_call),
564 metadata: None,
565 tool_meta: None,
566 };
567
568 let result = tool_request_to_markdown(&tool_request, true);
569 assert!(result.contains("#### Tool Call: `text_editor`"));
570 assert!(result.contains("**path**: `/path/to/file.txt`"));
571 assert!(result.contains("**code_edit**:"));
572 assert!(result.contains("print('Hello World')"));
573 }
574
575 #[test]
576 fn test_tool_response_to_markdown_text() {
577 let text_content = TextContent {
578 raw: RawTextContent {
579 text: "Command executed successfully".to_string(),
580 meta: None,
581 },
582 annotations: None,
583 };
584 let tool_response = ToolResponse {
585 metadata: None,
586 id: "test-id".to_string(),
587 tool_result: Ok(rmcp::model::CallToolResult {
588 content: vec![Content::text(text_content.raw.text)],
589 structured_content: None,
590 is_error: Some(false),
591 meta: None,
592 }),
593 };
594
595 let result = tool_response_to_markdown(&tool_response, true);
596 assert!(result.contains("#### Tool Response:"));
597 assert!(result.contains("Command executed successfully"));
598 }
599
600 #[test]
601 fn test_tool_response_to_markdown_json() {
602 let json_text = r#"{"status": "success", "data": "test"}"#;
603 let text_content = TextContent {
604 raw: RawTextContent {
605 text: json_text.to_string(),
606 meta: None,
607 },
608 annotations: None,
609 };
610 let tool_response = ToolResponse {
611 metadata: None,
612 id: "test-id".to_string(),
613 tool_result: Ok(rmcp::model::CallToolResult {
614 content: vec![Content::text(text_content.raw.text)],
615 structured_content: None,
616 is_error: Some(false),
617 meta: None,
618 }),
619 };
620
621 let result = tool_response_to_markdown(&tool_response, true);
622 assert!(result.contains("#### Tool Response:"));
623 assert!(result.contains("```json"));
624 assert!(result.contains(json_text));
625 }
626
627 #[test]
628 fn test_message_to_markdown_text() {
629 let message = Message::user().with_text("Hello, this is a test message");
630
631 let result = message_to_markdown(&message, true);
632 assert_eq!(result, "Hello, this is a test message");
633 }
634
635 #[test]
636 fn test_message_to_markdown_with_tool_request() {
637 let tool_call = CallToolRequestParam {
638 name: "test_tool".into(),
639 arguments: Some(object!({"param": "value"})),
640 };
641
642 let message = Message::assistant().with_tool_request("test-id", Ok(tool_call));
643
644 let result = message_to_markdown(&message, true);
645 assert!(result.contains("#### Tool Call: `test_tool`"));
646 assert!(result.contains("**param**"));
647 }
648
649 #[test]
650 fn test_message_to_markdown_thinking() {
651 let message = Message::assistant()
652 .with_thinking("I need to analyze this problem...", "test-signature");
653
654 let result = message_to_markdown(&message, true);
655 assert!(result.contains("**Thinking:**"));
656 assert!(result.contains("> I need to analyze this problem..."));
657 }
658
659 #[test]
660 fn test_message_to_markdown_redacted_thinking() {
661 let message = Message::assistant().with_redacted_thinking("redacted-data");
662
663 let result = message_to_markdown(&message, true);
664 assert!(result.contains("**Thinking:**"));
665 assert!(result.contains("> *Thinking was redacted*"));
666 }
667
668 #[test]
669 fn test_recursive_value_to_markdown() {
670 let value = json!({
672 "level1": {
673 "level2": {
674 "data": "nested value"
675 },
676 "array": [
677 {"item": "first"},
678 {"item": "second"}
679 ]
680 }
681 });
682
683 let result = value_to_markdown(&value, 0, true);
684 assert!(result.contains("**level1**"));
685 assert!(result.contains("**level2**"));
686 assert!(result.contains("**data**"));
687 assert!(result.contains("`nested value`"));
688 assert!(result.contains("**array**"));
689 assert!(result.contains("**item**"));
690 assert!(result.contains("`first`"));
691 assert!(result.contains("`second`"));
692 }
693
694 #[test]
695 fn test_shell_tool_with_code_output() {
696 let tool_call = CallToolRequestParam {
697 name: "developer__shell".into(),
698 arguments: Some(object!({
699 "command": "cat main.py"
700 })),
701 };
702 let tool_request = ToolRequest {
703 id: "shell-cat".to_string(),
704 tool_call: Ok(tool_call),
705 metadata: None,
706 tool_meta: None,
707 };
708
709 let python_code = r#"#!/usr/bin/env python3
710def hello_world():
711 print("Hello, World!")
712
713if __name__ == "__main__":
714 hello_world()"#;
715
716 let text_content = TextContent {
717 raw: RawTextContent {
718 text: python_code.to_string(),
719 meta: None,
720 },
721 annotations: None,
722 };
723 let tool_response = ToolResponse {
724 metadata: None,
725 id: "shell-cat".to_string(),
726 tool_result: Ok(rmcp::model::CallToolResult {
727 content: vec![Content::text(text_content.raw.text)],
728 structured_content: None,
729 is_error: Some(false),
730 meta: None,
731 }),
732 };
733
734 let request_result = tool_request_to_markdown(&tool_request, true);
735 let response_result = tool_response_to_markdown(&tool_response, true);
736
737 assert!(request_result.contains("#### Tool Call: `shell`"));
739 assert!(request_result.contains("```sh"));
740 assert!(request_result.contains("cat main.py"));
741
742 assert!(response_result.contains("#### Tool Response:"));
744 assert!(response_result.contains("def hello_world():"));
745 assert!(response_result.contains("print(\"Hello, World!\")"));
746 }
747
748 #[test]
749 fn test_shell_tool_with_git_commands() {
750 let git_status_call = CallToolRequestParam {
751 name: "developer__shell".into(),
752 arguments: Some(object!({
753 "command": "git status --porcelain"
754 })),
755 };
756 let tool_request = ToolRequest {
757 id: "git-status".to_string(),
758 tool_call: Ok(git_status_call),
759 metadata: None,
760 tool_meta: None,
761 };
762
763 let git_output = " M src/main.rs\n?? temp.txt\n A new_feature.rs";
764 let text_content = TextContent {
765 raw: RawTextContent {
766 text: git_output.to_string(),
767 meta: None,
768 },
769 annotations: None,
770 };
771 let tool_response = ToolResponse {
772 metadata: None,
773 id: "git-status".to_string(),
774 tool_result: Ok(rmcp::model::CallToolResult {
775 content: vec![Content::text(text_content.raw.text)],
776 structured_content: None,
777 is_error: Some(false),
778 meta: None,
779 }),
780 };
781
782 let request_result = tool_request_to_markdown(&tool_request, true);
783 let response_result = tool_response_to_markdown(&tool_response, true);
784
785 assert!(request_result.contains("git status --porcelain"));
787 assert!(request_result.contains("```sh"));
788
789 assert!(response_result.contains("M src/main.rs"));
791 assert!(response_result.contains("?? temp.txt"));
792 }
793
794 #[test]
795 fn test_shell_tool_with_build_output() {
796 let cargo_build_call = CallToolRequestParam {
797 name: "developer__shell".into(),
798 arguments: Some(object!({
799 "command": "cargo build"
800 })),
801 };
802 let _tool_request = ToolRequest {
803 id: "cargo-build".to_string(),
804 tool_call: Ok(cargo_build_call),
805 metadata: None,
806 tool_meta: None,
807 };
808
809 let build_output = r#" Compiling aster-cli v0.1.0 (/Users/user/aster)
810warning: unused variable `x`
811 --> src/main.rs:10:9
812 |
81310 | let x = 5;
814 | ^ help: if this is intentional, prefix it with an underscore: `_x`
815 |
816 = note: `#[warn(unused_variables)]` on by default
817
818 Finished dev [unoptimized + debuginfo] target(s) in 2.45s"#;
819
820 let text_content = TextContent {
821 raw: RawTextContent {
822 text: build_output.to_string(),
823 meta: None,
824 },
825 annotations: None,
826 };
827 let tool_response = ToolResponse {
828 metadata: None,
829 id: "cargo-build".to_string(),
830 tool_result: Ok(rmcp::model::CallToolResult {
831 content: vec![Content::text(text_content.raw.text)],
832 structured_content: None,
833 is_error: Some(false),
834 meta: None,
835 }),
836 };
837
838 let response_result = tool_response_to_markdown(&tool_response, true);
839
840 assert!(response_result.contains("Compiling aster-cli"));
842 assert!(response_result.contains("warning: unused variable"));
843 assert!(response_result.contains("Finished dev"));
844 }
845
846 #[test]
847 fn test_shell_tool_with_json_api_response() {
848 let curl_call = CallToolRequestParam {
849 name: "developer__shell".into(),
850 arguments: Some(object!({
851 "command": "curl -s https://api.github.com/repos/microsoft/vscode/releases/latest"
852 })),
853 };
854 let _tool_request = ToolRequest {
855 id: "curl-api".to_string(),
856 tool_call: Ok(curl_call),
857 metadata: None,
858 tool_meta: None,
859 };
860
861 let api_response = r#"{
862 "url": "https://api.github.com/repos/microsoft/vscode/releases/90543298",
863 "tag_name": "1.85.0",
864 "name": "1.85.0",
865 "published_at": "2023-12-07T16:54:32Z",
866 "assets": [
867 {
868 "name": "VSCode-darwin-universal.zip",
869 "download_count": 123456
870 }
871 ]
872}"#;
873
874 let text_content = TextContent {
875 raw: RawTextContent {
876 text: api_response.to_string(),
877 meta: None,
878 },
879 annotations: None,
880 };
881 let tool_response = ToolResponse {
882 metadata: None,
883 id: "curl-api".to_string(),
884 tool_result: Ok(rmcp::model::CallToolResult {
885 content: vec![Content::text(text_content.raw.text)],
886 structured_content: None,
887 is_error: Some(false),
888 meta: None,
889 }),
890 };
891
892 let response_result = tool_response_to_markdown(&tool_response, true);
893
894 assert!(response_result.contains("```json"));
896 assert!(response_result.contains("\"tag_name\": \"1.85.0\""));
897 assert!(response_result.contains("\"download_count\": 123456"));
898 }
899
900 #[test]
901 fn test_text_editor_tool_with_code_creation() {
902 let editor_call = CallToolRequestParam {
903 name: "developer__text_editor".into(),
904 arguments: Some(object!({
905 "command": "write",
906 "path": "/tmp/fibonacci.js",
907 "file_text": "function fibonacci(n) {\n if (n <= 1) return n;\n return fibonacci(n - 1) + fibonacci(n - 2);\n}\n\nconsole.log(fibonacci(10));"
908 })),
909 };
910 let tool_request = ToolRequest {
911 id: "editor-write".to_string(),
912 tool_call: Ok(editor_call),
913 metadata: None,
914 tool_meta: None,
915 };
916
917 let text_content = TextContent {
918 raw: RawTextContent {
919 text: "File created successfully".to_string(),
920 meta: None,
921 },
922 annotations: None,
923 };
924 let tool_response = ToolResponse {
925 metadata: None,
926 id: "editor-write".to_string(),
927 tool_result: Ok(rmcp::model::CallToolResult {
928 content: vec![Content::text(text_content.raw.text)],
929 structured_content: None,
930 is_error: Some(false),
931 meta: None,
932 }),
933 };
934
935 let request_result = tool_request_to_markdown(&tool_request, true);
936 let response_result = tool_response_to_markdown(&tool_response, true);
937
938 assert!(request_result.contains("#### Tool Call: `text_editor`"));
940 assert!(request_result.contains("**path**: `/tmp/fibonacci.js`"));
941 assert!(request_result.contains("**file_text**:"));
942 assert!(request_result.contains("function fibonacci(n)"));
943 assert!(request_result.contains("return fibonacci(n - 1)"));
944
945 assert!(response_result.contains("File created successfully"));
947 }
948
949 #[test]
950 fn test_text_editor_tool_view_code() {
951 let editor_call = CallToolRequestParam {
952 name: "developer__text_editor".into(),
953 arguments: Some(object!({
954 "command": "view",
955 "path": "/src/utils.py"
956 })),
957 };
958 let _tool_request = ToolRequest {
959 id: "editor-view".to_string(),
960 tool_call: Ok(editor_call),
961 metadata: None,
962 tool_meta: None,
963 };
964
965 let python_code = r#"import os
966import json
967from typing import Dict, List, Optional
968
969def load_config(config_path: str) -> Dict:
970 """Load configuration from JSON file."""
971 if not os.path.exists(config_path):
972 raise FileNotFoundError(f"Config file not found: {config_path}")
973
974 with open(config_path, 'r') as f:
975 return json.load(f)
976
977def process_data(data: List[Dict]) -> List[Dict]:
978 """Process a list of data dictionaries."""
979 return [item for item in data if item.get('active', False)]"#;
980
981 let text_content = TextContent {
982 raw: RawTextContent {
983 text: python_code.to_string(),
984 meta: None,
985 },
986 annotations: None,
987 };
988 let tool_response = ToolResponse {
989 metadata: None,
990 id: "editor-view".to_string(),
991 tool_result: Ok(rmcp::model::CallToolResult {
992 content: vec![Content::text(text_content.raw.text)],
993 structured_content: None,
994 is_error: Some(false),
995 meta: None,
996 }),
997 };
998
999 let response_result = tool_response_to_markdown(&tool_response, true);
1000
1001 assert!(response_result.contains("import os"));
1003 assert!(response_result.contains("def load_config"));
1004 assert!(response_result.contains("typing import Dict"));
1005 }
1006
1007 #[test]
1008 fn test_shell_tool_with_error_output() {
1009 let error_call = CallToolRequestParam {
1010 name: "developer__shell".into(),
1011 arguments: Some(object!({
1012 "command": "python nonexistent_script.py"
1013 })),
1014 };
1015 let _tool_request = ToolRequest {
1016 id: "shell-error".to_string(),
1017 tool_call: Ok(error_call),
1018 metadata: None,
1019 tool_meta: None,
1020 };
1021
1022 let error_output = r#"python: can't open file 'nonexistent_script.py': [Errno 2] No such file or directory
1023Command failed with exit code 2"#;
1024
1025 let text_content = TextContent {
1026 raw: RawTextContent {
1027 text: error_output.to_string(),
1028 meta: None,
1029 },
1030 annotations: None,
1031 };
1032 let tool_response = ToolResponse {
1033 metadata: None,
1034 id: "shell-error".to_string(),
1035 tool_result: Ok(rmcp::model::CallToolResult {
1036 content: vec![Content::text(text_content.raw.text)],
1037 structured_content: None,
1038 is_error: Some(false),
1039 meta: None,
1040 }),
1041 };
1042
1043 let response_result = tool_response_to_markdown(&tool_response, true);
1044
1045 assert!(response_result.contains("can't open file"));
1047 assert!(response_result.contains("Command failed with exit code 2"));
1048 }
1049
1050 #[test]
1051 fn test_shell_tool_complex_script_execution() {
1052 let script_call = CallToolRequestParam {
1053 name: "developer__shell".into(),
1054 arguments: Some(object!({
1055 "command": "python -c \"import sys; print(f'Python {sys.version}'); [print(f'{i}^2 = {i**2}') for i in range(1, 6)]\""
1056 })),
1057 };
1058 let tool_request = ToolRequest {
1059 id: "script-exec".to_string(),
1060 tool_call: Ok(script_call),
1061 metadata: None,
1062 tool_meta: None,
1063 };
1064
1065 let script_output = r#"Python 3.11.5 (main, Aug 24 2023, 15:18:16) [Clang 14.0.3 ]
10661^2 = 1
10672^2 = 4
10683^2 = 9
10694^2 = 16
10705^2 = 25"#;
1071
1072 let text_content = TextContent {
1073 raw: RawTextContent {
1074 text: script_output.to_string(),
1075 meta: None,
1076 },
1077 annotations: None,
1078 };
1079 let tool_response = ToolResponse {
1080 metadata: None,
1081 id: "script-exec".to_string(),
1082 tool_result: Ok(rmcp::model::CallToolResult {
1083 content: vec![Content::text(text_content.raw.text)],
1084 structured_content: None,
1085 is_error: Some(false),
1086 meta: None,
1087 }),
1088 };
1089
1090 let request_result = tool_request_to_markdown(&tool_request, true);
1091 let response_result = tool_response_to_markdown(&tool_response, true);
1092
1093 assert!(request_result.contains("```sh"));
1095 assert!(request_result.contains("python -c"));
1096 assert!(request_result.contains("sys.version"));
1097
1098 assert!(response_result.contains("Python 3.11.5"));
1100 assert!(response_result.contains("1^2 = 1"));
1101 assert!(response_result.contains("5^2 = 25"));
1102 }
1103
1104 #[test]
1105 fn test_shell_tool_with_multi_command() {
1106 let multi_call = CallToolRequestParam {
1107 name: "developer__shell".into(),
1108 arguments: Some(object!({
1109 "command": "cd /tmp && ls -la | head -5 && pwd"
1110 })),
1111 };
1112 let _tool_request = ToolRequest {
1113 id: "multi-cmd".to_string(),
1114 tool_call: Ok(multi_call),
1115 metadata: None,
1116 tool_meta: None,
1117 };
1118
1119 let multi_output = r#"total 24
1120drwxrwxrwt 15 root wheel 480 Dec 7 10:30 .
1121drwxr-xr-x 6 root wheel 192 Nov 15 09:15 ..
1122-rw-r--r-- 1 user staff 256 Dec 7 09:45 config.json
1123drwx------ 3 user staff 96 Dec 6 16:20 com.apple.launchd.abc
1124/tmp"#;
1125
1126 let text_content = TextContent {
1127 raw: RawTextContent {
1128 text: multi_output.to_string(),
1129 meta: None,
1130 },
1131 annotations: None,
1132 };
1133 let tool_response = ToolResponse {
1134 metadata: None,
1135 id: "multi-cmd".to_string(),
1136 tool_result: Ok(rmcp::model::CallToolResult {
1137 content: vec![Content::text(text_content.raw.text)],
1138 structured_content: None,
1139 is_error: Some(false),
1140 meta: None,
1141 }),
1142 };
1143
1144 let request_result = tool_request_to_markdown(&_tool_request, true);
1145 let response_result = tool_response_to_markdown(&tool_response, true);
1146
1147 assert!(request_result.contains("cd /tmp && ls -la | head -5 && pwd"));
1149
1150 assert!(response_result.contains("drwxrwxrwt"));
1152 assert!(response_result.contains("config.json"));
1153 assert!(response_result.contains("/tmp"));
1154 }
1155
1156 #[test]
1157 fn test_developer_tool_grep_code_search() {
1158 let grep_call = CallToolRequestParam {
1159 name: "developer__shell".into(),
1160 arguments: Some(object!({
1161 "command": "rg 'async fn' --type rust -n"
1162 })),
1163 };
1164 let tool_request = ToolRequest {
1165 id: "grep-search".to_string(),
1166 tool_call: Ok(grep_call),
1167 metadata: None,
1168 tool_meta: None,
1169 };
1170
1171 let grep_output = r#"src/main.rs:15:async fn process_request(req: Request) -> Result<Response> {
1172src/handler.rs:8:async fn handle_connection(stream: TcpStream) {
1173src/database.rs:23:async fn query_users(pool: &Pool) -> Result<Vec<User>> {
1174src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Result<Response> {"#;
1175
1176 let text_content = TextContent {
1177 raw: RawTextContent {
1178 text: grep_output.to_string(),
1179 meta: None,
1180 },
1181 annotations: None,
1182 };
1183 let tool_response = ToolResponse {
1184 metadata: None,
1185 id: "grep-search".to_string(),
1186 tool_result: Ok(rmcp::model::CallToolResult {
1187 content: vec![Content::text(text_content.raw.text)],
1188 structured_content: None,
1189 is_error: Some(false),
1190 meta: None,
1191 }),
1192 };
1193
1194 let request_result = tool_request_to_markdown(&tool_request, true);
1195 let response_result = tool_response_to_markdown(&tool_response, true);
1196
1197 assert!(request_result.contains("rg 'async fn' --type rust -n"));
1199
1200 assert!(response_result.contains("src/main.rs:15:"));
1202 assert!(response_result.contains("async fn process_request"));
1203 assert!(response_result.contains("src/database.rs:23:"));
1204 }
1205
1206 #[test]
1207 fn test_shell_tool_json_detection_works() {
1208 let tool_call = CallToolRequestParam {
1210 name: "developer__shell".into(),
1211 arguments: Some(object!({
1212 "command": "echo '{\"test\": \"json\"}'"
1213 })),
1214 };
1215 let _tool_request = ToolRequest {
1216 id: "json-test".to_string(),
1217 tool_call: Ok(tool_call),
1218 metadata: None,
1219 tool_meta: None,
1220 };
1221
1222 let json_output = r#"{"status": "success", "data": {"count": 42}}"#;
1223 let text_content = TextContent {
1224 raw: RawTextContent {
1225 text: json_output.to_string(),
1226 meta: None,
1227 },
1228 annotations: None,
1229 };
1230 let tool_response = ToolResponse {
1231 metadata: None,
1232 id: "json-test".to_string(),
1233 tool_result: Ok(rmcp::model::CallToolResult {
1234 content: vec![Content::text(text_content.raw.text)],
1235 structured_content: None,
1236 is_error: Some(false),
1237 meta: None,
1238 }),
1239 };
1240
1241 let response_result = tool_response_to_markdown(&tool_response, true);
1242
1243 assert!(response_result.contains("```json"));
1245 assert!(response_result.contains("\"status\": \"success\""));
1246 assert!(response_result.contains("\"count\": 42"));
1247 }
1248
1249 #[test]
1250 fn test_shell_tool_with_package_management() {
1251 let npm_call = CallToolRequestParam {
1252 name: "developer__shell".into(),
1253 arguments: Some(object!({
1254 "command": "npm install express typescript @types/node --save-dev"
1255 })),
1256 };
1257 let tool_request = ToolRequest {
1258 id: "npm-install".to_string(),
1259 tool_call: Ok(npm_call),
1260 metadata: None,
1261 tool_meta: None,
1262 };
1263
1264 let npm_output = r#"added 57 packages, and audited 58 packages in 3s
1265
12668 packages are looking for funding
1267 run `npm fund` for details
1268
1269found 0 vulnerabilities"#;
1270
1271 let text_content = TextContent {
1272 raw: RawTextContent {
1273 text: npm_output.to_string(),
1274 meta: None,
1275 },
1276 annotations: None,
1277 };
1278 let tool_response = ToolResponse {
1279 metadata: None,
1280 id: "npm-install".to_string(),
1281 tool_result: Ok(rmcp::model::CallToolResult {
1282 content: vec![Content::text(text_content.raw.text)],
1283 structured_content: None,
1284 is_error: Some(false),
1285 meta: None,
1286 }),
1287 };
1288
1289 let request_result = tool_request_to_markdown(&tool_request, true);
1290 let response_result = tool_response_to_markdown(&tool_response, true);
1291
1292 assert!(request_result.contains("npm install express typescript"));
1294 assert!(request_result.contains("--save-dev"));
1295
1296 assert!(response_result.contains("added 57 packages"));
1298 assert!(response_result.contains("found 0 vulnerabilities"));
1299 }
1300}