use std::path::PathBuf;
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct RunId(pub String);
impl RunId {
pub fn new() -> Self {
RunId(uuid::Uuid::new_v4().to_string())
}
pub fn from_string(s: String) -> Self {
RunId(s)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for RunId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct StepId(pub String);
impl StepId {
pub fn new() -> Self {
StepId(uuid::Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for StepId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct SessionId(pub String);
impl SessionId {
pub fn new() -> Self {
SessionId(uuid::Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ArtifactId(pub String);
impl ArtifactId {
pub fn new() -> Self {
ArtifactId(uuid::Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for ArtifactId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum RunStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
Timeout,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum RunTarget {
Agent {
spec_path: PathBuf,
},
Swarm {
swarmfile_path: PathBuf,
},
A2AAgent {
url: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum RuntimeKind {
Local,
Docker,
Http,
Cloud,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, serde::Serialize, serde::Deserialize)]
pub enum RunError {
#[error("Startup failed: {message}")]
StartupFailed {
message: String,
},
#[error("Execution failed: {message}")]
ExecutionFailed {
message: String,
},
#[error("Cancelled: {reason}")]
Cancelled {
reason: String,
},
#[error("Timeout after {after:?}")]
Timeout {
after: Duration,
},
#[error("Worker '{worker}' timed out after {after:?}")]
WorkerTimeout {
worker: String,
after: Duration,
},
#[error("Invalid config: {message}")]
InvalidConfig {
message: String,
},
#[error("Runtime error: {message}")]
RuntimeError {
message: String,
},
#[error("Artifact error: {message}")]
ArtifactError {
message: String,
},
#[error("Not found: {resource_type} '{id}'")]
NotFound {
resource_type: String,
id: String,
},
#[error("Pattern '{pattern}' error in step '{step}': {message}")]
PatternError {
pattern: String,
step: String,
message: String,
},
#[error("Agent spec validation failed: {message}")]
ValidationError {
message: String,
},
}
impl RunError {
pub fn startup_failed(message: impl Into<String>) -> Self {
RunError::StartupFailed {
message: message.into(),
}
}
pub fn execution_failed(message: impl Into<String>) -> Self {
RunError::ExecutionFailed {
message: message.into(),
}
}
pub fn cancelled(reason: impl Into<String>) -> Self {
RunError::Cancelled {
reason: reason.into(),
}
}
pub fn timeout(after: Duration) -> Self {
RunError::Timeout { after }
}
pub fn worker_timeout(worker: impl Into<String>, after: Duration) -> Self {
RunError::WorkerTimeout {
worker: worker.into(),
after,
}
}
pub fn invalid_config(message: impl Into<String>) -> Self {
RunError::InvalidConfig {
message: message.into(),
}
}
pub fn runtime_error(message: impl Into<String>) -> Self {
RunError::RuntimeError {
message: message.into(),
}
}
pub fn not_found(resource_type: impl Into<String>, id: impl Into<String>) -> Self {
RunError::NotFound {
resource_type: resource_type.into(),
id: id.into(),
}
}
pub fn pattern_error(
pattern: impl Into<String>,
step: impl Into<String>,
message: impl Into<String>,
) -> Self {
RunError::PatternError {
pattern: pattern.into(),
step: step.into(),
message: message.into(),
}
}
pub fn validation_error(message: impl Into<String>) -> Self {
RunError::ValidationError {
message: message.into(),
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
RunError::Timeout { .. }
| RunError::WorkerTimeout { .. }
| RunError::RuntimeError { .. }
| RunError::PatternError { .. }
)
}
pub fn is_cancellation(&self) -> bool {
matches!(self, RunError::Cancelled { .. })
}
pub fn is_config_error(&self) -> bool {
matches!(
self,
RunError::InvalidConfig { .. } | RunError::ValidationError { .. }
)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Run {
pub id: RunId,
pub status: RunStatus,
pub target: RunTarget,
pub runtime_kind: RuntimeKind,
pub session_id: Option<SessionId>,
pub created_at: SystemTime,
pub started_at: Option<SystemTime>,
pub finished_at: Option<SystemTime>,
pub timeout: Option<Duration>,
pub error: Option<RunError>,
pub steps: Vec<Step>,
pub artifacts: Vec<ArtifactId>,
#[serde(default)]
pub input: Option<serde_json::Value>,
}
impl Run {
pub fn new(target: RunTarget, runtime_kind: RuntimeKind) -> Self {
Run {
id: RunId::new(),
status: RunStatus::Pending,
target,
runtime_kind,
session_id: None,
created_at: SystemTime::now(),
started_at: None,
finished_at: None,
timeout: None,
error: None,
steps: Vec::new(),
artifacts: Vec::new(),
input: None,
}
}
pub fn with_input(mut self, input: serde_json::Value) -> Self {
self.input = Some(input);
self
}
pub fn with_session(mut self, session_id: SessionId) -> Self {
self.session_id = Some(session_id);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn start(&mut self) {
self.status = RunStatus::Running;
self.started_at = Some(SystemTime::now());
}
pub fn complete(&mut self) {
self.status = RunStatus::Completed;
self.finished_at = Some(SystemTime::now());
}
pub fn fail(&mut self, error: RunError) {
self.status = RunStatus::Failed;
self.error = Some(error);
self.finished_at = Some(SystemTime::now());
}
pub fn cancel(&mut self, reason: String) {
self.status = RunStatus::Cancelled;
self.error = Some(RunError::Cancelled { reason });
self.finished_at = Some(SystemTime::now());
}
pub fn timeout(&mut self) {
self.status = RunStatus::Timeout;
if let Some(timeout) = self.timeout {
self.error = Some(RunError::Timeout { after: timeout });
}
self.finished_at = Some(SystemTime::now());
}
pub fn duration(&self) -> Option<Duration> {
match (self.started_at, self.finished_at) {
(Some(start), Some(end)) => end.duration_since(start).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum StepStatus {
Pending,
Running,
Completed,
Failed,
Skipped,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, serde::Serialize, serde::Deserialize)]
pub enum StepError {
#[error("Agent failed: {message}")]
AgentFailed {
message: String,
},
#[error("Invalid input: {message}")]
InvalidInput {
message: String,
},
#[error("Output failed: {message}")]
OutputFailed {
message: String,
},
#[error("Timeout after {after:?}")]
Timeout {
after: Duration,
},
#[error("Cancelled: {reason}")]
Cancelled {
reason: String,
},
#[error("Pattern '{pattern}' error: {message}")]
PatternError {
pattern: String,
message: String,
},
}
impl StepError {
pub fn agent_failed(message: impl Into<String>) -> Self {
StepError::AgentFailed {
message: message.into(),
}
}
pub fn invalid_input(message: impl Into<String>) -> Self {
StepError::InvalidInput {
message: message.into(),
}
}
pub fn output_failed(message: impl Into<String>) -> Self {
StepError::OutputFailed {
message: message.into(),
}
}
pub fn timeout(after: Duration) -> Self {
StepError::Timeout { after }
}
pub fn cancelled(reason: impl Into<String>) -> Self {
StepError::Cancelled {
reason: reason.into(),
}
}
pub fn pattern_error(pattern: impl Into<String>, message: impl Into<String>) -> Self {
StepError::PatternError {
pattern: pattern.into(),
message: message.into(),
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
StepError::Timeout { .. } | StepError::PatternError { .. }
)
}
pub fn is_cancellation(&self) -> bool {
matches!(self, StepError::Cancelled { .. })
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Step {
pub id: StepId,
pub run_id: RunId,
pub status: StepStatus,
pub name: String,
pub agent_spec_path: PathBuf,
pub created_at: SystemTime,
pub started_at: Option<SystemTime>,
pub finished_at: Option<SystemTime>,
pub error: Option<StepError>,
pub output_artifacts: Vec<ArtifactId>,
}
impl Step {
pub fn new(run_id: RunId, name: String, agent_spec_path: PathBuf) -> Self {
Step {
id: StepId::new(),
run_id,
status: StepStatus::Pending,
name,
agent_spec_path,
created_at: SystemTime::now(),
started_at: None,
finished_at: None,
error: None,
output_artifacts: Vec::new(),
}
}
pub fn start(&mut self) {
self.status = StepStatus::Running;
self.started_at = Some(SystemTime::now());
}
pub fn complete(&mut self, artifacts: Vec<ArtifactId>) {
self.status = StepStatus::Completed;
self.output_artifacts = artifacts;
self.finished_at = Some(SystemTime::now());
}
pub fn fail(&mut self, error: StepError) {
self.status = StepStatus::Failed;
self.error = Some(error);
self.finished_at = Some(SystemTime::now());
}
pub fn skip(&mut self) {
self.status = StepStatus::Skipped;
self.finished_at = Some(SystemTime::now());
}
pub fn cancel(&mut self, reason: String) {
self.status = StepStatus::Cancelled;
self.error = Some(StepError::Cancelled { reason });
self.finished_at = Some(SystemTime::now());
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SessionStatus {
Active,
Ended,
Cancelled,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Session {
pub id: SessionId,
pub status: SessionStatus,
pub name: String,
pub created_at: SystemTime,
pub ended_at: Option<SystemTime>,
pub run_ids: Vec<RunId>,
}
impl Session {
pub fn new(name: String) -> Self {
Session {
id: SessionId::new(),
status: SessionStatus::Active,
name,
created_at: SystemTime::now(),
ended_at: None,
run_ids: Vec::new(),
}
}
pub fn add_run(&mut self, run_id: RunId) {
self.run_ids.push(run_id);
}
pub fn end(&mut self) {
self.status = SessionStatus::Ended;
self.ended_at = Some(SystemTime::now());
}
pub fn cancel(&mut self) {
self.status = SessionStatus::Cancelled;
self.ended_at = Some(SystemTime::now());
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ArtifactLocation {
Local {
path: PathBuf,
},
Http {
url: String,
},
CloudStorage {
uri: String,
},
Inline {
content: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Checksum {
pub algorithm: String,
pub value: String,
}
impl Checksum {
pub fn new(algorithm: impl Into<String>, value: impl Into<String>) -> Self {
Checksum {
algorithm: algorithm.into(),
value: value.into(),
}
}
pub fn sha256(value: impl Into<String>) -> Self {
Checksum::new("sha256", value)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Artifact {
pub id: ArtifactId,
pub name: String,
pub content_type: String,
pub size: u64,
pub location: ArtifactLocation,
pub checksum: Option<Checksum>,
pub created_at: SystemTime,
pub source_run: Option<RunId>,
pub source_step: Option<StepId>,
}
impl Artifact {
pub fn new(name: String, content_type: String, location: ArtifactLocation) -> Self {
Artifact {
id: ArtifactId::new(),
name,
content_type,
size: 0,
location,
checksum: None,
created_at: SystemTime::now(),
source_run: None,
source_step: None,
}
}
pub fn with_size(mut self, size: u64) -> Self {
self.size = size;
self
}
pub fn with_checksum(mut self, checksum: Checksum) -> Self {
self.checksum = Some(checksum);
self
}
pub fn from_run(mut self, run_id: RunId) -> Self {
self.source_run = Some(run_id);
self
}
pub fn from_step(mut self, step_id: StepId) -> Self {
self.source_step = Some(step_id);
self
}
}
#[derive(Debug, Clone)]
pub struct ExecutionHandle {
pub run_id: RunId,
pub runtime_kind: RuntimeKind,
pub created_at: Instant,
pub runtime_handle: String,
}
impl ExecutionHandle {
pub fn new(run_id: RunId, runtime_kind: RuntimeKind, runtime_handle: String) -> Self {
ExecutionHandle {
run_id,
runtime_kind,
created_at: Instant::now(),
runtime_handle,
}
}
pub fn elapsed(&self) -> Duration {
self.created_at.elapsed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_input_default_none() {
let run = Run::new(
RunTarget::Agent {
spec_path: PathBuf::from("agent.yaml"),
},
RuntimeKind::Local,
);
assert!(run.input.is_none());
}
#[test]
fn test_run_with_input() {
use serde_json::json;
let value = json!({"key": "value"});
let run = Run::new(
RunTarget::Agent {
spec_path: PathBuf::from("agent.yaml"),
},
RuntimeKind::Local,
)
.with_input(value.clone());
assert_eq!(run.input, Some(value));
}
#[test]
fn test_run_id_generation() {
let id1 = RunId::new();
let id2 = RunId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_run_lifecycle() {
let mut run = Run::new(
RunTarget::Agent {
spec_path: PathBuf::from("agent.yaml"),
},
RuntimeKind::Local,
);
assert_eq!(run.status, RunStatus::Pending);
assert!(run.started_at.is_none());
run.start();
assert_eq!(run.status, RunStatus::Running);
run.complete();
assert_eq!(run.status, RunStatus::Completed);
assert!(run.duration().is_some());
}
#[test]
fn test_run_failure() {
let mut run = Run::new(
RunTarget::Agent {
spec_path: PathBuf::from("agent.yaml"),
},
RuntimeKind::Local,
);
run.start();
run.fail(RunError::ExecutionFailed {
message: "test".into(),
});
assert_eq!(run.status, RunStatus::Failed);
}
#[test]
fn test_step_lifecycle() {
let run_id = RunId::new();
let mut step = Step::new(run_id, "test-step".into(), PathBuf::from("agent.yaml"));
assert_eq!(step.status, StepStatus::Pending);
step.start();
assert_eq!(step.status, StepStatus::Running);
step.complete(vec![ArtifactId::new()]);
assert_eq!(step.status, StepStatus::Completed);
assert!(!step.output_artifacts.is_empty());
}
#[test]
fn test_session() {
let mut session = Session::new("test".into());
session.add_run(RunId::new());
assert_eq!(session.run_ids.len(), 1);
session.end();
assert_eq!(session.status, SessionStatus::Ended);
}
#[test]
fn test_artifact() {
let artifact = Artifact::new(
"output.json".into(),
"application/json".into(),
ArtifactLocation::Local {
path: PathBuf::from("/tmp/out.json"),
},
)
.with_size(1024);
assert_eq!(artifact.name, "output.json");
assert_eq!(artifact.size, 1024);
}
}