feat: dual execution model (SSH CLI + HTTP pull)
- ExecutionMode enum: SshCli (orchestrator dispatches) | HttpPull (agent pulls) - SSH CLI executor: spawn remote agents via ssh + CLI template - Local subprocess as SSH special case (localhost) - HostConfig with capability matching and load-based selection - Dispatch loop: scan created tasks → select host → execute → update - CliAdapterConfig: CLI templates for Codex and Claude Code - Structured prompt construction (Issue → goal/constraints/validation) - Output parsers: Codex JSON, Claude Code JSON, raw fallback - TaskStatus::ReviewPending + review_count loop limit - Forgejo webhook: pull_request (opened→review_pending, merged→completed) - Forgejo webhook: push events (task/* branch → last_activity_at) - HTTP API: dequeue only returns http_pull tasks - HTTP API: status update only for http_pull mode - Token auth config for http_pull agents - Adapter module rewritten: AgentAdapter trait removed → config-driven CLI templates - New fields: execution_mode, assigned_host, branch_name, pr_title, last_activity_at, review_count - 30/30 tests pass
This commit is contained in:
parent
1bc7580ecc
commit
e39a16498c
34 changed files with 2541 additions and 1555 deletions
52
src/main.rs
52
src/main.rs
|
|
@ -2,6 +2,8 @@ mod adapters;
|
|||
mod api;
|
||||
mod config;
|
||||
mod core;
|
||||
mod dispatch;
|
||||
mod execution;
|
||||
mod integrations;
|
||||
|
||||
use clap::Parser;
|
||||
|
|
@ -9,15 +11,10 @@ use clap::Parser;
|
|||
#[derive(Parser)]
|
||||
#[command(name = "agent-fleet", about = "Agent Fleet Orchestrator")]
|
||||
struct Cli {
|
||||
/// Path to config file
|
||||
#[arg(short, long, default_value = "config.toml")]
|
||||
config: String,
|
||||
|
||||
/// Bind address
|
||||
#[arg(long)]
|
||||
bind: Option<String>,
|
||||
|
||||
/// Port
|
||||
#[arg(short, long)]
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
|
@ -32,7 +29,6 @@ async fn main() {
|
|||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let mut config = match config::Config::load(&cli.config) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
|
|
@ -40,7 +36,6 @@ async fn main() {
|
|||
config::Config::default()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(bind) = cli.bind {
|
||||
config.server.bind = bind;
|
||||
}
|
||||
|
|
@ -48,23 +43,10 @@ async fn main() {
|
|||
config.server.port = port;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"agent-fleet orchestrator starting on {}:{}",
|
||||
config.server.bind,
|
||||
config.server.port
|
||||
);
|
||||
|
||||
let event_store = core::event_store::EventStore::open(std::path::Path::new(
|
||||
&config.orchestrator.db_path,
|
||||
))
|
||||
.expect("failed to open event store");
|
||||
let event_store = core::event_store::EventStore::open(std::path::Path::new(&config.orchestrator.db_path))
|
||||
.expect("failed to open event store");
|
||||
let store = std::sync::Arc::new(std::sync::Mutex::new(event_store));
|
||||
|
||||
let state_machine = std::sync::Arc::new(core::state_machine::StateMachine::new(store.clone()));
|
||||
let _task_queue = std::sync::Arc::new(core::task_queue::TaskQueue::new(
|
||||
state_machine.clone(),
|
||||
store.clone(),
|
||||
));
|
||||
|
||||
let timeout_checker = std::sync::Arc::new(core::timeout::TimeoutChecker::new(
|
||||
state_machine.clone(),
|
||||
|
|
@ -83,33 +65,29 @@ async fn main() {
|
|||
));
|
||||
tokio::spawn(async move { heartbeat_checker.run().await });
|
||||
|
||||
let app_state = api::AppState::new(config.clone(), store.clone());
|
||||
let dispatcher = dispatch::Dispatcher::new(config.clone(), store.clone(), state_machine.clone());
|
||||
tokio::spawn(async move { dispatcher.run().await });
|
||||
|
||||
let app_state = api::AppState::new(config.clone(), store.clone());
|
||||
let app = axum::Router::new()
|
||||
.route("/healthz", axum::routing::get(|| async { "ok" }))
|
||||
// Agent registry
|
||||
.route("/api/v1/agents/register", axum::routing::post(api::register_agent))
|
||||
.route("/api/v1/agents/heartbeat", axum::routing::post(api::heartbeat))
|
||||
.route("/api/v1/agents/deregister", axum::routing::post(api::deregister))
|
||||
.route("/api/v1/agents", axum::routing::get(api::list_agents))
|
||||
// Task management
|
||||
.route("/api/v1/tasks", axum::routing::get(api::list_tasks))
|
||||
.route("/api/v1/tasks/dequeue", axum::routing::post(api::dequeue_task))
|
||||
.route("/api/v1/tasks/{task_id}", axum::routing::get(api::get_task))
|
||||
.route("/api/v1/tasks/{task_id}/status", axum::routing::post(api::update_task_status))
|
||||
.route("/api/v1/tasks/{task_id}/complete", axum::routing::post(api::complete_task))
|
||||
.route("/api/v1/tasks/{task_id}/retry", axum::routing::post(api::retry_task))
|
||||
// Receipts & webhooks
|
||||
.route("/api/v1/receipts", axum::routing::post(api::submit_receipt))
|
||||
.route(
|
||||
"/api/v1/webhooks/forgejo",
|
||||
axum::routing::post(api::forgejo_webhook),
|
||||
)
|
||||
.route("/api/v1/webhooks/forgejo", axum::routing::post(api::forgejo_webhook))
|
||||
.with_state(app_state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!(
|
||||
"{}:{}",
|
||||
config.server.bind, config.server.port
|
||||
))
|
||||
.await
|
||||
.expect("failed to bind");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.server.bind, config.server.port))
|
||||
.await
|
||||
.expect("failed to bind");
|
||||
tracing::info!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, app).await.expect("server error");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue