1pub mod jetstream;
14
15pub use jetstream::run_bridge_jetstream;
18
19use std::sync::Arc;
20
21use anyhow::Result;
22use async_nats::HeaderMap;
23use async_trait::async_trait;
24use greentic_types::{DispatchError, RuntimeDispatchRequest, RuntimeDispatchResponse};
25use serde_json::Value;
26
27pub use greentic_types::{request_topic, response_topic};
30
31pub const RUNTIME_NAME: &str = "agentic";
34
35pub struct InvokeOutcome {
37 pub ok: bool,
39 pub output: Value,
41 pub events: Vec<Value>,
43}
44
45#[async_trait]
54pub trait AgentDispatchInvoker: Send + Sync {
55 async fn invoke(
64 &self,
65 tenant: &str,
66 env: &str,
67 target: &str,
68 operation: &str,
69 input: Value,
70 idempotency_key: Option<&str>,
71 ) -> Result<InvokeOutcome>;
72}
73
74pub async fn build_response(
76 invoker: Arc<dyn AgentDispatchInvoker>,
77 tenant: &str,
78 env: &str,
79 idempotency_key: Option<&str>,
80 req: RuntimeDispatchRequest,
81) -> RuntimeDispatchResponse {
82 match invoker
83 .invoke(
84 tenant,
85 env,
86 &req.target,
87 &req.operation,
88 req.input,
89 idempotency_key,
90 )
91 .await
92 {
93 Ok(outcome) => RuntimeDispatchResponse {
94 ok: outcome.ok,
95 output: outcome.output,
96 events: outcome.events,
97 error: None,
98 },
99 Err(error) => RuntimeDispatchResponse {
100 ok: false,
101 output: Value::Null,
102 events: vec![],
103 error: Some(DispatchError {
104 code: "invoke_failed".into(),
105 message: error.to_string(),
106 }),
107 },
108 }
109}
110
111pub async fn handle_message(
117 client: &async_nats::Client,
118 invoker: Arc<dyn AgentDispatchInvoker>,
119 msg: async_nats::Message,
120) -> Result<()> {
121 let headers = msg.headers.as_ref();
122 let get_header = |name: &str| -> Option<String> {
123 headers
124 .and_then(|header_map| header_map.get(name))
125 .map(|value| value.as_str().to_string())
126 };
127
128 let correlation = get_header("Greentic-Correlation-Id");
129 let idempotency = get_header("Greentic-Idempotency-Key").or_else(|| correlation.clone());
133 let tenant = get_header("Greentic-Tenant").unwrap_or_default();
134 let env = get_header("Greentic-Env").unwrap_or_else(|| "default".to_string());
135
136 let req: RuntimeDispatchRequest = serde_json::from_slice(&msg.payload)?;
137 let resp = build_response(invoker, &tenant, &env, idempotency.as_deref(), req).await;
138
139 let mut out_headers = HeaderMap::new();
140 if let Some(correlation_value) = correlation.as_deref() {
141 out_headers.insert("Greentic-Correlation-Id", correlation_value);
142 }
143 out_headers.insert("Greentic-Tenant", tenant.as_str());
144 out_headers.insert("Greentic-Env", env.as_str());
145
146 let response_bytes = serde_json::to_vec(&resp)?;
147 client
148 .publish_with_headers(
149 response_topic(RUNTIME_NAME),
150 out_headers,
151 response_bytes.into(),
152 )
153 .await?;
154 Ok(())
155}
156
157pub async fn run_bridge(
160 client: async_nats::Client,
161 invoker: Arc<dyn AgentDispatchInvoker>,
162) -> Result<()> {
163 use futures_util::StreamExt;
164 let mut subscriber = client.subscribe(request_topic(RUNTIME_NAME)).await?;
165 while let Some(msg) = subscriber.next().await {
166 let client = client.clone();
167 let invoker = invoker.clone();
168 tokio::spawn(async move {
169 if let Err(error) = handle_message(&client, invoker, msg).await {
170 tracing::error!(%error, "aw event bridge failed to handle request");
171 }
172 });
173 }
174 Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use greentic_types::DispatchMode;
181 use serde_json::json;
182 use std::sync::Mutex;
183
184 type SeenCall = (String, String, Value, Option<String>);
186
187 struct StubInvoker {
188 seen: Mutex<Vec<SeenCall>>,
189 }
190
191 #[async_trait]
192 impl AgentDispatchInvoker for StubInvoker {
193 async fn invoke(
194 &self,
195 _tenant: &str,
196 _env: &str,
197 target: &str,
198 operation: &str,
199 input: Value,
200 idempotency_key: Option<&str>,
201 ) -> Result<InvokeOutcome> {
202 self.seen.lock().unwrap().push((
203 target.to_string(),
204 operation.to_string(),
205 input.clone(),
206 idempotency_key.map(str::to_string),
207 ));
208 Ok(InvokeOutcome {
209 ok: true,
210 output: json!({"reply": "pong", "trail": [], "terminated_by": "reply"}),
211 events: vec![],
212 })
213 }
214 }
215
216 fn sample_request() -> RuntimeDispatchRequest {
217 RuntimeDispatchRequest {
218 target: "greeter".into(),
219 operation: String::new(),
220 mode: DispatchMode::Await,
221 input: json!({"user_text": "ping"}),
222 deadline_ms: Some(30_000),
223 }
224 }
225
226 #[test]
227 fn subjects_use_agentic_runtime_name() {
228 assert_eq!(request_topic(RUNTIME_NAME), "greentic.agentic.request.v1");
229 assert_eq!(response_topic(RUNTIME_NAME), "greentic.agentic.response.v1");
230 }
231
232 #[tokio::test]
233 async fn handle_invokes_and_maps_agent_output_to_response() {
234 let invoker = Arc::new(StubInvoker {
235 seen: Mutex::new(vec![]),
236 });
237 let resp = build_response(
238 invoker.clone(),
239 "acme",
240 "prod",
241 Some("sess-1::pack=p::flow=f"),
242 sample_request(),
243 )
244 .await;
245
246 assert!(resp.ok);
247 assert_eq!(resp.output["reply"], json!("pong"));
248 assert_eq!(resp.output["terminated_by"], json!("reply"));
249 assert!(resp.error.is_none());
250
251 let seen = invoker.seen.lock().unwrap();
252 assert_eq!(seen.len(), 1);
253 let (target, _operation, input, idempotency) = &seen[0];
254 assert_eq!(target, "greeter", "dispatch target maps to agent id");
255 assert_eq!(input["user_text"], json!("ping"));
256 assert_eq!(
257 idempotency.as_deref(),
258 Some("sess-1::pack=p::flow=f"),
259 "correlation/idempotency hint is forwarded to the invoker"
260 );
261 }
262
263 #[tokio::test]
264 async fn invoke_error_maps_to_error_response() {
265 struct FailInvoker;
266
267 #[async_trait]
268 impl AgentDispatchInvoker for FailInvoker {
269 async fn invoke(
270 &self,
271 _tenant: &str,
272 _env: &str,
273 _target: &str,
274 _operation: &str,
275 _input: Value,
276 _idempotency_key: Option<&str>,
277 ) -> Result<InvokeOutcome> {
278 Err(anyhow::anyhow!("boom"))
279 }
280 }
281
282 let resp = build_response(
283 Arc::new(FailInvoker),
284 "acme",
285 "prod",
286 Some("c"),
287 sample_request(),
288 )
289 .await;
290
291 assert!(!resp.ok);
292 assert_eq!(resp.output, Value::Null);
293 let error = resp.error.expect("error response must carry details");
294 assert_eq!(error.code, "invoke_failed");
295 assert_eq!(error.message, "boom");
296 }
297}