Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
559 changes: 559 additions & 0 deletions rust/Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
resolver = "2"
members = [
"crates/ccb-runtime-accelerator",
"crates/ccb-project",
"crates/ccb-storage",
"crates/ccb-storage-classification",
"crates/ccb-jobs",
]

[workspace.package]
Expand Down
16 changes: 16 additions & 0 deletions rust/crates/ccb-jobs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "ccb-jobs"
version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
chrono = { version = "0.4", features = ["serde"] }
camino = { version = "1", features = ["serde1"] }
ccb-storage = { path = "../ccb-storage" }

[dev-dependencies]
tempfile = "3"
31 changes: 31 additions & 0 deletions rust/crates/ccb-jobs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! CCB job store abstraction.
//!
//! Canonical home for job records, submission records, job events, and the
//! persistent JSONL stores that back them. Mirrors `lib/jobs/` from Python
//! v7.5.2.

pub mod models;
pub mod store;

use thiserror::Error;

#[derive(Error, Debug)]
pub enum JobsError {
#[error("storage error: {0}")]
Storage(#[from] ccb_storage::StorageError),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("not found: {0}")]
NotFound(String),
}

pub type Result<T> = std::result::Result<T, JobsError>;

// Re-export the most commonly used types at crate root for ergonomics.
pub use models::{
DeliveryScope, JobEvent, JobRecord, JobStatus, MessageEnvelope, ProjectViewJobSummary,
SubmissionRecord, TargetKind,
};
pub use store::{JobEventStore, JobStore, SubmissionStore};
214 changes: 214 additions & 0 deletions rust/crates/ccb-jobs/src/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Delivery scope for a message envelope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum DeliveryScope {
#[default]
Agent,
Group,
Broadcast,
}

/// A message envelope submitted to the daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageEnvelope {
pub project_id: String,
pub to_agent: String,
pub from_actor: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reply_to: Option<String>,
pub message_type: String,
pub delivery_scope: DeliveryScope,
#[serde(default)]
pub silence_on_success: bool,
#[serde(default)]
pub route_options: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body_artifact: Option<Value>,
}

/// Job status enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum JobStatus {
#[default]
Accepted,
Running,
Completed,
Failed,
Incomplete,
Cancelled,
}

/// Target kind for job routing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum TargetKind {
#[default]
Agent,
Group,
}

/// A job record persisted per target.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobRecord {
pub job_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub submission_id: Option<String>,
#[serde(default)]
pub agent_name: String,
pub provider: String,
pub request: MessageEnvelope,
pub status: JobStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub terminal_decision: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancel_requested_at: Option<String>,
pub created_at: String,
pub updated_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_path: Option<String>,
#[serde(default)]
pub target_kind: TargetKind,
#[serde(default)]
pub target_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_instance: Option<String>,
#[serde(default)]
pub provider_options: Value,
}

/// A job event persisted per target.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobEvent {
pub event_id: String,
pub job_id: String,
#[serde(default)]
pub agent_name: String,
#[serde(default)]
pub target_kind: TargetKind,
#[serde(default)]
pub target_name: String,
#[serde(rename = "type")]
pub event_type: String,
#[serde(default)]
pub payload: Value,
pub timestamp: String,
}

/// A submission record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmissionRecord {
pub submission_id: String,
pub project_id: String,
pub from_actor: String,
pub target_scope: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default)]
pub job_ids: Vec<String>,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub updated_at: String,
}

/// Statuses that `list_project_view_recent_jobs` considers by default.
pub const PROJECT_VIEW_RECENT_JOB_STATUSES: &[JobStatus] = &[
JobStatus::Completed,
JobStatus::Cancelled,
JobStatus::Failed,
JobStatus::Incomplete,
];

/// Summary projection of a message envelope used by project view.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectViewMessageSummary {
pub project_id: String,
pub to_agent: String,
pub from_actor: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reply_to: Option<String>,
pub message_type: String,
pub delivery_scope: String,
#[serde(default)]
pub silence_on_success: bool,
#[serde(default)]
pub route_options: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body_artifact: Option<Value>,
}

impl From<&MessageEnvelope> for ProjectViewMessageSummary {
fn from(envelope: &MessageEnvelope) -> Self {
Self {
project_id: envelope.project_id.clone(),
to_agent: envelope.to_agent.clone(),
from_actor: envelope.from_actor.clone(),
body: envelope.body.clone(),
task_id: envelope.task_id.clone(),
reply_to: envelope.reply_to.clone(),
message_type: envelope.message_type.clone(),
delivery_scope: serde_json::to_value(envelope.delivery_scope)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| "agent".to_string()),
silence_on_success: envelope.silence_on_success,
route_options: envelope.route_options.clone(),
body_artifact: envelope.body_artifact.clone(),
}
}
}

/// Summary projection of a job record used by project view.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectViewJobSummary {
pub job_id: String,
pub agent_name: String,
pub provider: String,
pub request: ProjectViewMessageSummary,
pub status: JobStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub terminal_decision: Option<Value>,
pub created_at: String,
pub updated_at: String,
#[serde(default)]
pub target_kind: TargetKind,
#[serde(default)]
pub target_name: String,
#[serde(default)]
pub provider_options: Value,
}

impl From<&JobRecord> for ProjectViewJobSummary {
fn from(job: &JobRecord) -> Self {
Self {
job_id: job.job_id.clone(),
agent_name: job.agent_name.clone(),
provider: job.provider.clone(),
request: ProjectViewMessageSummary::from(&job.request),
status: job.status,
terminal_decision: job.terminal_decision.clone(),
created_at: job.created_at.clone(),
updated_at: job.updated_at.clone(),
target_kind: job.target_kind,
target_name: if job.target_name.is_empty() {
job.agent_name.clone()
} else {
job.target_name.clone()
},
provider_options: job.provider_options.clone(),
}
}
}
Loading
Loading