Skip to main content

agentoven_core/
client.rs

1//! AgentOven Client — connects to the AgentOven control plane.
2//!
3//! Provides a high-level API for agent registration, deployment, and management.
4
5use reqwest::Client;
6use url::Url;
7
8use crate::agent::{Agent, AgentMode};
9use crate::recipe::Recipe;
10
11/// Client for interacting with the AgentOven control plane.
12#[derive(Debug, Clone)]
13pub struct AgentOvenClient {
14    /// Control plane base URL.
15    base_url: Url,
16
17    /// HTTP client.
18    http: Client,
19
20    /// API key for authentication.
21    api_key: Option<String>,
22
23    /// Current kitchen (workspace) context.
24    kitchen_id: Option<String>,
25}
26
27impl AgentOvenClient {
28    /// Create a new client connected to the control plane.
29    pub fn new(base_url: &str) -> anyhow::Result<Self> {
30        Ok(Self {
31            base_url: Url::parse(base_url)?,
32            http: Client::new(),
33            api_key: None,
34            kitchen_id: None,
35        })
36    }
37
38    /// Create from config file + environment variables.
39    ///
40    /// Priority: CLI flags > env vars > `~/.agentoven/config.toml` > defaults.
41    pub fn from_env() -> anyhow::Result<Self> {
42        let cfg = crate::config::AgentOvenConfig::load();
43        Self::from_config(&cfg)
44    }
45
46    /// Create from an explicit config struct (no env-var lookup).
47    pub fn from_config(cfg: &crate::config::AgentOvenConfig) -> anyhow::Result<Self> {
48        let mut client = Self::new(&cfg.url)?;
49        client.api_key = cfg.auth_credential().map(|s| s.to_string());
50        client.kitchen_id = cfg.kitchen.clone();
51        Ok(client)
52    }
53
54    /// Set API key.
55    pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
56        self.api_key = Some(key.into());
57        self
58    }
59
60    /// Set the active kitchen (workspace).
61    pub fn with_kitchen(mut self, kitchen_id: impl Into<String>) -> Self {
62        self.kitchen_id = Some(kitchen_id.into());
63        self
64    }
65
66    // ── Agent Operations ─────────────────────────────────────
67
68    /// Register a new agent in the oven (menu).
69    ///
70    /// Sends only the user-provided fields; server-managed fields
71    /// (id, status, kitchen, timestamps, a2a_endpoint) are set server-side.
72    pub async fn register(&self, agent: &Agent) -> anyhow::Result<Agent> {
73        let url = self.url("/api/v1/agents");
74
75        // Build a minimal registration payload — only fields the server expects.
76        let mut body = serde_json::json!({
77            "name": agent.name,
78            "description": agent.description,
79            "framework": agent.framework,
80        });
81        let obj = body.as_object_mut().unwrap();
82
83        if !agent.version.is_empty() {
84            obj.insert("version".into(), agent.version.clone().into());
85        }
86        if !agent.model_provider.is_empty() {
87            obj.insert("model_provider".into(), agent.model_provider.clone().into());
88        }
89        if !agent.model_name.is_empty() {
90            obj.insert("model_name".into(), agent.model_name.clone().into());
91        }
92        if let Some(ref bp) = agent.backup_provider {
93            obj.insert("backup_provider".into(), bp.clone().into());
94        }
95        if let Some(ref bm) = agent.backup_model {
96            obj.insert("backup_model".into(), bm.clone().into());
97        }
98        if let Some(ref sp) = agent.system_prompt {
99            obj.insert("system_prompt".into(), sp.clone().into());
100        }
101        if let Some(mt) = agent.max_turns {
102            obj.insert("max_turns".into(), mt.into());
103        }
104        if !agent.skills.is_empty() {
105            obj.insert("skills".into(), serde_json::to_value(&agent.skills)?);
106        }
107        if !agent.ingredients.is_empty() {
108            obj.insert(
109                "ingredients".into(),
110                serde_json::to_value(&agent.ingredients)?,
111            );
112        }
113        if !agent.tags.is_empty() {
114            obj.insert("tags".into(), serde_json::to_value(&agent.tags)?);
115        }
116        if !agent.guardrails.is_empty() {
117            obj.insert(
118                "guardrails".into(),
119                serde_json::to_value(&agent.guardrails)?,
120            );
121        }
122        if agent.mode != AgentMode::default() {
123            obj.insert("mode".into(), serde_json::to_value(&agent.mode)?);
124        }
125
126        let resp = self
127            .authed_request(self.http.post(url))
128            .json(&body)
129            .send()
130            .await?
131            .error_for_status()?;
132        Ok(resp.json().await?)
133    }
134
135    /// Get an agent by name and optional version.
136    pub async fn get_agent(&self, name: &str, version: Option<&str>) -> anyhow::Result<Agent> {
137        let path = match version {
138            Some(v) => format!("/api/v1/agents/{name}/versions/{v}"),
139            None => format!("/api/v1/agents/{name}"),
140        };
141        let url = self.url(&path);
142        let resp = self
143            .authed_request(self.http.get(url))
144            .send()
145            .await?
146            .error_for_status()?;
147        Ok(resp.json().await?)
148    }
149
150    /// List all agents in the current kitchen.
151    pub async fn list_agents(&self) -> anyhow::Result<Vec<Agent>> {
152        let url = self.url("/api/v1/agents");
153        let resp = self
154            .authed_request(self.http.get(url))
155            .send()
156            .await?
157            .error_for_status()?;
158        Ok(resp.json().await?)
159    }
160
161    /// Bake (deploy) an agent to an environment.
162    pub async fn bake(&self, agent: &Agent, environment: &str) -> anyhow::Result<Agent> {
163        let url = self.url(&format!("/api/v1/agents/{}/bake", agent.name));
164        let resp = self
165            .authed_request(self.http.post(url))
166            .json(&serde_json::json!({
167                "version": agent.version,
168                "environment": environment,
169            }))
170            .send()
171            .await?
172            .error_for_status()?;
173        Ok(resp.json().await?)
174    }
175
176    /// Rewarm a cooled agent (transition back to ready).
177    pub async fn rewarm(&self, name: &str) -> anyhow::Result<serde_json::Value> {
178        let url = self.url(&format!("/api/v1/agents/{name}/rewarm"));
179        let resp = self
180            .authed_request(self.http.post(url))
181            .send()
182            .await?
183            .error_for_status()?;
184        Ok(resp.json().await?)
185    }
186
187    // ── Recipe Operations ────────────────────────────────────
188
189    /// Create a new recipe (workflow).
190    pub async fn create_recipe(&self, recipe: &Recipe) -> anyhow::Result<Recipe> {
191        let url = self.url("/api/v1/recipes");
192        let resp = self
193            .authed_request(self.http.post(url))
194            .json(recipe)
195            .send()
196            .await?
197            .error_for_status()?;
198        Ok(resp.json().await?)
199    }
200
201    /// Bake (execute) a recipe with input.
202    pub async fn bake_recipe(
203        &self,
204        recipe_name: &str,
205        input: serde_json::Value,
206    ) -> anyhow::Result<serde_json::Value> {
207        let url = self.url(&format!("/api/v1/recipes/{recipe_name}/bake"));
208        let resp = self
209            .authed_request(self.http.post(url))
210            .json(&input)
211            .send()
212            .await?
213            .error_for_status()?;
214        Ok(resp.json().await?)
215    }
216
217    // ── Agent Lifecycle ──────────────────────────────────────
218
219    /// Update an existing agent.
220    pub async fn update_agent(
221        &self,
222        name: &str,
223        updates: serde_json::Value,
224    ) -> anyhow::Result<Agent> {
225        let url = self.url(&format!("/api/v1/agents/{name}"));
226        let resp = self
227            .authed_request(self.http.put(url))
228            .json(&updates)
229            .send()
230            .await?
231            .error_for_status()?;
232        Ok(resp.json().await?)
233    }
234
235    /// Delete an agent.
236    pub async fn delete_agent(&self, name: &str) -> anyhow::Result<()> {
237        let url = self.url(&format!("/api/v1/agents/{name}"));
238        self.authed_request(self.http.delete(url))
239            .send()
240            .await?
241            .error_for_status()?;
242        Ok(())
243    }
244
245    /// Re-cook an agent with edits.
246    pub async fn recook_agent(
247        &self,
248        name: &str,
249        edits: serde_json::Value,
250    ) -> anyhow::Result<serde_json::Value> {
251        let url = self.url(&format!("/api/v1/agents/{name}/recook"));
252        let resp = self
253            .authed_request(self.http.post(url))
254            .json(&edits)
255            .send()
256            .await?
257            .error_for_status()?;
258        Ok(resp.json().await?)
259    }
260
261    /// Cool (pause) an agent.
262    pub async fn cool_agent(&self, name: &str) -> anyhow::Result<serde_json::Value> {
263        let url = self.url(&format!("/api/v1/agents/{name}/cool"));
264        let resp = self
265            .authed_request(self.http.post(url))
266            .send()
267            .await?
268            .error_for_status()?;
269        Ok(resp.json().await?)
270    }
271
272    /// Retire an agent permanently.
273    pub async fn retire_agent(&self, name: &str) -> anyhow::Result<serde_json::Value> {
274        let url = self.url(&format!("/api/v1/agents/{name}/retire"));
275        let resp = self
276            .authed_request(self.http.post(url))
277            .send()
278            .await?
279            .error_for_status()?;
280        Ok(resp.json().await?)
281    }
282
283    /// Test an agent (one-shot, via /test endpoint).
284    pub async fn test_agent(
285        &self,
286        name: &str,
287        message: &str,
288        thinking: bool,
289    ) -> anyhow::Result<serde_json::Value> {
290        let url = self.url(&format!("/api/v1/agents/{name}/test"));
291        let resp = self
292            .authed_request(self.http.post(url))
293            .json(&serde_json::json!({
294                "message": message,
295                "thinking_enabled": thinking,
296            }))
297            .send()
298            .await?
299            .error_for_status()?;
300        Ok(resp.json().await?)
301    }
302
303    /// Invoke a managed agent (full agentic loop with execution trace).
304    pub async fn invoke_agent(
305        &self,
306        name: &str,
307        message: &str,
308        variables: Option<serde_json::Value>,
309        thinking: bool,
310    ) -> anyhow::Result<serde_json::Value> {
311        let url = self.url(&format!("/api/v1/agents/{name}/invoke"));
312        let mut body = serde_json::json!({
313            "message": message,
314            "thinking_enabled": thinking,
315        });
316        if let Some(vars) = variables {
317            body["variables"] = vars;
318        }
319        let resp = self
320            .authed_request(self.http.post(url))
321            .json(&body)
322            .send()
323            .await?
324            .error_for_status()?;
325        Ok(resp.json().await?)
326    }
327
328    /// Get the resolved configuration for a baked agent.
329    pub async fn agent_config(&self, name: &str) -> anyhow::Result<serde_json::Value> {
330        let url = self.url(&format!("/api/v1/agents/{name}/config"));
331        let resp = self
332            .authed_request(self.http.get(url))
333            .send()
334            .await?
335            .error_for_status()?;
336        Ok(resp.json().await?)
337    }
338
339    /// Get the A2A Agent Card for an agent.
340    pub async fn agent_card(&self, name: &str) -> anyhow::Result<serde_json::Value> {
341        let url = self.url(&format!("/api/v1/agents/{name}/card"));
342        let resp = self
343            .authed_request(self.http.get(url))
344            .send()
345            .await?
346            .error_for_status()?;
347        Ok(resp.json().await?)
348    }
349
350    /// List version history for an agent.
351    pub async fn agent_versions(&self, name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
352        let url = self.url(&format!("/api/v1/agents/{name}/versions"));
353        let resp = self
354            .authed_request(self.http.get(url))
355            .send()
356            .await?
357            .error_for_status()?;
358        Ok(resp.json().await?)
359    }
360
361    // ── Provider Operations ──────────────────────────────────
362
363    /// List all model providers.
364    pub async fn list_providers(&self) -> anyhow::Result<Vec<serde_json::Value>> {
365        let url = self.url("/api/v1/models/providers");
366        let resp = self
367            .authed_request(self.http.get(url))
368            .send()
369            .await?
370            .error_for_status()?;
371        Ok(resp.json().await?)
372    }
373
374    /// Add a new model provider.
375    pub async fn add_provider(
376        &self,
377        provider: serde_json::Value,
378    ) -> anyhow::Result<serde_json::Value> {
379        let url = self.url("/api/v1/models/providers");
380        let resp = self
381            .authed_request(self.http.post(url))
382            .json(&provider)
383            .send()
384            .await?
385            .error_for_status()?;
386        Ok(resp.json().await?)
387    }
388
389    /// Get a specific model provider.
390    pub async fn get_provider(&self, name: &str) -> anyhow::Result<serde_json::Value> {
391        let url = self.url(&format!("/api/v1/models/providers/{name}"));
392        let resp = self
393            .authed_request(self.http.get(url))
394            .send()
395            .await?
396            .error_for_status()?;
397        Ok(resp.json().await?)
398    }
399
400    /// Update a model provider.
401    pub async fn update_provider(
402        &self,
403        name: &str,
404        provider: serde_json::Value,
405    ) -> anyhow::Result<serde_json::Value> {
406        let url = self.url(&format!("/api/v1/models/providers/{name}"));
407        let resp = self
408            .authed_request(self.http.put(url))
409            .json(&provider)
410            .send()
411            .await?
412            .error_for_status()?;
413        Ok(resp.json().await?)
414    }
415
416    /// Delete a model provider.
417    pub async fn delete_provider(&self, name: &str) -> anyhow::Result<()> {
418        let url = self.url(&format!("/api/v1/models/providers/{name}"));
419        self.authed_request(self.http.delete(url))
420            .send()
421            .await?
422            .error_for_status()?;
423        Ok(())
424    }
425
426    /// Test a provider's connectivity and credentials.
427    pub async fn test_provider(&self, name: &str) -> anyhow::Result<serde_json::Value> {
428        let url = self.url(&format!("/api/v1/models/providers/{name}/test"));
429        let resp = self
430            .authed_request(self.http.post(url))
431            .send()
432            .await?
433            .error_for_status()?;
434        Ok(resp.json().await?)
435    }
436
437    /// Discover models available from a provider.
438    pub async fn discover_provider(&self, name: &str) -> anyhow::Result<serde_json::Value> {
439        let url = self.url(&format!("/api/v1/models/providers/{name}/discover"));
440        let resp = self
441            .authed_request(self.http.post(url))
442            .send()
443            .await?
444            .error_for_status()?;
445        Ok(resp.json().await?)
446    }
447
448    // ── Tool Operations ──────────────────────────────────────
449
450    /// List all MCP tools.
451    pub async fn list_tools(&self) -> anyhow::Result<Vec<serde_json::Value>> {
452        let url = self.url("/api/v1/tools");
453        let resp = self
454            .authed_request(self.http.get(url))
455            .send()
456            .await?
457            .error_for_status()?;
458        Ok(resp.json().await?)
459    }
460
461    /// Add a new MCP tool.
462    pub async fn add_tool(&self, tool: serde_json::Value) -> anyhow::Result<serde_json::Value> {
463        let url = self.url("/api/v1/tools");
464        let resp = self
465            .authed_request(self.http.post(url))
466            .json(&tool)
467            .send()
468            .await?
469            .error_for_status()?;
470        Ok(resp.json().await?)
471    }
472
473    /// Bulk-add multiple MCP tools in one request.
474    pub async fn bulk_add_tools(
475        &self,
476        payload: serde_json::Value,
477    ) -> anyhow::Result<serde_json::Value> {
478        let url = self.url("/api/v1/tools/bulk");
479        let resp = self
480            .authed_request(self.http.post(url))
481            .json(&payload)
482            .send()
483            .await?
484            .error_for_status()?;
485        Ok(resp.json().await?)
486    }
487
488    /// Get a specific tool.
489    pub async fn get_tool(&self, name: &str) -> anyhow::Result<serde_json::Value> {
490        let url = self.url(&format!("/api/v1/tools/{name}"));
491        let resp = self
492            .authed_request(self.http.get(url))
493            .send()
494            .await?
495            .error_for_status()?;
496        Ok(resp.json().await?)
497    }
498
499    /// Update a tool.
500    pub async fn update_tool(
501        &self,
502        name: &str,
503        tool: serde_json::Value,
504    ) -> anyhow::Result<serde_json::Value> {
505        let url = self.url(&format!("/api/v1/tools/{name}"));
506        let resp = self
507            .authed_request(self.http.put(url))
508            .json(&tool)
509            .send()
510            .await?
511            .error_for_status()?;
512        Ok(resp.json().await?)
513    }
514
515    /// Delete a tool.
516    pub async fn delete_tool(&self, name: &str) -> anyhow::Result<()> {
517        let url = self.url(&format!("/api/v1/tools/{name}"));
518        self.authed_request(self.http.delete(url))
519            .send()
520            .await?
521            .error_for_status()?;
522        Ok(())
523    }
524
525    // ── Prompt Operations ────────────────────────────────────
526
527    /// List all prompt templates.
528    pub async fn list_prompts(&self) -> anyhow::Result<Vec<serde_json::Value>> {
529        let url = self.url("/api/v1/prompts");
530        let resp = self
531            .authed_request(self.http.get(url))
532            .send()
533            .await?
534            .error_for_status()?;
535        Ok(resp.json().await?)
536    }
537
538    /// Add a new prompt template.
539    pub async fn add_prompt(&self, prompt: serde_json::Value) -> anyhow::Result<serde_json::Value> {
540        let url = self.url("/api/v1/prompts");
541        let resp = self
542            .authed_request(self.http.post(url))
543            .json(&prompt)
544            .send()
545            .await?
546            .error_for_status()?;
547        Ok(resp.json().await?)
548    }
549
550    /// Get a specific prompt template.
551    pub async fn get_prompt(&self, name: &str) -> anyhow::Result<serde_json::Value> {
552        let url = self.url(&format!("/api/v1/prompts/{name}"));
553        let resp = self
554            .authed_request(self.http.get(url))
555            .send()
556            .await?
557            .error_for_status()?;
558        Ok(resp.json().await?)
559    }
560
561    /// Update a prompt template.
562    pub async fn update_prompt(
563        &self,
564        name: &str,
565        prompt: serde_json::Value,
566    ) -> anyhow::Result<serde_json::Value> {
567        let url = self.url(&format!("/api/v1/prompts/{name}"));
568        let resp = self
569            .authed_request(self.http.put(url))
570            .json(&prompt)
571            .send()
572            .await?
573            .error_for_status()?;
574        Ok(resp.json().await?)
575    }
576
577    /// Delete a prompt template.
578    pub async fn delete_prompt(&self, name: &str) -> anyhow::Result<()> {
579        let url = self.url(&format!("/api/v1/prompts/{name}"));
580        self.authed_request(self.http.delete(url))
581            .send()
582            .await?
583            .error_for_status()?;
584        Ok(())
585    }
586
587    /// Validate a prompt template.
588    pub async fn validate_prompt(&self, name: &str) -> anyhow::Result<serde_json::Value> {
589        let url = self.url(&format!("/api/v1/prompts/{name}/validate"));
590        let resp = self
591            .authed_request(self.http.post(url))
592            .send()
593            .await?
594            .error_for_status()?;
595        Ok(resp.json().await?)
596    }
597
598    /// List version history for a prompt.
599    pub async fn prompt_versions(&self, name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
600        let url = self.url(&format!("/api/v1/prompts/{name}/versions"));
601        let resp = self
602            .authed_request(self.http.get(url))
603            .send()
604            .await?
605            .error_for_status()?;
606        Ok(resp.json().await?)
607    }
608
609    // ── Kitchen Operations ───────────────────────────────────
610
611    /// List all kitchens.
612    pub async fn list_kitchens(&self) -> anyhow::Result<Vec<serde_json::Value>> {
613        let url = self.url("/api/v1/kitchens");
614        let resp = self
615            .authed_request(self.http.get(url))
616            .send()
617            .await?
618            .error_for_status()?;
619        Ok(resp.json().await?)
620    }
621
622    /// Get a specific kitchen.
623    pub async fn get_kitchen(&self, id: &str) -> anyhow::Result<serde_json::Value> {
624        let url = self.url(&format!("/api/v1/kitchens/{id}"));
625        let resp = self
626            .authed_request(self.http.get(url))
627            .send()
628            .await?
629            .error_for_status()?;
630        Ok(resp.json().await?)
631    }
632
633    /// Create a new kitchen.
634    pub async fn create_kitchen(
635        &self,
636        kitchen: serde_json::Value,
637    ) -> anyhow::Result<serde_json::Value> {
638        let url = self.url("/api/v1/kitchens");
639        let resp = self
640            .authed_request(self.http.post(url))
641            .json(&kitchen)
642            .send()
643            .await?
644            .error_for_status()?;
645        Ok(resp.json().await?)
646    }
647
648    /// Delete a kitchen.
649    pub async fn delete_kitchen(&self, id: &str) -> anyhow::Result<()> {
650        let url = self.url(&format!("/api/v1/kitchens/{id}"));
651        self.authed_request(self.http.delete(url))
652            .send()
653            .await?
654            .error_for_status()?;
655        Ok(())
656    }
657
658    /// Get server info (edition, features, limits).
659    /// The CLI calls this to discover what commands are available.
660    pub async fn server_info(&self) -> anyhow::Result<serde_json::Value> {
661        let url = self.url("/api/v1/info");
662        let resp = self
663            .authed_request(self.http.get(url))
664            .send()
665            .await?
666            .error_for_status()?;
667        Ok(resp.json().await?)
668    }
669
670    /// Get kitchen settings.
671    pub async fn get_settings(&self) -> anyhow::Result<serde_json::Value> {
672        let url = self.url("/api/v1/settings");
673        let resp = self
674            .authed_request(self.http.get(url))
675            .send()
676            .await?
677            .error_for_status()?;
678        Ok(resp.json().await?)
679    }
680
681    /// Update kitchen settings.
682    pub async fn update_settings(
683        &self,
684        settings: serde_json::Value,
685    ) -> anyhow::Result<serde_json::Value> {
686        let url = self.url("/api/v1/settings");
687        let resp = self
688            .authed_request(self.http.put(url))
689            .json(&settings)
690            .send()
691            .await?
692            .error_for_status()?;
693        Ok(resp.json().await?)
694    }
695
696    // ── Session Operations ───────────────────────────────────
697
698    /// List sessions for an agent.
699    pub async fn list_sessions(&self, agent_name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
700        let url = self.url(&format!("/api/v1/agents/{agent_name}/sessions"));
701        let resp = self
702            .authed_request(self.http.get(url))
703            .send()
704            .await?
705            .error_for_status()?;
706        Ok(resp.json().await?)
707    }
708
709    /// Create a new session for an agent.
710    pub async fn create_session(&self, agent_name: &str) -> anyhow::Result<serde_json::Value> {
711        let url = self.url(&format!("/api/v1/agents/{agent_name}/sessions"));
712        let resp = self
713            .authed_request(self.http.post(url))
714            .json(&serde_json::json!({}))
715            .send()
716            .await?
717            .error_for_status()?;
718        Ok(resp.json().await?)
719    }
720
721    /// Get a specific session.
722    pub async fn get_session(
723        &self,
724        agent_name: &str,
725        session_id: &str,
726    ) -> anyhow::Result<serde_json::Value> {
727        let url = self.url(&format!(
728            "/api/v1/agents/{agent_name}/sessions/{session_id}"
729        ));
730        let resp = self
731            .authed_request(self.http.get(url))
732            .send()
733            .await?
734            .error_for_status()?;
735        Ok(resp.json().await?)
736    }
737
738    /// Delete a session.
739    pub async fn delete_session(&self, agent_name: &str, session_id: &str) -> anyhow::Result<()> {
740        let url = self.url(&format!(
741            "/api/v1/agents/{agent_name}/sessions/{session_id}"
742        ));
743        self.authed_request(self.http.delete(url))
744            .send()
745            .await?
746            .error_for_status()?;
747        Ok(())
748    }
749
750    /// Send a message within a session.
751    pub async fn send_session_message(
752        &self,
753        agent_name: &str,
754        session_id: &str,
755        message: &str,
756        thinking: bool,
757    ) -> anyhow::Result<serde_json::Value> {
758        let url = self.url(&format!(
759            "/api/v1/agents/{agent_name}/sessions/{session_id}/messages"
760        ));
761        let resp = self
762            .authed_request(self.http.post(url))
763            .json(&serde_json::json!({
764                "message": message,
765                "thinking_enabled": thinking,
766            }))
767            .send()
768            .await?
769            .error_for_status()?;
770        Ok(resp.json().await?)
771    }
772
773    // ── RAG Operations ───────────────────────────────────────
774
775    /// Query the RAG pipeline.
776    pub async fn rag_query(&self, query: serde_json::Value) -> anyhow::Result<serde_json::Value> {
777        let url = self.url("/api/v1/rag/query");
778        let resp = self
779            .authed_request(self.http.post(url))
780            .json(&query)
781            .send()
782            .await?
783            .error_for_status()?;
784        Ok(resp.json().await?)
785    }
786
787    /// Ingest documents into the RAG pipeline.
788    pub async fn rag_ingest(
789        &self,
790        request: serde_json::Value,
791    ) -> anyhow::Result<serde_json::Value> {
792        let url = self.url("/api/v1/rag/ingest");
793        let resp = self
794            .authed_request(self.http.post(url))
795            .json(&request)
796            .send()
797            .await?
798            .error_for_status()?;
799        Ok(resp.json().await?)
800    }
801
802    // ── Trace Operations ─────────────────────────────────────
803
804    /// List recent traces.
805    pub async fn list_traces(
806        &self,
807        agent: Option<&str>,
808        limit: u32,
809    ) -> anyhow::Result<Vec<serde_json::Value>> {
810        let mut url = self.url("/api/v1/traces");
811        {
812            let mut query = url.query_pairs_mut();
813            query.append_pair("limit", &limit.to_string());
814            if let Some(a) = agent {
815                query.append_pair("agent", a);
816            }
817        }
818        let resp = self
819            .authed_request(self.http.get(url))
820            .send()
821            .await?
822            .error_for_status()?;
823        Ok(resp.json().await?)
824    }
825
826    /// Get a specific trace.
827    pub async fn get_trace(&self, trace_id: &str) -> anyhow::Result<serde_json::Value> {
828        let url = self.url(&format!("/api/v1/traces/{trace_id}"));
829        let resp = self
830            .authed_request(self.http.get(url))
831            .send()
832            .await?
833            .error_for_status()?;
834        Ok(resp.json().await?)
835    }
836
837    // ── Model Catalog Operations ─────────────────────────────
838
839    /// List model catalog.
840    pub async fn model_catalog(&self) -> anyhow::Result<Vec<serde_json::Value>> {
841        let url = self.url("/api/v1/models/catalog");
842        let resp = self
843            .authed_request(self.http.get(url))
844            .send()
845            .await?
846            .error_for_status()?;
847        Ok(resp.json().await?)
848    }
849
850    /// Refresh model catalog from providers.
851    pub async fn catalog_refresh(&self) -> anyhow::Result<serde_json::Value> {
852        let url = self.url("/api/v1/models/catalog/refresh");
853        let resp = self
854            .authed_request(self.http.post(url))
855            .send()
856            .await?
857            .error_for_status()?;
858        Ok(resp.json().await?)
859    }
860
861    /// Get cost summary.
862    pub async fn model_cost(&self) -> anyhow::Result<serde_json::Value> {
863        let url = self.url("/api/v1/models/cost");
864        let resp = self
865            .authed_request(self.http.get(url))
866            .send()
867            .await?
868            .error_for_status()?;
869        Ok(resp.json().await?)
870    }
871
872    // ── Recipe Extended Operations ───────────────────────────
873
874    /// Get a specific recipe.
875    pub async fn get_recipe(&self, name: &str) -> anyhow::Result<serde_json::Value> {
876        let url = self.url(&format!("/api/v1/recipes/{name}"));
877        let resp = self
878            .authed_request(self.http.get(url))
879            .send()
880            .await?
881            .error_for_status()?;
882        Ok(resp.json().await?)
883    }
884
885    /// List all recipes.
886    pub async fn list_recipes(&self) -> anyhow::Result<Vec<serde_json::Value>> {
887        let url = self.url("/api/v1/recipes");
888        let resp = self
889            .authed_request(self.http.get(url))
890            .send()
891            .await?
892            .error_for_status()?;
893        Ok(resp.json().await?)
894    }
895
896    /// Delete a recipe.
897    pub async fn delete_recipe(&self, name: &str) -> anyhow::Result<()> {
898        let url = self.url(&format!("/api/v1/recipes/{name}"));
899        self.authed_request(self.http.delete(url))
900            .send()
901            .await?
902            .error_for_status()?;
903        Ok(())
904    }
905
906    /// List runs for a recipe.
907    pub async fn recipe_runs(&self, name: &str) -> anyhow::Result<Vec<serde_json::Value>> {
908        let url = self.url(&format!("/api/v1/recipes/{name}/runs"));
909        let resp = self
910            .authed_request(self.http.get(url))
911            .send()
912            .await?
913            .error_for_status()?;
914        Ok(resp.json().await?)
915    }
916
917    /// Approve a human gate in a recipe run.
918    pub async fn approve_gate(
919        &self,
920        recipe: &str,
921        run_id: &str,
922        step_name: &str,
923        approved: bool,
924        comment: Option<&str>,
925    ) -> anyhow::Result<serde_json::Value> {
926        let url = self.url(&format!(
927            "/api/v1/recipes/{recipe}/runs/{run_id}/gates/{step_name}/approve"
928        ));
929        let mut body = serde_json::json!({ "approved": approved });
930        if let Some(c) = comment {
931            body["comment"] = serde_json::json!(c);
932        }
933        let resp = self
934            .authed_request(self.http.post(url))
935            .json(&body)
936            .send()
937            .await?
938            .error_for_status()?;
939        Ok(resp.json().await?)
940    }
941
942    // ── Audit Operations ─────────────────────────────────────
943
944    /// List audit events.
945    pub async fn list_audit(&self, limit: u32) -> anyhow::Result<Vec<serde_json::Value>> {
946        let mut url = self.url("/api/v1/audit");
947        url.query_pairs_mut()
948            .append_pair("limit", &limit.to_string());
949        let resp = self
950            .authed_request(self.http.get(url))
951            .send()
952            .await?
953            .error_for_status()?;
954        Ok(resp.json().await?)
955    }
956
957    // ── Guardrail Operations ─────────────────────────────────
958
959    /// List available guardrail kinds.
960    pub async fn guardrail_kinds(&self) -> anyhow::Result<Vec<serde_json::Value>> {
961        let url = self.url("/api/v1/guardrails/kinds");
962        let resp = self
963            .authed_request(self.http.get(url))
964            .send()
965            .await?
966            .error_for_status()?;
967        Ok(resp.json().await?)
968    }
969
970    // ── Generic HTTP helpers (for Pro-gated commands) ──────
971
972    /// Generic GET that returns deserialized JSON.
973    pub async fn raw_get<T: serde::de::DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
974        let url = self.url(path);
975        let resp = self
976            .authed_request(self.http.get(url))
977            .send()
978            .await?
979            .error_for_status()?;
980        Ok(resp.json().await?)
981    }
982
983    /// Generic POST that sends JSON and returns deserialized JSON.
984    pub async fn raw_post<T: serde::de::DeserializeOwned>(
985        &self,
986        path: &str,
987        body: &serde_json::Value,
988    ) -> anyhow::Result<T> {
989        let url = self.url(path);
990        let resp = self
991            .authed_request(self.http.post(url))
992            .json(body)
993            .send()
994            .await?
995            .error_for_status()?;
996        Ok(resp.json().await?)
997    }
998
999    /// Generic DELETE.
1000    pub async fn raw_delete(&self, path: &str) -> anyhow::Result<()> {
1001        let url = self.url(path);
1002        self.authed_request(self.http.delete(url))
1003            .send()
1004            .await?
1005            .error_for_status()?;
1006        Ok(())
1007    }
1008
1009    // ── Internal ─────────────────────────────────────────────
1010
1011    fn url(&self, path: &str) -> Url {
1012        self.base_url.join(path).expect("Invalid URL path")
1013    }
1014
1015    fn authed_request(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
1016        let mut b = builder;
1017        if let Some(ref key) = self.api_key {
1018            b = b.bearer_auth(key);
1019        }
1020        if let Some(ref kitchen) = self.kitchen_id {
1021            b = b.header("X-Kitchen-Id", kitchen.as_str());
1022        }
1023        b
1024    }
1025}