navi_core/
session_title.rs1use anyhow::{Context, Result};
4use async_trait::async_trait;
5use serde_json::json;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8
9use crate::tool::{Tool, ToolDefinition, ToolInvocation, ToolKind, ToolResult};
10
11#[derive(Clone, Default)]
14pub struct SessionTitleHandle {
15 pending: Arc<Mutex<Option<String>>>,
16 assigned: Arc<AtomicBool>,
17}
18
19impl SessionTitleHandle {
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn is_assigned(&self) -> bool {
25 self.assigned.load(Ordering::SeqCst)
26 }
27
28 pub fn set(&self, title: &str) -> Result<String> {
29 let title = title.split_whitespace().collect::<Vec<_>>().join(" ");
30 let title = title.trim();
31 if title.is_empty() {
32 anyhow::bail!("session title must not be empty");
33 }
34 let title: String = title.chars().take(120).collect();
35 *self.pending.lock().unwrap_or_else(|e| e.into_inner()) = Some(title.clone());
36 self.assigned.store(true, Ordering::SeqCst);
37 Ok(title)
38 }
39
40 pub fn take(&self) -> Option<String> {
41 self.pending
42 .lock()
43 .unwrap_or_else(|e| e.into_inner())
44 .take()
45 }
46}
47
48pub struct SessionTitleTool {
52 handle: SessionTitleHandle,
53}
54
55impl SessionTitleTool {
56 pub fn new(handle: SessionTitleHandle) -> Self {
57 Self { handle }
58 }
59}
60
61#[async_trait]
62impl Tool for SessionTitleTool {
63 fn definition(&self) -> ToolDefinition {
64 ToolDefinition::new(
65 "set_session_title",
66 "Set a concise title for the current session. Call this as the first action of a new session using the user's initial request. Call it again only when the session's primary objective changes materially.",
67 ToolKind::Read,
68 json!({
69 "type": "object",
70 "properties": {
71 "title": {
72 "type": "string",
73 "description": "Concise, specific session title (maximum 120 characters)."
74 }
75 },
76 "required": ["title"],
77 "additionalProperties": false
78 }),
79 )
80 }
81
82 async fn invoke(&self, invocation: ToolInvocation) -> Result<ToolResult> {
83 let title = invocation
84 .input
85 .get("title")
86 .and_then(serde_json::Value::as_str)
87 .context("missing required string `title`")?;
88 let title = self.handle.set(title)?;
89 Ok(ToolResult {
90 invocation_id: invocation.id,
91 ok: true,
92 output: json!({ "title": title }),
93 })
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[tokio::test]
102 async fn tool_normalizes_and_records_title() {
103 let handle = SessionTitleHandle::new();
104 let tool = SessionTitleTool::new(handle.clone());
105 let result = tool
106 .invoke(ToolInvocation {
107 id: "title-1".into(),
108 tool_name: "set_session_title".into(),
109 input: json!({ "title": " Repair prompt cache " }),
110 })
111 .await
112 .expect("title tool succeeds");
113
114 assert!(result.ok);
115 assert!(handle.is_assigned());
116 assert_eq!(handle.take().as_deref(), Some("Repair prompt cache"));
117 }
118}