Skip to main content

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