Skip to main content

bamboo_engine/session_app/
execution_prep.rs

1//! Single authoritative pre-execution session mutation point.
2//!
3//! Historically three execution entry points each duplicated — with subtly
4//! different logic — the work of (a) placing the authoritative leading System
5//! message and (b) setting `session.model` before handing the session to the
6//! agent loop. This module consolidates that into [`prepare_session_for_execution`]
7//! so there is exactly one place that defines the pre-execution mutation.
8//!
9//! The three callers are:
10//! - the SDK facade (`bamboo_sdk::agent::Agent::execute_internal`), which owns a
11//!   configured instruction and model and passes both;
12//! - the server spawn path (`runtime::execution::agent_spawn::spawn_session_execution`),
13//!   whose caller has already placed the system prompt, so it passes `None` for
14//!   `system_prompt` and only the resolved model;
15//! - the child spawn path (`sdk::spawn::run_child_spawn`), likewise `None` for the
16//!   system prompt and only the child model.
17
18use bamboo_agent_core::{Message, Role, Session};
19
20/// Apply the authoritative pre-execution mutations to `session`.
21///
22/// This encodes the single, authoritative behavior that every execution entry
23/// point must share:
24///
25/// - If `system_prompt` is `Some`, it is applied as the session's **leading**
26///   System message. The supplied prompt is authoritative: if the first message
27///   is already a [`Role::System`] message it is *replaced* (never duplicated),
28///   otherwise a System message is *inserted* at index 0. This guarantees a
29///   caller-supplied session can't silently shadow the configured instruction.
30/// - If `model` is `Some`, `session.model` is set to it.
31///
32/// Call sites that don't supply one of these inputs (e.g. the spawn paths, whose
33/// caller already placed the system prompt) pass `None` for that parameter, so
34/// behavior is identical to the previous inline logic.
35pub fn prepare_session_for_execution(
36    session: &mut Session,
37    system_prompt: Option<&str>,
38    model: Option<&str>,
39) {
40    if let Some(prompt) = system_prompt {
41        match session.messages.first() {
42            Some(first) if matches!(first.role, Role::System) => {
43                session.messages[0] = Message::system(prompt.to_string());
44            }
45            _ => session
46                .messages
47                .insert(0, Message::system(prompt.to_string())),
48        }
49    }
50
51    if let Some(model) = model {
52        session.model = model.to_string();
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use bamboo_agent_core::Message;
60
61    fn session_with(messages: Vec<Message>) -> Session {
62        let mut s = Session::new("test-session", "old-model");
63        s.messages = messages;
64        s
65    }
66
67    #[test]
68    fn empty_session_with_prompt_inserts_at_index_zero() {
69        let mut session = session_with(vec![Message::user("hello")]);
70
71        prepare_session_for_execution(&mut session, Some("you are helpful"), None);
72
73        assert_eq!(session.messages.len(), 2);
74        assert!(matches!(session.messages[0].role, Role::System));
75        assert_eq!(session.messages[0].content, "you are helpful");
76        assert!(matches!(session.messages[1].role, Role::User));
77    }
78
79    #[test]
80    fn leading_system_is_replaced_not_duplicated() {
81        let mut session = session_with(vec![
82            Message::system("stale prompt"),
83            Message::user("hello"),
84        ]);
85
86        prepare_session_for_execution(&mut session, Some("authoritative prompt"), None);
87
88        // Replaced in place, no duplicate System message.
89        assert_eq!(session.messages.len(), 2);
90        assert!(matches!(session.messages[0].role, Role::System));
91        assert_eq!(session.messages[0].content, "authoritative prompt");
92        assert!(matches!(session.messages[1].role, Role::User));
93    }
94
95    #[test]
96    fn leading_non_system_gets_prompt_inserted_at_zero() {
97        let mut session =
98            session_with(vec![Message::user("hello"), Message::assistant("hi", None)]);
99
100        prepare_session_for_execution(&mut session, Some("you are helpful"), None);
101
102        assert_eq!(session.messages.len(), 3);
103        assert!(matches!(session.messages[0].role, Role::System));
104        assert_eq!(session.messages[0].content, "you are helpful");
105        assert!(matches!(session.messages[1].role, Role::User));
106        assert!(matches!(session.messages[2].role, Role::Assistant));
107    }
108
109    #[test]
110    fn model_is_set_when_some() {
111        let mut session = session_with(vec![Message::user("hello")]);
112
113        prepare_session_for_execution(&mut session, None, Some("new-model"));
114
115        assert_eq!(session.model, "new-model");
116        // No system prompt supplied → message list untouched.
117        assert_eq!(session.messages.len(), 1);
118        assert!(matches!(session.messages[0].role, Role::User));
119    }
120
121    #[test]
122    fn none_inputs_leave_session_untouched() {
123        let mut session = session_with(vec![Message::user("hello")]);
124
125        prepare_session_for_execution(&mut session, None, None);
126
127        assert_eq!(session.model, "old-model");
128        assert_eq!(session.messages.len(), 1);
129    }
130}