1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
7#[serde(rename_all = "camelCase")]
8pub struct ApiKeyInfo {
9 pub api_key_name: String,
10 pub created_at: String,
11 pub user_email: Option<String>,
12}
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct ListResponse<T> {
17 pub items: Vec<T>,
18 pub next_cursor: Option<String>,
19}
20
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct ModelListResponse {
23 pub items: Vec<String>,
24}
25
26#[derive(Debug, Clone, Deserialize, Serialize)]
27pub struct RepositoryListResponse {
28 pub items: Vec<RepositoryItem>,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize)]
32pub struct RepositoryItem {
33 pub url: String,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize)]
37pub struct EnvironmentInfo {
38 #[serde(rename = "type")]
39 pub kind: String,
40 pub name: Option<String>,
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct RepositoryRef {
46 pub url: Option<String>,
47 pub starting_ref: Option<String>,
48 pub pr_url: Option<String>,
49}
50
51#[derive(Debug, Clone, Deserialize, Serialize)]
52pub struct ModelSelection {
53 pub id: String,
54}
55
56#[derive(Debug, Clone, Deserialize, Serialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ImageInput {
59 pub data: String,
60 pub mime_type: String,
61}
62
63#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct Prompt {
65 pub text: String,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub images: Vec<ImageInput>,
68}
69
70#[derive(Debug, Clone, Deserialize, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct AgentSummary {
73 pub id: String,
74 pub name: String,
75 pub status: String,
76 pub env: EnvironmentInfo,
77 pub url: String,
78 pub created_at: String,
79 pub updated_at: String,
80 pub latest_run_id: Option<String>,
81}
82
83#[derive(Debug, Clone, Deserialize, Serialize)]
84#[serde(rename_all = "camelCase")]
85pub struct Agent {
86 pub id: String,
87 pub name: String,
88 pub status: String,
89 pub env: EnvironmentInfo,
90 #[serde(default)]
91 pub repos: Vec<RepositoryRef>,
92 pub branch_name: Option<String>,
93 pub auto_generate_branch: Option<bool>,
94 pub auto_create_pr: Option<bool>,
95 pub skip_reviewer_request: Option<bool>,
96 pub url: String,
97 pub created_at: String,
98 pub updated_at: String,
99 pub latest_run_id: Option<String>,
100}
101
102#[derive(Debug, Clone, Deserialize, Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct Run {
105 pub id: String,
106 pub agent_id: String,
107 pub status: String,
108 pub created_at: String,
109 pub updated_at: String,
110}
111
112impl Run {
113 pub fn is_terminal(&self) -> bool {
114 matches!(
115 self.status.as_str(),
116 "FINISHED" | "ERROR" | "CANCELLED" | "EXPIRED"
117 )
118 }
119}
120
121#[derive(Debug, Clone, Deserialize, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct CreateAgentRequest {
124 pub prompt: Prompt,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub model: Option<ModelSelection>,
127 pub repos: Vec<RepositoryRef>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub branch_name: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub auto_generate_branch: Option<bool>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub auto_create_pr: Option<bool>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub skip_reviewer_request: Option<bool>,
136}
137
138#[derive(Debug, Clone, Deserialize, Serialize)]
139pub struct CreateRunRequest {
140 pub prompt: Prompt,
141}
142
143#[derive(Debug, Clone, Deserialize, Serialize)]
144pub struct CreateAgentResponse {
145 pub agent: Agent,
146 pub run: Run,
147}
148
149#[derive(Debug, Clone, Deserialize, Serialize)]
150pub struct CreateRunResponse {
151 pub run: Run,
152}
153
154#[derive(Debug, Clone, Deserialize, Serialize)]
155#[serde(rename_all = "camelCase")]
156pub struct Artifact {
157 pub path: String,
158 pub size_bytes: u64,
159 pub updated_at: String,
160}
161
162#[derive(Debug, Clone, Deserialize, Serialize)]
163#[serde(rename_all = "camelCase")]
164pub struct DownloadArtifactResponse {
165 pub url: String,
166 pub expires_at: String,
167}
168
169#[derive(Debug, Clone, Serialize)]
170pub struct RunStreamMessage {
171 pub id: Option<String>,
172 pub event: RunStreamEvent,
173}
174
175#[derive(Debug, Clone, Serialize)]
176pub enum RunStreamEvent {
177 Status {
178 run_id: String,
179 status: String,
180 },
181 Assistant {
182 text: String,
183 },
184 Thinking {
185 text: String,
186 },
187 ToolCall {
188 payload: Value,
189 },
190 Heartbeat,
191 Result {
192 run_id: String,
193 status: String,
194 },
195 Error {
196 code: Option<String>,
197 message: String,
198 },
199 Done,
200 Unknown {
201 name: String,
202 payload: Value,
203 },
204}
205
206#[derive(Debug, Clone)]
207pub struct WaitForRunOptions {
208 pub last_event_id: Option<String>,
209 pub poll_interval: Duration,
210 pub timeout: Option<Duration>,
211 pub max_poll_attempts: Option<u32>,
212}
213
214impl Default for WaitForRunOptions {
215 fn default() -> Self {
216 Self {
217 last_event_id: None,
218 poll_interval: Duration::from_secs(2),
219 timeout: None,
220 max_poll_attempts: None,
221 }
222 }
223}
224
225#[derive(Debug, Clone)]
226pub struct WaitForRunResult {
227 pub run: Run,
228 pub stream_messages: Vec<RunStreamMessage>,
229 pub last_event_id: Option<String>,
230 pub used_polling_fallback: bool,
231}
232
233#[cfg(test)]
234mod tests {
235 use super::Run;
236
237 fn run_with_status(status: &str) -> Run {
238 Run {
239 id: "run-1".to_owned(),
240 agent_id: "bc-1".to_owned(),
241 status: status.to_owned(),
242 created_at: "2026-04-13T18:30:00.000Z".to_owned(),
243 updated_at: "2026-04-13T18:30:00.000Z".to_owned(),
244 }
245 }
246
247 #[test]
248 fn terminal_statuses_are_detected() {
249 for status in ["FINISHED", "ERROR", "CANCELLED", "EXPIRED"] {
250 assert!(run_with_status(status).is_terminal(), "status {status} should be terminal");
251 }
252
253 for status in ["CREATING", "RUNNING", "ACTIVE"] {
254 assert!(!run_with_status(status).is_terminal(), "status {status} should not be terminal");
255 }
256 }
257}