Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::session::export_session,
super::routes::session::import_session,
super::routes::session::update_session_user_recipe_values,
super::routes::session::edit_message,
super::routes::schedule::create_schedule,
super::routes::schedule::list_schedules,
super::routes::schedule::delete_schedule,
Expand Down Expand Up @@ -405,6 +406,9 @@ derive_utoipa!(Icon as IconSchema);
super::routes::session::UpdateSessionNameRequest,
super::routes::session::UpdateSessionUserRecipeValuesRequest,
super::routes::session::UpdateSessionUserRecipeValuesResponse,
super::routes::session::EditType,
super::routes::session::EditMessageRequest,
super::routes::session::EditMessageResponse,
Message,
MessageContent,
MessageMetadata,
Expand Down
84 changes: 84 additions & 0 deletions crates/goose-server/src/routes/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ pub struct ImportSessionRequest {
json: String,
}

#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum EditType {
Fork,
Edit,
}

#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct EditMessageRequest {
timestamp: i64,
#[serde(default = "default_edit_type")]
edit_type: EditType,
}

fn default_edit_type() -> EditType {
EditType::Fork
}

#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct EditMessageResponse {
session_id: String,
}

const MAX_NAME_LENGTH: usize = 200;

#[utoipa::path(
Expand Down Expand Up @@ -307,6 +332,64 @@ async fn import_session(
Ok(Json(session))
}

