1use crate::agent::Agent;
14use crate::error::Result;
15use crate::harness::Harness;
16use crate::session::{Session, SessionParticipant, SessionSeedMode};
17use crate::typed_id::{AgentId, HarnessId, SessionId};
18use async_trait::async_trait;
19use chrono::{DateTime, Utc};
20use serde::{Deserialize, Serialize};
21use std::collections::HashSet;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct PlatformMessage {
26 pub role: String,
27 pub content: String,
28 pub created_at: DateTime<Utc>,
29}
30
31#[derive(Debug, Clone)]
33pub struct PlatformCreateSessionRequest {
34 pub harness_id: HarnessId,
35 pub agent_id: Option<AgentId>,
36 pub title: Option<String>,
37 pub goal: Option<String>,
38 pub locale: Option<String>,
39 pub blueprint_id: Option<String>,
40 pub blueprint_config: Option<serde_json::Value>,
41 pub parent_session_id: Option<SessionId>,
42 pub forked_from_session_id: Option<SessionId>,
43 pub budget_root_session_id: Option<SessionId>,
46 pub seed: SessionSeedMode,
47}
48
49#[async_trait]
53pub trait SubagentSessionDelegate: Send + Sync {
54 async fn get_agent_by_id(&self, id: AgentId) -> Result<Option<Agent>>;
56
57 async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>>;
59
60 async fn get_harness_chain(&self, id: HarnessId) -> Result<Vec<Harness>> {
63 let mut chain = Vec::new();
64 let mut current_id = Some(id);
65 let mut seen = HashSet::new();
66
67 while let Some(harness_id) = current_id {
68 if !seen.insert(harness_id) {
69 return Err(crate::error::AgentLoopError::tool(format!(
70 "Harness inheritance cycle detected at {harness_id}"
71 )));
72 }
73 let Some(harness) = self.get_harness(harness_id).await? else {
74 return Ok(Vec::new());
75 };
76 current_id = harness.parent_harness_id;
77 chain.push(harness);
78 }
79
80 chain.reverse();
81 Ok(chain)
82 }
83
84 async fn add_agent_session_participant(
86 &self,
87 session_id: SessionId,
88 agent_id: AgentId,
89 ) -> Result<SessionParticipant>;
90
91 async fn create_session_with_options(
93 &self,
94 request: PlatformCreateSessionRequest,
95 ) -> Result<Session>;
96
97 async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>>;
99
100 async fn send_message(&self, session_id: SessionId, content: &str) -> Result<()>;
102
103 async fn get_messages(
105 &self,
106 session_id: SessionId,
107 limit: Option<usize>,
108 ) -> Result<Vec<PlatformMessage>>;
109
110 async fn wait_for_idle(
112 &self,
113 session_id: SessionId,
114 timeout_secs: Option<u64>,
115 ) -> Result<String>;
116}
117
118#[cfg(test)]
119pub mod tests {
120 use super::*;
121 use crate::AgentCapabilityConfig;
122 use crate::agent::AgentStatus;
123 use crate::harness::HarnessStatus;
124 use crate::session::{SessionParticipant, SessionStatus};
125
126 pub struct MockSubagentDelegate {
132 pub harness: Harness,
133 pub extra_harnesses: std::sync::Mutex<std::collections::HashMap<HarnessId, Harness>>,
134 pub agent: Agent,
135 pub session: Session,
136 pub extra_sessions: std::sync::Mutex<std::collections::HashMap<SessionId, Session>>,
137 pub joined_participants: std::sync::Mutex<Vec<SessionParticipant>>,
138 pub created_session_harness_ids: std::sync::Mutex<Vec<HarnessId>>,
139 pub created_session_budget_roots: std::sync::Mutex<Vec<Option<SessionId>>>,
140 pub wait_for_idle_status: std::sync::Mutex<String>,
141 pub sent_messages: std::sync::Mutex<Vec<(SessionId, String)>>,
142 }
143
144 impl Default for MockSubagentDelegate {
145 fn default() -> Self {
146 Self::new()
147 }
148 }
149
150 impl MockSubagentDelegate {
151 pub fn new() -> Self {
152 Self {
153 harness: Harness {
154 id: HarnessId::new(),
155 name: "test-harness".to_string(),
156 display_name: Some("Test Harness".to_string()),
157 description: Some("test harness".to_string()),
158 system_prompt: Some("You are helpful.".to_string()),
159 parent_harness_id: None,
160 default_model_id: None,
161 tags: vec![],
162 capabilities: vec![AgentCapabilityConfig::new("session")],
163 initial_files: vec![],
164 network_access: None,
165 parallel_tool_calls: None,
166 mcp_servers: Default::default(),
167 embedder_metadata: Default::default(),
168 is_built_in: false,
169 status: HarnessStatus::Active,
170 created_at: chrono::Utc::now(),
171 updated_at: chrono::Utc::now(),
172 archived_at: None,
173 deleted_at: None,
174 },
175 extra_harnesses: std::sync::Mutex::new(std::collections::HashMap::new()),
176 agent: Agent {
177 public_id: crate::typed_id::AgentId::new(),
178 internal_id: uuid::Uuid::now_v7(),
179 name: "test-agent".to_string(),
180 display_name: Some("Test Agent".to_string()),
181 description: Some("test agent".to_string()),
182 system_prompt: "You are helpful.".to_string(),
183 default_model_id: None,
184 harness_id: crate::typed_id::HarnessId::from_uuid(uuid::Uuid::nil()),
185 default_version_id: None,
186 forked_from_agent_id: None,
187 forked_from_version_id: None,
188 root_agent_id: None,
189 tags: vec![],
190 capabilities: vec![],
191 initial_files: vec![],
192 network_access: None,
193 max_iterations: None,
194 parallel_tool_calls: None,
195 tools: vec![],
196 mcp_servers: Default::default(),
197 status: AgentStatus::Active,
198 created_at: chrono::Utc::now(),
199 updated_at: chrono::Utc::now(),
200 archived_at: None,
201 deleted_at: None,
202 usage: None,
203 },
204 session: {
205 let session_id = SessionId::new();
206 Session {
207 id: session_id,
208 workspace_id: crate::WorkspaceId::from_uuid(session_id.uuid()),
209 organization_id: "org_00000000000000000000000000000001".to_string(),
210 harness_id: HarnessId::new(),
211 agent_id: None,
212 agent_version_id: None,
213 agent_identity_id: None,
214 owner_principal_id: crate::PrincipalId::from_seed(1),
215 resolved_owner_user_id: None,
216 owner: None,
217 effective_owner: None,
218 title: Some("Test Session".to_string()),
219 goal: None,
220 locale: None,
221 preview: None,
222 output_preview: None,
223 tags: vec![],
224 model_id: None,
225 capabilities: vec![],
226 tools: vec![],
227 mcp_servers: Default::default(),
228 system_prompt: None,
229 initial_files: vec![],
230 hints: None,
231 network_access: None,
232 max_iterations: None,
233 parallel_tool_calls: None,
234 status: SessionStatus::Idle,
235 created_at: chrono::Utc::now(),
236 updated_at: chrono::Utc::now(),
237 started_at: None,
238 finished_at: None,
239 usage: None,
240 is_pinned: None,
241 active_schedule_count: None,
242 features: vec![],
243 parent_session_id: None,
244 forked_from_session_id: None,
245 forked_from_sequence: None,
246 blueprint_id: None,
247 blueprint_config: None,
248 }
249 },
250 extra_sessions: std::sync::Mutex::new(std::collections::HashMap::new()),
251 joined_participants: std::sync::Mutex::new(Vec::new()),
252 created_session_harness_ids: std::sync::Mutex::new(Vec::new()),
253 created_session_budget_roots: std::sync::Mutex::new(Vec::new()),
254 wait_for_idle_status: std::sync::Mutex::new("idle".to_string()),
255 sent_messages: std::sync::Mutex::new(Vec::new()),
256 }
257 }
258
259 #[allow(clippy::too_many_arguments)]
260 async fn create_session(
261 &self,
262 hid: HarnessId,
263 aid: Option<crate::typed_id::AgentId>,
264 title: Option<&str>,
265 locale: Option<&str>,
266 blueprint_id: Option<&str>,
267 blueprint_config: Option<&serde_json::Value>,
268 parent_session_id: Option<SessionId>,
269 ) -> Result<Session> {
270 if let Ok(mut recorder) = self.created_session_harness_ids.lock() {
271 recorder.push(hid);
272 }
273 let mut s = self.session.clone();
274 s.id = SessionId::new();
275 s.harness_id = hid;
276 s.agent_id = aid;
277 s.title = title.map(|t| t.to_string());
278 s.locale = locale.map(|value| value.to_string());
279 s.blueprint_id = blueprint_id.map(|b| b.to_string());
280 s.blueprint_config = blueprint_config.cloned();
281 s.parent_session_id = parent_session_id;
282 if let Ok(mut sessions) = self.extra_sessions.lock() {
283 sessions.insert(s.id, s.clone());
284 }
285 Ok(s)
286 }
287 }
288
289 #[async_trait]
290 impl SubagentSessionDelegate for MockSubagentDelegate {
291 async fn get_agent_by_id(&self, _id: crate::typed_id::AgentId) -> Result<Option<Agent>> {
292 Ok(Some(self.agent.clone()))
293 }
294
295 async fn add_agent_session_participant(
296 &self,
297 session_id: SessionId,
298 agent_id: AgentId,
299 ) -> Result<SessionParticipant> {
300 let participant = SessionParticipant {
301 id: crate::typed_id::SessionParticipantId::new(),
302 session_id,
303 kind: crate::session::SessionParticipantKind::Agent,
304 agent_id: Some(agent_id),
305 agent_version_id: self.agent.default_version_id,
306 principal_id: self.session.owner_principal_id,
307 display_name: None,
308 role: crate::session::SessionParticipantRole::Member,
309 joined_at: chrono::Utc::now(),
310 left_at: None,
311 };
312 if let Ok(mut participants) = self.joined_participants.lock() {
313 participants.push(participant.clone());
314 }
315 Ok(participant)
316 }
317
318 async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>> {
319 if let Some(harness) = self.extra_harnesses.lock().unwrap().get(&id).cloned() {
320 return Ok(Some(harness));
321 }
322 Ok(Some(self.harness.clone()))
323 }
324
325 async fn create_session_with_options(
326 &self,
327 request: PlatformCreateSessionRequest,
328 ) -> Result<Session> {
329 self.created_session_budget_roots
330 .lock()
331 .expect("budget root recorder")
332 .push(request.budget_root_session_id);
333 let mut session = self
334 .create_session(
335 request.harness_id,
336 request.agent_id,
337 request.title.as_deref(),
338 request.locale.as_deref(),
339 request.blueprint_id.as_deref(),
340 request.blueprint_config.as_ref(),
341 request.parent_session_id,
342 )
343 .await?;
344 session.goal = request.goal;
345 session.forked_from_session_id = request.forked_from_session_id;
346 if let Ok(mut sessions) = self.extra_sessions.lock() {
347 sessions.insert(session.id, session.clone());
348 }
349 Ok(session)
350 }
351
352 async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>> {
353 if id == self.session.id {
354 return Ok(Some(self.session.clone()));
355 }
356 if let Some(session) = self
357 .extra_sessions
358 .lock()
359 .ok()
360 .and_then(|sessions| sessions.get(&id).cloned())
361 {
362 return Ok(Some(session));
363 }
364 Ok(Some(self.session.clone()))
365 }
366
367 async fn send_message(&self, id: SessionId, content: &str) -> Result<()> {
368 self.sent_messages
369 .lock()
370 .unwrap()
371 .push((id, content.to_string()));
372 Ok(())
373 }
374
375 async fn get_messages(
376 &self,
377 _id: SessionId,
378 _limit: Option<usize>,
379 ) -> Result<Vec<PlatformMessage>> {
380 Ok(vec![
381 PlatformMessage {
382 role: "user".into(),
383 content: "Hello".into(),
384 created_at: chrono::Utc::now(),
385 },
386 PlatformMessage {
387 role: "agent".into(),
388 content: "Hi!".into(),
389 created_at: chrono::Utc::now(),
390 },
391 ])
392 }
393
394 async fn wait_for_idle(&self, _id: SessionId, _t: Option<u64>) -> Result<String> {
395 Ok(self.wait_for_idle_status.lock().unwrap().clone())
396 }
397 }
398}