diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index c677d03796fc..53015d940432 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -385,6 +385,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { super::routes::context::manage_context, super::routes::session::list_sessions, super::routes::session::get_session_history, + super::routes::session::update_session_recipe_parameters, super::routes::schedule::create_schedule, super::routes::schedule::list_schedules, super::routes::schedule::delete_schedule, @@ -420,6 +421,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema { super::routes::context::ContextManageResponse, super::routes::session::SessionListResponse, super::routes::session::SessionHistoryResponse, + super::routes::session::UpdateSessionRecipeParametersRequest, Message, MessageContent, MessageMetadata, diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 1c546d5d66f1..44d31a306d43 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -125,6 +125,7 @@ async fn start_agent( accumulated_output_tokens: Some(0), extension_data: Default::default(), recipe: payload.recipe, + recipe_parameters: None, }; let session_path = match session::get_path(session::Identifier::Name(session_id.clone())) { diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 9109d855d0dc..f17b47baa53d 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -42,6 +42,13 @@ pub struct UpdateSessionMetadataRequest { description: String, } +#[derive(Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateSessionRecipeParametersRequest { + /// Recipe parameter values entered by the user + recipe_parameters: HashMap, +} + const MAX_DESCRIPTION_LENGTH: usize = 200; #[derive(Serialize, ToSchema, Debug)] @@ -300,6 +307,46 @@ async fn update_session_metadata( Ok(StatusCode::OK) } +#[utoipa::path( + put, + path = "/sessions/{session_id}/recipe_parameters", + request_body = UpdateSessionRecipeParametersRequest, + params( + ("session_id" = String, Path, description = "Unique identifier for the session") + ), + responses( + (status = 200, description = "Session recipe parameters updated successfully"), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 404, description = "Session not found"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Session Management" +)] +// Update session recipe parameters +async fn update_session_recipe_parameters( + Path(session_id): Path, + Json(request): Json, +) -> Result { + let session_path = session::get_path(session::Identifier::Name(session_id.clone())) + .map_err(|_| StatusCode::BAD_REQUEST)?; + + // Read current metadata + let mut metadata = session::read_metadata(&session_path).map_err(|_| StatusCode::NOT_FOUND)?; + + // Update recipe parameters + metadata.recipe_parameters = Some(request.recipe_parameters); + + // Save updated metadata + session::update_metadata(&session_path, &metadata) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(StatusCode::OK) +} + #[utoipa::path( delete, path = "/sessions/{session_id}/delete", @@ -347,6 +394,10 @@ pub fn routes(state: Arc) -> Router { "/sessions/{session_id}/metadata", put(update_session_metadata), ) + .route( + "/sessions/{session_id}/recipe_parameters", + put(update_session_recipe_parameters), + ) .with_state(state) } diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 48b81d3e36cf..845123782b0b 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -1300,6 +1300,7 @@ async fn run_scheduled_job_internal( accumulated_output_tokens: None, extension_data: crate::session::ExtensionData::new(), recipe: None, + recipe_parameters: None, }; if let Err(e_fb) = crate::session::storage::save_messages_with_metadata( &session_file_path, diff --git a/crates/goose/src/session/storage.rs b/crates/goose/src/session/storage.rs index 66f761469198..d105405c858f 100644 --- a/crates/goose/src/session/storage.rs +++ b/crates/goose/src/session/storage.rs @@ -72,6 +72,8 @@ pub struct SessionMetadata { pub extension_data: ExtensionData, pub recipe: Option, + /// Recipe parameter values entered by the user + pub recipe_parameters: Option>, } // Custom deserializer to handle old sessions without working_dir @@ -95,6 +97,7 @@ impl<'de> Deserialize<'de> for SessionMetadata { #[serde(default)] extension_data: ExtensionData, recipe: Option, + recipe_parameters: Option>, } let helper = Helper::deserialize(deserializer)?; @@ -118,6 +121,7 @@ impl<'de> Deserialize<'de> for SessionMetadata { working_dir, extension_data: helper.extension_data, recipe: helper.recipe, + recipe_parameters: helper.recipe_parameters, }) } } @@ -144,6 +148,7 @@ impl SessionMetadata { accumulated_output_tokens: None, extension_data: ExtensionData::new(), recipe: None, + recipe_parameters: None, } } } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 9764466c2f25..4fc2ce9384a5 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1545,6 +1545,54 @@ ] } }, + "/sessions/{session_id}/recipe_parameters": { + "put": { + "tags": [ + "Session Management" + ], + "operationId": "update_session_recipe_parameters", + "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/UpdateSessionRecipeParametersRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Session recipe parameters updated successfully" + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "404": { + "description": "Session not found" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/status": { "get": { "tags": [ @@ -3606,6 +3654,14 @@ ], "nullable": true }, + "recipe_parameters": { + "type": "object", + "description": "Recipe parameter values entered by the user", + "additionalProperties": { + "type": "string" + }, + "nullable": true + }, "schedule_id": { "type": "string", "description": "ID of the schedule that triggered this session, if any", @@ -4007,6 +4063,21 @@ } } }, + "UpdateSessionRecipeParametersRequest": { + "type": "object", + "required": [ + "recipeParameters" + ], + "properties": { + "recipeParameters": { + "type": "object", + "description": "Recipe parameter values entered by the user", + "additionalProperties": { + "type": "string" + } + } + } + }, "UpsertConfigQuery": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 39ac4c781667..1e412c4e0a71 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from './client'; -import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors, StatusData, StatusResponses } from './types.gen'; +import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors, UpdateSessionRecipeParametersData, UpdateSessionRecipeParametersResponses, UpdateSessionRecipeParametersErrors, StatusData, StatusResponses } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -431,6 +431,17 @@ export const getSessionHistory = (options: }); }; +export const updateSessionRecipeParameters = (options: Options) => { + return (options.client ?? _heyApiClient).put({ + url: '/sessions/{session_id}/recipe_parameters', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + export const status = (options?: Options) => { return (options?.client ?? _heyApiClient).get({ url: '/status', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 98591036a965..5a1471884425 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -774,6 +774,12 @@ export type SessionMetadata = { */ output_tokens?: number | null; recipe?: Recipe | null; + /** + * Recipe parameter values entered by the user + */ + recipe_parameters?: { + [key: string]: string; + } | null; /** * ID of the schedule that triggered this session, if any */ @@ -926,6 +932,15 @@ export type UpdateScheduleRequest = { cron: string; }; +export type UpdateSessionRecipeParametersRequest = { + /** + * Recipe parameter values entered by the user + */ + recipeParameters: { + [key: string]: string; + }; +}; + export type UpsertConfigQuery = { is_secret: boolean; key: string; @@ -2176,6 +2191,40 @@ export type GetSessionHistoryResponses = { export type GetSessionHistoryResponse = GetSessionHistoryResponses[keyof GetSessionHistoryResponses]; +export type UpdateSessionRecipeParametersData = { + body: UpdateSessionRecipeParametersRequest; + path: { + /** + * Unique identifier for the session + */ + session_id: string; + }; + query?: never; + url: '/sessions/{session_id}/recipe_parameters'; +}; + +export type UpdateSessionRecipeParametersErrors = { + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Session not found + */ + 404: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type UpdateSessionRecipeParametersResponses = { + /** + * Session recipe parameters updated successfully + */ + 200: unknown; +}; + export type StatusData = { body?: never; path?: never; diff --git a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx index 92f44d009600..7f95342f7aea 100644 --- a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx +++ b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx @@ -4,7 +4,7 @@ import { Recipe } from '../../recipe'; import { Geese } from '../icons/Geese'; import { X, Save, Play, Loader2 } from 'lucide-react'; import { Button } from '../ui/button'; -import RecipeFormFields from './shared/RecipeFormFields'; +import { RecipeFormFields } from './shared/RecipeFormFields'; import { RecipeFormData } from './shared/recipeFormSchema'; import { createRecipe } from '../../api/sdk.gen'; import { toastError } from '../../toasts'; @@ -227,10 +227,16 @@ export default function CreateRecipeFromSessionModal({ if (!isOpen) return null; return ( -
+
{/* Header */} -
+
@@ -247,43 +253,61 @@ export default function CreateRecipeFromSessionModal({ variant="ghost" size="sm" className="p-2 hover:bg-bgSubtle rounded-lg transition-colors" + data-testid="close-button" >
{/* Content */} -
+
{isAnalyzing ? ( -
+
- -
+ +
Analyzing your conversation...
-
{analysisStage}
+
+ {analysisStage} +
Extracting insights from your chat
) : ( - +
+ +
)}
{/* Footer */} -
+
@@ -292,22 +316,24 @@ export default function CreateRecipeFromSessionModal({ {isAnalyzing ? (
) : createdRecipe ? ( - <> +
- +
) : (