Slash commands (#5718)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -94,7 +94,7 @@ pub async fn handle_schedule_add(
|
||||
.await
|
||||
.context("Failed to initialize scheduler")?;
|
||||
|
||||
match scheduler.add_scheduled_job(job).await {
|
||||
match scheduler.add_scheduled_job(job, true).await {
|
||||
Ok(_) => {
|
||||
// The scheduler has copied the recipe to its internal directory.
|
||||
// We can reconstruct the likely path for display if needed, or adjust success message.
|
||||
@@ -175,7 +175,7 @@ pub async fn handle_schedule_remove(schedule_id: String) -> Result<()> {
|
||||
.await
|
||||
.context("Failed to initialize scheduler")?;
|
||||
|
||||
match scheduler.remove_scheduled_job(&schedule_id).await {
|
||||
match scheduler.remove_scheduled_job(&schedule_id, true).await {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Scheduled job '{}' and its associated recipe removed.",
|
||||
|
||||
@@ -30,7 +30,7 @@ use anyhow::{Context, Result};
|
||||
use completion::GooseCompleter;
|
||||
use goose::agents::extension::{Envs, ExtensionConfig, PLATFORM_EXTENSIONS};
|
||||
use goose::agents::types::RetryConfig;
|
||||
use goose::agents::{Agent, SessionConfig, MANUAL_COMPACT_TRIGGER};
|
||||
use goose::agents::{Agent, SessionConfig, MANUAL_COMPACT_TRIGGERS};
|
||||
use goose::config::{Config, GooseMode};
|
||||
use goose::providers::pricing::initialize_pricing_cache;
|
||||
use goose::session::SessionManager;
|
||||
@@ -703,7 +703,7 @@ impl CliSession {
|
||||
};
|
||||
|
||||
if should_summarize {
|
||||
self.push_message(Message::user().with_text(MANUAL_COMPACT_TRIGGER));
|
||||
self.push_message(Message::user().with_text(MANUAL_COMPACT_TRIGGERS[0]));
|
||||
output::show_thinking();
|
||||
self.process_agent_response(true, CancellationToken::default())
|
||||
.await?;
|
||||
|
||||
@@ -23,6 +23,7 @@ use goose::conversation::message::{
|
||||
ToolConfirmationRequest, ToolRequest, ToolResponse,
|
||||
};
|
||||
|
||||
use crate::routes::recipe_utils::RecipeManifest;
|
||||
use crate::routes::reply::MessageEvent;
|
||||
use utoipa::openapi::schema::{
|
||||
AdditionalProperties, AnyOfBuilder, ArrayBuilder, ObjectBuilder, OneOfBuilder, Schema,
|
||||
@@ -340,6 +341,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::config_management::read_all_config,
|
||||
super::routes::config_management::providers,
|
||||
super::routes::config_management::get_provider_models,
|
||||
super::routes::config_management::get_slash_commands,
|
||||
super::routes::config_management::upsert_permissions,
|
||||
super::routes::config_management::create_custom_provider,
|
||||
super::routes::config_management::get_custom_provider,
|
||||
@@ -381,6 +383,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::recipe::scan_recipe,
|
||||
super::routes::recipe::list_recipes,
|
||||
super::routes::recipe::delete_recipe,
|
||||
super::routes::recipe::schedule_recipe,
|
||||
super::routes::recipe::set_recipe_slash_command,
|
||||
super::routes::recipe::save_recipe,
|
||||
super::routes::recipe::parse_recipe,
|
||||
super::routes::setup::start_openrouter_setup,
|
||||
@@ -392,6 +396,9 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::config_management::ConfigResponse,
|
||||
super::routes::config_management::ProvidersResponse,
|
||||
super::routes::config_management::ProviderDetails,
|
||||
super::routes::config_management::SlashCommandsResponse,
|
||||
super::routes::config_management::SlashCommand,
|
||||
super::routes::config_management::CommandType,
|
||||
super::routes::config_management::ExtensionResponse,
|
||||
super::routes::config_management::ExtensionQuery,
|
||||
super::routes::config_management::ToolPermission,
|
||||
@@ -441,6 +448,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
ExtensionConfig,
|
||||
ConfigKey,
|
||||
Envs,
|
||||
RecipeManifest,
|
||||
ToolSchema,
|
||||
ToolAnnotationsSchema,
|
||||
ToolInfo,
|
||||
@@ -471,8 +479,9 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::recipe::DecodeRecipeResponse,
|
||||
super::routes::recipe::ScanRecipeRequest,
|
||||
super::routes::recipe::ScanRecipeResponse,
|
||||
super::routes::recipe::RecipeManifestResponse,
|
||||
super::routes::recipe::ListRecipeResponse,
|
||||
super::routes::recipe::ScheduleRecipeRequest,
|
||||
super::routes::recipe::SetSlashCommandRequest,
|
||||
super::routes::recipe::DeleteRecipeRequest,
|
||||
super::routes::recipe::SaveRecipeRequest,
|
||||
super::routes::recipe::SaveRecipeResponse,
|
||||
|
||||
@@ -17,7 +17,7 @@ use goose::providers::pricing::{
|
||||
get_all_pricing, get_model_pricing, parse_model_id, refresh_pricing,
|
||||
};
|
||||
use goose::providers::providers as get_providers;
|
||||
use goose::{agents::ExtensionConfig, config::permission::PermissionLevel};
|
||||
use goose::{agents::ExtensionConfig, config::permission::PermissionLevel, slash_commands};
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -113,6 +113,23 @@ pub enum ConfigValueResponse {
|
||||
MaskedValue(MaskedSecret),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub enum CommandType {
|
||||
Builtin,
|
||||
Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SlashCommand {
|
||||
pub command: String,
|
||||
pub help: String,
|
||||
pub command_type: CommandType,
|
||||
}
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SlashCommandsResponse {
|
||||
pub commands: Vec<SlashCommand>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/upsert",
|
||||
@@ -390,6 +407,30 @@ pub async fn get_provider_models(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/config/slash_commands",
|
||||
responses(
|
||||
(status = 200, description = "Slash commands retrieved successfully", body = SlashCommandsResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn get_slash_commands() -> Result<Json<SlashCommandsResponse>, StatusCode> {
|
||||
let mut commands: Vec<_> = slash_commands::list_commands()
|
||||
.iter()
|
||||
.map(|command| SlashCommand {
|
||||
command: command.command.clone(),
|
||||
help: command.recipe_path.clone(),
|
||||
command_type: CommandType::Recipe,
|
||||
})
|
||||
.collect();
|
||||
commands.push(SlashCommand {
|
||||
command: "compact".to_string(),
|
||||
help: "Compact the current conversation to save tokens".to_string(),
|
||||
command_type: CommandType::Builtin,
|
||||
});
|
||||
Ok(Json(SlashCommandsResponse { commands }))
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct PricingData {
|
||||
pub provider: String,
|
||||
@@ -408,8 +449,7 @@ pub struct PricingResponse {
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct PricingQuery {
|
||||
/// If true, only return pricing for configured providers. If false, return all.
|
||||
pub configured_only: Option<bool>,
|
||||
pub configured_only: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -423,7 +463,7 @@ pub struct PricingQuery {
|
||||
pub async fn get_pricing(
|
||||
Json(query): Json<PricingQuery>,
|
||||
) -> Result<Json<PricingResponse>, StatusCode> {
|
||||
let configured_only = query.configured_only.unwrap_or(true);
|
||||
let configured_only = query.configured_only;
|
||||
|
||||
// If refresh requested (configured_only = false), refresh the cache
|
||||
if !configured_only {
|
||||
@@ -792,6 +832,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/config/extensions/{name}", delete(remove_extension))
|
||||
.route("/config/providers", get(providers))
|
||||
.route("/config/providers/{name}/models", get(get_provider_models))
|
||||
.route("/config/slash_commands", get(get_slash_commands))
|
||||
.route("/config/pricing", post(get_pricing))
|
||||
.route("/config/init", post(init_config))
|
||||
.route("/config/backup", post(backup_config))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::rejection::JsonRejection;
|
||||
@@ -8,8 +9,8 @@ use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
|
||||
use goose::recipe::local_recipes;
|
||||
use goose::recipe::validate_recipe::validate_recipe_template_from_content;
|
||||
use goose::recipe::Recipe;
|
||||
use goose::recipe_deeplink;
|
||||
use goose::session::SessionManager;
|
||||
use goose::{recipe_deeplink, slash_commands};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -39,7 +40,7 @@ fn clean_data_error(err: &axum::extract::rejection::JsonDataError) -> String {
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::routes::recipe_utils::{
|
||||
get_all_recipes_manifests, get_recipe_file_path_by_id, short_id_from_path, validate_recipe,
|
||||
RecipeValidationError,
|
||||
RecipeManifest, RecipeValidationError,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -114,14 +115,6 @@ pub struct ParseRecipeResponse {
|
||||
pub recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct RecipeManifestResponse {
|
||||
recipe: Recipe,
|
||||
#[serde(rename = "lastModified")]
|
||||
last_modified: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct DeleteRecipeRequest {
|
||||
id: String,
|
||||
@@ -129,7 +122,19 @@ pub struct DeleteRecipeRequest {
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ListRecipeResponse {
|
||||
recipe_manifest_responses: Vec<RecipeManifestResponse>,
|
||||
manifests: Vec<RecipeManifest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct ScheduleRecipeRequest {
|
||||
id: String,
|
||||
cron_schedule: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct SetSlashCommandRequest {
|
||||
id: String,
|
||||
slash_command: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -281,26 +286,36 @@ async fn scan_recipe(
|
||||
async fn list_recipes(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<ListRecipeResponse>, StatusCode> {
|
||||
let recipe_manifest_with_paths = get_all_recipes_manifests().unwrap_or_default();
|
||||
let mut recipe_file_hash_map = HashMap::new();
|
||||
let recipe_manifest_responses = recipe_manifest_with_paths
|
||||
let mut manifests = get_all_recipes_manifests().unwrap_or_default();
|
||||
let recipe_file_hash_map: HashMap<_, _> = manifests
|
||||
.iter()
|
||||
.map(|recipe_manifest_with_path| {
|
||||
let id = &recipe_manifest_with_path.id;
|
||||
let file_path = recipe_manifest_with_path.file_path.clone();
|
||||
recipe_file_hash_map.insert(id.clone(), file_path);
|
||||
RecipeManifestResponse {
|
||||
recipe: recipe_manifest_with_path.recipe.clone(),
|
||||
id: id.clone(),
|
||||
last_modified: recipe_manifest_with_path.last_modified.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<RecipeManifestResponse>>();
|
||||
.map(|m| (m.id.clone(), m.file_path.clone()))
|
||||
.collect();
|
||||
state.set_recipe_file_hash_map(recipe_file_hash_map).await;
|
||||
|
||||
Ok(Json(ListRecipeResponse {
|
||||
recipe_manifest_responses,
|
||||
}))
|
||||
let scheduler = state.scheduler();
|
||||
let scheduled_jobs = scheduler.list_scheduled_jobs().await;
|
||||
let schedule_map: HashMap<_, _> = scheduled_jobs
|
||||
.into_iter()
|
||||
.map(|j| (PathBuf::from(j.source), j.cron))
|
||||
.collect();
|
||||
|
||||
let all_commands = slash_commands::list_commands();
|
||||
let slash_map: HashMap<_, _> = all_commands
|
||||
.into_iter()
|
||||
.map(|sc| (PathBuf::from(sc.recipe_path), sc.command))
|
||||
.collect();
|
||||
|
||||
for manifest in &mut manifests {
|
||||
if let Some(cron) = schedule_map.get(&manifest.file_path) {
|
||||
manifest.schedule_cron = Some(cron.clone());
|
||||
}
|
||||
if let Some(command) = slash_map.get(&manifest.file_path) {
|
||||
manifest.slash_command = Some(command.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(ListRecipeResponse { manifests }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -331,6 +346,68 @@ async fn delete_recipe(
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/schedule",
|
||||
request_body = ScheduleRecipeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recipe scheduled successfully"),
|
||||
(status = 404, description = "Recipe not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
async fn schedule_recipe(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<ScheduleRecipeRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let file_path = match get_recipe_file_path_by_id(state.as_ref(), &request.id).await {
|
||||
Ok(path) => path,
|
||||
Err(err) => return Err(err.status),
|
||||
};
|
||||
|
||||
let scheduler = state.scheduler();
|
||||
match scheduler
|
||||
.schedule_recipe(file_path, request.cron_schedule)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(StatusCode::OK),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to schedule recipe: {}", e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/slash-command",
|
||||
request_body = SetSlashCommandRequest,
|
||||
responses(
|
||||
(status = 200, description = "Slash command set successfully"),
|
||||
(status = 404, description = "Recipe not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
async fn set_recipe_slash_command(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<SetSlashCommandRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let file_path = match get_recipe_file_path_by_id(state.as_ref(), &request.id).await {
|
||||
Ok(path) => path,
|
||||
Err(err) => return Err(err.status),
|
||||
};
|
||||
|
||||
match slash_commands::set_recipe_slash_command(file_path, request.slash_command) {
|
||||
Ok(_) => Ok(StatusCode::OK),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to set slash command: {}", e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/save",
|
||||
@@ -447,6 +524,8 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/recipes/scan", post(scan_recipe))
|
||||
.route("/recipes/list", get(list_recipes))
|
||||
.route("/recipes/delete", post(delete_recipe))
|
||||
.route("/recipes/schedule", post(schedule_recipe))
|
||||
.route("/recipes/slash-command", post(set_recipe_slash_command))
|
||||
.route("/recipes/save", post(save_recipe))
|
||||
.route("/recipes/parse", post(parse_recipe))
|
||||
.with_state(state)
|
||||
|
||||
@@ -5,30 +5,35 @@ use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::state::AppState;
|
||||
use anyhow::Result;
|
||||
use axum::http::StatusCode;
|
||||
use goose::agents::Agent;
|
||||
use goose::prompt_template::render_global_file;
|
||||
use goose::recipe::build_recipe::{build_recipe_from_template, RecipeError};
|
||||
use goose::recipe::local_recipes::{get_recipe_library_dir, list_local_recipes};
|
||||
use goose::recipe::validate_recipe::validate_recipe_template_from_content;
|
||||
use goose::recipe::Recipe;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use tracing::error;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub struct RecipeValidationError {
|
||||
pub status: StatusCode,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub struct RecipeManifestWithPath {
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct RecipeManifest {
|
||||
pub id: String,
|
||||
pub recipe: Recipe,
|
||||
#[schema(value_type = String)]
|
||||
pub file_path: PathBuf,
|
||||
pub last_modified: String,
|
||||
pub schedule_cron: Option<String>,
|
||||
pub slash_command: Option<String>,
|
||||
}
|
||||
|
||||
pub fn short_id_from_path(path: &str) -> String {
|
||||
@@ -38,7 +43,7 @@ pub fn short_id_from_path(path: &str) -> String {
|
||||
format!("{:016x}", h)
|
||||
}
|
||||
|
||||
pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
||||
pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifest>> {
|
||||
let recipes_with_path = list_local_recipes()?;
|
||||
let mut recipe_manifests_with_path = Vec::new();
|
||||
for (file_path, recipe) in recipes_with_path {
|
||||
@@ -48,11 +53,13 @@ pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let manifest_with_path = RecipeManifestWithPath {
|
||||
let manifest_with_path = RecipeManifest {
|
||||
id: short_id_from_path(file_path.to_string_lossy().as_ref()),
|
||||
recipe,
|
||||
file_path,
|
||||
last_modified,
|
||||
schedule_cron: None,
|
||||
slash_command: None,
|
||||
};
|
||||
recipe_manifests_with_path.push(manifest_with_path);
|
||||
}
|
||||
|
||||
@@ -88,10 +88,7 @@ async fn create_schedule(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateScheduleRequest>,
|
||||
) -> Result<Json<ScheduledJob>, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
tracing::info!(
|
||||
"Server: Calling scheduler.add_scheduled_job() for job '{}'",
|
||||
@@ -108,7 +105,7 @@ async fn create_schedule(
|
||||
process_start_time: None,
|
||||
};
|
||||
scheduler
|
||||
.add_scheduled_job(job.clone())
|
||||
.add_scheduled_job(job.clone(), true)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("Error creating schedule: {:?}", e); // Log error
|
||||
@@ -136,10 +133,7 @@ async fn create_schedule(
|
||||
async fn list_schedules(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<ListSchedulesResponse>, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
tracing::info!("Server: Calling scheduler.list_scheduled_jobs()");
|
||||
let jobs = scheduler.list_scheduled_jobs().await;
|
||||
@@ -164,17 +158,17 @@ async fn delete_schedule(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
let scheduler = state.scheduler();
|
||||
scheduler
|
||||
.remove_scheduled_job(&id, true)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
scheduler.remove_scheduled_job(&id).await.map_err(|e| {
|
||||
eprintln!("Error deleting schedule '{}': {:?}", id, e);
|
||||
match e {
|
||||
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
})?;
|
||||
.map_err(|e| {
|
||||
eprintln!("Error deleting schedule '{}': {:?}", id, e);
|
||||
match e {
|
||||
goose::scheduler::SchedulerError::JobNotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
})?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -196,10 +190,7 @@ async fn run_now_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<RunNowResponse>, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
let (recipe_display_name, recipe_version_opt) = if let Some(job) = scheduler
|
||||
.list_scheduled_jobs()
|
||||
@@ -291,10 +282,7 @@ async fn sessions_handler(
|
||||
Path(schedule_id_param): Path<String>, // Renamed to avoid confusion with session_id
|
||||
Query(query_params): Query<SessionsQuery>,
|
||||
) -> Result<Json<Vec<SessionDisplayInfo>>, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
match scheduler
|
||||
.sessions(&schedule_id_param, query_params.limit)
|
||||
@@ -349,10 +337,7 @@ async fn pause_schedule(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
scheduler.pause_schedule(&id).await.map_err(|e| {
|
||||
eprintln!("Error pausing schedule '{}': {:?}", id, e);
|
||||
@@ -383,10 +368,7 @@ async fn unpause_schedule(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
scheduler.unpause_schedule(&id).await.map_err(|e| {
|
||||
eprintln!("Error unpausing schedule '{}': {:?}", id, e);
|
||||
@@ -419,10 +401,7 @@ async fn update_schedule(
|
||||
Path(id): Path<String>,
|
||||
Json(req): Json<UpdateScheduleRequest>,
|
||||
) -> Result<Json<ScheduledJob>, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
scheduler
|
||||
.update_schedule(&id, req.cron)
|
||||
@@ -459,10 +438,7 @@ pub async fn kill_running_job(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<KillJobResponse>, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
scheduler.kill_running_job(&id).await.map_err(|e| {
|
||||
eprintln!("Error killing running job '{}': {:?}", id, e);
|
||||
@@ -496,10 +472,7 @@ pub async fn inspect_running_job(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<InspectJobResponse>, StatusCode> {
|
||||
let scheduler = state
|
||||
.scheduler()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let scheduler = state.scheduler();
|
||||
|
||||
match scheduler.get_running_job_info(&id).await {
|
||||
Ok(info) => {
|
||||
|
||||
@@ -26,8 +26,8 @@ impl AppState {
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn scheduler(&self) -> Result<Arc<dyn SchedulerTrait>, anyhow::Error> {
|
||||
self.agent_manager.scheduler().await
|
||||
pub fn scheduler(&self) -> Arc<dyn SchedulerTrait> {
|
||||
self.agent_manager.scheduler()
|
||||
}
|
||||
|
||||
pub async fn set_recipe_file_hash_map(&self, hash_map: HashMap<String, PathBuf>) {
|
||||
|
||||
@@ -59,14 +59,15 @@ use super::final_output_tool::FinalOutputTool;
|
||||
use super::platform_tools;
|
||||
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
||||
use crate::agents::subagent_task_config::TaskConfig;
|
||||
use crate::conversation::message::{Message, MessageContent, SystemNotificationType, ToolRequest};
|
||||
use crate::conversation::message::{Message, SystemNotificationType, ToolRequest};
|
||||
use crate::scheduler_trait::SchedulerTrait;
|
||||
use crate::session::extension_data::{EnabledExtensionsState, ExtensionState};
|
||||
use crate::session::{Session, SessionManager};
|
||||
|
||||
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||
const COMPACTION_THINKING_TEXT: &str = "goose is compacting the conversation...";
|
||||
pub const MANUAL_COMPACT_TRIGGER: &str = "Please compact this conversation";
|
||||
pub const MANUAL_COMPACT_TRIGGERS: &[&str] =
|
||||
&["Please compact this conversation", "/compact", "/summarize"];
|
||||
|
||||
/// Context needed for the reply function
|
||||
pub struct ReplyContext {
|
||||
@@ -776,15 +777,35 @@ impl Agent {
|
||||
session_config: SessionConfig,
|
||||
cancel_token: Option<CancellationToken>,
|
||||
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
|
||||
let is_manual_compact = user_message.content.iter().any(|c| {
|
||||
if let MessageContent::Text(text) = c {
|
||||
text.text.trim() == MANUAL_COMPACT_TRIGGER
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
let message_text = user_message.as_concat_text();
|
||||
let is_manual_compact = MANUAL_COMPACT_TRIGGERS.contains(&message_text.trim());
|
||||
|
||||
let slash_command_recipe = if message_text.trim().starts_with('/') {
|
||||
let command = message_text.split_whitespace().next();
|
||||
command.and_then(crate::slash_commands::resolve_slash_command)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(recipe) = slash_command_recipe {
|
||||
let prompt = [recipe.instructions.as_deref(), recipe.prompt.as_deref()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
let prompt_message = Message::user()
|
||||
.with_text(prompt)
|
||||
.with_visibility(false, true);
|
||||
SessionManager::add_message(&session_config.id, &prompt_message).await?;
|
||||
SessionManager::add_message(
|
||||
&session_config.id,
|
||||
&user_message.with_visibility(true, false),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
SessionManager::add_message(&session_config.id, &user_message).await?;
|
||||
}
|
||||
|
||||
SessionManager::add_message(&session_config.id, &user_message).await?;
|
||||
let session = SessionManager::get_session(&session_config.id, true).await?;
|
||||
|
||||
let conversation = session
|
||||
|
||||
@@ -26,7 +26,7 @@ mod tool_route_manager;
|
||||
mod tool_router_index_manager;
|
||||
pub mod types;
|
||||
|
||||
pub use agent::{Agent, AgentEvent, MANUAL_COMPACT_TRIGGER};
|
||||
pub use agent::{Agent, AgentEvent, MANUAL_COMPACT_TRIGGERS};
|
||||
pub use extension::ExtensionConfig;
|
||||
pub use extension_manager::ExtensionManager;
|
||||
pub use prompt_manager::PromptManager;
|
||||
|
||||
@@ -164,7 +164,7 @@ impl Agent {
|
||||
process_start_time: None,
|
||||
};
|
||||
|
||||
match scheduler.add_scheduled_job(job).await {
|
||||
match scheduler.add_scheduled_job(job, true).await {
|
||||
Ok(()) => Ok(vec![Content::text(format!(
|
||||
"Successfully created scheduled job '{}' for recipe '{}' with cron expression '{}' in {} mode",
|
||||
job_id, recipe_path, cron_expression, execution_mode
|
||||
@@ -284,7 +284,7 @@ impl Agent {
|
||||
)
|
||||
})?;
|
||||
|
||||
match scheduler.remove_scheduled_job(job_id).await {
|
||||
match scheduler.remove_scheduled_job(job_id, true).await {
|
||||
Ok(()) => Ok(vec![Content::text(format!(
|
||||
"Successfully deleted job '{}'",
|
||||
job_id
|
||||
|
||||
@@ -59,8 +59,8 @@ impl AgentManager {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn scheduler(&self) -> Result<Arc<dyn SchedulerTrait>> {
|
||||
Ok(Arc::clone(&self.scheduler))
|
||||
pub fn scheduler(&self) -> Arc<dyn SchedulerTrait> {
|
||||
Arc::clone(&self.scheduler)
|
||||
}
|
||||
|
||||
pub async fn set_default_provider(&self, provider: Arc<dyn crate::providers::base::Provider>) {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod scheduler_trait;
|
||||
pub mod security;
|
||||
pub mod session;
|
||||
pub mod session_context;
|
||||
pub mod slash_commands;
|
||||
pub mod subprocess;
|
||||
pub mod token_counter;
|
||||
pub mod tool_inspection;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn get_recipe_library_dir(is_global: bool) -> PathBuf {
|
||||
if is_global {
|
||||
Paths::config_dir().join("recipes")
|
||||
} else {
|
||||
std::env::current_dir().unwrap().join(".goose/recipes")
|
||||
env::current_dir().unwrap().join(".goose/recipes")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+119
-32
@@ -269,6 +269,7 @@ impl Scheduler {
|
||||
pub async fn add_scheduled_job(
|
||||
&self,
|
||||
original_job_spec: ScheduledJob,
|
||||
make_copy: bool,
|
||||
) -> Result<(), SchedulerError> {
|
||||
{
|
||||
let jobs_guard = self.jobs.lock().await;
|
||||
@@ -277,29 +278,30 @@ impl Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
let original_recipe_path = Path::new(&original_job_spec.source);
|
||||
if !original_recipe_path.is_file() {
|
||||
return Err(SchedulerError::RecipeLoadError(format!(
|
||||
"Recipe file not found: {}",
|
||||
original_job_spec.source
|
||||
)));
|
||||
}
|
||||
|
||||
let scheduled_recipes_dir = get_default_scheduled_recipes_dir()?;
|
||||
let original_extension = original_recipe_path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("yaml");
|
||||
|
||||
let destination_filename = format!("{}.{}", original_job_spec.id, original_extension);
|
||||
let destination_recipe_path = scheduled_recipes_dir.join(destination_filename);
|
||||
|
||||
fs::copy(original_recipe_path, &destination_recipe_path)?;
|
||||
|
||||
let mut stored_job = original_job_spec;
|
||||
stored_job.source = destination_recipe_path.to_string_lossy().into_owned();
|
||||
stored_job.current_session_id = None;
|
||||
stored_job.process_start_time = None;
|
||||
if make_copy {
|
||||
let original_recipe_path = Path::new(&stored_job.source);
|
||||
if !original_recipe_path.is_file() {
|
||||
return Err(SchedulerError::RecipeLoadError(format!(
|
||||
"Recipe file not found: {}",
|
||||
stored_job.source
|
||||
)));
|
||||
}
|
||||
|
||||
let scheduled_recipes_dir = get_default_scheduled_recipes_dir()?;
|
||||
let original_extension = original_recipe_path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("yaml");
|
||||
|
||||
let destination_filename = format!("{}.{}", stored_job.id, original_extension);
|
||||
let destination_recipe_path = scheduled_recipes_dir.join(destination_filename);
|
||||
|
||||
fs::copy(original_recipe_path, &destination_recipe_path)?;
|
||||
stored_job.source = destination_recipe_path.to_string_lossy().into_owned();
|
||||
stored_job.current_session_id = None;
|
||||
stored_job.process_start_time = None;
|
||||
}
|
||||
|
||||
let cron_task = self.create_cron_task(stored_job.clone())?;
|
||||
|
||||
@@ -318,6 +320,69 @@ impl Scheduler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn schedule_recipe(
|
||||
&self,
|
||||
recipe_path: PathBuf,
|
||||
cron_schedule: Option<String>,
|
||||
) -> Result<(), SchedulerError> {
|
||||
let recipe_path_str = recipe_path.to_string_lossy().to_string();
|
||||
|
||||
let existing_job_id = {
|
||||
let jobs_guard = self.jobs.lock().await;
|
||||
jobs_guard
|
||||
.iter()
|
||||
.find(|(_, (_, job))| job.source == recipe_path_str)
|
||||
.map(|(id, _)| id.clone())
|
||||
};
|
||||
|
||||
match cron_schedule {
|
||||
Some(cron) => {
|
||||
if let Some(job_id) = existing_job_id {
|
||||
self.update_schedule(&job_id, cron).await
|
||||
} else {
|
||||
let job_id = self.generate_unique_job_id(&recipe_path).await;
|
||||
let job = ScheduledJob {
|
||||
id: job_id,
|
||||
source: recipe_path_str,
|
||||
cron,
|
||||
last_run: None,
|
||||
currently_running: false,
|
||||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
};
|
||||
self.add_scheduled_job(job, false).await
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(job_id) = existing_job_id {
|
||||
self.remove_scheduled_job(&job_id, false).await
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_unique_job_id(&self, path: &Path) -> String {
|
||||
let base_id = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unnamed")
|
||||
.to_string();
|
||||
|
||||
let jobs_guard = self.jobs.lock().await;
|
||||
let mut id = base_id.clone();
|
||||
let mut counter = 1;
|
||||
|
||||
while jobs_guard.contains_key(&id) {
|
||||
id = format!("{}_{}", base_id, counter);
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
async fn load_jobs_from_storage(self: &Arc<Self>) {
|
||||
if !self.storage_path.exists() {
|
||||
return;
|
||||
@@ -395,7 +460,11 @@ impl Scheduler {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError> {
|
||||
pub async fn remove_scheduled_job(
|
||||
&self,
|
||||
id: &str,
|
||||
remove_recipe: bool,
|
||||
) -> Result<(), SchedulerError> {
|
||||
let (job_uuid, recipe_path) = {
|
||||
let mut jobs_guard = self.jobs.lock().await;
|
||||
match jobs_guard.remove(id) {
|
||||
@@ -409,9 +478,11 @@ impl Scheduler {
|
||||
.await
|
||||
.map_err(|e| SchedulerError::SchedulerInternalError(e.to_string()))?;
|
||||
|
||||
let path = Path::new(&recipe_path);
|
||||
if path.exists() {
|
||||
fs::remove_file(path)?;
|
||||
if remove_recipe {
|
||||
let path = Path::new(&recipe_path);
|
||||
if path.exists() {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
persist_jobs(&self.storage_path, &self.jobs).await?;
|
||||
@@ -733,16 +804,32 @@ async fn execute_job(
|
||||
|
||||
#[async_trait]
|
||||
impl SchedulerTrait for Scheduler {
|
||||
async fn add_scheduled_job(&self, job: ScheduledJob) -> Result<(), SchedulerError> {
|
||||
self.add_scheduled_job(job).await
|
||||
async fn add_scheduled_job(
|
||||
&self,
|
||||
job: ScheduledJob,
|
||||
make_copy: bool,
|
||||
) -> Result<(), SchedulerError> {
|
||||
self.add_scheduled_job(job, make_copy).await
|
||||
}
|
||||
|
||||
async fn schedule_recipe(
|
||||
&self,
|
||||
recipe_path: PathBuf,
|
||||
cron_schedule: Option<String>,
|
||||
) -> Result<(), SchedulerError> {
|
||||
self.schedule_recipe(recipe_path, cron_schedule).await
|
||||
}
|
||||
|
||||
async fn list_scheduled_jobs(&self) -> Vec<ScheduledJob> {
|
||||
self.list_scheduled_jobs().await
|
||||
}
|
||||
|
||||
async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError> {
|
||||
self.remove_scheduled_job(id).await
|
||||
async fn remove_scheduled_job(
|
||||
&self,
|
||||
id: &str,
|
||||
remove_recipe: bool,
|
||||
) -> Result<(), SchedulerError> {
|
||||
self.remove_scheduled_job(id, remove_recipe).await
|
||||
}
|
||||
|
||||
async fn pause_schedule(&self, id: &str) -> Result<(), SchedulerError> {
|
||||
@@ -815,7 +902,7 @@ mod tests {
|
||||
process_start_time: None,
|
||||
};
|
||||
|
||||
scheduler.add_scheduled_job(job).await.unwrap();
|
||||
scheduler.add_scheduled_job(job, true).await.unwrap();
|
||||
sleep(Duration::from_millis(1500)).await;
|
||||
|
||||
let jobs = scheduler.list_scheduled_jobs().await;
|
||||
@@ -840,7 +927,7 @@ mod tests {
|
||||
process_start_time: None,
|
||||
};
|
||||
|
||||
scheduler.add_scheduled_job(job).await.unwrap();
|
||||
scheduler.add_scheduled_job(job, true).await.unwrap();
|
||||
scheduler.pause_schedule("paused_job").await.unwrap();
|
||||
sleep(Duration::from_millis(1500)).await;
|
||||
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::scheduler::{ScheduledJob, SchedulerError};
|
||||
use crate::session::Session;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SchedulerTrait: Send + Sync {
|
||||
async fn add_scheduled_job(&self, job: ScheduledJob) -> Result<(), SchedulerError>;
|
||||
async fn add_scheduled_job(
|
||||
&self,
|
||||
job: ScheduledJob,
|
||||
copy_recipe: bool,
|
||||
) -> Result<(), SchedulerError>;
|
||||
async fn schedule_recipe(
|
||||
&self,
|
||||
recipe_path: PathBuf,
|
||||
cron_schedule: Option<String>,
|
||||
) -> anyhow::Result<(), SchedulerError>;
|
||||
async fn list_scheduled_jobs(&self) -> Vec<ScheduledJob>;
|
||||
async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError>;
|
||||
async fn remove_scheduled_job(
|
||||
&self,
|
||||
id: &str,
|
||||
remove_recipe: bool,
|
||||
) -> Result<(), SchedulerError>;
|
||||
async fn pause_schedule(&self, id: &str) -> Result<(), SchedulerError>;
|
||||
async fn unpause_schedule(&self, id: &str) -> Result<(), SchedulerError>;
|
||||
async fn run_now(&self, id: &str) -> Result<String, SchedulerError>;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::recipe::Recipe;
|
||||
|
||||
const SLASH_COMMANDS_CONFIG_KEY: &str = "slash_commands";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SlashCommandMapping {
|
||||
pub command: String,
|
||||
pub recipe_path: String,
|
||||
}
|
||||
|
||||
pub fn list_commands() -> Vec<SlashCommandMapping> {
|
||||
Config::global()
|
||||
.get_param(SLASH_COMMANDS_CONFIG_KEY)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
"Failed to load {}: {}. Falling back to empty list.",
|
||||
SLASH_COMMANDS_CONFIG_KEY, err
|
||||
);
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
||||
fn save_slash_commands(commands: Vec<SlashCommandMapping>) -> Result<()> {
|
||||
Config::global()
|
||||
.set_param(SLASH_COMMANDS_CONFIG_KEY, &commands)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save slash commands: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_recipe_slash_command(recipe_path: PathBuf, command: Option<String>) -> Result<()> {
|
||||
let recipe_path_str = recipe_path.to_string_lossy().to_string();
|
||||
|
||||
let mut commands = list_commands();
|
||||
commands.retain(|mapping| mapping.recipe_path != recipe_path_str);
|
||||
|
||||
if let Some(cmd) = command {
|
||||
let normalized_cmd = cmd.trim_start_matches('/').to_lowercase();
|
||||
if !normalized_cmd.is_empty() {
|
||||
commands.push(SlashCommandMapping {
|
||||
command: normalized_cmd,
|
||||
recipe_path: recipe_path_str,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
save_slash_commands(commands)
|
||||
}
|
||||
|
||||
pub fn get_recipe_for_command(command: &str) -> Option<PathBuf> {
|
||||
let normalized = command.trim_start_matches('/').to_lowercase();
|
||||
let commands = list_commands();
|
||||
commands
|
||||
.into_iter()
|
||||
.find(|mapping| mapping.command == normalized)
|
||||
.map(|mapping| PathBuf::from(mapping.recipe_path))
|
||||
}
|
||||
|
||||
pub fn resolve_slash_command(command: &str) -> Option<Recipe> {
|
||||
let recipe_path = get_recipe_for_command(command)?;
|
||||
|
||||
if !recipe_path.exists() {
|
||||
return None;
|
||||
}
|
||||
let recipe_content = std::fs::read_to_string(&recipe_path).ok()?;
|
||||
let recipe = Recipe::from_content(&recipe_content).ok()?;
|
||||
|
||||
Some(recipe)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ mod tests {
|
||||
use goose::scheduler::{ScheduledJob, SchedulerError};
|
||||
use goose::scheduler_trait::SchedulerTrait;
|
||||
use goose::session::Session;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct MockScheduler {
|
||||
@@ -34,18 +35,34 @@ mod tests {
|
||||
|
||||
#[async_trait]
|
||||
impl SchedulerTrait for MockScheduler {
|
||||
async fn add_scheduled_job(&self, job: ScheduledJob) -> Result<(), SchedulerError> {
|
||||
async fn add_scheduled_job(
|
||||
&self,
|
||||
job: ScheduledJob,
|
||||
_copy: bool,
|
||||
) -> Result<(), SchedulerError> {
|
||||
let mut jobs = self.jobs.lock().await;
|
||||
jobs.push(job);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn schedule_recipe(
|
||||
&self,
|
||||
_recipe_path: PathBuf,
|
||||
_cron_schedule: Option<String>,
|
||||
) -> Result<(), SchedulerError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_scheduled_jobs(&self) -> Vec<ScheduledJob> {
|
||||
let jobs = self.jobs.lock().await;
|
||||
jobs.clone()
|
||||
}
|
||||
|
||||
async fn remove_scheduled_job(&self, id: &str) -> Result<(), SchedulerError> {
|
||||
async fn remove_scheduled_job(
|
||||
&self,
|
||||
id: &str,
|
||||
_remove: bool,
|
||||
) -> Result<(), SchedulerError> {
|
||||
let mut jobs = self.jobs.lock().await;
|
||||
if let Some(pos) = jobs.iter().position(|job| job.id == id) {
|
||||
jobs.remove(pos);
|
||||
|
||||
Reference in New Issue
Block a user