| 1 | //! REPL fence-extraction utilities. |
| 2 | //! |
| 3 | //! The agent's main loop scans assistant text for ` ```repl ` fenced blocks |
| 4 | //! and feeds them to a [`crate::repl::runtime::PythonRuntime`]. Capturing |
| 5 | //! `FINAL(...)` and routing sub-LLM RPCs are handled inside the runtime via |
| 6 | //! a stdin/stdout protocol — no scraping required here. |
| 7 | |
| 8 | /// Check if a string contains a `` ```repl `` fenced code block. |
| 9 | pub fn has_repl_block(text: &str) -> bool { |
| 10 | text.contains("```repl") |
| 11 | } |
| 12 | |
| 13 | /// Extract every `` ```repl `` block from `text` with byte offsets. |
| 14 | pub fn extract_repl_blocks(text: &str) -> Vec<ReplBlock> { |
| 15 | let mut blocks = Vec::new(); |
| 16 | let mut rest = text; |
| 17 | |
| 18 | while let Some(start_idx) = rest.find("```repl") { |
| 19 | let after_fence = &rest[start_idx..]; |
| 20 | let code_start = after_fence.find('\n').unwrap_or(after_fence.len()); |
| 21 | let code_region = &after_fence[code_start..]; |
| 22 | let Some(end_offset) = code_region.find("\n```") else { |
| 23 | break; |
| 24 | }; |
| 25 | let code = code_region[..end_offset].to_string(); |
| 26 | let global_start = text.len() - rest.len() + start_idx; |
| 27 | let global_end = global_start + code_start + end_offset + 3; |
| 28 | blocks.push(ReplBlock { |
| 29 | code, |
| 30 | start_offset: global_start, |
| 31 | end_offset: global_end, |
| 32 | }); |
| 33 | rest = &after_fence[code_start + end_offset + 4..]; |
| 34 | } |
| 35 | |
| 36 | blocks |
| 37 | } |
| 38 | |
| 39 | /// A `` ```repl `` code block with byte-offset position info. |
| 40 | #[derive(Debug, Clone)] |
| 41 | pub struct ReplBlock { |
| 42 | pub code: String, |
| 43 | pub start_offset: usize, |
| 44 | pub end_offset: usize, |
| 45 | } |
| 46 | |
| 47 | #[cfg(test)] |
| 48 | mod tests { |
| 49 | use super::*; |
| 50 | |
| 51 | #[test] |
| 52 | fn has_repl_block_detects_fence() { |
| 53 | assert!(has_repl_block("some text ```repl\ncode\n``` more")); |
| 54 | assert!(!has_repl_block("no repl here ```python\ncode\n```")); |
| 55 | assert!(!has_repl_block("just text")); |
| 56 | } |
| 57 | |
| 58 | #[test] |
| 59 | fn extract_repl_blocks_single() { |
| 60 | let text = "before\n```repl\nprint('hello')\n```\nafter"; |
| 61 | let blocks = extract_repl_blocks(text); |
| 62 | assert_eq!(blocks.len(), 1); |
| 63 | assert_eq!(blocks[0].code.trim(), "print('hello')"); |
| 64 | } |
| 65 | |
| 66 | #[test] |
| 67 | fn extract_repl_blocks_multiple() { |
| 68 | let text = "```repl\ncode1\n```\nmid\n```repl\ncode2\n```\nend"; |
| 69 | let blocks = extract_repl_blocks(text); |
| 70 | assert_eq!(blocks.len(), 2); |
| 71 | assert_eq!(blocks[0].code.trim(), "code1"); |
| 72 | assert_eq!(blocks[1].code.trim(), "code2"); |
| 73 | } |
| 74 | |
| 75 | #[test] |
| 76 | fn extract_repl_blocks_empty_when_none() { |
| 77 | let blocks = extract_repl_blocks("no blocks here"); |
| 78 | assert!(blocks.is_empty()); |
| 79 | } |
| 80 | } |
| 81 |