refactor: remove Matrix bot, make agent-fleet platform-agnostic API service
- Remove src/integrations/matrix/ (bot connection, command parsing, notification formatting)
- Remove matrix-sdk dependency from Cargo.toml
- Remove MatrixConfig from config.rs and [matrix] from config.example.toml
- Add GET /api/v1/tasks (list with status/agent_id filter)
- Add POST /api/v1/tasks/{task_id}/retry (Failed/AgentLost → Assigned)
- Add EventStore::list_tasks() with parameterized query
- 29/29 tests pass
Platform integration (Telegram, Matrix, Feishu) is Agent-side responsibility.
agent-fleet is now a pure HTTP API orchestration engine.
This commit is contained in:
parent
6efca09018
commit
1bc7580ecc
15 changed files with 435 additions and 2367 deletions
197
src/api.rs
197
src/api.rs
|
|
@ -379,6 +379,58 @@ fn parse_task_source(source: &str) -> Option<(String, u64)> {
|
|||
Some((repo.to_string(), issue_number))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListTasksQuery {
|
||||
pub status: Option<String>,
|
||||
pub agent_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list_tasks(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<ListTasksQuery>,
|
||||
) -> Result<Json<Vec<Task>>, ApiError> {
|
||||
let store = state.store.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<Json<Vec<Task>>, ApiError> {
|
||||
let store = store.lock().map_err(|e| ApiError::Poisoned(e.to_string()))?;
|
||||
let tasks = store.list_tasks(query.status.as_deref(), query.agent_id.as_deref())?;
|
||||
Ok(Json(tasks))
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
pub async fn retry_task(
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Path(task_id): axum::extract::Path<String>,
|
||||
) -> Result<Json<Task>, ApiError> {
|
||||
let store = state.store.clone();
|
||||
let sm = StateMachine::new(store.clone());
|
||||
|
||||
let task_id_for_check = task_id.clone();
|
||||
let current = tokio::task::spawn_blocking(move || -> Result<Option<Task>, ApiError> {
|
||||
let store = store.lock().map_err(|e| ApiError::Poisoned(e.to_string()))?;
|
||||
Ok(store.read_task(&task_id_for_check)?)
|
||||
})
|
||||
.await??;
|
||||
|
||||
let task = current.ok_or_else(|| ApiError::NotFound(format!("task {}", task_id)))?;
|
||||
|
||||
if !matches!(task.status, TaskStatus::Failed | TaskStatus::AgentLost) {
|
||||
return Err(ApiError::BadRequest(format!(
|
||||
"task {} is not retryable (current status: {})",
|
||||
task.task_id,
|
||||
task.status.as_str()
|
||||
)));
|
||||
}
|
||||
|
||||
let updated = sm
|
||||
.transition(&task_id, TaskStatus::Assigned, None, "retry")
|
||||
.await
|
||||
.map_err(|e| ApiError::BadRequest(e.to_string()))?;
|
||||
|
||||
Ok(Json(updated))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -734,4 +786,149 @@ mod tests {
|
|||
};
|
||||
assert_eq!(task.status, TaskStatus::Running);
|
||||
}
|
||||
|
||||
// ─── Task API tests ─────────────────────────────────────────
|
||||
|
||||
fn sample_task_variant(task_id: &str, status: TaskStatus, agent_id: Option<&str>) -> Task {
|
||||
Task {
|
||||
task_id: task_id.to_string(),
|
||||
source: format!("forgejo:org/repo#{task_id}"),
|
||||
task_type: "code".into(),
|
||||
priority: Priority::High,
|
||||
status,
|
||||
assigned_agent_id: agent_id.map(String::from),
|
||||
requirements: "do something".into(),
|
||||
labels: vec!["agent:code".into(), "priority:high".into()],
|
||||
created_at: Utc::now(),
|
||||
assigned_at: None,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
retry_count: 0,
|
||||
max_retries: 2,
|
||||
timeout_seconds: 1800,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_tasks_returns_all_tasks() {
|
||||
let (_dir, state) = test_store();
|
||||
{
|
||||
let store = state.store.lock().unwrap();
|
||||
store.insert_task(&sample_task_variant("task-1", TaskStatus::Created, None)).unwrap();
|
||||
store.insert_task(&sample_task_variant("task-2", TaskStatus::Running, Some("worker-01"))).unwrap();
|
||||
}
|
||||
|
||||
let tasks = list_tasks(
|
||||
State(state),
|
||||
Query(ListTasksQuery { status: None, agent_id: None }),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(tasks.0.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_tasks_filters_by_status() {
|
||||
let (_dir, state) = test_store();
|
||||
{
|
||||
let store = state.store.lock().unwrap();
|
||||
store.insert_task(&sample_task_variant("task-1", TaskStatus::Created, None)).unwrap();
|
||||
store.insert_task(&sample_task_variant("task-2", TaskStatus::Running, Some("worker-01"))).unwrap();
|
||||
}
|
||||
|
||||
let tasks = list_tasks(
|
||||
State(state),
|
||||
Query(ListTasksQuery { status: Some("running".into()), agent_id: None }),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(tasks.0.len(), 1);
|
||||
assert_eq!(tasks.0[0].task_id, "task-2");
|
||||
assert_eq!(tasks.0[0].status, TaskStatus::Running);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_tasks_filters_by_agent() {
|
||||
let (_dir, state) = test_store();
|
||||
{
|
||||
let store = state.store.lock().unwrap();
|
||||
store.insert_task(&sample_task_variant("task-1", TaskStatus::Running, Some("worker-01"))).unwrap();
|
||||
store.insert_task(&sample_task_variant("task-2", TaskStatus::Running, Some("worker-02"))).unwrap();
|
||||
}
|
||||
|
||||
let tasks = list_tasks(
|
||||
State(state),
|
||||
Query(ListTasksQuery { status: None, agent_id: Some("worker-01".into()) }),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(tasks.0.len(), 1);
|
||||
assert_eq!(tasks.0[0].task_id, "task-1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_task_succeeds_for_failed_task() {
|
||||
let (_dir, state) = test_store();
|
||||
{
|
||||
let store = state.store.lock().unwrap();
|
||||
store.insert_task(&sample_task_variant("task-failed", TaskStatus::Failed, Some("worker-01"))).unwrap();
|
||||
}
|
||||
|
||||
let updated = retry_task(State(state.clone()), axum::extract::Path("task-failed".to_string()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.0.status, TaskStatus::Assigned);
|
||||
|
||||
// Verify in DB
|
||||
let task = {
|
||||
let store = state.store.lock().unwrap();
|
||||
store.read_task("task-failed").unwrap().unwrap()
|
||||
};
|
||||
assert_eq!(task.status, TaskStatus::Assigned);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_task_succeeds_for_agent_lost_task() {
|
||||
let (_dir, state) = test_store();
|
||||
{
|
||||
let store = state.store.lock().unwrap();
|
||||
store.insert_task(&sample_task_variant("task-lost", TaskStatus::AgentLost, Some("worker-01"))).unwrap();
|
||||
}
|
||||
|
||||
let updated = retry_task(State(state.clone()), axum::extract::Path("task-lost".to_string()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.0.status, TaskStatus::Assigned);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_task_rejects_non_retryable_status() {
|
||||
let (_dir, state) = test_store();
|
||||
{
|
||||
let store = state.store.lock().unwrap();
|
||||
store.insert_task(&sample_task_variant("task-running", TaskStatus::Running, Some("worker-01"))).unwrap();
|
||||
}
|
||||
|
||||
let err = retry_task(State(state.clone()), axum::extract::Path("task-running".to_string()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, ApiError::BadRequest(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retry_task_returns_not_found_for_missing_task() {
|
||||
let (_dir, state) = test_store();
|
||||
|
||||
let err = retry_task(State(state), axum::extract::Path("nonexistent".to_string()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, ApiError::NotFound(_)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue