Skip to main content

atman_runtime/
session_naming.rs

1use 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}