Skip to main content

lash_core/plugin/
runtime_host.rs

1use serde::{Deserialize, Serialize};
2
3use super::*;
4
5#[async_trait::async_trait]
6pub trait SessionSnapshotHost: Send + Sync {
7    async fn snapshot_current(&self) -> Result<SessionSnapshot, PluginError>;
8    async fn snapshot_session(&self, session_id: &str) -> Result<SessionSnapshot, PluginError>;
9}
10
11#[async_trait::async_trait]
12pub trait ToolCatalogHost: Send + Sync {
13    async fn tool_catalog(&self, session_id: &str) -> Result<Vec<serde_json::Value>, PluginError>;
14}
15
16#[async_trait::async_trait]
17pub trait ToolStateHost: Send + Sync {
18    async fn tool_state(&self, _session_id: &str) -> Result<crate::ToolState, PluginError> {
19        Err(PluginError::Session(
20            "tool state is unavailable in this session".to_string(),
21        ))
22    }
23    async fn apply_tool_state(
24        &self,
25        _session_id: &str,
26        _snapshot: crate::ToolState,
27    ) -> Result<u64, PluginError> {
28        Err(PluginError::Session(
29            "tool state mutation is unavailable in this session".to_string(),
30        ))
31    }
32    async fn set_tools_availability(
33        &self,
34        session_id: &str,
35        tool_names: &[String],
36        availability: Option<crate::ToolAvailability>,
37    ) -> Result<u64, PluginError> {
38        let mut snapshot = self.tool_state(session_id).await?;
39        for name in tool_names {
40            snapshot
41                .set_availability(name, availability)
42                .map_err(|err| PluginError::Session(err.to_string()))?;
43        }
44        self.apply_tool_state(session_id, snapshot).await
45    }
46    async fn set_tool_availability(
47        &self,
48        session_id: &str,
49        tool_name: &str,
50        availability: Option<ToolAvailability>,
51    ) -> Result<u64, PluginError> {
52        let mut snapshot = self.tool_state(session_id).await?;
53        snapshot
54            .set_availability(tool_name, availability)
55            .map_err(|err| PluginError::Session(err.to_string()))?;
56        self.apply_tool_state(session_id, snapshot).await
57    }
58}
59
60#[async_trait::async_trait]
61pub trait SessionLifecycleHost: Send + Sync {
62    async fn create_session(
63        &self,
64        request: SessionCreateRequest,
65    ) -> Result<SessionHandle, PluginError>;
66    /// Pop the seed message that was queued for `session_id` via
67    /// `SessionCreateRequest::first_turn_input`. Returns `None` if no
68    /// seed was queued, or after a previous caller has already taken
69    /// it. Hosts call this when starting the inaugural turn on a
70    /// freshly created session.
71    async fn take_first_turn_input(
72        &self,
73        _session_id: &str,
74    ) -> Result<Option<PluginMessage>, PluginError> {
75        Ok(None)
76    }
77    async fn close_session(&self, session_id: &str) -> Result<(), PluginError>;
78}
79
80#[async_trait::async_trait]
81pub trait TurnHost: Send + Sync {
82    async fn start_turn_stream(
83        &self,
84        session_id: &str,
85        input: TurnInput,
86    ) -> Result<SessionTurnHandle, PluginError>;
87    async fn await_turn(&self, turn_id: &str) -> Result<AssembledTurn, PluginError>;
88    async fn cancel_turn(&self, turn_id: &str) -> Result<(), PluginError>;
89    async fn start_turn(
90        &self,
91        session_id: &str,
92        input: TurnInput,
93    ) -> Result<AssembledTurn, PluginError> {
94        let handle = self.start_turn_stream(session_id, input).await?;
95        drop(handle.events);
96        self.await_turn(&handle.turn_id).await
97    }
98}
99
100#[async_trait::async_trait]
101pub trait TaskHost: Send + Sync {
102    /// Push a user-visible message into the target session's turn-input
103    /// injection bridge so it surfaces at the next iteration boundary of
104    /// the current turn (or at the start of the next turn if the target
105    /// is idle). Used by monitor and other wake-up flows where a note
106    /// should land at the next available step rather than waiting for a
107    /// brand-new task.
108    async fn inject_turn_input(
109        &self,
110        _session_id: &str,
111        _input: crate::InjectedTurnInput,
112    ) -> Result<(), PluginError> {
113        Err(PluginError::Session(
114            "turn input injection is unavailable in this session".to_string(),
115        ))
116    }
117    async fn spawn_hidden_task(
118        &self,
119        _session_id: &str,
120        _label: &str,
121        _task: PluginSessionTask,
122    ) -> Result<(), PluginError> {
123        Err(PluginError::Session(
124            "background tasks are unavailable in this session".to_string(),
125        ))
126    }
127    async fn await_hidden_tasks(&self, _session_id: &str) -> Result<(), PluginError> {
128        Ok(())
129    }
130    async fn spawn_managed_task(
131        &self,
132        _session_id: &str,
133        _spec: crate::BackgroundTaskRegistration,
134        _task: PluginSessionTask,
135    ) -> Result<(), PluginError> {
136        Err(PluginError::Session(
137            "managed background tasks are unavailable in this session".to_string(),
138        ))
139    }
140    async fn cancel_managed_task(
141        &self,
142        _session_id: &str,
143        _task_id: &str,
144    ) -> Result<(), PluginError> {
145        Err(PluginError::Session(
146            "managed background tasks are unavailable in this session".to_string(),
147        ))
148    }
149    async fn register_background_task(
150        &self,
151        _session_id: &str,
152        _spec: crate::BackgroundTaskRegistration,
153        _cancel: Option<crate::LocalBackgroundTaskCancel>,
154    ) -> Result<(), PluginError> {
155        Err(PluginError::Session(
156            "background task registry is unavailable in this session".to_string(),
157        ))
158    }
159    async fn unregister_background_task(&self, _session_id: &str, _task_id: &str) {}
160    async fn complete_background_task(
161        &self,
162        _session_id: &str,
163        _task_id: &str,
164        _state: crate::BackgroundTaskState,
165    ) {
166    }
167    /// Transition a still-live background task between the non-terminal
168    /// `Running` and `Idle` run states. Used by subagent hosts to
169    /// reflect whether the subagent is actively working or waiting for
170    /// a follow-up task.
171    async fn transition_background_task_live_state(
172        &self,
173        _session_id: &str,
174        _task_id: &str,
175        _state: crate::BackgroundTaskState,
176    ) {
177    }
178    async fn list_background_tasks(
179        &self,
180        _session_id: &str,
181    ) -> Result<Vec<crate::BackgroundTaskRecord>, PluginError> {
182        Err(PluginError::Session(
183            "background task registry is unavailable in this session".to_string(),
184        ))
185    }
186    /// Dispatch a kind-aware cancel for any registered background task.
187    /// Monitor tasks terminate their process trees; subagent tasks close
188    /// the agent subtree; other managed tasks are aborted.
189    async fn cancel_background_task(
190        &self,
191        _session_id: &str,
192        _task_id: &str,
193    ) -> Result<crate::BackgroundTaskRecord, PluginError> {
194        Err(PluginError::Session(
195            "background task registry is unavailable in this session".to_string(),
196        ))
197    }
198    async fn cancel_all_background_tasks(
199        &self,
200        session_id: &str,
201    ) -> Result<Vec<crate::BackgroundTaskRecord>, PluginError> {
202        let tasks = self.list_background_tasks(session_id).await?;
203        let mut cancelled = Vec::new();
204        for task in tasks {
205            if task.state.is_terminal() {
206                continue;
207            }
208            cancelled.push(self.cancel_background_task(session_id, &task.id).await?);
209        }
210        Ok(cancelled)
211    }
212    async fn validate_async_handles_visible(
213        &self,
214        _session_id: &str,
215        _handle_ids: &[String],
216    ) -> Result<(), PluginError> {
217        Ok(())
218    }
219    async fn transfer_async_handles(
220        &self,
221        _from_session_id: &str,
222        _to_session_id: &str,
223        _handle_ids: &[String],
224    ) -> Result<(), PluginError> {
225        Ok(())
226    }
227    async fn cancel_unreferenced_async_handles(
228        &self,
229        _session_id: &str,
230        _keep_handle_ids: &[String],
231    ) -> Result<Vec<crate::BackgroundTaskRecord>, PluginError> {
232        Ok(Vec::new())
233    }
234}
235
236#[async_trait::async_trait]
237pub trait MonitorHost: Send + Sync {
238    async fn monitor_snapshot(&self, _session_id: &str) -> Result<MonitorSnapshot, PluginError> {
239        Err(PluginError::Session(
240            "monitors are unavailable in this session".to_string(),
241        ))
242    }
243    async fn take_monitor_updates(
244        &self,
245        _session_id: &str,
246    ) -> Result<MonitorUpdateBatch, PluginError> {
247        Err(PluginError::Session(
248            "monitors are unavailable in this session".to_string(),
249        ))
250    }
251    async fn start_monitor(
252        &self,
253        _session_id: &str,
254        _spec: MonitorSpec,
255    ) -> Result<MonitorSnapshot, PluginError> {
256        Err(PluginError::Session(
257            "monitors are unavailable in this session".to_string(),
258        ))
259    }
260    async fn stop_monitor(
261        &self,
262        _session_id: &str,
263        _monitor_id: &str,
264    ) -> Result<MonitorSnapshot, PluginError> {
265        Err(PluginError::Session(
266            "monitors are unavailable in this session".to_string(),
267        ))
268    }
269}
270
271#[async_trait::async_trait]
272pub trait SessionGraphHost: Send + Sync {
273    async fn append_session_nodes(
274        &self,
275        _session_id: &str,
276        _request: AppendSessionNodesRequest,
277    ) -> Result<AppendSessionNodesResult, PluginError> {
278        Err(PluginError::Session(
279            "session graph mutation is unavailable in this session".to_string(),
280        ))
281    }
282}
283
284#[async_trait::async_trait]
285pub trait DirectCompletionHost: Send + Sync {
286    /// Make a single LLM call without creating a full session. Used by
287    /// plugins for structured extraction, summarization, observation,
288    /// and other one-shot calls that don't need tools, turn loops, or
289    /// session state. The `usage_source` label tags the resulting
290    /// token cost in the parent session's ledger.
291    async fn direct_completion(
292        &self,
293        _request: crate::DirectRequest,
294        _usage_source: &str,
295    ) -> Result<DirectCompletion, PluginError> {
296        Err(PluginError::Session(
297            "direct completions are unavailable in this session".to_string(),
298        ))
299    }
300
301    async fn direct_llm_completion(
302        &self,
303        _request: crate::LlmRequest,
304        _usage_source: &str,
305    ) -> Result<DirectLlmCompletion, PluginError> {
306        Err(PluginError::Session(
307            "direct LLM completions are unavailable in this session".to_string(),
308        ))
309    }
310}
311
312#[async_trait::async_trait]
313pub trait TraceHost: Send + Sync {
314    async fn emit_trace_event(
315        &self,
316        _context: lash_trace::TraceContext,
317        _event: lash_trace::TraceEvent,
318    ) -> Result<(), PluginError> {
319        Ok(())
320    }
321}
322
323pub trait PromptHookHost:
324    SessionSnapshotHost + ToolCatalogHost + TaskHost + DirectCompletionHost
325{
326}
327impl<T> PromptHookHost for T where
328    T: SessionSnapshotHost + ToolCatalogHost + TaskHost + DirectCompletionHost + ?Sized
329{
330}
331
332pub trait TurnHookHost:
333    SessionSnapshotHost + ToolStateHost + SessionLifecycleHost + TraceHost
334{
335}
336impl<T> TurnHookHost for T where
337    T: SessionSnapshotHost + ToolStateHost + SessionLifecycleHost + TraceHost + ?Sized
338{
339}
340
341pub trait ToolHookHost:
342    SessionSnapshotHost
343    + ToolCatalogHost
344    + ToolStateHost
345    + SessionLifecycleHost
346    + TurnHost
347    + TaskHost
348    + MonitorHost
349    + SessionGraphHost
350    + DirectCompletionHost
351    + TraceHost
352    + TurnResultHookHost
353    + CheckpointHookHost
354{
355}
356impl<T> ToolHookHost for T where
357    T: SessionSnapshotHost
358        + ToolCatalogHost
359        + ToolStateHost
360        + SessionLifecycleHost
361        + TurnHost
362        + TaskHost
363        + MonitorHost
364        + SessionGraphHost
365        + DirectCompletionHost
366        + TraceHost
367        + ?Sized
368{
369}
370
371pub trait TurnResultHookHost: SessionLifecycleHost + TraceHost {}
372impl<T> TurnResultHookHost for T where T: SessionLifecycleHost + TraceHost + ?Sized {}
373
374pub trait CheckpointHookHost: SessionLifecycleHost + TraceHost {}
375impl<T> CheckpointHookHost for T where T: SessionLifecycleHost + TraceHost + ?Sized {}
376
377pub trait HistoryHost:
378    SessionSnapshotHost
379    + SessionLifecycleHost
380    + TurnHost
381    + TaskHost
382    + SessionGraphHost
383    + DirectCompletionHost
384{
385}
386impl<T> HistoryHost for T where
387    T: SessionSnapshotHost
388        + SessionLifecycleHost
389        + TurnHost
390        + TaskHost
391        + SessionGraphHost
392        + DirectCompletionHost
393        + ?Sized
394{
395}
396
397pub trait PluginActionHost:
398    SessionSnapshotHost
399    + ToolCatalogHost
400    + ToolStateHost
401    + SessionLifecycleHost
402    + TurnHost
403    + TaskHost
404    + MonitorHost
405    + SessionGraphHost
406    + DirectCompletionHost
407    + TraceHost
408{
409}
410impl<T> PluginActionHost for T where
411    T: SessionSnapshotHost
412        + ToolCatalogHost
413        + ToolStateHost
414        + SessionLifecycleHost
415        + TurnHost
416        + TaskHost
417        + MonitorHost
418        + SessionGraphHost
419        + DirectCompletionHost
420        + TraceHost
421        + ?Sized
422{
423}
424
425pub trait RuntimeSessionHost:
426    PluginActionHost + ToolHookHost + HistoryHost + TurnHookHost + PromptHookHost
427{
428}
429impl<T> RuntimeSessionHost for T where
430    T: PluginActionHost + ToolHookHost + HistoryHost + TurnHookHost + PromptHookHost + ?Sized
431{
432}
433
434/// Result of a single-shot LLM call via
435/// [`DirectCompletionHost::direct_completion`].
436#[derive(Clone, Debug)]
437pub struct DirectCompletion {
438    pub text: String,
439    pub usage: crate::TokenUsage,
440}
441
442#[derive(Clone, Debug)]
443pub struct DirectLlmCompletion {
444    pub response: crate::LlmResponse,
445    pub usage: crate::TokenUsage,
446}
447
448#[derive(Clone, Debug, Serialize, Deserialize)]
449pub struct AppendSessionNodesRequest {
450    pub nodes: Vec<SessionAppendNode>,
451    #[serde(default)]
452    pub requires_ancestor_node_id: Option<String>,
453}
454
455#[derive(Clone, Debug, Serialize, Deserialize)]
456#[serde(tag = "status", rename_all = "snake_case")]
457pub enum AppendSessionNodesResult {
458    Appended {
459        node_ids: Vec<String>,
460        leaf_node_id: String,
461    },
462    StaleBranch {
463        current_leaf_node_id: Option<String>,
464    },
465}