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