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, SessionParticipant, SessionSeedMode};
15use crate::typed_id::{AgentId, AgentIdentityId, AppChannelId, AppId, HarnessId, SessionId};
16use async_trait::async_trait;
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19use std::collections::HashSet;
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]
53pub trait PlatformStore: Send + Sync {
54 async fn list_harnesses(&self) -> Result<Vec<Harness>>;
60
61 async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>>;
63
64 async fn get_harness_chain(&self, id: HarnessId) -> Result<Vec<Harness>> {
70 let mut chain = Vec::new();
71 let mut current_id = Some(id);
72 let mut seen = HashSet::new();
73
74 while let Some(harness_id) = current_id {
75 if !seen.insert(harness_id) {
76 return Err(crate::error::AgentLoopError::tool(format!(
77 "Harness inheritance cycle detected at {harness_id}"
78 )));
79 }
80 let Some(harness) = self.get_harness(harness_id).await? else {
81 return Ok(Vec::new());
82 };
83 current_id = harness.parent_harness_id;
84 chain.push(harness);
85 }
86
87 chain.reverse();
88 Ok(chain)
89 }
90
91 async fn create_harness(
97 &self,
98 name: &str,
99 display_name: Option<&str>,
100 description: Option<&str>,
101 system_prompt: Option<&str>,
102 parent_harness_id: Option<HarnessId>,
103 capabilities: &[String],
104 ) -> Result<Harness>;
105
106 async fn update_harness(
108 &self,
109 id: HarnessId,
110 name: Option<&str>,
111 display_name: Option<&str>,
112 description: Option<&str>,
113 system_prompt: Option<&str>,
114 parent_harness_id: Option<Option<HarnessId>>,
115 ) -> Result<Harness>;
116
117 async fn delete_harness(&self, id: HarnessId) -> Result<()>;
119
120 async fn copy_harness(&self, id: HarnessId, new_name: Option<&str>) -> Result<Harness>;
122
123 async fn list_agents(&self) -> Result<Vec<Agent>>;
129
130 async fn get_agent_by_id(&self, id: AgentId) -> Result<Option<Agent>>;
132
133 async fn create_agent(
135 &self,
136 name: &str,
137 display_name: Option<&str>,
138 description: Option<&str>,
139 system_prompt: &str,
140 capabilities: &[String],
141 ) -> Result<Agent>;
142
143 async fn update_agent(
145 &self,
146 id: AgentId,
147 name: Option<&str>,
148 display_name: Option<&str>,
149 description: Option<&str>,
150 system_prompt: Option<&str>,
151 ) -> Result<Agent>;
152
153 async fn delete_agent(&self, id: AgentId) -> Result<()>;
155
156 async fn list_apps(&self, search: Option<&str>, include_archived: bool) -> Result<Vec<App>>;
162
163 async fn get_app(&self, id: AppId) -> Result<Option<App>>;
165
166 #[allow(clippy::too_many_arguments)]
168 async fn create_app(
169 &self,
170 name: &str,
171 description: Option<&str>,
172 harness_id: HarnessId,
173 agent_id: Option<AgentId>,
174 agent_identity_id: Option<AgentIdentityId>,
175 channel_type: Option<ChannelType>,
176 channel_config: Option<&serde_json::Value>,
177 ) -> Result<App>;
178
179 #[allow(clippy::too_many_arguments)]
181 async fn update_app(
182 &self,
183 id: AppId,
184 name: Option<&str>,
185 description: Option<&str>,
186 harness_id: Option<HarnessId>,
187 agent_id: Option<AgentId>,
188 agent_identity_id: Option<Option<AgentIdentityId>>,
189 ) -> Result<App>;
190
191 async fn delete_app(&self, id: AppId) -> Result<()>;
193
194 async fn destroy_app(&self, id: AppId) -> Result<()>;
196
197 async fn publish_app(&self, id: AppId) -> Result<App>;
199
200 async fn unpublish_app(&self, id: AppId) -> Result<App>;
202
203 async fn add_app_channel(
205 &self,
206 app_id: AppId,
207 channel_type: ChannelType,
208 channel_config: Option<&serde_json::Value>,
209 enabled: Option<bool>,
210 ) -> Result<AppChannel>;
211
212 async fn update_app_channel(
214 &self,
215 app_id: AppId,
216 channel_id: AppChannelId,
217 channel_type: Option<ChannelType>,
218 channel_config: Option<&serde_json::Value>,
219 enabled: Option<bool>,
220 ) -> Result<AppChannel>;
221
222 async fn delete_app_channel(&self, app_id: AppId, channel_id: AppChannelId) -> Result<()>;
224
225 async fn list_sessions(
231 &self,
232 limit: Option<usize>,
233 agent_id: Option<AgentId>,
234 ) -> Result<Vec<Session>>;
235
236 #[allow(clippy::too_many_arguments)]
244 async fn create_session(
245 &self,
246 harness_id: HarnessId,
247 agent_id: Option<AgentId>,
248 title: Option<&str>,
249 locale: Option<&str>,
250 blueprint_id: Option<&str>,
251 blueprint_config: Option<&serde_json::Value>,
252 parent_session_id: Option<SessionId>,
253 ) -> Result<Session>;
254
255 async fn create_session_with_options(
256 &self,
257 request: PlatformCreateSessionRequest,
258 ) -> Result<Session> {
259 if request.goal.is_some()
260 || request.forked_from_session_id.is_some()
261 || request.budget_root_session_id.is_some()
262 || request.seed != SessionSeedMode::Fresh
263 {
264 return Err(crate::error::AgentLoopError::tool(
265 "platform store does not support goal, lineage, budget-root override, or seeded session creation",
266 ));
267 }
268 self.create_session(
269 request.harness_id,
270 request.agent_id,
271 request.title.as_deref(),
272 request.locale.as_deref(),
273 request.blueprint_id.as_deref(),
274 request.blueprint_config.as_ref(),
275 request.parent_session_id,
276 )
277 .await
278 }
279
280 async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>>;
282
283 async fn add_agent_session_participant(
285 &self,
286 session_id: SessionId,
287 agent_id: AgentId,
288 ) -> Result<SessionParticipant>;
289
290 async fn get_session_context_report(&self, id: SessionId) -> Result<SessionContextReport>;
292
293 async fn delete_session(&self, id: SessionId) -> Result<()>;
295
296 async fn send_message(&self, session_id: SessionId, content: &str) -> Result<()>;
302
303 async fn get_messages(
306 &self,
307 session_id: SessionId,
308 limit: Option<usize>,
309 ) -> Result<Vec<PlatformMessage>>;
310
311 async fn wait_for_idle(
319 &self,
320 session_id: SessionId,
321 timeout_secs: Option<u64>,
322 ) -> Result<String>;
323
324 async fn list_capabilities(&self, search: Option<&str>) -> Result<Vec<CapabilityInfo>>;
333
334 fn base_url(&self) -> &str;
340}
341
342#[cfg(test)]
343pub mod tests {
344 use super::*;
345 use crate::AgentCapabilityConfig;
346 use crate::agent::{Agent, AgentStatus};
347 use crate::app::{App, AppChannel, AppStatus, ChannelType};
348 use crate::harness::{Harness, HarnessStatus};
349 use crate::session::{Session, SessionStatus};
350
351 pub struct MockPlatformStore {
358 pub harness: Harness,
359 pub extra_harnesses: std::sync::Mutex<std::collections::HashMap<HarnessId, Harness>>,
360 pub agent: Agent,
361 pub app: App,
362 pub app_channel: AppChannel,
363 pub session: Session,
364 pub extra_sessions: std::sync::Mutex<std::collections::HashMap<SessionId, Session>>,
365 pub joined_participants: std::sync::Mutex<Vec<SessionParticipant>>,
366 pub created_session_harness_ids: std::sync::Mutex<Vec<HarnessId>>,
370 pub created_session_budget_roots: std::sync::Mutex<Vec<Option<SessionId>>>,
372 pub wait_for_idle_status: std::sync::Mutex<String>,
375 pub sent_messages: std::sync::Mutex<Vec<(SessionId, String)>>,
379 }
380
381 impl Default for MockPlatformStore {
382 fn default() -> Self {
383 Self::new()
384 }
385 }
386
387 impl MockPlatformStore {
388 pub fn new() -> Self {
389 Self {
390 harness: Harness {
391 id: HarnessId::new(),
392 name: "test-harness".to_string(),
393 display_name: Some("Test Harness".to_string()),
394 description: Some("test harness".to_string()),
395 system_prompt: Some("You are helpful.".to_string()),
396 parent_harness_id: None,
397 default_model_id: None,
398 tags: vec![],
399 capabilities: vec![AgentCapabilityConfig::new("session")],
400 initial_files: vec![],
401 network_access: None,
402 parallel_tool_calls: None,
403 mcp_servers: Default::default(),
404 embedder_metadata: Default::default(),
405 is_built_in: false,
406 status: HarnessStatus::Active,
407 created_at: chrono::Utc::now(),
408 updated_at: chrono::Utc::now(),
409 archived_at: None,
410 deleted_at: None,
411 },
412 extra_harnesses: std::sync::Mutex::new(std::collections::HashMap::new()),
413 agent: Agent {
414 public_id: crate::typed_id::AgentId::new(),
415 internal_id: uuid::Uuid::now_v7(),
416 name: "test-agent".to_string(),
417 display_name: Some("Test Agent".to_string()),
418 description: Some("test agent".to_string()),
419 system_prompt: "You are helpful.".to_string(),
420 default_model_id: None,
421
422 harness_id: crate::typed_id::HarnessId::from_uuid(uuid::Uuid::nil()),
423 default_version_id: None,
424 forked_from_agent_id: None,
425 forked_from_version_id: None,
426 root_agent_id: None,
427 tags: vec![],
428 capabilities: vec![],
429 initial_files: vec![],
430 network_access: None,
431 max_iterations: None,
432 parallel_tool_calls: None,
433 tools: vec![],
434 mcp_servers: Default::default(),
435 status: AgentStatus::Active,
436 created_at: chrono::Utc::now(),
437 updated_at: chrono::Utc::now(),
438 archived_at: None,
439 deleted_at: None,
440 usage: None,
441 },
442 app_channel: AppChannel {
443 public_id: AppChannelId::new(),
444 internal_id: uuid::Uuid::now_v7(),
445 channel_type: ChannelType::Webhook,
446 channel_config: serde_json::json!({
447 "token": "secret-1",
448 "session_mode": "shared_session",
449 "message": "Run checks for {{payload.repo.name}}"
450 }),
451 enabled: true,
452 created_at: chrono::Utc::now(),
453 updated_at: chrono::Utc::now(),
454 },
455 app: App {
456 public_id: AppId::new(),
457 internal_id: uuid::Uuid::now_v7(),
458 org_id: 1,
459 name: "test-app".to_string(),
460 description: Some("test app".to_string()),
461 harness_id: HarnessId::new(),
462 agent_id: Some(crate::typed_id::AgentId::new()),
463 agent_version_policy: crate::app::AgentVersionPolicy::Default,
464 agent_version_id: None,
465 agent_identity_id: Some(crate::typed_id::AgentIdentityId::new()),
466 owner_principal_id: crate::PrincipalId::from_seed(1),
467 resolved_owner_user_id: None,
468 owner: None,
469 effective_owner: None,
470 channels: vec![],
471 status: AppStatus::Draft,
472 published_at: None,
473 created_at: chrono::Utc::now(),
474 updated_at: chrono::Utc::now(),
475 archived_at: None,
476 deleted_at: None,
477 },
478 session: {
479 let session_id = SessionId::new();
480 Session {
481 id: session_id,
483 workspace_id: crate::WorkspaceId::from_uuid(session_id.uuid()),
484 organization_id: "org_00000000000000000000000000000001".to_string(),
485 harness_id: HarnessId::new(),
486 agent_id: None,
487 agent_version_id: None,
488 agent_identity_id: None,
489 owner_principal_id: crate::PrincipalId::from_seed(1),
490 resolved_owner_user_id: None,
491 owner: None,
492 effective_owner: None,
493 title: Some("Test Session".to_string()),
494 goal: None,
495 locale: None,
496 preview: None,
497 output_preview: None,
498 tags: vec![],
499 model_id: None,
500 capabilities: vec![],
501 tools: vec![],
502 mcp_servers: Default::default(),
503 system_prompt: None,
504 initial_files: vec![],
505 hints: None,
506 network_access: None,
507 max_iterations: None,
508 parallel_tool_calls: None,
509 status: SessionStatus::Idle,
510 created_at: chrono::Utc::now(),
511 updated_at: chrono::Utc::now(),
512 started_at: None,
513 finished_at: None,
514 usage: None,
515 is_pinned: None,
516 active_schedule_count: None,
517 features: vec![],
518 parent_session_id: None,
519 forked_from_session_id: None,
520 forked_from_sequence: None,
521 blueprint_id: None,
522 blueprint_config: None,
523 }
524 },
525 extra_sessions: std::sync::Mutex::new(std::collections::HashMap::new()),
526 joined_participants: std::sync::Mutex::new(Vec::new()),
527 created_session_harness_ids: std::sync::Mutex::new(Vec::new()),
528 created_session_budget_roots: std::sync::Mutex::new(Vec::new()),
529 wait_for_idle_status: std::sync::Mutex::new("idle".to_string()),
530 sent_messages: std::sync::Mutex::new(Vec::new()),
531 }
532 }
533 }
534
535 #[async_trait]
536 impl PlatformStore for MockPlatformStore {
537 async fn list_harnesses(&self) -> Result<Vec<Harness>> {
538 Ok(vec![self.harness.clone()])
539 }
540 async fn get_harness(&self, id: HarnessId) -> Result<Option<Harness>> {
541 if let Some(harness) = self.extra_harnesses.lock().unwrap().get(&id).cloned() {
542 return Ok(Some(harness));
543 }
544 Ok(Some(self.harness.clone()))
545 }
546 async fn create_harness(
547 &self,
548 name: &str,
549 display_name: Option<&str>,
550 _desc: Option<&str>,
551 _prompt: Option<&str>,
552 parent_harness_id: Option<HarnessId>,
553 _caps: &[String],
554 ) -> Result<Harness> {
555 let mut h = self.harness.clone();
556 h.name = name.to_string();
557 h.display_name = display_name.map(|s| s.to_string());
558 h.parent_harness_id = parent_harness_id;
559 Ok(h)
560 }
561 async fn update_harness(
562 &self,
563 _id: HarnessId,
564 name: Option<&str>,
565 display_name: Option<&str>,
566 _desc: Option<&str>,
567 _prompt: Option<&str>,
568 parent_harness_id: Option<Option<HarnessId>>,
569 ) -> Result<Harness> {
570 let mut h = self.harness.clone();
571 if let Some(n) = name {
572 h.name = n.to_string();
573 }
574 if let Some(dn) = display_name {
575 h.display_name = Some(dn.to_string());
576 }
577 if let Some(parent_harness_id) = parent_harness_id {
578 h.parent_harness_id = parent_harness_id;
579 }
580 Ok(h)
581 }
582 async fn delete_harness(&self, _id: HarnessId) -> Result<()> {
583 Ok(())
584 }
585 async fn copy_harness(&self, _id: HarnessId, new_name: Option<&str>) -> Result<Harness> {
586 let mut h = self.harness.clone();
587 h.id = HarnessId::new();
588 h.name = new_name.unwrap_or("copy").to_string();
589 Ok(h)
590 }
591 async fn list_agents(&self) -> Result<Vec<Agent>> {
592 Ok(vec![self.agent.clone()])
593 }
594 async fn get_agent_by_id(&self, _id: crate::typed_id::AgentId) -> Result<Option<Agent>> {
595 Ok(Some(self.agent.clone()))
596 }
597 async fn create_agent(
598 &self,
599 name: &str,
600 display_name: Option<&str>,
601 _desc: Option<&str>,
602 _prompt: &str,
603 _caps: &[String],
604 ) -> Result<Agent> {
605 let mut a = self.agent.clone();
606 a.name = name.to_string();
607 a.display_name = display_name.map(|s| s.to_string());
608 Ok(a)
609 }
610 async fn update_agent(
611 &self,
612 _id: crate::typed_id::AgentId,
613 name: Option<&str>,
614 display_name: Option<&str>,
615 _desc: Option<&str>,
616 _prompt: Option<&str>,
617 ) -> Result<Agent> {
618 let mut a = self.agent.clone();
619 if let Some(n) = name {
620 a.name = n.to_string();
621 }
622 if let Some(dn) = display_name {
623 a.display_name = Some(dn.to_string());
624 }
625 Ok(a)
626 }
627 async fn delete_agent(&self, _id: crate::typed_id::AgentId) -> Result<()> {
628 Ok(())
629 }
630 async fn list_apps(
631 &self,
632 _search: Option<&str>,
633 _include_archived: bool,
634 ) -> Result<Vec<App>> {
635 let mut app = self.app.clone();
636 app.channels = vec![self.app_channel.clone()];
637 Ok(vec![app])
638 }
639 async fn get_app(&self, _id: AppId) -> Result<Option<App>> {
640 let mut app = self.app.clone();
641 app.channels = vec![self.app_channel.clone()];
642 Ok(Some(app))
643 }
644 async fn create_app(
645 &self,
646 name: &str,
647 description: Option<&str>,
648 harness_id: HarnessId,
649 agent_id: Option<AgentId>,
650 agent_identity_id: Option<AgentIdentityId>,
651 channel_type: Option<ChannelType>,
652 channel_config: Option<&serde_json::Value>,
653 ) -> Result<App> {
654 let mut app = self.app.clone();
655 app.name = name.to_string();
656 app.description = description.map(|value| value.to_string());
657 app.harness_id = harness_id;
658 app.agent_id = agent_id;
659 app.agent_identity_id = agent_identity_id;
660 app.channels = channel_type
661 .map(|channel_type| {
662 let mut channel = self.app_channel.clone();
663 channel.channel_type = channel_type;
664 if let Some(channel_config) = channel_config {
665 channel.channel_config = channel_config.clone();
666 }
667 vec![channel]
668 })
669 .unwrap_or_default();
670 Ok(app)
671 }
672 async fn update_app(
673 &self,
674 _id: AppId,
675 name: Option<&str>,
676 description: Option<&str>,
677 harness_id: Option<HarnessId>,
678 agent_id: Option<AgentId>,
679 agent_identity_id: Option<Option<AgentIdentityId>>,
680 ) -> Result<App> {
681 let mut app = self.app.clone();
682 app.channels = vec![self.app_channel.clone()];
683 if let Some(name) = name {
684 app.name = name.to_string();
685 }
686 if let Some(description) = description {
687 app.description = Some(description.to_string());
688 }
689 if let Some(harness_id) = harness_id {
690 app.harness_id = harness_id;
691 }
692 if let Some(agent_id) = agent_id {
693 app.agent_id = Some(agent_id);
694 }
695 if let Some(agent_identity_id) = agent_identity_id {
696 app.agent_identity_id = agent_identity_id;
697 }
698 Ok(app)
699 }
700 async fn delete_app(&self, _id: AppId) -> Result<()> {
701 Ok(())
702 }
703 async fn destroy_app(&self, _id: AppId) -> Result<()> {
704 Ok(())
705 }
706 async fn publish_app(&self, _id: AppId) -> Result<App> {
707 let mut app = self.app.clone();
708 app.channels = vec![self.app_channel.clone()];
709 app.status = AppStatus::Published;
710 app.published_at = Some(chrono::Utc::now());
711 Ok(app)
712 }
713 async fn unpublish_app(&self, _id: AppId) -> Result<App> {
714 let mut app = self.app.clone();
715 app.channels = vec![self.app_channel.clone()];
716 app.status = AppStatus::Draft;
717 app.published_at = None;
718 Ok(app)
719 }
720 async fn add_app_channel(
721 &self,
722 _app_id: AppId,
723 channel_type: ChannelType,
724 channel_config: Option<&serde_json::Value>,
725 enabled: Option<bool>,
726 ) -> Result<AppChannel> {
727 let mut channel = self.app_channel.clone();
728 channel.channel_type = channel_type;
729 if let Some(channel_config) = channel_config {
730 channel.channel_config = channel_config.clone();
731 }
732 if let Some(enabled) = enabled {
733 channel.enabled = enabled;
734 }
735 Ok(channel)
736 }
737 async fn update_app_channel(
738 &self,
739 _app_id: AppId,
740 _channel_id: AppChannelId,
741 channel_type: Option<ChannelType>,
742 channel_config: Option<&serde_json::Value>,
743 enabled: Option<bool>,
744 ) -> Result<AppChannel> {
745 let mut channel = self.app_channel.clone();
746 if let Some(channel_type) = channel_type {
747 channel.channel_type = channel_type;
748 }
749 if let Some(channel_config) = channel_config {
750 channel.channel_config = channel_config.clone();
751 }
752 if let Some(enabled) = enabled {
753 channel.enabled = enabled;
754 }
755 Ok(channel)
756 }
757 async fn delete_app_channel(
758 &self,
759 _app_id: AppId,
760 _channel_id: AppChannelId,
761 ) -> Result<()> {
762 Ok(())
763 }
764 async fn list_sessions(
765 &self,
766 _limit: Option<usize>,
767 _agent_id: Option<crate::typed_id::AgentId>,
768 ) -> Result<Vec<Session>> {
769 Ok(vec![self.session.clone()])
770 }
771 async fn create_session(
772 &self,
773 hid: HarnessId,
774 aid: Option<crate::typed_id::AgentId>,
775 title: Option<&str>,
776 locale: Option<&str>,
777 blueprint_id: Option<&str>,
778 blueprint_config: Option<&serde_json::Value>,
779 parent_session_id: Option<SessionId>,
780 ) -> Result<Session> {
781 if let Ok(mut recorder) = self.created_session_harness_ids.lock() {
782 recorder.push(hid);
783 }
784 let mut s = self.session.clone();
785 s.id = SessionId::new();
786 s.harness_id = hid;
787 s.agent_id = aid;
788 s.title = title.map(|t| t.to_string());
789 s.locale = locale.map(|value| value.to_string());
790 s.blueprint_id = blueprint_id.map(|b| b.to_string());
791 s.blueprint_config = blueprint_config.cloned();
792 s.parent_session_id = parent_session_id;
793 if let Ok(mut sessions) = self.extra_sessions.lock() {
794 sessions.insert(s.id, s.clone());
795 }
796 Ok(s)
797 }
798 async fn create_session_with_options(
799 &self,
800 request: PlatformCreateSessionRequest,
801 ) -> Result<Session> {
802 self.created_session_budget_roots
803 .lock()
804 .expect("budget root recorder")
805 .push(request.budget_root_session_id);
806 let mut session = self
807 .create_session(
808 request.harness_id,
809 request.agent_id,
810 request.title.as_deref(),
811 request.locale.as_deref(),
812 request.blueprint_id.as_deref(),
813 request.blueprint_config.as_ref(),
814 request.parent_session_id,
815 )
816 .await?;
817 session.goal = request.goal;
818 session.forked_from_session_id = request.forked_from_session_id;
819 if let Ok(mut sessions) = self.extra_sessions.lock() {
820 sessions.insert(session.id, session.clone());
821 }
822 Ok(session)
823 }
824 async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>> {
825 if id == self.session.id {
826 return Ok(Some(self.session.clone()));
827 }
828 if let Some(session) = self
829 .extra_sessions
830 .lock()
831 .ok()
832 .and_then(|sessions| sessions.get(&id).cloned())
833 {
834 return Ok(Some(session));
835 }
836 Ok(Some(self.session.clone()))
837 }
838 async fn add_agent_session_participant(
839 &self,
840 session_id: SessionId,
841 agent_id: AgentId,
842 ) -> Result<SessionParticipant> {
843 let participant = SessionParticipant {
844 id: crate::typed_id::SessionParticipantId::new(),
845 session_id,
846 kind: crate::session::SessionParticipantKind::Agent,
847 agent_id: Some(agent_id),
848 agent_version_id: self.agent.default_version_id,
849 principal_id: self.session.owner_principal_id,
850 role: crate::session::SessionParticipantRole::Member,
851 joined_at: chrono::Utc::now(),
852 left_at: None,
853 };
854 if let Ok(mut participants) = self.joined_participants.lock() {
855 participants.push(participant.clone());
856 }
857 Ok(participant)
858 }
859 async fn get_session_context_report(&self, id: SessionId) -> Result<SessionContextReport> {
860 Ok(SessionContextReport {
861 session_id: id.to_string(),
862 model: "llmsim".to_string(),
863 context_window_tokens: Some(128_000),
864 estimated_input_tokens: 42,
865 sections: vec![crate::ContextReportSection {
866 key: "conversation".to_string(),
867 label: "Conversation".to_string(),
868 tokens: 42,
869 items: 1,
870 }],
871 contributions: vec![],
872 cumulative_usage: None,
873 })
874 }
875 async fn delete_session(&self, _id: SessionId) -> Result<()> {
876 Ok(())
877 }
878 async fn send_message(&self, id: SessionId, content: &str) -> Result<()> {
879 self.sent_messages
880 .lock()
881 .unwrap()
882 .push((id, content.to_string()));
883 Ok(())
884 }
885 async fn get_messages(
886 &self,
887 _id: SessionId,
888 _limit: Option<usize>,
889 ) -> Result<Vec<PlatformMessage>> {
890 Ok(vec![
891 PlatformMessage {
892 role: "user".into(),
893 content: "Hello".into(),
894 created_at: chrono::Utc::now(),
895 },
896 PlatformMessage {
897 role: "agent".into(),
898 content: "Hi!".into(),
899 created_at: chrono::Utc::now(),
900 },
901 ])
902 }
903 async fn wait_for_idle(&self, _id: SessionId, _t: Option<u64>) -> Result<String> {
904 Ok(self.wait_for_idle_status.lock().unwrap().clone())
905 }
906 async fn list_capabilities(&self, search: Option<&str>) -> Result<Vec<CapabilityInfo>> {
907 let registry = crate::capabilities::CapabilityRegistry::with_builtins();
908 let mut caps: Vec<CapabilityInfo> = registry
909 .list()
910 .iter()
911 .map(|c| CapabilityInfo::from_core(c.as_ref()))
912 .collect();
913 if let Some(q) = search {
914 caps.retain(|c| c.matches_search(q));
915 }
916 caps.sort_by(|a, b| a.name.cmp(&b.name));
917 Ok(caps)
918 }
919 fn base_url(&self) -> &str {
920 "http://localhost:9300"
921 }
922 }
923}