Tasks completed: - 2.7: Core unit tests (14 tests: state machine, event store, queue, timeout, retry) - 3.1: POST /api/v1/agents/register (upsert on duplicate) - 3.2: POST /api/v1/agents/heartbeat - 3.3: POST /api/v1/agents/deregister (offline + requeue running tasks) - 3.4: GET /api/v1/agents (filter by capability + status) - 3.5: Background heartbeat checker (marks offline, sets tasks agent_lost) - 3.6: API unit tests (register, duplicate, heartbeat, deregister, checker) All 14 tests pass. cargo check clean (warnings only).
211 lines
5.6 KiB
Rust
211 lines
5.6 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
// ─── Agent ───────────────────────────────────────────────────────
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum AgentType {
|
|
OpenClaw,
|
|
ClaudeCode,
|
|
CodexCli,
|
|
Hermes,
|
|
Acp,
|
|
Shell,
|
|
Other(String),
|
|
}
|
|
|
|
impl AgentType {
|
|
pub fn as_str(&self) -> &str {
|
|
match self {
|
|
Self::OpenClaw => "openclaw",
|
|
Self::ClaudeCode => "claude-code",
|
|
Self::CodexCli => "codex-cli",
|
|
Self::Hermes => "hermes",
|
|
Self::Acp => "acp",
|
|
Self::Shell => "shell",
|
|
Self::Other(value) => value.as_str(),
|
|
}
|
|
}
|
|
|
|
pub fn from_str(value: &str) -> Self {
|
|
match value {
|
|
"openclaw" => Self::OpenClaw,
|
|
"claude-code" => Self::ClaudeCode,
|
|
"codex-cli" => Self::CodexCli,
|
|
"hermes" => Self::Hermes,
|
|
"acp" => Self::Acp,
|
|
"shell" => Self::Shell,
|
|
other => Self::Other(other.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum AgentStatus {
|
|
Online,
|
|
Offline,
|
|
Draining,
|
|
}
|
|
|
|
impl AgentStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Online => "online",
|
|
Self::Offline => "offline",
|
|
Self::Draining => "draining",
|
|
}
|
|
}
|
|
|
|
pub fn from_str(value: &str) -> Self {
|
|
match value {
|
|
"online" => Self::Online,
|
|
"offline" => Self::Offline,
|
|
"draining" => Self::Draining,
|
|
_ => Self::Offline,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct Agent {
|
|
pub agent_id: String,
|
|
pub agent_type: AgentType,
|
|
pub hostname: String,
|
|
pub capabilities: Vec<String>,
|
|
pub max_concurrency: u32,
|
|
pub current_tasks: u32,
|
|
pub status: AgentStatus,
|
|
pub last_heartbeat_at: DateTime<Utc>,
|
|
pub registered_at: DateTime<Utc>,
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
// ─── Task ────────────────────────────────────────────────────────
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum TaskStatus {
|
|
Created,
|
|
Assigned,
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
AgentLost,
|
|
Cancelled,
|
|
}
|
|
|
|
impl TaskStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Created => "created",
|
|
Self::Assigned => "assigned",
|
|
Self::Running => "running",
|
|
Self::Completed => "completed",
|
|
Self::Failed => "failed",
|
|
Self::AgentLost => "agent_lost",
|
|
Self::Cancelled => "cancelled",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum Priority {
|
|
Low,
|
|
Normal,
|
|
High,
|
|
Urgent,
|
|
}
|
|
|
|
impl Priority {
|
|
pub fn order(&self) -> u8 {
|
|
match self {
|
|
Self::Urgent => 0,
|
|
Self::High => 1,
|
|
Self::Normal => 2,
|
|
Self::Low => 3,
|
|
}
|
|
}
|
|
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Low => "low",
|
|
Self::Normal => "normal",
|
|
Self::High => "high",
|
|
Self::Urgent => "urgent",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct Task {
|
|
pub task_id: String,
|
|
pub source: String,
|
|
pub task_type: String,
|
|
pub priority: Priority,
|
|
pub status: TaskStatus,
|
|
pub assigned_agent_id: Option<String>,
|
|
pub requirements: String,
|
|
pub labels: Vec<String>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub assigned_at: Option<DateTime<Utc>>,
|
|
pub started_at: Option<DateTime<Utc>>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
pub retry_count: u32,
|
|
pub max_retries: u32,
|
|
pub timeout_seconds: u64,
|
|
}
|
|
|
|
// ─── Receipt ─────────────────────────────────────────────────────
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ReceiptStatus {
|
|
Completed,
|
|
Failed,
|
|
Partial,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ArtifactType {
|
|
Pr,
|
|
Commit,
|
|
File,
|
|
Comment,
|
|
Url,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct Artifact {
|
|
pub artifact_type: ArtifactType,
|
|
pub url: Option<String>,
|
|
pub path: Option<String>,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct Receipt {
|
|
pub task_id: String,
|
|
pub agent_id: String,
|
|
pub status: ReceiptStatus,
|
|
pub duration_seconds: u64,
|
|
pub summary: String,
|
|
pub artifacts: Vec<Artifact>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
// ─── TaskEvent (event sourcing) ──────────────────────────────────
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct TaskEvent {
|
|
pub event_id: String,
|
|
pub task_id: String,
|
|
pub event_type: String,
|
|
pub agent_id: Option<String>,
|
|
pub timestamp: DateTime<Utc>,
|
|
pub payload: serde_json::Value,
|
|
}
|