mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
feat(CLI): Add optional --parameters to scheduled recipe (#8741)
Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
parent
bc6e0b25dc
commit
ba60b597fa
9 changed files with 108 additions and 19 deletions
|
|
@ -576,6 +576,14 @@ enum SchedulerCommand {
|
|||
help = "Recipe source (path to file, or base64 encoded recipe string)"
|
||||
)]
|
||||
recipe_source: String,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "KEY=VALUE",
|
||||
help = "Recipe parameter in KEY=VALUE format (can be specified multiple times)",
|
||||
action = clap::ArgAction::Append,
|
||||
value_parser = parse_key_val,
|
||||
)]
|
||||
params: Vec<(String, String)>,
|
||||
},
|
||||
#[command(about = "List all scheduled jobs")]
|
||||
List {},
|
||||
|
|
@ -1562,7 +1570,8 @@ async fn handle_schedule_command(command: SchedulerCommand) -> Result<()> {
|
|||
schedule_id,
|
||||
cron,
|
||||
recipe_source,
|
||||
} => handle_schedule_add(schedule_id, cron, recipe_source).await,
|
||||
params,
|
||||
} => handle_schedule_add(schedule_id, cron, recipe_source, params).await,
|
||||
SchedulerCommand::List {} => handle_schedule_list().await,
|
||||
SchedulerCommand::Remove { schedule_id } => handle_schedule_remove(schedule_id).await,
|
||||
SchedulerCommand::Sessions { schedule_id, limit } => {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ pub async fn handle_schedule_add(
|
|||
schedule_id: String,
|
||||
cron: String,
|
||||
recipe_source_arg: String, // This is expected to be a file path by the Scheduler
|
||||
params: Vec<(String, String)>,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"[CLI Debug] Scheduling job ID: {}, Cron: {}, Recipe Source Path: {}",
|
||||
|
|
@ -88,6 +89,8 @@ pub async fn handle_schedule_add(
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: params,
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
|
||||
let scheduler_storage_path =
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@ async fn create_schedule(
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: vec![],
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
|
||||
let scheduler = state.scheduler();
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ impl Agent {
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: vec![],
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
|
||||
match scheduler.add_scheduled_job(job, true).await {
|
||||
|
|
|
|||
|
|
@ -173,7 +173,14 @@ pub fn resolve_sub_recipe_path(
|
|||
});
|
||||
}
|
||||
|
||||
Ok(path.display().to_string())
|
||||
let canonical = path.canonicalize().map_err(|e| RecipeError::Invalid {
|
||||
source: anyhow::anyhow!(
|
||||
"Failed to resolve sub-recipe path {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
),
|
||||
})?;
|
||||
Ok(canonical.display().to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -446,7 +446,10 @@ instructions: Child instructions"#;
|
|||
let result = resolve_sub_recipe_path("./sub-recipes/child.yaml", parent_dir);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let expected_path = parent_dir.join("./sub-recipes/child.yaml");
|
||||
let expected_path = parent_dir
|
||||
.join("sub-recipes/child.yaml")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
assert_eq!(result.unwrap(), expected_path.to_str().unwrap());
|
||||
}
|
||||
|
||||
|
|
@ -466,7 +469,8 @@ instructions: Absolute instructions"#;
|
|||
|
||||
let result = resolve_sub_recipe_path(absolute_path_str, parent_dir);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), absolute_path_str);
|
||||
let expected = absolute_path.canonicalize().unwrap();
|
||||
assert_eq!(result.unwrap(), expected.to_str().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -534,7 +538,10 @@ instructions: Child instructions
|
|||
assert_eq!(sub_recipes.len(), 1);
|
||||
assert_eq!(sub_recipes[0].name, "child");
|
||||
|
||||
let expected_absolute_path = temp_path.join("./sub-recipes/child.yaml");
|
||||
let expected_absolute_path = temp_path
|
||||
.join("sub-recipes/child.yaml")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
sub_recipes[0].path,
|
||||
expected_absolute_path.to_str().unwrap()
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use crate::conversation::Conversation;
|
|||
#[cfg(feature = "telemetry")]
|
||||
use crate::posthog;
|
||||
use crate::providers::create;
|
||||
use crate::recipe::build_recipe::build_recipe_from_template;
|
||||
use crate::recipe::Recipe;
|
||||
use crate::scheduler_trait::SchedulerTrait;
|
||||
use crate::session::session_manager::SessionType;
|
||||
|
|
@ -115,6 +116,13 @@ pub struct ScheduledJob {
|
|||
pub current_session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub process_start_time: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub parameters: Vec<(String, String)>,
|
||||
/// Original directory of the recipe file before it was copied to scheduled_recipes/.
|
||||
/// Preserved so that relative paths (sub-recipes, template includes) resolve correctly
|
||||
/// against the source tree rather than the scheduler's internal storage directory.
|
||||
#[serde(default)]
|
||||
pub recipe_base_dir: Option<String>,
|
||||
}
|
||||
|
||||
async fn persist_jobs(
|
||||
|
|
@ -291,7 +299,13 @@ impl Scheduler {
|
|||
|
||||
let mut stored_job = original_job_spec;
|
||||
if make_copy {
|
||||
let original_recipe_path = Path::new(&stored_job.source);
|
||||
let original_recipe_path =
|
||||
Path::new(&stored_job.source).canonicalize().map_err(|e| {
|
||||
SchedulerError::RecipeLoadError(format!(
|
||||
"Recipe file not found: {}: {}",
|
||||
stored_job.source, e
|
||||
))
|
||||
})?;
|
||||
if !original_recipe_path.is_file() {
|
||||
return Err(SchedulerError::RecipeLoadError(format!(
|
||||
"Recipe file not found: {}",
|
||||
|
|
@ -308,7 +322,10 @@ impl Scheduler {
|
|||
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)?;
|
||||
fs::copy(&original_recipe_path, &destination_recipe_path)?;
|
||||
stored_job.recipe_base_dir = original_recipe_path
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().into_owned());
|
||||
stored_job.source = destination_recipe_path.to_string_lossy().into_owned();
|
||||
stored_job.current_session_id = None;
|
||||
stored_job.process_start_time = None;
|
||||
|
|
@ -361,6 +378,8 @@ impl Scheduler {
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: vec![],
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
self.add_scheduled_job(job, false).await
|
||||
}
|
||||
|
|
@ -792,20 +811,24 @@ async fn execute_job(
|
|||
|
||||
let recipe_path = Path::new(&job.source);
|
||||
let recipe_content = fs::read_to_string(recipe_path)?;
|
||||
|
||||
let recipe: Recipe = {
|
||||
let extension = recipe_path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("yaml")
|
||||
.to_lowercase();
|
||||
|
||||
match extension.as_str() {
|
||||
"json" | "jsonl" => serde_json::from_str(&recipe_content)?,
|
||||
_ => serde_yaml::from_str(&recipe_content)?,
|
||||
}
|
||||
// Use the original recipe directory for path resolution so that relative
|
||||
// references (sub-recipes, template includes) survive the copy into scheduled_recipes/.
|
||||
let recipe_dir_owned;
|
||||
let recipe_dir = if let Some(ref base) = job.recipe_base_dir {
|
||||
recipe_dir_owned = PathBuf::from(base);
|
||||
recipe_dir_owned.as_path()
|
||||
} else {
|
||||
recipe_path.parent().unwrap_or(Path::new("."))
|
||||
};
|
||||
|
||||
let recipe: Recipe = build_recipe_from_template(
|
||||
recipe_content,
|
||||
recipe_dir,
|
||||
job.parameters.clone(),
|
||||
None::<fn(&str, &str) -> anyhow::Result<String>>,
|
||||
)
|
||||
.map_err(|e| anyhow!(e.to_string()))?;
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
let config = Config::global();
|
||||
|
|
@ -1106,6 +1129,8 @@ mod tests {
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: vec![],
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
|
||||
scheduler.add_scheduled_job(job, true).await.unwrap();
|
||||
|
|
@ -1138,6 +1163,8 @@ mod tests {
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: vec![],
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
|
||||
scheduler.add_scheduled_job(job, true).await.unwrap();
|
||||
|
|
@ -1165,6 +1192,8 @@ mod tests {
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: vec![],
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
|
||||
scheduler
|
||||
|
|
@ -1220,6 +1249,8 @@ mod tests {
|
|||
paused: false,
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
parameters: vec![],
|
||||
recipe_base_dir: None,
|
||||
};
|
||||
|
||||
// Schedule the job and let it run — should not panic
|
||||
|
|
|
|||
|
|
@ -7712,6 +7712,22 @@
|
|||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"parameters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"allOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"paused": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -7720,6 +7736,11 @@
|
|||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"recipe_base_dir": {
|
||||
"type": "string",
|
||||
"description": "Original directory of the recipe file before it was copied to scheduled_recipes/.\nPreserved so that relative paths (sub-recipes, template includes) resolve correctly\nagainst the source tree rather than the scheduler's internal storage directory.",
|
||||
"nullable": true
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1255,8 +1255,15 @@ export type ScheduledJob = {
|
|||
currently_running?: boolean;
|
||||
id: string;
|
||||
last_run?: string | null;
|
||||
parameters?: Array<Array<string>>;
|
||||
paused?: boolean;
|
||||
process_start_time?: string | null;
|
||||
/**
|
||||
* Original directory of the recipe file before it was copied to scheduled_recipes/.
|
||||
* Preserved so that relative paths (sub-recipes, template includes) resolve correctly
|
||||
* against the source tree rather than the scheduler's internal storage directory.
|
||||
*/
|
||||
recipe_base_dir?: string | null;
|
||||
source: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue