1use oino_ai::{AgentMessage, Usage};
4use oino_session::{BranchSummaryDraft, EntryId, EntryKind};
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use tokio_util::sync::CancellationToken;
8
9use super::{AgentSession, SessionOpError, lock};
10use crate::compaction::{
11 BranchSummaryOptions, collect_entries_for_branch_summary, generate_branch_summary,
12};
13use crate::events::SummarizationSource;
14
15#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct NavigateTreeOptions {
19 #[serde(default)]
21 pub summarize: bool,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub custom_instructions: Option<String>,
25 #[serde(default)]
27 pub replace_instructions: bool,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub label: Option<String>,
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct NavigateTreeResult {
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub editor_text: Option<String>,
40 pub cancelled: bool,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub aborted: Option<bool>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub summary_entry: Option<oino_session::SessionEntry>,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct ForkMessage {
54 pub entry_id: String,
56 pub text: String,
58}
59
60impl AgentSession {
61 #[must_use]
63 pub fn user_messages_for_forking(&self) -> Vec<ForkMessage> {
64 lock(&self.inner.manager)
65 .entries()
66 .iter()
67 .filter_map(|entry| match entry.message() {
68 Some(AgentMessage::User(user)) => {
69 let text = user.content.text("");
70 (!text.is_empty()).then(|| ForkMessage {
71 entry_id: entry.id.as_str().to_owned(),
72 text,
73 })
74 }
75 _ => None,
76 })
77 .collect()
78 }
79
80 #[allow(clippy::too_many_lines, reason = "one-to-one port of navigateTree")]
86 pub async fn navigate_tree(
87 &self,
88 target_id: &EntryId,
89 options: NavigateTreeOptions,
90 ) -> Result<NavigateTreeResult, SessionOpError> {
91 if self.is_streaming() {
92 return Err(SessionOpError::Failed(
93 "Wait for the current response to finish before navigating the session tree."
94 .into(),
95 ));
96 }
97 if self.is_compacting() {
98 return Err(SessionOpError::Failed(
99 "Wait for the current compaction or tree navigation to finish before navigating the session tree.".into(),
100 ));
101 }
102 let old_leaf = lock(&self.inner.manager).leaf_id().cloned();
103 if old_leaf.as_ref() == Some(target_id) {
104 return Ok(NavigateTreeResult::default());
105 }
106 if options.summarize && self.model().is_none() {
107 return Err(SessionOpError::Failed(
108 "No model available for summarization".into(),
109 ));
110 }
111 let (target, collected) = {
112 let manager = lock(&self.inner.manager);
113 let target = manager.entry(target_id).cloned().ok_or_else(|| {
114 SessionOpError::Failed(format!("Entry {} not found", target_id.as_str()))
115 })?;
116 let index = manager.index();
117 (
118 target,
119 collect_entries_for_branch_summary(index, old_leaf.as_ref(), target_id),
120 )
121 };
122 let mut custom_instructions = options.custom_instructions.clone();
123 let mut replace_instructions = options.replace_instructions;
124 let mut label = options.label.clone();
125 let cancel = CancellationToken::new();
126 lock(&self.inner.cancels).branch_summary = Some(cancel.clone());
127 self.mark_busy();
128 let result = async {
129 let mut extension_summary: Option<(String, Option<serde_json::Value>, Option<Usage>)> =
130 None;
131 let mut from_extension = false;
132 let runner = self.runner();
133 if runner.has_handlers("session_before_tree") {
134 let mut preparation = json!({
135 "targetId": target_id.as_str(),
136 "oldLeafId": old_leaf.as_ref().map(EntryId::as_str),
137 "commonAncestorId": collected.common_ancestor_id.as_ref().map(EntryId::as_str),
138 "entriesToSummarize": collected.entries,
139 "userWantsSummary": options.summarize,
140 });
141 if let Some(instructions) = &custom_instructions {
142 preparation["customInstructions"] = json!(instructions);
143 }
144 if options.replace_instructions {
145 preparation["replaceInstructions"] = json!(true);
146 }
147 if let Some(label) = &label {
148 preparation["label"] = json!(label);
149 }
150 if let Some(result) = runner
151 .emit(json!({"type": "session_before_tree", "preparation": preparation}))
152 .await
153 {
154 if result.get("cancel") == Some(&serde_json::Value::Bool(true)) {
155 return Ok(NavigateTreeResult {
156 cancelled: true,
157 ..NavigateTreeResult::default()
158 });
159 }
160 if let (Some(summary), true) = (
161 result.get("summary").filter(|s| s.is_object()),
162 options.summarize,
163 ) {
164 extension_summary = Some((
165 summary["summary"].as_str().unwrap_or_default().to_owned(),
166 summary.get("details").cloned(),
167 summary
168 .get("usage")
169 .and_then(|u| serde_json::from_value(u.clone()).ok()),
170 ));
171 from_extension = true;
172 }
173 if let Some(value) = result
174 .get("customInstructions")
175 .and_then(serde_json::Value::as_str)
176 {
177 custom_instructions = Some(value.to_owned());
178 }
179 if let Some(value) = result
180 .get("replaceInstructions")
181 .and_then(serde_json::Value::as_bool)
182 {
183 replace_instructions = value;
184 }
185 if let Some(value) = result.get("label").and_then(serde_json::Value::as_str) {
186 label = Some(value.to_owned());
187 }
188 }
189 }
190 let mut summary: Option<(String, Option<serde_json::Value>, Option<Usage>)> = None;
191 if options.summarize && !collected.entries.is_empty() && extension_summary.is_none() {
192 let model = self.model().ok_or_else(|| {
193 SessionOpError::Failed("No model available for summarization".into())
194 })?;
195 let summarizer = self
196 .summarizer(&model, cancel.clone(), SummarizationSource::BranchSummary)
197 .await
198 .map_err(SessionOpError::Failed)?;
199 let reserve = lock(&self.inner.settings)
200 .settings()
201 .branch_summary_settings()
202 .reserve_tokens;
203 let result = generate_branch_summary(
204 &collected.entries,
205 &summarizer,
206 &BranchSummaryOptions {
207 custom_instructions: custom_instructions.clone(),
208 replace_instructions,
209 reserve_tokens: reserve,
210 },
211 )
212 .await;
213 if result.aborted == Some(true) {
214 return Ok(NavigateTreeResult {
215 cancelled: true,
216 aborted: Some(true),
217 ..NavigateTreeResult::default()
218 });
219 }
220 if let Some(error) = result.error {
221 return Err(SessionOpError::Failed(error));
222 }
223 summary = Some((
224 result.summary.unwrap_or_default(),
225 Some(json!({
226 "readFiles": result.read_files.unwrap_or_default(),
227 "modifiedFiles": result.modified_files.unwrap_or_default(),
228 })),
229 result.usage,
230 ));
231 } else if let Some(extension) = extension_summary {
232 summary = Some(extension);
233 }
234 let (new_leaf, editor_text) = match &target.kind {
235 EntryKind::Message {
236 message: AgentMessage::User(user),
237 } => (target.parent_id.clone(), Some(user.content.text(""))),
238 EntryKind::CustomMessage { content, .. } => {
239 (target.parent_id.clone(), Some(content.text("")))
240 }
241 _ => (Some(target_id.clone()), None),
242 };
243 let mut summary_entry = None;
244 let summarized = summary
245 .as_ref()
246 .is_some_and(|(text, _, _)| !text.is_empty());
247 {
248 let mut manager = lock(&self.inner.manager);
249 if let (Some((text, details, usage)), true) = (summary, summarized) {
250 let from_id = old_leaf.clone().unwrap_or_else(|| EntryId::new("root"));
251 let id = manager
252 .branch_with_summary(
253 new_leaf.as_ref(),
254 BranchSummaryDraft {
255 from_id,
256 summary: text,
257 details,
258 from_hook: Some(from_extension),
259 usage,
260 },
261 )
262 .map_err(|e| SessionOpError::Failed(e.to_string()))?;
263 if let Some(label) = &label {
264 manager
265 .append_label_change(&id, Some(label))
266 .map_err(|e| SessionOpError::Failed(e.to_string()))?;
267 }
268 summary_entry = manager.entry(&id).cloned();
269 } else {
270 match &new_leaf {
271 None => manager.reset_leaf(),
272 Some(leaf) => manager
273 .set_leaf(leaf)
274 .map_err(|e| SessionOpError::Failed(e.to_string()))?,
275 }
276 if let Some(label) = &label {
277 manager
278 .append_label_change(target_id, Some(label))
279 .map_err(|e| SessionOpError::Failed(e.to_string()))?;
280 }
281 }
282 }
283 self.refresh_context();
284 self.restore_tools_from_transcript(&[]);
285 let new_leaf_id = lock(&self.inner.manager).leaf_id().cloned();
286 let runner = self.runner();
287 if runner.has_handlers("session_tree") {
288 let mut payload = json!({
289 "type": "session_tree",
290 "newLeafId": new_leaf_id.as_ref().map(EntryId::as_str),
291 "oldLeafId": old_leaf.as_ref().map(EntryId::as_str),
292 });
293 if let Some(entry) = &summary_entry {
294 payload["summaryEntry"] = json!(entry);
295 payload["fromExtension"] = json!(from_extension);
296 }
297 runner.emit(payload).await;
298 }
299 Ok(NavigateTreeResult {
300 editor_text,
301 cancelled: false,
302 aborted: None,
303 summary_entry,
304 })
305 }
306 .await;
307 lock(&self.inner.cancels).branch_summary = None;
308 self.set_idle_if_idle();
309 result
310 }
311}