Skip to main content

arcane_core/agent/
mod.rs

1pub mod inspector;
2pub mod mcp;
3
4use std::sync::mpsc;
5
6/// Requests the inspector HTTP server can send to the game loop.
7#[derive(Debug)]
8pub enum InspectorRequest {
9    Health,
10    GetState { path: Option<String> },
11    Describe { verbosity: Option<String> },
12    ListActions,
13    ExecuteAction { name: String, payload: String },
14    Rewind { steps: u32 },
15    Simulate { action: String },
16    GetHistory,
17    GetFrameStats,
18    CaptureFrame,
19}
20
21/// Response from the game loop back to the inspector HTTP server.
22#[derive(Debug)]
23pub struct InspectorResponse {
24    pub status: u16,
25    pub content_type: String,
26    pub body: String,
27}
28
29impl InspectorResponse {
30    pub fn json(body: String) -> Self {
31        Self {
32            status: 200,
33            content_type: "application/json".into(),
34            body,
35        }
36    }
37
38    pub fn text(body: String) -> Self {
39        Self {
40            status: 200,
41            content_type: "text/plain".into(),
42            body,
43        }
44    }
45
46    pub fn error(status: u16, message: String) -> Self {
47        Self {
48            status,
49            content_type: "application/json".into(),
50            body: format!("{{\"error\":\"{message}\"}}"),
51        }
52    }
53}
54
55/// Sender half: used by the game loop to respond to inspector requests.
56pub type ResponseSender = mpsc::Sender<InspectorResponse>;
57
58/// A request bundled with a channel to send the response back.
59pub type InspectorMessage = (InspectorRequest, ResponseSender);
60
61/// Sender half: used by the inspector HTTP server to send requests to the game loop.
62pub type RequestSender = mpsc::Sender<InspectorMessage>;
63
64/// Receiver half: used by the game loop to receive requests from the inspector.
65pub type RequestReceiver = mpsc::Receiver<InspectorMessage>;
66
67/// Create a new inspector channel pair.
68pub fn inspector_channel() -> (RequestSender, RequestReceiver) {
69    mpsc::channel()
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn inspector_channel_round_trip() {
78        let (tx, rx) = inspector_channel();
79
80        // Simulate inspector sending a request
81        let (resp_tx, resp_rx) = mpsc::channel();
82        tx.send((InspectorRequest::Health, resp_tx)).unwrap();
83
84        // Simulate game loop receiving and responding
85        let (req, sender) = rx.recv().unwrap();
86        assert!(matches!(req, InspectorRequest::Health));
87        sender
88            .send(InspectorResponse::json("{\"status\":\"ok\"}".into()))
89            .unwrap();
90
91        // Verify inspector gets the response
92        let response = resp_rx.recv().unwrap();
93        assert_eq!(response.status, 200);
94        assert_eq!(response.content_type, "application/json");
95        assert!(response.body.contains("ok"));
96    }
97
98    #[test]
99    fn inspector_response_constructors() {
100        let json = InspectorResponse::json("{\"key\":1}".into());
101        assert_eq!(json.status, 200);
102        assert_eq!(json.content_type, "application/json");
103
104        let text = InspectorResponse::text("hello".into());
105        assert_eq!(text.status, 200);
106        assert_eq!(text.content_type, "text/plain");
107
108        let err = InspectorResponse::error(404, "not found".into());
109        assert_eq!(err.status, 404);
110        assert!(err.body.contains("not found"));
111    }
112
113    #[test]
114    fn inspector_request_variants() {
115        // Ensure all variants construct correctly
116        let requests = vec![
117            InspectorRequest::Health,
118            InspectorRequest::GetState { path: None },
119            InspectorRequest::GetState {
120                path: Some("player.hp".into()),
121            },
122            InspectorRequest::Describe {
123                verbosity: Some("full".into()),
124            },
125            InspectorRequest::ListActions,
126            InspectorRequest::ExecuteAction {
127                name: "move".into(),
128                payload: "{}".into(),
129            },
130            InspectorRequest::Rewind { steps: 3 },
131            InspectorRequest::Simulate {
132                action: "attack".into(),
133            },
134            InspectorRequest::GetHistory,
135            InspectorRequest::GetFrameStats,
136            InspectorRequest::CaptureFrame,
137        ];
138        assert_eq!(requests.len(), 11);
139    }
140}