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        state: Value,
20    },
21    Display {
22        event: Value,
23    },
24    /// Engine asks a player for a decision. `prompt` is `AgentPrompt` for the
25    /// Rust engine; the Java bridge emits a different shape, so the payload is
26    /// kept as raw `Value` here and parsed by the receiver.
27    Prompt {
28        #[serde(rename = "forPlayer")]
29        for_player: String,
30        prompt: Value,
31    },
32    /// Player answers a prompt. `action` is `PlayerAction` for Rust; raw value
33    /// for the Java bridge.
34    Response {
35        #[serde(rename = "fromPlayer")]
36        from_player: String,
37        action: Value,
38    },
39    Directive {
40        #[serde(rename = "fromPlayer")]
41        from_player: String,
42        directive: Value,
43    },
44    /// Engine log entry broadcast to observers.
45    Log {
46        #[serde(rename = "fromPlayer")]
47        from_player: String,
48        entry: GameLogEntryDto,
49    },
50    /// Engine snapshot broadcast to observers.
51    Snapshot {
52        #[serde(rename = "fromPlayer")]
53        from_player: String,
54        entry: GameSnapshotEventDto,
55    },
56    Fatal {
57        message: String,
58    },
59    /// Out-of-band message tunneled through the relay (manual tabletop launch,
60    /// self-hosted-node control plane, heartbeats, …). The relay never
61    /// interprets the `payload`.
62    RoomRelay {
63        protocol: String,
64        version: u32,
65        #[serde(rename = "messageId")]
66        message_id: String,
67        #[serde(
68            rename = "fromPlayer",
69            default,
70            skip_serializing_if = "Option::is_none"
71        )]
72        from_player: Option<String>,
73        #[serde(
74            rename = "targetPlayer",
75            default,
76            skip_serializing_if = "Option::is_none"
77        )]
78        target_player: Option<String>,
79        #[serde(rename = "roomId", default, skip_serializing_if = "Option::is_none")]
80        room_id: Option<String>,
81        payload: Value,
82    },
83}
84
85impl StateEnvelope {
86    pub fn for_agent_message(for_player: String, message: &crate::prompt::AgentMessage) -> Self {
87        use crate::prompt::AgentMessage;
88        match message {
89            AgentMessage::State(state) => StateEnvelope::State {
90                state: serde_json::to_value(state).unwrap_or(Value::Null),
91            },
92            AgentMessage::Display(event) => StateEnvelope::Display {
93                event: serde_json::to_value(event).unwrap_or(Value::Null),
94            },
95            AgentMessage::Prompt(prompt) => StateEnvelope::Prompt {
96                for_player,
97                prompt: serde_json::to_value(prompt).unwrap_or(Value::Null),
98            },
99        }
100    }
101}