use crate::SessionContextReport;
use crate::agent::Agent;
use crate::app::{App, AppChannel, ChannelType};
use crate::capability_dto::CapabilityInfo;
use crate::error::Result;
use crate::harness::Harness;
use crate::session::Session;
use crate::typed_id::{AgentId, AgentIdentityId, AppChannelId, AppId, HarnessId, SessionId};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformMessage {
pub role: String,
pub content: String,
pub created_at: DateTime<Utc>,
}
#[async_trait]
pub trait PlatformStore: Send + Sync {
async fn list_harnesses(&self) -> Result<Vec<Harness>>;
async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>>;
async fn create_harness(
&self,
name: &str,
display_name: Option<&str>,
description: Option<&str>,
system_prompt: &str,
parent_harness_id: Option<HarnessId>,
capabilities: &[String],
) -> Result<Harness>;
async fn update_harness(
&self,
id: HarnessId,
name: Option<&str>,
display_name: Option<&str>,
description: Option<&str>,
system_prompt: Option<&str>,
parent_harness_id: Option<Option<HarnessId>>,
) -> Result<Harness>;
async fn delete_harness(&self, id: HarnessId) -> Result<()>;
async fn copy_harness(&self, id: HarnessId, new_name: Option<&str>) -> Result<Harness>;
async fn list_agents(&self) -> Result<Vec<Agent>>;
async fn get_agent_by_id(&self, id: AgentId) -> Result<Option<Agent>>;
async fn create_agent(
&self,
name: &str,
display_name: Option<&str>,
description: Option<&str>,
system_prompt: &str,
capabilities: &[String],
) -> Result<Agent>;
async fn update_agent(
&self,
id: AgentId,
name: Option<&str>,
display_name: Option<&str>,
description: Option<&str>,
system_prompt: Option<&str>,
) -> Result<Agent>;
async fn delete_agent(&self, id: AgentId) -> Result<()>;
async fn list_apps(&self, search: Option<&str>, include_archived: bool) -> Result<Vec<App>>;
async fn get_app(&self, id: AppId) -> Result<Option<App>>;
#[allow(clippy::too_many_arguments)]
async fn create_app(
&self,
name: &str,
description: Option<&str>,
harness_id: HarnessId,
agent_id: Option<AgentId>,
agent_identity_id: Option<AgentIdentityId>,
channel_type: Option<ChannelType>,
channel_config: Option<&serde_json::Value>,
) -> Result<App>;
#[allow(clippy::too_many_arguments)]
async fn update_app(
&self,
id: AppId,
name: Option<&str>,
description: Option<&str>,
harness_id: Option<HarnessId>,
agent_id: Option<AgentId>,
agent_identity_id: Option<Option<AgentIdentityId>>,
) -> Result<App>;
async fn delete_app(&self, id: AppId) -> Result<()>;
async fn destroy_app(&self, id: AppId) -> Result<()>;
async fn publish_app(&self, id: AppId) -> Result<App>;
async fn unpublish_app(&self, id: AppId) -> Result<App>;
async fn add_app_channel(
&self,
app_id: AppId,
channel_type: ChannelType,
channel_config: Option<&serde_json::Value>,
enabled: Option<bool>,
) -> Result<AppChannel>;
async fn update_app_channel(
&self,
app_id: AppId,
channel_id: AppChannelId,
channel_type: Option<ChannelType>,
channel_config: Option<&serde_json::Value>,
enabled: Option<bool>,
) -> Result<AppChannel>;
async fn delete_app_channel(&self, app_id: AppId, channel_id: AppChannelId) -> Result<()>;
async fn list_sessions(
&self,
limit: Option<usize>,
agent_id: Option<AgentId>,
) -> Result<Vec<Session>>;
#[allow(clippy::too_many_arguments)]
async fn create_session(
&self,
harness_id: HarnessId,
agent_id: Option<AgentId>,
title: Option<&str>,
locale: Option<&str>,
blueprint_id: Option<&str>,
blueprint_config: Option<&serde_json::Value>,
) -> Result<Session>;
async fn set_subagent_metadata(
&self,
session_id: SessionId,
parent_session_id: SessionId,
subagent_name: &str,
subagent_task: &str,
subagent_status: crate::session::SubagentStatus,
) -> Result<Session>;
async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>>;
async fn get_session_context_report(&self, id: SessionId) -> Result<SessionContextReport>;
async fn delete_session(&self, id: SessionId) -> Result<()>;
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>;
async fn list_capabilities(&self, search: Option<&str>) -> Result<Vec<CapabilityInfo>>;
fn base_url(&self) -> &str;
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::AgentCapabilityConfig;
use crate::agent::{Agent, AgentStatus};
use crate::app::{App, AppChannel, AppStatus, ChannelType};
use crate::harness::{Harness, HarnessStatus};
use crate::session::{Session, SessionStatus};
pub struct MockPlatformStore {
pub harness: Harness,
pub agent: Agent,
pub app: App,
pub app_channel: AppChannel,
pub session: Session,
pub created_session_harness_ids: std::sync::Mutex<Vec<HarnessId>>,
}
impl Default for MockPlatformStore {
fn default() -> Self {
Self::new()
}
}
impl MockPlatformStore {
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: "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,
mcp_servers: 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,
},
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,
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,
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,
},
app_channel: AppChannel {
public_id: AppChannelId::new(),
internal_id: uuid::Uuid::now_v7(),
channel_type: ChannelType::Webhook,
channel_config: serde_json::json!({
"token": "secret-1",
"session_mode": "shared_session",
"message": "Run checks for {{payload.repo.name}}"
}),
enabled: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
app: App {
public_id: AppId::new(),
internal_id: uuid::Uuid::now_v7(),
org_id: 1,
name: "test-app".to_string(),
description: Some("test app".to_string()),
harness_id: HarnessId::new(),
agent_id: Some(crate::typed_id::AgentId::new()),
agent_version_policy: crate::app::AgentVersionPolicy::Default,
agent_version_id: None,
agent_identity_id: Some(crate::typed_id::AgentIdentityId::new()),
owner_principal_id: crate::PrincipalId::from_seed(1),
resolved_owner_user_id: None,
owner: None,
effective_owner: None,
channels: vec![],
status: AppStatus::Draft,
published_at: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
archived_at: None,
deleted_at: None,
},
session: Session {
id: SessionId::new(),
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()),
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,
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,
subagent_name: None,
subagent_task: None,
subagent_status: None,
blueprint_id: None,
blueprint_config: None,
},
created_session_harness_ids: std::sync::Mutex::new(Vec::new()),
}
}
}
#[async_trait]
impl PlatformStore for MockPlatformStore {
async fn list_harnesses(&self) -> Result<Vec<Harness>> {
Ok(vec![self.harness.clone()])
}
async fn get_harness(&self, _id: HarnessId) -> Result<Option<Harness>> {
Ok(Some(self.harness.clone()))
}
async fn create_harness(
&self,
name: &str,
display_name: Option<&str>,
_desc: Option<&str>,
_prompt: &str,
parent_harness_id: Option<HarnessId>,
_caps: &[String],
) -> Result<Harness> {
let mut h = self.harness.clone();
h.name = name.to_string();
h.display_name = display_name.map(|s| s.to_string());
h.parent_harness_id = parent_harness_id;
Ok(h)
}
async fn update_harness(
&self,
_id: HarnessId,
name: Option<&str>,
display_name: Option<&str>,
_desc: Option<&str>,
_prompt: Option<&str>,
parent_harness_id: Option<Option<HarnessId>>,
) -> Result<Harness> {
let mut h = self.harness.clone();
if let Some(n) = name {
h.name = n.to_string();
}
if let Some(dn) = display_name {
h.display_name = Some(dn.to_string());
}
if let Some(parent_harness_id) = parent_harness_id {
h.parent_harness_id = parent_harness_id;
}
Ok(h)
}
async fn delete_harness(&self, _id: HarnessId) -> Result<()> {
Ok(())
}
async fn copy_harness(&self, _id: HarnessId, new_name: Option<&str>) -> Result<Harness> {
let mut h = self.harness.clone();
h.id = HarnessId::new();
h.name = new_name.unwrap_or("copy").to_string();
Ok(h)
}
async fn list_agents(&self) -> Result<Vec<Agent>> {
Ok(vec![self.agent.clone()])
}
async fn get_agent_by_id(&self, _id: crate::typed_id::AgentId) -> Result<Option<Agent>> {
Ok(Some(self.agent.clone()))
}
async fn create_agent(
&self,
name: &str,
display_name: Option<&str>,
_desc: Option<&str>,
_prompt: &str,
_caps: &[String],
) -> Result<Agent> {
let mut a = self.agent.clone();
a.name = name.to_string();
a.display_name = display_name.map(|s| s.to_string());
Ok(a)
}
async fn update_agent(
&self,
_id: crate::typed_id::AgentId,
name: Option<&str>,
display_name: Option<&str>,
_desc: Option<&str>,
_prompt: Option<&str>,
) -> Result<Agent> {
let mut a = self.agent.clone();
if let Some(n) = name {
a.name = n.to_string();
}
if let Some(dn) = display_name {
a.display_name = Some(dn.to_string());
}
Ok(a)
}
async fn delete_agent(&self, _id: crate::typed_id::AgentId) -> Result<()> {
Ok(())
}
async fn list_apps(
&self,
_search: Option<&str>,
_include_archived: bool,
) -> Result<Vec<App>> {
let mut app = self.app.clone();
app.channels = vec![self.app_channel.clone()];
Ok(vec![app])
}
async fn get_app(&self, _id: AppId) -> Result<Option<App>> {
let mut app = self.app.clone();
app.channels = vec![self.app_channel.clone()];
Ok(Some(app))
}
async fn create_app(
&self,
name: &str,
description: Option<&str>,
harness_id: HarnessId,
agent_id: Option<AgentId>,
agent_identity_id: Option<AgentIdentityId>,
channel_type: Option<ChannelType>,
channel_config: Option<&serde_json::Value>,
) -> Result<App> {
let mut app = self.app.clone();
app.name = name.to_string();
app.description = description.map(|value| value.to_string());
app.harness_id = harness_id;
app.agent_id = agent_id;
app.agent_identity_id = agent_identity_id;
app.channels = channel_type
.map(|channel_type| {
let mut channel = self.app_channel.clone();
channel.channel_type = channel_type;
if let Some(channel_config) = channel_config {
channel.channel_config = channel_config.clone();
}
vec![channel]
})
.unwrap_or_default();
Ok(app)
}
async fn update_app(
&self,
_id: AppId,
name: Option<&str>,
description: Option<&str>,
harness_id: Option<HarnessId>,
agent_id: Option<AgentId>,
agent_identity_id: Option<Option<AgentIdentityId>>,
) -> Result<App> {
let mut app = self.app.clone();
app.channels = vec![self.app_channel.clone()];
if let Some(name) = name {
app.name = name.to_string();
}
if let Some(description) = description {
app.description = Some(description.to_string());
}
if let Some(harness_id) = harness_id {
app.harness_id = harness_id;
}
if let Some(agent_id) = agent_id {
app.agent_id = Some(agent_id);
}
if let Some(agent_identity_id) = agent_identity_id {
app.agent_identity_id = agent_identity_id;
}
Ok(app)
}
async fn delete_app(&self, _id: AppId) -> Result<()> {
Ok(())
}
async fn destroy_app(&self, _id: AppId) -> Result<()> {
Ok(())
}
async fn publish_app(&self, _id: AppId) -> Result<App> {
let mut app = self.app.clone();
app.channels = vec![self.app_channel.clone()];
app.status = AppStatus::Published;
app.published_at = Some(chrono::Utc::now());
Ok(app)
}
async fn unpublish_app(&self, _id: AppId) -> Result<App> {
let mut app = self.app.clone();
app.channels = vec![self.app_channel.clone()];
app.status = AppStatus::Draft;
app.published_at = None;
Ok(app)
}
async fn add_app_channel(
&self,
_app_id: AppId,
channel_type: ChannelType,
channel_config: Option<&serde_json::Value>,
enabled: Option<bool>,
) -> Result<AppChannel> {
let mut channel = self.app_channel.clone();
channel.channel_type = channel_type;
if let Some(channel_config) = channel_config {
channel.channel_config = channel_config.clone();
}
if let Some(enabled) = enabled {
channel.enabled = enabled;
}
Ok(channel)
}
async fn update_app_channel(
&self,
_app_id: AppId,
_channel_id: AppChannelId,
channel_type: Option<ChannelType>,
channel_config: Option<&serde_json::Value>,
enabled: Option<bool>,
) -> Result<AppChannel> {
let mut channel = self.app_channel.clone();
if let Some(channel_type) = channel_type {
channel.channel_type = channel_type;
}
if let Some(channel_config) = channel_config {
channel.channel_config = channel_config.clone();
}
if let Some(enabled) = enabled {
channel.enabled = enabled;
}
Ok(channel)
}
async fn delete_app_channel(
&self,
_app_id: AppId,
_channel_id: AppChannelId,
) -> Result<()> {
Ok(())
}
async fn list_sessions(
&self,
_limit: Option<usize>,
_agent_id: Option<crate::typed_id::AgentId>,
) -> Result<Vec<Session>> {
Ok(vec![self.session.clone()])
}
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>,
) -> 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();
Ok(s)
}
async fn get_session_by_id(&self, _id: SessionId) -> Result<Option<Session>> {
Ok(Some(self.session.clone()))
}
async fn get_session_context_report(&self, id: SessionId) -> Result<SessionContextReport> {
Ok(SessionContextReport {
session_id: id.to_string(),
model: "llmsim".to_string(),
context_window_tokens: Some(128_000),
estimated_input_tokens: 42,
sections: vec![crate::ContextReportSection {
key: "conversation".to_string(),
label: "Conversation".to_string(),
tokens: 42,
items: 1,
}],
contributions: vec![],
cumulative_usage: None,
})
}
async fn set_subagent_metadata(
&self,
session_id: SessionId,
parent_session_id: SessionId,
subagent_name: &str,
subagent_task: &str,
subagent_status: crate::session::SubagentStatus,
) -> Result<Session> {
let mut s = self.session.clone();
s.id = session_id;
s.parent_session_id = Some(parent_session_id);
s.subagent_name = Some(subagent_name.to_string());
s.subagent_task = Some(subagent_task.to_string());
s.subagent_status = Some(subagent_status);
Ok(s)
}
async fn delete_session(&self, _id: SessionId) -> Result<()> {
Ok(())
}
async fn send_message(&self, _id: SessionId, _content: &str) -> Result<()> {
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("idle".to_string())
}
async fn list_capabilities(&self, search: Option<&str>) -> Result<Vec<CapabilityInfo>> {
let registry = crate::capabilities::CapabilityRegistry::with_builtins();
let mut caps: Vec<CapabilityInfo> = registry
.list()
.iter()
.map(|c| CapabilityInfo::from_core(c.as_ref()))
.collect();
if let Some(q) = search {
caps.retain(|c| c.matches_search(q));
}
caps.sort_by(|a, b| a.name.cmp(&b.name));
Ok(caps)
}
fn base_url(&self) -> &str {
"http://localhost:9300"
}
}
}