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, Default, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "camelCase")]
25pub struct ForkOwnership {
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub creator_thread_id: Option<crate::events::ThreadId>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub owner_thread_id: Option<crate::events::ThreadId>,
30}
31
32#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
35#[serde(rename_all = "snake_case")]
36pub enum AgentWorkspaceIsolation {
37 #[default]
38 Shared,
39 Worktree,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "camelCase")]
44pub struct ForkProviderDescriptor {
45 pub id: ForkProviderId,
46 pub display_name: String,
47 pub capabilities: ForkCapabilities,
48}
49
50#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "camelCase")]
52pub struct ForkCapabilities {
53 pub create: bool,
54 pub list: bool,
55 pub remove: bool,
56 pub resume: bool,
57 pub diff_summary: bool,
58 pub merge_back: bool,
59 pub copy_on_write: bool,
60 pub remote_compute: bool,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "snake_case")]
66pub enum ForkReason {
67 ConversationFork,
68 SubagentLane,
69 TaskLane,
70 Experiment,
71 Other,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
76#[serde(rename_all = "camelCase")]
77pub struct ForkPolicy {
78 #[serde(default)]
83 pub allow_dirty_source: bool,
84}
85
86#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "snake_case")]
88pub enum ForkStatus {
89 Active,
90 Removed,
91 Missing,
93}
94
95#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97pub enum ForkCleanupPolicy {
98 #[default]
100 Explicit,
101 AutoOnTaskExit,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "camelCase")]
109pub struct ForkProvenance {
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub branch: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub source_branch: Option<String>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub source_commit: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub snapshot_id: Option<String>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub session_id: Option<String>,
120 #[serde(with = "time::serde::rfc3339")]
121 pub created_at: OffsetDateTime,
122}
123
124impl ForkProvenance {
125 pub fn at(created_at: OffsetDateTime) -> Self {
126 Self {
127 branch: None,
128 source_branch: None,
129 source_commit: None,
130 snapshot_id: None,
131 session_id: None,
132 created_at,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
138#[serde(rename_all = "camelCase")]
139pub struct ForkRequest {
140 pub source_workspace: PathBuf,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub ownership: Option<ForkOwnership>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub name: Option<String>,
146 pub reason: ForkReason,
147 #[serde(default)]
148 pub policy: ForkPolicy,
149 #[serde(default)]
151 pub provider_config: serde_json::Value,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155#[serde(rename_all = "camelCase")]
156pub struct WorkspaceFork {
157 pub id: ForkId,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub ownership: Option<ForkOwnership>,
160 pub provider_id: ForkProviderId,
161 pub source_workspace: PathBuf,
162 pub workspace: PathBuf,
164 pub status: ForkStatus,
165 pub provenance: ForkProvenance,
166 #[serde(default)]
167 pub cleanup: ForkCleanupPolicy,
168 #[serde(default)]
170 pub metadata: serde_json::Value,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176#[serde(rename_all = "camelCase")]
177pub struct RemoveForkPolicy {
178 pub confirm_workspace: PathBuf,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
182#[serde(rename_all = "camelCase")]
183pub struct RemoveForkResult {
184 pub id: ForkId,
185 pub removed: bool,
186 pub workspace: PathBuf,
187}
188
189#[async_trait::async_trait]
193pub trait ForkProvider: Send + Sync + 'static {
194 fn descriptor(&self) -> ForkProviderDescriptor;
195
196 async fn create_fork(&self, request: ForkRequest) -> anyhow::Result<WorkspaceFork>;
197
198 async fn list_forks(&self, source_workspace: &Path) -> anyhow::Result<Vec<WorkspaceFork>>;
200
201 async fn resume_fork(&self, id: &ForkId) -> anyhow::Result<WorkspaceFork>;
204
205 async fn remove_fork(
206 &self,
207 id: &ForkId,
208 policy: RemoveForkPolicy,
209 ) -> anyhow::Result<RemoveForkResult>;
210}