Skip to main content

everruns_local/
platform_store.rs

1// Local PlatformStore.
2//
3// Scope (EVE-594): implement the subagent-critical core honestly, backed by a
4// caller-supplied `LocalSessionRunner` (the embedder wires this to its
5// `InProcessRuntime` so `create_session` / `send_message` / `wait_for_idle`
6// actually create and drive real local sessions, and `get_messages` /
7// `get_session_by_id` / `list_sessions` read real local state). The
8// platform-management-only operations (harness/agent/app CRUD, publish,
9// channels, capability listing, delete/context-report) return an explicit
10// unsupported error rather than half-implementing them — local embedded hosts
11// manage those entities in code, not through this tool surface.
12
13use async_trait::async_trait;
14use everruns_core::agent::Agent;
15use everruns_core::app::{App, AppChannel, ChannelType};
16use everruns_core::capability_dto::CapabilityInfo;
17use everruns_core::error::{AgentLoopError, Result};
18use everruns_core::harness::Harness;
19use everruns_core::platform_store::{PlatformMessage, PlatformStore};
20use everruns_core::session::Session;
21use everruns_core::typed_id::{
22    AgentId, AgentIdentityId, AppChannelId, AppId, HarnessId, SessionId,
23};
24use std::sync::Arc;
25
26/// Drives real local sessions for the platform store. An embedder implements
27/// this over its `InProcessRuntime` (or a thin wrapper) so subagent spawning
28/// can create child sessions, run turns, and read back session state. This is
29/// the seam that keeps the platform store honest without it owning the runtime.
30#[async_trait]
31pub trait LocalSessionRunner: Send + Sync {
32    /// Create a child/local session and persist it in the session catalog.
33    async fn create_session(
34        &self,
35        harness_id: HarnessId,
36        agent_id: Option<AgentId>,
37        title: Option<&str>,
38        locale: Option<&str>,
39        parent_session_id: Option<SessionId>,
40    ) -> Result<Session>;
41
42    /// Deliver a user message and run a turn to completion.
43    async fn send_message(&self, session_id: SessionId, content: &str) -> Result<()>;
44
45    /// List sessions known to the runner. Optionally filtered by agent.
46    async fn list_sessions(
47        &self,
48        limit: Option<usize>,
49        agent_id: Option<AgentId>,
50    ) -> Result<Vec<Session>>;
51
52    /// Look up a single session by id.
53    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>>;
54
55    /// Read recent messages (most recent first) as platform messages.
56    async fn get_messages(
57        &self,
58        session_id: SessionId,
59        limit: Option<usize>,
60    ) -> Result<Vec<PlatformMessage>>;
61
62    /// Current session status string, or `None` if the session is unknown.
63    async fn get_session_status(&self, session_id: SessionId) -> Result<Option<String>>;
64}
65
66fn unsupported(op: &str) -> AgentLoopError {
67    AgentLoopError::tool(format!(
68        "operation '{op}' is not supported by the local platform store; \
69         manage this entity in embedder code"
70    ))
71}
72
73/// Local platform store for one (org, session) scope, backed by a
74/// [`LocalSessionRunner`].
75#[derive(Clone)]
76pub struct LocalPlatformStore {
77    runner: Arc<dyn LocalSessionRunner>,
78    base_url: String,
79}
80
81impl LocalPlatformStore {
82    pub fn new(runner: Arc<dyn LocalSessionRunner>, base_url: impl Into<String>) -> Self {
83        Self {
84            runner,
85            base_url: base_url.into(),
86        }
87    }
88}
89
90#[async_trait]
91impl PlatformStore for LocalPlatformStore {
92    // ---- Honestly implemented: subagent-critical core -----------------------
93
94    async fn create_session(
95        &self,
96        harness_id: HarnessId,
97        agent_id: Option<AgentId>,
98        title: Option<&str>,
99        locale: Option<&str>,
100        blueprint_id: Option<&str>,
101        _blueprint_config: Option<&serde_json::Value>,
102        parent_session_id: Option<SessionId>,
103    ) -> Result<Session> {
104        if blueprint_id.is_some() {
105            return Err(unsupported("create_session(blueprint)"));
106        }
107        self.runner
108            .create_session(harness_id, agent_id, title, locale, parent_session_id)
109            .await
110    }
111
112    async fn get_session_by_id(&self, id: SessionId) -> Result<Option<Session>> {
113        self.runner.get_session(id).await
114    }
115
116    async fn list_sessions(
117        &self,
118        limit: Option<usize>,
119        agent_id: Option<AgentId>,
120    ) -> Result<Vec<Session>> {
121        self.runner.list_sessions(limit, agent_id).await
122    }
123
124    async fn send_message(&self, session_id: SessionId, content: &str) -> Result<()> {
125        self.runner.send_message(session_id, content).await
126    }
127
128    async fn get_messages(
129        &self,
130        session_id: SessionId,
131        limit: Option<usize>,
132    ) -> Result<Vec<PlatformMessage>> {
133        self.runner.get_messages(session_id, limit).await
134    }
135
136    async fn wait_for_idle(
137        &self,
138        session_id: SessionId,
139        _timeout_secs: Option<u64>,
140    ) -> Result<String> {
141        // `send_message` runs the turn synchronously to completion in the local
142        // host, so by the time a caller polls, the session is already idle.
143        self.runner
144            .get_session_status(session_id)
145            .await?
146            .ok_or_else(|| AgentLoopError::session_not_found(session_id))
147    }
148
149    fn base_url(&self) -> &str {
150        &self.base_url
151    }
152
153    // ---- Platform-management-only: explicit unsupported ---------------------
154
155    async fn list_harnesses(&self) -> Result<Vec<Harness>> {
156        Err(unsupported("list_harnesses"))
157    }
158    async fn get_harness(&self, _id: HarnessId) -> Result<Option<Harness>> {
159        Err(unsupported("get_harness"))
160    }
161    async fn create_harness(
162        &self,
163        _name: &str,
164        _display_name: Option<&str>,
165        _description: Option<&str>,
166        _system_prompt: Option<&str>,
167        _parent_harness_id: Option<HarnessId>,
168        _capabilities: &[String],
169    ) -> Result<Harness> {
170        Err(unsupported("create_harness"))
171    }
172    async fn update_harness(
173        &self,
174        _id: HarnessId,
175        _name: Option<&str>,
176        _display_name: Option<&str>,
177        _description: Option<&str>,
178        _system_prompt: Option<&str>,
179        _parent_harness_id: Option<Option<HarnessId>>,
180    ) -> Result<Harness> {
181        Err(unsupported("update_harness"))
182    }
183    async fn delete_harness(&self, _id: HarnessId) -> Result<()> {
184        Err(unsupported("delete_harness"))
185    }
186    async fn copy_harness(&self, _id: HarnessId, _new_name: Option<&str>) -> Result<Harness> {
187        Err(unsupported("copy_harness"))
188    }
189    async fn list_agents(&self) -> Result<Vec<Agent>> {
190        Err(unsupported("list_agents"))
191    }
192    async fn get_agent_by_id(&self, _id: AgentId) -> Result<Option<Agent>> {
193        Err(unsupported("get_agent_by_id"))
194    }
195    async fn create_agent(
196        &self,
197        _name: &str,
198        _display_name: Option<&str>,
199        _description: Option<&str>,
200        _system_prompt: &str,
201        _capabilities: &[String],
202    ) -> Result<Agent> {
203        Err(unsupported("create_agent"))
204    }
205    async fn update_agent(
206        &self,
207        _id: AgentId,
208        _name: Option<&str>,
209        _display_name: Option<&str>,
210        _description: Option<&str>,
211        _system_prompt: Option<&str>,
212    ) -> Result<Agent> {
213        Err(unsupported("update_agent"))
214    }
215    async fn delete_agent(&self, _id: AgentId) -> Result<()> {
216        Err(unsupported("delete_agent"))
217    }
218    async fn list_apps(&self, _search: Option<&str>, _include_archived: bool) -> Result<Vec<App>> {
219        Err(unsupported("list_apps"))
220    }
221    async fn get_app(&self, _id: AppId) -> Result<Option<App>> {
222        Err(unsupported("get_app"))
223    }
224    async fn create_app(
225        &self,
226        _name: &str,
227        _description: Option<&str>,
228        _harness_id: HarnessId,
229        _agent_id: Option<AgentId>,
230        _agent_identity_id: Option<AgentIdentityId>,
231        _channel_type: Option<ChannelType>,
232        _channel_config: Option<&serde_json::Value>,
233    ) -> Result<App> {
234        Err(unsupported("create_app"))
235    }
236    async fn update_app(
237        &self,
238        _id: AppId,
239        _name: Option<&str>,
240        _description: Option<&str>,
241        _harness_id: Option<HarnessId>,
242        _agent_id: Option<AgentId>,
243        _agent_identity_id: Option<Option<AgentIdentityId>>,
244    ) -> Result<App> {
245        Err(unsupported("update_app"))
246    }
247    async fn delete_app(&self, _id: AppId) -> Result<()> {
248        Err(unsupported("delete_app"))
249    }
250    async fn destroy_app(&self, _id: AppId) -> Result<()> {
251        Err(unsupported("destroy_app"))
252    }
253    async fn publish_app(&self, _id: AppId) -> Result<App> {
254        Err(unsupported("publish_app"))
255    }
256    async fn unpublish_app(&self, _id: AppId) -> Result<App> {
257        Err(unsupported("unpublish_app"))
258    }
259    async fn add_app_channel(
260        &self,
261        _app_id: AppId,
262        _channel_type: ChannelType,
263        _channel_config: Option<&serde_json::Value>,
264        _enabled: Option<bool>,
265    ) -> Result<AppChannel> {
266        Err(unsupported("add_app_channel"))
267    }
268    async fn update_app_channel(
269        &self,
270        _app_id: AppId,
271        _channel_id: AppChannelId,
272        _channel_type: Option<ChannelType>,
273        _channel_config: Option<&serde_json::Value>,
274        _enabled: Option<bool>,
275    ) -> Result<AppChannel> {
276        Err(unsupported("update_app_channel"))
277    }
278    async fn delete_app_channel(&self, _app_id: AppId, _channel_id: AppChannelId) -> Result<()> {
279        Err(unsupported("delete_app_channel"))
280    }
281    async fn get_session_context_report(
282        &self,
283        _id: SessionId,
284    ) -> Result<everruns_core::SessionContextReport> {
285        Err(unsupported("get_session_context_report"))
286    }
287    async fn delete_session(&self, _id: SessionId) -> Result<()> {
288        Err(unsupported("delete_session"))
289    }
290    async fn list_capabilities(&self, _search: Option<&str>) -> Result<Vec<CapabilityInfo>> {
291        Err(unsupported("list_capabilities"))
292    }
293}