1use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use harn_session_store::{
12 AppendEvent, CreateSession, Embedder, EventId, ListFilter, ReadRange, SearchMode, SearchQuery,
13 SessionStore, SessionType, SqliteSessionStore, StoreError, StoreHooks, UpdateSession,
14 MAX_READ_BATCH,
15};
16use harn_vm::process_sandbox::FsAccess;
17use harn_vm::VmValue;
18use serde::de::DeserializeOwned;
19use serde::Deserialize;
20
21use crate::error::HostlibError;
22use crate::json::vm_value_to_json;
23use crate::registry::{BuiltinRegistry, HostlibCapability};
24use crate::tools::permissions::enforce_path_scope;
25
26pub const OPEN_BUILTIN: &str = "hostlib_session_open";
28pub const UPDATE_BUILTIN: &str = "hostlib_session_update";
30pub const APPEND_BUILTIN: &str = "hostlib_session_append";
32pub const CLOSE_BUILTIN: &str = "hostlib_session_close";
34pub const GET_BUILTIN: &str = "hostlib_session_get";
36pub const LIST_BUILTIN: &str = "hostlib_session_list";
38pub const FORK_BUILTIN: &str = "hostlib_session_fork";
40pub const SEARCH_FTS_BUILTIN: &str = "hostlib_search_fts";
42pub const SEARCH_SEMANTIC_BUILTIN: &str = "hostlib_search_semantic";
44pub const SEARCH_HYBRID_BUILTIN: &str = "hostlib_search_hybrid";
46
47#[derive(Clone)]
49pub struct SessionCapability {
50 embedder: Arc<dyn Embedder>,
51}
52
53impl SessionCapability {
54 pub fn with_embedder(embedder: Arc<dyn Embedder>) -> Self {
56 Self { embedder }
57 }
58
59 fn store(
60 &self,
61 builtin: &'static str,
62 root: &Path,
63 ) -> Result<SqliteSessionStore, HostlibError> {
64 enforce_path_scope(builtin, root, FsAccess::Write)?;
65 let hooks = StoreHooks {
66 redaction: Some(Arc::new(harn_vm::redact::current_policy())),
67 embedder: self.embedder.clone(),
68 ..StoreHooks::default()
69 };
70 SqliteSessionStore::open_with_hooks(database_path(root), hooks)
71 .map_err(|error| backend_error(builtin, error))
72 }
73
74 async fn open(&self, args: Vec<VmValue>) -> Result<VmValue, HostlibError> {
75 let mut request: OpenRequest = request(OPEN_BUILTIN, &args)?;
76 let root = normalized_root(OPEN_BUILTIN, &request.root)?;
77 if request.session.project_scope.is_none() {
78 request.session.project_scope = Some(project_scope(&root));
79 }
80 if request.session.cwd.is_none() {
81 request.session.cwd = Some(project_scope(&root));
82 }
83 if request.session.session_type.is_none() {
84 request.session.session_type = Some(SessionType::User);
85 }
86 let store = self.store(OPEN_BUILTIN, &root)?;
87 let meta = match request.session.id.as_deref() {
88 Some(id) => match store.describe(id).await {
89 Ok(meta) => meta,
90 Err(StoreError::NotFound(_)) => store
91 .create(request.session)
92 .await
93 .map_err(|error| backend_error(OPEN_BUILTIN, error))?,
94 Err(error) => return Err(backend_error(OPEN_BUILTIN, error)),
95 },
96 None => store
97 .create(request.session)
98 .await
99 .map_err(|error| backend_error(OPEN_BUILTIN, error))?,
100 };
101 response(OPEN_BUILTIN, meta)
102 }
103
104 async fn append(&self, args: Vec<VmValue>) -> Result<VmValue, HostlibError> {
105 let request: AppendRequest = request(APPEND_BUILTIN, &args)?;
106 let root = normalized_root(APPEND_BUILTIN, &request.root)?;
107 let stored = self
108 .store(APPEND_BUILTIN, &root)?
109 .append(&request.session_id, request.event)
110 .await
111 .map_err(|error| backend_error(APPEND_BUILTIN, error))?;
112 response(APPEND_BUILTIN, stored)
113 }
114
115 async fn update(&self, args: Vec<VmValue>) -> Result<VmValue, HostlibError> {
116 let request: UpdateRequest = request(UPDATE_BUILTIN, &args)?;
117 let root = normalized_root(UPDATE_BUILTIN, &request.root)?;
118 let meta = self
119 .store(UPDATE_BUILTIN, &root)?
120 .update(&request.session_id, request.update)
121 .await
122 .map_err(|error| backend_error(UPDATE_BUILTIN, error))?;
123 response(UPDATE_BUILTIN, meta)
124 }
125
126 async fn close(&self, args: Vec<VmValue>) -> Result<VmValue, HostlibError> {
127 let request: SessionRequest = request(CLOSE_BUILTIN, &args)?;
128 let root = normalized_root(CLOSE_BUILTIN, &request.root)?;
129 let meta = self
130 .store(CLOSE_BUILTIN, &root)?
131 .close(&request.session_id)
132 .await
133 .map_err(|error| backend_error(CLOSE_BUILTIN, error))?;
134 response(CLOSE_BUILTIN, meta)
135 }
136
137 async fn get(&self, args: Vec<VmValue>) -> Result<VmValue, HostlibError> {
138 let request: SessionRequest = request(GET_BUILTIN, &args)?;
139 let root = normalized_root(GET_BUILTIN, &request.root)?;
140 let store = self.store(GET_BUILTIN, &root)?;
141 let session = store
142 .describe(&request.session_id)
143 .await
144 .map_err(|error| backend_error(GET_BUILTIN, error))?;
145 let mut events = Vec::new();
146 let mut cursor = None;
147 loop {
148 let page = store
149 .read(
150 &request.session_id,
151 ReadRange {
152 from_event_id: cursor,
153 limit: Some(MAX_READ_BATCH),
154 ..ReadRange::default()
155 },
156 )
157 .await
158 .map_err(|error| backend_error(GET_BUILTIN, error))?;
159 cursor = page.next_cursor;
160 events.extend(page.events);
161 if cursor.is_none() {
162 break;
163 }
164 }
165 response(
166 GET_BUILTIN,
167 serde_json::json!({"session": session, "events": events}),
168 )
169 }
170
171 async fn list(&self, args: Vec<VmValue>) -> Result<VmValue, HostlibError> {
172 let mut request: ListRequest = request(LIST_BUILTIN, &args)?;
173 let root = normalized_root(LIST_BUILTIN, &request.root)?;
174 if request.filter.project_scope.is_none() && request.filter.tenant_id.is_none() {
175 request.filter.project_scope = Some(project_scope(&root));
176 }
177 let sessions = self
178 .store(LIST_BUILTIN, &root)?
179 .list(request.filter)
180 .await
181 .map_err(|error| backend_error(LIST_BUILTIN, error))?;
182 response(LIST_BUILTIN, serde_json::json!({"sessions": sessions}))
183 }
184
185 async fn fork(&self, args: Vec<VmValue>) -> Result<VmValue, HostlibError> {
186 let request: ForkRequest = request(FORK_BUILTIN, &args)?;
187 let root = normalized_root(FORK_BUILTIN, &request.root)?;
188 let result = self
189 .store(FORK_BUILTIN, &root)?
190 .fork(
191 &request.session_id,
192 request.at_event_id,
193 request.child_session_id,
194 )
195 .await
196 .map_err(|error| backend_error(FORK_BUILTIN, error))?;
197 response(FORK_BUILTIN, result)
198 }
199
200 async fn search(
201 &self,
202 builtin: &'static str,
203 mode: SearchMode,
204 args: Vec<VmValue>,
205 ) -> Result<VmValue, HostlibError> {
206 let mut request: SearchRequest = request(builtin, &args)?;
207 let root = normalized_root(builtin, &request.root)?;
208 if request.query.filter.project_scope.is_none()
209 && request.query.filter.tenant_id.is_none()
210 && request.query.filter.session_id.is_none()
211 {
212 request.query.filter.project_scope = Some(project_scope(&root));
213 }
214 request.query.mode = mode;
215 let result = self
216 .store(builtin, &root)?
217 .search(request.query)
218 .await
219 .map_err(|error| backend_error(builtin, error))?;
220 response(builtin, result)
221 }
222}
223
224impl Default for SessionCapability {
225 fn default() -> Self {
226 Self::with_embedder(Arc::new(harn_session_store::LexicalEmbedder::default()))
227 }
228}
229
230impl HostlibCapability for SessionCapability {
231 fn module_name(&self) -> &'static str {
232 "session"
233 }
234
235 fn register_builtins(&self, registry: &mut BuiltinRegistry) {
236 let capability = self.clone();
237 registry.register_async_fn("session", OPEN_BUILTIN, "open", move |args| {
238 let capability = capability.clone();
239 async move { capability.open(args).await }
240 });
241 let capability = self.clone();
242 registry.register_async_fn("session", UPDATE_BUILTIN, "update", move |args| {
243 let capability = capability.clone();
244 async move { capability.update(args).await }
245 });
246 let capability = self.clone();
247 registry.register_async_fn("session", APPEND_BUILTIN, "append", move |args| {
248 let capability = capability.clone();
249 async move { capability.append(args).await }
250 });
251 let capability = self.clone();
252 registry.register_async_fn("session", CLOSE_BUILTIN, "close", move |args| {
253 let capability = capability.clone();
254 async move { capability.close(args).await }
255 });
256 let capability = self.clone();
257 registry.register_async_fn("session", GET_BUILTIN, "get", move |args| {
258 let capability = capability.clone();
259 async move { capability.get(args).await }
260 });
261 let capability = self.clone();
262 registry.register_async_fn("session", LIST_BUILTIN, "list", move |args| {
263 let capability = capability.clone();
264 async move { capability.list(args).await }
265 });
266 let capability = self.clone();
267 registry.register_async_fn("session", FORK_BUILTIN, "fork", move |args| {
268 let capability = capability.clone();
269 async move { capability.fork(args).await }
270 });
271 for (builtin, method, mode) in [
272 (SEARCH_FTS_BUILTIN, "search_fts", SearchMode::Fts),
273 (
274 SEARCH_SEMANTIC_BUILTIN,
275 "search_semantic",
276 SearchMode::Semantic,
277 ),
278 (SEARCH_HYBRID_BUILTIN, "search_hybrid", SearchMode::Hybrid),
279 ] {
280 let capability = self.clone();
281 registry.register_async_fn("session", builtin, method, move |args| {
282 let capability = capability.clone();
283 async move { capability.search(builtin, mode, args).await }
284 });
285 }
286 }
287}
288
289#[derive(Deserialize)]
290struct OpenRequest {
291 root: String,
292 #[serde(flatten)]
293 session: CreateSession,
294}
295
296#[derive(Deserialize)]
297struct SessionRequest {
298 root: String,
299 session_id: String,
300}
301
302#[derive(Deserialize)]
303struct UpdateRequest {
304 root: String,
305 session_id: String,
306 #[serde(flatten)]
307 update: UpdateSession,
308}
309
310#[derive(Deserialize)]
311struct AppendRequest {
312 root: String,
313 session_id: String,
314 event: AppendEvent,
315}
316
317#[derive(Deserialize)]
318struct ListRequest {
319 root: String,
320 #[serde(default)]
321 filter: ListFilter,
322}
323
324#[derive(Deserialize)]
325struct ForkRequest {
326 root: String,
327 session_id: String,
328 at_event_id: EventId,
329 #[serde(default)]
330 child_session_id: Option<String>,
331}
332
333#[derive(Deserialize)]
334struct SearchRequest {
335 root: String,
336 #[serde(flatten)]
337 query: SearchQuery,
338}
339
340fn request<T: DeserializeOwned>(
341 builtin: &'static str,
342 args: &[VmValue],
343) -> Result<T, HostlibError> {
344 let value = args.first().ok_or(HostlibError::MissingParameter {
345 builtin,
346 param: "params",
347 })?;
348 serde_json::from_value(vm_value_to_json(value)).map_err(|error| {
349 HostlibError::InvalidParameter {
350 builtin,
351 param: "params",
352 message: error.to_string(),
353 }
354 })
355}
356
357fn response(builtin: &'static str, value: impl serde::Serialize) -> Result<VmValue, HostlibError> {
358 let value = serde_json::to_value(value).map_err(|error| HostlibError::Backend {
359 builtin,
360 message: format!("failed to encode response: {error}"),
361 })?;
362 Ok(harn_vm::json_to_vm_value(&value))
363}
364
365fn normalized_root(builtin: &'static str, root: &str) -> Result<PathBuf, HostlibError> {
366 let root = root.trim();
367 if root.is_empty() {
368 return Err(HostlibError::InvalidParameter {
369 builtin,
370 param: "root",
371 message: "must be non-empty".to_string(),
372 });
373 }
374 let path = crate::tools::args::resolve_host_path(root);
375 Ok(path.canonicalize().unwrap_or(path))
376}
377
378fn project_scope(root: &Path) -> String {
379 crate::tools::args::to_agent_path(root)
380}
381
382fn database_path(root: &Path) -> PathBuf {
383 root.join(".harn").join("session-store.sqlite")
384}
385
386fn backend_error(builtin: &'static str, error: StoreError) -> HostlibError {
387 HostlibError::Backend {
388 builtin,
389 message: error.to_string(),
390 }
391}