Skip to main content

stasis/application/orchestration/
handoff_pattern_pipeline.rs

1use crate::application::orchestration::prompt_pipeline::{
2    PromptExecutionContext, PromptExecutionPipeline, PromptExecutionRequest,
3};
4use crate::application::runtime::chat_options_resolver::resolve_reasoning_effort;
5use crate::domain::errors::Result;
6
7#[derive(Clone, Debug)]
8pub struct HandoffPatternTurn {
9    pub actor_id: String,
10    pub user_prompt_template: String,
11    pub system_prompt: Option<String>,
12    pub policy_profile: Option<String>,
13    pub model_hint: Option<String>,
14    pub reasoning_effort: Option<String>,
15}
16
17#[derive(Clone, Debug)]
18pub struct HandoffPatternExecutionRequest {
19    pub initial_user_prompt: String,
20    pub trace_id: Option<String>,
21    pub correlation_id: Option<String>,
22    pub policy_profile: Option<String>,
23    pub model_hint: Option<String>,
24    pub reasoning_effort: Option<String>,
25    pub turns: Vec<HandoffPatternTurn>,
26}
27
28#[derive(Clone, Debug)]
29pub struct HandoffPatternTurnResult {
30    pub actor_id: String,
31    pub rendered_prompt: String,
32    pub output_text: String,
33}
34
35#[derive(Clone, Debug)]
36pub struct HandoffTransition {
37    pub from_actor_id: String,
38    pub to_actor_id: String,
39}
40
41#[derive(Clone, Debug)]
42pub struct HandoffPatternExecutionResponse {
43    pub final_text: String,
44    pub turns: Vec<HandoffPatternTurnResult>,
45    pub handoffs: Vec<HandoffTransition>,
46    pub termination_reason: String,
47}
48
49#[derive(Clone)]
50pub struct HandoffPatternPipeline {
51    prompt_pipeline: PromptExecutionPipeline,
52}
53
54impl HandoffPatternPipeline {
55    pub fn new(prompt_pipeline: PromptExecutionPipeline) -> Self {
56        Self { prompt_pipeline }
57    }
58
59    pub async fn execute(
60        &self,
61        request: HandoffPatternExecutionRequest,
62    ) -> Result<HandoffPatternExecutionResponse> {
63        let mut current_input = request.initial_user_prompt;
64        let mut turn_results = Vec::with_capacity(request.turns.len());
65        let mut handoffs = Vec::new();
66        let mut previous_actor: Option<String> = None;
67
68        for turn in request.turns {
69            if let Some(from_actor_id) = previous_actor.clone() {
70                handoffs.push(HandoffTransition {
71                    from_actor_id,
72                    to_actor_id: turn.actor_id.clone(),
73                });
74            }
75
76            let rendered_prompt = turn
77                .user_prompt_template
78                .replace("{{input}}", &current_input)
79                .replace("{input}", &current_input);
80
81            let context = PromptExecutionContext {
82                trace_id: request.trace_id.clone(),
83                correlation_id: request.correlation_id.clone(),
84                policy_profile: turn
85                    .policy_profile
86                    .clone()
87                    .or_else(|| request.policy_profile.clone()),
88                model_hint: turn
89                    .model_hint
90                    .clone()
91                    .or_else(|| request.model_hint.clone()),
92                reasoning_effort: resolve_reasoning_effort(
93                    turn.reasoning_effort.clone(),
94                    request.reasoning_effort.clone(),
95                ),
96            };
97
98            let mut prompt_request =
99                PromptExecutionRequest::from_user_prompt(rendered_prompt.clone())
100                    .with_context(context);
101            if let Some(system_prompt) = turn.system_prompt {
102                prompt_request = prompt_request.with_system_prompt(system_prompt);
103            }
104
105            let response = self.prompt_pipeline.execute(prompt_request).await?;
106            previous_actor = Some(turn.actor_id.clone());
107            current_input = response.text.clone();
108            turn_results.push(HandoffPatternTurnResult {
109                actor_id: turn.actor_id,
110                rendered_prompt,
111                output_text: response.text,
112            });
113        }
114
115        Ok(HandoffPatternExecutionResponse {
116            final_text: current_input,
117            turns: turn_results,
118            handoffs,
119            termination_reason: "completed_all_turns".to_string(),
120        })
121    }
122}