feat: agent registry API + heartbeat checker + core unit tests

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).
This commit is contained in:
Zer4tul 2026-05-11 19:29:16 +08:00
parent 2658a74730
commit b75546bda6
9 changed files with 1023 additions and 115 deletions

View file

@ -1,5 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// ─── Agent ───────────────────────────────────────────────────────
@ -15,6 +16,32 @@ pub enum AgentType {
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 {
@ -23,7 +50,26 @@ pub enum AgentStatus {
Draining,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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,
@ -34,7 +80,7 @@ pub struct Agent {
pub status: AgentStatus,
pub last_heartbeat_at: DateTime<Utc>,
pub registered_at: DateTime<Utc>,
pub metadata: std::collections::HashMap<String, String>,
pub metadata: HashMap<String, String>,
}
// ─── Task ────────────────────────────────────────────────────────
@ -75,8 +121,6 @@ pub enum Priority {
}
impl Priority {
/// Explicit priority ordering (lower = higher priority).
/// Not reliant on variant declaration order.
pub fn order(&self) -> u8 {
match self {
Self::Urgent => 0,
@ -86,7 +130,6 @@ impl Priority {
}
}
/// Serialize to the string stored in the DB.
pub fn as_str(&self) -> &'static str {
match self {
Self::Low => "low",
@ -97,15 +140,15 @@ impl Priority {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Task {
pub task_id: String,
pub source: String, // "forgejo:<repo>#<issue>"
pub task_type: String, // "code", "review", "test", "deploy", "research"
pub source: String,
pub task_type: String,
pub priority: Priority,
pub status: TaskStatus,
pub assigned_agent_id: Option<String>,
pub requirements: String, // Issue body
pub requirements: String,
pub labels: Vec<String>,
pub created_at: DateTime<Utc>,
pub assigned_at: Option<DateTime<Utc>>,
@ -136,7 +179,7 @@ pub enum ArtifactType {
Url,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Artifact {
pub artifact_type: ArtifactType,
pub url: Option<String>,
@ -144,7 +187,7 @@ pub struct Artifact {
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Receipt {
pub task_id: String,
pub agent_id: String,
@ -157,7 +200,7 @@ pub struct Receipt {
// ─── TaskEvent (event sourcing) ──────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskEvent {
pub event_id: String,
pub task_id: String,