used recipe id or deeplink to start agent (#5154)
This commit is contained in:
@@ -466,6 +466,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::recipe::ListRecipeResponse,
|
||||
super::routes::recipe::DeleteRecipeRequest,
|
||||
super::routes::recipe::SaveRecipeRequest,
|
||||
super::routes::recipe::SaveRecipeResponse,
|
||||
super::routes::errors::ErrorResponse,
|
||||
super::routes::recipe::ParseRecipeRequest,
|
||||
super::routes::recipe::ParseRecipeResponse,
|
||||
@@ -489,7 +490,6 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::UpdateRouterToolSelectorRequest,
|
||||
super::routes::agent::StartAgentRequest,
|
||||
super::routes::agent::ResumeAgentRequest,
|
||||
super::routes::agent::ErrorResponse,
|
||||
super::routes::setup::SetupResponse,
|
||||
))
|
||||
)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::routes::recipe_utils::{load_recipe_by_id, validate_recipe};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
@@ -10,6 +12,7 @@ use goose::config::PermissionManager;
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::create;
|
||||
use goose::recipe::{Recipe, Response};
|
||||
use goose::recipe_deeplink;
|
||||
use goose::session::{Session, SessionManager};
|
||||
use goose::{
|
||||
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
|
||||
@@ -20,6 +23,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct ExtendPromptRequest {
|
||||
@@ -70,7 +74,12 @@ pub struct UpdateRouterToolSelectorRequest {
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct StartAgentRequest {
|
||||
working_dir: String,
|
||||
#[serde(default)]
|
||||
recipe: Option<Recipe>,
|
||||
#[serde(default)]
|
||||
recipe_id: Option<String>,
|
||||
#[serde(default)]
|
||||
recipe_deeplink: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
@@ -78,44 +87,92 @@ pub struct ResumeAgentRequest {
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct ErrorResponse {
|
||||
error: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/start",
|
||||
request_body = StartAgentRequest,
|
||||
responses(
|
||||
(status = 200, description = "Agent started successfully", body = Session),
|
||||
(status = 400, description = "Bad request - invalid working directory"),
|
||||
(status = 400, description = "Bad request", body = ErrorResponse),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
)
|
||||
)]
|
||||
async fn start_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<StartAgentRequest>,
|
||||
) -> Result<Json<Session>, StatusCode> {
|
||||
) -> Result<Json<Session>, ErrorResponse> {
|
||||
let StartAgentRequest {
|
||||
working_dir,
|
||||
recipe,
|
||||
recipe_id,
|
||||
recipe_deeplink,
|
||||
} = payload;
|
||||
|
||||
let resolved_recipe = if let Some(deeplink) = recipe_deeplink {
|
||||
match recipe_deeplink::decode(&deeplink) {
|
||||
Ok(recipe) => Some(recipe),
|
||||
Err(err) => {
|
||||
error!("Failed to decode recipe deeplink: {}", err);
|
||||
return Err(ErrorResponse {
|
||||
message: err.to_string(),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if let Some(id) = recipe_id {
|
||||
match load_recipe_by_id(state.as_ref(), &id).await {
|
||||
Ok(recipe) => Some(recipe),
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
} else {
|
||||
recipe
|
||||
};
|
||||
|
||||
if let Some(ref recipe) = resolved_recipe {
|
||||
if let Err(err) = validate_recipe(recipe) {
|
||||
return Err(ErrorResponse {
|
||||
message: err.message,
|
||||
status: err.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let counter = state.session_counter.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let description = format!("New session {}", counter);
|
||||
|
||||
let mut session =
|
||||
SessionManager::create_session(PathBuf::from(&payload.working_dir), description)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let mut session = SessionManager::create_session(PathBuf::from(&working_dir), description)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to create session: {}", err);
|
||||
ErrorResponse {
|
||||
message: format!("Failed to create session: {}", err),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(recipe) = payload.recipe {
|
||||
if let Some(recipe) = resolved_recipe {
|
||||
SessionManager::update_session(&session.id)
|
||||
.recipe(Some(recipe))
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
.map_err(|err| {
|
||||
error!("Failed to update session with recipe: {}", err);
|
||||
ErrorResponse {
|
||||
message: format!("Failed to update session with recipe: {}", err),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
})?;
|
||||
|
||||
session = SessionManager::get_session(&session.id, false)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
.map_err(|err| {
|
||||
error!("Failed to get updated session: {}", err);
|
||||
ErrorResponse {
|
||||
message: format!("Failed to get updated session: {}", err),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Json(session))
|
||||
@@ -134,10 +191,16 @@ async fn start_agent(
|
||||
)]
|
||||
async fn resume_agent(
|
||||
Json(payload): Json<ResumeAgentRequest>,
|
||||
) -> Result<Json<Session>, StatusCode> {
|
||||
) -> Result<Json<Session>, ErrorResponse> {
|
||||
let session = SessionManager::get_session(&payload.session_id, true)
|
||||
.await
|
||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
.map_err(|err| {
|
||||
error!("Failed to resume session {}: {}", payload.session_id, err);
|
||||
ErrorResponse {
|
||||
message: format!("Failed to resume session: {}", err),
|
||||
status: StatusCode::NOT_FOUND,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Json(session))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::rejection::JsonRejection;
|
||||
@@ -38,7 +37,10 @@ fn clean_data_error(err: &axum::extract::rejection::JsonDataError) -> String {
|
||||
}
|
||||
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::routes::recipe_utils::get_all_recipes_manifests;
|
||||
use crate::routes::recipe_utils::{
|
||||
get_all_recipes_manifests, get_recipe_file_path_by_id, short_id_from_path, validate_recipe,
|
||||
RecipeValidationError,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
@@ -97,6 +99,11 @@ pub struct SaveRecipeRequest {
|
||||
recipe: Recipe,
|
||||
id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct SaveRecipeResponse {
|
||||
id: String,
|
||||
}
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct ParseRecipeRequest {
|
||||
pub content: String,
|
||||
@@ -231,7 +238,10 @@ async fn decode_recipe(
|
||||
Json(request): Json<DecodeRecipeRequest>,
|
||||
) -> Result<Json<DecodeRecipeResponse>, StatusCode> {
|
||||
match recipe_deeplink::decode(&request.deeplink) {
|
||||
Ok(recipe) => Ok(Json(DecodeRecipeResponse { recipe })),
|
||||
Ok(recipe) => match validate_recipe(&recipe) {
|
||||
Ok(_) => Ok(Json(DecodeRecipeResponse { recipe })),
|
||||
Err(RecipeValidationError { status, .. }) => Err(status),
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to decode deeplink: {}", err);
|
||||
Err(StatusCode::BAD_REQUEST)
|
||||
@@ -309,7 +319,7 @@ async fn delete_recipe(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<DeleteRecipeRequest>,
|
||||
) -> StatusCode {
|
||||
let file_path = match get_recipe_file_path_by_id(state.clone(), &request.id).await {
|
||||
let file_path = match get_recipe_file_path_by_id(state.as_ref(), &request.id).await {
|
||||
Ok(path) => path,
|
||||
Err(err) => return err.status,
|
||||
};
|
||||
@@ -326,8 +336,10 @@ async fn delete_recipe(
|
||||
path = "/recipes/save",
|
||||
request_body = SaveRecipeRequest,
|
||||
responses(
|
||||
(status = 204, description = "Recipe saved to file successfully"),
|
||||
(status = 204, description = "Recipe saved to file successfully", body = SaveRecipeResponse),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||
(status = 401, description = "Unauthorized", body = ErrorResponse),
|
||||
(status = 404, description = "Not found", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
@@ -335,18 +347,20 @@ async fn delete_recipe(
|
||||
async fn save_recipe(
|
||||
State(state): State<Arc<AppState>>,
|
||||
payload: Result<Json<Value>, JsonRejection>,
|
||||
) -> Result<StatusCode, ErrorResponse> {
|
||||
) -> Result<Json<SaveRecipeResponse>, ErrorResponse> {
|
||||
let Json(raw_json) = payload.map_err(json_rejection_to_error_response)?;
|
||||
let request = deserialize_save_recipe_request(raw_json)?;
|
||||
validate_recipe(&request.recipe)?;
|
||||
ensure_recipe_valid(&request.recipe)?;
|
||||
|
||||
let file_path = match request.id.as_ref() {
|
||||
Some(id) => Some(get_recipe_file_path_by_id(state.clone(), id).await?),
|
||||
Some(id) => Some(get_recipe_file_path_by_id(state.as_ref(), id).await?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
match local_recipes::save_recipe_to_file(request.recipe, file_path) {
|
||||
Ok(_) => Ok(StatusCode::NO_CONTENT),
|
||||
match local_recipes::save_recipe_to_file(request.recipe, file_path.clone()) {
|
||||
Ok(save_file_path) => Ok(Json(SaveRecipeResponse {
|
||||
id: short_id_from_path(&save_file_path.display().to_string()),
|
||||
})),
|
||||
Err(e) => Err(ErrorResponse {
|
||||
message: e.to_string(),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -361,16 +375,13 @@ fn json_rejection_to_error_response(rejection: JsonRejection) -> ErrorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_recipe(recipe: &Recipe) -> Result<(), ErrorResponse> {
|
||||
let recipe_json = serde_json::to_string(recipe).map_err(|err| ErrorResponse {
|
||||
message: err.to_string(),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
})?;
|
||||
|
||||
validate_recipe_template_from_content(&recipe_json, None).map_err(|err| ErrorResponse {
|
||||
message: err.to_string(),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
})?;
|
||||
fn ensure_recipe_valid(recipe: &Recipe) -> Result<(), ErrorResponse> {
|
||||
if let Err(err) = validate_recipe(recipe) {
|
||||
return Err(ErrorResponse {
|
||||
message: err.message,
|
||||
status: err.status,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -398,41 +409,6 @@ fn deserialize_save_recipe_request(value: Value) -> Result<SaveRecipeRequest, Er
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_recipe_file_path_by_id(
|
||||
state: Arc<AppState>,
|
||||
id: &str,
|
||||
) -> Result<PathBuf, ErrorResponse> {
|
||||
let cached_path = {
|
||||
let map = state.recipe_file_hash_map.lock().await;
|
||||
map.get(id).cloned()
|
||||
};
|
||||
|
||||
if let Some(path) = cached_path {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let recipe_manifest_with_paths = get_all_recipes_manifests().unwrap_or_default();
|
||||
let mut recipe_file_hash_map = HashMap::new();
|
||||
let mut resolved_path: Option<PathBuf> = None;
|
||||
|
||||
for recipe_manifest_with_path in &recipe_manifest_with_paths {
|
||||
if recipe_manifest_with_path.id == id {
|
||||
resolved_path = Some(recipe_manifest_with_path.file_path.clone());
|
||||
}
|
||||
recipe_file_hash_map.insert(
|
||||
recipe_manifest_with_path.id.clone(),
|
||||
recipe_manifest_with_path.file_path.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
state.set_recipe_file_hash_map(recipe_file_hash_map).await;
|
||||
|
||||
resolved_path.ok_or_else(|| ErrorResponse {
|
||||
message: format!("Recipe not found: {}", id),
|
||||
status: StatusCode::NOT_FOUND,
|
||||
})
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/parse",
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::hash::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::state::AppState;
|
||||
use goose::recipe::local_recipes::list_local_recipes;
|
||||
use goose::recipe::validate_recipe::validate_recipe_template_from_content;
|
||||
use goose::recipe::Recipe;
|
||||
use serde_json;
|
||||
use tracing::error;
|
||||
|
||||
pub struct RecipeValidationError {
|
||||
pub status: StatusCode,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub struct RecipeManifestWithPath {
|
||||
pub id: String,
|
||||
@@ -15,7 +27,7 @@ pub struct RecipeManifestWithPath {
|
||||
pub last_modified: String,
|
||||
}
|
||||
|
||||
fn short_id_from_path(path: &str) -> String {
|
||||
pub fn short_id_from_path(path: &str) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
path.hash(&mut hasher);
|
||||
let h = hasher.finish();
|
||||
@@ -44,3 +56,69 @@ pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
||||
|
||||
Ok(recipe_manifests_with_path)
|
||||
}
|
||||
|
||||
pub fn validate_recipe(recipe: &Recipe) -> Result<(), RecipeValidationError> {
|
||||
let recipe_json = serde_json::to_string(recipe).map_err(|err| {
|
||||
let message = err.to_string();
|
||||
error!("Failed to serialize recipe for validation: {}", message);
|
||||
RecipeValidationError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
message,
|
||||
}
|
||||
})?;
|
||||
|
||||
validate_recipe_template_from_content(&recipe_json, None).map_err(|err| {
|
||||
let message = err.to_string();
|
||||
error!("Recipe validation failed: {}", message);
|
||||
RecipeValidationError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
message,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_recipe_file_path_by_id(
|
||||
state: &AppState,
|
||||
id: &str,
|
||||
) -> Result<PathBuf, ErrorResponse> {
|
||||
let cached_path = {
|
||||
let map = state.recipe_file_hash_map.lock().await;
|
||||
map.get(id).cloned()
|
||||
};
|
||||
|
||||
if let Some(path) = cached_path {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let recipe_manifest_with_paths = get_all_recipes_manifests().unwrap_or_default();
|
||||
let mut recipe_file_hash_map = HashMap::new();
|
||||
let mut resolved_path: Option<PathBuf> = None;
|
||||
|
||||
for recipe_manifest_with_path in &recipe_manifest_with_paths {
|
||||
if recipe_manifest_with_path.id == id {
|
||||
resolved_path = Some(recipe_manifest_with_path.file_path.clone());
|
||||
}
|
||||
recipe_file_hash_map.insert(
|
||||
recipe_manifest_with_path.id.clone(),
|
||||
recipe_manifest_with_path.file_path.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
state.set_recipe_file_hash_map(recipe_file_hash_map).await;
|
||||
|
||||
resolved_path.ok_or_else(|| ErrorResponse {
|
||||
message: format!("Recipe not found: {}", id),
|
||||
status: StatusCode::NOT_FOUND,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_recipe_by_id(state: &AppState, id: &str) -> Result<Recipe, ErrorResponse> {
|
||||
let path = get_recipe_file_path_by_id(state, id).await?;
|
||||
|
||||
Recipe::from_file_path(&path).map_err(|err| ErrorResponse {
|
||||
message: format!("Failed to load recipe: {}", err),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::recipe::Recipe;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecodeError {
|
||||
#[error("All decoding methods failed")]
|
||||
#[error("Failed to decode recipe deeplink")]
|
||||
AllMethodsFailed,
|
||||
}
|
||||
|
||||
|
||||
+63
-6
@@ -191,13 +191,27 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid working directory"
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - invalid secret key"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1235,10 +1249,34 @@
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Recipe saved to file successfully"
|
||||
"description": "Recipe saved to file successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SaveRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - Invalid or missing API key"
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
@@ -2485,10 +2523,10 @@
|
||||
"ErrorResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"error"
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"error": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -3978,6 +4016,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SaveRecipeResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ScanRecipeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -4310,6 +4359,14 @@
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"recipe_deeplink": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"recipe_id": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"working_dir": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ export type Envs = {
|
||||
};
|
||||
|
||||
export type ErrorResponse = {
|
||||
error: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ExtendPromptRequest = {
|
||||
@@ -667,6 +667,10 @@ export type SaveRecipeRequest = {
|
||||
recipe: Recipe;
|
||||
};
|
||||
|
||||
export type SaveRecipeResponse = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ScanRecipeRequest = {
|
||||
recipe: Recipe;
|
||||
};
|
||||
@@ -764,6 +768,8 @@ export type SetupResponse = {
|
||||
|
||||
export type StartAgentRequest = {
|
||||
recipe?: Recipe | null;
|
||||
recipe_deeplink?: string | null;
|
||||
recipe_id?: string | null;
|
||||
working_dir: string;
|
||||
};
|
||||
|
||||
@@ -1041,9 +1047,9 @@ export type StartAgentData = {
|
||||
|
||||
export type StartAgentErrors = {
|
||||
/**
|
||||
* Bad request - invalid working directory
|
||||
* Bad request
|
||||
*/
|
||||
400: unknown;
|
||||
400: ErrorResponse;
|
||||
/**
|
||||
* Unauthorized - invalid secret key
|
||||
*/
|
||||
@@ -1051,9 +1057,11 @@ export type StartAgentErrors = {
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
500: ErrorResponse;
|
||||
};
|
||||
|
||||
export type StartAgentError = StartAgentErrors[keyof StartAgentErrors];
|
||||
|
||||
export type StartAgentResponses = {
|
||||
/**
|
||||
* Agent started successfully
|
||||
@@ -1873,9 +1881,13 @@ export type SaveRecipeData = {
|
||||
|
||||
export type SaveRecipeErrors = {
|
||||
/**
|
||||
* Unauthorized - Invalid or missing API key
|
||||
* Unauthorized
|
||||
*/
|
||||
401: unknown;
|
||||
401: ErrorResponse;
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: ErrorResponse;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
@@ -1888,10 +1900,10 @@ export type SaveRecipeResponses = {
|
||||
/**
|
||||
* Recipe saved to file successfully
|
||||
*/
|
||||
204: void;
|
||||
204: SaveRecipeResponse;
|
||||
};
|
||||
|
||||
export type SaveRecipeResponse = SaveRecipeResponses[keyof SaveRecipeResponses];
|
||||
export type SaveRecipeResponse2 = SaveRecipeResponses[keyof SaveRecipeResponses];
|
||||
|
||||
export type ScanRecipeData = {
|
||||
body: ScanRecipeRequest;
|
||||
|
||||
@@ -295,7 +295,7 @@ export default function CreateEditRecipeModal({
|
||||
try {
|
||||
const recipe = getCurrentRecipe();
|
||||
|
||||
await saveRecipe(recipe, recipeId);
|
||||
let saved_recipe_id = await saveRecipe(recipe, recipeId);
|
||||
|
||||
// Close modal first
|
||||
onClose(true);
|
||||
@@ -306,9 +306,9 @@ export default function CreateEditRecipeModal({
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
recipe,
|
||||
undefined,
|
||||
recipeId ?? undefined
|
||||
undefined,
|
||||
saved_recipe_id
|
||||
);
|
||||
|
||||
toastSuccess({
|
||||
|
||||
@@ -183,13 +183,21 @@ export default function CreateRecipeFromSessionModal({
|
||||
extensions: [], // Will be populated based on current extensions
|
||||
};
|
||||
|
||||
await saveRecipe(recipe, null);
|
||||
let recipeId = await saveRecipe(recipe, null);
|
||||
|
||||
onRecipeCreated?.(recipe);
|
||||
onClose();
|
||||
|
||||
if (runAfterSave) {
|
||||
window.electron.createChatWindow(undefined, undefined, undefined, undefined, recipe);
|
||||
window.electron.createChatWindow(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
recipeId
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create recipe:', error);
|
||||
|
||||
@@ -47,15 +47,23 @@ export function useAgent(): UseAgentReturn {
|
||||
const [agentState, setAgentState] = useState<AgentState>(AgentState.UNINITIALIZED);
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const initPromiseRef = useRef<Promise<ChatType> | null>(null);
|
||||
const [recipeFromAppConfig, setRecipeFromAppConfig] = useState<Recipe | null>(
|
||||
(window.appConfig.get('recipe') as Recipe) || null
|
||||
const recipeIdFromConfig = useRef<string | null>(
|
||||
(window.appConfig.get('recipeId') as string | null | undefined) ?? null
|
||||
);
|
||||
const recipeDeeplinkFromConfig = useRef<string | null>(
|
||||
(window.appConfig.get('recipeDeeplink') as string | null | undefined) ?? null
|
||||
);
|
||||
const scheduledJobIdFromConfig = useRef<string | null>(
|
||||
(window.appConfig.get('scheduledJobId') as string | null | undefined) ?? null
|
||||
);
|
||||
const { getExtensions, addExtension, read } = useConfig();
|
||||
|
||||
const resetChat = useCallback(() => {
|
||||
setSessionId(null);
|
||||
setAgentState(AgentState.UNINITIALIZED);
|
||||
setRecipeFromAppConfig(null);
|
||||
recipeIdFromConfig.current = null;
|
||||
recipeDeeplinkFromConfig.current = null;
|
||||
scheduledJobIdFromConfig.current = null;
|
||||
}, []);
|
||||
|
||||
const agentIsInitialized = agentState === AgentState.INITIALIZED;
|
||||
@@ -110,7 +118,11 @@ export function useAgent(): UseAgentReturn {
|
||||
: await startAgent({
|
||||
body: {
|
||||
working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string,
|
||||
recipe: recipeFromAppConfig ?? initContext.recipe,
|
||||
...buildRecipeInput(
|
||||
initContext.recipe,
|
||||
recipeIdFromConfig.current,
|
||||
recipeDeeplinkFromConfig.current
|
||||
),
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
@@ -121,6 +133,18 @@ export function useAgent(): UseAgentReturn {
|
||||
}
|
||||
setSessionId(agentSession.id);
|
||||
|
||||
if (!initContext.recipe && agentSession.recipe && scheduledJobIdFromConfig.current) {
|
||||
agentSession.recipe = {
|
||||
...agentSession.recipe,
|
||||
scheduledJobId: scheduledJobIdFromConfig.current,
|
||||
isScheduledExecution: true,
|
||||
} as Recipe;
|
||||
scheduledJobIdFromConfig.current = null;
|
||||
}
|
||||
|
||||
recipeIdFromConfig.current = null;
|
||||
recipeDeeplinkFromConfig.current = null;
|
||||
|
||||
agentWaitingMessage('Agent is loading config');
|
||||
|
||||
await initConfig();
|
||||
@@ -169,10 +193,17 @@ export function useAgent(): UseAgentReturn {
|
||||
|
||||
return initChat;
|
||||
} catch (error) {
|
||||
if ((error + '').includes('Failed to create provider')) {
|
||||
if (
|
||||
(error + '').includes('Failed to create provider') ||
|
||||
error instanceof NoProviderOrModelError
|
||||
) {
|
||||
setAgentState(AgentState.NO_PROVIDER);
|
||||
} else {
|
||||
setAgentState(AgentState.ERROR);
|
||||
throw error;
|
||||
}
|
||||
setAgentState(AgentState.ERROR);
|
||||
if (typeof error === 'object' && error !== null && 'message' in error) {
|
||||
let error_message = error.message as string;
|
||||
throw new Error(error_message);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -184,7 +215,7 @@ export function useAgent(): UseAgentReturn {
|
||||
initPromiseRef.current = initPromise;
|
||||
return initPromise;
|
||||
},
|
||||
[agentIsInitialized, sessionId, read, recipeFromAppConfig, getExtensions, addExtension]
|
||||
[agentIsInitialized, sessionId, read, getExtensions, addExtension]
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -220,3 +251,23 @@ const handleConfigRecovery = async () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buildRecipeInput = (
|
||||
recipeOverride?: Recipe,
|
||||
recipeId?: string | null,
|
||||
recipeDeeplink?: string | null
|
||||
) => {
|
||||
if (recipeId) {
|
||||
return { recipe_id: recipeId };
|
||||
}
|
||||
|
||||
if (recipeDeeplink) {
|
||||
return { recipe_deeplink: recipeDeeplink };
|
||||
}
|
||||
|
||||
if (recipeOverride) {
|
||||
return { recipe: recipeOverride };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -68,13 +68,6 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a recipe from app config (deeplink), persist it
|
||||
// But only if the chat context doesn't explicitly have null (which indicates it was cleared)
|
||||
const appRecipe = window.appConfig.get('recipe') as Recipe | null;
|
||||
if (appRecipe && chatContext.chat.recipe === undefined) {
|
||||
chatContext.setRecipe(appRecipe);
|
||||
}
|
||||
}, [chatContext, recipe]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -87,18 +80,6 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => {
|
||||
if (finalRecipe) {
|
||||
hasCheckedRecipeRef.current = true;
|
||||
|
||||
// If the recipe comes from session metadata (not from navigation state),
|
||||
// it means it was already accepted in a previous session, so auto-accept it
|
||||
const hasMessages = chat.messages.length > 0;
|
||||
const isFromSessionMetadata = !recipe && finalRecipe && hasMessages;
|
||||
|
||||
if (isFromSessionMetadata) {
|
||||
// Recipe loaded from session metadata should be automatically accepted
|
||||
setRecipeAccepted(true);
|
||||
setIsRecipeWarningModalOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const hasAccepted = await window.electron.hasAcceptedRecipeBefore(finalRecipe);
|
||||
|
||||
|
||||
+6
-55
@@ -50,25 +50,9 @@ import {
|
||||
import { UPDATES_ENABLED } from './updates';
|
||||
import { Recipe } from './recipe';
|
||||
import './utils/recipeHash';
|
||||
import { decodeRecipe } from './api';
|
||||
import { Client, createClient, createConfig } from './api/client';
|
||||
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
|
||||
|
||||
async function decodeRecipeMain(client: Client, deeplink: string): Promise<Recipe | null> {
|
||||
try {
|
||||
return (
|
||||
await decodeRecipe({
|
||||
client,
|
||||
throwOnError: true,
|
||||
body: { deeplink },
|
||||
})
|
||||
).data.recipe;
|
||||
} catch (e) {
|
||||
console.error('Failed to decode recipe:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Updater functions (moved here to keep updates.ts minimal for release replacement)
|
||||
function shouldSetupUpdater(): boolean {
|
||||
// Setup updater if either the flag is enabled OR dev updates are enabled
|
||||
@@ -544,12 +528,6 @@ const createChat = async (
|
||||
}
|
||||
|
||||
// Create window config with loading state for recipe deeplinks
|
||||
let isLoadingRecipe = false;
|
||||
if (!recipe && recipeDeeplink) {
|
||||
isLoadingRecipe = true;
|
||||
console.log('[Main] Creating window with recipe loading state for deeplink:', recipeDeeplink);
|
||||
}
|
||||
|
||||
// Load and manage window state
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 940, // large enough to show the sidebar on launch
|
||||
@@ -584,8 +562,9 @@ const createChat = async (
|
||||
REQUEST_DIR: dir,
|
||||
GOOSE_BASE_URL_SHARE: baseUrlShare,
|
||||
GOOSE_VERSION: version,
|
||||
recipe: recipe,
|
||||
recipeId: recipeId,
|
||||
recipeDeeplink: recipeDeeplink,
|
||||
scheduledJobId: scheduledJobId,
|
||||
}),
|
||||
],
|
||||
partition: 'persist:goose', // Add this line to ensure persistence
|
||||
@@ -697,7 +676,10 @@ const createChat = async (
|
||||
if (viewType) {
|
||||
appPath = routeMap[viewType] || '/';
|
||||
}
|
||||
if (appPath === '/' && (recipe !== undefined || recipeDeeplink !== undefined)) {
|
||||
if (
|
||||
appPath === '/' &&
|
||||
(recipe !== undefined || recipeDeeplink !== undefined || recipeId !== undefined)
|
||||
) {
|
||||
appPath = '/pair';
|
||||
}
|
||||
|
||||
@@ -747,37 +729,6 @@ const createChat = async (
|
||||
|
||||
windowMap.set(windowId, mainWindow);
|
||||
|
||||
// Handle recipe decoding in the background after window is created
|
||||
if (isLoadingRecipe && recipeDeeplink) {
|
||||
console.log('[Main] Starting background recipe decoding for:', recipeDeeplink);
|
||||
|
||||
// Decode recipe asynchronously after window is created
|
||||
decodeRecipeMain(goosedClient, recipeDeeplink)
|
||||
.then((decodedRecipe) => {
|
||||
if (decodedRecipe) {
|
||||
console.log('[Main] Recipe decoded successfully, updating window config');
|
||||
|
||||
// Handle scheduled job parameters if present
|
||||
if (scheduledJobId) {
|
||||
decodedRecipe.scheduledJobId = scheduledJobId;
|
||||
decodedRecipe.isScheduledExecution = true;
|
||||
}
|
||||
|
||||
// Send the decoded recipe to the renderer process
|
||||
mainWindow.webContents.send('recipe-decoded', decodedRecipe);
|
||||
} else {
|
||||
console.error('[Main] Failed to decode recipe from deeplink');
|
||||
// Send error to renderer
|
||||
mainWindow.webContents.send('recipe-decode-error', 'Failed to decode recipe');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Main] Error decoding recipe:', error);
|
||||
// Send error to renderer
|
||||
mainWindow.webContents.send('recipe-decode-error', error.message || 'Unknown error');
|
||||
});
|
||||
}
|
||||
|
||||
// Handle window closure
|
||||
mainWindow.on('closed', () => {
|
||||
windowMap.delete(windowId);
|
||||
|
||||
@@ -255,11 +255,6 @@ const appConfigAPI: AppConfigAPI = {
|
||||
getAll: () => config,
|
||||
};
|
||||
|
||||
// Listen for recipe updates and update config directly
|
||||
ipcRenderer.on('recipe-decoded', (_, decodedRecipe) => {
|
||||
config.recipe = decodedRecipe;
|
||||
});
|
||||
|
||||
// Expose the APIs
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||
contextBridge.exposeInMainWorld('appConfig', appConfigAPI);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Recipe, saveRecipe as saveRecipeApi, listRecipes, RecipeManifestResponse } from '../api';
|
||||
|
||||
export async function saveRecipe(recipe: Recipe, recipeId?: string | null): Promise<void> {
|
||||
export async function saveRecipe(recipe: Recipe, recipeId?: string | null): Promise<string> {
|
||||
try {
|
||||
await saveRecipeApi({
|
||||
let response = await saveRecipeApi({
|
||||
body: {
|
||||
recipe,
|
||||
id: recipeId,
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
return response.data.id;
|
||||
} catch (error) {
|
||||
let error_message = 'unknown error';
|
||||
if (typeof error === 'object' && error !== null && 'message' in error) {
|
||||
|
||||
@@ -225,7 +225,7 @@ export const initializeSystem = async (
|
||||
}
|
||||
|
||||
// Get recipe - prefer from options (session metadata) over app config
|
||||
const recipe = options?.recipe || window.appConfig?.get?.('recipe');
|
||||
const recipe = options?.recipe;
|
||||
const recipe_instructions = (recipe as { instructions?: string })?.instructions;
|
||||
const responseConfig = (recipe as { response?: { json_schema?: unknown } })?.response;
|
||||
const subRecipes = (recipe as { sub_recipes?: SubRecipe[] })?.sub_recipes;
|
||||
|
||||
Reference in New Issue
Block a user