1use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13use time::OffsetDateTime;
14
15pub type ForkProviderId = String;
16pub type ForkId = String;
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "camelCase")]
22pub struct ForkProviderDescriptor {
23 pub id: ForkProviderId,
24 pub display_name: String,
25 pub capabilities: ForkCapabilities,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(rename_all = "camelCase")]
30pub struct ForkCapabilities {
31 pub create: bool,
32 pub list: bool,
33 pub remove: bool,
34 pub resume: bool,
35 pub diff_summary: bool,
36 pub merge_back: bool,
37 pub copy_on_write: bool,
38 pub remote_compute: bool,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44pub enum ForkReason {
45 ConversationFork,
46 SubagentLane,
47 TaskLane,
48 Experiment,
49 Other,
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
54#[serde(rename_all = "camelCase")]
55pub struct ForkPolicy {
56 #[serde(default)]
61 pub allow_dirty_source: bool,
62}
63
64#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "snake_case")]
66pub enum ForkStatus {
67 Active,
68 Removed,
69 Missing,
71}
72
73#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub enum ForkCleanupPolicy {
76 #[default]
78 Explicit,
79 AutoOnTaskExit,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(rename_all = "camelCase")]
87pub struct ForkProvenance {
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub branch: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub source_branch: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub source_commit: Option<String>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub snapshot_id: Option<String>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub session_id: Option<String>,
98 #[serde(with = "time::serde::rfc3339")]
99 pub created_at: OffsetDateTime,
100}
101
102impl ForkProvenance {
103 pub fn at(created_at: OffsetDateTime) -> Self {
104 Self {
105 branch: None,
106 source_branch: None,
107 source_commit: None,
108 snapshot_id: None,
109 session_id: None,
110 created_at,
111 }
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[serde(rename_all = "camelCase")]
117pub struct ForkRequest {
118 pub source_workspace: PathBuf,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub name: Option<String>,
122 pub reason: ForkReason,
123 #[serde(default)]
124 pub policy: ForkPolicy,
125 #[serde(default)]
127 pub provider_config: serde_json::Value,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "camelCase")]
132pub struct WorkspaceFork {
133 pub id: ForkId,
134 pub provider_id: ForkProviderId,
135 pub source_workspace: PathBuf,
136 pub workspace: PathBuf,
138 pub status: ForkStatus,
139 pub provenance: ForkProvenance,
140 #[serde(default)]
141 pub cleanup: ForkCleanupPolicy,
142 #[serde(default)]
144 pub metadata: serde_json::Value,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150#[serde(rename_all = "camelCase")]
151pub struct RemoveForkPolicy {
152 pub confirm_workspace: PathBuf,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
156#[serde(rename_all = "camelCase")]
157pub struct RemoveForkResult {
158 pub id: ForkId,
159 pub removed: bool,
160 pub workspace: PathBuf,
161}
162
163#[async_trait::async_trait]
167pub trait ForkProvider: Send + Sync + 'static {
168 fn descriptor(&self) -> ForkProviderDescriptor;
169
170 async fn create_fork(&self, request: ForkRequest) -> anyhow::Result<WorkspaceFork>;
171
172 async fn list_forks(&self, source_workspace: &Path) -> anyhow::Result<Vec<WorkspaceFork>>;
174
175 async fn resume_fork(&self, id: &ForkId) -> anyhow::Result<WorkspaceFork>;
178
179 async fn remove_fork(
180 &self,
181 id: &ForkId,
182 policy: RemoveForkPolicy,
183 ) -> anyhow::Result<RemoveForkResult>;
184}