bamboo_engine/session_app/
session_create.rs1use bamboo_agent_core::{Message, Session};
8use bamboo_domain::reasoning::ReasoningEffort;
9use bamboo_domain::ProviderModelRef;
10
11use super::provider_model::{persist_legacy_model_provider, persist_model_ref};
12use crate::model_config_helper::GOLD_CONFIG_METADATA_KEY;
13
14pub struct CreateSessionInput {
16 pub id: String,
17 pub title: Option<String>,
18 pub system_prompt: Option<String>,
19 pub model: Option<String>,
20 pub model_ref: Option<ProviderModelRef>,
21 pub reasoning_effort: Option<ReasoningEffort>,
22 pub gold_config_json: Option<String>,
23 pub workspace_path: Option<String>,
30}
31
32pub struct CreateSessionConfig {
37 pub default_model: Option<String>,
38 pub default_reasoning_effort: Option<ReasoningEffort>,
39 pub global_default_prompt: String,
40 pub builtin_fallback_prompt: &'static str,
41}
42
43pub fn build_new_session(input: &CreateSessionInput, config: &CreateSessionConfig) -> Session {
45 let model = input
46 .model_ref
47 .as_ref()
48 .map(|model_ref| model_ref.model.clone())
49 .unwrap_or_else(|| resolve_model(input.model.as_deref(), config.default_model.as_deref()));
50 let mut session = Session::new(input.id.clone(), model);
51 if let Some(model_ref) = input.model_ref.as_ref() {
52 persist_model_ref(&mut session, model_ref);
53 } else {
54 persist_legacy_model_provider(&mut session, input.model.as_deref(), None);
55 }
56 session.reasoning_effort =
57 resolve_reasoning_effort(input.reasoning_effort, config.default_reasoning_effort);
58 if let Some(gold_config_json) = trimmed_non_empty(input.gold_config_json.as_deref()) {
59 session
60 .metadata
61 .insert(GOLD_CONFIG_METADATA_KEY.to_string(), gold_config_json);
62 }
63 if let Some(workspace_path) = trimmed_non_empty(input.workspace_path.as_deref()) {
64 session.set_workspace_path_meta(workspace_path);
65 }
66
67 if let Some(title) = trimmed_non_empty(input.title.as_deref()) {
68 session.title = title;
69 }
70 let explicit_prompt = trimmed_non_empty(input.system_prompt.as_deref());
71 let has_explicit_prompt = explicit_prompt.is_some();
72 let base_prompt = explicit_prompt.unwrap_or_else(|| {
73 let trimmed = config.global_default_prompt.trim();
74 if trimmed.is_empty() {
75 config.builtin_fallback_prompt.to_string()
76 } else {
77 trimmed.to_string()
78 }
79 });
80 session
81 .metadata
82 .insert("base_system_prompt".to_string(), base_prompt.clone());
83
84 if has_explicit_prompt {
85 session.add_message(Message::system(base_prompt));
86 crate::runner::refresh_prompt_snapshot(&mut session);
87 }
88
89 session
90}
91
92pub fn resolve_model(request_model: Option<&str>, config_model: Option<&str>) -> String {
94 trimmed_non_empty(request_model)
95 .or_else(|| config_model.map(ToString::to_string))
96 .unwrap_or_else(|| "unknown".to_string())
97}
98
99pub fn resolve_reasoning_effort(
101 request_effort: Option<ReasoningEffort>,
102 config_effort: Option<ReasoningEffort>,
103) -> Option<ReasoningEffort> {
104 request_effort.or(config_effort)
105}
106
107pub fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
109 value
110 .map(str::trim)
111 .filter(|value| !value.is_empty())
112 .map(ToString::to_string)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 const BUILTIN_FALLBACK: &str = "You are a helpful assistant.";
120
121 fn default_config() -> CreateSessionConfig {
122 CreateSessionConfig {
123 default_model: None,
124 default_reasoning_effort: None,
125 global_default_prompt: "Global fallback".to_string(),
126 builtin_fallback_prompt: BUILTIN_FALLBACK,
127 }
128 }
129
130 #[test]
131 fn resolve_model_uses_request_model_when_present() {
132 assert_eq!(resolve_model(Some(" gpt-5 "), None), "gpt-5");
133 }
134
135 #[test]
136 fn resolve_model_falls_back_to_config() {
137 assert_eq!(resolve_model(None, Some("gpt-4")), "gpt-4");
138 }
139
140 #[test]
141 fn resolve_model_falls_back_to_unknown() {
142 assert_eq!(resolve_model(None, None), "unknown");
143 }
144
145 #[test]
146 fn resolve_model_ignores_blank_request() {
147 assert_eq!(resolve_model(Some(" "), Some("gpt-4")), "gpt-4");
148 }
149
150 #[test]
151 fn resolve_reasoning_effort_prefers_request() {
152 assert_eq!(
153 resolve_reasoning_effort(Some(ReasoningEffort::High), Some(ReasoningEffort::Low)),
154 Some(ReasoningEffort::High)
155 );
156 }
157
158 #[test]
159 fn resolve_reasoning_effort_falls_back_to_config() {
160 assert_eq!(
161 resolve_reasoning_effort(None, Some(ReasoningEffort::Medium)),
162 Some(ReasoningEffort::Medium)
163 );
164 }
165
166 #[test]
167 fn build_new_session_applies_title_and_system_prompt() {
168 let input = CreateSessionInput {
169 id: "session-1".to_string(),
170 title: Some(" Sprint Session ".to_string()),
171 system_prompt: Some(" You are helpful ".to_string()),
172 model: Some("gpt-5".to_string()),
173 model_ref: None,
174 reasoning_effort: Some(ReasoningEffort::High),
175 gold_config_json: None,
176 workspace_path: None,
177 };
178 let session = build_new_session(&input, &default_config());
179
180 assert_eq!(session.title, "Sprint Session");
181 assert_eq!(
182 session
183 .metadata
184 .get("base_system_prompt")
185 .map(String::as_str),
186 Some("You are helpful")
187 );
188 assert_eq!(session.reasoning_effort, Some(ReasoningEffort::High));
189 assert_eq!(
190 session.messages.first().map(|m| m.content.as_str()),
191 Some("You are helpful")
192 );
193 }
194
195 #[test]
196 fn build_new_session_uses_global_default_when_no_explicit_prompt() {
197 let input = CreateSessionInput {
198 id: "session-2".to_string(),
199 title: None,
200 system_prompt: None,
201 model: Some("gpt-5".to_string()),
202 model_ref: None,
203 reasoning_effort: None,
204 gold_config_json: None,
205 workspace_path: None,
206 };
207 let session = build_new_session(&input, &default_config());
208
209 assert_eq!(
210 session
211 .metadata
212 .get("base_system_prompt")
213 .map(String::as_str),
214 Some("Global fallback")
215 );
216 assert!(session.messages.is_empty());
217 }
218
219 #[test]
220 fn build_new_session_uses_builtin_fallback_when_global_is_empty() {
221 let config = CreateSessionConfig {
222 global_default_prompt: " ".to_string(),
223 ..default_config()
224 };
225 let input = CreateSessionInput {
226 id: "session-3".to_string(),
227 title: None,
228 system_prompt: None,
229 model: Some("gpt-5".to_string()),
230 model_ref: None,
231 reasoning_effort: None,
232 gold_config_json: None,
233 workspace_path: None,
234 };
235 let session = build_new_session(&input, &config);
236
237 assert_eq!(
238 session
239 .metadata
240 .get("base_system_prompt")
241 .map(String::as_str),
242 Some(BUILTIN_FALLBACK)
243 );
244 }
245
246 #[test]
247 fn build_new_session_with_explicit_prompt_generates_snapshot() {
248 let input = CreateSessionInput {
249 id: "session-4".to_string(),
250 title: None,
251 system_prompt: Some("Custom prompt".to_string()),
252 model: Some("gpt-5".to_string()),
253 model_ref: None,
254 reasoning_effort: None,
255 gold_config_json: None,
256 workspace_path: None,
257 };
258 let session = build_new_session(&input, &default_config());
259
260 let snapshot =
261 crate::runner::read_prompt_snapshot(&session).expect("prompt snapshot should exist");
262 assert_eq!(snapshot.base_system_prompt, "Custom prompt");
263 assert_eq!(snapshot.effective_system_prompt, "Custom prompt");
264 }
265
266 #[test]
267 fn build_new_session_with_model_ref_persists_bare_model_and_provider_metadata() {
268 let input = CreateSessionInput {
269 id: "session-5".to_string(),
270 title: None,
271 system_prompt: None,
272 model: Some("ignored-compat-model".to_string()),
273 model_ref: Some(ProviderModelRef::new("anthropic", "claude-3-7-sonnet")),
274 reasoning_effort: None,
275 gold_config_json: None,
276 workspace_path: None,
277 };
278 let session = build_new_session(&input, &default_config());
279
280 assert_eq!(session.model, "claude-3-7-sonnet");
281 assert_eq!(
282 session.model_ref,
283 Some(ProviderModelRef::new("anthropic", "claude-3-7-sonnet"))
284 );
285 assert_eq!(
286 session.metadata.get("provider_name").map(String::as_str),
287 Some("anthropic")
288 );
289 }
290
291 #[test]
294 fn build_new_session_with_workspace_path_sets_workspace_metadata() {
295 let input = CreateSessionInput {
296 id: "session-6".to_string(),
297 title: None,
298 system_prompt: None,
299 model: Some("gpt-5".to_string()),
300 model_ref: None,
301 reasoning_effort: None,
302 gold_config_json: None,
303 workspace_path: Some(" /tmp/my-workspace ".to_string()),
304 };
305 let session = build_new_session(&input, &default_config());
306
307 assert_eq!(
308 session.workspace_path_meta().as_deref(),
309 Some("/tmp/my-workspace")
310 );
311 }
312
313 #[test]
314 fn build_new_session_without_workspace_path_leaves_metadata_unset() {
315 let input = CreateSessionInput {
316 id: "session-7".to_string(),
317 title: None,
318 system_prompt: None,
319 model: Some("gpt-5".to_string()),
320 model_ref: None,
321 reasoning_effort: None,
322 gold_config_json: None,
323 workspace_path: None,
324 };
325 let session = build_new_session(&input, &default_config());
326
327 assert!(session.workspace_path_meta().is_none());
328 }
329}