#[utoipa::path(
post,
path = "/sessions/{session_id}/edit_message",
request_body = EditMessageRequest,
params(
("session_id" = String, Path, description = "Unique identifier for the session")
),
responses(
(status = 200, description = "Session prepared for editing - frontend should submit the edited message", body = EditMessageResponse),
(status = 400, description = "Bad request - Invalid message timestamp"),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Session or message not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Session Management"
)]
async fn edit_message(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here you'd just want two simple primitive; copy which takes a session id and returns a new one with an id and truncate which takes a session id and a message id and then deletes all messages including and after that message id

Path(session_id): Path<String>,
Json(request): Json<EditMessageRequest>,
) -> Result<Json<EditMessageResponse>, StatusCode> {
match request.edit_type {
EditType::Fork => {
let new_session = SessionManager::copy_session(&session_id, "(edited)".to_string())
.await
.map_err(|e| {
tracing::error!("Failed to copy session: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;

SessionManager::truncate_conversation(&new_session.id, request.timestamp)
.await
.map_err(|e| {
tracing::error!("Failed to truncate conversation: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;

Ok(Json(EditMessageResponse {
session_id: new_session.id,
}))
}
EditType::Edit => {
SessionManager::truncate_conversation(&session_id, request.timestamp)
.await
.map_err(|e| {
tracing::error!("Failed to truncate conversation: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;

Ok(Json(EditMessageResponse {
session_id: session_id.clone(),
}))
}
}
}

pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/sessions", get(list_sessions))
Expand All @@ -320,5 +403,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
"/sessions/{session_id}/user_recipe_values",
put(update_session_user_recipe_values),
)
.route("/sessions/{session_id}/edit_message", post(edit_message))
.with_state(state)
}
2 changes: 1 addition & 1 deletion crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,8 +786,8 @@ impl Agent {
});

SessionManager::add_message(&session_config.id, &user_message).await?;
let session = SessionManager::get_session(&session_config.id, true).await?;

let session = SessionManager::get_session(&session_config.id, true).await?;
let conversation = session
.conversation
.clone()
Expand Down
51 changes: 51 additions & 0 deletions crates/goose/src/session/session_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,20 @@ impl SessionManager {
Self::instance().await?.import_session(json).await
}

pub async fn copy_session(session_id: &str, new_name: String) -> Result<Session> {
Self::instance()
.await?
.copy_session(session_id, new_name)
.await
}

pub async fn truncate_conversation(session_id: &str, timestamp: i64) -> Result<()> {
Self::instance()
.await?
.truncate_conversation(session_id, timestamp)
.await
}

pub async fn maybe_update_name(id: &str, provider: Arc<dyn Provider>) -> Result<()> {
let session = Self::get_session(id, true).await?;

Expand Down Expand Up @@ -1137,6 +1151,43 @@ impl SessionStorage {
self.get_session(&session.id, true).await
}

async fn copy_session(&self, session_id: &str, new_name: String) -> Result<Session> {
let original_session = self.get_session(session_id, true).await?;

let new_session = self
.create_session(
original_session.working_dir.clone(),
new_name,
original_session.session_type,
)
.await?;

let builder = SessionUpdateBuilder::new(new_session.id.clone())
.extension_data(original_session.extension_data)
.schedule_id(original_session.schedule_id)
.recipe(original_session.recipe)
.user_recipe_values(original_session.user_recipe_values);

self.apply_update(builder).await?;

if let Some(conversation) = original_session.conversation {
self.replace_conversation(&new_session.id, &conversation)
.await?;
}

self.get_session(&new_session.id, true).await
}

async fn truncate_conversation(&self, session_id: &str, timestamp: i64) -> Result<()> {
sqlx::query("DELETE FROM messages WHERE session_id = ? AND created_timestamp >= ?")
.bind(session_id)
.bind(timestamp)
.execute(&self.pool)
.await?;

Ok(())
}

async fn search_chat_history(
&self,
query: &str,
Expand Down
91 changes: 91 additions & 0 deletions ui/desktop/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1916,6 +1916,64 @@
]
}
},
"/sessions/{session_id}/edit_message": {
"post": {
"tags": [
"Session Management"
],
"operationId": "edit_message",
"parameters": [
{
"name": "session_id",
"in": "path",
"description": "Unique identifier for the session",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EditMessageRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Session prepared for editing - frontend should submit the edited message",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EditMessageResponse"
}
}
}
},
"400": {
"description": "Bad request - Invalid message timestamp"
},
"401": {
"description": "Unauthorized - Invalid or missing API key"
},
"404": {
"description": "Session or message not found"
},
"500": {
"description": "Internal server error"
}
},
"security": [
{
"api_key": []
}
]
}
},
"/sessions/{session_id}/export": {
"get": {
"tags": [
Expand Down Expand Up @@ -2448,6 +2506,39 @@
}
}
},
"EditMessageRequest": {
"type": "object",
"required": [
"timestamp"
],
"properties": {
"editType": {
"$ref": "#/components/schemas/EditType"
},
"timestamp": {
"type": "integer",
"format": "int64"
}
}
},
"EditMessageResponse": {
"type": "object",
"required": [
"sessionId"
],
"properties": {
"sessionId": {
"type": "string"
}
}
},
"EditType": {
"type": "string",
"enum": [
"fork",
"edit"
]
},
"EmbeddedResource": {
"type": "object",
"required": [
Expand Down
1 change: 1 addition & 0 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const PairRouteWrapper = ({

return (
<Pair
key={sessionId}
setChat={setChat}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
sessionId={sessionId}
Expand Down
13 changes: 12 additions & 1 deletion ui/desktop/src/api/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';

export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
Expand Down Expand Up @@ -522,6 +522,17 @@ export const getSession = <ThrowOnError extends boolean = false>(options: Option
});
};

export const editMessage = <ThrowOnError extends boolean = false>(options: Options<EditMessageData, ThrowOnError>) => {
return (options.client ?? client).post<EditMessageResponses, EditMessageErrors, ThrowOnError>({
url: '/sessions/{session_id}/edit_message',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};

export const exportSession = <ThrowOnError extends boolean = false>(options: Options<ExportSessionData, ThrowOnError>) => {
return (options.client ?? client).get<ExportSessionResponses, ExportSessionErrors, ThrowOnError>({
url: '/sessions/{session_id}/export',
Expand Down
Loading