agentic_core/executor/modes/
conversation.rs1use crate::storage::{ConversationData, ConversationStore, InOutItem, ResponseMetadata};
4use crate::types::io::OutputItem;
5
6use crate::executor::error::{ExecutorError, ExecutorResult};
7use crate::executor::request::RequestContext;
8
9#[derive(Clone, Debug)]
11pub struct ConversationHandler {
12 store: ConversationStore,
13}
14
15impl ConversationHandler {
16 #[must_use]
17 pub fn new(store: ConversationStore) -> Self {
18 Self { store }
19 }
20
21 pub async fn get_or_create(&self, ctx: &RequestContext) -> ExecutorResult<ConversationData> {
29 let conv_id = ctx
30 .original_request
31 .conversation_id
32 .as_deref()
33 .ok_or_else(|| ExecutorError::InvalidRequest("conversation_id is required for get_or_create".into()))?;
34 self.store.get_or_create(conv_id).await.map_err(ExecutorError::Storage)
35 }
36
37 pub async fn get(&self, ctx: &RequestContext) -> ExecutorResult<ConversationData> {
45 let conv_id = ctx
46 .original_request
47 .conversation_id
48 .as_deref()
49 .ok_or_else(|| ExecutorError::InvalidRequest("conversation_id is required for get".into()))?;
50 self.store.get(conv_id).await.map_err(ExecutorError::Storage)
51 }
52
53 pub async fn create(&self) -> ExecutorResult<ConversationData> {
58 self.store.create().await.map_err(ExecutorError::Storage)
59 }
60
61 pub async fn rehydrate(&self, ctx: &RequestContext) -> ExecutorResult<Vec<InOutItem>> {
70 let conv_id = ctx
71 .original_request
72 .conversation_id
73 .as_deref()
74 .ok_or_else(|| ExecutorError::InvalidRequest("conversation_id is required for rehydrate".into()))?;
75 self.store.rehydrate(conv_id).await.map_err(ExecutorError::Storage)
76 }
77
78 pub async fn execute_turn(&self, ctx: RequestContext, output_items: Vec<OutputItem>) -> ExecutorResult<()> {
88 let conversation_id = ctx
89 .conversation_id
90 .ok_or_else(|| ExecutorError::InvalidRequest("conversation_id is required for execute_turn".into()))?;
91
92 let metadata = ResponseMetadata {
93 model: ctx.enriched_request.model,
94 previous_response_id: ctx.original_request.previous_response_id,
95 effective_tools: ctx.enriched_request.tools,
96 effective_tool_choice: ctx.enriched_request.tool_choice.unwrap_or_default(),
97 effective_instructions: ctx.enriched_request.instructions,
98 };
99
100 let mut new_items = Vec::with_capacity(ctx.new_input_items.len() + output_items.len());
101 new_items.extend(ctx.new_input_items.into_iter().map(InOutItem::Input));
102 new_items.extend(output_items.into_iter().map(InOutItem::Output));
103
104 self.store
105 .persist(
106 &conversation_id,
107 &ctx.response_id,
108 metadata.previous_response_id.as_deref(),
109 new_items,
110 &metadata,
111 )
112 .await
113 .map_err(ExecutorError::Storage)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::types::io::ResponsesInput;
121 use crate::types::request_response::RequestPayload;
122
123 fn disabled_handler() -> ConversationHandler {
124 ConversationHandler::new(ConversationStore::disabled())
125 }
126
127 fn make_ctx(conversation_id: Option<&str>) -> RequestContext {
128 let req = RequestPayload {
129 model: "test".into(),
130 input: ResponsesInput::Text("hi".into()),
131 instructions: None,
132 previous_response_id: None,
133 conversation_id: conversation_id.map(str::to_string),
134 tools: None,
135 tool_choice: None,
136 stream: false,
137 store: true,
138 include: None,
139 temperature: None,
140 top_p: None,
141 max_output_tokens: None,
142 truncation: None,
143 metadata: None,
144 };
145 RequestContext {
146 enriched_request: req.clone(),
147 original_request: req,
148 new_input_items: vec![],
149 response_id: "resp_test".into(),
150 conversation_id: conversation_id.map(str::to_string),
151 }
152 }
153
154 #[tokio::test]
155 async fn test_get_or_create_missing_id_returns_error() {
156 let result = disabled_handler().get_or_create(&make_ctx(None)).await;
157 assert!(result.is_err());
158 }
159
160 #[tokio::test]
161 async fn test_rehydrate_missing_id_returns_error() {
162 let result = disabled_handler().rehydrate(&make_ctx(None)).await;
163 assert!(result.is_err());
164 }
165
166 #[tokio::test]
167 async fn test_get_or_create_disabled_store_returns_error() {
168 let result = disabled_handler().get_or_create(&make_ctx(Some("conv_1"))).await;
169 assert!(result.is_err());
170 }
171
172 #[tokio::test]
173 async fn test_get_disabled_store_returns_error() {
174 let result = disabled_handler().get(&make_ctx(Some("conv_1"))).await;
175 assert!(result.is_err());
176 }
177
178 #[tokio::test]
179 async fn test_rehydrate_disabled_store_returns_error() {
180 let result = disabled_handler().rehydrate(&make_ctx(Some("conv_1"))).await;
181 assert!(result.is_err());
182 }
183
184 #[tokio::test]
185 async fn test_execute_turn_missing_conv_id_returns_error() {
186 let result = disabled_handler().execute_turn(make_ctx(None), vec![]).await;
187 assert!(result.is_err());
188 }
189}