657718d8c0
Co-authored-by: jack <jack@deck.local>
216 lines
7.1 KiB
Rust
216 lines
7.1 KiB
Rust
use anyhow::{anyhow, Result};
|
|
use clap::Parser;
|
|
use goose::agents::{Agent, SessionConfig};
|
|
use goose::config::Config;
|
|
use goose::message::Message;
|
|
use goose::providers::create;
|
|
use goose::recipe::Recipe;
|
|
use goose::session;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use tracing::info;
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(author, version, about, long_about = None)]
|
|
struct Args {
|
|
/// Job ID for the scheduled job
|
|
job_id: String,
|
|
|
|
/// Path to the recipe file to execute
|
|
recipe_path: String,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Initialize tracing
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
|
.init();
|
|
|
|
let args = Args::parse();
|
|
|
|
info!("Starting goose-scheduler-executor for job: {}", args.job_id);
|
|
info!("Recipe path: {}", args.recipe_path);
|
|
|
|
// Execute the recipe and get session ID
|
|
let session_id = execute_recipe(&args.job_id, &args.recipe_path).await?;
|
|
|
|
// Output session ID to stdout (this is what the Go service expects)
|
|
println!("{}", session_id);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn execute_recipe(job_id: &str, recipe_path: &str) -> Result<String> {
|
|
let recipe_path_buf = Path::new(recipe_path);
|
|
|
|
// Check if recipe file exists
|
|
if !recipe_path_buf.exists() {
|
|
return Err(anyhow!("Recipe file not found: {}", recipe_path));
|
|
}
|
|
|
|
// Read and parse recipe
|
|
let recipe_content = fs::read_to_string(recipe_path_buf)?;
|
|
let recipe: Recipe = {
|
|
let extension = recipe_path_buf
|
|
.extension()
|
|
.and_then(|os_str| os_str.to_str())
|
|
.unwrap_or("yaml")
|
|
.to_lowercase();
|
|
|
|
match extension.as_str() {
|
|
"json" | "jsonl" => serde_json::from_str::<Recipe>(&recipe_content)
|
|
.map_err(|e| anyhow!("Failed to parse JSON recipe '{}': {}", recipe_path, e))?,
|
|
"yaml" | "yml" => serde_yaml::from_str::<Recipe>(&recipe_content)
|
|
.map_err(|e| anyhow!("Failed to parse YAML recipe '{}': {}", recipe_path, e))?,
|
|
_ => {
|
|
return Err(anyhow!(
|
|
"Unsupported recipe file extension '{}' for: {}",
|
|
extension,
|
|
recipe_path
|
|
));
|
|
}
|
|
}
|
|
};
|
|
|
|
// Create agent
|
|
let agent = Agent::new();
|
|
|
|
// Get provider configuration
|
|
let global_config = Config::global();
|
|
let provider_name: String = global_config.get_param("GOOSE_PROVIDER").map_err(|_| {
|
|
anyhow!("GOOSE_PROVIDER not configured. Run 'goose configure' or set env var.")
|
|
})?;
|
|
let model_name: String = global_config.get_param("GOOSE_MODEL").map_err(|_| {
|
|
anyhow!("GOOSE_MODEL not configured. Run 'goose configure' or set env var.")
|
|
})?;
|
|
|
|
let model_config = goose::model::ModelConfig::new(model_name);
|
|
let provider = create(&provider_name, model_config)
|
|
.map_err(|e| anyhow!("Failed to create provider '{}': {}", provider_name, e))?;
|
|
|
|
// Set provider on agent
|
|
agent
|
|
.update_provider(provider)
|
|
.await
|
|
.map_err(|e| anyhow!("Failed to set provider on agent: {}", e))?;
|
|
|
|
info!(
|
|
"Agent configured with provider '{}' for job '{}'",
|
|
provider_name, job_id
|
|
);
|
|
|
|
// Generate session ID
|
|
let session_id = session::generate_session_id();
|
|
|
|
// Check if recipe has a prompt
|
|
let Some(prompt_text) = recipe.prompt else {
|
|
info!(
|
|
"Recipe '{}' has no prompt to execute for job '{}'",
|
|
recipe_path, job_id
|
|
);
|
|
|
|
// Create empty session for consistency
|
|
let session_file_path = goose::session::storage::get_path(
|
|
goose::session::storage::Identifier::Name(session_id.clone()),
|
|
);
|
|
|
|
let metadata = goose::session::storage::SessionMetadata {
|
|
working_dir: env::current_dir().unwrap_or_default(),
|
|
description: "Empty job - no prompt".to_string(),
|
|
schedule_id: Some(job_id.to_string()),
|
|
message_count: 0,
|
|
..Default::default()
|
|
};
|
|
|
|
goose::session::storage::save_messages_with_metadata(&session_file_path, &metadata, &[])
|
|
.map_err(|e| anyhow!("Failed to persist metadata for empty job: {}", e))?;
|
|
|
|
return Ok(session_id);
|
|
};
|
|
|
|
// Create session configuration
|
|
let current_dir =
|
|
env::current_dir().map_err(|e| anyhow!("Failed to get current directory: {}", e))?;
|
|
|
|
let session_config = SessionConfig {
|
|
id: goose::session::storage::Identifier::Name(session_id.clone()),
|
|
working_dir: current_dir.clone(),
|
|
schedule_id: Some(job_id.to_string()),
|
|
};
|
|
|
|
// Execute the recipe
|
|
let mut messages = vec![Message::user().with_text(prompt_text)];
|
|
|
|
info!("Executing recipe for job '{}' with prompt", job_id);
|
|
|
|
let mut stream = agent
|
|
.reply(&messages, Some(session_config))
|
|
.await
|
|
.map_err(|e| anyhow!("Agent failed to reply for recipe '{}': {}", recipe_path, e))?;
|
|
|
|
// Process the response stream
|
|
use futures::StreamExt;
|
|
use goose::agents::AgentEvent;
|
|
|
|
while let Some(message_result) = stream.next().await {
|
|
match message_result {
|
|
Ok(AgentEvent::Message(msg)) => {
|
|
if msg.role == mcp_core::role::Role::Assistant {
|
|
info!("[Job {}] Assistant response received", job_id);
|
|
}
|
|
messages.push(msg);
|
|
}
|
|
Ok(AgentEvent::McpNotification(_)) => {
|
|
// Handle notifications if needed
|
|
}
|
|
Ok(AgentEvent::ModelChange { .. }) => {
|
|
// Model change events are informational, just continue
|
|
}
|
|
Err(e) => {
|
|
return Err(anyhow!("Error receiving message from agent: {}", e));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save session
|
|
let session_file_path = goose::session::storage::get_path(
|
|
goose::session::storage::Identifier::Name(session_id.clone()),
|
|
);
|
|
|
|
// Try to read updated metadata, or create fallback
|
|
match goose::session::storage::read_metadata(&session_file_path) {
|
|
Ok(mut updated_metadata) => {
|
|
updated_metadata.message_count = messages.len();
|
|
goose::session::storage::save_messages_with_metadata(
|
|
&session_file_path,
|
|
&updated_metadata,
|
|
&messages,
|
|
)
|
|
.map_err(|e| anyhow!("Failed to persist final messages: {}", e))?;
|
|
}
|
|
Err(_) => {
|
|
let fallback_metadata = goose::session::storage::SessionMetadata {
|
|
working_dir: current_dir,
|
|
description: format!("Scheduled job: {}", job_id),
|
|
schedule_id: Some(job_id.to_string()),
|
|
message_count: messages.len(),
|
|
..Default::default()
|
|
};
|
|
goose::session::storage::save_messages_with_metadata(
|
|
&session_file_path,
|
|
&fallback_metadata,
|
|
&messages,
|
|
)
|
|
.map_err(|e| anyhow!("Failed to persist messages with fallback metadata: {}", e))?;
|
|
}
|
|
}
|
|
|
|
info!(
|
|
"Finished executing job '{}', session: {}",
|
|
job_id, session_id
|
|
);
|
|
Ok(session_id)
|
|
}
|