1use crate::SessionContextReport;
9use crate::agent::Agent;
10use crate::app::{App, AppChannel, ChannelType};
11use crate::capability_dto::CapabilityInfo;
12use crate::error::Result;
13use crate::harness::Harness;
14use crate::session::Session;
15use crate::typed_id::{AgentId, AgentIdentityId, AppChannelId, AppId, HarnessId, SessionId};
16use async_trait::async_trait;
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct PlatformMessage {
23 pub role: String,
24 pub content: String,
25 pub created_at: DateTime<Utc>,
26}
27
28#[async_trait]
34pub trait PlatformStore: Send + Sync {
35 async fn list_harnesses(&self) -> Result<Vec<Harness>>;
41
42 async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>>;
44
45 async fn create_harness(
47 &self,
48 name: &str,
49 display_name: Option<&str>,
50 description: Option<&str>,
51 system_prompt: &str,
52 parent_harness_id: Option<HarnessId>,
53 capabilities: &[String],
54 ) -> Result<Harness>;
55
56 async fn update_harness(
58 &self,
59 id: HarnessId,
60 name: Option<&str>,
61 display_name: Option<&str>,
62 description: Option<&str>,
63 system_prompt: Option<&str>,
64 parent_harness_id: Option<Option<HarnessId>>,
65 ) -> Result<Harness>;
66
67 async fn delete_harness(&self, id: HarnessId) -> Result<()>;
69
70 async fn copy_harness(&self, id: HarnessId, new_name: Option<&str>) -> Result<Harness>;
72
73 async fn list_agents(&self) -> Result<Vec<Agent>>;
79
80 async fn get_agent_by_id(&self, id: AgentId) -> Result<Option<Agent>>;
82
83 async fn create_agent(
85 &self,
86 name: &str,
87 display_name: Option<&str>,
88 description: Option<&str>,
89 system_prompt: &str,
90 capabilities: &[String],
91 ) -> Result<Agent>;
92
93 async fn update_agent(
95 &self,
96 id: AgentId,
97 name: Option<&str>,
98 display_name: Option<&str>,
99 description: Option<&str>,
100 system_prompt: Option<&str>,
101 ) -> Result<Agent>;
102
103 async fn delete_agent(&self, id: AgentId) -> Result<()>;
105
106 async fn list_apps(&self, search: Option<&str>, include_archived: bool) -> Result<Vec<App>>;
112
113 async fn get_app(&self, id: AppId) -> Result<Option<App>>;
115
116 #[allow(clippy::too_many_arguments)]
118 async fn create_app(
119 &self,
120 name: &str,
121 description: Option<&str>,
122 harness_id: HarnessId,
123 agent_id: Option<AgentId>,
124 agent_identity_id: Option<AgentIdentityId>,
125 channel_type: Option<ChannelType>,
126 channel_config: Option<&serde_json::Value>,
127 ) -> Result<App>;
128
129 #[allow(clippy::too_many_arguments)]
131 async fn update_app(
132 &self,
133 id: AppId,
134 name: Option<&str>,
135 description: Option<&str>,
136 harness_id: Option<HarnessId>,
137 agent_id: Option<AgentId>,
138 agent_identity_id: Option<Option<AgentIdentityId>>,
139 ) -> Result<App>;
140
141 async fn delete_app(&self, id: AppId) -> Result<()>;
143
144 async fn destroy_app(&self, id: AppId) -> Result<()>;
146
147 async fn publish_app(&self, id: AppId) -> Result<App>;
149
150 async fn unpublish_app(&self, id: AppId) -> Result<App>;
152
153 async fn add_app_channel(
155 &self,
156 app_id: AppId,
157 channel_type: ChannelType,
158 channel_config: Option<&serde_json::Value>,
159 enabled: Option<bool>,
160 ) -> Result<AppChannel>;
161
162 async fn update_app_channel(
164 &self,
165 app_id: AppId,
166 channel_id: AppChannelId,
167 channel_type: Option<ChannelType>,
168 channel_config: Option<&serde_json::Value>,
169 enabled: Option<bool>,
170 ) -> Result<AppChannel>;
171
172 async fn delete_app_channel(&self, app_id: AppId, channel_id: AppChannelId) -> Result<()>;
174
175 async fn list_sessions(
181 &self,
182 limit: Option<usize>,
183 agent_id: Option<AgentId>,
184 ) -> Result<Vec<Session>>;
185
186 #[allow(clippy::too_many_arguments)]
192 async fn create_session(
193 &self,
194 harness_id: HarnessId,
195 agent_id: Option<AgentId>,
196 title: Option<&str>,
197 locale: Option<&str>,
198 blueprint_id: Option<&str>,
199 blueprint_config: Option<&serde_json::Value>,
200 ) -> Result<Session>;
201
202 async fn set_subagent_metadata(
207 &self,
208 session_id: SessionId,
209 parent_session_id: SessionId,
210 subagent_name: &str,
211 subagent_task: &str,
212 subagent_status: crate::session::SubagentStatus,
213 ) -> Result<Session>;
214
215 async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>>;
217
218 async fn get_session_context_report(&self, id: SessionId) -> Result<SessionContextReport>;
220
221 async fn delete_session(&self, id: SessionId) -> Result<()>;
223
224 async fn send_message(&self, session_id: SessionId, content: &str) -> Result<()>;
230
231 async fn get_messages(
234 &self,
235 session_id: SessionId,
236 limit: Option<usize>,
237 ) -> Result<Vec<PlatformMessage>>;
238
239 async fn wait_for_idle(
247 &self,
248 session_id: SessionId,
249 timeout_secs: Option<u64>,
250 ) -> Result<String>;
251
252 async fn list_capabilities(&self, search: Option<&str>) -> Result<Vec<CapabilityInfo>>;
261
262 fn base_url(&self) -> &str;
268}
269
270#[cfg(test)]
271pub mod tests {
272 use super::*;
273 use crate::AgentCapabilityConfig;
274 use crate::agent::{Agent, AgentStatus};
275 use crate::app::{App, AppChannel, AppStatus, ChannelType};
276 use crate::harness::{Harness, HarnessStatus};
277 use crate::session::{Session, SessionStatus};
278
279 pub struct MockPlatformStore {
286 pub harness: Harness,
287 pub agent: Agent,
288 pub app: App,
289 pub app_channel: AppChannel,
290 pub session: Session,
291 pub created_session_harness_ids: std::sync::Mutex<Vec<HarnessId>>,
295 }
296
297 impl Default for MockPlatformStore {
298 fn default() -> Self {
299 Self::new()
300 }
301 }
302
303 impl MockPlatformStore {
304 pub fn new() -> Self {
305 Self {
306 harness: Harness {
307 id: HarnessId::new(),
308 name: "test-harness".to_string(),
309 display_name: Some("Test Harness".to_string()),
310 description: Some("test harness".to_string()),
311 system_prompt: "You are helpful.".to_string(),
312 parent_harness_id: None,
313 default_model_id: None,
314 tags: vec![],
315 capabilities: vec![AgentCapabilityConfig::new("session")],
316 initial_files: vec![],
317 network_access: None,
318 mcp_servers: Default::default(),
319 is_built_in: false,
320 status: HarnessStatus::Active,
321 created_at: chrono::Utc::now(),
322 updated_at: chrono::Utc::now(),
323 archived_at: None,
324 deleted_at: None,
325 },
326 agent: Agent {
327 public_id: crate::typed_id::AgentId::new(),
328 internal_id: uuid::Uuid::now_v7(),
329 name: "test-agent".to_string(),
330 display_name: Some("Test Agent".to_string()),
331 description: Some("test agent".to_string()),
332 system_prompt: "You are helpful.".to_string(),
333 default_model_id: None,
334 default_version_id: None,
335 forked_from_agent_id: None,
336 forked_from_version_id: None,
337 root_agent_id: None,
338 tags: vec![],
339 capabilities: vec![],
340 initial_files: vec![],
341 network_access: None,
342 max_iterations: None,
343 tools: vec![],
344 mcp_servers: Default::default(),
345 status: AgentStatus::Active,
346 created_at: chrono::Utc::now(),
347 updated_at: chrono::Utc::now(),
348 archived_at: None,
349 deleted_at: None,
350 usage: None,
351 },
352 app_channel: AppChannel {
353 public_id: AppChannelId::new(),
354 internal_id: uuid::Uuid::now_v7(),
355 channel_type: ChannelType::Webhook,
356 channel_config: serde_json::json!({
357 "token": "secret-1",
358 "session_mode": "shared_session",
359 "message": "Run checks for {{payload.repo.name}}"
360 }),
361 enabled: true,
362 created_at: chrono::Utc::now(),
363 updated_at: chrono::Utc::now(),
364 },
365 app: App {
366 public_id: AppId::new(),
367 internal_id: uuid::Uuid::now_v7(),
368 org_id: 1,
369 name: "test-app".to_string(),
370 description: Some("test app".to_string()),
371 harness_id: HarnessId::new(),
372 agent_id: Some(crate::typed_id::AgentId::new()),
373 agent_version_policy: crate::app::AgentVersionPolicy::Default,
374 agent_version_id: None,
375 agent_identity_id: Some(crate::typed_id::AgentIdentityId::new()),
376 owner_principal_id: crate::PrincipalId::from_seed(1),
377 resolved_owner_user_id: None,
378 owner: None,
379 effective_owner: None,
380 channels: vec![],
381 status: AppStatus::Draft,
382 published_at: None,
383 created_at: chrono::Utc::now(),
384 updated_at: chrono::Utc::now(),
385 archived_at: None,
386 deleted_at: None,
387 },
388 session: Session {
389 id: SessionId::new(),
390 organization_id: "org_00000000000000000000000000000001".to_string(),
391 harness_id: HarnessId::new(),
392 agent_id: None,
393 agent_version_id: None,
394 agent_identity_id: None,
395 owner_principal_id: crate::PrincipalId::from_seed(1),
396 resolved_owner_user_id: None,
397 owner: None,
398 effective_owner: None,
399 title: Some("Test Session".to_string()),
400 locale: None,
401 preview: None,
402 output_preview: None,
403 tags: vec![],
404 model_id: None,
405 capabilities: vec![],
406 tools: vec![],
407 mcp_servers: Default::default(),
408 system_prompt: None,
409 initial_files: vec![],
410 hints: None,
411 network_access: None,
412 max_iterations: None,
413 status: SessionStatus::Idle,
414 created_at: chrono::Utc::now(),
415 updated_at: chrono::Utc::now(),
416 started_at: None,
417 finished_at: None,
418 usage: None,
419 is_pinned: None,
420 active_schedule_count: None,
421 features: vec![],
422 parent_session_id: None,
423 subagent_name: None,
424 subagent_task: None,
425 subagent_status: None,
426 blueprint_id: None,
427 blueprint_config: None,
428 },
429 created_session_harness_ids: std::sync::Mutex::new(Vec::new()),
430 }
431 }
432 }
433
434 #[async_trait]
435 impl PlatformStore for MockPlatformStore {
436 async fn list_harnesses(&self) -> Result<Vec<Harness>> {
437 Ok(vec![self.harness.clone()])
438 }
439 async fn get_harness(&self, _id: HarnessId) -> Result<Option<Harness>> {
440 Ok(Some(self.harness.clone()))
441 }
442 async fn create_harness(
443 &self,
444 name: &str,
445 display_name: Option<&str>,
446 _desc: Option<&str>,
447 _prompt: &str,
448 parent_harness_id: Option<HarnessId>,
449 _caps: &[String],
450 ) -> Result<Harness> {
451 let mut h = self.harness.clone();
452 h.name = name.to_string();
453 h.display_name = display_name.map(|s| s.to_string());
454 h.parent_harness_id = parent_harness_id;
455 Ok(h)
456 }
457 async fn update_harness(
458 &self,
459 _id: HarnessId,
460 name: Option<&str>,
461 display_name: Option<&str>,
462 _desc: Option<&str>,
463 _prompt: Option<&str>,
464 parent_harness_id: Option<Option<HarnessId>>,
465 ) -> Result<Harness> {
466 let mut h = self.harness.clone();
467 if let Some(n) = name {
468 h.name = n.to_string();
469 }
470 if let Some(dn) = display_name {
471 h.display_name = Some(dn.to_string());
472 }
473 if let Some(parent_harness_id) = parent_harness_id {
474 h.parent_harness_id = parent_harness_id;
475 }
476 Ok(h)
477 }
478 async fn delete_harness(&self, _id: HarnessId) -> Result<()> {
479 Ok(())
480 }
481 async fn copy_harness(&self, _id: HarnessId, new_name: Option<&str>) -> Result<Harness> {
482 let mut h = self.harness.clone();
483 h.id = HarnessId::new();
484 h.name = new_name.unwrap_or("copy").to_string();
485 Ok(h)
486 }
487 async fn list_agents(&self) -> Result<Vec<Agent>> {
488 Ok(vec![self.agent.clone()])
489 }
490 async fn get_agent_by_id(&self, _id: crate::typed_id::AgentId) -> Result<Option<Agent>> {
491 Ok(Some(self.agent.clone()))
492 }
493 async fn create_agent(
494 &self,
495 name: &str,
496 display_name: Option<&str>,
497 _desc: Option<&str>,
498 _prompt: &str,
499 _caps: &[String],
500 ) -> Result<Agent> {
501 let mut a = self.agent.clone();
502 a.name = name.to_string();
503 a.display_name = display_name.map(|s| s.to_string());
504 Ok(a)
505 }
506 async fn update_agent(
507 &self,
508 _id: crate::typed_id::AgentId,
509 name: Option<&str>,
510 display_name: Option<&str>,
511 _desc: Option<&str>,
512 _prompt: Option<&str>,
513 ) -> Result<Agent> {
514 let mut a = self.agent.clone();
515 if let Some(n) = name {
516 a.name = n.to_string();
517 }
518 if let Some(dn) = display_name {
519 a.display_name = Some(dn.to_string());
520 }
521 Ok(a)
522 }
523 async fn delete_agent(&self, _id: crate::typed_id::AgentId) -> Result<()> {
524 Ok(())
525 }
526 async fn list_apps(
527 &self,
528 _search: Option<&str>,
529 _include_archived: bool,
530 ) -> Result<Vec<App>> {
531 let mut app = self.app.clone();
532 app.channels = vec![self.app_channel.clone()];
533 Ok(vec![app])
534 }
535 async fn get_app(&self, _id: AppId) -> Result<Option<App>> {
536 let mut app = self.app.clone();
537 app.channels = vec![self.app_channel.clone()];
538 Ok(Some(app))
539 }
540 async fn create_app(
541 &self,
542 name: &str,
543 description: Option<&str>,
544 harness_id: HarnessId,
545 agent_id: Option<AgentId>,
546 agent_identity_id: Option<AgentIdentityId>,
547 channel_type: Option<ChannelType>,
548 channel_config: Option<&serde_json::Value>,
549 ) -> Result<App> {
550 let mut app = self.app.clone();
551 app.name = name.to_string();
552 app.description = description.map(|value| value.to_string());
553 app.harness_id = harness_id;
554 app.agent_id = agent_id;
555 app.agent_identity_id = agent_identity_id;
556 app.channels = channel_type
557 .map(|channel_type| {
558 let mut channel = self.app_channel.clone();
559 channel.channel_type = channel_type;
560 if let Some(channel_config) = channel_config {
561 channel.channel_config = channel_config.clone();
562 }
563 vec![channel]
564 })
565 .unwrap_or_default();
566 Ok(app)
567 }
568 async fn update_app(
569 &self,
570 _id: AppId,
571 name: Option<&str>,
572 description: Option<&str>,
573 harness_id: Option<HarnessId>,
574 agent_id: Option<AgentId>,
575 agent_identity_id: Option<Option<AgentIdentityId>>,
576 ) -> Result<App> {
577 let mut app = self.app.clone();
578 app.channels = vec![self.app_channel.clone()];
579 if let Some(name) = name {
580 app.name = name.to_string();
581 }
582 if let Some(description) = description {
583 app.description = Some(description.to_string());
584 }
585 if let Some(harness_id) = harness_id {
586 app.harness_id = harness_id;
587 }
588 if let Some(agent_id) = agent_id {
589 app.agent_id = Some(agent_id);
590 }
591 if let Some(agent_identity_id) = agent_identity_id {
592 app.agent_identity_id = agent_identity_id;
593 }
594 Ok(app)
595 }
596 async fn delete_app(&self, _id: AppId) -> Result<()> {
597 Ok(())
598 }
599 async fn destroy_app(&self, _id: AppId) -> Result<()> {
600 Ok(())
601 }
602 async fn publish_app(&self, _id: AppId) -> Result<App> {
603 let mut app = self.app.clone();
604 app.channels = vec![self.app_channel.clone()];
605 app.status = AppStatus::Published;
606 app.published_at = Some(chrono::Utc::now());
607 Ok(app)
608 }
609 async fn unpublish_app(&self, _id: AppId) -> Result<App> {
610 let mut app = self.app.clone();
611 app.channels = vec![self.app_channel.clone()];
612 app.status = AppStatus::Draft;
613 app.published_at = None;
614 Ok(app)
615 }
616 async fn add_app_channel(
617 &self,
618 _app_id: AppId,
619 channel_type: ChannelType,
620 channel_config: Option<&serde_json::Value>,
621 enabled: Option<bool>,
622 ) -> Result<AppChannel> {
623 let mut channel = self.app_channel.clone();
624 channel.channel_type = channel_type;
625 if let Some(channel_config) = channel_config {
626 channel.channel_config = channel_config.clone();
627 }
628 if let Some(enabled) = enabled {
629 channel.enabled = enabled;
630 }
631 Ok(channel)
632 }
633 async fn update_app_channel(
634 &self,
635 _app_id: AppId,
636 _channel_id: AppChannelId,
637 channel_type: Option<ChannelType>,
638 channel_config: Option<&serde_json::Value>,
639 enabled: Option<bool>,
640 ) -> Result<AppChannel> {
641 let mut channel = self.app_channel.clone();
642 if let Some(channel_type) = channel_type {
643 channel.channel_type = channel_type;
644 }
645 if let Some(channel_config) = channel_config {
646 channel.channel_config = channel_config.clone();
647 }
648 if let Some(enabled) = enabled {
649 channel.enabled = enabled;
650 }
651 Ok(channel)
652 }
653 async fn delete_app_channel(
654 &self,
655 _app_id: AppId,
656 _channel_id: AppChannelId,
657 ) -> Result<()> {
658 Ok(())
659 }
660 async fn list_sessions(
661 &self,
662 _limit: Option<usize>,
663 _agent_id: Option<crate::typed_id::AgentId>,
664 ) -> Result<Vec<Session>> {
665 Ok(vec![self.session.clone()])
666 }
667 async fn create_session(
668 &self,
669 hid: HarnessId,
670 aid: Option<crate::typed_id::AgentId>,
671 title: Option<&str>,
672 locale: Option<&str>,
673 blueprint_id: Option<&str>,
674 blueprint_config: Option<&serde_json::Value>,
675 ) -> Result<Session> {
676 if let Ok(mut recorder) = self.created_session_harness_ids.lock() {
677 recorder.push(hid);
678 }
679 let mut s = self.session.clone();
680 s.id = SessionId::new();
681 s.harness_id = hid;
682 s.agent_id = aid;
683 s.title = title.map(|t| t.to_string());
684 s.locale = locale.map(|value| value.to_string());
685 s.blueprint_id = blueprint_id.map(|b| b.to_string());
686 s.blueprint_config = blueprint_config.cloned();
687 Ok(s)
688 }
689 async fn get_session_by_id(&self, _id: SessionId) -> Result<Option<Session>> {
690 Ok(Some(self.session.clone()))
691 }
692 async fn get_session_context_report(&self, id: SessionId) -> Result<SessionContextReport> {
693 Ok(SessionContextReport {
694 session_id: id.to_string(),
695 model: "llmsim".to_string(),
696 context_window_tokens: Some(128_000),
697 estimated_input_tokens: 42,
698 sections: vec![crate::ContextReportSection {
699 key: "conversation".to_string(),
700 label: "Conversation".to_string(),
701 tokens: 42,
702 items: 1,
703 }],
704 contributions: vec![],
705 cumulative_usage: None,
706 })
707 }
708 async fn set_subagent_metadata(
709 &self,
710 session_id: SessionId,
711 parent_session_id: SessionId,
712 subagent_name: &str,
713 subagent_task: &str,
714 subagent_status: crate::session::SubagentStatus,
715 ) -> Result<Session> {
716 let mut s = self.session.clone();
717 s.id = session_id;
718 s.parent_session_id = Some(parent_session_id);
719 s.subagent_name = Some(subagent_name.to_string());
720 s.subagent_task = Some(subagent_task.to_string());
721 s.subagent_status = Some(subagent_status);
722 Ok(s)
723 }
724 async fn delete_session(&self, _id: SessionId) -> Result<()> {
725 Ok(())
726 }
727 async fn send_message(&self, _id: SessionId, _content: &str) -> Result<()> {
728 Ok(())
729 }
730 async fn get_messages(
731 &self,
732 _id: SessionId,
733 _limit: Option<usize>,
734 ) -> Result<Vec<PlatformMessage>> {
735 Ok(vec![
736 PlatformMessage {
737 role: "user".into(),
738 content: "Hello".into(),
739 created_at: chrono::Utc::now(),
740 },
741 PlatformMessage {
742 role: "agent".into(),
743 content: "Hi!".into(),
744 created_at: chrono::Utc::now(),
745 },
746 ])
747 }
748 async fn wait_for_idle(&self, _id: SessionId, _t: Option<u64>) -> Result<String> {
749 Ok("idle".to_string())
750 }
751 async fn list_capabilities(&self, search: Option<&str>) -> Result<Vec<CapabilityInfo>> {
752 let registry = crate::capabilities::CapabilityRegistry::with_builtins();
753 let mut caps: Vec<CapabilityInfo> = registry
754 .list()
755 .iter()
756 .map(|c| CapabilityInfo::from_core(c.as_ref()))
757 .collect();
758 if let Some(q) = search {
759 caps.retain(|c| c.matches_search(q));
760 }
761 caps.sort_by(|a, b| a.name.cmp(&b.name));
762 Ok(caps)
763 }
764 fn base_url(&self) -> &str {
765 "http://localhost:9300"
766 }
767 }
768}