use super::*;
impl Session {
pub fn from_goose(path: impl AsRef<Path>) -> Result<Session> {
Self::from_goose_str(&std::fs::read_to_string(path.as_ref())?)
}
pub fn from_goose_str(json: &str) -> Result<Session> {
let document: Value = serde_json::from_str(json).map_err(crate::Error::Decode)?;
let object = document.as_object().ok_or_else(|| {
Error::InvalidSession("Goose session export must be a JSON object".to_string())
})?;
let conversation = object
.get("conversation")
.and_then(Value::as_array)
.ok_or_else(|| {
Error::InvalidSession(
"Goose session export must contain a conversation array".to_string(),
)
})?;
let mut meta = SessionMeta::new(SessionSource::Goose);
meta.session_id = object.get("id").and_then(Value::as_str).map(str::to_string);
meta.cwd = object
.get("working_dir")
.or_else(|| object.get("workingDir"))
.and_then(Value::as_str)
.map(PathBuf::from);
meta.model = object
.get("model_config")
.or_else(|| object.get("modelConfig"))
.and_then(|model| model.get("model_name").or_else(|| model.get("modelName")))
.and_then(Value::as_str)
.map(str::to_string);
for (source, target) in [
("name", "session_name"),
("created_at", "created_at"),
("updated_at", "updated_at"),
("session_type", "goose_session_type"),
("goose_mode", "goose_mode"),
("provider_name", "goose_provider_name"),
("parent_session_id", "parent_session_id"),
] {
if let Some(value) = object.get(source) {
meta.lineage.insert(
target.to_string(),
value
.as_str()
.map(str::to_string)
.unwrap_or_else(|| value.to_string()),
);
}
}
let mut header = document.clone();
if let Some(header) = header.as_object_mut() {
header.remove("conversation");
}
meta.goose_header = Some(header.clone());
let mut messages = Vec::new();
for (native_index, native) in conversation.iter().enumerate() {
let before = messages.len();
normalize_goose_message(native, native_index, &mut messages);
if let Some(first) = messages.get_mut(before) {
first
.metadata
.insert("goose_native_message".to_string(), native.to_string());
first
.metadata
.insert("goose_native_index".to_string(), native_index.to_string());
if native_index == 0 {
first
.metadata
.insert("goose_session_header".to_string(), header.to_string());
}
restore_grok_message_extension(native, first);
}
for message in messages.iter_mut().skip(before + 1) {
message
.metadata
.insert("goose_native_index".to_string(), native_index.to_string());
}
}
ensure_tool_results_paired(&mut messages);
let (raw_lines, raw_trailing_newline) = split_lines_verbatim(json);
let raw = raw_lines.iter().map(|line| line.to_string()).collect();
let imported_message_count = Some(messages.len());
Ok(Session {
meta,
messages,
subagents: Vec::new(),
raw,
raw_trailing_newline,
imported_message_count,
raw_is_verbatim: true,
parse_error_lines: 0,
load_residue: Vec::new(),
})
}
pub fn from_goose_sqlite(db_path: &Path, session_id: &str) -> Result<Session> {
Self::from_goose_sqlite_with_limit(db_path, session_id, None)
}
#[doc(hidden)]
pub fn from_goose_sqlite_display(
db_path: &Path,
session_id: &str,
message_limit: usize,
) -> Result<Session> {
Self::from_goose_sqlite_with_limit(db_path, session_id, Some(message_limit.max(1)))
}
fn from_goose_sqlite_with_limit(
db_path: &Path,
session_id: &str,
message_limit: Option<usize>,
) -> Result<Session> {
let connection = Connection::open_with_flags(
db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|error| Error::Other(format!("failed to open Goose SQLite store: {error}")))?;
let mut statement = connection
.prepare(
"SELECT id, name, working_dir, created_at, updated_at, session_type, \
extension_data, goose_mode, provider_name, model_config_json \
FROM sessions WHERE id = ?1",
)
.map_err(|error| Error::Other(format!("failed to query Goose sessions: {error}")))?;
let mut document = statement
.query_row([session_id], |row| {
let extension_data: Option<String> = row.get(6)?;
let model_config: Option<String> = row.get(9)?;
Ok(serde_json::json!({
"id": row.get::<_, String>(0)?,
"working_dir": row.get::<_, String>(2)?,
"name": row.get::<_, String>(1)?,
"user_set_name": false,
"session_type": row.get::<_, String>(5)?,
"created_at": row.get::<_, String>(3)?,
"updated_at": row.get::<_, String>(4)?,
"extension_data": extension_data
.as_deref()
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.unwrap_or_else(|| serde_json::json!({})),
"usage": {},
"accumulated_usage": {},
"accumulated_cost": Value::Null,
"schedule_id": Value::Null,
"recipe": Value::Null,
"user_recipe_values": Value::Null,
"conversation": [],
"message_count": 0,
"last_message_at": Value::Null,
"provider_name": row.get::<_, Option<String>>(8)?,
"model_config": model_config
.as_deref()
.and_then(|value| serde_json::from_str::<Value>(value).ok()),
"goose_mode": row.get::<_, String>(7)?,
"archived_at": Value::Null,
"project_id": Value::Null,
"parent_session_id": Value::Null,
"last_message_snippet": Value::Null,
}))
})
.map_err(|error| Error::Other(format!("failed to load Goose session: {error}")))?;
let message_query = message_limit.map_or_else(
|| {
"SELECT message_id, role, content_json, created_timestamp, metadata_json \
FROM messages WHERE session_id = ?1 ORDER BY created_timestamp, id"
.to_string()
},
|limit| {
format!(
"SELECT message_id, role, content_json, created_timestamp, metadata_json \
FROM (SELECT id AS native_row_id, message_id, role, content_json, \
created_timestamp, metadata_json \
FROM messages WHERE session_id = ?1 \
ORDER BY created_timestamp DESC, id DESC LIMIT {limit}) \
ORDER BY created_timestamp, native_row_id"
)
},
);
let mut message_statement = connection
.prepare(&message_query)
.map_err(|error| Error::Other(format!("failed to query Goose messages: {error}")))?;
let rows = message_statement
.query_map([session_id], |row| {
let content: String = row.get(2)?;
let metadata: Option<String> = row.get(4)?;
Ok(serde_json::json!({
"id": row.get::<_, Option<String>>(0)?,
"role": row.get::<_, String>(1)?,
"created": row.get::<_, i64>(3)?,
"content": serde_json::from_str::<Value>(&content)
.unwrap_or_else(|_| Value::Array(Vec::new())),
"metadata": metadata
.as_deref()
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.unwrap_or_else(|| serde_json::json!({
"userVisible": true,
"agentVisible": true
})),
}))
})
.map_err(|error| Error::Other(format!("failed to load Goose messages: {error}")))?;
let conversation = rows
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|error| Error::Other(format!("failed to decode Goose messages: {error}")))?;
document["message_count"] = Value::from(conversation.len());
document["conversation"] = Value::Array(conversation);
let json = serde_json::to_string_pretty(&document).map_err(crate::Error::Decode)?;
let mut session = Self::from_goose_str(&json)?;
session.raw_is_verbatim = false;
Ok(session)
}
}
fn normalize_goose_message(native: &Value, native_index: usize, out: &mut Vec<ChatMessage>) {
let role = match native.get("role").and_then(Value::as_str) {
Some("assistant") => Role::Assistant,
_ => Role::User,
};
let created = native.get("created").and_then(Value::as_i64);
let native_id = native.get("id").and_then(Value::as_str);
let mut text = Vec::new();
let mut content_parts = Vec::new();
let mut tool_calls = Vec::new();
let mut tool_results = Vec::new();
for (block_index, block) in native
.get("content")
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
{
match block.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(value) = block.get("text").and_then(Value::as_str) {
text.push(value.to_string());
content_parts.push(serde_json::json!({"type": "text", "text": value}));
}
}
Some("image") => {
let data = block
.get("data")
.and_then(Value::as_str)
.unwrap_or_default();
let media_type = block
.get("mimeType")
.or_else(|| block.get("mime_type"))
.and_then(Value::as_str)
.unwrap_or("application/octet-stream");
content_parts.push(serde_json::json!({
"type": "image_url",
"image_url": {"url": format!("data:{media_type};base64,{data}")},
}));
}
Some("toolRequest" | "frontendToolRequest") => {
let id = block
.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("goose-{native_index}-{block_index}"));
let call = block
.get("toolCall")
.and_then(|call| {
(call.get("status").and_then(Value::as_str) == Some("success"))
.then(|| call.get("value"))
.flatten()
})
.or_else(|| block.get("toolCall"));
let Some(call) = call else { continue };
let name = call.get("name").and_then(Value::as_str).unwrap_or("tool");
let arguments = call
.get("arguments")
.map(value_to_arg_string)
.unwrap_or_else(|| "{}".to_string());
tool_calls.push(function_call(&id, name, arguments));
}
Some("toolResponse") => tool_results.push(block.clone()),
_ => {}
}
}
if !text.is_empty() || !content_parts.is_empty() || !tool_calls.is_empty() {
let has_non_text = content_parts
.iter()
.any(|part| part.get("type").and_then(Value::as_str) != Some("text"));
let mut message = ChatMessage {
role,
content: (!text.is_empty()).then(|| text.join("\n")),
content_parts: has_non_text.then_some(content_parts),
tool_calls: (!tool_calls.is_empty()).then_some(tool_calls),
tool_call_id: None,
name: None,
metadata: Default::default(),
};
capture_goose_message_metadata(native, created, native_id, &mut message);
out.push(message);
}
for (result_index, block) in tool_results.into_iter().enumerate() {
let id = block
.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("goose-{native_index}-result-{result_index}"));
let result = block.get("toolResult").unwrap_or(&Value::Null);
let status_error = result.get("status").and_then(Value::as_str) == Some("error");
let value = result.get("value").unwrap_or(result);
let is_error = status_error || value.get("isError").and_then(Value::as_bool) == Some(true);
let output = if status_error {
result
.get("error")
.and_then(Value::as_str)
.unwrap_or("Goose tool call failed")
.to_string()
} else {
value
.get("content")
.and_then(Value::as_array)
.map(|content| {
content
.iter()
.filter_map(|part| {
part.get("text")
.and_then(Value::as_str)
.map(str::to_string)
.or_else(|| Some(part.to_string()))
})
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_else(|| value.to_string())
};
let mut message = tool_message(&id, output);
if is_error {
crate::mark_tool_error(&mut message);
}
capture_goose_message_metadata(native, created, native_id, &mut message);
out.push(message);
}
}
fn capture_goose_message_metadata(
native: &Value,
created: Option<i64>,
native_id: Option<&str>,
message: &mut ChatMessage,
) {
if let Some(created) = created {
message
.metadata
.insert("goose_created".to_string(), created.to_string());
}
if let Some(native_id) = native_id {
message
.metadata
.insert("goose_message_id".to_string(), native_id.to_string());
}
if let Some(metadata) = native.get("metadata") {
message
.metadata
.insert("goose_metadata".to_string(), metadata.to_string());
}
}
impl Session {
pub(super) fn to_goose_json(&self) -> String {
if self.meta.source == SessionSource::Goose
&& !self.raw.is_empty()
&& self.imported_message_count == Some(self.messages.len())
{
return join_lines_verbatim(&self.raw, self.raw_trailing_newline);
}
self.synthesized_goose_document(None, &self.messages)
}
pub(super) fn to_goose_json_spliced(&self, session_id: Option<&str>) -> String {
let message_prefix_len = self
.imported_message_count
.unwrap_or(self.messages.len())
.min(self.messages.len());
if self.meta.source == SessionSource::Goose && !self.raw.is_empty() {
if session_id.is_none() && message_prefix_len == self.messages.len() {
return join_lines_verbatim(&self.raw, self.raw_trailing_newline);
}
let source = join_lines_verbatim(&self.raw, self.raw_trailing_newline);
if let Ok(mut document) = serde_json::from_str::<Value>(&source) {
if let Some(session_id) = session_id {
document["id"] = Value::String(session_id.to_string());
}
let appended = self.goose_conversation(&self.messages[message_prefix_len..]);
if let Some(conversation) = document
.get_mut("conversation")
.and_then(Value::as_array_mut)
{
conversation.extend(appended);
document["message_count"] = Value::from(conversation.len());
}
return serde_json::to_string_pretty(&document).unwrap_or_else(|_| {
self.synthesized_goose_document(session_id, &self.messages)
});
}
}
self.synthesized_goose_document(session_id, &self.messages)
}
fn synthesized_goose_document(
&self,
session_id: Option<&str>,
messages: &[ChatMessage],
) -> String {
let mut document = self
.meta
.goose_header
.clone()
.or_else(|| {
self.messages.iter().find_map(|message| {
message
.metadata
.get("goose_session_header")
.and_then(|value| serde_json::from_str(value).ok())
})
})
.unwrap_or_else(|| {
serde_json::json!({
"id": "supercode-goose-session",
"working_dir": self.cwd_string(),
"name": "supercode export",
"user_set_name": false,
"session_type": "user",
"created_at": SYNTH_TS,
"updated_at": SYNTH_TS,
"extension_data": {},
"usage": {},
"accumulated_usage": {},
"accumulated_cost": Value::Null,
"schedule_id": Value::Null,
"recipe": Value::Null,
"user_recipe_values": Value::Null,
"message_count": 0,
"last_message_at": Value::Null,
"provider_name": Value::Null,
"model_config": Value::Null,
"goose_mode": "auto",
"archived_at": Value::Null,
"project_id": Value::Null,
"parent_session_id": Value::Null,
"last_message_snippet": Value::Null,
})
});
document["id"] = Value::String(
session_id
.map(str::to_string)
.or_else(|| self.meta.session_id.clone())
.unwrap_or_else(|| "supercode-goose-session".to_string()),
);
document["working_dir"] = Value::String(self.cwd_string());
let conversation = self.goose_conversation(messages);
document["message_count"] = Value::from(conversation.len());
document["conversation"] = Value::Array(conversation);
serde_json::to_string_pretty(&document).unwrap_or_else(|_| "{}".to_string())
}
fn goose_conversation(&self, messages: &[ChatMessage]) -> Vec<Value> {
let mut out = Vec::new();
let mut last_native_index: Option<String> = None;
let mut tool_names = HashMap::<String, String>::new();
for (index, message) in messages.iter().enumerate() {
if is_replay_excluded(message) {
continue;
}
if let Some(native_index) = message.metadata.get("goose_native_index") {
if last_native_index.as_ref() == Some(native_index) {
continue;
}
last_native_index = Some(native_index.clone());
if let Some(native) = message
.metadata
.get("goose_native_message")
.and_then(|value| serde_json::from_str::<Value>(value).ok())
{
out.push(native);
continue;
}
} else {
last_native_index = None;
}
for call in message.tool_calls() {
tool_names.insert(call.id.clone(), call.function.name.clone());
}
let created = message
.metadata
.get("goose_created")
.and_then(|value| value.parse::<i64>().ok())
.unwrap_or(SYNTH_TS_MS / 1000 + index as i64);
let role = match message.role {
Role::Assistant => "assistant",
_ => "user",
};
let mut content = Vec::new();
if message.role != Role::Tool {
if let Some(text) = &message.content {
let text = if message.role == Role::System {
format!("[System] {text}")
} else {
text.clone()
};
content.push(serde_json::json!({"type": "text", "text": text}));
}
if let Some(parts) = &message.content_parts {
for part in parts {
if let Some(text) = part.get("text").and_then(Value::as_str) {
if message.content.is_none() {
content.push(serde_json::json!({"type": "text", "text": text}));
}
}
let Some(url) = part
.get("image_url")
.and_then(|image| image.get("url"))
.and_then(Value::as_str)
else {
continue;
};
let Some(data) = url.strip_prefix("data:") else {
continue;
};
let Some((media_type, data)) = data.split_once(";base64,") else {
continue;
};
content.push(serde_json::json!({
"type": "image",
"data": data,
"mimeType": media_type,
}));
}
}
}
for call in message.tool_calls() {
let arguments = serde_json::from_str::<Value>(&call.function.arguments)
.unwrap_or_else(|_| Value::String(call.function.arguments.clone()));
content.push(serde_json::json!({
"type": "toolRequest",
"id": call.id,
"toolCall": {
"status": "success",
"value": {"name": call.function.name, "arguments": arguments}
}
}));
}
if message.role == Role::Tool {
let id = message.tool_call_id.clone().unwrap_or_default();
let output = message.content.clone().unwrap_or_else(|| {
message
.content_parts
.as_ref()
.map(|parts| Value::Array(parts.clone()).to_string())
.unwrap_or_default()
});
let tool_result = if crate::is_tool_error(message) {
serde_json::json!({"status": "error", "error": output})
} else {
serde_json::json!({
"status": "success",
"value": {
"content": [{"type": "text", "text": output}],
"isError": false
}
})
};
content.push(serde_json::json!({
"type": "toolResponse",
"id": id,
"toolResult": tool_result,
"metadata": {
"toolName": message.name.as_ref()
.or_else(|| tool_names.get(&id))
}
}));
}
if content.is_empty() {
continue;
}
let metadata = message
.metadata
.get("goose_metadata")
.and_then(|value| serde_json::from_str::<Value>(value).ok())
.unwrap_or_else(|| {
serde_json::json!({
"userVisible": true,
"agentVisible": true
})
});
let mut native = serde_json::json!({
"id": message.metadata.get("goose_message_id")
.cloned().unwrap_or_else(|| format!("supercode-goose-{index}")),
"role": role,
"created": created,
"content": content,
"metadata": metadata,
});
set_grok_target_message_extension(&mut native, message);
out.push(native);
}
out
}
}