nanocodex_oai_api/session/
builder.rs1use std::sync::Arc;
2
3use ::tower::Service;
4use tokio::sync::mpsc;
5
6use crate::{
7 ContentItem, EventSink, MessageRole, OpenAi, ResponseItem, ResponsesAttempt, ResponsesClient,
8 ResponsesServiceResponse, Thinking, ToolDefinition, TransportStats,
9 openai::{ResponsesServiceFactory, StandardServiceFactory},
10 responses::RequestProfile,
11};
12
13use super::{
14 context::assign_missing_response_item_id,
15 response::{
16 CompletedCompaction, Response, ResponseError, ResponseInput, run_compact, run_create,
17 },
18 state::{ManagedSessionState, SessionId},
19};
20
21const RESPONSE_EVENT_CAPACITY: usize = 64;
22
23#[derive(Clone)]
25pub struct SessionBuilder<F = StandardServiceFactory> {
26 openai: OpenAi<F>,
27 instructions: Arc<str>,
28 session_id: Option<SessionId>,
29 prompt_cache_key: Option<String>,
30 tools: Vec<ToolDefinition>,
31}
32
33impl<F> SessionBuilder<F>
34where
35 F: ResponsesServiceFactory,
36{
37 pub(crate) const fn new(openai: OpenAi<F>, instructions: Arc<str>) -> Self {
38 Self {
39 openai,
40 instructions,
41 session_id: None,
42 prompt_cache_key: None,
43 tools: Vec::new(),
44 }
45 }
46
47 #[must_use]
50 pub const fn session_id(mut self, session_id: SessionId) -> Self {
51 self.session_id = Some(session_id);
52 self
53 }
54
55 #[must_use]
57 pub fn prompt_cache_key(mut self, prompt_cache_key: impl Into<String>) -> Self {
58 self.prompt_cache_key = Some(prompt_cache_key.into());
59 self
60 }
61
62 #[must_use]
100 pub fn tool_definitions(mut self, tools: impl IntoIterator<Item = ToolDefinition>) -> Self {
101 self.tools = tools.into_iter().collect();
102 self
103 }
104
105 pub fn build(self) -> Result<Session<F::Service>, SessionBuildError> {
111 if self.instructions.trim().is_empty() {
112 return Err(SessionBuildError::EmptyInstructions);
113 }
114 let session_id = self.session_id.unwrap_or_default();
115 let prompt_cache_key = self
116 .prompt_cache_key
117 .unwrap_or_else(|| session_id.to_string());
118 if prompt_cache_key.trim().is_empty() {
119 return Err(SessionBuildError::EmptyPromptCacheKey);
120 }
121
122 let mut prefix = [
123 ResponseItem::additional_tools(self.tools),
124 ResponseItem::message(
125 MessageRole::Developer,
126 [ContentItem::InputText {
127 text: self.instructions.to_string().into_boxed_str(),
128 }],
129 ),
130 ];
131 assign_request_prefix_ids(&mut prefix);
132 let profile =
133 RequestProfile::new(session_id.to_string(), prompt_cache_key, Arc::from(prefix));
134 let config = self.openai.config().clone();
135 let service = self.openai.make_service();
136
137 Ok(Session {
138 id: session_id,
139 client: ResponsesClient::new(service),
140 profile,
141 state: ManagedSessionState::new(Vec::new()),
142 canonical_context: Vec::new(),
143 canonical_context_reinjection_pending: false,
144 next_call_index: 1,
145 next_logical_turn: 1,
146 thinking: config.thinking,
147 fast_mode: config.fast_mode,
148 transport_stats: Arc::new(TransportStats::default()),
149 })
150 }
151}
152
153#[derive(Debug, thiserror::Error)]
155pub enum SessionBuildError {
156 #[error("OpenAI session instructions must not be empty")]
158 EmptyInstructions,
159 #[error("OpenAI prompt cache key must not be empty")]
161 EmptyPromptCacheKey,
162}
163
164pub struct Session<S> {
169 pub(super) id: SessionId,
170 pub(super) client: ResponsesClient<S>,
171 pub(super) profile: RequestProfile,
172 pub(super) state: ManagedSessionState,
173 pub(super) canonical_context: Vec<ResponseItem>,
174 pub(super) canonical_context_reinjection_pending: bool,
175 pub(super) next_call_index: u32,
176 next_logical_turn: u64,
177 pub(super) thinking: Thinking,
178 pub(super) fast_mode: bool,
179 pub(super) transport_stats: Arc<TransportStats>,
180}
181
182impl<S> Session<S> {
183 #[must_use]
185 pub const fn id(&self) -> SessionId {
186 self.id
187 }
188
189 pub const fn turn(&mut self) -> ResponseTurn<'_, S> {
196 let logical_turn = self.next_logical_turn;
197 self.next_logical_turn = self.next_logical_turn.saturating_add(1);
198 ResponseTurn {
199 session: self,
200 logical_turn,
201 completed_generation: false,
202 }
203 }
204
205 #[must_use]
207 pub fn history_len(&self) -> usize {
208 self.state.history_len()
209 }
210
211 pub fn history(&self) -> impl ExactSizeIterator<Item = &ResponseItem> {
214 self.state.history()
215 }
216
217 #[must_use]
225 pub fn active_context_tokens(&self) -> u64 {
226 self.state.active_context_tokens()
227 }
228}
229
230pub struct ResponseTurn<'session, S> {
232 pub(super) session: &'session mut Session<S>,
233 pub(super) logical_turn: u64,
234 pub(super) completed_generation: bool,
235}
236
237impl<S> ResponseTurn<'_, S> {
238 #[must_use]
240 pub fn active_context_tokens(&self) -> u64 {
241 self.session.active_context_tokens()
242 }
243}
244
245#[cfg(not(target_family = "wasm"))]
246impl<S> ResponseTurn<'_, S>
247where
248 S: Service<ResponsesAttempt, Response = ResponsesServiceResponse> + Send,
249 S::Error: Into<ResponseError> + Send,
250 S::Future: Send,
251{
252 pub fn create(&mut self, input: impl Into<ResponseInput>) -> Response<'_> {
254 let (sink, raw_events) = EventSink::channel(self.session.profile.session_id().to_owned());
255 drop(raw_events);
256 let (response_events, events) = mpsc::channel(RESPONSE_EVENT_CAPACITY);
257 let run = Box::pin(run_create(self, input.into(), sink, response_events));
258 Response::new(events, run)
259 }
260
261 pub async fn compact(&mut self) -> Result<CompletedCompaction, ResponseError> {
275 run_compact(self).await
276 }
277}
278
279#[cfg(target_family = "wasm")]
280impl<S> ResponseTurn<'_, S>
281where
282 S: Service<ResponsesAttempt, Response = ResponsesServiceResponse>,
283 S::Error: Into<ResponseError>,
284{
285 pub fn create(&mut self, input: impl Into<ResponseInput>) -> Response<'_> {
287 let (sink, raw_events) = EventSink::channel(self.session.profile.session_id().to_owned());
288 drop(raw_events);
289 let (response_events, events) = mpsc::channel(RESPONSE_EVENT_CAPACITY);
290 let run = Box::pin(run_create(self, input.into(), sink, response_events));
291 Response::new(events, run)
292 }
293
294 pub async fn compact(&mut self) -> Result<CompletedCompaction, ResponseError> {
308 run_compact(self).await
309 }
310}
311
312fn assign_request_prefix_ids(prefix: &mut [ResponseItem]) {
313 for item in prefix {
314 if matches!(
315 item,
316 ResponseItem::AdditionalTools { .. }
317 | ResponseItem::Message {
318 role: MessageRole::Developer,
319 ..
320 }
321 ) {
322 item.strip_id();
323 continue;
324 }
325 assign_missing_response_item_id(item);
326 }
327}