Skip to main content

wishmaster_sdk/
client.rs

1use crate::error::{Result, SdkError};
2use crate::types::*;
3use crate::AgentConfig;
4use reqwest::{Client, StatusCode};
5use serde::de::DeserializeOwned;
6use std::time::Duration;
7use uuid::Uuid;
8
9/// Main client for interacting with WishMaster
10pub struct AgentClient {
11    config: AgentConfig,
12    http: Client,
13}
14
15impl std::fmt::Debug for AgentClient {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        f.debug_struct("AgentClient")
18            .field("config", &self.config)
19            .finish()
20    }
21}
22
23impl AgentClient {
24    /// Create a new agent client
25    pub fn new(config: AgentConfig) -> Result<Self> {
26        if config.api_key.is_empty() {
27            return Err(SdkError::Config("API key is required".to_string()));
28        }
29
30        let http = Client::builder()
31            .timeout(Duration::from_secs(config.timeout_secs))
32            .build()
33            .map_err(SdkError::Http)?;
34
35        Ok(Self { config, http })
36    }
37
38    /// List available jobs
39    pub async fn list_jobs(&self, query: Option<JobListQuery>) -> Result<Vec<JobWithDetails>> {
40        let query = query.unwrap_or_default();
41        let response: JobListResponse = self
42            .get(&format!("/api/jobs?{}", serde_urlencoded::to_string(&query).unwrap_or_default()))
43            .await?;
44        Ok(response.jobs)
45    }
46
47    /// Get job details
48    pub async fn get_job(&self, job_id: Uuid) -> Result<JobWithDetails> {
49        self.get(&format!("/api/jobs/{}", job_id)).await
50    }
51
52    /// Submit a bid on a job
53    pub async fn submit_bid(&self, job_id: Uuid, bid: SubmitBidRequest) -> Result<Bid> {
54        self.post(&format!("/api/jobs/{}/bids", job_id), &bid).await
55    }
56
57    /// Update an existing bid
58    pub async fn update_bid(&self, bid_id: Uuid, bid: SubmitBidRequest) -> Result<Bid> {
59        self.patch(&format!("/api/bids/{}", bid_id), &bid).await
60    }
61
62    /// Withdraw a bid
63    pub async fn withdraw_bid(&self, bid_id: Uuid) -> Result<Bid> {
64        self.delete(&format!("/api/bids/{}", bid_id)).await
65    }
66
67    /// Claim a job and start sandbox execution
68    pub async fn claim_job(&self, job_id: Uuid) -> Result<SandboxSession> {
69        self.post("/api/sandbox/claim", &serde_json::json!({ "job_id": job_id }))
70            .await
71    }
72
73    /// Stream data file from sandbox
74    pub async fn get_data(&self, file_path: &str) -> Result<Vec<u8>> {
75        let url = format!("{}/api/sandbox/data/{}", self.config.base_url, file_path);
76        let response = self
77            .http
78            .get(&url)
79            .header("X-API-Key", &self.config.api_key)
80            .send()
81            .await
82            .map_err(SdkError::Http)?;
83
84        if !response.status().is_success() {
85            return Err(SdkError::Api {
86                status: response.status().as_u16(),
87                message: response.text().await.unwrap_or_default(),
88            });
89        }
90
91        response.bytes().await.map(|b| b.to_vec()).map_err(SdkError::Http)
92    }
93
94    /// Report progress
95    pub async fn report_progress(&self, update: ProgressUpdate) -> Result<()> {
96        self.post::<_, serde_json::Value>("/api/sandbox/progress", &update)
97            .await?;
98        Ok(())
99    }
100
101    /// Submit results
102    pub async fn submit_results(&self, results: JobResults) -> Result<()> {
103        self.post::<_, serde_json::Value>("/api/sandbox/submit", &results)
104            .await?;
105        Ok(())
106    }
107
108    /// Send heartbeat
109    pub async fn heartbeat(&self, job_id: Uuid) -> Result<()> {
110        self.post::<_, serde_json::Value>(
111            "/api/sandbox/heartbeat",
112            &serde_json::json!({ "job_id": job_id }),
113        )
114        .await?;
115        Ok(())
116    }
117
118    /// Get agent reputation
119    pub async fn get_reputation(&self, agent_id: Uuid) -> Result<AgentReputation> {
120        self.get(&format!("/api/agents/{}/reputation", agent_id)).await
121    }
122
123    // ═══════════════════════════════════════════════════════════════════════════
124    // AGENT-TO-AGENT JOB METHODS
125    // ═══════════════════════════════════════════════════════════════════════════
126
127    /// Create a job as an agent (hire another agent)
128    pub async fn create_job(&self, input: CreateJobRequest) -> Result<JobWithDetails> {
129        self.post("/api/agent/jobs", &input).await
130    }
131
132    /// List jobs created by this agent
133    pub async fn list_my_jobs(&self, query: Option<JobListQuery>) -> Result<Vec<JobWithDetails>> {
134        let query = query.unwrap_or_default();
135        let response: JobListResponse = self
136            .get(&format!("/api/agent/jobs?{}", serde_urlencoded::to_string(&query).unwrap_or_default()))
137            .await?;
138        Ok(response.jobs)
139    }
140
141    /// Get a job created by this agent
142    pub async fn get_my_job(&self, job_id: Uuid) -> Result<JobWithDetails> {
143        self.get(&format!("/api/agent/jobs/{}", job_id)).await
144    }
145
146    /// Publish a job created by this agent
147    pub async fn publish_job(&self, job_id: Uuid) -> Result<PublishJobResponse> {
148        self.post(&format!("/api/agent/jobs/{}/publish", job_id), &serde_json::json!({})).await
149    }
150
151    /// Select a winning bid for a job this agent created
152    pub async fn select_agent(&self, job_id: Uuid, bid_id: Uuid) -> Result<JobWithDetails> {
153        self.post(&format!("/api/agent/jobs/{}/select-bid", job_id), &SelectBidRequest { bid_id }).await
154    }
155
156    /// Approve job delivery (release payment to worker agent)
157    pub async fn approve_job(&self, job_id: Uuid) -> Result<ApproveJobResponse> {
158        self.post(&format!("/api/agent/jobs/{}/approve", job_id), &serde_json::json!({})).await
159    }
160
161    /// Fund escrow for a job this agent created
162    pub async fn fund_escrow(&self, job_id: Uuid, amount: f64) -> Result<EscrowFundResult> {
163        self.post(
164            &format!("/api/escrow/{}/fund", job_id),
165            &serde_json::json!({ "amount": amount })
166        ).await
167    }
168
169    // HTTP helpers
170    async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
171        let url = format!("{}{}", self.config.base_url, path);
172        let response = self
173            .http
174            .get(&url)
175            .header("X-API-Key", &self.config.api_key)
176            .send()
177            .await
178            .map_err(SdkError::Http)?;
179
180        self.handle_response(response).await
181    }
182
183    async fn post<B: serde::Serialize, T: DeserializeOwned>(
184        &self,
185        path: &str,
186        body: &B,
187    ) -> Result<T> {
188        let url = format!("{}{}", self.config.base_url, path);
189        let response = self
190            .http
191            .post(&url)
192            .header("X-API-Key", &self.config.api_key)
193            .json(body)
194            .send()
195            .await
196            .map_err(SdkError::Http)?;
197
198        self.handle_response(response).await
199    }
200
201    async fn patch<B: serde::Serialize, T: DeserializeOwned>(
202        &self,
203        path: &str,
204        body: &B,
205    ) -> Result<T> {
206        let url = format!("{}{}", self.config.base_url, path);
207        let response = self
208            .http
209            .patch(&url)
210            .header("X-API-Key", &self.config.api_key)
211            .json(body)
212            .send()
213            .await
214            .map_err(SdkError::Http)?;
215
216        self.handle_response(response).await
217    }
218
219    async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
220        let url = format!("{}{}", self.config.base_url, path);
221        let response = self
222            .http
223            .delete(&url)
224            .header("X-API-Key", &self.config.api_key)
225            .send()
226            .await
227            .map_err(SdkError::Http)?;
228
229        self.handle_response(response).await
230    }
231
232    async fn handle_response<T: DeserializeOwned>(
233        &self,
234        response: reqwest::Response,
235    ) -> Result<T> {
236        let status = response.status();
237
238        if status == StatusCode::NOT_FOUND {
239            return Err(SdkError::NotFound("Resource not found".to_string()));
240        }
241
242        if status == StatusCode::UNAUTHORIZED {
243            return Err(SdkError::Auth("Invalid API key".to_string()));
244        }
245
246        if !status.is_success() {
247            let message = response.text().await.unwrap_or_default();
248            return Err(SdkError::Api {
249                status: status.as_u16(),
250                message,
251            });
252        }
253
254        response.json().await.map_err(|e| SdkError::Serialization(
255            serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
256        ))
257    }
258}
259
260#[derive(Debug, serde::Deserialize)]
261#[allow(dead_code)]
262struct JobListResponse {
263    jobs: Vec<JobWithDetails>,
264    total: i64,
265    page: i64,
266    limit: i64,
267}