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}