Skip to main content

stasis/application/runtime/
grapheme_echo_job_handler.rs

1use 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}