gemini_adk_fluent_rs/compose/
artifacts.rs

1//! A — Artifact composition.
2//!
3//! Declare the artifacts an agent consumes and produces, combined with `+`,
4//! and attach them with
5//! [`AgentBuilder::artifacts`](crate::builder::AgentBuilder::artifacts), where
6//! contract checks read them.
7
8use std::sync::Arc;
9
10use serde::{Deserialize, Serialize};
11
12/// An artifact schema describing expected artifact structure.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ArtifactSchema {
15    /// Artifact name/key.
16    pub name: String,
17    /// MIME type.
18    pub mime_type: String,
19    /// Description of what this artifact contains.
20    pub description: String,
21}
22
23/// An artifact transform — a pipeline step that produces or consumes artifacts.
24#[derive(Debug, Clone)]
25pub struct ArtifactTransform {
26    /// Artifacts consumed (input).
27    pub inputs: Vec<ArtifactSchema>,
28    /// Artifacts produced (output).
29    pub outputs: Vec<ArtifactSchema>,
30}
31
32impl ArtifactTransform {
33    /// Create a transform that only produces artifacts.
34    pub fn produces(schemas: Vec<ArtifactSchema>) -> Self {
35        Self {
36            inputs: Vec::new(),
37            outputs: schemas,
38        }
39    }
40
41    /// Create a transform that only consumes artifacts.
42    pub fn consumes(schemas: Vec<ArtifactSchema>) -> Self {
43        Self {
44            inputs: schemas,
45            outputs: Vec::new(),
46        }
47    }
48
49    /// Number of input + output schemas.
50    pub fn len(&self) -> usize {
51        self.inputs.len() + self.outputs.len()
52    }
53
54    /// Whether empty.
55    pub fn is_empty(&self) -> bool {
56        self.inputs.is_empty() && self.outputs.is_empty()
57    }
58}
59
60/// An artifact composite — multiple transforms composed together.
61#[derive(Debug, Clone)]
62pub struct ArtifactComposite {
63    /// The list of artifact transforms in this composite.
64    pub transforms: Vec<ArtifactTransform>,
65}
66
67impl ArtifactComposite {
68    /// Create from a single transform.
69    pub fn from_transform(transform: ArtifactTransform) -> Self {
70        Self {
71            transforms: vec![transform],
72        }
73    }
74
75    /// All input schemas across all transforms.
76    pub fn all_inputs(&self) -> Vec<&ArtifactSchema> {
77        self.transforms.iter().flat_map(|t| &t.inputs).collect()
78    }
79
80    /// All output schemas across all transforms.
81    pub fn all_outputs(&self) -> Vec<&ArtifactSchema> {
82        self.transforms.iter().flat_map(|t| &t.outputs).collect()
83    }
84
85    /// Total number of transforms.
86    pub fn len(&self) -> usize {
87        self.transforms.len()
88    }
89
90    /// Whether empty.
91    pub fn is_empty(&self) -> bool {
92        self.transforms.is_empty()
93    }
94}
95
96/// Compose two artifact composites with `+`.
97impl std::ops::Add for ArtifactComposite {
98    type Output = ArtifactComposite;
99
100    fn add(mut self, rhs: ArtifactComposite) -> Self::Output {
101        self.transforms.extend(rhs.transforms);
102        self
103    }
104}
105
106/// The `A` namespace — static factory methods for artifact composition.
107pub struct A;
108
109impl A {
110    /// Declare an artifact that this agent produces.
111    pub fn output(
112        name: impl Into<String>,
113        mime_type: impl Into<String>,
114        description: impl Into<String>,
115    ) -> ArtifactComposite {
116        ArtifactComposite::from_transform(ArtifactTransform::produces(vec![ArtifactSchema {
117            name: name.into(),
118            mime_type: mime_type.into(),
119            description: description.into(),
120        }]))
121    }
122
123    /// Declare an artifact that this agent consumes.
124    pub fn input(
125        name: impl Into<String>,
126        mime_type: impl Into<String>,
127        description: impl Into<String>,
128    ) -> ArtifactComposite {
129        ArtifactComposite::from_transform(ArtifactTransform::consumes(vec![ArtifactSchema {
130            name: name.into(),
131            mime_type: mime_type.into(),
132            description: description.into(),
133        }]))
134    }
135
136    /// Declare a JSON artifact output.
137    pub fn json_output(
138        name: impl Into<String>,
139        description: impl Into<String>,
140    ) -> ArtifactComposite {
141        Self::output(name, "application/json", description)
142    }
143
144    /// Declare a JSON artifact input.
145    pub fn json_input(
146        name: impl Into<String>,
147        description: impl Into<String>,
148    ) -> ArtifactComposite {
149        Self::input(name, "application/json", description)
150    }
151
152    /// Declare a text artifact output.
153    pub fn text_output(
154        name: impl Into<String>,
155        description: impl Into<String>,
156    ) -> ArtifactComposite {
157        Self::output(name, "text/plain", description)
158    }
159
160    /// Declare a text artifact input.
161    pub fn text_input(
162        name: impl Into<String>,
163        description: impl Into<String>,
164    ) -> ArtifactComposite {
165        Self::input(name, "text/plain", description)
166    }
167
168    /// Publish an artifact with the given name and MIME type.
169    pub fn publish(name: impl Into<String>, mime_type: impl Into<String>) -> ArtifactOp {
170        ArtifactOp::Publish {
171            name: name.into(),
172            mime_type: mime_type.into(),
173        }
174    }
175
176    /// Save an artifact to storage.
177    pub fn save(name: impl Into<String>) -> ArtifactOp {
178        ArtifactOp::Save { name: name.into() }
179    }
180
181    /// Load an artifact from storage.
182    pub fn load(name: impl Into<String>) -> ArtifactOp {
183        ArtifactOp::Load { name: name.into() }
184    }
185
186    /// List available artifacts.
187    pub fn list() -> ArtifactOp {
188        ArtifactOp::List
189    }
190
191    /// Delete an artifact.
192    pub fn delete(name: impl Into<String>) -> ArtifactOp {
193        ArtifactOp::Delete { name: name.into() }
194    }
195
196    /// Get a specific version of an artifact.
197    pub fn version(name: impl Into<String>, version: u32) -> ArtifactOp {
198        ArtifactOp::Version {
199            name: name.into(),
200            version,
201        }
202    }
203
204    /// Convert an artifact to JSON format.
205    pub fn as_json(name: impl Into<String>) -> ArtifactOp {
206        ArtifactOp::AsJson { name: name.into() }
207    }
208
209    /// Convert an artifact to text format.
210    pub fn as_text(name: impl Into<String>) -> ArtifactOp {
211        ArtifactOp::AsText { name: name.into() }
212    }
213
214    /// Create an artifact from a JSON string.
215    pub fn from_json(name: impl Into<String>, data: impl Into<String>) -> ArtifactOp {
216        ArtifactOp::FromJson {
217            name: name.into(),
218            data: data.into(),
219        }
220    }
221
222    /// Create an artifact from a text string.
223    pub fn from_text(name: impl Into<String>, data: impl Into<String>) -> ArtifactOp {
224        ArtifactOp::FromText {
225            name: name.into(),
226            data: data.into(),
227        }
228    }
229
230    /// Conditional artifact operation — executes `inner` only when `predicate` returns true.
231    pub fn when(
232        predicate: impl Fn() -> bool + Send + Sync + 'static,
233        inner: ArtifactOp,
234    ) -> ArtifactOp {
235        ArtifactOp::When {
236            predicate: Arc::new(predicate),
237            inner: Box::new(inner),
238        }
239    }
240}
241
242/// A runtime artifact operation.
243///
244/// These represent deferred operations on artifacts that can be composed
245/// into pipelines using the `+` operator.
246#[derive(Clone)]
247pub enum ArtifactOp {
248    /// Publish an artifact with a given MIME type.
249    Publish {
250        /// Artifact name.
251        name: String,
252        /// MIME type.
253        mime_type: String,
254    },
255    /// Save an artifact to storage.
256    Save {
257        /// Artifact name.
258        name: String,
259    },
260    /// Load an artifact from storage.
261    Load {
262        /// Artifact name.
263        name: String,
264    },
265    /// List available artifacts.
266    List,
267    /// Delete an artifact.
268    Delete {
269        /// Artifact name.
270        name: String,
271    },
272    /// Get a specific version of an artifact.
273    Version {
274        /// Artifact name.
275        name: String,
276        /// Version number.
277        version: u32,
278    },
279    /// Convert an artifact to JSON format.
280    AsJson {
281        /// Artifact name.
282        name: String,
283    },
284    /// Convert an artifact to text format.
285    AsText {
286        /// Artifact name.
287        name: String,
288    },
289    /// Create an artifact from a JSON string.
290    FromJson {
291        /// Artifact name.
292        name: String,
293        /// JSON data.
294        data: String,
295    },
296    /// Create an artifact from a text string.
297    FromText {
298        /// Artifact name.
299        name: String,
300        /// Text data.
301        data: String,
302    },
303    /// Conditional operation — execute inner only when predicate is true.
304    When {
305        /// Predicate function.
306        #[allow(clippy::type_complexity)]
307        predicate: Arc<dyn Fn() -> bool + Send + Sync>,
308        /// Inner operation to conditionally execute.
309        inner: Box<ArtifactOp>,
310    },
311    /// A sequence of operations composed with `+`.
312    Sequence(Vec<ArtifactOp>),
313}
314
315impl ArtifactOp {
316    /// Returns the artifact name associated with this operation, if any.
317    pub fn name(&self) -> Option<&str> {
318        match self {
319            ArtifactOp::Publish { name, .. }
320            | ArtifactOp::Save { name }
321            | ArtifactOp::Load { name }
322            | ArtifactOp::Delete { name }
323            | ArtifactOp::Version { name, .. }
324            | ArtifactOp::AsJson { name }
325            | ArtifactOp::AsText { name }
326            | ArtifactOp::FromJson { name, .. }
327            | ArtifactOp::FromText { name, .. } => Some(name),
328            ArtifactOp::List => None,
329            ArtifactOp::When { inner, .. } => inner.name(),
330            ArtifactOp::Sequence(_) => None,
331        }
332    }
333
334    /// Returns true if this operation should execute (always true unless `When`).
335    pub fn should_execute(&self) -> bool {
336        match self {
337            ArtifactOp::When { predicate, .. } => predicate(),
338            _ => true,
339        }
340    }
341
342    /// Flatten this operation into a list of leaf operations.
343    pub fn flatten(&self) -> Vec<&ArtifactOp> {
344        match self {
345            ArtifactOp::Sequence(ops) => ops.iter().flat_map(|op| op.flatten()).collect(),
346            other => vec![other],
347        }
348    }
349}
350
351impl std::fmt::Debug for ArtifactOp {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        match self {
354            ArtifactOp::Publish { name, mime_type } => f
355                .debug_struct("Publish")
356                .field("name", name)
357                .field("mime_type", mime_type)
358                .finish(),
359            ArtifactOp::Save { name } => f.debug_struct("Save").field("name", name).finish(),
360            ArtifactOp::Load { name } => f.debug_struct("Load").field("name", name).finish(),
361            ArtifactOp::List => write!(f, "List"),
362            ArtifactOp::Delete { name } => f.debug_struct("Delete").field("name", name).finish(),
363            ArtifactOp::Version { name, version } => f
364                .debug_struct("Version")
365                .field("name", name)
366                .field("version", version)
367                .finish(),
368            ArtifactOp::AsJson { name } => f.debug_struct("AsJson").field("name", name).finish(),
369            ArtifactOp::AsText { name } => f.debug_struct("AsText").field("name", name).finish(),
370            ArtifactOp::FromJson { name, .. } => {
371                f.debug_struct("FromJson").field("name", name).finish()
372            }
373            ArtifactOp::FromText { name, .. } => {
374                f.debug_struct("FromText").field("name", name).finish()
375            }
376            ArtifactOp::When { inner, .. } => f.debug_struct("When").field("inner", inner).finish(),
377            ArtifactOp::Sequence(ops) => f.debug_struct("Sequence").field("ops", ops).finish(),
378        }
379    }
380}
381
382/// Compose two artifact operations with `+`.
383impl std::ops::Add for ArtifactOp {
384    type Output = ArtifactOp;
385
386    fn add(self, rhs: ArtifactOp) -> Self::Output {
387        match self {
388            ArtifactOp::Sequence(mut ops) => {
389                match rhs {
390                    ArtifactOp::Sequence(rhs_ops) => ops.extend(rhs_ops),
391                    other => ops.push(other),
392                }
393                ArtifactOp::Sequence(ops)
394            }
395            other => match rhs {
396                ArtifactOp::Sequence(mut rhs_ops) => {
397                    rhs_ops.insert(0, other);
398                    ArtifactOp::Sequence(rhs_ops)
399                }
400                rhs_other => ArtifactOp::Sequence(vec![other, rhs_other]),
401            },
402        }
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn artifact_schema() {
412        let schema = ArtifactSchema {
413            name: "report".into(),
414            mime_type: "application/json".into(),
415            description: "Analysis report".into(),
416        };
417        assert_eq!(schema.name, "report");
418    }
419
420    #[test]
421    fn artifact_transform_produces() {
422        let t = ArtifactTransform::produces(vec![ArtifactSchema {
423            name: "output".into(),
424            mime_type: "text/plain".into(),
425            description: "Result".into(),
426        }]);
427        assert_eq!(t.outputs.len(), 1);
428        assert!(t.inputs.is_empty());
429        assert_eq!(t.len(), 1);
430    }
431
432    #[test]
433    fn artifact_transform_consumes() {
434        let t = ArtifactTransform::consumes(vec![ArtifactSchema {
435            name: "input".into(),
436            mime_type: "text/plain".into(),
437            description: "Source".into(),
438        }]);
439        assert!(t.outputs.is_empty());
440        assert_eq!(t.inputs.len(), 1);
441    }
442
443    #[test]
444    fn a_json_output() {
445        let comp = A::json_output("report", "Analysis results");
446        assert_eq!(comp.len(), 1);
447        let outputs = comp.all_outputs();
448        assert_eq!(outputs.len(), 1);
449        assert_eq!(outputs[0].mime_type, "application/json");
450    }
451
452    #[test]
453    fn a_text_input() {
454        let comp = A::text_input("source", "Source document");
455        let inputs = comp.all_inputs();
456        assert_eq!(inputs.len(), 1);
457        assert_eq!(inputs[0].mime_type, "text/plain");
458    }
459
460    #[test]
461    fn compose_with_add() {
462        let comp = A::json_output("report", "Report")
463            + A::text_input("source", "Source")
464            + A::json_output("summary", "Summary");
465        assert_eq!(comp.len(), 3);
466        assert_eq!(comp.all_inputs().len(), 1);
467        assert_eq!(comp.all_outputs().len(), 2);
468    }
469
470    #[test]
471    fn empty_composite() {
472        let comp = ArtifactComposite { transforms: vec![] };
473        assert!(comp.is_empty());
474        assert_eq!(comp.len(), 0);
475    }
476
477    #[test]
478    fn publish_op() {
479        let op = A::publish("report", "application/json");
480        assert_eq!(op.name(), Some("report"));
481        assert!(op.should_execute());
482    }
483
484    #[test]
485    fn save_and_load_ops() {
486        let save = A::save("report");
487        let load = A::load("report");
488        assert_eq!(save.name(), Some("report"));
489        assert_eq!(load.name(), Some("report"));
490    }
491
492    #[test]
493    fn list_op() {
494        let op = A::list();
495        assert_eq!(op.name(), None);
496        assert!(op.should_execute());
497    }
498
499    #[test]
500    fn delete_op() {
501        let op = A::delete("old_report");
502        assert_eq!(op.name(), Some("old_report"));
503    }
504
505    #[test]
506    fn version_op() {
507        let op = A::version("report", 3);
508        assert_eq!(op.name(), Some("report"));
509        if let ArtifactOp::Version { version, .. } = &op {
510            assert_eq!(*version, 3);
511        } else {
512            panic!("Expected Version variant");
513        }
514    }
515
516    #[test]
517    fn as_json_and_as_text() {
518        let json_op = A::as_json("data");
519        let text_op = A::as_text("data");
520        assert_eq!(json_op.name(), Some("data"));
521        assert_eq!(text_op.name(), Some("data"));
522    }
523
524    #[test]
525    fn from_json_and_from_text() {
526        let json_op = A::from_json("config", r#"{"key": "value"}"#);
527        let text_op = A::from_text("note", "hello world");
528        assert_eq!(json_op.name(), Some("config"));
529        assert_eq!(text_op.name(), Some("note"));
530    }
531
532    #[test]
533    fn when_op_true() {
534        let op = A::when(|| true, A::save("report"));
535        assert!(op.should_execute());
536        assert_eq!(op.name(), Some("report"));
537    }
538
539    #[test]
540    fn when_op_false() {
541        let op = A::when(|| false, A::save("report"));
542        assert!(!op.should_execute());
543    }
544
545    #[test]
546    fn compose_ops_with_add() {
547        let pipeline = A::load("source") + A::as_json("source") + A::save("output");
548        let ops = pipeline.flatten();
549        assert_eq!(ops.len(), 3);
550    }
551
552    #[test]
553    fn op_debug_format() {
554        let op = A::publish("report", "application/json");
555        let debug = format!("{op:?}");
556        assert!(debug.contains("Publish"));
557        assert!(debug.contains("report"));
558    }
559}