1use crate::agent_definition::AgentDefinition;
13use crate::error::Result;
14use crate::harness_definition::HarnessDefinition;
15use crate::session::{ExecutionSession, SessionSeedMode};
16use crate::typed_id::{AgentId, HarnessId, SessionId, SessionParticipantId};
17use async_trait::async_trait;
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct PlatformMessage {
24 pub role: String,
25 pub content: String,
26 pub created_at: DateTime<Utc>,
27}
28
29#[derive(Debug, Clone)]
31pub struct PlatformCreateSessionRequest {
32 pub harness_id: HarnessId,
33 pub agent_id: Option<AgentId>,
34 pub title: Option<String>,
35 pub goal: Option<String>,
36 pub locale: Option<String>,
37 pub blueprint_id: Option<String>,
38 pub blueprint_config: Option<serde_json::Value>,
39 pub parent_session_id: Option<SessionId>,
40 pub forked_from_session_id: Option<SessionId>,
41 pub budget_root_session_id: Option<SessionId>,
44 pub seed: SessionSeedMode,
45}
46
47#[async_trait]
51pub trait SubagentSessionDelegate: Send + Sync {
52 async fn get_agent_by_id(&self, id: AgentId) -> Result<Option<AgentDefinition>>;
56
57 async fn get_harness(&self, id: HarnessId) -> Result<Option<HarnessDefinition>>;
62
63 async fn add_agent_session_participant(
68 &self,
69 session_id: SessionId,
70 agent_id: AgentId,
71 ) -> Result<SessionParticipantId>;
72
73 async fn create_session_with_options(
76 &self,
77 request: PlatformCreateSessionRequest,
78 ) -> Result<ExecutionSession>;
79
80 async fn get_session_by_id(&self, id: SessionId) -> Result<Option<ExecutionSession>>;
82
83 async fn send_message(&self, session_id: SessionId, content: &str) -> Result<()>;
85
86 async fn get_messages(
88 &self,
89 session_id: SessionId,
90 limit: Option<usize>,
91 ) -> Result<Vec<PlatformMessage>>;
92
93 async fn wait_for_idle(
95 &self,
96 session_id: SessionId,
97 timeout_secs: Option<u64>,
98 ) -> Result<String>;
99}
100
101#[cfg(test)]
102pub mod tests {
103 use super::*;
104 use crate::AgentCapabilityConfig;
105 use crate::session::SessionExecutionState;
106
107 pub struct MockSubagentDelegate {
113 pub harness: HarnessDefinition,
114 pub extra_harnesses:
115 std::sync::Mutex<std::collections::HashMap<HarnessId, HarnessDefinition>>,
116 pub agent: AgentDefinition,
117 pub session: ExecutionSession,
118 pub extra_sessions:
119 std::sync::Mutex<std::collections::HashMap<SessionId, ExecutionSession>>,
120 pub joined_participants: std::sync::Mutex<Vec<(SessionId, Option<AgentId>)>>,
121 pub created_session_harness_ids: std::sync::Mutex<Vec<HarnessId>>,
122 pub created_session_budget_roots: std::sync::Mutex<Vec<Option<SessionId>>>,
123 pub wait_for_idle_status: std::sync::Mutex<String>,
124 pub sent_messages: std::sync::Mutex<Vec<(SessionId, String)>>,
125 }
126
127 impl Default for MockSubagentDelegate {
128 fn default() -> Self {
129 Self::new()
130 }
131 }
132
133 impl MockSubagentDelegate {
134 pub fn new() -> Self {
135 Self {
136 harness: HarnessDefinition {
137 capabilities: vec![AgentCapabilityConfig::new("session")],
138 ..HarnessDefinition::new("test-harness", "You are helpful.")
139 },
140 extra_harnesses: std::sync::Mutex::new(std::collections::HashMap::new()),
141 agent: AgentDefinition {
142 display_name: Some("Test Agent".to_string()),
143 description: Some("test agent".to_string()),
144 ..AgentDefinition::new(
145 crate::typed_id::AgentId::new(),
146 "test-agent",
147 "You are helpful.",
148 )
149 },
150 session: ExecutionSession {
151 title: Some("Test Session".to_string()),
152 status: SessionExecutionState::Idle,
153 ..ExecutionSession::with_own_workspace(SessionId::new(), HarnessId::new())
154 },
155 extra_sessions: std::sync::Mutex::new(std::collections::HashMap::new()),
156 joined_participants: std::sync::Mutex::new(Vec::new()),
157 created_session_harness_ids: std::sync::Mutex::new(Vec::new()),
158 created_session_budget_roots: std::sync::Mutex::new(Vec::new()),
159 wait_for_idle_status: std::sync::Mutex::new("idle".to_string()),
160 sent_messages: std::sync::Mutex::new(Vec::new()),
161 }
162 }
163
164 #[allow(clippy::too_many_arguments)]
165 async fn create_session(
166 &self,
167 hid: HarnessId,
168 aid: Option<crate::typed_id::AgentId>,
169 title: Option<&str>,
170 locale: Option<&str>,
171 blueprint_id: Option<&str>,
172 blueprint_config: Option<&serde_json::Value>,
173 parent_session_id: Option<SessionId>,
174 ) -> Result<ExecutionSession> {
175 if let Ok(mut recorder) = self.created_session_harness_ids.lock() {
176 recorder.push(hid);
177 }
178 let mut s = self.session.clone();
179 s.id = SessionId::new();
180 s.harness_id = hid;
181 s.agent_id = aid;
182 s.title = title.map(|t| t.to_string());
183 s.locale = locale.map(|value| value.to_string());
184 s.blueprint_id = blueprint_id.map(|b| b.to_string());
185 s.blueprint_config = blueprint_config.cloned();
186 s.parent_session_id = parent_session_id;
187 if let Ok(mut sessions) = self.extra_sessions.lock() {
188 sessions.insert(s.id, s.clone());
189 }
190 Ok(s)
191 }
192 }
193
194 #[async_trait]
195 impl SubagentSessionDelegate for MockSubagentDelegate {
196 async fn get_agent_by_id(
197 &self,
198 _id: crate::typed_id::AgentId,
199 ) -> Result<Option<AgentDefinition>> {
200 Ok(Some(self.agent.clone()))
201 }
202
203 async fn add_agent_session_participant(
204 &self,
205 session_id: SessionId,
206 agent_id: AgentId,
207 ) -> Result<SessionParticipantId> {
208 if let Ok(mut participants) = self.joined_participants.lock() {
209 participants.push((session_id, Some(agent_id)));
210 }
211 Ok(SessionParticipantId::new())
212 }
213
214 async fn get_harness(&self, id: HarnessId) -> Result<Option<HarnessDefinition>> {
215 if let Some(harness) = self.extra_harnesses.lock().unwrap().get(&id).cloned() {
216 return Ok(Some(harness));
217 }
218 Ok(Some(self.harness.clone()))
219 }
220
221 async fn create_session_with_options(
222 &self,
223 request: PlatformCreateSessionRequest,
224 ) -> Result<ExecutionSession> {
225 self.created_session_budget_roots
226 .lock()
227 .expect("budget root recorder")
228 .push(request.budget_root_session_id);
229 let mut session = self
230 .create_session(
231 request.harness_id,
232 request.agent_id,
233 request.title.as_deref(),
234 request.locale.as_deref(),
235 request.blueprint_id.as_deref(),
236 request.blueprint_config.as_ref(),
237 request.parent_session_id,
238 )
239 .await?;
240 session.goal = request.goal;
241 session.forked_from_session_id = request.forked_from_session_id;
242 if let Ok(mut sessions) = self.extra_sessions.lock() {
243 sessions.insert(session.id, session.clone());
244 }
245 Ok(session)
246 }
247
248 async fn get_session_by_id(&self, id: SessionId) -> Result<Option<ExecutionSession>> {
249 if id == self.session.id {
250 return Ok(Some(self.session.clone()));
251 }
252 if let Some(session) = self
253 .extra_sessions
254 .lock()
255 .ok()
256 .and_then(|sessions| sessions.get(&id).cloned())
257 {
258 return Ok(Some(session));
259 }
260 Ok(Some(self.session.clone()))
261 }
262
263 async fn send_message(&self, id: SessionId, content: &str) -> Result<()> {
264 self.sent_messages
265 .lock()
266 .unwrap()
267 .push((id, content.to_string()));
268 Ok(())
269 }
270
271 async fn get_messages(
272 &self,
273 _id: SessionId,
274 _limit: Option<usize>,
275 ) -> Result<Vec<PlatformMessage>> {
276 Ok(vec![
277 PlatformMessage {
278 role: "user".into(),
279 content: "Hello".into(),
280 created_at: chrono::Utc::now(),
281 },
282 PlatformMessage {
283 role: "agent".into(),
284 content: "Hi!".into(),
285 created_at: chrono::Utc::now(),
286 },
287 ])
288 }
289
290 async fn wait_for_idle(&self, _id: SessionId, _t: Option<u64>) -> Result<String> {
291 Ok(self.wait_for_idle_status.lock().unwrap().clone())
292 }
293 }
294}