Skip to main content

objectiveai_sdk/agent/
laboratory.rs

1//! Laboratories embedded in agent definitions.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// The reserved id namespace for agent laboratories. A laboratory
7/// whose id starts with this prefix is DERIVED from an agent (see
8/// [`laboratories::derived_id`]) — never user-created, never
9/// attachable or detachable.
10pub const AGENT_LABORATORY_ID_PREFIX: &str = "oai-agent-";
11
12/// A laboratory provisioned for an agent: the container spec the CLI
13/// conduit materializes on demand at MCP-initialize. No mounts —
14/// agent laboratories don't support them.
15#[derive(
16    Debug,
17    Clone,
18    PartialEq,
19    Eq,
20    PartialOrd,
21    Ord,
22    Hash,
23    Serialize,
24    Deserialize,
25    JsonSchema,
26    arbitrary::Arbitrary,
27)]
28#[schemars(rename = "agent.Laboratory")]
29pub struct Laboratory {
30    /// The base image — an inline Containerfile XOR a split registry
31    /// reference.
32    pub image: crate::laboratories::LaboratoryImage,
33    /// `[key, value]` environment pairs. Sorted by prepare; empty
34    /// collapses to `None`.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    #[schemars(extend("omitempty" = true))]
37    pub env: Option<Vec<[String; 2]>>,
38    /// Working directory new agents start in. `"/"` (the create-time
39    /// default) collapses to `None` in prepare.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    #[schemars(extend("omitempty" = true))]
42    pub cwd: Option<String>,
43}
44
45/// A list of agent laboratories.
46pub type Laboratories = Vec<Laboratory>;
47
48pub mod laboratories {
49    //! Functions for working with [`Laboratories`](super::Laboratories).
50
51    use twox_hash::XxHash3_128;
52
53    /// Normalizes for the content-addressed agent id: per entry, an
54    /// empty `env` collapses to `None` (else sorts), a `cwd` of `"/"`
55    /// (the create-time default) collapses to `None`; the entries then
56    /// sort as a whole; an empty list collapses to `None` — identical
57    /// specs always serialize to identical bytes.
58    pub fn prepare(
59        mut this: super::Laboratories,
60    ) -> Option<super::Laboratories> {
61        if this.is_empty() {
62            return None;
63        }
64        for laboratory in &mut this {
65            laboratory.env = match laboratory.env.take() {
66                Some(mut env) if !env.is_empty() => {
67                    env.sort();
68                    Some(env)
69                }
70                _ => None,
71            };
72            if laboratory.cwd.as_deref() == Some("/") {
73                laboratory.cwd = None;
74            }
75        }
76        this.sort();
77        Some(this)
78    }
79
80    /// Validates all laboratories in the list: each image must be
81    /// valid, and no duplicate entries may exist (duplicates would
82    /// derive the same laboratory id).
83    pub fn validate(this: &super::Laboratories) -> Result<(), String> {
84        for laboratory in this {
85            laboratory
86                .image
87                .validate()
88                .map_err(|message| format!("`laboratories` image: {message}"))?;
89        }
90        for (i, a) in this.iter().enumerate() {
91            for b in &this[i + 1..] {
92                if a == b {
93                    return Err(
94                        "`laboratories` contains duplicate entries".to_string()
95                    );
96                }
97            }
98        }
99        Ok(())
100    }
101
102    /// The laboratory's DERIVED id: the reserved
103    /// [`AGENT_LABORATORY_ID_PREFIX`](super::AGENT_LABORATORY_ID_PREFIX)
104    /// plus a 22-character base62 XxHash3_128 over the agent's full id,
105    /// a `0x00` separator, and the prepared spec's JSON — the same spec
106    /// on the same agent always lands on the same laboratory.
107    pub fn derived_id(
108        agent_full_id: &str,
109        laboratory: &super::Laboratory,
110    ) -> String {
111        let mut hasher = XxHash3_128::with_seed(0);
112        hasher.write(agent_full_id.as_bytes());
113        hasher.write(&[0u8]);
114        hasher.write(serde_json::to_string(laboratory).unwrap().as_bytes());
115        format!(
116            "{}{:0>22}",
117            super::AGENT_LABORATORY_ID_PREFIX,
118            base62::encode(hasher.finish_128())
119        )
120    }
121}