stasis/application/runtime/
grapheme_echo_job_handler.rs1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::json;
6
7use crate::application::runtime::grapheme_job_handler::GraphemeJobHandler;
8use crate::application::runtime::in_memory_runtime::{JobExecutionOutcome, JobHandler};
9use crate::domain::errors::Result;
10use crate::domain::runtime::job::Job;
11use crate::ports::outbound::runtime::workflow_engine::WorkflowEngine;
12
13const MAX_MESSAGE_LEN: usize = 512;
14
15#[derive(Deserialize)]
16struct EchoPayload {
17 message: String,
18}
19
20pub struct GraphemeEchoJobHandler {
21 delegate: GraphemeJobHandler,
22}
23
24impl GraphemeEchoJobHandler {
25 pub fn new(engine: Arc<dyn WorkflowEngine>) -> Self {
26 Self {
27 delegate: GraphemeJobHandler::new(engine),
28 }
29 }
30
31 fn build_failure(message: String) -> JobExecutionOutcome {
32 let diagnostics = json!({
33 "provider": "grapheme-sdk",
34 "status": "failure",
35 "guardrail_code": "POLICY_VIOLATION",
36 "policy_reason": &message,
37 })
38 .to_string();
39
40 JobExecutionOutcome::FatalFailure {
41 message,
42 execution_id: None,
43 diagnostics: Some(diagnostics),
44 }
45 }
46
47 fn parse_payload(raw: &str) -> std::result::Result<EchoPayload, String> {
48 let payload: EchoPayload = serde_json::from_str(raw)
49 .map_err(|err| format!("policy violation: invalid echo payload json: {err}"))?;
50
51 if payload.message.trim().is_empty() {
52 return Err("policy violation: echo payload.message must be non-empty".to_string());
53 }
54
55 if payload.message.len() > MAX_MESSAGE_LEN {
56 return Err(format!(
57 "policy violation: echo payload.message exceeds max length {}",
58 MAX_MESSAGE_LEN
59 ));
60 }
61
62 Ok(payload)
63 }
64
65 fn build_inline_source(message: &str) -> String {
66 let cleaned = message
67 .replace('"', "'")
68 .replace(['\n', '\r'], " ");
69
70 format!(
71 "import core from \"grapheme/core\"\n\nquery Echo {{\n core.echo(message: \"{}\") {{\n state {{ current }}\n }}\n}}\n",
72 cleaned
73 )
74 }
75}
76
77#[async_trait]
78impl JobHandler for GraphemeEchoJobHandler {
79 fn job_type(&self) -> &'static str {
80 "workflow.grapheme.echo"
81 }
82
83 async fn execute(&self, job: &Job) -> Result<JobExecutionOutcome> {
84 let payload = match Self::parse_payload(&job.payload_ref) {
85 Ok(payload) => payload,
86 Err(message) => return Ok(Self::build_failure(message)),
87 };
88
89 let source = Self::build_inline_source(&payload.message);
90 let synthetic_job = Job {
91 payload_ref: format!("grapheme:inline:{}", source),
92 ..job.clone()
93 };
94
95 self.delegate.execute(&synthetic_job).await
96 }
97}