use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
pub type ForkProviderId = String;
pub type ForkId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ForkProviderDescriptor {
pub id: ForkProviderId,
pub display_name: String,
pub capabilities: ForkCapabilities,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ForkCapabilities {
pub create: bool,
pub list: bool,
pub remove: bool,
pub resume: bool,
pub diff_summary: bool,
pub merge_back: bool,
pub copy_on_write: bool,
pub remote_compute: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ForkReason {
ConversationFork,
SubagentLane,
TaskLane,
Experiment,
Other,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ForkPolicy {
#[serde(default)]
pub allow_dirty_source: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ForkStatus {
Active,
Removed,
Missing,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ForkCleanupPolicy {
#[default]
Explicit,
AutoOnTaskExit,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ForkProvenance {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_branch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
}
impl ForkProvenance {
pub fn at(created_at: OffsetDateTime) -> Self {
Self {
branch: None,
source_branch: None,
source_commit: None,
snapshot_id: None,
session_id: None,
created_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ForkRequest {
pub source_workspace: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub reason: ForkReason,
#[serde(default)]
pub policy: ForkPolicy,
#[serde(default)]
pub provider_config: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFork {
pub id: ForkId,
pub provider_id: ForkProviderId,
pub source_workspace: PathBuf,
pub workspace: PathBuf,
pub status: ForkStatus,
pub provenance: ForkProvenance,
#[serde(default)]
pub cleanup: ForkCleanupPolicy,
#[serde(default)]
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RemoveForkPolicy {
pub confirm_workspace: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RemoveForkResult {
pub id: ForkId,
pub removed: bool,
pub workspace: PathBuf,
}
#[async_trait::async_trait]
pub trait ForkProvider: Send + Sync + 'static {
fn descriptor(&self) -> ForkProviderDescriptor;
async fn create_fork(&self, request: ForkRequest) -> anyhow::Result<WorkspaceFork>;
async fn list_forks(&self, source_workspace: &Path) -> anyhow::Result<Vec<WorkspaceFork>>;
async fn resume_fork(&self, id: &ForkId) -> anyhow::Result<WorkspaceFork>;
async fn remove_fork(
&self,
id: &ForkId,
policy: RemoveForkPolicy,
) -> anyhow::Result<RemoveForkResult>;
}