Skip to main content

manabrew_agent_interface/
protocol.rs

1use crate::game_log_event::GameLogEntryDto;
2use crate::game_snapshot_event::GameSnapshotEventDto;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6// Wire types (ClientMessage, ServerMessage, RoomInfo, Deck, …) live in
7// manabrew-protocol; StateEnvelope stays here because it carries engine DTOs.
8pub use manabrew_protocol::protocol::*;
9
10/// Typed envelope carried inside `ClientMessage::BroadcastState.state` /
11/// `ServerMessage::StateUpdate.state`. One discriminator (`kind`) plus the
12/// payload for that variant. Constructed and parsed in every layer that
13/// touches the relay (engine, bot, host, web/Tauri UI) — anything that needs
14/// to handcraft `json!({"kind": "..."})` belongs here instead.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "kind", rename_all = "camelCase")]
17pub enum StateEnvelope {
18    State {
19        #[serde(rename = "forPlayer", default, skip_serializing_if = "Option::is_none")]
20        for_player: Option<String>,
21        state: Value,
22    },
23    Display {
24        event: Value,
25    },
26    /// Engine asks a player for a decision. `prompt` is `AgentPrompt` for the
27    /// Rust engine; the Java bridge emits a different shape, so the payload is
28    /// kept as raw `Value` here and parsed by the receiver.
29    Prompt {
30        #[serde(rename = "forPlayer")]
31        for_player: String,
32        prompt: Value,
33    },
34    /// Player answers a prompt. `action` is `PlayerAction` for Rust; raw value
35    Response {
36        #[serde(rename = "fromPlayer")]
37        from_player: String,
38        #[serde(rename = "promptId", default)]
39        prompt_id: u32,
40        action: Value,
41    },
42    Directive {
43        #[serde(rename = "fromPlayer")]
44        from_player: String,
45        directive: Value,
46    },
47    /// Engine log entry broadcast to observers.
48    Log {
49        #[serde(rename = "fromPlayer")]
50        from_player: String,
51        entry: GameLogEntryDto,
52    },
53    /// Engine snapshot broadcast to observers.
54    Snapshot {
55        #[serde(rename = "fromPlayer")]
56        from_player: String,
57        entry: GameSnapshotEventDto,
58    },
59    Fatal {
60        message: String,
61    },
62    /// Engine rejects a response (stale prompt, wrong player, unknown action).
63    /// `error` is `ProtocolError`; recoverable unless the session is terminated.
64    Error {
65        #[serde(rename = "forPlayer")]
66        for_player: String,
67        error: Value,
68    },
69    /// Out-of-band message tunneled through the relay (manual tabletop launch,
70    /// self-hosted-node control plane, heartbeats, …). The relay never
71    /// interprets the `payload`.
72    RoomRelay {
73        protocol: String,
74        version: u32,
75        #[serde(rename = "messageId")]
76        message_id: String,
77        #[serde(
78            rename = "fromPlayer",
79            default,
80            skip_serializing_if = "Option::is_none"
81        )]
82        from_player: Option<String>,
83        #[serde(
84            rename = "targetPlayer",
85            default,
86            skip_serializing_if = "Option::is_none"
87        )]
88        target_player: Option<String>,
89        #[serde(rename = "roomId", default, skip_serializing_if = "Option::is_none")]
90        room_id: Option<String>,
91        payload: Value,
92    },
93}
94
95impl StateEnvelope {
96    pub fn for_agent_message(for_player: String, message: &crate::prompt::AgentMessage) -> Self {
97        use crate::prompt::AgentMessage;
98        match message {
99            AgentMessage::State(state) => StateEnvelope::State {
100                for_player: Some(for_player),
101                state: serde_json::to_value(state).unwrap_or(Value::Null),
102            },
103            AgentMessage::Display(event) => StateEnvelope::Display {
104                event: serde_json::to_value(event).unwrap_or(Value::Null),
105            },
106            AgentMessage::Prompt(prompt) => StateEnvelope::Prompt {
107                for_player,
108                prompt: serde_json::to_value(prompt).unwrap_or(Value::Null),
109            },
110            AgentMessage::Error(error) => StateEnvelope::Error {
111                for_player,
112                error: serde_json::to_value(error).unwrap_or(Value::Null),
113            },
114        }
115    }
116}