1use super::*;
4
5impl Session {
6 pub fn from_goose(path: impl AsRef<Path>) -> Result<Session> {
8 Self::from_goose_str(&std::fs::read_to_string(path.as_ref())?)
9 }
10
11 pub fn from_goose_str(json: &str) -> Result<Session> {
20 let document: Value = serde_json::from_str(json).map_err(crate::Error::Decode)?;
21 let object = document.as_object().ok_or_else(|| {
22 Error::InvalidSession("Goose session export must be a JSON object".to_string())
23 })?;
24 let conversation = object
25 .get("conversation")
26 .and_then(Value::as_array)
27 .ok_or_else(|| {
28 Error::InvalidSession(
29 "Goose session export must contain a conversation array".to_string(),
30 )
31 })?;
32
33 let mut meta = SessionMeta::new(SessionSource::Goose);
34 meta.session_id = object.get("id").and_then(Value::as_str).map(str::to_string);
35 meta.cwd = object
36 .get("working_dir")
37 .or_else(|| object.get("workingDir"))
38 .and_then(Value::as_str)
39 .map(PathBuf::from);
40 meta.model = object
41 .get("model_config")
42 .or_else(|| object.get("modelConfig"))
43 .and_then(|model| model.get("model_name").or_else(|| model.get("modelName")))
44 .and_then(Value::as_str)
45 .map(str::to_string);
46 for (source, target) in [
47 ("name", "session_name"),
48 ("created_at", "created_at"),
49 ("updated_at", "updated_at"),
50 ("session_type", "goose_session_type"),
51 ("goose_mode", "goose_mode"),
52 ("provider_name", "goose_provider_name"),
53 ("parent_session_id", "parent_session_id"),
54 ] {
55 if let Some(value) = object.get(source) {
56 meta.lineage.insert(
57 target.to_string(),
58 value
59 .as_str()
60 .map(str::to_string)
61 .unwrap_or_else(|| value.to_string()),
62 );
63 }
64 }
65 let mut header = document.clone();
66 if let Some(header) = header.as_object_mut() {
67 header.remove("conversation");
68 }
69 meta.goose_header = Some(header.clone());
70
71 let mut messages = Vec::new();
72 for (native_index, native) in conversation.iter().enumerate() {
73 let before = messages.len();
74 normalize_goose_message(native, native_index, &mut messages);
75 if let Some(first) = messages.get_mut(before) {
76 first
77 .metadata
78 .insert("goose_native_message".to_string(), native.to_string());
79 first
80 .metadata
81 .insert("goose_native_index".to_string(), native_index.to_string());
82 if native_index == 0 {
83 first
84 .metadata
85 .insert("goose_session_header".to_string(), header.to_string());
86 }
87 restore_grok_message_extension(native, first);
88 }
89 for message in messages.iter_mut().skip(before + 1) {
90 message
91 .metadata
92 .insert("goose_native_index".to_string(), native_index.to_string());
93 }
94 }
95 ensure_tool_results_paired(&mut messages);
96
97 let (raw_lines, raw_trailing_newline) = split_lines_verbatim(json);
98 let raw = raw_lines.iter().map(|line| line.to_string()).collect();
99 let imported_message_count = Some(messages.len());
100 Ok(Session {
101 meta,
102 messages,
103 subagents: Vec::new(),
104 raw,
105 raw_trailing_newline,
106 imported_message_count,
107 raw_is_verbatim: true,
108 parse_error_lines: 0,
109 load_residue: Vec::new(),
110 })
111 }
112
113 pub fn from_goose_sqlite(db_path: &Path, session_id: &str) -> Result<Session> {
119 Self::from_goose_sqlite_with_limit(db_path, session_id, None)
120 }
121
122 #[doc(hidden)]
127 pub fn from_goose_sqlite_display(
128 db_path: &Path,
129 session_id: &str,
130 message_limit: usize,
131 ) -> Result<Session> {
132 Self::from_goose_sqlite_with_limit(db_path, session_id, Some(message_limit.max(1)))
133 }
134
135 fn from_goose_sqlite_with_limit(
136 db_path: &Path,
137 session_id: &str,
138 message_limit: Option<usize>,
139 ) -> Result<Session> {
140 let connection = Connection::open_with_flags(
141 db_path,
142 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
143 )
144 .map_err(|error| Error::Other(format!("failed to open Goose SQLite store: {error}")))?;
145 let mut statement = connection
146 .prepare(
147 "SELECT id, name, working_dir, created_at, updated_at, session_type, \
148 extension_data, goose_mode, provider_name, model_config_json \
149 FROM sessions WHERE id = ?1",
150 )
151 .map_err(|error| Error::Other(format!("failed to query Goose sessions: {error}")))?;
152 let mut document = statement
153 .query_row([session_id], |row| {
154 let extension_data: Option<String> = row.get(6)?;
155 let model_config: Option<String> = row.get(9)?;
156 Ok(serde_json::json!({
157 "id": row.get::<_, String>(0)?,
158 "working_dir": row.get::<_, String>(2)?,
159 "name": row.get::<_, String>(1)?,
160 "user_set_name": false,
161 "session_type": row.get::<_, String>(5)?,
162 "created_at": row.get::<_, String>(3)?,
163 "updated_at": row.get::<_, String>(4)?,
164 "extension_data": extension_data
165 .as_deref()
166 .and_then(|value| serde_json::from_str::<Value>(value).ok())
167 .unwrap_or_else(|| serde_json::json!({})),
168 "usage": {},
169 "accumulated_usage": {},
170 "accumulated_cost": Value::Null,
171 "schedule_id": Value::Null,
172 "recipe": Value::Null,
173 "user_recipe_values": Value::Null,
174 "conversation": [],
175 "message_count": 0,
176 "last_message_at": Value::Null,
177 "provider_name": row.get::<_, Option<String>>(8)?,
178 "model_config": model_config
179 .as_deref()
180 .and_then(|value| serde_json::from_str::<Value>(value).ok()),
181 "goose_mode": row.get::<_, String>(7)?,
182 "archived_at": Value::Null,
183 "project_id": Value::Null,
184 "parent_session_id": Value::Null,
185 "last_message_snippet": Value::Null,
186 }))
187 })
188 .map_err(|error| Error::Other(format!("failed to load Goose session: {error}")))?;
189
190 let message_query = message_limit.map_or_else(
191 || {
192 "SELECT message_id, role, content_json, created_timestamp, metadata_json \
193 FROM messages WHERE session_id = ?1 ORDER BY created_timestamp, id"
194 .to_string()
195 },
196 |limit| {
197 format!(
198 "SELECT message_id, role, content_json, created_timestamp, metadata_json \
199 FROM (SELECT id AS native_row_id, message_id, role, content_json, \
200 created_timestamp, metadata_json \
201 FROM messages WHERE session_id = ?1 \
202 ORDER BY created_timestamp DESC, id DESC LIMIT {limit}) \
203 ORDER BY created_timestamp, native_row_id"
204 )
205 },
206 );
207 let mut message_statement = connection
208 .prepare(&message_query)
209 .map_err(|error| Error::Other(format!("failed to query Goose messages: {error}")))?;
210 let rows = message_statement
211 .query_map([session_id], |row| {
212 let content: String = row.get(2)?;
213 let metadata: Option<String> = row.get(4)?;
214 Ok(serde_json::json!({
215 "id": row.get::<_, Option<String>>(0)?,
216 "role": row.get::<_, String>(1)?,
217 "created": row.get::<_, i64>(3)?,
218 "content": serde_json::from_str::<Value>(&content)
219 .unwrap_or_else(|_| Value::Array(Vec::new())),
220 "metadata": metadata
221 .as_deref()
222 .and_then(|value| serde_json::from_str::<Value>(value).ok())
223 .unwrap_or_else(|| serde_json::json!({
224 "userVisible": true,
225 "agentVisible": true
226 })),
227 }))
228 })
229 .map_err(|error| Error::Other(format!("failed to load Goose messages: {error}")))?;
230 let conversation = rows
231 .collect::<std::result::Result<Vec<_>, _>>()
232 .map_err(|error| Error::Other(format!("failed to decode Goose messages: {error}")))?;
233 document["message_count"] = Value::from(conversation.len());
234 document["conversation"] = Value::Array(conversation);
235 let json = serde_json::to_string_pretty(&document).map_err(crate::Error::Decode)?;
236 let mut session = Self::from_goose_str(&json)?;
237 session.raw_is_verbatim = false;
239 Ok(session)
240 }
241}
242
243fn normalize_goose_message(native: &Value, native_index: usize, out: &mut Vec<ChatMessage>) {
244 let role = match native.get("role").and_then(Value::as_str) {
245 Some("assistant") => Role::Assistant,
246 _ => Role::User,
247 };
248 let created = native.get("created").and_then(Value::as_i64);
249 let native_id = native.get("id").and_then(Value::as_str);
250 let mut text = Vec::new();
251 let mut content_parts = Vec::new();
252 let mut tool_calls = Vec::new();
253 let mut tool_results = Vec::new();
254
255 for (block_index, block) in native
256 .get("content")
257 .and_then(Value::as_array)
258 .into_iter()
259 .flatten()
260 .enumerate()
261 {
262 match block.get("type").and_then(Value::as_str) {
263 Some("text") => {
264 if let Some(value) = block.get("text").and_then(Value::as_str) {
265 text.push(value.to_string());
266 content_parts.push(serde_json::json!({"type": "text", "text": value}));
267 }
268 }
269 Some("image") => {
270 let data = block
271 .get("data")
272 .and_then(Value::as_str)
273 .unwrap_or_default();
274 let media_type = block
275 .get("mimeType")
276 .or_else(|| block.get("mime_type"))
277 .and_then(Value::as_str)
278 .unwrap_or("application/octet-stream");
279 content_parts.push(serde_json::json!({
280 "type": "image_url",
281 "image_url": {"url": format!("data:{media_type};base64,{data}")},
282 }));
283 }
284 Some("toolRequest" | "frontendToolRequest") => {
285 let id = block
286 .get("id")
287 .and_then(Value::as_str)
288 .map(str::to_string)
289 .unwrap_or_else(|| format!("goose-{native_index}-{block_index}"));
290 let call = block
291 .get("toolCall")
292 .and_then(|call| {
293 (call.get("status").and_then(Value::as_str) == Some("success"))
294 .then(|| call.get("value"))
295 .flatten()
296 })
297 .or_else(|| block.get("toolCall"));
298 let Some(call) = call else { continue };
299 let name = call.get("name").and_then(Value::as_str).unwrap_or("tool");
300 let arguments = call
301 .get("arguments")
302 .map(value_to_arg_string)
303 .unwrap_or_else(|| "{}".to_string());
304 tool_calls.push(function_call(&id, name, arguments));
305 }
306 Some("toolResponse") => tool_results.push(block.clone()),
307 _ => {}
308 }
309 }
310
311 if !text.is_empty() || !content_parts.is_empty() || !tool_calls.is_empty() {
312 let has_non_text = content_parts
313 .iter()
314 .any(|part| part.get("type").and_then(Value::as_str) != Some("text"));
315 let mut message = ChatMessage {
316 role,
317 content: (!text.is_empty()).then(|| text.join("\n")),
318 content_parts: has_non_text.then_some(content_parts),
319 tool_calls: (!tool_calls.is_empty()).then_some(tool_calls),
320 tool_call_id: None,
321 name: None,
322 metadata: Default::default(),
323 };
324 capture_goose_message_metadata(native, created, native_id, &mut message);
325 out.push(message);
326 }
327
328 for (result_index, block) in tool_results.into_iter().enumerate() {
329 let id = block
330 .get("id")
331 .and_then(Value::as_str)
332 .map(str::to_string)
333 .unwrap_or_else(|| format!("goose-{native_index}-result-{result_index}"));
334 let result = block.get("toolResult").unwrap_or(&Value::Null);
335 let status_error = result.get("status").and_then(Value::as_str) == Some("error");
336 let value = result.get("value").unwrap_or(result);
337 let is_error = status_error || value.get("isError").and_then(Value::as_bool) == Some(true);
338 let output = if status_error {
339 result
340 .get("error")
341 .and_then(Value::as_str)
342 .unwrap_or("Goose tool call failed")
343 .to_string()
344 } else {
345 value
346 .get("content")
347 .and_then(Value::as_array)
348 .map(|content| {
349 content
350 .iter()
351 .filter_map(|part| {
352 part.get("text")
353 .and_then(Value::as_str)
354 .map(str::to_string)
355 .or_else(|| Some(part.to_string()))
356 })
357 .collect::<Vec<_>>()
358 .join("\n")
359 })
360 .unwrap_or_else(|| value.to_string())
361 };
362 let mut message = tool_message(&id, output);
363 if is_error {
364 crate::mark_tool_error(&mut message);
365 }
366 capture_goose_message_metadata(native, created, native_id, &mut message);
367 out.push(message);
368 }
369}
370
371fn capture_goose_message_metadata(
372 native: &Value,
373 created: Option<i64>,
374 native_id: Option<&str>,
375 message: &mut ChatMessage,
376) {
377 if let Some(created) = created {
378 message
379 .metadata
380 .insert("goose_created".to_string(), created.to_string());
381 }
382 if let Some(native_id) = native_id {
383 message
384 .metadata
385 .insert("goose_message_id".to_string(), native_id.to_string());
386 }
387 if let Some(metadata) = native.get("metadata") {
388 message
389 .metadata
390 .insert("goose_metadata".to_string(), metadata.to_string());
391 }
392}
393
394impl Session {
395 pub(super) fn to_goose_json(&self) -> String {
398 if self.meta.source == SessionSource::Goose
399 && !self.raw.is_empty()
400 && self.imported_message_count == Some(self.messages.len())
401 {
402 return join_lines_verbatim(&self.raw, self.raw_trailing_newline);
403 }
404 self.synthesized_goose_document(None, &self.messages)
405 }
406
407 pub(super) fn to_goose_json_spliced(&self, session_id: Option<&str>) -> String {
408 let message_prefix_len = self
409 .imported_message_count
410 .unwrap_or(self.messages.len())
411 .min(self.messages.len());
412 if self.meta.source == SessionSource::Goose && !self.raw.is_empty() {
413 if session_id.is_none() && message_prefix_len == self.messages.len() {
414 return join_lines_verbatim(&self.raw, self.raw_trailing_newline);
415 }
416 let source = join_lines_verbatim(&self.raw, self.raw_trailing_newline);
417 if let Ok(mut document) = serde_json::from_str::<Value>(&source) {
418 if let Some(session_id) = session_id {
419 document["id"] = Value::String(session_id.to_string());
420 }
421 let appended = self.goose_conversation(&self.messages[message_prefix_len..]);
422 if let Some(conversation) = document
423 .get_mut("conversation")
424 .and_then(Value::as_array_mut)
425 {
426 conversation.extend(appended);
427 document["message_count"] = Value::from(conversation.len());
428 }
429 return serde_json::to_string_pretty(&document).unwrap_or_else(|_| {
430 self.synthesized_goose_document(session_id, &self.messages)
431 });
432 }
433 }
434 self.synthesized_goose_document(session_id, &self.messages)
435 }
436
437 fn synthesized_goose_document(
438 &self,
439 session_id: Option<&str>,
440 messages: &[ChatMessage],
441 ) -> String {
442 let mut document = self
443 .meta
444 .goose_header
445 .clone()
446 .or_else(|| {
447 self.messages.iter().find_map(|message| {
448 message
449 .metadata
450 .get("goose_session_header")
451 .and_then(|value| serde_json::from_str(value).ok())
452 })
453 })
454 .unwrap_or_else(|| {
455 serde_json::json!({
456 "id": "supercode-goose-session",
457 "working_dir": self.cwd_string(),
458 "name": "supercode export",
459 "user_set_name": false,
460 "session_type": "user",
461 "created_at": SYNTH_TS,
462 "updated_at": SYNTH_TS,
463 "extension_data": {},
464 "usage": {},
465 "accumulated_usage": {},
466 "accumulated_cost": Value::Null,
467 "schedule_id": Value::Null,
468 "recipe": Value::Null,
469 "user_recipe_values": Value::Null,
470 "message_count": 0,
471 "last_message_at": Value::Null,
472 "provider_name": Value::Null,
473 "model_config": Value::Null,
474 "goose_mode": "auto",
475 "archived_at": Value::Null,
476 "project_id": Value::Null,
477 "parent_session_id": Value::Null,
478 "last_message_snippet": Value::Null,
479 })
480 });
481 document["id"] = Value::String(
482 session_id
483 .map(str::to_string)
484 .or_else(|| self.meta.session_id.clone())
485 .unwrap_or_else(|| "supercode-goose-session".to_string()),
486 );
487 document["working_dir"] = Value::String(self.cwd_string());
488 let conversation = self.goose_conversation(messages);
489 document["message_count"] = Value::from(conversation.len());
490 document["conversation"] = Value::Array(conversation);
491 serde_json::to_string_pretty(&document).unwrap_or_else(|_| "{}".to_string())
492 }
493
494 fn goose_conversation(&self, messages: &[ChatMessage]) -> Vec<Value> {
495 let mut out = Vec::new();
496 let mut last_native_index: Option<String> = None;
497 let mut tool_names = HashMap::<String, String>::new();
498 for (index, message) in messages.iter().enumerate() {
499 if is_replay_excluded(message) {
500 continue;
501 }
502 if let Some(native_index) = message.metadata.get("goose_native_index") {
503 if last_native_index.as_ref() == Some(native_index) {
504 continue;
505 }
506 last_native_index = Some(native_index.clone());
507 if let Some(native) = message
508 .metadata
509 .get("goose_native_message")
510 .and_then(|value| serde_json::from_str::<Value>(value).ok())
511 {
512 out.push(native);
513 continue;
514 }
515 } else {
516 last_native_index = None;
517 }
518
519 for call in message.tool_calls() {
520 tool_names.insert(call.id.clone(), call.function.name.clone());
521 }
522 let created = message
523 .metadata
524 .get("goose_created")
525 .and_then(|value| value.parse::<i64>().ok())
526 .unwrap_or(SYNTH_TS_MS / 1000 + index as i64);
527 let role = match message.role {
528 Role::Assistant => "assistant",
529 _ => "user",
530 };
531 let mut content = Vec::new();
532 if message.role != Role::Tool {
536 if let Some(text) = &message.content {
537 let text = if message.role == Role::System {
538 format!("[System] {text}")
539 } else {
540 text.clone()
541 };
542 content.push(serde_json::json!({"type": "text", "text": text}));
543 }
544 if let Some(parts) = &message.content_parts {
545 for part in parts {
546 if let Some(text) = part.get("text").and_then(Value::as_str) {
547 if message.content.is_none() {
548 content.push(serde_json::json!({"type": "text", "text": text}));
549 }
550 }
551 let Some(url) = part
552 .get("image_url")
553 .and_then(|image| image.get("url"))
554 .and_then(Value::as_str)
555 else {
556 continue;
557 };
558 let Some(data) = url.strip_prefix("data:") else {
559 continue;
560 };
561 let Some((media_type, data)) = data.split_once(";base64,") else {
562 continue;
563 };
564 content.push(serde_json::json!({
565 "type": "image",
566 "data": data,
567 "mimeType": media_type,
568 }));
569 }
570 }
571 }
572 for call in message.tool_calls() {
573 let arguments = serde_json::from_str::<Value>(&call.function.arguments)
574 .unwrap_or_else(|_| Value::String(call.function.arguments.clone()));
575 content.push(serde_json::json!({
576 "type": "toolRequest",
577 "id": call.id,
578 "toolCall": {
579 "status": "success",
580 "value": {"name": call.function.name, "arguments": arguments}
581 }
582 }));
583 }
584 if message.role == Role::Tool {
585 let id = message.tool_call_id.clone().unwrap_or_default();
586 let output = message.content.clone().unwrap_or_else(|| {
587 message
588 .content_parts
589 .as_ref()
590 .map(|parts| Value::Array(parts.clone()).to_string())
591 .unwrap_or_default()
592 });
593 let tool_result = if crate::is_tool_error(message) {
594 serde_json::json!({"status": "error", "error": output})
595 } else {
596 serde_json::json!({
597 "status": "success",
598 "value": {
599 "content": [{"type": "text", "text": output}],
600 "isError": false
601 }
602 })
603 };
604 content.push(serde_json::json!({
605 "type": "toolResponse",
606 "id": id,
607 "toolResult": tool_result,
608 "metadata": {
609 "toolName": message.name.as_ref()
610 .or_else(|| tool_names.get(&id))
611 }
612 }));
613 }
614 if content.is_empty() {
615 continue;
616 }
617 let metadata = message
618 .metadata
619 .get("goose_metadata")
620 .and_then(|value| serde_json::from_str::<Value>(value).ok())
621 .unwrap_or_else(|| {
622 serde_json::json!({
623 "userVisible": true,
624 "agentVisible": true
625 })
626 });
627 let mut native = serde_json::json!({
628 "id": message.metadata.get("goose_message_id")
629 .cloned().unwrap_or_else(|| format!("supercode-goose-{index}")),
630 "role": role,
631 "created": created,
632 "content": content,
633 "metadata": metadata,
634 });
635 set_grok_target_message_extension(&mut native, message);
640 out.push(native);
641 }
642 out
643 }
644}