feat: implement orchestrator core (Rust)

Task 1.1:  Cargo.toml with axum, rusqlite, matrix-sdk, serde, etc.
Task 1.2:  Directory structure: src/core, src/adapters, src/integrations, src/api
Task 1.5:  config.example.toml with full schema
Task 2.1:  Data models: Agent, Task, Receipt, Artifact, TaskEvent
Task 2.2:  Event Store: SQLite append-only with task/agent tables
Task 2.3:  Task state machine: created→assigned→running→completed/failed
Task 2.4:  Global task queue with priority ordering
Task 2.5:  Background timeout checker
Task 2.6:  Retry policy with configurable max_retries

Compiles clean (warnings only, no errors).
API handler stubs in place for Phase 2.
This commit is contained in:
Zer4tul 2026-05-11 14:57:23 +08:00
parent e983955036
commit 4e01728a67
15 changed files with 5220 additions and 3 deletions

131
src/main.rs Normal file
View file

@ -0,0 +1,131 @@
mod config;
mod core;
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>,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "agent_fleet=info,tower_http=info".into()),
)
.init();
let cli = Cli::parse();
let mut config = match config::Config::load(&cli.config) {
Ok(c) => c,
Err(e) => {
tracing::warn!("could not load config from {}: {e}, using defaults", cli.config);
config::Config::default()
}
};
if let Some(bind) = cli.bind {
config.server.bind = bind;
}
if let Some(port) = cli.port {
config.server.port = port;
}
tracing::info!(
"agent-fleet orchestrator starting on {}:{}",
config.server.bind,
config.server.port
);
// Initialize 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(tokio::sync::Mutex::new(event_store));
// Initialize core components
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()));
// Start timeout checker
let timeout_checker = std::sync::Arc::new(core::timeout::TimeoutChecker::new(
state_machine.clone(),
store.clone(),
std::time::Duration::from_secs(30),
std::time::Duration::from_secs(config.orchestrator.task_timeout_secs),
));
tokio::spawn(async move { timeout_checker.run().await });
// Build axum router (API stubs for now)
let app = axum::Router::new()
.route("/healthz", axum::routing::get(|| async { "ok" }))
.route(
"/api/v1/agents/register",
axum::routing::post(handlers::register_agent),
)
.route(
"/api/v1/agents/heartbeat",
axum::routing::post(handlers::heartbeat),
)
.route(
"/api/v1/agents/deregister",
axum::routing::post(handlers::deregister),
)
.route(
"/api/v1/agents",
axum::routing::get(handlers::list_agents),
)
.route(
"/api/v1/receipts",
axum::routing::post(handlers::submit_receipt),
)
.route(
"/api/v1/webhooks/forgejo",
axum::routing::post(handlers::forgejo_webhook),
)
.with_state(store.clone());
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");
}
mod handlers {
pub async fn register_agent() -> &'static str {
"TODO"
}
pub async fn heartbeat() -> &'static str {
"TODO"
}
pub async fn deregister() -> &'static str {
"TODO"
}
pub async fn list_agents() -> &'static str {
"TODO"
}
pub async fn submit_receipt() -> &'static str {
"TODO"
}
pub async fn forgejo_webhook() -> &'static str {
"TODO"
}
}