Skip to main content

objectiveai_sdk/agent/mock/
agent.rs

1//! Mock Agent types and validation logic.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use twox_hash::XxHash3_128;
6
7/// The base configuration for a Mock Agent (without computed ID).
8#[derive(
9    Clone,
10    Debug,
11    Default,
12    PartialEq,
13    Serialize,
14    Deserialize,
15    JsonSchema,
16    arbitrary::Arbitrary,
17)]
18#[schemars(rename = "agent.mock.AgentBase")]
19pub struct AgentBase {
20    /// The upstream provider marker.
21    pub upstream: super::Upstream,
22
23    /// The output mode for vector completions. Ignored for agent completions.
24    pub output_mode: super::OutputMode,
25
26    /// Number of top log probabilities to return (2-20).
27    ///
28    /// **Vector completions only.** Ignored for agent completions.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    #[schemars(extend("omitempty" = true))]
31    #[arbitrary(with = crate::arbitrary_util::arbitrary_option_u64)]
32    pub top_logprobs: Option<u64>,
33
34    /// If true, the mock client will return an error instead of a response.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    #[schemars(extend("omitempty" = true))]
37    pub error: Option<bool>,
38
39    /// Probability (0-100) that the mock returns an error mid-stream.
40    /// Requires `error` to be `Some(true)`.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    #[schemars(extend("omitempty" = true))]
43    pub error_probability: Option<u8>,
44
45    /// MCP servers the agent can connect to.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    #[schemars(extend("omitempty" = true))]
48    pub mcp_servers: Option<super::super::McpServers>,
49
50    /// Laboratories provisioned for the agent — each becomes a
51    /// client-side laboratory MCP server whose id DERIVES from the
52    /// agent's full id plus the spec (see
53    /// [`laboratories::derived_id`](super::super::laboratory::laboratories::derived_id)).
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[schemars(extend("omitempty" = true))]
56    pub laboratories: Option<super::super::Laboratories>,
57
58    /// Client-side ObjectiveAI MCP surface the calling client is
59    /// expected to expose locally back to the API (objectiveai
60    /// built-in, plus specific plugins / tools by owner+name+version).
61    #[serde(skip_serializing_if = "Option::is_none")]
62    #[schemars(extend("omitempty" = true))]
63    pub client_objectiveai_mcp: Option<super::super::ClientObjectiveaiMcp>,
64
65    /// Deterministic-script override. When `Some`, the mock agent
66    /// emits each [`super::Call`] as its own assistant turn —
67    /// `tool_calls` first, then `content` — in array order. Each
68    /// subsequent turn inspects the continuation to count how many
69    /// `Call`s have already been satisfied (assistant message with
70    /// exactly that `Call`'s `tool_calls` (by name+arguments) and
71    /// `content`); the next un-matched `Call` is what that turn
72    /// emits. Once every `Call` has been satisfied in the
73    /// continuation, the mock falls through to its normal
74    /// dispatcher. Pure addition — agents without `calls` are
75    /// unaffected.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    #[schemars(extend("omitempty" = true))]
78    pub calls: Option<Vec<super::Call>>,
79}
80
81impl AgentBase {
82    /// Normalizes the configuration for deterministic ID computation.
83    pub fn prepare(&mut self) {
84        self.top_logprobs = match self.top_logprobs {
85            Some(0) | Some(1) => None,
86            other => other,
87        };
88        if self.error == Some(true) && self.error_probability == Some(0) {
89            self.error = None;
90            self.error_probability = None;
91        }
92        if self.error == Some(false) {
93            self.error = None;
94        }
95        self.mcp_servers = match self.mcp_servers.take() {
96            Some(mcp_servers) => {
97                super::super::mcp::mcp_servers::prepare(mcp_servers)
98            }
99            None => None,
100        };
101        self.laboratories = match self.laboratories.take() {
102            Some(laboratories) => {
103                super::super::laboratory::laboratories::prepare(laboratories)
104            }
105            None => None,
106        };
107        self.client_objectiveai_mcp = match self.client_objectiveai_mcp.take() {
108            Some(cm) => super::super::client_objectiveai_mcp::prepare(cm),
109            None => None,
110        };
111    }
112
113    /// Validates the configuration.
114    pub fn validate(&self) -> Result<(), String> {
115        if let Some(top_logprobs) = self.top_logprobs
116            && top_logprobs > 20
117        {
118            return Err("`top_logprobs` must be at most 20".to_string());
119        }
120        if let Some(mcp_servers) = &self.mcp_servers {
121            super::super::mcp::mcp_servers::validate(mcp_servers)?;
122        }
123        if let Some(laboratories) = &self.laboratories {
124            super::super::laboratory::laboratories::validate(laboratories)?;
125        }
126        if let Some(cm) = &self.client_objectiveai_mcp {
127            super::super::client_objectiveai_mcp::validate(cm)?;
128        }
129        if let Some(p) = self.error_probability {
130            if p > 100 {
131                return Err(
132                    "`error_probability` must be at most 100".to_string()
133                );
134            }
135            if self.error != Some(true) {
136                return Err("`error_probability` requires `error` to be true"
137                    .to_string());
138            }
139        }
140        Ok(())
141    }
142
143    /// Returns the messages as-is.
144    pub fn merged_messages(
145        &self,
146        messages: Vec<super::super::completions::message::Message>,
147    ) -> Vec<super::super::completions::message::Message> {
148        messages
149    }
150
151    /// Computes the deterministic content-addressed ID.
152    pub fn id(&self) -> String {
153        let mut hasher = XxHash3_128::with_seed(0);
154        hasher.write(serde_json::to_string(self).unwrap().as_bytes());
155        format!("{:0>22}", base62::encode(hasher.finish_128()))
156    }
157
158    pub const fn model() -> &'static str {
159        "mock"
160    }
161}
162
163/// A validated Mock Agent with its computed content-addressed ID.
164#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
165#[schemars(rename = "agent.mock.Agent")]
166pub struct Agent {
167    /// The deterministic content-addressed ID (22-character base62 string).
168    pub id: String,
169    /// The normalized configuration.
170    #[serde(flatten)]
171    pub base: AgentBase,
172}
173
174impl TryFrom<AgentBase> for Agent {
175    type Error = String;
176    fn try_from(mut base: AgentBase) -> Result<Self, Self::Error> {
177        base.prepare();
178        base.validate()?;
179        let id = base.id();
180        Ok(Agent { id, base })
181    }
182}