Skip to main content

stasis/application/runtime/
prompt_chat_job_handler.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde_json::json;
5
6use crate::application::orchestration::runtime_job_payloads::{
7    PromptJobPayload,
8};
9use crate::application::runtime::chat_options_resolver::validate_reasoning_effort;
10use crate::application::runtime::identity_context_compiler::{
11    load_identity_context_summary, prepend_identity_snapshot,
12};
13use crate::application::runtime::memory_recall_context_compiler::prepend_memory_recall_context;
14use crate::application::runtime::memory_persistence_helpers::{
15    SttpPromptNodeFormat, memory_query_fingerprint, memory_query_id, memory_scope_hash,
16    render_prompt_response_sttp_node, resolve_sttp_output_node_id, should_store,
17};
18use crate::application::runtime::memory_recall_request_builder::build_memory_recall_request;
19use crate::application::orchestration::prompt_pipeline::{
20    PromptExecutionPipeline, PromptExecutionRequest,
21};
22use crate::application::runtime::in_memory_runtime::{JobExecutionOutcome, JobHandler};
23use crate::application::runtime::runtime_diagnostics_helpers::{
24    build_runtime_failure_identity_context_section, build_runtime_failure_memory_recall_section,
25    build_runtime_memory_diagnostics_bundle, RuntimeIdentityDiagnosticsInput,
26    RuntimeMemoryRecallDiagnosticsInput, RuntimeMemoryStoreDiagnosticsInput,
27};
28use crate::application::runtime::runtime_handler_execution_context::RuntimeHandlerExecutionContext;
29use crate::domain::errors::Result;
30use crate::domain::runtime::job::Job;
31use crate::ports::outbound::ai_chat_client::AiChatClient;
32use crate::ports::outbound::memory::identity_memory_store::IdentityMemoryStore;
33use crate::ports::outbound::memory::memory_context_reader::MemoryContextReader;
34use crate::ports::outbound::memory::memory_context_writer::MemoryContextWriter;
35use crate::ports::outbound::memory::memory_models::MemoryStoreRequest;
36
37pub struct PromptChatJobHandler {
38    pipeline: PromptExecutionPipeline,
39    memory_reader: Option<Arc<dyn MemoryContextReader>>,
40    memory_writer: Option<Arc<dyn MemoryContextWriter>>,
41    identity_memory_store: Option<Arc<dyn IdentityMemoryStore>>,
42}
43
44impl PromptChatJobHandler {
45    pub fn new(chat_client: Arc<dyn AiChatClient>) -> Self {
46        Self::new_with_memory_and_identity(chat_client, None, None, None)
47    }
48
49    pub fn new_with_memory(
50        chat_client: Arc<dyn AiChatClient>,
51        memory_reader: Option<Arc<dyn MemoryContextReader>>,
52        memory_writer: Option<Arc<dyn MemoryContextWriter>>,
53    ) -> Self {
54        Self::new_with_memory_and_identity(chat_client, memory_reader, memory_writer, None)
55    }
56
57    pub fn new_with_memory_and_identity(
58        chat_client: Arc<dyn AiChatClient>,
59        memory_reader: Option<Arc<dyn MemoryContextReader>>,
60        memory_writer: Option<Arc<dyn MemoryContextWriter>>,
61        identity_memory_store: Option<Arc<dyn IdentityMemoryStore>>,
62    ) -> Self {
63        Self {
64            pipeline: PromptExecutionPipeline::new(chat_client),
65            memory_reader,
66            memory_writer,
67            identity_memory_store,
68        }
69    }
70
71    fn parse_payload(raw: &str) -> std::result::Result<PromptJobPayload, String> {
72        let payload: PromptJobPayload = serde_json::from_str(raw)
73            .map_err(|err| format!("policy violation: invalid prompt job payload json: {err}"))?;
74
75        if payload.user_prompt.trim().is_empty() {
76            return Err(
77                "policy violation: prompt payload.user_prompt must be non-empty".to_string(),
78            );
79        }
80
81        validate_reasoning_effort(payload.reasoning_effort.as_deref())
82            .map_err(|err| format!("policy violation: {err}"))?;
83
84        Ok(payload)
85    }
86
87    fn build_failure(message: String) -> JobExecutionOutcome {
88        let diagnostics = json!({
89            "provider": "stasis-pipeline",
90            "status": "failure",
91            "guardrail_code": "POLICY_VIOLATION",
92            "policy_reason": &message,
93        })
94        .to_string();
95
96        JobExecutionOutcome::FatalFailure {
97            message,
98            execution_id: None,
99            diagnostics: Some(diagnostics),
100        }
101    }
102
103}
104
105#[async_trait]
106impl JobHandler for PromptChatJobHandler {
107    fn job_type(&self) -> &'static str {
108        "workflow.stasis.prompt"
109    }
110
111    async fn execute(&self, job: &Job) -> Result<JobExecutionOutcome> {
112        let payload = match Self::parse_payload(&job.payload_ref) {
113            Ok(payload) => payload,
114            Err(message) => return Ok(Self::build_failure(message)),
115        };
116
117        let execution_context = RuntimeHandlerExecutionContext::new(
118            job,
119            payload.policy_profile.clone(),
120            payload.model_hint.clone(),
121            payload.reasoning_effort.clone(),
122            self.memory_reader.is_some(),
123            self.memory_writer.is_some(),
124            self.identity_memory_store.is_some(),
125        );
126
127        let mut memory_recall = None;
128        let mut memory_recall_error = None;
129        let mut input_memory_query_id = None;
130        let mut input_memory_query_fingerprint = None;
131
132        let memory_policy = payload.memory_policy.as_ref();
133        let (identity_summary, identity_error) = load_identity_context_summary(
134            self.identity_memory_store.as_ref(),
135            execution_context.correlation_id(),
136            execution_context.policy_profile(),
137        )
138        .await;
139
140        let mut effective_user_prompt =
141            prepend_identity_snapshot(&payload.user_prompt, identity_summary.as_deref());
142
143        if let Some(reader) = &self.memory_reader {
144            let recall_request = build_memory_recall_request(
145                execution_context.correlation_id(),
146                Some(&effective_user_prompt),
147                memory_policy,
148            );
149            input_memory_query_id = Some(memory_query_id(
150                execution_context.correlation_id(),
151                &recall_request,
152            ));
153            input_memory_query_fingerprint = Some(memory_query_fingerprint(&recall_request));
154
155            match reader.recall(&recall_request).await {
156                Ok(response) => {
157                    effective_user_prompt = prepend_memory_recall_context(&effective_user_prompt, &response);
158                    memory_recall = Some(response);
159                }
160                Err(err) => memory_recall_error = Some(err.to_string()),
161            }
162        }
163
164        let context = execution_context.prompt_context_clone();
165
166        let user_prompt = effective_user_prompt;
167        let mut request = PromptExecutionRequest::from_user_prompt(user_prompt.clone())
168            .with_context(context.clone());
169        if let Some(system_prompt) = payload.system_prompt {
170            request = request.with_system_prompt(system_prompt);
171        }
172
173        let response = match self.pipeline.execute(request).await {
174            Ok(response) => response,
175            Err(err) => {
176                let error = err.to_string();
177                return Ok(JobExecutionOutcome::FatalFailure {
178                    message: error.clone(),
179                    execution_id: None,
180                    diagnostics: Some(
181                        json!({
182                            "provider": "stasis-pipeline",
183                            "status": "failure",
184                            "error": error,
185                            "policy_profile": execution_context.policy_profile_clone(),
186                            "model_hint": execution_context.model_hint_clone(),
187                            "memory_recall": build_runtime_failure_memory_recall_section(
188                                execution_context.memory_reader_enabled(),
189                                memory_recall_error,
190                            ),
191                            "identity_context": build_runtime_failure_identity_context_section(
192                                execution_context.identity_enabled(),
193                                identity_summary,
194                                identity_error,
195                            ),
196                        })
197                        .to_string(),
198                    ),
199                });
200            }
201        };
202
203        let mut memory_store = None;
204        let mut memory_store_error = None;
205        if should_store(memory_policy)
206            && let Some(writer) = &self.memory_writer
207        {
208            let store_request = MemoryStoreRequest {
209                session_id: execution_context.correlation_id().to_string(),
210                raw_node: render_prompt_response_sttp_node(
211                    execution_context.correlation_id(),
212                    &user_prompt,
213                    &response.text,
214                    SttpPromptNodeFormat::UntaggedNoSchema,
215                ),
216            };
217
218            match writer.store_context(&store_request).await {
219                Ok(stored) => memory_store = Some(stored),
220                Err(err) => memory_store_error = Some(err.to_string()),
221            }
222        }
223
224        let sttp_output_node_id =
225            resolve_sttp_output_node_id(memory_store.as_ref(), format!("sttp:prompt:{}", job.id));
226        let memory_scope_hash = memory_scope_hash(execution_context.correlation_id(), memory_policy);
227        let input_memory_query_id_for_top_level = input_memory_query_id.clone();
228        let input_memory_query_fingerprint_for_top_level =
229            input_memory_query_fingerprint.clone();
230        let diagnostics_bundle = build_runtime_memory_diagnostics_bundle(
231            RuntimeMemoryRecallDiagnosticsInput {
232                attempted: execution_context.memory_reader_enabled(),
233                response: memory_recall,
234                query_id: input_memory_query_id,
235                query_fingerprint: input_memory_query_fingerprint,
236                error: memory_recall_error,
237            },
238            RuntimeMemoryStoreDiagnosticsInput {
239                attempted: execution_context.memory_writer_enabled(),
240                response: memory_store,
241                error: memory_store_error,
242            },
243            RuntimeIdentityDiagnosticsInput {
244                attempted: execution_context.identity_enabled(),
245                summary: identity_summary,
246                error: identity_error,
247            },
248        );
249
250        let diagnostics = json!({
251            "provider": "stasis-pipeline",
252            "status": "success",
253            "trace_id": context.trace_id,
254            "correlation_id": context.correlation_id,
255            "policy_profile": response.metadata.policy_profile,
256            "model_hint": response.metadata.model_hint,
257            "output_text": response.text,
258            "output_preview": response.text.chars().take(160).collect::<String>(),
259            "memory_retrieved_count": diagnostics_bundle.retrieved_count,
260            "memory_retrieval_path": diagnostics_bundle.retrieval_path,
261            "memory_fallback_triggered": diagnostics_bundle.fallback_triggered,
262            "memory_fallback_reason": diagnostics_bundle.fallback_reason,
263            "memory_scope_hash": memory_scope_hash,
264            "memory_store_valid": diagnostics_bundle.store_valid,
265            "memory_store_node_id": diagnostics_bundle.store_node_id,
266            "input_memory_query_id": input_memory_query_id_for_top_level,
267            "input_memory_query_fingerprint": input_memory_query_fingerprint_for_top_level,
268            "output_memory_node_id": diagnostics_bundle.store_node_id,
269            "memory_recall": diagnostics_bundle.memory_recall,
270            "memory_store": diagnostics_bundle.memory_store,
271            "identity_context": diagnostics_bundle.identity_context,
272        })
273        .to_string();
274
275        Ok(JobExecutionOutcome::Success {
276            sttp_output_node_id,
277            execution_id: None,
278            diagnostics: Some(diagnostics),
279        })
280    }
281}