use crate::agent::Agent;
use crate::error::Result;
use crate::harness::Harness;
use crate::session::{Session, SessionParticipant, SessionSeedMode};
use crate::typed_id::{AgentId, HarnessId, SessionId};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformMessage {
pub role: String,
pub content: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub struct PlatformCreateSessionRequest {
pub harness_id: HarnessId,
pub agent_id: Option<AgentId>,
pub title: Option<String>,
pub goal: Option<String>,
pub locale: Option<String>,
pub blueprint_id: Option<String>,
pub blueprint_config: Option<serde_json::Value>,
pub parent_session_id: Option<SessionId>,
pub forked_from_session_id: Option<SessionId>,
pub budget_root_session_id: Option<SessionId>,
pub seed: SessionSeedMode,
}
#[async_trait]
pub trait SubagentSessionDelegate: Send + Sync {
async fn get_agent_by_id(&self, id: AgentId) -> Result<Option<Agent>>;
async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>>;
async fn get_harness_chain(&self, id: HarnessId) -> Result<Vec<Harness>> {
let mut chain = Vec::new();
let mut current_id = Some(id);
let mut seen = HashSet::new();
while let Some(harness_id) = current_id {
if !seen.insert(harness_id) {
return Err(crate::error::AgentLoopError::tool(format!(
"Harness inheritance cycle detected at {harness_id}"
)));
}
let Some(harness) = self.get_harness(harness_id).await? else {
return Ok(Vec::new());
};
current_id = harness.parent_harness_id;
chain.push(harness);
}
chain.reverse();
Ok(chain)
}
async fn add_agent_session_participant(
&self,
session_id: SessionId,
agent_id: AgentId,
) -> Result<SessionParticipant>;
async fn create_session_with_options(
&self,
request: PlatformCreateSessionRequest,
) -> Result<Session>;
async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>>;
async fn send_message(&self, session_id: SessionId, content: &str) -> Result<()>;
async fn get_messages(
&self,
session_id: SessionId,
limit: Option<usize>,
) -> Result<Vec<PlatformMessage>>;
async fn wait_for_idle(
&self,
session_id: SessionId,
timeout_secs: Option<u64>,
) -> Result<String>;
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::AgentCapabilityConfig;
use crate::agent::AgentStatus;
use crate::harness::HarnessStatus;
use crate::session::{SessionParticipant, SessionStatus};
pub struct MockSubagentDelegate {
pub harness: Harness,
pub extra_harnesses: std::sync::Mutex<std::collections::HashMap<HarnessId, Harness>>,
pub agent: Agent,
pub session: Session,
pub extra_sessions: std::sync::Mutex<std::collections::HashMap<SessionId, Session>>,
pub joined_participants: std::sync::Mutex<Vec<SessionParticipant>>,
pub created_session_harness_ids: std::sync::Mutex<Vec<HarnessId>>,
pub created_session_budget_roots: std::sync::Mutex<Vec<Option<SessionId>>>,
pub wait_for_idle_status: std::sync::Mutex<String>,
pub sent_messages: std::sync::Mutex<Vec<(SessionId, String)>>,
}
impl Default for MockSubagentDelegate {
fn default() -> Self {
Self::new()
}
}
impl MockSubagentDelegate {
pub fn new() -> Self {
Self {
harness: Harness {
id: HarnessId::new(),
name: "test-harness".to_string(),
display_name: Some("Test Harness".to_string()),
description: Some("test harness".to_string()),
system_prompt: Some("You are helpful.".to_string()),
parent_harness_id: None,
default_model_id: None,
tags: vec![],
capabilities: vec![AgentCapabilityConfig::new("session")],
initial_files: vec![],
network_access: None,
parallel_tool_calls: None,
mcp_servers: Default::default(),
embedder_metadata: Default::default(),
is_built_in: false,
status: HarnessStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
archived_at: None,
deleted_at: None,
},
extra_harnesses: std::sync::Mutex::new(std::collections::HashMap::new()),
agent: Agent {
public_id: crate::typed_id::AgentId::new(),
internal_id: uuid::Uuid::now_v7(),
name: "test-agent".to_string(),
display_name: Some("Test Agent".to_string()),
description: Some("test agent".to_string()),
system_prompt: "You are helpful.".to_string(),
default_model_id: None,
harness_id: crate::typed_id::HarnessId::from_uuid(uuid::Uuid::nil()),
default_version_id: None,
forked_from_agent_id: None,
forked_from_version_id: None,
root_agent_id: None,
tags: vec![],
capabilities: vec![],
initial_files: vec![],
network_access: None,
max_iterations: None,
parallel_tool_calls: None,
tools: vec![],
mcp_servers: Default::default(),
status: AgentStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
archived_at: None,
deleted_at: None,
usage: None,
},
session: {
let session_id = SessionId::new();
Session {
id: session_id,
workspace_id: crate::WorkspaceId::from_uuid(session_id.uuid()),
organization_id: "org_00000000000000000000000000000001".to_string(),
harness_id: HarnessId::new(),
agent_id: None,
agent_version_id: None,
agent_identity_id: None,
owner_principal_id: crate::PrincipalId::from_seed(1),
resolved_owner_user_id: None,
owner: None,
effective_owner: None,
title: Some("Test Session".to_string()),
goal: None,
locale: None,
preview: None,
output_preview: None,
tags: vec![],
model_id: None,
capabilities: vec![],
tools: vec![],
mcp_servers: Default::default(),
system_prompt: None,
initial_files: vec![],
hints: None,
network_access: None,
max_iterations: None,
parallel_tool_calls: None,
status: SessionStatus::Idle,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
started_at: None,
finished_at: None,
usage: None,
is_pinned: None,
active_schedule_count: None,
features: vec![],
parent_session_id: None,
forked_from_session_id: None,
forked_from_sequence: None,
blueprint_id: None,
blueprint_config: None,
}
},
extra_sessions: std::sync::Mutex::new(std::collections::HashMap::new()),
joined_participants: std::sync::Mutex::new(Vec::new()),
created_session_harness_ids: std::sync::Mutex::new(Vec::new()),
created_session_budget_roots: std::sync::Mutex::new(Vec::new()),
wait_for_idle_status: std::sync::Mutex::new("idle".to_string()),
sent_messages: std::sync::Mutex::new(Vec::new()),
}
}
#[allow(clippy::too_many_arguments)]
async fn create_session(
&self,
hid: HarnessId,
aid: Option<crate::typed_id::AgentId>,
title: Option<&str>,
locale: Option<&str>,
blueprint_id: Option<&str>,
blueprint_config: Option<&serde_json::Value>,
parent_session_id: Option<SessionId>,
) -> Result<Session> {
if let Ok(mut recorder) = self.created_session_harness_ids.lock() {
recorder.push(hid);
}
let mut s = self.session.clone();
s.id = SessionId::new();
s.harness_id = hid;
s.agent_id = aid;
s.title = title.map(|t| t.to_string());
s.locale = locale.map(|value| value.to_string());
s.blueprint_id = blueprint_id.map(|b| b.to_string());
s.blueprint_config = blueprint_config.cloned();
s.parent_session_id = parent_session_id;
if let Ok(mut sessions) = self.extra_sessions.lock() {
sessions.insert(s.id, s.clone());
}
Ok(s)
}
}
#[async_trait]
impl SubagentSessionDelegate for MockSubagentDelegate {
async fn get_agent_by_id(&self, _id: crate::typed_id::AgentId) -> Result<Option<Agent>> {
Ok(Some(self.agent.clone()))
}
async fn add_agent_session_participant(
&self,
session_id: SessionId,
agent_id: AgentId,
) -> Result<SessionParticipant> {
let participant = SessionParticipant {
id: crate::typed_id::SessionParticipantId::new(),
session_id,
kind: crate::session::SessionParticipantKind::Agent,
agent_id: Some(agent_id),
agent_version_id: self.agent.default_version_id,
principal_id: self.session.owner_principal_id,
display_name: None,
role: crate::session::SessionParticipantRole::Member,
joined_at: chrono::Utc::now(),
left_at: None,
};
if let Ok(mut participants) = self.joined_participants.lock() {
participants.push(participant.clone());
}
Ok(participant)
}
async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>> {
if let Some(harness) = self.extra_harnesses.lock().unwrap().get(&id).cloned() {
return Ok(Some(harness));
}
Ok(Some(self.harness.clone()))
}
async fn create_session_with_options(
&self,
request: PlatformCreateSessionRequest,
) -> Result<Session> {
self.created_session_budget_roots
.lock()
.expect("budget root recorder")
.push(request.budget_root_session_id);
let mut session = self
.create_session(
request.harness_id,
request.agent_id,
request.title.as_deref(),
request.locale.as_deref(),
request.blueprint_id.as_deref(),
request.blueprint_config.as_ref(),
request.parent_session_id,
)
.await?;
session.goal = request.goal;
session.forked_from_session_id = request.forked_from_session_id;
if let Ok(mut sessions) = self.extra_sessions.lock() {
sessions.insert(session.id, session.clone());
}
Ok(session)
}
async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>> {
if id == self.session.id {
return Ok(Some(self.session.clone()));
}
if let Some(session) = self
.extra_sessions
.lock()
.ok()
.and_then(|sessions| sessions.get(&id).cloned())
{
return Ok(Some(session));
}
Ok(Some(self.session.clone()))
}
async fn send_message(&self, id: SessionId, content: &str) -> Result<()> {
self.sent_messages
.lock()
.unwrap()
.push((id, content.to_string()));
Ok(())
}
async fn get_messages(
&self,
_id: SessionId,
_limit: Option<usize>,
) -> Result<Vec<PlatformMessage>> {
Ok(vec![
PlatformMessage {
role: "user".into(),
content: "Hello".into(),
created_at: chrono::Utc::now(),
},
PlatformMessage {
role: "agent".into(),
content: "Hi!".into(),
created_at: chrono::Utc::now(),
},
])
}
async fn wait_for_idle(&self, _id: SessionId, _t: Option<u64>) -> Result<String> {
Ok(self.wait_for_idle_status.lock().unwrap().clone())
}
}
}