use crate::context::ContextReader;
use crate::context::ir::{IrMessage, IrPart, build_tool_call, build_tool_result, extract_text};
use crate::message::{
AssistantResponse, Context, ContextListing, ConversationMessage, Entry, MessageKind,
TextContent,
};
use anyhow::Context as _;
use rusqlite::Connection;
use serde_json::Value;
use std::path::PathBuf;
pub struct GooseReader {
db_path: PathBuf,
}
impl GooseReader {
pub fn new(db_path: PathBuf) -> Self {
Self { db_path }
}
}
impl ContextReader for GooseReader {
fn list_contexts(&self) -> anyhow::Result<Vec<ContextListing>> {
let conn = Connection::open(&self.db_path)
.with_context(|| format!("failed to open goose db at {}", self.db_path.display()))?;
let sql = "SELECT s.id, s.name, s.working_dir, COUNT(m.id) \
FROM sessions s LEFT JOIN messages m ON m.session_id = s.id \
GROUP BY s.id ORDER BY s.updated_at DESC";
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, i64>(3)?,
))
})?;
let mut contexts = Vec::new();
for row in rows {
let (id, name, working_dir, count) = row?;
let detail = if name.is_empty() {
format!("{count} messages, {working_dir}")
} else {
format!("{name}: {count} messages, {working_dir}")
};
contexts.push(ContextListing { id, detail });
}
Ok(contexts)
}
fn delete_context(&self, context_id: &str) -> anyhow::Result<()> {
let mut conn = Connection::open(&self.db_path)
.with_context(|| format!("failed to open goose db at {}", self.db_path.display()))?;
let tx = conn.transaction()?;
tx.execute(
"DELETE FROM messages WHERE session_id = ?1",
rusqlite::params![context_id],
)?;
tx.execute(
"DELETE FROM sessions WHERE id = ?1",
rusqlite::params![context_id],
)?;
tx.commit()?;
Ok(())
}
fn read_context(&self, context_id: &str) -> anyhow::Result<Context> {
let conn = Connection::open(&self.db_path)
.with_context(|| format!("failed to open goose db at {}", self.db_path.display()))?;
let cwd: Option<String> = conn
.query_row(
"SELECT working_dir FROM sessions WHERE id = ?1",
rusqlite::params![context_id],
|row| row.get::<_, String>(0),
)
.ok()
.filter(|dir| !dir.is_empty());
let sql = "SELECT id, role, content_json FROM messages \
WHERE session_id = ?1 ORDER BY created_timestamp, id";
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map(rusqlite::params![context_id], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})?;
let mut entries = Vec::new();
let mut messages = Vec::new();
let mut prev_entry_id = String::new();
for row in rows {
let (id, role, content_str) = row?;
let blocks: Value = serde_json::from_str(&content_str)
.with_context(|| format!("goose: failed to parse content_json for message {id}"))?;
let entry_id = format!("goose-{id}");
let parent_id = prev_entry_id.clone();
let kind = goose_build_message_kind(
&role,
blocks.as_array().map_or(&[] as &[Value], |a| a.as_slice()),
);
messages.push(ConversationMessage::new(entry_id.clone(), kind));
entries.push(Entry {
id: entry_id.clone(),
parent_id,
});
prev_entry_id = entry_id;
}
Ok(Context {
entries,
messages,
cwd,
})
}
}
fn goose_build_message_kind(role: &str, blocks: &[Value]) -> MessageKind {
let array_value = Value::Array(blocks.to_vec());
match role {
"user" => {
for block in blocks {
if block["type"].as_str() == Some("toolResponse") {
let call_id = block["id"].as_str().unwrap_or("");
let tool_name = block["toolResult"]["value"]["name"]
.as_str()
.unwrap_or("tool")
.to_string();
let content_value = &block["toolResult"]["value"]["content"];
let content = extract_text(Some(content_value));
let is_error = block["toolResult"]["status"].as_str() == Some("error");
return MessageKind::ToolResultData(build_tool_result(
call_id, tool_name, content, is_error,
));
}
}
MessageKind::TextContent(TextContent {
role: "user".to_string(),
text: extract_text(Some(&array_value)),
})
}
"assistant" => {
let mut ir = IrMessage::new();
for block in blocks {
match block["type"].as_str() {
Some("text") => {
ir.push(IrPart::Text(
block["text"].as_str().unwrap_or("").to_string(),
));
}
Some("thinking") => {
if let Some(t) = block["thinking"].as_str() {
ir.push(IrPart::Thinking(t.to_string()));
}
}
Some("toolRequest") => {
if let Some(tc) = block.get("toolCall") {
let id = block["id"].as_str().unwrap_or("");
let name = tc["value"]["name"].as_str().unwrap_or("");
let arguments = tc["value"].get("arguments");
ir.push(IrPart::ToolCall(build_tool_call(id, name, arguments)));
}
}
_ => {}
}
}
MessageKind::AssistantResponse(AssistantResponse {
thinking: ir.thinking(),
tool_calls: ir.tool_calls(),
text: ir.text(),
})
}
_ => MessageKind::TextContent(TextContent {
role: role.to_string(),
text: extract_text(Some(&array_value)),
}),
}
}