atman_runtime/
session_naming.rs1use std::sync::Arc;
2
3use crate::{Executor, Session, Value};
4
5const MAX_MESSAGES: usize = 24;
6const MAX_MESSAGE_CHARS: usize = 600;
7
8pub async fn maybe_generate_session_name(
9 executor: &Executor,
10 session: &Arc<Session>,
11) -> anyhow::Result<bool> {
12 generate_session_name(executor, session, false).await
13}
14
15pub async fn force_generate_session_name(
16 executor: &Executor,
17 session: &Arc<Session>,
18) -> anyhow::Result<bool> {
19 generate_session_name(executor, session, true).await
20}
21
22async fn generate_session_name(
23 executor: &Executor,
24 session: &Arc<Session>,
25 force: bool,
26) -> anyhow::Result<bool> {
27 let meta = crate::session_meta::SessionMeta::load(session.dir()).unwrap_or_default();
28 if !force && meta.name_source == crate::session_meta::NameSource::User {
29 return Ok(false);
30 }
31 let flow = atman_dsl::parse::parse_file(crate::templates::SESSION_NAME_AT)
32 .map_err(|error| anyhow::anyhow!("parsing built-in session name flow: {error}"))?;
33 let input = naming_input(session);
34 let mut naming_executor = executor.clone();
35 naming_executor.events = crate::event::EventSink::new();
36 naming_executor.tool_ctx.events = None;
37 naming_executor.tool_ctx.session_runtime = None;
38 naming_executor.tool_ctx.session_messages = None;
39 naming_executor.tool_ctx.session_messages_handle = None;
40 naming_executor.tool_ctx.stdout_broadcast = None;
41 let value = naming_executor
42 .run(
43 &flow,
44 "session_name",
45 vec![("input".into(), Value::Str(input))],
46 )
47 .await
48 .map_err(|error| anyhow::anyhow!("running built-in session name flow: {error}"))?;
49 let Value::Str(title) = value else {
50 anyhow::bail!("session name flow did not return a string");
51 };
52 Ok(crate::session_meta::SessionMeta::set_auto_title_with_force(
53 session.dir(),
54 title,
55 force,
56 )?)
57}
58
59fn naming_input(session: &Session) -> String {
60 let messages = session.messages();
61 let start = messages.len().saturating_sub(MAX_MESSAGES);
62 let mut input = format!(
63 "Goal:\n{}\n\nRecent conversation:\n",
64 session.goal().unwrap_or_default()
65 );
66 for message in &messages[start..] {
67 let text: String = message
68 .text_concat()
69 .chars()
70 .take(MAX_MESSAGE_CHARS)
71 .collect();
72 if !text.trim().is_empty() {
73 input.push_str(message.role.as_str());
74 input.push_str(": ");
75 input.push_str(&text);
76 input.push('\n');
77 }
78 }
79 input
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::message::Message;
86
87 #[test]
88 fn naming_input_uses_goal_and_recent_messages() {
89 let session = Session::open_ephemeral();
90 session.set_goal(Some("Ship session naming".into()));
91 session.append_message(
92 Message::user_text(crate::event::TurnId::now(), "Fix switcher"),
93 None,
94 );
95 let input = naming_input(&session);
96 assert!(input.contains("Ship session naming"));
97 assert!(input.contains("user: Fix switcher"));
98 }
99}