Skip to main content

greentic_types/
runtime_dispatch.rs

1//! Wire contract for dispatching work from a flow to a separate runtime
2//! (sorla / operala / agentic) over a pub/sub transport. Shared by the runner
3//! and every runtime-side bridge so both ends agree on the message payload.
4
5use alloc::{format, string::String, vec::Vec};
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11/// Whether the flow blocks for a response (`Await`) or continues immediately
12/// after publishing the request (`FireAndForget`).
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
16pub enum DispatchMode {
17    /// Block the flow until the runtime responds (durable pause/resume).
18    Await,
19    /// Publish the request and continue the flow immediately.
20    FireAndForget,
21}
22
23/// Payload of a `greentic.<runtime>.request.v1` message.
24#[derive(Clone, Debug, PartialEq)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub struct RuntimeDispatchRequest {
27    /// Logical target inside the runtime (e.g. a sorx deployment id).
28    pub target: String,
29    /// Operation/endpoint within the runtime.
30    pub operation: String,
31    /// Await vs fire-and-forget dispatch semantics.
32    pub mode: DispatchMode,
33    /// Opaque runtime input.
34    pub input: Value,
35    /// Await-mode timeout budget in milliseconds.
36    pub deadline_ms: Option<u64>,
37}
38
39/// Payload of a `greentic.<runtime>.response.v1` message.
40/// The transport correlation id MUST echo the request's.
41#[derive(Clone, Debug, PartialEq)]
42#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
43pub struct RuntimeDispatchResponse {
44    /// Whether the runtime handled the request successfully.
45    pub ok: bool,
46    /// Runtime output payload (meaningful when `ok`).
47    pub output: Value,
48    /// Optional runtime-emitted events.
49    #[cfg_attr(feature = "serde", serde(default))]
50    pub events: Vec<Value>,
51    /// Error details when `ok` is false.
52    #[cfg_attr(
53        feature = "serde",
54        serde(default, skip_serializing_if = "Option::is_none")
55    )]
56    pub error: Option<DispatchError>,
57}
58
59/// Structured error returned by a runtime when a dispatch fails.
60#[derive(Clone, Debug, PartialEq)]
61#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
62pub struct DispatchError {
63    /// Stable machine-readable error code.
64    pub code: String,
65    /// Human-readable error message.
66    pub message: String,
67}
68
69/// Request topic/subject for a runtime, e.g. `greentic.sorla.request.v1`.
70pub fn request_topic(runtime: &str) -> String {
71    format!("greentic.{runtime}.request.v1")
72}
73
74/// Response topic/subject for a runtime, e.g. `greentic.sorla.response.v1`.
75pub fn response_topic(runtime: &str) -> String {
76    format!("greentic.{runtime}.response.v1")
77}
78
79#[cfg(all(test, feature = "serde"))]
80#[allow(clippy::unwrap_used, clippy::expect_used)]
81mod tests {
82    use super::*;
83    use serde_json::json;
84
85    #[test]
86    fn request_round_trips_through_json() {
87        let req = RuntimeDispatchRequest {
88            target: "dep-123".into(),
89            operation: "create_invoice".into(),
90            mode: DispatchMode::Await,
91            input: json!({ "amount": 10 }),
92            deadline_ms: Some(30_000),
93        };
94        let encoded = serde_json::to_value(&req).unwrap();
95        let decoded: RuntimeDispatchRequest = serde_json::from_value(encoded).unwrap();
96        assert_eq!(decoded, req);
97    }
98
99    #[test]
100    fn topic_helpers_use_runtime_name() {
101        assert_eq!(request_topic("sorla"), "greentic.sorla.request.v1");
102        assert_eq!(response_topic("sorla"), "greentic.sorla.response.v1");
103    }
104}