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:
parent
e983955036
commit
4e01728a67
15 changed files with 5220 additions and 3 deletions
128
src/core/state_machine.rs
Normal file
128
src/core/state_machine.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use chrono::Utc;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use super::event_store::EventStore;
|
||||
use super::models::*;
|
||||
|
||||
pub struct StateMachine {
|
||||
store: Arc<Mutex<EventStore>>,
|
||||
}
|
||||
|
||||
impl StateMachine {
|
||||
pub fn new(store: Arc<Mutex<EventStore>>) -> Self {
|
||||
Self { store }
|
||||
}
|
||||
|
||||
pub async fn transition(
|
||||
&self,
|
||||
task_id: &str,
|
||||
new_status: TaskStatus,
|
||||
agent_id: Option<&str>,
|
||||
reason: &str,
|
||||
) -> Result<Task, StateError> {
|
||||
let store = self.store.lock().await;
|
||||
|
||||
let task = store.read_task(task_id)?
|
||||
.ok_or(StateError::TaskNotFound(task_id.to_string()))?;
|
||||
|
||||
Self::validate_transition(&task.status, &new_status)?;
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
store.update_task_status(
|
||||
task_id,
|
||||
new_status.as_str(),
|
||||
agent_id,
|
||||
if new_status == TaskStatus::Assigned { Some(now.to_rfc3339()) } else { None },
|
||||
if new_status == TaskStatus::Running { Some(now.to_rfc3339()) } else { None },
|
||||
if matches!(new_status, TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled) { Some(now.to_rfc3339()) } else { None },
|
||||
task.retry_count,
|
||||
)?;
|
||||
|
||||
let event = TaskEvent {
|
||||
event_id: uuid::Uuid::new_v4().to_string(),
|
||||
task_id: task_id.to_string(),
|
||||
event_type: format!("task.{}", new_status.as_str()),
|
||||
agent_id: agent_id.map(String::from),
|
||||
timestamp: now,
|
||||
payload: serde_json::json!({
|
||||
"from_status": task.status.as_str(),
|
||||
"to_status": new_status.as_str(),
|
||||
"reason": reason,
|
||||
}),
|
||||
};
|
||||
store.append_event(&event)?;
|
||||
|
||||
drop(store);
|
||||
|
||||
// Re-read to return updated task
|
||||
let store = self.store.lock().await;
|
||||
let updated = store.read_task(task_id)?.unwrap();
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub async fn create_task(&self, task: &Task) -> Result<Task, StateError> {
|
||||
let store = self.store.lock().await;
|
||||
|
||||
store.insert_task(task)?;
|
||||
|
||||
let event = TaskEvent {
|
||||
event_id: uuid::Uuid::new_v4().to_string(),
|
||||
task_id: task.task_id.clone(),
|
||||
event_type: "task.created".into(),
|
||||
agent_id: None,
|
||||
timestamp: Utc::now(),
|
||||
payload: serde_json::json!({ "source": task.source }),
|
||||
};
|
||||
store.append_event(&event)?;
|
||||
|
||||
Ok(task.clone())
|
||||
}
|
||||
|
||||
fn validate_transition(from: &TaskStatus, to: &TaskStatus) -> Result<(), StateError> {
|
||||
let valid = match from {
|
||||
TaskStatus::Created => matches!(to, TaskStatus::Assigned | TaskStatus::Cancelled),
|
||||
TaskStatus::Assigned => matches!(to, TaskStatus::Running | TaskStatus::Cancelled),
|
||||
TaskStatus::Running => matches!(
|
||||
to,
|
||||
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::AgentLost | TaskStatus::Cancelled
|
||||
),
|
||||
TaskStatus::Failed | TaskStatus::AgentLost => {
|
||||
matches!(to, TaskStatus::Assigned | TaskStatus::Cancelled)
|
||||
}
|
||||
TaskStatus::Completed | TaskStatus::Cancelled => false,
|
||||
};
|
||||
if !valid {
|
||||
return Err(StateError::InvalidTransition(
|
||||
from.as_str().to_string(),
|
||||
to.as_str().to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_status(s: &str) -> TaskStatus {
|
||||
match s {
|
||||
"created" => TaskStatus::Created,
|
||||
"assigned" => TaskStatus::Assigned,
|
||||
"running" => TaskStatus::Running,
|
||||
"completed" => TaskStatus::Completed,
|
||||
"failed" => TaskStatus::Failed,
|
||||
"agent_lost" => TaskStatus::AgentLost,
|
||||
"cancelled" => TaskStatus::Cancelled,
|
||||
_ => TaskStatus::Created,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StateError {
|
||||
#[error("task not found: {0}")]
|
||||
TaskNotFound(String),
|
||||
#[error("invalid transition: {0} -> {1}")]
|
||||
InvalidTransition(String, String),
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue