stasis/application/orchestration/
handoff_pattern_pipeline.rs1use 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}}", ¤t_input)
79 .replace("{input}", ¤t_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}